RoyLiu пре 4 година
родитељ
комит
7660da2ad9
100 измењених фајлова са 3233 додато и 1091 уклоњено
  1. 1 12
      .babelrc
  2. 6 0
      actions/main.ts
  3. 8 5
      actions/pdf.ts
  4. 0 95
      components/Annotation/index.tsx
  5. 48 0
      components/AnnotationItem/data.ts
  6. 12 44
      components/AnnotationItem/index.tsx
  7. 3 0
      components/AnnotationItem/styled.ts
  8. 43 70
      components/AnnotationList/index.tsx
  9. 62 0
      components/AnnotationListHead/index.tsx
  10. 10 19
      components/AnnotationSelector/index.tsx
  11. 33 0
      components/AnnotationWrapper/index.tsx
  12. 6 0
      components/AnnotationWrapper/styled.ts
  13. 1 1
      components/Box/index.tsx
  14. 4 2
      components/Button/index.tsx
  15. 57 18
      components/ColorSelector/data.ts
  16. 74 24
      components/ColorSelector/index.tsx
  17. 28 23
      components/DeleteDialog/index.tsx
  18. 1 1
      components/Dialog/index.tsx
  19. 1 1
      components/Divider/index.tsx
  20. 1 1
      components/Drawer/index.tsx
  21. 2 0
      components/Drawer/styled.ts
  22. 1 1
      components/ExpansionPanel/index.tsx
  23. 108 0
      components/FreeText/index.tsx
  24. 19 0
      components/FreeText/styled.ts
  25. 91 0
      components/FreeTextOption/data.ts
  26. 130 0
      components/FreeTextOption/index.tsx
  27. 0 87
      components/FreehandOption/index.tsx
  28. 35 33
      components/Head.tsx
  29. 65 0
      components/Highlight/index.tsx
  30. 1 0
      components/Annotation/styled.ts
  31. 47 38
      components/HighlightOption/index.tsx
  32. 18 6
      components/Icon/data.ts
  33. 7 2
      components/Icon/index.tsx
  34. 62 33
      components/Icon/styled.ts
  35. 138 0
      components/Ink/index.tsx
  36. 12 0
      components/Ink/styled.ts
  37. 92 0
      components/InkOption/index.tsx
  38. 169 0
      components/Line/index.tsx
  39. 2 2
      components/Markup/index.tsx
  40. 1 1
      components/Modal/index.tsx
  41. 4 4
      components/Navbar/data.ts
  42. 4 2
      components/Navbar/index.tsx
  43. 56 0
      components/OuterRect/data.ts
  44. 176 0
      components/OuterRect/index.tsx
  45. 30 0
      components/OuterRect/styled.ts
  46. 138 0
      components/OuterRectForLine/index.tsx
  47. 12 0
      components/OuterRectForLine/styled.ts
  48. 25 7
      components/Page/index.tsx
  49. 6 5
      components/Page/styled.ts
  50. 8 4
      components/Pagination/index.tsx
  51. 1 1
      components/PdfSkeleton/index.tsx
  52. 1 1
      components/Portal/index.tsx
  53. 10 6
      components/Search/index.tsx
  54. 3 5
      components/SelectBox/index.tsx
  55. 4 4
      components/SelectBox/styled.ts
  56. 83 0
      components/Shape/index.tsx
  57. 38 0
      components/ShapeOption/data.tsx
  58. 82 0
      components/ShapeOption/index.tsx
  59. 0 92
      components/ShapeTools/index.tsx
  60. 1 1
      components/Skeleton/index.tsx
  61. 8 2
      components/SliderWithTitle/index.tsx
  62. 44 19
      components/Sliders/index.tsx
  63. 105 0
      components/StickyNote/index.tsx
  64. 25 0
      components/StickyNote/styled.ts
  65. 56 0
      components/SvgShapeElement/index.tsx
  66. 7 8
      components/Tabs/index.tsx
  67. 2 1
      components/TextField/index.tsx
  68. 0 50
      components/TextTools/data.ts
  69. 0 87
      components/TextTools/index.tsx
  70. 1 1
      components/Thumbnail/index.tsx
  71. 1 1
      components/ThumbnailViewer/index.tsx
  72. 9 9
      components/Toolbar/data.ts
  73. 16 5
      components/Toolbar/index.tsx
  74. 1 1
      components/Tooltip/index.tsx
  75. 9 12
      components/Typography/index.tsx
  76. 1 1
      components/Viewer/index.tsx
  77. 1 0
      components/Viewer/styled.ts
  78. 34 90
      components/Watermark/index.tsx
  79. 3 6
      components/Watermark/styled.ts
  80. 39 0
      components/WatermarkImageSelector/index.tsx
  81. 6 0
      components/WatermarkImageSelector/styled.ts
  82. 127 0
      components/WatermarkOption/index.tsx
  83. 5 0
      components/WatermarkOption/styled.ts
  84. 25 0
      components/WatermarkPageSelector/index.tsx
  85. 32 0
      components/WatermarkTextBox/index.tsx
  86. 6 0
      components/WatermarkTextBox/styled.ts
  87. 1 1
      config/index.js
  88. 5 2
      constants/actionTypes.ts
  89. 12 1
      constants/index.ts
  90. 1 1
      constants/style.ts
  91. 104 18
      constants/type.ts
  92. 107 36
      containers/Annotation.tsx
  93. 29 13
      containers/AnnotationList.tsx
  94. 25 0
      containers/AnnotationListHead.tsx
  95. 7 16
      containers/AutoSave.tsx
  96. 43 0
      containers/ColorSelector.tsx
  97. 7 5
      containers/CreateForm.tsx
  98. 126 0
      containers/FreeTextTools.tsx
  99. 122 54
      containers/FreehandTools.tsx
  100. 0 0
      containers/HighlightTools.tsx

+ 1 - 12
.babelrc

@@ -1,17 +1,6 @@
 {
   "presets": [
-    [
-      "next/babel",
-      {
-        "preset-env": {
-          "targets": {
-            "ie": "11"
-          },
-          "corejs": "3",
-          "useBuiltIns": "entry"
-        }
-      }
-    ]
+    "next/babel"
   ],
   "plugins": [
     ["inline-react-svg", {

+ 6 - 0
actions/main.ts

@@ -17,6 +17,12 @@ const actions: ActionType = dispatch => ({
   setInfo: (state: Record<string, any>): void => (
     dispatch({ type: types.SET_INFO, payload: state })
   ),
+  setSaved: (state: boolean): void => (
+    dispatch({ type: types.SET_SAVED, payload: state })
+  ),
+  setSaving: (state: boolean): void => (
+    dispatch({ type: types.SET_SAVING, payload: state })
+  ),
 });
 
 export default actions;

+ 8 - 5
actions/pdf.ts

@@ -1,6 +1,6 @@
 import * as types from '../constants/actionTypes';
 import {
-  ProgressType, ViewportType, AnnotationType, ActionType,
+  ProgressType, ViewportType, AnnotationType, ActionType, WatermarkType,
 } from '../constants/type';
 
 const actions: ActionType = dispatch => ({
@@ -25,11 +25,14 @@ const actions: ActionType = dispatch => ({
   changeRotate: (rotation: number): void => (
     dispatch({ type: types.CHANGE_ROTATE, payload: rotation })
   ),
-  addAnnotation: (annotations: AnnotationType[], init: boolean): void => (
-    dispatch({ type: types.ADD_ANNOTATIONS, payload: { annotations, init } })
+  addAnnots: (annotations: AnnotationType[], init: boolean): void => (
+    dispatch({ type: types.ADD_ANNOTS, payload: { annotations, init } })
   ),
-  updateAnnotation: (annotations: AnnotationType[]): void => (
-    dispatch({ type: types.UPDATE_ANNOTATIONS, payload: annotations })
+  updateAnnots: (annotations: AnnotationType[]): void => (
+    dispatch({ type: types.UPDATE_ANNOTS, payload: annotations })
+  ),
+  updateWatermark: (watermark: WatermarkType): void => (
+    dispatch({ type: types.UPDATE_WATERMARK, payload: watermark })
   ),
 });
 

+ 0 - 95
components/Annotation/index.tsx

@@ -1,95 +0,0 @@
-import React from 'react';
-
-import {
-  ViewportType,
-  AnnotationType,
-  OnUpdateType,
-  PositionType,
-  HTMLCoordinateType,
-} from '../../constants/type';
-import AnnotationSelector from '../AnnotationSelector';
-import Markup from '../Markup';
-
-import {
-  Popper,
-} from './styled';
-
-type Props = AnnotationType & {
-  isCovered: boolean;
-  isCollapse: boolean;
-  mousePosition: Record<string, any>;
-  onUpdate: OnUpdateType;
-  onDelete: () => void;
-  scale: number;
-  viewport: ViewportType;
-};
-
-const Annotation: React.FunctionComponent<Props> = ({
-  obj_type,
-  obj_attr,
-  isCovered,
-  mousePosition,
-  isCollapse,
-  onUpdate,
-  onDelete,
-  scale,
-  viewport,
-}: Props) => {
-  const {
-    page, position, bdcolor, transparency,
-  } = obj_attr;
-
-  const processPosition = (type: string, ele: PositionType): HTMLCoordinateType => {
-    switch (type) {
-      case 'Squiggly':
-        return {
-          top: `${viewport.height - ele.top * scale}px`,
-          left: `${ele.left * scale}px`,
-          width: `${(ele.right - ele.left) * scale}px`,
-          height: `${(ele.top - ele.bottom) * scale + 5}px`,
-        };
-      default:
-        return {
-          top: `${viewport.height - ele.top * scale}px`,
-          left: `${ele.left * scale}px`,
-          width: `${(ele.right - ele.left) * scale}px`,
-          height: `${(ele.top - ele.bottom) * scale + 2}px`,
-        };
-    }
-  };
-
-  return (
-    <>
-      {
-        position.map((ele: any, index: number) => {
-          const pos = processPosition(obj_type, ele);
-
-          return (
-            <Markup
-              key={`block_${page + index}`}
-              position={pos}
-              bdcolor={bdcolor}
-              opacity={transparency}
-              markupType={obj_type}
-              isCovered={isCovered}
-            />
-          );
-        })
-      }
-      {
-        !isCollapse ? (
-          <Popper position={mousePosition}>
-            <AnnotationSelector
-              onUpdate={onUpdate}
-              onDelete={onDelete}
-              colorProps={bdcolor}
-              opacityProps={transparency * 100}
-            />
-          </Popper>
-        ) : ''
-      }
-    </>
-  );
-};
-
-export default Annotation;

+ 48 - 0
components/AnnotationItem/data.ts

@@ -0,0 +1,48 @@
+const data: Record<string, any> = {
+  Highlight: {
+    text: 'highlight',
+    icon: 'highlight',
+  },
+  Underline: {
+    text: 'underline',
+    icon: 'underline',
+  },
+  Squiggly: {
+    text: 'squiggly',
+    icon: 'squiggly',
+  },
+  StrikeOut: {
+    text: 'strikeout',
+    icon: 'strikeout',
+  },
+  Ink: {
+    text: 'freehand',
+    icon: 'freehand',
+  },
+  FreeText: {
+    text: 'text box',
+    icon: 'text',
+  },
+  Text: {
+    text: 'sticky note',
+    icon: 'sticky-note',
+  },
+  Square: {
+    text: 'square',
+    icon: 'square',
+  },
+  Circle: {
+    text: 'circle',
+    icon: 'circle',
+  },
+  Line: {
+    text: 'line',
+    icon: 'line',
+  },
+  Arrow: {
+    text: 'arrow line',
+    icon: 'arrow',
+  },
+};
+
+export default data;

+ 12 - 44
components/AnnotationItem/index.tsx

@@ -1,11 +1,12 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 
+import Icon from '../Icon';
 import Divider from '../Divider';
-import Markup from '../Markup';
 import { scrollIntoView } from '../../helpers/utility';
 
+import data from './data';
 import {
-  PageNumber, AnnotationBox, Content, Inner, Text,
+  PageNumber, AnnotationBox,
 } from './styled';
 
 type Props = {
@@ -21,12 +22,7 @@ const AnnotationItem = ({
   type,
   page,
   showPageNum,
-  bdcolor,
-  getText,
-  transparency,
 }: Props): React.ReactElement => {
-  const [content, setContent] = useState([]);
-
   const handleClick = (): void => {
     const ele: HTMLElement | null = document.getElementById(`page_${page}`);
 
@@ -35,45 +31,17 @@ const AnnotationItem = ({
     }
   };
 
-  useEffect(() => {
-    getText().then((text) => {
-      let textArray = [];
-
-      if (text.includes(' ')) {
-        textArray = text.split(' ');
-      } else {
-        textArray = text.match(/.{1,12}/g);
-      }
-
-      setContent(textArray);
-    });
-  }, []);
-
   return (
     <>
-      {showPageNum && <Divider orientation="horizontal" />}
-      {showPageNum && <PageNumber>{`Page ${page}`}</PageNumber>}
+      {showPageNum ? (
+        <>
+          <Divider orientation="horizontal" />
+          <PageNumber>{`Page ${page}`}</PageNumber>
+        </>
+      ) : null}
       <AnnotationBox onClick={handleClick}>
-        <Content>
-          {
-            content.map((textContent: string, index: number): React.ReactElement | null => {
-              const key = `key_${index}`;
-              return textContent ? (
-                <Inner key={key}>
-                  <Text>{textContent}</Text>
-                  <Markup
-                    position={{
-                      top: 0, left: 0, width: '100%', height: '100%',
-                    }}
-                    markupType={type}
-                    bdcolor={bdcolor}
-                    opacity={transparency}
-                  />
-                </Inner>
-              ) : null;
-            })
-          }
-        </Content>
+        <Icon glyph={data[type].icon} />
+        {data[type].text}
       </AnnotationBox>
     </>
   );

+ 3 - 0
components/AnnotationItem/styled.ts

@@ -16,6 +16,9 @@ export const AnnotationBox = styled.div`
   padding: 12px;
   width: 235px;
   margin-bottom: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 `;
 
 export const Content = styled.div`

+ 43 - 70
components/AnnotationList/index.tsx

@@ -1,50 +1,39 @@
 import React, {
   useEffect, useState, useRef, useCallback,
 } from 'react';
-import queryString from 'query-string';
+import { useTranslation } from 'react-i18next';
 
-import Icon from '../Icon';
-import Drawer from '../Drawer';
 import Typography from '../Typography';
 import Item from '../AnnotationItem';
 import {
   AnnotationType, ViewportType, ScrollStateType, PositionType,
 } from '../../constants/type';
 
-import { downloadFileWithUri, watchScroll } from '../../helpers/utility';
+import { watchScroll } from '../../helpers/utility';
 import { getAnnotationText } from '../../helpers/annotation';
-import { Separator } from '../../global/otherStyled';
-import {
-  Container, Head, Body, IconWrapper,
-} from '../../global/sidebarStyled';
+
+import { Body } from '../../global/sidebarStyled';
 
 type Props = {
   isActive?: boolean;
-  close: () => void;
   annotations: AnnotationType[];
   viewport: ViewportType;
   scale: number;
   pdf: any;
 };
 
-const AnnotationsList: React.FunctionComponent<Props> = ({
+const AnnotationList: React.FC<Props> = ({
   isActive = false,
-  close,
   annotations,
   viewport,
   scale,
   pdf,
 }: Props) => {
+  const { t } = useTranslation('sidebar');
   const [renderQueue, setQueue] = useState<AnnotationType[]>([]);
   const containerRef = useRef<HTMLDivElement>(null);
   const innerRef = useRef<HTMLDivElement>(null);
 
-  const handleExport = (): void => {
-    const parsed = queryString.parse(window.location.search);
-    const uri = `/api/v1/output.xfdf?f=${parsed.token}`;
-    downloadFileWithUri('output.xfdf', uri);
-  };
-
   const getText = async (page: number, position: PositionType[]): Promise<any> => {
     const text = await getAnnotationText({
       pdf,
@@ -53,6 +42,7 @@ const AnnotationsList: React.FunctionComponent<Props> = ({
       page,
       coords: position,
     });
+
     return text;
   };
 
@@ -83,61 +73,44 @@ const AnnotationsList: React.FunctionComponent<Props> = ({
     if (isActive) {
       setQueue(annotations.slice(0, 10));
     }
-  }, [isActive]);
+  }, [isActive, annotations]);
 
   return (
-    <Drawer anchor="right" open={isActive}>
-      <Container>
-        <Head>
-          <IconWrapper>
-            <Icon glyph="right-back" onClick={close} />
-          </IconWrapper>
-          <Separator />
-          <IconWrapper>
-            <Icon glyph="sort" />
-          </IconWrapper>
-          <IconWrapper onClick={handleExport}>
-            <Icon glyph="annotation-export" />
-          </IconWrapper>
-          <IconWrapper>
-            <Icon glyph="import" />
-          </IconWrapper>
-        </Head>
-        <Body ref={containerRef}>
-          <div ref={innerRef}>
-            <Typography light align="left">
-              {`${annotations.length} Annotations`}
-            </Typography>
-            {isActive && renderQueue.map((ele, index) => {
-              const key = `annot_item_${index}`;
-              const {
-                obj_type,
-                obj_attr: {
-                  page,
-                  bdcolor,
-                  position,
-                  transparency,
-                },
-              } = ele;
-              const actualPage = page + 1;
-              const prevPage = index > 0 ? annotations[index - 1].obj_attr.page + 1 : -1;
-              return (
-                <Item
-                  key={key}
-                  type={obj_type}
-                  page={actualPage}
-                  bdcolor={bdcolor}
-                  transparency={transparency}
-                  getText={(): Promise<any> => getText(actualPage, position)}
-                  showPageNum={actualPage !== prevPage}
-                />
-              );
-            })}
-          </div>
-        </Body>
-      </Container>
-    </Drawer>
+    <Body ref={containerRef}>
+      <div ref={innerRef}>
+        <Typography light align="left">
+          {`${annotations.length} ${t('annotation')}`}
+        </Typography>
+        {isActive && renderQueue.map((ele, index) => {
+          const key = `annot_item_${index}`;
+          const {
+            obj_type,
+            obj_attr: {
+              page,
+              bdcolor,
+              position,
+              transparency,
+            },
+          } = ele;
+          const actualPage = page + 1;
+          const prevAnnot = annotations[index - 1];
+          const prevPage = index > 0 && prevAnnot ? prevAnnot.obj_attr.page + 1 : -1;
+
+          return (
+            <Item
+              key={key}
+              type={obj_type}
+              page={actualPage}
+              bdcolor={bdcolor || ''}
+              transparency={transparency || 0}
+              getText={(): Promise<any> => getText(actualPage, position as PositionType[])}
+              showPageNum={actualPage !== prevPage}
+            />
+          );
+        })}
+      </div>
+    </Body>
   );
 };
 
-export default AnnotationsList;
+export default AnnotationList;

+ 62 - 0
components/AnnotationListHead/index.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+import queryString from 'query-string';
+
+import { AnnotationType } from '../../constants/type';
+import Icon from '../Icon';
+import { downloadFileWithUri, uploadFile } from '../../helpers/utility';
+import { parseAnnotationFromXml } from '../../helpers/annotation';
+
+import { Separator } from '../../global/otherStyled';
+import {
+  Head, IconWrapper,
+} from '../../global/sidebarStyled';
+
+type Props = {
+  close: () => void;
+  addAnnots: (annotations: AnnotationType[]) => void;
+  hasAnnots: boolean;
+};
+
+const index: React.FC<Props> = ({
+  close,
+  addAnnots,
+  hasAnnots,
+}: Props) => {
+  const handleExport = (): void => {
+    if (hasAnnots) {
+      const parsed = queryString.parse(window.location.search);
+      const uri = `/api/v1/output.xfdf?f=${parsed.token}`;
+      downloadFileWithUri('output.xfdf', uri);
+    }
+  };
+
+  const handleImport = (): void => {
+    uploadFile('.xfdf').then((data) => {
+      const annotations = parseAnnotationFromXml(data);
+      addAnnots(annotations);
+    });
+  };
+
+  return (
+    <Head>
+      <IconWrapper>
+        <Icon glyph="right-back" onClick={close} />
+      </IconWrapper>
+      <Separator />
+      <IconWrapper>
+        <Icon glyph="sort" />
+      </IconWrapper>
+      <IconWrapper onClick={handleExport}>
+        <Icon
+          glyph="annotation-export"
+          isDisabled={!hasAnnots}
+        />
+      </IconWrapper>
+      <IconWrapper onClick={handleImport}>
+        <Icon glyph="import" />
+      </IconWrapper>
+    </Head>
+  );
+};
+
+export default index;

+ 10 - 19
components/AnnotationSelector/index.tsx

@@ -1,12 +1,11 @@
 import React, { useState } from 'react';
 
-import ColorSelector from '../ColorSelector';
 import Button from '../Button';
 import Icon from '../Icon';
 import Divider from '../Divider';
 import Box from '../Box';
 import Sliders from '../Sliders';
-import DeleteDialog from '../DeleteDialog';
+import ColorSelector from '../../containers/ColorSelector';
 
 import { OnUpdateType } from '../../constants/type';
 
@@ -21,23 +20,22 @@ type Props = {
   opacityProps?: number;
 };
 
-const index: React.FunctionComponent<Props> = ({
+const index: React.FC<Props> = ({
   onUpdate,
   onDelete,
   colorProps,
   opacityProps,
 }: Props) => {
   const [openSlider, setSlider] = useState(false);
-  const [openDialog, setDialog] = useState(false);
   const [opacity, setOpacity] = useState(opacityProps);
 
   const handleClick = (color: string): void => {
-    onUpdate({ color });
+    onUpdate({ bdcolor: color });
   };
 
   const handleChange = (value: number): void => {
     setOpacity(value);
-    onUpdate({ opacity: value });
+    onUpdate({ transparency: value * 0.01 });
   };
 
   const sliderToggle = (e: React.MouseEvent<HTMLElement>): void => {
@@ -45,11 +43,6 @@ const index: React.FunctionComponent<Props> = ({
     setSlider(!openSlider);
   };
 
-  const DialogToggle = (e: React.MouseEvent<HTMLElement>): void => {
-    e.preventDefault();
-    setDialog(!openDialog);
-  };
-
   return (
     <Container>
       {
@@ -60,13 +53,16 @@ const index: React.FunctionComponent<Props> = ({
             </Box>
             <Divider style={{ margin: '0 10px 0 10px' }} />
             <SliderWrapper>
-              <Sliders defaultValue={opacity} onChange={handleChange} />
+              <Sliders value={opacity} onChange={handleChange} />
             </SliderWrapper>
             <Subtitle>{`${opacity} %`}</Subtitle>
           </>
         ) : (
           <>
-            <ColorSelector color={colorProps} onClick={handleClick} />
+            <ColorSelector
+              selectedColor={colorProps}
+              onClick={handleClick}
+            />
             <Subtitle>opacity</Subtitle>
             <Button
               appearance="dark"
@@ -77,16 +73,11 @@ const index: React.FunctionComponent<Props> = ({
             </Button>
             <Divider style={{ margin: '0 6px 0 15px' }} />
             <Box w="48px" h="36px" d="flex" j="center">
-              <Icon glyph="trash" onClick={DialogToggle} />
+              <Icon glyph="trash" onClick={onDelete} />
             </Box>
           </>
         )
       }
-      <DeleteDialog
-        open={openDialog}
-        onCancel={DialogToggle}
-        onDelete={onDelete}
-      />
     </Container>
   );
 };

+ 33 - 0
components/AnnotationWrapper/index.tsx

@@ -0,0 +1,33 @@
+import React, { forwardRef } from 'react';
+
+import { Wrapper } from './styled';
+
+type Props = {
+  onBlur: () => void;
+  onMouseDown: () => void;
+  onMouseOver: () => void;
+  onMouseOut: () => void;
+  onKeyDown: (e: React.KeyboardEvent) => void;
+  children: React.ReactNode;
+};
+
+type Ref = HTMLDivElement;
+
+const index = forwardRef<Ref, Props>(({
+  children,
+  ...rest
+}: Props, ref) => (
+  <Wrapper
+    ref={ref}
+    tabIndex={0}
+    role="button"
+    onFocus={(): void => {
+      // do nothing
+    }}
+    {...rest}
+  >
+    {children}
+  </Wrapper>
+));
+
+export default index;

+ 6 - 0
components/AnnotationWrapper/styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+  touch-action: none;
+  outline: none;
+`;

+ 1 - 1
components/Box/index.tsx

@@ -25,7 +25,7 @@ type Props = {
   children: React.ReactNode;
 };
 
-const Box: React.FunctionComponent<Props> = ({
+const Box: React.FC<Props> = ({
   children,
   ...rest
 }: Props) => {

+ 4 - 2
components/Button/index.tsx

@@ -20,7 +20,7 @@ export type Props = {
   tabIndex?: number;
 };
 
-const Button: React.FunctionComponent<Props> = ({
+const Button: React.FC<Props> = ({
   children,
   isDisabled,
   onClick,
@@ -46,7 +46,9 @@ Button.defaultProps = {
   shouldFitContainer: false,
   isDisabled: false,
   isActive: false,
-  onFocus: (): void => { console.log('focus'); },
+  onFocus: (): void => {
+    // do nothing
+  },
 };
 
 export default Button;

+ 57 - 18
components/ColorSelector/data.ts

@@ -1,18 +1,57 @@
-export default [
-  {
-    key: 'lemon-yellow',
-    color: '#FCFF36',
-  },
-  {
-    key: 'strong-pink',
-    color: '#FF1B89',
-  },
-  {
-    key: 'neon-green',
-    color: '#02FF36',
-  },
-  {
-    key: 'light-blue',
-    color: '#27BEFD',
-  },
-];
+export default {
+  normal: [
+    {
+      key: 'lemon-yellow',
+      color: '#FCFF36',
+    },
+    {
+      key: 'strong-pink',
+      color: '#FF1B89',
+    },
+    {
+      key: 'neon-green',
+      color: '#02FF36',
+    },
+    {
+      key: 'light-blue',
+      color: '#27BEFD',
+    },
+  ],
+  shape: [
+    {
+      key: 'transparency',
+      color: 'none',
+      icon: 'none',
+    },
+    {
+      key: 'strong-pink',
+      color: '#FF1B89',
+    },
+    {
+      key: 'neon-green',
+      color: '#02FF36',
+    },
+    {
+      key: 'light-blue',
+      color: '#27BEFD',
+    },
+  ],
+  watermark: [
+    {
+      key: 'gray',
+      color: '#d8d8d8',
+    },
+    {
+      key: 'strong-pink',
+      color: '#FF1B89',
+    },
+    {
+      key: 'neon-green',
+      color: '#02FF36',
+    },
+    {
+      key: 'light-blue',
+      color: '#27BEFD',
+    },
+  ],
+};

+ 74 - 24
components/ColorSelector/index.tsx

@@ -1,40 +1,90 @@
 import React from 'react';
+import { ChromePicker } from 'react-color';
 
+import Portal from '../Portal';
 import Icon from '../Icon';
 import Typography from '../Typography';
 
-import { Group, Item, Circle } from '../../global/toolStyled';
+import {
+  Group,
+  Item,
+  Circle,
+  PickerContainer,
+  Blanket,
+} from '../../global/toolStyled';
 
 import data from './data';
 
 type Props = {
-  showTitle?: boolean;
-  color?: string;
+  title?: string;
+  selectedColor?: string;
   onClick: (color: string) => void;
+  mode?: 'normal' | 'shape' | 'watermark';
+  isCollapse: boolean;
+  pickerToggle: () => void;
 };
 
-const ColorSelector: React.FunctionComponent<Props> = ({
-  showTitle = false,
-  color = '',
+type ColorItem = {
+  key: string | number | undefined;
+  color: string;
+  icon?: string;
+};
+
+const ColorSelector: React.FC<Props> = ({
+  title = '',
+  mode = 'normal',
+  selectedColor = '',
   onClick,
-}: Props) => (
-  <>
-    { showTitle ? <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">Color</Typography> : null}
-    <Group>
-      {data.map(ele => (
-        <Item
-          key={ele.key}
-          selected={color === ele.color}
-          onMouseDown={(): void => { onClick(ele.color); }}
-        >
-          <Circle color={ele.color} />
+  pickerToggle,
+  isCollapse,
+}: Props) => {
+  const colorList: ColorItem[] = data[mode];
+
+  return (
+    <>
+      { title ? (
+        <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">
+          {title}
+        </Typography>
+      ) : null}
+      <Group>
+        {
+          colorList.map((ele: ColorItem) => (
+            <Item
+              key={ele.key}
+              selected={selectedColor === ele.color}
+              onMouseDown={(): void => { onClick(ele.color); }}
+            >
+              {
+                ele.icon ? (
+                  <Icon glyph={ele.icon} />
+                ) : (
+                  <Circle color={ele.color} />
+                )
+              }
+            </Item>
+          ))
+        }
+        <Item onClick={pickerToggle}>
+          <Icon glyph="color-picker" />
         </Item>
-      ))}
-      <Item>
-        <Icon glyph="color-picker" />
-      </Item>
-    </Group>
-  </>
-);
+        {
+          !isCollapse ? (
+            <Portal>
+              <PickerContainer>
+                <Blanket onClick={pickerToggle} />
+                <ChromePicker
+                  color={selectedColor}
+                  disableAlpha
+                  onChange={(color: any): void => { onClick(color.hex); }}
+                />
+              </PickerContainer>
+            </Portal>
+          ) : null
+        }
+      </Group>
+    </>
+  );
+};
 
 export default ColorSelector;

+ 28 - 23
components/DeleteDialog/index.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
 
 import Dialog from '../Dialog';
 import Button from '../Button';
@@ -13,31 +14,35 @@ type Props = {
   onDelete: (e: any) => void;
 };
 
-const index: React.FunctionComponent<Props> = ({
+const index: React.FC<Props> = ({
   open,
   onCancel,
   onDelete,
-}: Props) => (
-  <Dialog open={open}>
-    <TextWrapper>
-      This will permanently delete the annotation. Do you want to continue?
-    </TextWrapper>
-    <BtnWrapper>
-      <Button
-        appearance="default-hollow"
-        onClick={onCancel}
-      >
-        Cancel
-      </Button>
-      <Button
-        appearance="primary"
-        onClick={onDelete}
-        style={{ marginLeft: '16px' }}
-      >
-        OK
-      </Button>
-    </BtnWrapper>
-  </Dialog>
-);
+}: Props) => {
+  const { t } = useTranslation('dialog');
+
+  return (
+    <Dialog open={open}>
+      <TextWrapper>
+        {t('deleteAnnotationAlert')}
+      </TextWrapper>
+      <BtnWrapper>
+        <Button
+          appearance="default-hollow"
+          onClick={onCancel}
+        >
+          {t('cancel')}
+        </Button>
+        <Button
+          appearance="primary"
+          onClick={onDelete}
+          style={{ marginLeft: '16px' }}
+        >
+          {t('continue')}
+        </Button>
+      </BtnWrapper>
+    </Dialog>
+  );
+};
 
 export default index;

+ 1 - 1
components/Dialog/index.tsx

@@ -9,7 +9,7 @@ type Props = {
   open: boolean;
 };
 
-const Dialog: React.FunctionComponent<Props> = ({
+const Dialog: React.FC<Props> = ({
   children,
   open,
 }: Props) => (

+ 1 - 1
components/Divider/index.tsx

@@ -10,7 +10,7 @@ export type Props = {
   style?: Record<string, any>;
 }
 
-const Divider: React.FunctionComponent<Props> = props => (
+const Divider: React.FC<Props> = props => (
   <Component
     {...props}
   />

+ 1 - 1
components/Drawer/index.tsx

@@ -11,7 +11,7 @@ type Props = {
   zIndex?: number;
 };
 
-const Drawer: React.FunctionComponent<Props> = ({
+const Drawer: React.FC<Props> = ({
   anchor = 'bottom',
   children,
   open = false,

+ 2 - 0
components/Drawer/styled.ts

@@ -62,4 +62,6 @@ export const Wrapper = styled.div`
   height: 100%;
   width: 100%;
   box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.24);
+  -ms-overflow-style: none;
+  overflow: auto;
 `;

+ 1 - 1
components/ExpansionPanel/index.tsx

@@ -11,7 +11,7 @@ type Props = {
   showBottomBorder?: boolean;
 };
 
-const Collapse: React.FunctionComponent<Props> = ({
+const Collapse: React.FC<Props> = ({
   label = '',
   children = '',
   isActive = false,

+ 108 - 0
components/FreeText/index.tsx

@@ -0,0 +1,108 @@
+import React from 'react';
+
+import OuterRect from '../OuterRect';
+import {
+  AnnotationElementPropsType, PositionType, CoordType,
+} from '../../constants/type';
+import { rectCalc, parsePositionForBackend } from '../../helpers/position';
+
+import { AnnotationContainer } from '../../global/otherStyled';
+import { TextWrapper, Input } from './styled';
+
+const FreeText: React.SFC<AnnotationElementPropsType> = ({
+  obj_type,
+  obj_attr: {
+    content,
+    position,
+    fontname,
+    fontsize,
+    textcolor,
+  },
+  scale,
+  viewport,
+  isCollapse,
+  isEdit,
+  onEdit,
+  onUpdate,
+  onBlur,
+}: AnnotationElementPropsType) => {
+  const annotRect = rectCalc(position as PositionType, viewport.height, scale);
+
+  const handleChange = (event: React.FormEvent<HTMLInputElement>): void => {
+    const textValue = event.currentTarget.value;
+    const fontWidth = textValue.length * (fontsize || 1);
+    const newPosition = { ...position as PositionType };
+    newPosition.right = newPosition.left + fontWidth;
+
+    onUpdate({
+      content: textValue,
+      position: newPosition,
+    });
+  };
+
+  const handleScaleOrMove = ({
+    top, left, width = 0, height = 0,
+  }: CoordType): void => {
+    const newPosition = {
+      top,
+      left,
+      bottom: top + (height || annotRect.height),
+      right: left + (width || annotRect.width),
+    };
+
+    onUpdate({
+      fontsize: (height || annotRect.height) / scale,
+      position: parsePositionForBackend(obj_type, newPosition, viewport.height, scale),
+    });
+  };
+
+  const styles = {
+    fontFamily: fontname,
+    fontSize: fontsize ? fontsize * scale : 0,
+    color: textcolor,
+  };
+
+  return (
+    <div>
+      <AnnotationContainer
+        top={`${annotRect.top}px`}
+        left={`${annotRect.left}px`}
+        width={`${annotRect.width + 2}px`}
+        height={`${annotRect.height + 4}px`}
+      >
+        {
+          isEdit ? (
+            <Input
+              defaultValue={content}
+              onChange={handleChange}
+              autoFocus
+              onBlur={onBlur}
+              style={styles}
+            />
+          ) : (
+            <TextWrapper
+              style={styles}
+            >
+              {content}
+            </TextWrapper>
+          )
+        }
+      </AnnotationContainer>
+      {
+        !isCollapse ? (
+          <OuterRect
+            top={annotRect.top}
+            left={annotRect.left}
+            width={annotRect.width}
+            height={annotRect.height}
+            onMove={handleScaleOrMove}
+            onScale={handleScaleOrMove}
+            onDoubleClick={onEdit}
+          />
+        ) : ''
+      }
+    </div>
+  );
+};
+
+export default FreeText;

+ 19 - 0
components/FreeText/styled.ts

@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+import { color } from '../../constants/style';
+
+export const TextWrapper = styled.div`
+  text-align: left;
+  user-select: none;
+  line-height: 1;
+`;
+
+export const Input = styled.input`
+  border: 1px solid ${color.primary};
+  padding: 0;
+  margin: 0;
+  width: 95%;
+  outline: none;
+  height: 93%;
+  display: inline-block;
+`;

+ 91 - 0
components/FreeTextOption/data.ts

@@ -0,0 +1,91 @@
+export const fontOptions = [
+  {
+    key: 'helvetica',
+    content: 'Helvetica',
+    child: 'Helvetica',
+  },
+  {
+    key: 'arial',
+    content: 'Arial',
+    child: 'Arial',
+  },
+  {
+    key: 'courier',
+    content: 'Courier',
+    child: 'Courier',
+  },
+  {
+    key: 'times new roman',
+    content: 'Times New Roman',
+    child: 'Times New Roman',
+  },
+];
+
+export const styleOptions = [
+  {
+    key: 'bold',
+    content: 'Bold',
+    child: 'Bold',
+  },
+  {
+    key: 'italic',
+    content: 'Italic',
+    child: 'Italic',
+  },
+];
+
+export const alignOptions = [
+  {
+    key: 'align-left',
+    content: 'left',
+    child: 'left',
+  },
+  {
+    key: 'align-center',
+    content: 'center',
+    child: 'center',
+  },
+  {
+    key: 'align-right',
+    content: 'right',
+    child: 'right',
+  },
+];
+
+export const sizeOptions = [
+  {
+    key: 'size_12',
+    content: 12,
+    child: 12,
+  },
+  {
+    key: 'size_14',
+    content: 14,
+    child: 14,
+  },
+  {
+    key: 'size_18',
+    content: 18,
+    child: 18,
+  },
+  {
+    key: 'size_24',
+    content: 24,
+    child: 24,
+  },
+  {
+    key: 'size_36',
+    content: 36,
+    child: 36,
+  },
+  {
+    key: 'size_48',
+    content: 48,
+    child: 48,
+  },
+  {
+    key: 'size_64',
+    content: 64,
+    child: 64,
+  },
+];

+ 130 - 0
components/FreeTextOption/index.tsx

@@ -0,0 +1,130 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import Icon from '../Icon';
+import Typography from '../Typography';
+import SliderWithTitle from '../SliderWithTitle';
+import SelectBox from '../SelectBox';
+import ColorSelector from '../../containers/ColorSelector';
+import { OptionPropsType, SelectOptionType } from '../../constants/type';
+
+import {
+  Wrapper, Group, Item,
+} from '../../global/toolStyled';
+
+import {
+  fontOptions,
+  sizeOptions,
+  alignOptions,
+  styleOptions,
+} from './data';
+
+const TextOption: React.SFC<OptionPropsType> = ({
+  fontName,
+  fontSize,
+  align,
+  fontStyle,
+  color,
+  opacity,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      <Wrapper>
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('font')}
+        </Typography>
+        <Group>
+          <SelectBox
+            style={{ width: '100px' }}
+            defaultValue={fontName}
+            options={fontOptions}
+            onChange={(option: SelectOptionType): void => {
+              setDataState({ fontName: option.child });
+            }}
+          />
+          {
+            styleOptions.map(ele => (
+              <Item
+                key={ele.key}
+                size="small"
+                selected={fontStyle === ele.child}
+                onClick={(): void => {
+                  setDataState({ fontStyle: ele.child === fontStyle ? '' : ele.child });
+                }}
+              >
+                <Icon glyph={ele.key} />
+              </Item>
+            ))
+          }
+        </Group>
+      </Wrapper>
+      <Wrapper width="40%">
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('size')}
+        </Typography>
+        <Group>
+          <SelectBox
+            defaultValue={fontSize}
+            options={sizeOptions}
+            onChange={(option: SelectOptionType): void => {
+              setDataState({ fontSize: option.child });
+            }}
+          />
+        </Group>
+      </Wrapper>
+      <Wrapper width="60%">
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('align')}
+        </Typography>
+        <Group>
+          {
+            alignOptions.map(ele => (
+              <Item
+                key={ele.key}
+                size="small"
+                selected={align === ele.child}
+                onClick={(): void => { setDataState({ align: ele.child }); }}
+              >
+                <Icon glyph={ele.key} />
+              </Item>
+            ))
+          }
+        </Group>
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+    </>
+  );
+};
+
+export default TextOption;

+ 0 - 87
components/FreehandOption/index.tsx

@@ -1,87 +0,0 @@
-import React from 'react';
-
-import Icon from '../Icon';
-import Button from '../Button';
-import Typography from '../Typography';
-import ColorSelect from '../ColorSelector';
-import SliderWithTitle from '../SliderWithTitle';
-
-import { Group, Item } from '../../global/toolStyled';
-
-type Props = {
-  type: string;
-  setType: (arg: string) => void;
-  color: string;
-  setColor: (arg: string) => void;
-  opacity: number;
-  handleOpacity: (arg: number) => void;
-  width: number;
-  handleWidth: (arg: number) => void;
-};
-
-const FreehandOption = ({
-  type,
-  setType,
-  color,
-  setColor,
-  opacity,
-  handleOpacity,
-  width,
-  handleWidth,
-}: Props): React.ReactElement => (
-  <>
-    <Typography variant="subtitle" style={{ marginTop: '4px' }} align="left">Tools</Typography>
-    <Group>
-      <Item
-        size="small"
-        selected={type === 'pen'}
-        onClick={(): void => { setType('pen'); }}
-      >
-        <Icon glyph="freehand" />
-      </Item>
-      <Item
-        size="small"
-        selected={type === 'eraser'}
-        onClick={(): void => { setType('eraser'); }}
-      >
-        <Icon glyph="eraser" />
-      </Item>
-      <Item size="small">
-        <Icon glyph="redo" />
-      </Item>
-      <Item size="small">
-        <Icon glyph="undo" />
-      </Item>
-    </Group>
-    <ColorSelect
-      showTitle
-      color={color}
-      onClick={setColor}
-    />
-    <SliderWithTitle
-      title="Opacity"
-      value={opacity}
-      tips={`${opacity}%`}
-      onSlide={handleOpacity}
-    />
-    <SliderWithTitle
-      title="Width"
-      value={width}
-      tips={`${width} pt`}
-      onSlide={handleWidth}
-      maximum={40}
-    />
-    <Group style={{ marginTop: '20px', paddingRight: '40px' }}>
-      <Button
-        appearance="danger-hollow"
-        shouldFitContainer
-      >
-        <Icon glyph="clear" style={{ marginRight: '5px' }} />
-        Clear all
-      </Button>
-    </Group>
-  </>
-);
-
-
-export default FreehandOption;

+ 35 - 33
components/Head.tsx

@@ -1,51 +1,53 @@
 import React from 'react';
 import NextHead from 'next/head';
-import { withTranslation } from '../i18n';
+import { useTranslation } from 'react-i18next';
 
 const defaultOGURL = '';
 const defaultOGImage = '';
 
 type Props = {
-  t: (key: string) => string;
   title?: string;
   description?: string;
   url?: string;
   ogImage?: string;
 };
 
-const Head: React.StatelessComponent<Props> = ({
-  t,
+const Head: React.SFC<Props> = ({
   title = 'title',
   description = 'description',
   url = '',
   ogImage = '',
-}: Props) => (
-  <NextHead>
-    <meta charSet="UTF-8" />
-    <title>{t(title)}</title>
-    <meta
-      name="description"
-      content={t(description)}
-    />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
-    <link rel="apple-touch-icon" href="/static/touch-icon.png" />
-    <link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
-    <link rel="icon" href="/static/favicon.ico" />
-    <meta property="og:url" content={url || defaultOGURL} />
-    <meta property="og:title" content={title || ''} />
-    <meta
-      property="og:description"
-      content={t(description)}
-    />
-    <meta name="twitter:site" content={url || defaultOGURL} />
-    <meta name="twitter:card" content="summary_large_image" />
-    <meta name="twitter:image" content={ogImage || defaultOGImage} />
-    <meta property="og:image" content={ogImage || defaultOGImage} />
-    <meta property="og:image:width" content="1200" />
-    <meta property="og:image:height" content="630" />
-    <meta httpEquiv="X-UA-Compatible" content="IE=EmulateIE11,chrome=1" />
-  </NextHead>
-);
+}: Props) => {
+  const { t } = useTranslation('meta');
 
-export default withTranslation('meta')(Head);
+  return (
+    <NextHead>
+      <meta charSet="UTF-8" />
+      <title>{t(title)}</title>
+      <meta
+        name="description"
+        content={t(description)}
+      />
+      <meta name="viewport" content="width=device-width, initial-scale=1" />
+      <link rel="icon" sizes="192x192" href="/static/touch-icon.png" />
+      <link rel="apple-touch-icon" href="/static/touch-icon.png" />
+      <link rel="mask-icon" href="/static/favicon-mask.svg" color="#49B882" />
+      <link rel="icon" href="/static/favicon.ico" />
+      <meta property="og:url" content={url || defaultOGURL} />
+      <meta property="og:title" content={title || ''} />
+      <meta
+        property="og:description"
+        content={t(description)}
+      />
+      <meta name="twitter:site" content={url || defaultOGURL} />
+      <meta name="twitter:card" content="summary_large_image" />
+      <meta name="twitter:image" content={ogImage || defaultOGImage} />
+      <meta property="og:image" content={ogImage || defaultOGImage} />
+      <meta property="og:image:width" content="1200" />
+      <meta property="og:image:height" content="630" />
+      <meta httpEquiv="X-UA-Compatible" content="IE=EmulateIE11,chrome=1" />
+    </NextHead>
+  );
+};
+
+export default Head;

+ 65 - 0
components/Highlight/index.tsx

@@ -0,0 +1,65 @@
+import React from 'react';
+
+import { AnnotationElementPropsType } from '../../constants/type';
+import AnnotationSelector from '../AnnotationSelector';
+import Markup from '../Markup';
+import { rectCalc } from '../../helpers/position';
+
+import {
+  Popper,
+} from './styled';
+
+const Highlight: React.SFC<AnnotationElementPropsType> = ({
+  obj_type,
+  obj_attr: {
+    page,
+    position,
+    bdcolor,
+    transparency,
+  },
+  isCovered,
+  mousePosition,
+  isCollapse,
+  onUpdate,
+  onDelete,
+  scale,
+  viewport,
+}: AnnotationElementPropsType) => (
+  <>
+    {
+      Array.isArray(position) && position.map((ele: any, index: number) => {
+        const annotRect = rectCalc(ele, viewport.height, scale);
+
+        return (
+          <Markup
+            key={`block_${page + index}`}
+            position={{
+              top: `${annotRect.top}px`,
+              left: `${annotRect.left}px`,
+              width: `${annotRect.width}px`,
+              height: `${annotRect.height}px`,
+            }}
+            bdcolor={bdcolor || ''}
+            opacity={transparency || 0}
+            markupType={obj_type}
+            isCovered={isCovered}
+          />
+        );
+      })
+    }
+    {
+      !isCollapse ? (
+        <Popper position={mousePosition}>
+          <AnnotationSelector
+            onUpdate={onUpdate}
+            onDelete={onDelete}
+            colorProps={bdcolor}
+            opacityProps={transparency ? transparency * 100 : 0}
+          />
+        </Popper>
+      ) : ''
+    }
+  </>
+);
+
+export default Highlight;

+ 1 - 0
components/Annotation/styled.ts

@@ -5,4 +5,5 @@ export const Popper = styled('div')<{position: Record<string, any>}>`
   top: ${props => props.position.y}px;
   left: ${props => props.position.x}px;
   transform: translate(-50%, -80px);
+  z-index: 10;
 `;

+ 47 - 38
components/HighlightOption/index.tsx

@@ -1,51 +1,60 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
 
+import { OptionPropsType } from '../../constants/type';
 import Icon from '../Icon';
 import Typography from '../Typography';
-import ColorSelector from '../ColorSelector';
 import SliderWithTitle from '../SliderWithTitle';
+import ColorSelector from '../../containers/ColorSelector';
 
-import { Group, Item } from '../../global/toolStyled';
+import { Group, Item, Wrapper } from '../../global/toolStyled';
 import data from './data';
 
-type Props = {
-  type: string;
-  setType: (arg: string) => void;
-  color: string;
-  setColor: (arg: string) => void;
-  opacity: number;
-  handleOpacity: (arg: number) => void;
-};
-
-const HighlightOption = ({
+const HighlightOption: React.SFC<OptionPropsType> = ({
   type,
-  setType,
   color,
-  setColor,
   opacity,
-  handleOpacity,
-}: Props): React.ReactElement => (
-  <>
-    <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">Style</Typography>
-    <Group>
-      {data.lineType.map(ele => (
-        <Item
-          key={ele.key}
-          selected={type === ele.key}
-          onClick={(): void => { setType(ele.key); }}
-        >
-          <Icon glyph={ele.icon} />
-        </Item>
-      ))}
-    </Group>
-    <ColorSelector showTitle color={color} onClick={setColor} />
-    <SliderWithTitle
-      title="Opacity"
-      value={opacity}
-      tips={`${opacity}%`}
-      onSlide={handleOpacity}
-    />
-  </>
-);
+  setDataState = (): void => {
+    // do nothing
+  },
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      <Wrapper>
+        <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">
+          {t('style')}
+        </Typography>
+        <Group>
+          {data.lineType.map(ele => (
+            <Item
+              key={ele.key}
+              selected={type === ele.key}
+              onClick={(): void => { setDataState({ type: ele.key }); }}
+            >
+              <Icon glyph={ele.icon} />
+            </Item>
+          ))}
+        </Group>
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+    </>
+  );
+};
 
 export default HighlightOption;

+ 18 - 6
components/Icon/data.ts

@@ -31,7 +31,8 @@ import Highlight from '../../public/icons/sidebar/Highlight.svg';
 import Freehand from '../../public/icons/sidebar/Freehand.svg';
 import Markpen from '../../public/icons/sidebar/markpen.svg';
 import Text from '../../public/icons/sidebar/Text.svg';
-import StickyNote from '../../public/icons/sidebar/StickyNote.svg';
+import StickyNote1 from '../../public/icons/sidebar/StickyNote.svg';
+import StickyNote2 from '../../public/icons/annotation/StickyNote02.svg';
 import Shape from '../../public/icons/sidebar/Shape.svg';
 import CreateForm from '../../public/icons/sidebar/CreatForm00.svg';
 import AddImage from '../../public/icons/sidebar/AddImage00.svg';
@@ -55,7 +56,7 @@ import AlignLeftActive from '../../public/icons/sidebar/Alignleft01.svg';
 import AlignRight from '../../public/icons/sidebar/Alignright00.svg';
 import AlignRightActive from '../../public/icons/sidebar/Alignright01.svg';
 import Circle from '../../public/icons/sidebar/Circle.svg';
-import Rectangle from '../../public/icons/sidebar/Rectangle.svg';
+import Square from '../../public/icons/sidebar/Rectangle.svg';
 import Line from '../../public/icons/sidebar/Line.svg';
 import Arrow from '../../public/icons/sidebar/Arrow.svg';
 import Border from '../../public/icons/sidebar/Border.svg';
@@ -70,9 +71,11 @@ import Import from '../../public/icons/annotation/import.svg';
 import AnnotationExport from '../../public/icons/annotation/Export.svg';
 import ToolOpen from '../../public/icons/btn_ToolsOpen.svg';
 import ToolClose from '../../public/icons/btn_ToolsClose.svg';
-import Trash from '../../public/icons/toolbar/delete-00.svg';
+import Trash from '../../public/icons/selector/delete00.svg';
+import Trash2 from '../../public/icons/selector/scale_delete.svg';
 import RightArrow from '../../public/icons/right-arrow.svg';
 import Clear from '../../public/icons/sidebar/clear.svg';
+import None from '../../public/icons/sidebar/None.svg';
 
 const data: {[index: string]: any} = {
   close: {
@@ -138,7 +141,10 @@ const data: {[index: string]: any} = {
     Normal: Text,
   },
   'sticky-note': {
-    Normal: StickyNote,
+    Normal: StickyNote1,
+  },
+  'sticky-note-2': {
+    Normal: StickyNote2,
   },
   shape: {
     Normal: Shape,
@@ -200,8 +206,8 @@ const data: {[index: string]: any} = {
   circle: {
     Normal: Circle,
   },
-  rectangle: {
-    Normal: Rectangle,
+  square: {
+    Normal: Square,
   },
   line: {
     Normal: Line,
@@ -248,12 +254,18 @@ const data: {[index: string]: any} = {
   trash: {
     Normal: Trash,
   },
+  'trash-2': {
+    Normal: Trash2,
+  },
   'right-arrow': {
     Normal: RightArrow,
   },
   clear: {
     Normal: Clear,
   },
+  none: {
+    Normal: None,
+  },
 };
 
 export default data;

+ 7 - 2
components/Icon/index.tsx

@@ -13,15 +13,17 @@ type Props = {
   onBlur?: () => void;
   style?: {};
   isActive? : boolean;
+  isDisabled? : boolean;
 };
 
-const Icon: React.FunctionComponent<Props> = ({
+const Icon: React.FC<Props> = ({
   glyph,
   id = '',
   onClick,
   onBlur,
   style,
   isActive = false,
+  isDisabled = false,
 }: Props) => {
   const {
     Normal,
@@ -32,13 +34,16 @@ const Icon: React.FunctionComponent<Props> = ({
     <IconWrapper
       isHover={!!Hover}
       style={style}
+      isDisabled={isDisabled}
     >
       {Normal && <Normal data-status="normal" />}
       {Hover && <Hover data-status="hover" />}
       {isActive && Hover ? <Hover data-status="active" /> : null}
       <ClickZone
         id={id}
-        onMouseDown={onClick}
+        onMouseDown={isDisabled ? (): void => {
+          // do something
+        } : onClick}
         onBlur={onBlur}
       />
     </IconWrapper>

+ 62 - 33
components/Icon/styled.ts

@@ -1,48 +1,77 @@
 import styled, { css } from 'styled-components';
 
-export const IconWrapper = styled('div')<{isHover: boolean}>`
+export const IconWrapper = styled('div')<{isHover: boolean; isDisabled: boolean }>`
   position: relative;
   display: inline-flex;
-  cursor: pointer;
   outline: none;
   width: auto;
   height: auto;
   align-items: center;
   justify-content: center;
+  opacity: ${props => (props.isDisabled ? 0.7 : 1)};
 
-  [data-status='normal'] {
-    opacity: 1;
-    transition-delay: 100ms;
-  }
-  [data-status='hover'] {
-    transition-delay: 100ms;
-    opacity: 0;
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    margin: auto;
-  }
-  [data-status='active'] {
-    transition-delay: 100ms;
-    opacity: 1;
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    margin: auto;
-  }
+  ${props => (props.isDisabled ? css`
+    [data-status='normal'] {
+      opacity: 0.6;
+      cursor: default;
+    }
+    [data-status='hover'] {
+      opacity: 0;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      margin: auto;
+    }
+    [data-status='active'] {
+      opacity: 0;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      margin: auto;
+    }
+  ` : css`
+    cursor: pointer;
 
-  ${props => (props.isHover ? css`
-    :hover {
-      [data-status='hover'] {
-        opacity: 1;
-      }
-      [data-status='normal'] {
-        opacity: 0;
-      }
+    [data-status='normal'] {
+      opacity: 1;
+      transition-delay: 100ms;
     }
-  ` : null)}
+    [data-status='hover'] {
+      transition-delay: 100ms;
+      opacity: 0;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      margin: auto;
+    }
+    [data-status='active'] {
+      transition-delay: 100ms;
+      opacity: 1;
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      margin: auto;
+    }
+
+    ${props.isHover ? css`
+      :hover {
+        [data-status='hover'] {
+          opacity: 1;
+        }
+        [data-status='normal'] {
+          opacity: 0;
+        }
+      }
+    ` : null}
+  `)}
 `;
 
 export const ClickZone = styled.div`

+ 138 - 0
components/Ink/index.tsx

@@ -0,0 +1,138 @@
+import React from 'react';
+
+import {
+  AnnotationElementPropsType, PointType, HTMLCoordinateType, CoordType, UpdateData,
+} from '../../constants/type';
+import OuterRect from '../OuterRect';
+import {
+  svgPath, bezierCommand, controlPoint, line,
+} from '../../helpers/svgBezierCurve';
+
+import { AnnotationContainer } from '../../global/otherStyled';
+import { SVG } from './styled';
+
+const Ink: React.SFC<AnnotationElementPropsType> = ({
+  obj_attr: {
+    position,
+    bdcolor,
+    bdwidth = 0,
+    transparency,
+  },
+  isCollapse,
+  onUpdate,
+  viewport,
+  scale,
+  id,
+}: AnnotationElementPropsType) => {
+  const pointCalc = (_points: PointType[][], h: number, s: number): PointType[][] => {
+    const reducer = _points.reduce((acc: PointType[][], cur: PointType[]): PointType[][] => {
+      const p = cur.map((point: PointType) => ({
+        x: point.x * s,
+        y: h - point.y * s,
+      }));
+      acc.push(p);
+      return acc;
+    }, []);
+
+    return reducer;
+  };
+
+  const rectCalcWithPoint = (
+    pointsGroup: PointType[][], borderWidth: number,
+  ): HTMLCoordinateType => {
+    const xArray: number[] = [];
+    const yArray: number[] = [];
+    pointsGroup[0].forEach((point: PointType) => {
+      xArray.push(point.x);
+      yArray.push(point.y);
+    });
+    const top = Math.min(...yArray);
+    const left = Math.min(...xArray);
+    const bottom = Math.max(...yArray);
+    const right = Math.max(...xArray);
+    return {
+      top,
+      left,
+      width: right - left + borderWidth,
+      height: bottom - top + borderWidth,
+    };
+  };
+
+  const borderWidth = bdwidth * scale;
+  const annotPoints = pointCalc(position as PointType[][], viewport.height, scale);
+  const annotRect = rectCalcWithPoint(annotPoints, borderWidth);
+
+  const scaleOrMoveCalc = (
+    rect: HTMLCoordinateType, updatePoints: (data: UpdateData) => void,
+  ) => ({
+    top, left, width = 0, height = 0,
+  }: CoordType): void => {
+    const xScaleRate = width / rect.width;
+    const yScaleRate = height / rect.height;
+    const xDistance = left - rect.left;
+    const yDistance = rect.top - top;
+
+    const newPosition = (position as PointType[][])[0].map(ele => ({
+      x: (ele.x + xDistance) * (xScaleRate || 1),
+      y: (ele.y + yDistance) * (yScaleRate || 1),
+    }));
+
+    updatePoints({
+      position: [newPosition],
+    });
+  };
+
+  const handleScaleOrMove = scaleOrMoveCalc(annotRect, onUpdate);
+
+  const calcViewBox = ({
+    top, left, width, height,
+  }: HTMLCoordinateType,
+  _borderWidth: number): string => `
+    ${left - _borderWidth / 2} ${top - _borderWidth / 2} ${width} ${height}
+  `;
+
+  return (
+    <>
+      <AnnotationContainer
+        top={`${annotRect.top}px`}
+        left={`${annotRect.left}px`}
+        width={`${annotRect.width}px`}
+        height={`${annotRect.height}px`}
+      >
+        <SVG
+          viewBox={calcViewBox(annotRect, bdwidth * scale)}
+        >
+          {
+            annotPoints.map((ele: PointType[], index: number) => {
+              const key = `${id}_path_${index}`;
+              return (
+                <path
+                  key={key}
+                  d={svgPath(ele as PointType[], bezierCommand(controlPoint(line, 0.2)))}
+                  fill="none"
+                  stroke={bdcolor}
+                  strokeWidth={bdwidth * scale}
+                  strokeOpacity={transparency}
+                />
+              );
+            })
+          }
+        </SVG>
+      </AnnotationContainer>
+      {
+        !isCollapse ? (
+          <OuterRect
+            top={annotRect.top}
+            left={annotRect.left}
+            width={annotRect.width}
+            height={annotRect.height}
+            onMove={handleScaleOrMove}
+            onScale={handleScaleOrMove}
+          />
+        ) : ''
+      }
+    </>
+  );
+};
+
+export default Ink;

+ 12 - 0
components/Ink/styled.ts

@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+export const SVG = styled.svg`
+  pointer-events: none;
+  user-select: none;
+  touch-action: none;
+`;
+
+export const PolyLine = styled('polyline')<{isCovered: boolean}>`
+  pointer-events: visiblepainted;
+  cursor: ${props => (props.isCovered ? 'pointer' : 'default')};
+`;

+ 92 - 0
components/InkOption/index.tsx

@@ -0,0 +1,92 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { OptionPropsType } from '../../constants/type';
+// import Icon from '../Icon';
+// import Button from '../Button';
+// import Typography from '../Typography';
+import SliderWithTitle from '../SliderWithTitle';
+import ColorSelector from '../../containers/ColorSelector';
+
+import {
+  Wrapper,
+  // Group,
+  // Item,
+} from '../../global/toolStyled';
+
+const FreehandOption: React.SFC<OptionPropsType> = ({
+  // type,
+  color,
+  opacity,
+  width,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      {/* <Wrapper>
+        <Typography variant="subtitle" style={{ marginTop: '4px' }} align="left">
+          {t('style')}
+        </Typography>
+        <Group>
+          <Item
+            size="small"
+            selected={type === 'pen'}
+            onClick={(): void => { setDataState({ type: 'pen' }); }}
+          >
+            <Icon glyph="freehand" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="eraser" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="redo" />
+          </Item>
+          <Item size="small">
+            <Icon glyph="undo" />
+          </Item>
+        </Group>
+      </Wrapper> */}
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('brushSize')}
+          value={width}
+          tips={`${width} pt`}
+          onSlide={(val: number): void => { setDataState({ width: val }); }}
+          maximum={40}
+        />
+      </Wrapper>
+      {/* <Wrapper>
+        <Group style={{ marginTop: '20px', paddingRight: '40px' }}>
+          <Button
+            appearance="danger-hollow"
+            shouldFitContainer
+          >
+            <Icon glyph="clear" style={{ marginRight: '5px' }} />
+            {t('clear')}
+          </Button>
+        </Group>
+      </Wrapper> */}
+    </>
+  );
+};
+
+export default FreehandOption;

+ 169 - 0
components/Line/index.tsx

@@ -0,0 +1,169 @@
+import React from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import Outer from '../OuterRectForLine';
+import {
+  AnnotationElementPropsType, LinePositionType, HTMLCoordinateType,
+} from '../../constants/type';
+import { parsePositionForBackend } from '../../helpers/position';
+
+import { AnnotationContainer } from '../../global/otherStyled';
+
+const Note: React.SFC<AnnotationElementPropsType> = ({
+  obj_type,
+  obj_attr,
+  scale,
+  viewport,
+  isCollapse,
+  onUpdate,
+}: AnnotationElementPropsType) => {
+  const {
+    bdcolor = '',
+    bdwidth = 0,
+    transparency = 0,
+    position,
+    is_arrow,
+  } = obj_attr;
+  const uuid = uuidv4();
+
+  const getActualPoint = (
+    { start, end }: LinePositionType, h: number, s: number,
+  ): LinePositionType => {
+    const x1 = start.x * scale;
+    const y1 = h - start.y * s;
+    const x2 = end.x * scale;
+    const y2 = h - end.y * s;
+    return {
+      start: {
+        x: x1,
+        y: y1,
+      },
+      end: {
+        x: x2,
+        y: y2,
+      },
+    };
+  };
+
+  const pointCalc = (
+    { start, end }: LinePositionType,
+    borderWidth: number,
+  ): LinePositionType => ({
+    start: {
+      x: start.x + borderWidth,
+      y: start.y + borderWidth,
+    },
+    end: {
+      x: end.x + borderWidth,
+      y: end.y + borderWidth,
+    },
+  });
+
+  const rectCalc = (
+    { start, end }: LinePositionType, h: number, s: number,
+  ): HTMLCoordinateType => {
+    const startY = h - start.y * s;
+    const endY = h - end.y * s;
+
+    return {
+      top: startY > endY ? endY : startY,
+      left: start.x > end.x ? end.x * s : start.x * s,
+      width: Math.abs((end.x - start.x) * s),
+      height: Math.abs(endY - startY),
+    };
+  };
+
+  const actualbdwidth = bdwidth * scale;
+  const annotRect = rectCalc(position as LinePositionType, viewport.height, scale);
+  const { start, end } = getActualPoint(position as LinePositionType, viewport.height, scale);
+  const {
+    start: completeStart,
+    end: completeEnd,
+  } = pointCalc({ start, end } as LinePositionType, actualbdwidth);
+
+  const actualWidth = annotRect.width + actualbdwidth + 15;
+  const actualHeight = annotRect.height + actualbdwidth + 15;
+
+  const handleMove = ({ start: moveStart, end: moveEnd }: LinePositionType): void => {
+    const newPosition = {
+      start: {
+        x: moveStart.x, y: moveStart.y,
+      },
+      end: {
+        x: moveEnd.x, y: moveEnd.y,
+      },
+    };
+
+    onUpdate({
+      position: parsePositionForBackend(obj_type, newPosition, viewport.height, scale),
+    });
+  };
+
+  return (
+    <>
+      <AnnotationContainer
+        top={`${annotRect.top}px`}
+        left={`${annotRect.left}px`}
+        width={`${actualWidth}px`}
+        height={`${actualHeight}px`}
+      >
+        <svg
+          width={`${actualWidth}px`}
+          height={`${actualHeight}px`}
+          viewBox={`${annotRect.left} ${annotRect.top} ${actualWidth} ${actualHeight}`}
+        >
+          {
+            is_arrow ? (
+              <defs>
+                <marker
+                  id={uuid}
+                  markerWidth={actualbdwidth * 2}
+                  markerHeight={actualbdwidth * 3}
+                  refX={3}
+                  refY={2}
+                  orient="auto"
+                  markerUnits="strokeWidth"
+                >
+                  <polyline
+                    points="0.25,0.5 3.25,2 0.25,3.5 3.25,2 -0.25,2"
+                    stroke={bdcolor}
+                    strokeWidth={1}
+                    fill="none"
+                    strokeDasharray={100}
+                  />
+                </marker>
+              </defs>
+            ) : null
+          }
+          <line
+            x1={completeStart.x}
+            y1={completeStart.y}
+            x2={completeEnd.x}
+            y2={completeEnd.y}
+            stroke={bdcolor}
+            strokeWidth={actualbdwidth}
+            strokeOpacity={transparency}
+            markerEnd={is_arrow ? `url(#${uuid})` : ''}
+          />
+        </svg>
+      </AnnotationContainer>
+      {
+        !isCollapse ? (
+          <Outer
+            top={annotRect.top}
+            left={annotRect.left}
+            width={actualWidth}
+            height={actualHeight}
+            start={start}
+            end={end}
+            completeStart={completeStart}
+            completeEnd={completeEnd}
+            onMove={handleMove}
+          />
+        ) : ''
+      }
+    </>
+  );
+};
+
+export default Note;

+ 2 - 2
components/Markup/index.tsx

@@ -10,13 +10,13 @@ type Props = {
   isCovered?: boolean;
 };
 
-const index = ({
+const index: React.SFC<Props> = ({
   position,
   bdcolor,
   opacity,
   markupType,
   isCovered,
-}: Props): React.ReactElement => (
+}: Props) => (
   <Markup
     position={position}
     bdcolor={bdcolor}

+ 1 - 1
components/Modal/index.tsx

@@ -9,7 +9,7 @@ type Props = {
   hideBackdrop?: boolean;
 };
 
-const Modal: React.FunctionComponent<Props> = ({
+const Modal: React.FC<Props> = ({
   children,
   hideBackdrop = false,
 }: Props) => (

+ 4 - 4
components/Navbar/data.ts

@@ -12,10 +12,10 @@ export default {
       key: 'nav-thumbnails',
       content: 'thumbnails',
     },
-    {
-      key: 'nav-print',
-      content: 'print',
-    },
+    // {
+    //   key: 'nav-print',
+    //   content: 'print',
+    // },
     {
       key: 'nav-export',
       content: 'export',

+ 4 - 2
components/Navbar/index.tsx

@@ -13,18 +13,19 @@ type Props = {
   children: React.ReactNode;
   displayMode: string;
   fileName: string;
+  isSaving: boolean;
 };
 
-const Navbar: React.FunctionComponent<Props> = ({
+const Navbar: React.FC<Props> = ({
   onClick,
   navbarState,
   children,
   displayMode,
   fileName,
+  isSaving,
 }: Props) => (
   <Container isHidden={displayMode === 'full'}>
     <Typography variant="title">{fileName}</Typography>
-    {/* <Icon glyph="edit" /> */}
     <Separator />
     {
       data.btnGroup.map(ele => (
@@ -32,6 +33,7 @@ const Navbar: React.FunctionComponent<Props> = ({
           key={ele.key}
           glyph={ele.content}
           isActive={navbarState === ele.content}
+          isDisabled={!!(isSaving && ele.key === 'nav-export')}
           onClick={(): void => { onClick(ele.content); }}
         />
       ))

+ 56 - 0
components/OuterRect/data.ts

@@ -0,0 +1,56 @@
+import { CircleType } from '../../constants/type';
+
+const RADIUS = 6;
+
+const generateCirclesData = (width: number, height: number): Array<CircleType> => ([
+  {
+    direction: 'top-left',
+    cx: RADIUS,
+    cy: RADIUS,
+    r: RADIUS,
+  },
+  {
+    direction: 'left',
+    cx: RADIUS,
+    cy: height / 2 + 12,
+    r: RADIUS,
+  },
+  {
+    direction: 'bottom-left',
+    cx: RADIUS,
+    cy: height + 18,
+    r: RADIUS,
+  },
+  {
+    direction: 'bottom',
+    cx: width / 2 + 12,
+    cy: height + 18,
+    r: RADIUS,
+  },
+  {
+    direction: 'bottom-right',
+    cx: width + 18,
+    cy: height + 18,
+    r: RADIUS,
+  },
+  {
+    direction: 'right',
+    cx: width + 18,
+    cy: height / 2 + 12,
+    r: RADIUS,
+  },
+  {
+    direction: 'top-right',
+    cx: width + 18,
+    cy: RADIUS,
+    r: RADIUS,
+  },
+  {
+    direction: 'top',
+    cx: width / 2 + 12,
+    cy: RADIUS,
+    r: RADIUS,
+  },
+]);
+
+export default generateCirclesData;

+ 176 - 0
components/OuterRect/index.tsx

@@ -0,0 +1,176 @@
+import React, { useEffect, useState } from 'react';
+
+import { color } from '../../constants/style';
+import useCursorPosition from '../../hooks/useCursorPosition';
+import { getAbsoluteCoordinate } from '../../helpers/position';
+import { calcDragAndDropScale } from '../../helpers/utility';
+import { CoordType, PointType, CircleType } from '../../constants/type';
+
+import generateCirclesData from './data';
+import { SVG, Rect, Circle } from './styled';
+
+type Props = {
+  left: number;
+  top: number;
+  width: number;
+  height: number;
+  onMove?: (moveCoord: CoordType) => void;
+  onScale?: (scaleCoord: CoordType) => void;
+  onClick?: (event: React.MouseEvent) => void;
+  onDoubleClick?: () => void;
+};
+
+type ObjPositionType = {
+  top: number;
+  left: number;
+  width: number;
+  height: number;
+  operator: string;
+  clickX: number;
+  clickY: number;
+};
+
+const initState = {
+  top: 0,
+  left: 0,
+  width: 0,
+  height: 0,
+  operator: '',
+  clickX: 0,
+  clickY: 0,
+};
+
+const index: React.FC<Props> = ({
+  left,
+  top,
+  width,
+  height,
+  onMove,
+  onScale,
+  onClick,
+  onDoubleClick,
+}: Props) => {
+  const data = generateCirclesData(width, height);
+  const [state, setState] = useState(initState);
+  const [cursorPosition, setRef] = useCursorPosition(40);
+
+  const handleMouseDown = (e: React.MouseEvent | React.TouchEvent): void => {
+    e.preventDefault();
+    const operatorId = (e.target as HTMLElement).getAttribute('data-id') as string;
+    const coord = getAbsoluteCoordinate(document.body, e);
+
+    setRef(document.body);
+    setState({
+      top,
+      left,
+      width,
+      height,
+      operator: operatorId,
+      clickX: coord.x,
+      clickY: coord.y,
+    });
+  };
+
+  const handleMouseUp = (): void => {
+    setRef(null);
+    setState(initState);
+  };
+
+  const calcMoveResult = (
+    currentPosition: PointType,
+    startPosition: PointType,
+    objPosition: ObjPositionType,
+  ): CoordType => ({
+    left: currentPosition.x - (startPosition.x - objPosition.left),
+    top: currentPosition.y - (startPosition.y - objPosition.top),
+  });
+
+  const calcScaleResult = (
+    currentPosition: PointType,
+    objPosition: ObjPositionType,
+  ): CoordType => {
+    const scaleData = calcDragAndDropScale({
+      ...objPosition,
+      moveX: currentPosition.x || 0,
+      moveY: currentPosition.y || 0,
+    });
+    const scaleWidth = scaleData.width || 0;
+    const scaleHeight = scaleData.height || 0;
+
+    const maxTop = objPosition.top + objPosition.height;
+    const maxLeft = objPosition.left + objPosition.width;
+    scaleData.left = scaleData.left > maxLeft ? maxLeft : scaleData.left;
+    scaleData.top = scaleData.top > maxTop ? maxTop : scaleData.top;
+    scaleData.width = scaleWidth > 0 ? scaleWidth : 0;
+    scaleData.height = scaleHeight > 0 ? scaleHeight : 0;
+    return scaleData;
+  };
+
+  useEffect(() => {
+    if (cursorPosition.x && cursorPosition.y && state.clickX) {
+      if (state.operator === 'move' && onMove) {
+        onMove(calcMoveResult(
+          { x: cursorPosition.x, y: cursorPosition.y },
+          { x: state.clickX, y: state.clickY },
+          state,
+        ));
+      } else if (onScale) {
+        onScale(calcScaleResult(
+          {
+            x: cursorPosition.x,
+            y: cursorPosition.y,
+          },
+          state,
+        ));
+      }
+    }
+  }, [cursorPosition, state]);
+
+  useEffect(() => {
+    window.addEventListener('mouseup', handleMouseUp);
+    window.addEventListener('touchend', handleMouseUp);
+
+    return (): void => {
+      window.removeEventListener('mouseup', handleMouseUp);
+      window.removeEventListener('touchend', handleMouseUp);
+    };
+  }, []);
+
+  return (
+    <SVG
+      style={{
+        left: `${left - 12}px`,
+        top: `${top - 12}px`,
+        width: `${width + 24}px`,
+        height: `${height + 24}px`,
+      }}
+      onClick={onClick}
+      onDoubleClick={onDoubleClick}
+    >
+      <Rect
+        x={6}
+        y={6}
+        width={width + 12}
+        height={height + 12}
+        stroke={onScale ? color.primary : 'transparency'}
+        onMouseDown={handleMouseDown}
+        onTouchStart={handleMouseDown}
+        data-id="move"
+      />
+      {
+        onScale && data.map((attr: CircleType) => (
+          <Circle
+            key={attr.direction}
+            data-id={attr.direction}
+            onMouseDown={handleMouseDown}
+            onTouchStart={handleMouseDown}
+            fill={color.primary}
+            {...attr}
+          />
+        ))
+      }
+    </SVG>
+  );
+};
+
+export default index;

+ 30 - 0
components/OuterRect/styled.ts

@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+
+export const SVG = styled.svg`
+  position: absolute;
+  cursor: pointer;
+  z-index: 10;
+  outline: none;
+`;
+
+export const Rect = styled.rect`
+  cursor: move;
+  fill: #ffffff;
+  fill-opacity: 0;
+  stroke-width: 3;
+`;
+
+const arrowDirection: {[index: string]: any} = {
+  'top-right': 'ne-resize',
+  top: 'n-resize',
+  'top-left': 'nw-resize',
+  left: 'w-resize',
+  'bottom-left': 'sw-resize',
+  bottom: 's-resize',
+  'bottom-right': 'se-resize',
+  right: 'e-resize',
+};
+
+export const Circle = styled('circle')<{direction: string}>`
+  cursor: ${props => (arrowDirection[props.direction])};
+`;

+ 138 - 0
components/OuterRectForLine/index.tsx

@@ -0,0 +1,138 @@
+import React, { useEffect, useState } from 'react';
+
+import { color } from '../../constants/style';
+import useCursorPosition from '../../hooks/useCursorPosition';
+import { getAbsoluteCoordinate } from '../../helpers/position';
+import { LinePositionType, PointType } from '../../constants/type';
+
+import { SVG, Circle } from './styled';
+
+type Props = LinePositionType & {
+  top: number;
+  left: number;
+  width: number;
+  height: number;
+  onMove: (position: LinePositionType) => void;
+  onClick?: (event: React.MouseEvent) => void;
+  completeStart: PointType;
+  completeEnd: PointType;
+};
+
+const MARGIN_DISTANCE = 18;
+
+const initState = {
+  start: { x: 0, y: 0 },
+  end: { x: 0, y: 0 },
+  operator: '',
+  clickX: 0,
+  clickY: 0,
+};
+
+const index: React.FC<Props> = ({
+  top,
+  left,
+  width,
+  height,
+  start,
+  end,
+  onMove,
+  onClick,
+  completeStart,
+  completeEnd,
+}: Props) => {
+  const [state, setState] = useState(initState);
+  const [cursorPosition, setRef] = useCursorPosition();
+
+  const handleMouseDown = (e: React.MouseEvent): void => {
+    const operatorId = (e.target as HTMLElement).getAttribute('data-id') as string;
+    const coord = getAbsoluteCoordinate(document.body, e);
+
+    setRef(document.body);
+    setState({
+      start,
+      end,
+      operator: operatorId,
+      clickX: coord.x,
+      clickY: coord.y,
+    });
+  };
+
+  const handleMouseUp = (): void => {
+    setRef(null);
+    setState(initState);
+  };
+
+  useEffect(() => {
+    if (cursorPosition.x && cursorPosition.y) {
+      if (state.operator === 'start') {
+        const x1 = cursorPosition.x - (state.clickX - state.start.x);
+        const y1 = cursorPosition.y - (state.clickY - state.start.y);
+
+        onMove({
+          start: { x: x1, y: y1 },
+          end,
+        });
+      } else if (state.operator === 'end') {
+        const x2 = cursorPosition.x - (state.clickX - state.end.x);
+        const y2 = cursorPosition.y - (state.clickY - state.end.y);
+
+        onMove({
+          start,
+          end: { x: x2, y: y2 },
+        });
+      } else if (state.operator === 'move') {
+        const x1 = cursorPosition.x - (state.clickX - state.start.x);
+        const y1 = cursorPosition.y - (state.clickY - state.start.y);
+        const x2 = cursorPosition.x - (state.clickX - state.end.x);
+        const y2 = cursorPosition.y - (state.clickY - state.end.y);
+
+        onMove({
+          start: { x: x1, y: y1 },
+          end: { x: x2, y: y2 },
+        });
+      }
+    }
+  }, [cursorPosition, state]);
+
+  useEffect(() => {
+    window.addEventListener('mouseup', handleMouseUp);
+
+    return (): void => {
+      window.removeEventListener('mouseup', handleMouseUp);
+    };
+  }, []);
+
+  return (
+    <SVG
+      data-id="move"
+      viewBox={`${left} ${top} ${width + 10} ${height + 10}`}
+      style={{
+        left: `${left - MARGIN_DISTANCE}px`,
+        top: `${top - MARGIN_DISTANCE}px`,
+        width: `${width + 10}px`,
+        height: `${height + 10}px`,
+      }}
+      onClick={onClick}
+      onMouseDown={handleMouseDown}
+    >
+      <Circle
+        data-id="start"
+        onMouseDown={handleMouseDown}
+        fill={color.primary}
+        cx={completeStart.x + MARGIN_DISTANCE}
+        cy={completeStart.y + MARGIN_DISTANCE}
+        r={6}
+      />
+      <Circle
+        data-id="end"
+        onMouseDown={handleMouseDown}
+        fill={color.primary}
+        cx={completeEnd.x + MARGIN_DISTANCE}
+        cy={completeEnd.y + MARGIN_DISTANCE}
+        r={6}
+      />
+    </SVG>
+  );
+};
+
+export default index;

+ 12 - 0
components/OuterRectForLine/styled.ts

@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+
+export const SVG = styled.svg`
+  position: absolute;
+  cursor: pointer;
+  z-index: 10;
+  touch-action: none;
+`;
+
+export const Circle = styled.circle`
+  cursor: all-scroll;
+`;

+ 25 - 7
components/Page/index.tsx

@@ -2,29 +2,41 @@ import React, {
   useEffect, useRef,
 } from 'react';
 
+import Watermark from '../Watermark';
 import { renderPdfPage } from '../../helpers/pdf';
-import { TypeRenderingStates, ViewportType } from '../../constants/type';
+import {
+  RenderingStateType, ViewportType, WatermarkType,
+} from '../../constants/type';
 
 import {
-  PageWrapper, PdfCanvas, AnnotationLayer, TextLayer, DrawingLayer, LoadingLayer,
+  PageWrapper,
+  PdfCanvas,
+  AnnotationLayer,
+  TextLayer,
+  WatermarkLayer,
 } from './styled';
 
 type Props = {
   pageNum: number;
-  renderingState?: TypeRenderingStates;
+  renderingState?: RenderingStateType;
   getPage?: () => void;
   viewport: ViewportType;
   rotation?: number;
-  annotations?: React.ReactElement[];
+  scale: number;
+  annotations?: React.ReactNode[];
+  drawing?: React.ReactNode[];
+  watermark?: WatermarkType;
 };
 
-const PageView: React.FunctionComponent<Props> = ({
+const PageView: React.FC<Props> = ({
   pageNum,
   getPage,
   viewport,
   renderingState = 'PAUSED',
   rotation,
+  scale,
   annotations = [],
+  watermark = {},
 }: Props) => {
   const rootEle = useRef<HTMLDivElement | null>(null);
   let pdfPage: any = null;
@@ -67,13 +79,19 @@ const PageView: React.FunctionComponent<Props> = ({
       {
         renderingState === 'LOADING' ? (
           <>
-            <LoadingLayer />
             <TextLayer data-id="text-layer" />
           </>
         ) : (
           <>
             <PdfCanvas />
-            <DrawingLayer data-id="drawing-layer" />
+            {watermark.text || watermark.imagepath ? (
+              <WatermarkLayer>
+                <Watermark
+                  viewScale={scale}
+                  {...watermark}
+                />
+              </WatermarkLayer>
+            ) : ''}
             <TextLayer data-id="text-layer" />
             <AnnotationLayer data-id="annotation-layer">
               {annotations}

+ 6 - 5
components/Page/styled.ts

@@ -50,12 +50,13 @@ export const AnnotationLayer = styled.div`
   top: 0;
 `;
 
-export const DrawingLayer = styled.svg`
+export const WatermarkLayer = styled.div`
   position: absolute;
   left: 0;
   top: 0;
-  width: 100%;
-  height: 100%;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 `;
-
-export const LoadingLayer = styled.div``;

+ 8 - 4
components/Pagination/index.tsx

@@ -1,4 +1,5 @@
 import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
 
 import {
   Container, Text, Input, ArrowButton,
@@ -10,11 +11,14 @@ type Props = {
   onChange: (page: number) => void;
 };
 
-const Pagination: React.FunctionComponent<Props> = ({
+const Pagination: React.FC<Props> = ({
   currentPage = 1,
   totalPage = 1,
-  onChange,
+  onChange = (): void => {
+    // do nothing
+  },
 }: Props) => {
+  const { t } = useTranslation('toolbar');
   const [inputValue, setInputValue] = useState(currentPage);
 
   const handleRightClick = (): void => {
@@ -53,7 +57,7 @@ const Pagination: React.FunctionComponent<Props> = ({
 
   return (
     <Container>
-      <Text>Page</Text>
+      <Text>{t('page')}</Text>
       <ArrowButton onClick={handleLeftClick} variant="left" />
       <Input
         type="tel"
@@ -63,7 +67,7 @@ const Pagination: React.FunctionComponent<Props> = ({
       />
       <ArrowButton onClick={handleRightClick} variant="right" />
       <Text>
-        of
+        {t('of')}
         {' '}
         {totalPage}
       </Text>

+ 1 - 1
components/PdfSkeleton/index.tsx

@@ -5,7 +5,7 @@ import Skeleton from '../Skeleton';
 
 import { Container } from './styled';
 
-const PdfSkeleton: React.FunctionComponent = () => (
+const PdfSkeleton: React.FC = () => (
   <Container>
     <Skeleton variant="rect" width="50%" height="36px" />
     <Skeleton variant="rect" width="45%" height="12px" />

+ 1 - 1
components/Portal/index.tsx

@@ -14,7 +14,7 @@ type Props = {
   children: React.ReactNode;
 };
 
-const Portal: React.FunctionComponent<Props> = ({
+const Portal: React.FC<Props> = ({
   zIndex = 0,
   children = '',
 }) => {

+ 10 - 6
components/Search/index.tsx

@@ -1,4 +1,5 @@
 import React, { useRef, useEffect, MutableRefObject } from 'react';
+import { useTranslation } from 'react-i18next';
 
 import Button from '../Button';
 import Portal from '../Portal';
@@ -9,7 +10,7 @@ import {
 
 type Props = {
   matchesTotal: number;
-  matchIndex: number;
+  current: number;
   onEnter: (val: string) => void;
   onPrev: () => void;
   onNext: () => void;
@@ -17,15 +18,16 @@ type Props = {
   close: () => void;
 };
 
-const Search: React.FunctionComponent<Props> = ({
+const Search: React.FC<Props> = ({
   matchesTotal = 0,
-  matchIndex = 1,
+  current = 0,
   onPrev,
   onNext,
   onEnter,
   isActive = false,
   close,
 }: Props) => {
+  const { t } = useTranslation('toolbar');
   const inputRef = useRef(null) as MutableRefObject<any>;
 
   const handleKeyDown = (e: React.KeyboardEvent): void => {
@@ -44,14 +46,16 @@ const Search: React.FunctionComponent<Props> = ({
     <Portal>
       <Wrapper open={isActive}>
         <InputWrapper>
-          <Input ref={inputRef} onKeyDown={handleKeyDown} />
+          <Input ref={inputRef} placeholder={t('searchDocument')} onKeyDown={handleKeyDown} />
           <ResultInfo>
-            {matchesTotal ? `${matchIndex + 1} / ${matchesTotal}` : ''}
+            {matchesTotal ? `${current} / ${matchesTotal}` : ''}
           </ResultInfo>
         </InputWrapper>
         <ArrowButton variant="top" onClick={onPrev} />
         <ArrowButton variant="bottom" onClick={onNext} />
-        <Button appearance="primary" style={{ marginLeft: '16px' }} onClick={close}>Close</Button>
+        <Button appearance="primary" style={{ marginLeft: '16px' }} onClick={close}>
+          {t('close')}
+        </Button>
       </Wrapper>
     </Portal>
   );

+ 3 - 5
components/SelectBox/index.tsx

@@ -18,17 +18,15 @@ type Props = {
   options: SelectOptionType[];
   defaultValue?: React.ReactNode;
   isDivide?: boolean;
-  width?: string;
   useInput?: boolean;
   style?: {};
 };
 
-const SelectBox: React.FunctionComponent<Props> = ({
+const SelectBox: React.FC<Props> = ({
   onChange,
   options,
   defaultValue,
   isDivide = false,
-  width = 'auto',
   useInput = false,
   style,
 }: Props) => {
@@ -62,7 +60,7 @@ const SelectBox: React.FunctionComponent<Props> = ({
       const param = {
         key: '',
         content: '',
-        value: parseInt(value as string, 10),
+        child: parseInt(value as string, 10),
       };
       onChange(param);
       setCollapse(true);
@@ -88,7 +86,7 @@ const SelectBox: React.FunctionComponent<Props> = ({
   });
 
   return (
-    <Container width={width} style={style}>
+    <Container style={style}>
       <Selected
         ref={selectRef}
         onMouseDown={handleClick}

+ 4 - 4
components/SelectBox/styled.ts

@@ -2,12 +2,11 @@
 import styled from 'styled-components';
 import { color } from '../../constants/style';
 
-export const Container = styled('div')<{width: string}>`
+export const Container = styled.div`
   position: relative;
   cursor: pointer;
   font-size: 12px;
   min-width: 74px;
-  width: ${props => props.width};
   display: inline-block;
 `;
 
@@ -18,16 +17,17 @@ export const Selected = styled.div`
   background-color: ${color.gray};
   height: 32px;
   border-radius: 4px;
-  padding: 0 6px 0 14px;
+  padding: 0 6px 0 10px;
   transition: background-color 200ms cubic-bezier(0.0, 0, 0.2, 1) 0ms;
   outline: none;
+  width: 100%;
 `;
 
 export const InputContent = styled.input`
   background-color: ${color.gray};
   border: none;
   outline: none;
-  width: 100%;
+  width: 50%;
 `;
 
 export const Content = styled.div`

+ 83 - 0
components/Shape/index.tsx

@@ -0,0 +1,83 @@
+import React from 'react';
+
+import OuterRect from '../OuterRect';
+import SvgShapeElement from '../SvgShapeElement';
+import {
+  AnnotationElementPropsType, PositionType, CoordType,
+} from '../../constants/type';
+import { rectCalc, parsePositionForBackend } from '../../helpers/position';
+
+import { AnnotationContainer } from '../../global/otherStyled';
+
+const Note: React.SFC<AnnotationElementPropsType> = ({
+  obj_type,
+  obj_attr: {
+    bdcolor = '',
+    fcolor = '',
+    bdwidth = 0,
+    transparency = 0,
+    ftransparency = 0,
+    position,
+  },
+  scale,
+  viewport,
+  isCollapse,
+  onUpdate,
+}: AnnotationElementPropsType) => {
+  const annotRect = rectCalc(position as PositionType, viewport.height, scale);
+
+  const handleScaleOrMove = ({
+    top, left, width = 0, height = 0,
+  }: CoordType): void => {
+    const newPosition = {
+      top,
+      left,
+      bottom: top + (height || annotRect.height),
+      right: left + (width || annotRect.width),
+    };
+
+    onUpdate({
+      position: parsePositionForBackend(obj_type, newPosition, viewport.height, scale),
+    });
+  };
+
+  const actualbdwidth = bdwidth * scale;
+  const actualWidth = annotRect.width;
+  const actualHeight = annotRect.height;
+
+  return (
+    <>
+      <AnnotationContainer
+        top={`${annotRect.top}px`}
+        left={`${annotRect.left}px`}
+        width={`${actualWidth}px`}
+        height={`${actualHeight}px`}
+      >
+        <SvgShapeElement
+          shape={obj_type}
+          width={actualWidth}
+          height={actualHeight}
+          bdcolor={bdcolor}
+          transparency={transparency}
+          bdwidth={actualbdwidth}
+          fcolor={fcolor}
+          ftransparency={ftransparency}
+        />
+      </AnnotationContainer>
+      {
+        !isCollapse ? (
+          <OuterRect
+            top={annotRect.top}
+            left={annotRect.left}
+            width={actualWidth}
+            height={actualHeight}
+            onMove={handleScaleOrMove}
+            onScale={handleScaleOrMove}
+          />
+        ) : ''
+      }
+    </>
+  );
+};
+
+export default Note;

+ 38 - 0
components/ShapeOption/data.tsx

@@ -0,0 +1,38 @@
+import React from 'react';
+import Icon from '../Icon';
+
+export const shapeOptions = [
+  {
+    key: 'circle',
+    content: <Icon glyph="circle" />,
+    child: '',
+  },
+  {
+    key: 'square',
+    content: <Icon glyph="square" />,
+    child: '',
+  },
+  {
+    key: 'line',
+    content: <Icon glyph="line" />,
+    child: '',
+  },
+  {
+    key: 'arrow',
+    content: <Icon glyph="arrow" />,
+    child: '',
+  },
+];
+
+export const typeOptions = [
+  {
+    key: 'border',
+    content: <Icon glyph="border" />,
+    child: '',
+  },
+  {
+    key: 'fill',
+    content: <Icon glyph="fill" />,
+    child: '',
+  },
+];

+ 82 - 0
components/ShapeOption/index.tsx

@@ -0,0 +1,82 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { OptionPropsType } from '../../constants/type';
+import Typography from '../Typography';
+import SelectBox from '../SelectBox';
+import SliderWithTitle from '../SliderWithTitle';
+import ColorSelector from '../../containers/ColorSelector';
+
+import {
+  Wrapper,
+} from '../../global/toolStyled';
+
+import {
+  shapeOptions, typeOptions,
+} from './data';
+
+const ShapeOption: React.SFC<OptionPropsType> = ({
+  shape,
+  color,
+  opacity,
+  width,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: OptionPropsType) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <>
+      <Wrapper>
+        <Typography
+          variant="subtitle"
+          style={{ marginTop: '8px' }}
+          align="left"
+        >
+          {t('style')}
+        </Typography>
+        <SelectBox
+          options={shapeOptions}
+          style={{ marginRight: '10px' }}
+          onChange={(item: Record<string, any>): void => { setDataState({ shape: item.key }); }}
+        />
+        {
+          shape !== 'line' && shape !== 'arrow' && (
+            <SelectBox
+              options={typeOptions}
+              onChange={(item: Record<string, any>): void => { setDataState({ type: item.key }); }}
+            />
+          )
+        }
+      </Wrapper>
+      <Wrapper>
+        <ColorSelector
+          title={t('color')}
+          mode="shape"
+          selectedColor={color}
+          onClick={(selected: string): void => { setDataState({ color: selected }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('opacity')}
+          value={opacity}
+          tips={`${opacity}%`}
+          onSlide={(val: number): void => { setDataState({ opacity: val }); }}
+        />
+      </Wrapper>
+      <Wrapper>
+        <SliderWithTitle
+          title={t('width')}
+          value={width}
+          tips={`${width}pt`}
+          onSlide={(val: number): void => { setDataState({ width: val }); }}
+          maximum={40}
+        />
+      </Wrapper>
+    </>
+  );
+};
+
+export default ShapeOption;

+ 0 - 92
components/ShapeTools/index.tsx

@@ -1,92 +0,0 @@
-import React from 'react';
-
-import Button from '../Button';
-import ExpansionPanel from '../ExpansionPanel';
-import Icon from '../Icon';
-import SelectBox from '../SelectBox';
-import Typography from '../Typography';
-import ColorSelect from '../ColorSelector';
-import Sliders from '../Sliders';
-
-import { Group, SliderWrapper } from '../../global/toolStyled';
-
-const shapeOptions = [
-  {
-    key: 'circle',
-    content: <Icon glyph="circle" />,
-    value: '',
-  },
-  {
-    key: 'rectangle',
-    content: <Icon glyph="rectangle" />,
-    value: '',
-  },
-  {
-    key: 'line',
-    content: <Icon glyph="line" />,
-    value: '',
-  },
-  {
-    key: 'arrow',
-    content: <Icon glyph="arrow" />,
-    value: '',
-  },
-];
-const typeOptions = [
-  {
-    key: 'border',
-    content: <Icon glyph="border" />,
-    value: '',
-  },
-  {
-    key: 'fill',
-    content: <Icon glyph="fill" />,
-    value: '',
-  },
-];
-
-type Props = {
-  isActive: boolean;
-  onClick: () => void;
-};
-
-const Shape: React.FunctionComponent<Props> = ({
-  isActive,
-  onClick,
-}: Props) => (
-  <ExpansionPanel
-    label={(
-      <Button
-        shouldFitContainer
-        align="left"
-        onClick={onClick}
-        isActive={isActive}
-      >
-        <Icon glyph="shape" style={{ marginRight: '10px' }} />
-        Shape
-      </Button>
-    )}
-    isActive={isActive}
-  >
-    <Typography>Shape</Typography>
-    <SelectBox options={shapeOptions} style={{ marginRight: '10px' }} />
-    <SelectBox options={typeOptions} />
-    <ColorSelect showTitle color="" onClick={(): void => {}} />
-    <Typography variant="subtitle" style={{ marginTop: '8px' }}>Opacity</Typography>
-    <Group>
-      <SliderWrapper>
-        <Sliders />
-      </SliderWrapper>
-      40%
-    </Group>
-    <Typography variant="subtitle" style={{ marginTop: '8px' }}>Width</Typography>
-    <Group>
-      <SliderWrapper>
-        <Sliders />
-      </SliderWrapper>
-      12pt
-    </Group>
-  </ExpansionPanel>
-);
-
-export default Shape;

+ 1 - 1
components/Skeleton/index.tsx

@@ -8,7 +8,7 @@ export type Props = {
   width?: number | string;
 };
 
-const Skeleton: React.FunctionComponent<Props> = ({
+const Skeleton: React.FC<Props> = ({
   variant = 'text',
   height = 'auto',
   width = 'auto',

+ 8 - 2
components/SliderWithTitle/index.tsx

@@ -7,17 +7,21 @@ import { Group, SliderWrapper } from '../../global/toolStyled';
 
 type Props = {
   title: string;
-  value: number;
+  defaultValue?: number;
+  value?: number;
   tips: string;
   onSlide: (value: number) => void;
+  minimum?: number;
   maximum?: number;
 };
 
 const index = ({
   title,
+  defaultValue,
   value,
   tips,
   onSlide,
+  minimum,
   maximum,
 }: Props): React.ReactElement => (
   <>
@@ -31,8 +35,10 @@ const index = ({
     <Group>
       <SliderWrapper>
         <Sliders
+          minimum={minimum}
           maximum={maximum}
-          defaultValue={value}
+          defaultValue={defaultValue}
+          value={value}
           onChange={onSlide}
         />
       </SliderWrapper>

+ 44 - 19
components/Sliders/index.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useEffect, useState, useRef, useCallback,
+  useEffect, useState, useRef,
 } from 'react';
 import { fromEvent } from 'rxjs';
 import {
@@ -12,30 +12,43 @@ import {
 
 type Props = {
   color?: 'primary' | 'secondary';
+  minimum?: number;
   maximum?: number;
   defaultValue?: number;
+  value?: number;
   disabled?: boolean;
   onChange?: (value: number) => void;
 };
 
-const Sliders: React.FunctionComponent<Props> = ({
+const Sliders: React.FC<Props> = ({
   defaultValue = 0,
+  value = 0,
   onChange,
+  // minimum = 0,
   maximum = 100,
 }: Props) => {
   const sliderRef = useRef<HTMLDivElement>(null);
   const [valueState, setValueState] = useState(defaultValue);
   const [isActive, setActive] = useState(false);
-  let subscription: any = null;
+  let mouseSubscription: any = null;
+  let touchSubscription: any = null;
 
-  const parseValueToPercent = (value: number): number => Math.floor(value * 100);
+  const parseValueToPercent = (val: number): number => Math.floor(val * 100);
 
-  const getFingerMoveValue = (event: MouseEvent | React.MouseEvent<HTMLElement>): void => {
+  const getFingerMoveValue = (event: any): void => {
     const { current: slider } = sliderRef;
+    let clientX = 0;
+
+    if (event.touches) {
+      const touches = event.touches[0];
+      ({ clientX } = touches);
+    } else {
+      ({ clientX } = event);
+    }
 
     if (slider) {
       const { width, left } = slider.getBoundingClientRect();
-      const moveDistance = event.clientX - left;
+      const moveDistance = clientX - left;
       const moveRate = moveDistance / width;
       let percent = parseValueToPercent(moveRate);
 
@@ -45,46 +58,58 @@ const Sliders: React.FunctionComponent<Props> = ({
         percent = 100;
       }
 
-      setValueState(percent);
       if (onChange) {
-        const value = Math.floor(percent * (maximum * 0.01));
-        onChange(value);
+        const val = Math.floor(percent * (maximum * 0.01));
+        onChange(val);
       }
     }
   };
 
-  const handleTouchMove = useCallback((event: any) => {
+  const handleTouchMove = (event: any): void => {
     event.preventDefault();
     getFingerMoveValue(event);
-  }, []);
+  };
 
-  const handleTouchEnd = useCallback((event: MouseEvent) => {
+  const handleTouchEnd = (event: MouseEvent): void => {
     event.preventDefault();
 
     setActive(false);
-    subscription.unsubscribe();
-  }, []);
+    mouseSubscription.unsubscribe();
+    touchSubscription.unsubscribe();
+  };
 
-  const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
+  const handleMouseDown = (
+    event: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>,
+  ): void => {
     event.preventDefault();
 
     getFingerMoveValue(event);
     setActive(true);
 
-    subscription = fromEvent(document.body, 'mousemove').pipe(throttleTime(35)).subscribe(handleTouchMove);
+    mouseSubscription = fromEvent(document.body, 'mousemove').pipe(throttleTime(35)).subscribe(handleTouchMove);
+    touchSubscription = fromEvent(document.body, 'touchmove').pipe(throttleTime(35)).subscribe(handleTouchMove);
     document.body.addEventListener('mouseup', handleTouchEnd);
-  }, []);
+  };
 
-  useEffect(() => {
-    const percent = parseValueToPercent(defaultValue / maximum);
+  const setPercent = (): void => {
+    const percent = parseValueToPercent((defaultValue || value) / maximum);
     setValueState(percent);
+  };
+
+  useEffect(() => {
+    setPercent();
   }, []);
 
+  useEffect(() => {
+    setPercent();
+  }, [value]);
+
   return (
     <OuterWrapper>
       <Wrapper
         ref={sliderRef}
         onMouseDown={handleMouseDown}
+        onTouchStart={handleMouseDown}
       >
         <Rail track={valueState} />
         <Track

+ 105 - 0
components/StickyNote/index.tsx

@@ -0,0 +1,105 @@
+import React from 'react';
+
+import {
+  AnnotationElementPropsType, PositionType, CoordType,
+} from '../../constants/type';
+import Icon from '../Icon';
+import OuterRect from '../OuterRect';
+import { AnnotationContainer } from '../../global/otherStyled';
+import { rectCalc } from '../../helpers/position';
+
+import { TextArea, TextAreaContainer, TrashCan } from './styled';
+
+const StickyNote: React.SFC<AnnotationElementPropsType> = ({
+  viewport,
+  scale,
+  obj_attr: {
+    position,
+    content,
+  },
+  onEdit,
+  isEdit,
+  onBlur,
+  onUpdate,
+  onDelete,
+}: AnnotationElementPropsType) => {
+  const annotRect = rectCalc(position as PositionType, viewport.height, scale);
+
+  const handleClick = (event: React.MouseEvent): void => {
+    event.preventDefault();
+    onEdit();
+  };
+
+  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>): void => {
+    const textValue = event.currentTarget.value;
+
+    onUpdate({
+      content: textValue,
+    });
+  };
+
+  const handleMove = (moveCoord: CoordType): void => {
+    const top = (viewport.height - moveCoord.top) / scale;
+    const left = moveCoord.left / scale;
+
+    onUpdate({
+      position: {
+        top,
+        left,
+        bottom: top,
+        right: left,
+      },
+    });
+  };
+
+  return (
+    <>
+      <AnnotationContainer
+        top={`${annotRect.top}px`}
+        left={`${annotRect.left}px`}
+        width="auto"
+        height="auto"
+      >
+        <Icon
+          glyph="sticky-note-2"
+          style={{ width: '30px' }}
+          onClick={handleClick}
+        />
+      </AnnotationContainer>
+      {
+        isEdit ? (
+          <TextAreaContainer
+            top={`${annotRect.top + 36}px`}
+            left={`${annotRect.left - 106}px`}
+          >
+            <TextArea
+              autoFocus
+              defaultValue={content}
+              onChange={handleChange}
+            />
+            <TrashCan>
+              <Icon
+                glyph="trash-2"
+                onClick={onDelete}
+              />
+            </TrashCan>
+          </TextAreaContainer>
+        ) : null
+      }
+      {
+        isEdit ? (
+          <OuterRect
+            top={annotRect.top}
+            left={annotRect.left}
+            width={25}
+            height={25}
+            onMove={handleMove}
+            onClick={onBlur}
+          />
+        ) : null
+      }
+    </>
+  );
+};
+
+export default StickyNote;

+ 25 - 0
components/StickyNote/styled.ts

@@ -0,0 +1,25 @@
+import styled from 'styled-components';
+
+export const TextAreaContainer = styled('div')<{top: string; left: string}>`
+  position: absolute;
+  top: ${props => props.top};
+  left: ${props => props.left};
+  z-index: 10;
+`;
+
+export const TextArea = styled.textarea`
+  width: 244px;
+  height: 200px;
+  padding: 16px;
+  box-sizing: border-box;
+  background-color: #fceb9b;
+  border-radius: 5px;
+  border: none;
+  outline: none;
+`;
+
+export const TrashCan = styled.div`
+  position: absolute;
+  bottom: 6px;
+  right: 6px;
+`;

+ 56 - 0
components/SvgShapeElement/index.tsx

@@ -0,0 +1,56 @@
+import React from 'react';
+
+type Props = {
+  shape: string;
+  width: number;
+  height: number;
+  bdcolor: string;
+  bdwidth: number;
+  transparency: number;
+  fcolor?: string;
+  ftransparency?: number;
+};
+
+const SvgShapeElement: React.FC<Props> = ({
+  shape,
+  width,
+  height,
+  bdcolor,
+  bdwidth,
+  transparency,
+  fcolor,
+  ftransparency,
+}: Props) => (
+  <svg
+    viewBox={`0 0 ${width} ${height}`}
+  >
+    {shape === 'Circle' ? (
+      <ellipse
+        cx="50%"
+        cy="50%"
+        rx={(width - bdwidth) / 2}
+        ry={(height - bdwidth) / 2}
+        stroke={bdcolor}
+        strokeWidth={bdwidth}
+        strokeOpacity={transparency}
+        fill={fcolor}
+        fillOpacity={ftransparency}
+      />
+    ) : null}
+    {shape === 'Square' ? (
+      <rect
+        x={bdwidth / 2}
+        y={bdwidth / 2}
+        width={`${width - bdwidth}px`}
+        height={`${height - bdwidth}px`}
+        stroke={bdcolor}
+        strokeWidth={bdwidth}
+        strokeOpacity={transparency}
+        fill={fcolor}
+        fillOpacity={ftransparency}
+      />
+    ) : null}
+  </svg>
+);
+
+export default SvgShapeElement;

+ 7 - 8
components/Tabs/index.tsx

@@ -1,24 +1,23 @@
 import React, { useState } from 'react';
 
-import { Wrapper, BtnGroup, Btn } from './styled';
+import { SelectOptionType } from '../../constants/type';
 
-type OptionProps = {
-  key: string;
-  content: React.ReactNode;
-  child: React.ReactNode;
-};
+import { Wrapper, BtnGroup, Btn } from './styled';
 
 type Props = {
-  options: OptionProps[];
+  options: SelectOptionType[];
+  onChange: (selected: SelectOptionType) => void;
 };
 
-const Tabs: React.FunctionComponent<Props> = ({
+const Tabs: React.FC<Props> = ({
   options,
+  onChange,
 }: Props) => {
   const [selectedIndex, setSelect] = useState(0);
 
   const handleClick = (index: number): void => {
     setSelect(index);
+    onChange(options[index]);
   };
 
   return (

+ 2 - 1
components/TextField/index.tsx

@@ -5,8 +5,9 @@ import { Input, TextArea } from './styled';
 type Props = {
   id?: string;
   name?: string;
-  onChange?: (val: string | number) => void;
+  onChange?: (val: string) => void;
   onBlur?: () => void;
+  value?: string | number;
   defaultValue?: string | number;
   placeholder?: string;
   disabled?: boolean;

+ 0 - 50
components/TextTools/data.ts

@@ -1,50 +0,0 @@
-export const fontOptions = [
-  {
-    key: 'helvetica',
-    content: 'Helvetica',
-    value: '',
-  },
-  {
-    key: 'arial',
-    content: 'Arial',
-    value: '',
-  },
-];
-
-export const sizeOptions = [
-  {
-    key: 'size_12',
-    content: 12,
-    value: '',
-  },
-  {
-    key: 'size_14',
-    content: 14,
-    value: '',
-  },
-  {
-    key: 'size_18',
-    content: 18,
-    value: '',
-  },
-  {
-    key: 'size_24',
-    content: 24,
-    value: '',
-  },
-  {
-    key: 'size_36',
-    content: 36,
-    value: '',
-  },
-  {
-    key: 'size_48',
-    content: 48,
-    value: '',
-  },
-  {
-    key: 'size_64',
-    content: 64,
-    value: '',
-  },
-];

+ 0 - 87
components/TextTools/index.tsx

@@ -1,87 +0,0 @@
-import React from 'react';
-
-import Icon from '../Icon';
-import Button from '../Button';
-import Typography from '../Typography';
-import ExpansionPanel from '../ExpansionPanel';
-import Sliders from '../Sliders';
-import ColorSelect from '../ColorSelector';
-import SelectBox from '../SelectBox';
-
-import {
-  Wrapper, Group, Item, SliderWrapper,
-} from '../../global/toolStyled';
-
-import { fontOptions, sizeOptions } from './data';
-
-type Props = {
-  isActive: boolean;
-  onClick: () => void;
-};
-
-const TextTools: React.FunctionComponent<Props> = ({
-  isActive,
-  onClick,
-}: Props) => (
-  <ExpansionPanel
-    label={(
-      <Button
-        shouldFitContainer
-        align="left"
-        onClick={onClick}
-        isActive={isActive}
-      >
-        <Icon glyph="text" style={{ marginRight: '10px' }} />
-        Text
-      </Button>
-    )}
-    isActive={isActive}
-  >
-    <Wrapper>
-      <Typography variant="subtitle" style={{ marginTop: '8px' }}>Fonts</Typography>
-      <Group>
-        <SelectBox options={fontOptions} />
-        <Item size="small">
-          <Icon glyph="bold" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="italic" />
-        </Item>
-      </Group>
-    </Wrapper>
-    <Wrapper width="40%">
-      <Typography variant="subtitle" style={{ marginTop: '8px' }}>Size</Typography>
-      <Group>
-        <SelectBox options={sizeOptions} />
-      </Group>
-    </Wrapper>
-    <Wrapper width="60%">
-      <Typography variant="subtitle" style={{ marginTop: '8px' }}>Align</Typography>
-      <Group>
-        <Item size="small">
-          <Icon glyph="align-left" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="align-center" />
-        </Item>
-        <Item size="small">
-          <Icon glyph="align-right" />
-        </Item>
-      </Group>
-    </Wrapper>
-    <Wrapper>
-      <ColorSelect showTitle color="" onClick={(): void => {}} />
-    </Wrapper>
-    <Wrapper>
-      <Typography variant="subtitle" style={{ marginTop: '8px' }}>Opacity</Typography>
-      <Group>
-        <SliderWrapper>
-          <Sliders />
-        </SliderWrapper>
-        40%
-      </Group>
-    </Wrapper>
-  </ExpansionPanel>
-);
-
-export default TextTools;

+ 1 - 1
components/Thumbnail/index.tsx

@@ -12,7 +12,7 @@ type Props = {
   renderingState: string;
 };
 
-const index: React.FunctionComponent<Props> = ({
+const index: React.FC<Props> = ({
   pageNum,
   getPdfImage,
   renderingState,

+ 1 - 1
components/ThumbnailViewer/index.tsx

@@ -20,7 +20,7 @@ type Props = {
   getPdfImage: (page: number) => Promise<string>;
 };
 
-const Thumbnails: React.FunctionComponent<Props> = ({
+const Thumbnails: React.FC<Props> = ({
   isActive = false,
   close,
   currentPage,

+ 9 - 9
components/Toolbar/data.ts

@@ -1,34 +1,34 @@
-export default {
+export default (t: (key: string) => string): Record<string, any> => ({
   scaleOptions: [
     {
       key: 'fit',
-      content: 'Fit',
-      value: 'fit',
+      content: t('fit'),
+      child: 'fit',
     },
     {
       key: '50',
       content: '50 %',
-      value: 50,
+      child: 50,
     },
     {
       key: '100',
       content: '100 %',
-      value: 100,
+      child: 100,
     },
     {
       key: '150',
       content: '150 %',
-      value: 150,
+      child: 150,
     },
     {
       key: '200',
       content: '200 %',
-      value: 200,
+      child: 200,
     },
     {
       key: '250',
       content: '250 %',
-      value: 250,
+      child: 250,
     },
   ],
-};
+});

+ 16 - 5
components/Toolbar/index.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
 
 import Icon from '../Icon';
 import Pagination from '../Pagination';
@@ -8,7 +9,7 @@ import { SelectOptionType, ViewportType } from '../../constants/type';
 import { scaleCheck } from '../../helpers/utility';
 
 import { Container, ToggleButton } from './styled';
-import data from './data';
+import dataset from './data';
 
 type Props = {
   totalPage: number;
@@ -24,7 +25,7 @@ type Props = {
   handleHandClick: () => void;
 };
 
-const Toolbar: React.FunctionComponent<Props> = ({
+const Toolbar: React.FC<Props> = ({
   totalPage,
   currentPage,
   setCurrentPage,
@@ -37,6 +38,9 @@ const Toolbar: React.FunctionComponent<Props> = ({
   toggleDisplayMode,
   handleHandClick,
 }: Props) => {
+  const { t } = useTranslation('toolbar');
+  const data = dataset(t);
+
   const handleClockwiseRotation = (): void => {
     const r = rotation + 90;
     changeRotate(r);
@@ -48,13 +52,13 @@ const Toolbar: React.FunctionComponent<Props> = ({
   };
 
   const handleScaleSelect = async (selected: SelectOptionType): Promise<any> => {
-    if (selected.value === 'fit') {
+    if (selected.child === 'fit') {
       const screenWidth = window.document.body.offsetWidth - 276;
       const originPdfWidth = viewport.width / scale;
       const rate = screenWidth / originPdfWidth;
       changeScale(rate);
     } else {
-      changeScale(scaleCheck(selected.value as number));
+      changeScale(scaleCheck(selected.child as number));
     }
   };
 
@@ -74,7 +78,14 @@ const Toolbar: React.FunctionComponent<Props> = ({
         <Icon glyph="hand" style={{ width: '30px' }} onClick={handleHandClick} />
         <Icon glyph="zoom-in" style={{ width: '30px' }} onClick={handleZoomIn} />
         <Icon glyph="zoom-out" style={{ width: '30px' }} onClick={handleZoomOut} />
-        <SelectBox options={data.scaleOptions} width="94px" useInput isDivide onChange={handleScaleSelect} defaultValue={`${Math.round(scale * 100)} %`} />
+        <SelectBox
+          options={data.scaleOptions}
+          style={{ width: '94px' }}
+          useInput
+          isDivide
+          onChange={handleScaleSelect}
+          defaultValue={`${Math.round(scale * 100)} %`}
+        />
         <Divider />
         <Icon glyph="rotate-left" style={{ width: '30px' }} onClick={handleCounterclockwiseRotation} />
         <Icon glyph="rotate-right" style={{ width: '30px' }} onClick={handleClockwiseRotation} />

+ 1 - 1
components/Tooltip/index.tsx

@@ -10,7 +10,7 @@ type Props = {
   content: React.ReactNode;
 };
 
-const Tooltip: React.FunctionComponent<Props> = ({
+const Tooltip: React.FC<Props> = ({
   anchor = 'left',
   children,
   content,

+ 9 - 12
components/Typography/index.tsx

@@ -5,26 +5,23 @@ import { Title, Subtitle, Body } from './styled';
 type Props = {
   variant?: 'title' | 'subtitle' | 'body';
   light?: boolean;
-  children: string | number;
+  children: React.ReactNode;
   style?: {};
   align?: 'left' | 'center' | 'right';
 };
 
-const Typography: React.FunctionComponent<Props> = ({
+const Typography: React.FC<Props> = ({
   variant = 'title',
   children,
   ...rest
 }: Props) => {
-  const getComponent = (): React.FunctionComponent => {
-    if (variant === 'title') {
-      return Title;
-    }
-    if (variant === 'subtitle') {
-      return Subtitle;
-    }
-    return Body;
-  };
-  const Component = getComponent();
+  let Component = Body;
+  if (variant === 'title') {
+    Component = Title;
+  }
+  if (variant === 'subtitle') {
+    Component = Subtitle;
+  }
 
   return (
     <Component

+ 1 - 1
components/Viewer/index.tsx

@@ -21,7 +21,7 @@ const Viewer = forwardRef<Ref, Props>(({
   const width = (Math.abs(rotation) / 90) % 2 === 1 ? viewport.height : viewport.width;
 
   return (
-    <OuterWrapper ref={ref} isFull={displayMode === 'full'}>
+    <OuterWrapper id="pdf_viewer" ref={ref} isFull={displayMode === 'full'}>
       <Wrapper width={width}>
         {children}
       </Wrapper>

+ 1 - 0
components/Viewer/styled.ts

@@ -16,4 +16,5 @@ export const Wrapper = styled('div')<{width: number}>`
   display: inline-flex;
   flex-direction: column;
   width: ${props => props.width}px;
+  margin: 0 10px;
 `;

+ 34 - 90
components/Watermark/index.tsx

@@ -1,99 +1,43 @@
 import React from 'react';
 
-import Button from '../Button';
-import Icon from '../Icon';
-import Drawer from '../Drawer';
-import Typography from '../Typography';
-import Tabs from '../Tabs';
-import Sliders from '../Sliders';
-import Divider from '../Divider';
-import ColorSelect from '../ColorSelector';
-import TextField from '../TextField';
+import { WatermarkType } from '../../constants/type';
 
-import { BtnWrapper, ContentWrapper } from './styled';
-import {
-  Container, Head, Body, Footer, IconWrapper,
-} from '../../global/sidebarStyled';
-import { Group, SliderWrapper } from '../../global/toolStyled';
+import { TextWrapper, Img } from './styled';
 
-type Props = {
-  onClick: () => void;
-  isActive: boolean;
+type Props = WatermarkType & {
+  viewScale: number;
 };
 
-const TextContent = (): React.ReactElement => (
-  <>
-    <ContentWrapper>
-      <Typography variant="subtitle">Content</Typography>
-    </ContentWrapper>
-    <TextField variant="multiline" shouldFitContainer />
-  </>
-);
-
-const ImageContent = (): React.ReactElement => (
-  <ContentWrapper>
-    <Icon glyph="add-image" />
-    <Button appearance="link" style={{ marginLeft: '10px' }}>Select Image</Button>
-  </ContentWrapper>
-);
-
-const Watermark: React.FunctionComponent<Props> = ({
-  onClick,
-  isActive,
+const index: React.FC<Props> = ({
+  type,
+  text,
+  imagepath,
+  viewScale,
+  scale = 1,
+  opacity,
+  textcolor,
+  rotation,
 }: Props) => (
-  <>
-    <BtnWrapper>
-      <Button shouldFitContainer align="left" onClick={onClick}>
-        <Icon glyph="watermark" style={{ marginRight: '10px' }} />
-        Watermark
-      </Button>
-    </BtnWrapper>
-    <Drawer open={isActive} anchor="left" zIndex={4}>
-      <Container>
-        <Head>
-          <IconWrapper>
-            <Icon glyph="left-back" onClick={onClick} />
-          </IconWrapper>
-          <Typography light>Watermark</Typography>
-        </Head>
-        <Body>
-          <Typography variant="subtitle">Type</Typography>
-          <Tabs
-            options={[
-              {
-                key: 'text',
-                content: 'Text',
-                child: <TextContent />,
-              },
-              {
-                key: 'image',
-                content: 'Image',
-                child: <ImageContent />,
-              },
-            ]}
-          />
-          <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-          <ColorSelect showTitle color="" onClick={(): void => {}} />
-          <Typography variant="subtitle">Opacity</Typography>
-          <Group>
-            <SliderWrapper>
-              <Sliders />
-            </SliderWrapper>
-            40%
-          </Group>
-          <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-          <Group>
-            <Typography variant="subtitle">Page Range</Typography>
-            <Button appearance="primary-hollow" style={{ width: '50%' }}>All Pages</Button>
-          </Group>
-        </Body>
-        <Footer>
-          <Button appearance="primary">Save</Button>
-          <Button appearance="danger-link">Remove watermark</Button>
-        </Footer>
-      </Container>
-    </Drawer>
-  </>
+  type === 'text' ? (
+    <TextWrapper
+      style={{
+        fontSize: `${viewScale * scale * 24}px`,
+        opacity,
+        color: textcolor,
+        transform: `rotate(-${rotation}deg)`,
+      }}
+    >
+      {text}
+    </TextWrapper>
+  ) : (
+    <Img
+      style={{
+        opacity,
+        transform: `scale(${viewScale * scale}) rotate(-${rotation}deg)`,
+      }}
+      src={imagepath}
+    />
+  )
 );
 
-export default Watermark;
+export default index;

+ 3 - 6
components/Watermark/styled.ts

@@ -1,10 +1,7 @@
 import styled from 'styled-components';
 
-export const BtnWrapper = styled.div`
-  padding: 8px;
+export const TextWrapper = styled.span`
+  white-space: pre;
 `;
 
-export const ContentWrapper = styled.div`
-  margin-top: 20px;
-  display: flex;
-`;
+export const Img = styled.img``;

+ 39 - 0
components/WatermarkImageSelector/index.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+
+import Icon from '../Icon';
+import Button from '../Button';
+import { uploadFile } from '../../helpers/utility';
+
+import { ContentWrapper } from './styled';
+
+type Props = {
+  t: (key: string) => string;
+  onChange: (data: string) => void;
+};
+
+const ImageSelector: React.FC<Props> = ({
+  t,
+  onChange,
+}: Props): React.ReactElement => {
+  const handleClick = (): void => {
+    uploadFile('.png,.jpg,.jpeg,.svg')
+      .then((urlData) => {
+        onChange(urlData);
+      });
+  };
+
+  return (
+    <ContentWrapper>
+      <Icon glyph="add-image" />
+      <Button
+        appearance="link"
+        style={{ marginLeft: '10px' }}
+        onClick={handleClick}
+      >
+        {t('selectImage')}
+      </Button>
+    </ContentWrapper>
+  );
+};
+
+export default ImageSelector;

+ 6 - 0
components/WatermarkImageSelector/styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const ContentWrapper = styled.div`
+  margin-top: 20px;
+  display: flex;
+`;

+ 127 - 0
components/WatermarkOption/index.tsx

@@ -0,0 +1,127 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { WatermarkType, SelectOptionType } from '../../constants/type';
+import Button from '../Button';
+import Box from '../Box';
+import Icon from '../Icon';
+import Typography from '../Typography';
+import Tabs from '../Tabs';
+import Divider from '../Divider';
+import SliderWithTitle from '../SliderWithTitle';
+import TextBox from '../WatermarkTextBox';
+import ColorSelector from '../../containers/ColorSelector';
+
+import {
+  Container, Head, Body, Footer, IconWrapper,
+} from '../../global/sidebarStyled';
+
+type Props = WatermarkType & {
+  onClick: () => void;
+  isActive: boolean;
+  onSave: () => void;
+  onDelete: () => void;
+  setDataState: (arg: Record<string, any>) => void;
+};
+
+const WatermarkOption: React.SFC<Props> = ({
+  onClick,
+  type,
+  opacity = 0,
+  scale = 1,
+  rotation,
+  textcolor,
+  text,
+  onSave,
+  onDelete,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => {
+  const { t } = useTranslation('sidebar');
+
+  return (
+    <Container>
+      <Head>
+        <IconWrapper>
+          <Icon glyph="left-back" onClick={onClick} />
+        </IconWrapper>
+        <Typography light>
+          {t('watermark')}
+        </Typography>
+      </Head>
+      <Body>
+        <Typography variant="subtitle" align="left">
+          {t('type')}
+        </Typography>
+        <Tabs
+          options={[
+            {
+              key: 'text',
+              content: t('text'),
+              child: (
+                <TextBox
+                  t={t}
+                  value={text}
+                  onChange={(value: string): void => { setDataState({ text: value }); }}
+                />
+              ),
+            },
+          ]}
+          onChange={(option: SelectOptionType): void => { setDataState({ type: option.key }); }}
+        />
+        {
+          type === 'text' ? (
+            <>
+              <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+              <ColorSelector
+                title={t('color')}
+                mode="watermark"
+                selectedColor={textcolor}
+                onClick={(val: string): void => { setDataState({ textcolor: val }); }}
+              />
+            </>
+          ) : null
+        }
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('opacity')}
+            value={opacity * 100}
+            tips={`${(opacity * 100).toFixed(0)}%`}
+            onSlide={(val: number): void => { setDataState({ opacity: val * 0.01 }); }}
+          />
+        </Box>
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('rotate')}
+            value={rotation}
+            maximum={360}
+            tips={`${rotation}°`}
+            onSlide={(val: number): void => { setDataState({ rotation: val }); }}
+          />
+        </Box>
+        <Box mt="20">
+          <SliderWithTitle
+            title={t('scale')}
+            value={scale * 100}
+            tips={`${Math.round(scale * 100)}%`}
+            minimum={50}
+            maximum={250}
+            onSlide={(val: number): void => { setDataState({ scale: val / 100 }); }}
+          />
+        </Box>
+        <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
+      </Body>
+      <Footer>
+        <Button appearance="primary" onClick={onSave}>
+          {t('save')}
+        </Button>
+        <Button appearance="danger-link" onClick={onDelete}>
+          {t('removeWatermark')}
+        </Button>
+      </Footer>
+    </Container>
+  );
+};
+
+export default WatermarkOption;

+ 5 - 0
components/WatermarkOption/styled.ts

@@ -0,0 +1,5 @@
+import styled from 'styled-components';
+
+export const BtnWrapper = styled.div`
+  padding: 8px;
+`;

+ 25 - 0
components/WatermarkPageSelector/index.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+import Button from '../Button';
+import Typography from '../Typography';
+
+import { Group } from '../../global/toolStyled';
+
+type Props = {
+  t: (key: string) => string;
+};
+
+const PageSelector: React.FC<Props> = ({
+  t,
+}: Props) => (
+  <Group>
+    <Typography variant="subtitle">
+      {t('pageRange')}
+    </Typography>
+    <Button appearance="primary-hollow" style={{ width: '50%' }}>
+      {t('All Pages')}
+    </Button>
+  </Group>
+);
+
+export default PageSelector;

+ 32 - 0
components/WatermarkTextBox/index.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+
+import TextField from '../TextField';
+import Typography from '../Typography';
+
+import { ContentWrapper } from './styled';
+
+type Props = {
+  t: (key: string) => string;
+  onChange: (value: string) => void;
+  value?: string;
+};
+
+const TextBox: React.FC<Props> = ({
+  t,
+  onChange,
+  value = '',
+}: Props): React.ReactElement => (
+  <>
+    <ContentWrapper>
+      <Typography variant="subtitle">{t('text')}</Typography>
+    </ContentWrapper>
+    <TextField
+      variant="multiline"
+      onChange={onChange}
+      value={value}
+      shouldFitContainer
+    />
+  </>
+);
+
+export default TextBox;

+ 6 - 0
components/WatermarkTextBox/styled.ts

@@ -0,0 +1,6 @@
+import styled from 'styled-components';
+
+export const ContentWrapper = styled.div`
+  margin-top: 20px;
+  display: flex;
+`;

+ 1 - 1
config/index.js

@@ -1,4 +1,4 @@
-let API_HOST = 'http://192.168.173.26:3000';
+let API_HOST = 'http://127.0.0.1:3000';
 
 switch (process.env.NODE_ENV) {
   case 'production':

+ 5 - 2
constants/actionTypes.ts

@@ -3,6 +3,8 @@ export const SET_NAVBAR = 'SET_NAVBAR';
 export const SET_SIDEBAR = 'SET_SIDEBAR';
 export const SET_MARKUP_TOOL = 'SET_MARKUP_TOOL';
 export const SET_INFO = 'SET_INFO';
+export const SET_SAVED = 'SET_SAVED';
+export const SET_SAVING = 'SET_SAVING';
 
 export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
 export const SET_TOTAL_PAGE = 'SET_TOTAL_PAGE';
@@ -12,5 +14,6 @@ export const SET_VIEWPORT = 'SET_VIEWPORT';
 export const CHANGE_SCALE = 'CHANGE_SCALE';
 export const CHANGE_ROTATE = 'CHANGE_ROTATE';
 
-export const ADD_ANNOTATIONS = 'ADD_ANNOTATIONS';
-export const UPDATE_ANNOTATIONS = 'UPDATE_ANNOTATIONS';
+export const ADD_ANNOTS = 'ADD_ANNOTS';
+export const UPDATE_ANNOTS = 'UPDATE_ANNOTS';
+export const UPDATE_WATERMARK = 'UPDATE_WATERMARK';

+ 12 - 1
constants/index.ts

@@ -2,9 +2,20 @@ export const MAX_SCALE = 5;
 export const MIN_SCALE = 0.25;
 export const RENDER_RANGE = 3;
 
-export const LINE_TYPE: Record<string, any> = {
+export const MARKUP_TYPE: Record<string, any> = {
   highlight: 'Highlight',
   underline: 'Underline',
   squiggly: 'Squiggly',
   strikeout: 'StrikeOut',
 };
+
+export const ANNOTATION_TYPE: Record<string, any> = {
+  ...MARKUP_TYPE,
+  ink: 'Ink',
+  freetext: 'FreeText',
+  text: 'Text',
+  square: 'Square',
+  circle: 'Circle',
+  line: 'Line',
+  arrow: 'Line',
+};

+ 1 - 1
constants/style.ts

@@ -17,7 +17,7 @@ export const color: {[index: string]: any} = {
   'strong-pink': '#ff1b89',
   'neon-green': '#02ff36',
   'light-blue': '#27befd',
-  info: '#90caf9',
+  info: '#2979ff',
   success: '#43a047',
   error: '#d32f2f',
   danger: '#fe2712',

+ 104 - 18
constants/type.ts

@@ -1,10 +1,10 @@
-export type TypeRenderingStates = 'RENDERING' | 'LOADING' | 'FINISHED' | 'PAUSED';
+export type RenderingStateType = 'RENDERING' | 'LOADING' | 'FINISHED' | 'PAUSED';
 
 export type LineType = 'Highlight' | 'Underline' | 'Squiggly' | 'StrikeOut';
 
-export type PayloadType = {
-  payload: any;
-};
+export type ReducerFuncType = (
+  (state: Record<string, any>, action: { type: string; payload: any}) => any
+);
 
 export type ViewportType = {
   width: number;
@@ -27,7 +27,7 @@ export type ScrollStateType = {
 export type SelectOptionType = {
   key: string | number;
   content: React.ReactNode;
-  value: number | string;
+  child: React.ReactNode;
 };
 
 export type PositionType = {
@@ -38,25 +38,54 @@ export type PositionType = {
 };
 
 export type HTMLCoordinateType = {
-  top: number | string;
-  left: number | string;
-  width: number | string;
-  height: number | string;
+  top: number;
+  left: number;
+  width: number;
+  height: number;
+};
+
+export type PointType = {
+  x: number;
+  y: number;
+}
+
+export type LinePositionType = {
+  start: PointType;
+  end: PointType;
+};
+
+export type AnnotationPositionType = (
+  string | PositionType | LinePositionType | PointType | (PositionType | PointType[])[]
+);
+
+export type AnnotationAttributeType = {
+  page: number;
+  bdcolor?: string | undefined;
+  position?: AnnotationPositionType;
+  transparency?: number | undefined;
+  content?: string | undefined;
+  style?: number | undefined;
+  fcolor?: string | undefined;
+  ftransparency?: number | undefined;
+  bdwidth?: number | undefined;
+  fontname?: string | undefined;
+  fontsize?: number | undefined;
+  textcolor?: string | undefined;
+  is_arrow?: boolean | undefined;
 };
 
 export type AnnotationType = {
+  id?: string;
   obj_type: string;
-  obj_attr: {
-    page: number;
-    bdcolor: string;
-    position: PositionType[];
-    transparency: number;
-  };
+  obj_attr: AnnotationAttributeType;
 };
 
-type UpdateData = {
-  color?: string;
-  opacity?: number;
+export type UpdateData = {
+  bdcolor?: string;
+  transparency?: number;
+  position?: AnnotationPositionType;
+  content?: string;
+  fontsize?: number;
 };
 
 export type OnUpdateType = (
@@ -71,3 +100,60 @@ type DispatchType = {
 export type ActionType = (
   (dispatch: (obj: DispatchType) => void) => Record<string, any>
 );
+
+export type AnnotationElementPropsType = AnnotationType & {
+  isCovered: boolean;
+  isCollapse: boolean;
+  mousePosition: Record<string, any>;
+  onUpdate: OnUpdateType;
+  onDelete: () => void;
+  scale: number;
+  viewport: ViewportType;
+  onEdit: () => void;
+  isEdit: boolean;
+  onBlur: () => void;
+};
+
+export type OptionPropsType = {
+  type?: string;
+  color?: string;
+  opacity?: number;
+  fontName?: string;
+  fontSize?: number;
+  width?: number;
+  align?: string;
+  fontStyle?: string;
+  shape?: string;
+  text?: string;
+  setDataState?: (arg: Record<string, any>) => void;
+};
+
+export type CoordType = {
+  left: number;
+  top: number;
+  width?: number;
+  height?: number;
+};
+
+export type CircleType = {
+  direction: string;
+  cx: number;
+  cy: number;
+  r: number;
+};
+
+export type WatermarkType = {
+  type?: 'image' | 'text';
+  scale?: number;
+  opacity?: number;
+  rotation?: number;
+  pages?: string;
+  vertalign?: 'top' | 'center' | 'bottom';
+  horizalign?: 'left' | 'center' | 'right';
+  xoffset?: number;
+  yoffset?: number;
+  imagepath?: string;
+  text?: string;
+  textcolor?: string;
+  isfront?: 'yes' | 'no';
+};

+ 107 - 36
containers/Annotation.tsx

@@ -1,8 +1,14 @@
-/* eslint-disable @typescript-eslint/camelcase */
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 
 import { AnnotationType, OnUpdateType } from '../constants/type';
-import AnnotationComp from '../components/Annotation';
+import AnnotationWrapper from '../components/AnnotationWrapper';
+import Highlight from '../components/Highlight';
+import Ink from '../components/Ink';
+import FreeText from '../components/FreeText';
+import StickyNote from '../components/StickyNote';
+import Shape from '../components/Shape';
+import Line from '../components/Line';
+import DeleteDialog from '../components/DeleteDialog';
 import useCursorPosition from '../hooks/useCursorPosition';
 
 import useStore from '../store';
@@ -13,21 +19,24 @@ type Props = AnnotationType & {
   scale: number;
 };
 
-const Annotation: React.FunctionComponent<Props> = ({
+const Annotation: React.FC<Props> = ({
+  id,
   obj_type,
   obj_attr,
   index,
   scale,
 }: Props) => {
   const [isCollapse, setCollapse] = useState(true);
+  const [isEdit, setEdit] = useState(false);
   const [isCovered, setMouseOver] = useState(false);
   const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+  const [openDialog, setDialog] = useState(false);
   const [cursorPosition, ref] = useCursorPosition();
   const [{ viewport, annotations }, dispatch] = useStore();
-  const { updateAnnotation } = useActions(dispatch);
+  const { updateAnnots } = useActions(dispatch);
 
   const handleClick = (): void => {
-    if (isCollapse) {
+    if (isCollapse && !isEdit) {
       setCollapse(false);
       setMousePosition({
         x: cursorPosition.x || 0,
@@ -36,10 +45,6 @@ const Annotation: React.FunctionComponent<Props> = ({
     }
   };
 
-  const handleBlur = (): void => {
-    setCollapse(true);
-  };
-
   const handleMouseOver = (): void => {
     setMouseOver(true);
   };
@@ -49,43 +54,109 @@ const Annotation: React.FunctionComponent<Props> = ({
   };
 
   const handleUpdate: OnUpdateType = (data) => {
-    if (data.color) {
-      annotations[index].obj_attr.bdcolor = data.color;
-    }
-    if (data.opacity) {
-      annotations[index].obj_attr.transparency = data.opacity * 0.01;
-    }
-    updateAnnotation(annotations);
+    const newAttributes = {
+      ...annotations[index].obj_attr,
+      ...data,
+      isInit: false,
+    };
+
+    annotations[index].obj_attr = newAttributes;
+
+    updateAnnots(annotations);
+  };
+
+  const handleEdit = (): void => {
+    setEdit(true);
+    setCollapse(true);
+  };
+
+  const deleteDialogToggle = (): void => {
+    setDialog(!openDialog);
   };
 
   const handleDelete = (): void => {
     annotations.splice(index, 1);
-    updateAnnotation(annotations);
+    setDialog(false);
+    updateAnnots(annotations);
+  };
+
+  const handleBlur = (): void => {
+    setCollapse(true);
+  };
+
+  const handleInputBlur = (): void => {
+    setEdit(false);
+
+    if (obj_type === 'FreeText' && !obj_attr.content) {
+      handleDelete();
+    }
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent): void => {
+    if (e.keyCode === 46) {
+      deleteDialogToggle();
+    }
+  };
+
+  useEffect(() => {
+    if (obj_type === 'FreeText' && !obj_attr.content) {
+      setEdit(true);
+    }
+  }, [obj_type, obj_attr]);
+
+  const childProps = {
+    id,
+    obj_type,
+    obj_attr,
+    scale,
+    isCollapse,
+    isCovered,
+    mousePosition,
+    onUpdate: handleUpdate,
+    onDelete: deleteDialogToggle,
+    viewport,
+    onEdit: handleEdit,
+    isEdit,
+    onBlur: handleInputBlur,
+  };
+
+  const wrapperProps = {
+    onBlur: handleBlur,
+    onMouseDown: handleClick,
+    onMouseOver: handleMouseOver,
+    onMouseOut: handleMouseOut,
+    onKeyDown: handleKeyDown,
   };
 
   return (
-    <div
+    <AnnotationWrapper
       ref={ref}
-      tabIndex={0}
-      role="button"
-      onFocus={(): void => { console.log('focus'); }}
-      onBlur={handleBlur}
-      onMouseDown={handleClick}
-      onMouseOver={handleMouseOver}
-      onMouseOut={handleMouseOut}
+      {...wrapperProps}
     >
-      <AnnotationComp
-        obj_type={obj_type}
-        obj_attr={obj_attr}
-        scale={scale}
-        isCollapse={isCollapse}
-        isCovered={isCovered}
-        mousePosition={mousePosition}
-        onUpdate={handleUpdate}
+      {((): React.ReactNode => {
+        switch (obj_type) {
+          case 'Ink':
+            return <Ink {...childProps} />;
+          case 'FreeText':
+            return <FreeText {...childProps} />;
+          case 'Text':
+            return <StickyNote {...childProps} />;
+          case 'Square':
+          case 'Circle':
+            return <Shape {...childProps} />;
+          case 'Line':
+          case 'Arrow':
+            return <Line {...childProps} />;
+          default:
+            return <Highlight {...childProps} />;
+        }
+      })()}
+      <DeleteDialog
+        open={openDialog}
+        onCancel={deleteDialogToggle}
         onDelete={handleDelete}
-        viewport={viewport}
       />
-    </div>
+    </AnnotationWrapper>
   );
 };
 

+ 29 - 13
containers/AnnotationList.tsx

@@ -1,29 +1,45 @@
 import React from 'react';
 
+import { ANNOTATION_TYPE } from '../constants';
+import { AnnotationType } from '../constants/type';
+import Head from './AnnotationListHead';
+import Drawer from '../components/Drawer';
+import Body from '../components/AnnotationList';
+
 import useStore from '../store';
-import useActions from '../actions';
 
-import AnnotationListComp from '../components/AnnotationList';
+import {
+  Container,
+} from '../global/sidebarStyled';
+
+const ANNOT_TYPE_ARRAY = Object.values(ANNOTATION_TYPE);
 
-const AnnotationList: React.FunctionComponent = () => {
+const AnnotationList: React.FC = () => {
   const [{
     navbarState,
     annotations,
     viewport,
     scale,
     pdf,
-  }, dispatch] = useStore();
-  const { setNavbar } = useActions(dispatch);
+  }] = useStore();
+
+  const isActive = navbarState === 'annotations';
 
   return (
-    <AnnotationListComp
-      annotations={annotations}
-      viewport={viewport}
-      scale={scale}
-      pdf={pdf}
-      isActive={navbarState === 'annotations'}
-      close={(): void => { setNavbar(''); }}
-    />
+    <Drawer anchor="right" open={isActive}>
+      <Container>
+        <Head />
+        <Body
+          annotations={annotations.filter(
+            (ele: AnnotationType) => ANNOT_TYPE_ARRAY.includes(ele.obj_type),
+          )}
+          viewport={viewport}
+          scale={scale}
+          pdf={pdf}
+          isActive={isActive}
+        />
+      </Container>
+    </Drawer>
   );
 };
 

+ 25 - 0
containers/AnnotationListHead.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+import Head from '../components/AnnotationListHead';
+import useStore from '../store';
+import useActions from '../actions';
+
+const AnnotationListHead: React.FC = () => {
+  const [{
+    annotations,
+  }, dispatch] = useStore();
+  const {
+    setNavbar,
+    addAnnots,
+  } = useActions(dispatch);
+
+  return (
+    <Head
+      close={(): void => { setNavbar(''); }}
+      addAnnots={addAnnots}
+      hasAnnots={!!annotations.length}
+    />
+  );
+};
+
+export default AnnotationListHead;

+ 7 - 16
containers/AutoSave.tsx

@@ -1,29 +1,20 @@
 import React, { useEffect } from 'react';
-import { useSnackbar } from 'notistack';
+import { useTranslation } from 'react-i18next';
+import { useToasts } from 'react-toast-notifications';
 
 import useAutoSave from '../hooks/useAutoSave';
-import Icon from '../components/Icon';
 
-const index: React.FunctionComponent = () => {
+const index: React.FC = () => {
+  const { t } = useTranslation('toast');
   const [isSaved, isSaving] = useAutoSave();
-  const { enqueueSnackbar, closeSnackbar } = useSnackbar();
+  const { addToast } = useToasts();
 
   useEffect(() => {
     if (isSaving) {
-      enqueueSnackbar('File saving', {
-        variant: 'info',
-        action: key => (
-          <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />
-        ),
-      });
+      addToast(t('saving'), { appearance: 'info', placement: 'bottom-center', autoDismiss: true });
     }
     if (!isSaving && isSaved) {
-      enqueueSnackbar('File is saved', {
-        variant: 'success',
-        action: key => (
-          <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />
-        ),
-      });
+      addToast(t('saved'), { appearance: 'success', placement: 'bottom-center', autoDismiss: true });
     }
   }, [isSaved, isSaving]);
 

+ 43 - 0
containers/ColorSelector.tsx

@@ -0,0 +1,43 @@
+import React, { useState, useEffect } from 'react';
+
+import ColorSelectorComp from '../components/ColorSelector';
+
+import useStore from '../store';
+
+type Props = {
+  title?: string;
+  selectedColor?: string;
+  onClick: (color: string) => void;
+  mode?: 'normal' | 'shape' | 'watermark';
+};
+
+const ColorSelector: React.FC<Props> = ({
+  title,
+  onClick,
+  selectedColor,
+  mode,
+}: Props) => {
+  const [isCollapse, setCollapse] = useState(true);
+  const [{ sidebarState, markupToolState }] = useStore();
+
+  const pickerToggle = (): void => {
+    setCollapse(!isCollapse);
+  };
+
+  useEffect(() => {
+    setCollapse(true);
+  }, [markupToolState, sidebarState]);
+
+  return (
+    <ColorSelectorComp
+      title={title}
+      onClick={onClick}
+      selectedColor={selectedColor}
+      mode={mode}
+      pickerToggle={pickerToggle}
+      isCollapse={isCollapse}
+    />
+  );
+};
+
+export default ColorSelector;

+ 7 - 5
containers/CreateForm.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { useTranslation } from 'react-i18next';
 
 import Button from '../components/Button';
 import Icon from '../components/Icon';
@@ -8,7 +9,8 @@ import useActions from '../actions';
 import useStore from '../store';
 import { BtnWrapper } from '../global/toolStyled';
 
-const CreateForm: React.FunctionComponent = () => {
+const CreateForm: React.FC = () => {
+  const { t } = useTranslation('sidebar');
   const [{ sidebarState }, dispatch] = useStore();
   const { setSidebar } = useActions(dispatch);
 
@@ -25,7 +27,7 @@ const CreateForm: React.FunctionComponent = () => {
       label={(
         <Button shouldFitContainer align="left" onClick={(): void => { onClickSidebar('create-form'); }}>
           <Icon glyph="create-form" style={{ marginRight: '10px' }} />
-          Create Form
+          {t('createForm')}
         </Button>
       )}
       isActive={sidebarState === 'create-form'}
@@ -33,19 +35,19 @@ const CreateForm: React.FunctionComponent = () => {
       <BtnWrapper>
         <Button shouldFitContainer align="left">
           <Icon glyph="text-field" style={{ marginRight: '10px' }} />
-          Text Field
+          {t('textField')}
         </Button>
       </BtnWrapper>
       <BtnWrapper>
         <Button shouldFitContainer align="left">
           <Icon glyph="checkbox" style={{ marginRight: '10px' }} />
-          Check box
+          {t('checkBox')}
         </Button>
       </BtnWrapper>
       <BtnWrapper>
         <Button shouldFitContainer align="left">
           <Icon glyph="radio-button" style={{ marginRight: '10px' }} />
-          Radio Button
+          {t('radioButton')}
         </Button>
       </BtnWrapper>
     </ExpansionPanel>

+ 126 - 0
containers/FreeTextTools.tsx

@@ -0,0 +1,126 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import { ANNOTATION_TYPE } from '../constants';
+import Icon from '../components/Icon';
+import Button from '../components/Button';
+import ExpansionPanel from '../components/ExpansionPanel';
+import FreeTextOption from '../components/FreeTextOption';
+import { OptionPropsType } from '../constants/type';
+import { getAbsoluteCoordinate } from '../helpers/position';
+import { parseAnnotationObject } from '../helpers/annotation';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+type Props = {
+  title: string;
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const HighlightTools: React.FC<Props> = ({
+  title,
+  isActive,
+  onClick,
+}: Props) => {
+  const [data, setData] = useState({
+    fontName: 'Helvetica',
+    fontSize: 12,
+    align: 'left',
+    fontStyle: '',
+    color: '#FCFF36',
+    opacity: 100,
+  });
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
+
+  const setDataState = (obj: OptionPropsType): void => {
+    setData(prev => ({
+      ...prev,
+      ...obj,
+    }));
+  };
+
+  const addFreeText = (
+    pageEle: HTMLElement,
+    event: MouseEvent | TouchEvent,
+    attributes: OptionPropsType,
+  ): void => {
+    const {
+      fontStyle, fontName, fontSize = 0, opacity, color, align,
+    } = attributes;
+    const pageNum = pageEle.getAttribute('data-page-num') || 0;
+    const coordinate = getAbsoluteCoordinate(pageEle, event);
+
+    const annotData = {
+      obj_type: ANNOTATION_TYPE.freetext,
+      obj_attr: {
+        page: pageNum as number,
+        position: {
+          top: coordinate.y,
+          left: coordinate.x,
+          bottom: coordinate.y + fontSize,
+          right: coordinate.x + 30,
+        },
+        transparency: opacity,
+        fontname: fontStyle ? `${fontName}-${fontStyle}` : fontName,
+        fontsize: fontSize,
+        textcolor: color,
+        align,
+        content: '',
+      },
+    };
+    const freeText = parseAnnotationObject(annotData, viewport.height, scale);
+
+    addAnnots([freeText], true);
+  };
+
+  const handleMouseDown = useCallback((event: MouseEvent | TouchEvent): void => {
+    event.preventDefault();
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      addFreeText(pageEle, event, data);
+    }
+  }, [data, viewport, scale]);
+
+  useEffect(() => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    if (isActive && pdfViewer) {
+      pdfViewer.addEventListener('mousedown', handleMouseDown);
+      pdfViewer.addEventListener('touchstart', handleMouseDown);
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        pdfViewer.removeEventListener('mousedown', handleMouseDown);
+        pdfViewer.removeEventListener('touchstart', handleMouseDown);
+      }
+    };
+  }, [isActive, handleMouseDown]);
+
+  return (
+    <ExpansionPanel
+      label={(
+        <Button
+          shouldFitContainer
+          align="left"
+          onClick={onClick}
+          isActive={isActive}
+        >
+          <Icon glyph="text" style={{ marginRight: '10px' }} />
+          {title}
+        </Button>
+      )}
+      isActive={isActive}
+      showBottomBorder
+    >
+      <FreeTextOption
+        {...data}
+        setDataState={setDataState}
+      />
+    </ExpansionPanel>
+  );
+};
+
+export default HighlightTools;

+ 122 - 54
containers/FreehandTools.tsx

@@ -1,78 +1,149 @@
 import React, { useState, useEffect, useCallback } from 'react';
+import { v4 as uuidv4 } from 'uuid';
 
+import { OptionPropsType, PointType } from '../constants/type';
+import { ANNOTATION_TYPE } from '../constants';
 import Icon from '../components/Icon';
 import Button from '../components/Button';
 import ExpansionPanel from '../components/ExpansionPanel';
-import FreehandOption from '../components/FreehandOption';
+import InkOption from '../components/InkOption';
 
-import { createPolyline, completePath } from '../helpers/brush';
-import { getAbsoluteCoordinate } from '../helpers/utility';
+import { getAbsoluteCoordinate, parsePositionForBackend } from '../helpers/position';
+import { parseAnnotationObject } from '../helpers/annotation';
 import useCursorPosition from '../hooks/useCursorPosition';
 
+import useActions from '../actions';
+import useStore from '../store';
+
 type Props = {
+  title: string;
   isActive: boolean;
   onClick: () => void;
 };
 
-const FreehandTools: React.FunctionComponent<Props> = ({
+const FreehandTools: React.FC<Props> = ({
+  title,
   isActive,
   onClick,
 }: Props) => {
-  const [position, setRef] = useCursorPosition();
-  const [type, setType] = useState('pen');
-  const [color, setColor] = useState('#FCFF36');
-  const [opacity, setOpacity] = useState(100);
-  const [width, setWidth] = useState(12);
-  const [polylineElement, setElement] = useState<SVGPolylineElement | null>(null);
-
-  const handleOpacity = (value: number): void => {
-    setOpacity(value);
-  };
-
-  const handleWidth = (value: number): void => {
-    setWidth(value);
+  const [cursorPosition, setRef] = useCursorPosition(40);
+
+  const [uuid, setUuid] = useState('');
+  const [data, setData] = useState({
+    type: 'pen',
+    opacity: 100,
+    color: '#FCFF36',
+    width: 12,
+  });
+  const [{ viewport, scale, annotations }, dispatch] = useStore();
+  const { addAnnots, updateAnnots } = useActions(dispatch);
+
+  const setDataState = (obj: OptionPropsType): void => {
+    setData(prev => ({
+      ...prev,
+      ...obj,
+    }));
   };
 
-  const handleMouseDown = useCallback((e: MouseEvent): void => {
-    const page = (e.target as HTMLElement).parentNode as HTMLElement;
-    if (page.hasAttribute('data-page-num')) {
-      setRef(page);
-
-      const coordinate = getAbsoluteCoordinate(page, e);
-      const element = createPolyline(coordinate, color, width);
+  const handleMouseDown = useCallback((e: MouseEvent | TouchEvent): void => {
+    const pageEle = (e.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      setRef(pageEle);
+      const pageNum = pageEle.getAttribute('data-page-num') || 0;
+      const coordinate = getAbsoluteCoordinate(pageEle, e);
+      const id = uuidv4();
+
+      setUuid(id);
+
+      const annotData = {
+        id,
+        obj_type: ANNOTATION_TYPE.ink,
+        obj_attr: {
+          page: pageNum as number,
+          bdcolor: data.color,
+          bdwidth: data.width,
+          position: [[coordinate]],
+          transparency: data.opacity,
+        },
+      };
+      const freehand = parseAnnotationObject(annotData, viewport.height, scale);
+
+      addAnnots([freehand], true);
+    }
+  }, [data, viewport, scale]);
+
+  const handleMouseUp = useCallback((): void => {
+    const index = annotations.length - 1;
+    const position = annotations[index].obj_attr.position as PointType[][];
+
+    if (position[0].length === 1 && annotations[index].id === uuid) {
+      const point = position[0][0];
+      annotations[index].obj_attr.position = [[
+        { x: point.x - 5, y: point.y - 5 }, { x: point.x + 5, y: point.y + 5 },
+      ]];
+      updateAnnots([...annotations]);
+    }
 
-      setElement(element);
+    setRef(null);
+    setUuid('');
+  }, [annotations, uuid]);
 
-      const svg = page.querySelector('svg');
-      if (svg && element) {
-        svg.appendChild(element);
+  useEffect(() => {
+    const index = annotations.length - 1;
+
+    if (
+      annotations[index] && annotations[index].id === uuid
+      && cursorPosition.x && cursorPosition.y
+    ) {
+      const type = annotations[index].obj_type;
+      const position = annotations[index].obj_attr.position as PointType[][];
+      const coordinates = parsePositionForBackend(
+        type, { x: cursorPosition.x, y: cursorPosition.y }, viewport.height, scale,
+      ) as PointType;
+
+      const lastPosition = position[0].slice(-1)[0];
+
+      if (
+        coordinates.x !== lastPosition.x
+        && coordinates.y !== lastPosition.y
+      ) {
+        position[0].push(coordinates);
+        annotations[index].obj_attr.position = position;
+        updateAnnots([...annotations]);
       }
     }
-  }, [color, width]);
+  }, [annotations, cursorPosition, uuid]);
+
+  const subscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.addEventListener('mousedown', handleMouseDown);
+    pdfViewer.addEventListener('mouseup', handleMouseUp);
+    pdfViewer.addEventListener('touchstart', handleMouseDown);
+    pdfViewer.addEventListener('touchend', handleMouseUp);
+  };
 
-  const handleMouseUp = (): void => {
-    setRef(null);
-    setElement(null);
+  const unsubscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.removeEventListener('mousedown', handleMouseDown);
+    pdfViewer.removeEventListener('mouseup', handleMouseUp);
+    pdfViewer.removeEventListener('touchstart', handleMouseDown);
+    pdfViewer.removeEventListener('touchend', handleMouseUp);
   };
 
   useEffect(() => {
-    if (polylineElement) {
-      const coordinate = { x: position.x, y: position.y };
-      completePath(polylineElement, coordinate);
-    }
-  }, [polylineElement, position]);
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
-  useEffect(() => {
-    if (isActive) {
-      window.addEventListener('mousedown', handleMouseDown);
-      window.addEventListener('mouseup', handleMouseUp);
+    if (isActive && pdfViewer) {
+      subscribeEvent();
     }
 
     return (): void => {
-      window.removeEventListener('mousedown', handleMouseDown);
-      window.removeEventListener('mouseup', handleMouseUp);
+      if (pdfViewer) {
+        unsubscribeEvent();
+      }
     };
-  }, [isActive]);
+  }, [isActive, handleMouseDown, handleMouseUp]);
 
   return (
     <ExpansionPanel
@@ -84,21 +155,18 @@ const FreehandTools: React.FunctionComponent<Props> = ({
           isActive={isActive}
         >
           <Icon glyph="freehand" style={{ marginRight: '10px' }} />
-          Freehand
+          {title}
         </Button>
       )}
       isActive={isActive}
       showBottomBorder
     >
-      <FreehandOption
-        type={type}
-        setType={setType}
-        color={color}
-        setColor={setColor}
-        opacity={opacity}
-        handleOpacity={handleOpacity}
-        width={width}
-        handleWidth={handleWidth}
+      <InkOption
+        type={data.type}
+        color={data.color}
+        opacity={data.opacity}
+        width={data.width}
+        setDataState={setDataState}
       />
     </ExpansionPanel>
   );

+ 0 - 0
containers/HighlightTools.tsx


Неке датотеке нису приказане због велике количине промена