Przeglądaj źródła

update i18n language and optimization module

RoyLiu 5 lat temu
rodzic
commit
682e798891
100 zmienionych plików z 3612 dodań i 1063 usunięć
  1. 8 5
      actions/pdf.ts
  2. 0 95
      components/Annotation/index.tsx
  3. 7 3
      components/AnnotationItem/index.tsx
  4. 53 70
      components/AnnotationList/index.tsx
  5. 55 0
      components/AnnotationListHead/index.tsx
  6. 5 17
      components/AnnotationSelector/index.tsx
  7. 31 0
      components/AnnotationWrapper/index.tsx
  8. 5 0
      components/AnnotationWrapper/styled.ts
  9. 1 1
      components/Box/index.tsx
  10. 1 1
      components/Button/index.tsx
  11. 50 15
      components/ColorSelector/index.tsx
  12. 19 6
      components/DeleteDialog/index.tsx
  13. 1 1
      components/Dialog/index.tsx
  14. 1 1
      components/Divider/index.tsx
  15. 1 1
      components/Drawer/index.tsx
  16. 1 1
      components/ExpansionPanel/index.tsx
  17. 108 0
      components/FreeText/index.tsx
  18. 19 0
      components/FreeText/styled.ts
  19. 91 0
      components/FreeTextOption/data.ts
  20. 138 0
      components/FreeTextOption/index.tsx
  21. 0 87
      components/FreehandOption/index.tsx
  22. 65 0
      components/Highlight/index.tsx
  23. 1 0
      components/Annotation/styled.ts
  24. 51 34
      components/HighlightOption/index.tsx
  25. 18 6
      components/Icon/data.ts
  26. 1 1
      components/Icon/index.tsx
  27. 138 0
      components/Ink/index.tsx
  28. 12 0
      components/Ink/styled.ts
  29. 96 0
      components/InkOption/index.tsx
  30. 169 0
      components/Line/index.tsx
  31. 2 2
      components/Markup/index.tsx
  32. 1 1
      components/Modal/index.tsx
  33. 1 1
      components/Navbar/index.tsx
  34. 56 0
      components/OuterRect/data.ts
  35. 171 0
      components/OuterRect/index.tsx
  36. 29 0
      components/OuterRect/styled.ts
  37. 138 0
      components/OuterRectForLine/index.tsx
  38. 12 0
      components/OuterRectForLine/styled.ts
  39. 25 7
      components/Page/index.tsx
  40. 6 5
      components/Page/styled.ts
  41. 21 6
      components/Pagination/index.tsx
  42. 1 1
      components/PdfSkeleton/index.tsx
  43. 1 1
      components/Portal/index.tsx
  44. 20 5
      components/Search/index.tsx
  45. 3 5
      components/SelectBox/index.tsx
  46. 2 2
      components/SelectBox/styled.ts
  47. 83 0
      components/Shape/index.tsx
  48. 38 0
      components/ShapeOption/data.tsx
  49. 90 0
      components/ShapeOption/index.tsx
  50. 0 92
      components/ShapeTools/index.tsx
  51. 1 1
      components/Skeleton/index.tsx
  52. 8 2
      components/SliderWithTitle/index.tsx
  53. 18 7
      components/Sliders/index.tsx
  54. 101 0
      components/StickyNote/index.tsx
  55. 25 0
      components/StickyNote/styled.ts
  56. 71 0
      components/SvgShapeElement/index.tsx
  57. 7 8
      components/Tabs/index.tsx
  58. 1 1
      components/TextField/index.tsx
  59. 0 50
      components/TextTools/data.ts
  60. 0 87
      components/TextTools/index.tsx
  61. 1 1
      components/Thumbnail/index.tsx
  62. 1 1
      components/ThumbnailViewer/index.tsx
  63. 3 3
      components/Toolbar/data.ts
  64. 29 7
      components/Toolbar/index.tsx
  65. 1 1
      components/Tooltip/index.tsx
  66. 2 2
      components/Typography/index.tsx
  67. 1 1
      components/Viewer/index.tsx
  68. 1 1
      config/index.js
  69. 3 2
      constants/actionTypes.ts
  70. 13 1
      constants/index.ts
  71. 104 18
      constants/type.ts
  72. 107 36
      containers/Annotation.tsx
  73. 29 13
      containers/AnnotationList.tsx
  74. 22 0
      containers/AnnotationListHead.tsx
  75. 1 1
      containers/AutoSave.tsx
  76. 13 6
      containers/CreateForm.tsx
  77. 124 0
      containers/FreeTextTools.tsx
  78. 95 52
      containers/FreehandTools.tsx
  79. 28 22
      containers/HighlightTools.tsx
  80. 22 12
      containers/MarkupTools.tsx
  81. 1 1
      containers/Navbar.tsx
  82. 61 0
      containers/PdfPage.tsx
  83. 12 34
      containers/PdfPages.tsx
  84. 19 11
      containers/PdfViewer.tsx
  85. 1 1
      containers/Placeholder.tsx
  86. 1 1
      containers/Search.tsx
  87. 191 0
      containers/ShapeTools.tsx
  88. 15 6
      containers/Sidebar.tsx
  89. 83 0
      containers/StickyNoteTools.tsx
  90. 1 1
      containers/Thumbnails.tsx
  91. 1 1
      containers/Toolbar.tsx
  92. 13 0
      global/otherStyled.ts
  93. 2 2
      global/toolStyled.ts
  94. 85 194
      helpers/annotation.ts
  95. 5 2
      helpers/brush.ts
  96. 11 0
      helpers/dom.ts
  97. 166 0
      helpers/markup.ts
  98. 186 0
      helpers/position.ts
  99. 80 0
      helpers/svgBezierCurve.ts
  100. 0 0
      helpers/utility.ts

+ 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;

+ 7 - 3
components/AnnotationItem/index.tsx

@@ -51,8 +51,12 @@ const AnnotationItem = ({
 
   return (
     <>
-      {showPageNum && <Divider orientation="horizontal" />}
-      {showPageNum && <PageNumber>{`Page ${page}`}</PageNumber>}
+      {showPageNum ? (
+        <>
+          <Divider orientation="horizontal" />
+          <PageNumber>{`Page ${page}`}</PageNumber>
+        </>
+      ) : null}
       <AnnotationBox onClick={handleClick}>
         <Content>
           {
@@ -63,7 +67,7 @@ const AnnotationItem = ({
                   <Text>{textContent}</Text>
                   <Markup
                     position={{
-                      top: 0, left: 0, width: '100%', height: '100%',
+                      top: '0', left: '0', width: '100%', height: '100%',
                     }}
                     markupType={type}
                     bdcolor={bdcolor}

+ 53 - 70
components/AnnotationList/index.tsx

@@ -1,35 +1,37 @@
 import React, {
   useEffect, useState, useRef, useCallback,
 } from 'react';
-import queryString from 'query-string';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
-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';
 
-type Props = {
+import { Body } from '../../global/sidebarStyled';
+
+type i18nProps = {
+  t: (key: string) => string;
+};
+
+type OwnerProps = {
   isActive?: boolean;
-  close: () => void;
   annotations: AnnotationType[];
   viewport: ViewportType;
   scale: number;
   pdf: any;
 };
 
-const AnnotationsList: React.FunctionComponent<Props> = ({
+type Props = i18nProps & OwnerProps;
+
+const AnnotationList: React.FC<Props> = ({
+  t,
   isActive = false,
-  close,
   annotations,
   viewport,
   scale,
@@ -39,12 +41,6 @@ const AnnotationsList: React.FunctionComponent<Props> = ({
   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 +49,7 @@ const AnnotationsList: React.FunctionComponent<Props> = ({
       page,
       coords: position,
     });
+
     return text;
   };
 
@@ -86,58 +83,44 @@ const AnnotationsList: React.FunctionComponent<Props> = ({
   }, [isActive]);
 
   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 prevPage = index > 0 ? annotations[index - 1].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;
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(AnnotationList);

+ 55 - 0
components/AnnotationListHead/index.tsx

@@ -0,0 +1,55 @@
+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;
+};
+
+const index: React.FC<Props> = ({
+  close,
+  addAnnots,
+}: Props) => {
+  const handleExport = (): void => {
+    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" />
+      </IconWrapper>
+      <IconWrapper onClick={handleImport}>
+        <Icon glyph="import" />
+      </IconWrapper>
+    </Head>
+  );
+};
+
+export default index;

+ 5 - 17
components/AnnotationSelector/index.tsx

@@ -6,7 +6,6 @@ import Icon from '../Icon';
 import Divider from '../Divider';
 import Box from '../Box';
 import Sliders from '../Sliders';
-import DeleteDialog from '../DeleteDialog';
 
 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>
       {
@@ -66,7 +59,7 @@ const index: React.FunctionComponent<Props> = ({
           </>
         ) : (
           <>
-            <ColorSelector color={colorProps} onClick={handleClick} />
+            <ColorSelector selectedColor={colorProps} onClick={handleClick} />
             <Subtitle>opacity</Subtitle>
             <Button
               appearance="dark"
@@ -77,16 +70,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>
   );
 };

+ 31 - 0
components/AnnotationWrapper/index.tsx

@@ -0,0 +1,31 @@
+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 => { console.log('focus'); }}
+    {...rest}
+  >
+    {children}
+  </Wrapper>
+));
+
+export default index;

+ 5 - 0
components/AnnotationWrapper/styled.ts

@@ -0,0 +1,5 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+  touch-action: 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) => {

+ 1 - 1
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,

+ 50 - 15
components/ColorSelector/index.tsx

@@ -3,33 +3,68 @@ import React from 'react';
 import Icon from '../Icon';
 import Typography from '../Typography';
 
+import { color as theme } from '../../constants/style';
 import { Group, Item, Circle } 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';
 };
 
-const ColorSelector: React.FunctionComponent<Props> = ({
-  showTitle = false,
-  color = '',
+const ColorSelector: React.FC<Props> = ({
+  title = '',
+  mode = 'normal',
+  selectedColor = '',
   onClick,
 }: Props) => (
   <>
-    { showTitle ? <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">Color</Typography> : null}
+    { title ? (
+      <Typography variant="subtitle" style={{ marginTop: '8px' }} align="left">
+        {title}
+      </Typography>
+    ) : null}
     <Group>
-      {data.map(ele => (
-        <Item
-          key={ele.key}
-          selected={color === ele.color}
-          onMouseDown={(): void => { onClick(ele.color); }}
-        >
-          <Circle color={ele.color} />
-        </Item>
-      ))}
+      {
+        data.map((ele, index) => {
+          if (mode === 'shape' && index === 0) {
+            return (
+              <Item
+                key={ele.key}
+                selected={selectedColor === 'transparency'}
+                onMouseDown={(): void => { onClick('transparency'); }}
+              >
+                <Icon glyph="none" />
+              </Item>
+            );
+          }
+          if (mode === 'watermark' && index === 0) {
+            const color = theme.gray;
+            return (
+              <Item
+                key={color}
+                selected={selectedColor === color}
+                onMouseDown={(): void => { onClick(color); }}
+              >
+                <Circle color={color} />
+              </Item>
+            );
+          }
+
+          return (
+            <Item
+              key={ele.key}
+              selected={selectedColor === ele.color}
+              onMouseDown={(): void => { onClick(ele.color); }}
+            >
+              <Circle color={ele.color} />
+            </Item>
+          );
+        })
+      }
       <Item>
         <Icon glyph="color-picker" />
       </Item>

+ 19 - 6
components/DeleteDialog/index.tsx

@@ -1,4 +1,6 @@
 import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
 import Dialog from '../Dialog';
 import Button from '../Button';
@@ -7,37 +9,48 @@ import {
   TextWrapper, BtnWrapper,
 } from './styled';
 
-type Props = {
+type i18nProps = {
+  t: (key: string) => string;
+}
+
+type OwnerProps = {
   open: boolean;
   onCancel: (e: any) => void;
   onDelete: (e: any) => void;
 };
 
-const index: React.FunctionComponent<Props> = ({
+type Props = i18nProps & OwnerProps
+
+const index: React.FC<Props> = ({
+  t,
   open,
   onCancel,
   onDelete,
 }: Props) => (
   <Dialog open={open}>
     <TextWrapper>
-      This will permanently delete the annotation. Do you want to continue?
+      {t('deleteAnnotationAlert')}
     </TextWrapper>
     <BtnWrapper>
       <Button
         appearance="default-hollow"
         onClick={onCancel}
       >
-        Cancel
+        {t('cancel')}
       </Button>
       <Button
         appearance="primary"
         onClick={onDelete}
         style={{ marginLeft: '16px' }}
       >
-        OK
+        {t('continue')}
       </Button>
     </BtnWrapper>
   </Dialog>
 );
 
-export default index;
+const translator = withTranslation('dialog');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(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,

+ 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 (
+    <>
+      <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}
+          />
+        ) : ''
+      }
+    </>
+  );
+};
+
+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,
+  },
+];

+ 138 - 0
components/FreeTextOption/index.tsx

@@ -0,0 +1,138 @@
+import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
+
+import Icon from '../Icon';
+import Typography from '../Typography';
+import SliderWithTitle from '../SliderWithTitle';
+import ColorSelector from '../ColorSelector';
+import SelectBox from '../SelectBox';
+import { OptionPropsType, SelectOptionType } from '../../constants/type';
+
+import {
+  Wrapper, Group, Item,
+} from '../../global/toolStyled';
+
+import {
+  fontOptions,
+  sizeOptions,
+  alignOptions,
+  styleOptions,
+} from './data';
+
+type i18nProps = {
+  t: (key: string) => string;
+}
+
+type Props = i18nProps & OptionPropsType;
+
+const TextOption: React.SFC<Props> = ({
+  t,
+  fontName,
+  fontSize,
+  align,
+  fontStyle,
+  color,
+  opacity,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => (
+  <>
+    <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>
+  </>
+);
+
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OptionPropsType;
+
+export default translator<TransProps>(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;

+ 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;
 `;

+ 51 - 34
components/HighlightOption/index.tsx

@@ -1,51 +1,68 @@
 import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
 import Icon from '../Icon';
 import Typography from '../Typography';
 import ColorSelector from '../ColorSelector';
 import SliderWithTitle from '../SliderWithTitle';
+import { OptionPropsType } from '../../constants/type';
 
-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;
-};
+type i18nProps = {
+  t: (key: string) => string;
+}
 
-const HighlightOption = ({
+type Props = OptionPropsType & i18nProps;
+
+const HighlightOption: React.SFC<Props> = ({
+  t,
   type,
-  setType,
   color,
-  setColor,
   opacity,
-  handleOpacity,
-}: Props): React.ReactElement => (
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => (
   <>
-    <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}
-    />
+    <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;
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OptionPropsType;
+
+export default translator<TransProps>(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;

+ 1 - 1
components/Icon/index.tsx

@@ -15,7 +15,7 @@ type Props = {
   isActive? : boolean;
 };
 
-const Icon: React.FunctionComponent<Props> = ({
+const Icon: React.FC<Props> = ({
   glyph,
   id = '',
   onClick,

+ 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')};
+`;

+ 96 - 0
components/InkOption/index.tsx

@@ -0,0 +1,96 @@
+import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
+
+import { OptionPropsType } from '../../constants/type';
+import Icon from '../Icon';
+import Button from '../Button';
+import Typography from '../Typography';
+import ColorSelector from '../ColorSelector';
+import SliderWithTitle from '../SliderWithTitle';
+
+import { Group, Item, Wrapper } from '../../global/toolStyled';
+
+type i18nProps = {
+  t: (key: string) => string;
+}
+
+type Props = i18nProps & OptionPropsType;
+
+const FreehandOption: React.SFC<Props> = ({
+  t,
+  type,
+  color,
+  opacity,
+  width,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => (
+  <>
+    <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>
+  </>
+);
+
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OptionPropsType;
+
+export default translator<TransProps>(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) => (

+ 1 - 1
components/Navbar/index.tsx

@@ -15,7 +15,7 @@ type Props = {
   fileName: string;
 };
 
-const Navbar: React.FunctionComponent<Props> = ({
+const Navbar: React.FC<Props> = ({
   onClick,
   navbarState,
   children,

+ 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;

+ 171 - 0
components/OuterRect/index.tsx

@@ -0,0 +1,171 @@
+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(100);
+
+  const handleMouseDown = (e: React.MouseEvent): void => {
+    const operatorId = (e.target as HTMLElement).dataset.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);
+
+    return (): void => {
+      window.removeEventListener('mouseup', 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}
+        data-id="move"
+      />
+      {
+        onScale && data.map((attr: CircleType) => (
+          <Circle
+            key={attr.direction}
+            data-id={attr.direction}
+            onMouseDown={handleMouseDown}
+            fill={color.primary}
+            {...attr}
+          />
+        ))
+      }
+    </SVG>
+  );
+};
+
+export default index;

+ 29 - 0
components/OuterRect/styled.ts

@@ -0,0 +1,29 @@
+import styled from 'styled-components';
+
+export const SVG = styled.svg`
+  position: absolute;
+  cursor: pointer;
+  z-index: 10;
+`;
+
+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).dataset.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``;

+ 21 - 6
components/Pagination/index.tsx

@@ -1,19 +1,30 @@
 import React, { useEffect, useState } from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
 import {
   Container, Text, Input, ArrowButton,
 } from './styled';
 
-type Props = {
+type I18nProps = {
+  t: (key: string) => string;
+};
+
+type OwnerProps = {
   currentPage: number;
   totalPage: number;
   onChange: (page: number) => void;
 };
 
-const Pagination: React.FunctionComponent<Props> = ({
+type Props = OwnerProps & I18nProps;
+
+const Pagination: React.FC<Props> = ({
+  t,
   currentPage = 1,
   totalPage = 1,
-  onChange,
+  onChange = (): void => {
+    // do nothing
+  },
 }: Props) => {
   const [inputValue, setInputValue] = useState(currentPage);
 
@@ -53,7 +64,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 +74,7 @@ const Pagination: React.FunctionComponent<Props> = ({
       />
       <ArrowButton onClick={handleRightClick} variant="right" />
       <Text>
-        of
+        {t('of')}
         {' '}
         {totalPage}
       </Text>
@@ -71,4 +82,8 @@ const Pagination: React.FunctionComponent<Props> = ({
   );
 };
 
-export default Pagination;
+const translator = withTranslation('toolbar');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(Pagination);

+ 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 = '',
 }) => {

+ 20 - 5
components/Search/index.tsx

@@ -1,4 +1,6 @@
 import React, { useRef, useEffect, MutableRefObject } from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
 import Button from '../Button';
 import Portal from '../Portal';
@@ -7,7 +9,11 @@ import {
   Wrapper, InputWrapper, Input, ResultInfo, ArrowButton,
 } from './styled';
 
-type Props = {
+type i18nProps = {
+  t: (key: string) => string;
+}
+
+type OwnerProps = {
   matchesTotal: number;
   matchIndex: number;
   onEnter: (val: string) => void;
@@ -17,7 +23,10 @@ type Props = {
   close: () => void;
 };
 
-const Search: React.FunctionComponent<Props> = ({
+type Props = i18nProps & OwnerProps;
+
+const Search: React.FC<Props> = ({
+  t,
   matchesTotal = 0,
   matchIndex = 1,
   onPrev,
@@ -44,17 +53,23 @@ 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}` : ''}
           </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>
   );
 };
 
-export default Search;
+const translator = withTranslation('toolbar');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(Search);

+ 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}

+ 2 - 2
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;
 `;
 
@@ -21,6 +20,7 @@ export const Selected = styled.div`
   padding: 0 6px 0 14px;
   transition: background-color 200ms cubic-bezier(0.0, 0, 0.2, 1) 0ms;
   outline: none;
+  width: 100%;
 `;
 
 export const InputContent = styled.input`

+ 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: '',
+  },
+];

+ 90 - 0
components/ShapeOption/index.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
+
+import { OptionPropsType } from '../../constants/type';
+import Typography from '../Typography';
+import SelectBox from '../SelectBox';
+import SliderWithTitle from '../SliderWithTitle';
+import ColorSelector from '../ColorSelector';
+
+import {
+  Wrapper,
+} from '../../global/toolStyled';
+
+import {
+  shapeOptions, typeOptions,
+} from './data';
+
+type i18nProps = {
+  t: (key: string) => string;
+}
+
+type Props = i18nProps & OptionPropsType;
+
+const ShapeOption: React.SFC<Props> = ({
+  t,
+  shape,
+  color,
+  opacity,
+  width,
+  setDataState = (): void => {
+    // do nothing
+  },
+}: Props) => (
+  <>
+    <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>
+  </>
+);
+
+const translator = withTranslation('sidebar');
+
+type TransProps = WithTranslation & OptionPropsType;
+
+export default translator<TransProps>(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>

+ 18 - 7
components/Sliders/index.tsx

@@ -12,15 +12,19 @@ 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);
@@ -28,7 +32,7 @@ const Sliders: React.FunctionComponent<Props> = ({
   const [isActive, setActive] = useState(false);
   let subscription: 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 { current: slider } = sliderRef;
@@ -45,10 +49,9 @@ 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);
       }
     }
   };
@@ -75,11 +78,19 @@ const Sliders: React.FunctionComponent<Props> = ({
     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

+ 101 - 0
components/StickyNote/index.tsx

@@ -0,0 +1,101 @@
+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 Note: 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' }}
+        />
+      </AnnotationContainer>
+      {
+        isEdit ? (
+          <TextAreaContainer
+            top={`${annotRect.top + 36}px`}
+            left={`${annotRect.left - 106}px`}
+          >
+            <TextArea
+              autoFocus
+              onBlur={onBlur}
+              defaultValue={content}
+              onChange={handleChange}
+            />
+            <TrashCan>
+              <Icon
+                glyph="trash-2"
+                onClick={onDelete}
+              />
+            </TrashCan>
+          </TextAreaContainer>
+        ) : null
+      }
+      <OuterRect
+        top={annotRect.top}
+        left={annotRect.left}
+        width={25}
+        height={25}
+        onMove={handleMove}
+        onClick={handleClick}
+      />
+    </>
+  );
+};
+
+export default Note;

+ 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;
+`;

+ 71 - 0
components/SvgShapeElement/index.tsx

@@ -0,0 +1,71 @@
+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) => {
+  const generateShape = (): React.ReactNode => {
+    switch (shape) {
+      case 'Circle':
+        return (
+          <ellipse
+            cx="50%"
+            cy="50%"
+            rx={(width - bdwidth) / 2}
+            ry={(height - bdwidth) / 2}
+            stroke={bdcolor}
+            strokeWidth={bdwidth}
+            strokeOpacity={transparency}
+            fill={fcolor}
+            fillOpacity={ftransparency}
+          />
+        );
+      case 'Square': {
+        const indent = bdwidth / 2;
+        return (
+          <rect
+            x={indent}
+            y={indent}
+            width={`${width - bdwidth}px`}
+            height={`${height - bdwidth}px`}
+            stroke={bdcolor}
+            strokeWidth={bdwidth}
+            strokeOpacity={transparency}
+            fill={fcolor}
+            fillOpacity={ftransparency}
+          />
+        );
+      }
+      default: {
+        return {};
+      }
+    }
+  };
+
+  return (
+    <svg
+      viewBox={`0 0 ${width} ${height}`}
+    >
+      {generateShape()}
+    </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 (

+ 1 - 1
components/TextField/index.tsx

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

+ 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,

+ 3 - 3
components/Toolbar/data.ts

@@ -1,8 +1,8 @@
-export default {
+export default (t: (key: string) => string): Record<string, any> => ({
   scaleOptions: [
     {
       key: 'fit',
-      content: 'Fit',
+      content: t('fit'),
       value: 'fit',
     },
     {
@@ -31,4 +31,4 @@ export default {
       value: 250,
     },
   ],
-};
+});

+ 29 - 7
components/Toolbar/index.tsx

@@ -1,4 +1,6 @@
 import React from 'react';
+import { WithTranslation } from 'next-i18next';
+import { withTranslation } from '../../i18n';
 
 import Icon from '../Icon';
 import Pagination from '../Pagination';
@@ -8,9 +10,13 @@ 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 = {
+type I18nProps = {
+  t: (key: string) => string;
+};
+
+type OwnerProps = {
   totalPage: number;
   currentPage: number;
   setCurrentPage: (num: number) => void;
@@ -24,7 +30,10 @@ type Props = {
   handleHandClick: () => void;
 };
 
-const Toolbar: React.FunctionComponent<Props> = ({
+type Props = I18nProps & OwnerProps;
+
+const Toolbar: React.FC<Props> = ({
+  t,
   totalPage,
   currentPage,
   setCurrentPage,
@@ -37,6 +46,8 @@ const Toolbar: React.FunctionComponent<Props> = ({
   toggleDisplayMode,
   handleHandClick,
 }: Props) => {
+  const data = dataset(t);
+
   const handleClockwiseRotation = (): void => {
     const r = rotation + 90;
     changeRotate(r);
@@ -48,13 +59,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 +85,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} />
@@ -92,4 +110,8 @@ const Toolbar: React.FunctionComponent<Props> = ({
   );
 };
 
-export default Toolbar;
+const translator = withTranslation('toolbar');
+
+type TransProps = WithTranslation & OwnerProps;
+
+export default translator<TransProps>(Toolbar);

+ 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,

+ 2 - 2
components/Typography/index.tsx

@@ -10,12 +10,12 @@ type Props = {
   align?: 'left' | 'center' | 'right';
 };
 
-const Typography: React.FunctionComponent<Props> = ({
+const Typography: React.FC<Props> = ({
   variant = 'title',
   children,
   ...rest
 }: Props) => {
-  const getComponent = (): React.FunctionComponent => {
+  const getComponent = (): React.FC => {
     if (variant === 'title') {
       return Title;
     }

+ 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 - 1
config/index.js

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

+ 3 - 2
constants/actionTypes.ts

@@ -12,5 +12,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';

+ 13 - 1
constants/index.ts

@@ -2,9 +2,21 @@ 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',
+  watermark: 'watermark',
+};

+ 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,23 @@ type Props = AnnotationType & {
   scale: number;
 };
 
-const Annotation: React.FunctionComponent<Props> = ({
+const Annotation: React.FC<Props> = ({
   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 +44,6 @@ const Annotation: React.FunctionComponent<Props> = ({
     }
   };
 
-  const handleBlur = (): void => {
-    setCollapse(true);
-  };
-
   const handleMouseOver = (): void => {
     setMouseOver(true);
   };
@@ -49,43 +53,110 @@ 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 Component = (type: string): React.ReactNode => {
+    const childProps = {
+      obj_type,
+      obj_attr,
+      scale,
+      isCollapse,
+      isCovered,
+      mousePosition,
+      onUpdate: handleUpdate,
+      onDelete: deleteDialogToggle,
+      viewport,
+      onEdit: handleEdit,
+      isEdit,
+      onBlur: handleInputBlur,
+    };
+
+    switch (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} />;
+    }
+  };
+
+  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}
+      {Component(obj_type)}
+      <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 { MARKUP_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 MARKUP_TYPE_ARRAY = Object.values(MARKUP_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) => MARKUP_TYPE_ARRAY.includes(ele.obj_type),
+          )}
+          viewport={viewport}
+          scale={scale}
+          pdf={pdf}
+          isActive={isActive}
+        />
+      </Container>
+    </Drawer>
   );
 };
 

+ 22 - 0
containers/AnnotationListHead.tsx

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

+ 1 - 1
containers/AutoSave.tsx

@@ -4,7 +4,7 @@ import { useSnackbar } from 'notistack';
 import useAutoSave from '../hooks/useAutoSave';
 import Icon from '../components/Icon';
 
-const index: React.FunctionComponent = () => {
+const index: React.FC = () => {
   const [isSaved, isSaving] = useAutoSave();
   const { enqueueSnackbar, closeSnackbar } = useSnackbar();
 

+ 13 - 6
containers/CreateForm.tsx

@@ -1,4 +1,5 @@
 import React from 'react';
+import { withTranslation } from '../i18n';
 
 import Button from '../components/Button';
 import Icon from '../components/Icon';
@@ -8,7 +9,13 @@ import useActions from '../actions';
 import useStore from '../store';
 import { BtnWrapper } from '../global/toolStyled';
 
-const CreateForm: React.FunctionComponent = () => {
+type Props = {
+  t: (key: string) => string;
+}
+
+const CreateForm: React.FC<Props> = ({
+  t,
+}: Props) => {
   const [{ sidebarState }, dispatch] = useStore();
   const { setSidebar } = useActions(dispatch);
 
@@ -25,7 +32,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,23 +40,23 @@ 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>
   );
 };
 
-export default CreateForm;
+export default withTranslation('sidebar')(CreateForm);

+ 124 - 0
containers/FreeTextTools.tsx

@@ -0,0 +1,124 @@
+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,
+    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): 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);
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        pdfViewer.removeEventListener('mousedown', 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;

+ 95 - 52
containers/FreehandTools.tsx

@@ -1,78 +1,124 @@
 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(50);
+
+  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);
-
-      setElement(element);
-
-      const svg = page.querySelector('svg');
-      if (svg && element) {
-        svg.appendChild(element);
-      }
+    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);
+      freehand.obj_attr.position = [[freehand.obj_attr.position as PointType]];
+
+      addAnnots([freehand], true);
     }
-  }, [color, width]);
+  }, [data, viewport, scale]);
 
   const handleMouseUp = (): void => {
     setRef(null);
-    setElement(null);
+    setUuid('');
   };
 
   useEffect(() => {
-    if (polylineElement) {
-      const coordinate = { x: position.x, y: position.y };
-      completePath(polylineElement, coordinate);
+    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;
+      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
+      ) {
+        annotations[index].obj_attr.position[0].push(coordinates);
+        updateAnnots([...annotations]);
+      }
     }
-  }, [polylineElement, position]);
+  }, [annotations, cursorPosition, uuid]);
 
   useEffect(() => {
-    if (isActive) {
-      window.addEventListener('mousedown', handleMouseDown);
-      window.addEventListener('mouseup', handleMouseUp);
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
+    if (isActive && pdfViewer) {
+      pdfViewer.addEventListener('mousedown', handleMouseDown);
+      pdfViewer.addEventListener('mouseup', handleMouseUp);
     }
 
     return (): void => {
-      window.removeEventListener('mousedown', handleMouseDown);
-      window.removeEventListener('mouseup', handleMouseUp);
+      if (pdfViewer) {
+        pdfViewer.removeEventListener('mousedown', handleMouseDown);
+        pdfViewer.removeEventListener('mouseup', handleMouseUp);
+      }
     };
-  }, [isActive]);
+  }, [isActive, handleMouseDown]);
 
   return (
     <ExpansionPanel
@@ -84,21 +130,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>
   );

+ 28 - 22
containers/HighlightTools.tsx

@@ -4,54 +4,64 @@ import Icon from '../components/Icon';
 import Button from '../components/Button';
 import ExpansionPanel from '../components/ExpansionPanel';
 import HighlightOption from '../components/HighlightOption';
+import { OptionPropsType } from '../constants/type';
 
 import useActions from '../actions';
 import useStore from '../store';
 
-import { getAnnotationWithSelection } from '../helpers/annotation';
+import { getMarkupWithSelection } from '../helpers/markup';
 
 type Props = {
+  title: string;
   isActive: boolean;
   onClick: () => void;
 };
 
-const HighlightTools: React.FunctionComponent<Props> = ({
+const HighlightTools: React.FC<Props> = ({
+  title,
   isActive,
   onClick,
 }: Props) => {
-  const [color, setColor] = useState('#FCFF36');
-  const [type, setType] = useState('highlight');
-  const [opacity, setOpacity] = useState(40);
+  const [data, setData] = useState({
+    type: 'highlight',
+    color: '#FCFF36',
+    opacity: 40,
+  });
 
   const [{ scale }, dispatch] = useStore();
-  const { addAnnotation } = useActions(dispatch);
+  const { addAnnots } = useActions(dispatch);
 
-  const handleOpacity = (value: number): void => {
-    setOpacity(value);
+  const setDataState = (obj: OptionPropsType): void => {
+    setData(prev => ({
+      ...prev,
+      ...obj,
+    }));
   };
 
   const selectRange = useCallback((e: MouseEvent): void => {
     if (e.target && isActive) {
       const textLayer = (e.target as HTMLElement).parentNode as HTMLElement;
       if (textLayer && textLayer.getAttribute('data-id') === 'text-layer') {
-        const newAnnotations = getAnnotationWithSelection({
-          color, type, opacity, scale,
+        const newMarkup = getMarkupWithSelection({
+          ...data, scale,
         });
 
-        if (newAnnotations && newAnnotations.length) {
-          addAnnotation(newAnnotations, true);
+        if (newMarkup) {
+          addAnnots([newMarkup], true);
         }
       }
     }
-  }, [isActive, color, type, opacity, scale]);
+  }, [isActive, data, scale]);
 
   useEffect(() => {
-    document.addEventListener('mouseup', selectRange);
+    if (isActive) {
+      document.addEventListener('mouseup', selectRange);
+    }
 
     return (): void => {
       document.removeEventListener('mouseup', selectRange);
     };
-  }, [selectRange]);
+  }, [isActive, selectRange]);
 
   return (
     <ExpansionPanel
@@ -63,19 +73,15 @@ const HighlightTools: React.FunctionComponent<Props> = ({
           isActive={isActive}
         >
           <Icon glyph="highlight" style={{ marginRight: '10px' }} />
-          Highlight Tools
+          {title}
         </Button>
       )}
       isActive={isActive}
       showBottomBorder
     >
       <HighlightOption
-        type={type}
-        setType={setType}
-        color={color}
-        setColor={setColor}
-        opacity={opacity}
-        handleOpacity={handleOpacity}
+        {...data}
+        setDataState={setDataState}
       />
     </ExpansionPanel>
   );

+ 22 - 12
containers/MarkupTools.tsx

@@ -1,18 +1,25 @@
 import React, { useEffect } from 'react';
+import { withTranslation } from '../i18n';
 
 import Button from '../components/Button';
 import ExpansionPanel from '../components/ExpansionPanel';
 import Icon from '../components/Icon';
 import HighlightTools from './HighlightTools';
 import FreehandTools from './FreehandTools';
-import TextTools from '../components/TextTools';
-import ShapeTools from '../components/ShapeTools';
+import TextTools from './FreeTextTools';
+import StickyNoteTools from './StickyNoteTools';
+import ShapeTools from './ShapeTools';
 
 import useActions from '../actions';
 import useStore from '../store';
-import { BtnWrapper } from '../global/toolStyled';
 
-const MarkupTools: React.FunctionComponent = () => {
+type Props = {
+  t: (key: string) => string;
+}
+
+const MarkupTools: React.FC<Props> = ({
+  t,
+}: Props) => {
   const [{ sidebarState, markupToolState }, dispatch] = useStore();
   const { setSidebar, setMarkupTool } = useActions(dispatch);
 
@@ -47,30 +54,33 @@ const MarkupTools: React.FunctionComponent = () => {
           onClick={(): void => { onClickSidebar('markup-tools'); }}
         >
           <Icon glyph="markup-tools" style={{ marginRight: '10px' }} />
-          Markup Tools
+          {t('markupTool')}
         </Button>
       )}
       isActive={sidebarState === 'markup-tools'}
     >
       <HighlightTools
+        title={t('annotate')}
         isActive={markupToolState === 'highlight'}
         onClick={(): void => { onClickTool('highlight'); }}
       />
       <FreehandTools
+        title={t('freehand')}
         isActive={markupToolState === 'freehand'}
         onClick={(): void => { onClickTool('freehand'); }}
       />
       <TextTools
+        title={t('textBox')}
         isActive={markupToolState === 'text'}
         onClick={(): void => { onClickTool('text'); }}
       />
-      <BtnWrapper>
-        <Button shouldFitContainer align="left" onClick={(): void => { onClickTool('sticky'); }} isActive={markupToolState === 'sticky'}>
-          <Icon glyph="sticky-note" style={{ marginRight: '10px' }} />
-          Sticky Note
-        </Button>
-      </BtnWrapper>
+      <StickyNoteTools
+        title={t('stickyNote')}
+        isActive={markupToolState === 'sticky'}
+        onClick={(): void => { onClickTool('sticky'); }}
+      />
       <ShapeTools
+        title={t('shape')}
         isActive={markupToolState === 'shape'}
         onClick={(): void => { onClickTool('shape'); }}
       />
@@ -78,4 +88,4 @@ const MarkupTools: React.FunctionComponent = () => {
   );
 };
 
-export default MarkupTools;
+export default withTranslation('sidebar')(MarkupTools);

+ 1 - 1
containers/Navbar.tsx

@@ -10,7 +10,7 @@ import Thumbnails from './Thumbnails';
 import AnnotationList from './AnnotationList';
 import { downloadFileWithUri } from '../helpers/utility';
 
-const Navbar: React.FunctionComponent = () => {
+const Navbar: React.FC = () => {
   const [{
     navbarState,
     displayMode,

+ 61 - 0
containers/PdfPage.tsx

@@ -0,0 +1,61 @@
+import React from 'react';
+
+import { AnnotationType, RenderingStateType } from '../constants/type';
+import Page from '../components/Page';
+import Annotation from './Annotation';
+import { getPdfPage } from '../helpers/pdf';
+
+import useStore from '../store';
+
+type Props = {
+  index: number;
+  renderingState: RenderingStateType;
+};
+
+const PdfPage: React.FC<Props> = ({
+  index,
+  renderingState,
+}: Props) => {
+  const [{
+    viewport,
+    pdf,
+    rotation,
+    annotations,
+    scale,
+    watermark,
+  }] = useStore();
+
+  const getAnnotationWithPage = (
+    arr: AnnotationType[],
+    pageNum: number,
+  ): React.ReactNode[] => {
+    const result: React.ReactNode[] = [];
+
+    arr.forEach((ele: AnnotationType, i: number) => {
+      const page = ele.obj_attr ? ele.obj_attr.page + 1 : -1;
+
+      if (page === pageNum) {
+        result.push(
+          <Annotation key={`annotations_${pageNum + i}`} scale={scale} index={i} {...ele} />,
+        );
+      }
+    });
+
+    return result;
+  };
+
+  return (
+    <Page
+      pageNum={index}
+      renderingState={renderingState}
+      viewport={viewport}
+      scale={scale}
+      getPage={(): Promise<any> => getPdfPage(pdf, index)}
+      rotation={rotation}
+      watermark={watermark}
+      annotations={getAnnotationWithPage(annotations, index)}
+    />
+  );
+};
+
+export default PdfPage;

+ 12 - 34
containers/PdfPages.tsx

@@ -3,12 +3,10 @@ import React, {
 } from 'react';
 import _ from 'lodash';
 
-import { AnnotationType, ScrollStateType } from '../constants/type';
-import Page from '../components/Page';
+import { ScrollStateType } from '../constants/type';
 import Viewer from '../components/Viewer';
-import Annotation from './Annotation';
+import PdfPage from './PdfPage';
 import { watchScroll } from '../helpers/utility';
-import { getPdfPage } from '../helpers/pdf';
 
 import useStore from '../store';
 
@@ -16,7 +14,7 @@ type Props = {
   scrollToUpdate: (state: ScrollStateType) => void;
 };
 
-const PdfPages: React.FunctionComponent<Props> = ({
+const PdfPages: React.FC<Props> = ({
   scrollToUpdate,
 }: Props) => {
   const [elements, setElement] = useState<React.ReactNode[]>([]);
@@ -25,38 +23,22 @@ const PdfPages: React.FunctionComponent<Props> = ({
     totalPage,
     currentPage,
     viewport,
-    pdf,
     rotation,
     displayMode,
     annotations,
-    scale,
   }] = useStore();
 
-  const getAnnotations = (arr: AnnotationType[], pageNum: number): any[] => {
-    const result: any[] = [];
-    arr.forEach((ele: AnnotationType, index: number) => {
-      const page = ele.obj_attr ? ele.obj_attr.page as number + 1 : 0;
-      if (page === pageNum) {
-        result.push(<Annotation key={`annotations_${pageNum + index}`} scale={scale} index={index} {...ele} />);
-      }
-    });
-    return result;
-  };
-
   const createPages = (): void => {
     const pagesContent: React.ReactNode[] = [];
 
     for (let i = 1; i <= totalPage; i += 1) {
-      const annotationElements = getAnnotations(annotations, i);
+      const key = `page-${i}`;
+
       const component = (
-        <Page
-          key={`page-${i}`}
-          pageNum={i}
+        <PdfPage
+          key={key}
+          index={i}
           renderingState={_.range(1, 4).includes(i) ? 'RENDERING' : 'LOADING'}
-          viewport={viewport}
-          getPage={(): Promise<any> => getPdfPage(pdf, i)}
-          rotation={rotation}
-          annotations={annotationElements}
         />
       );
       pagesContent.push(component);
@@ -73,17 +55,13 @@ const PdfPages: React.FunctionComponent<Props> = ({
     while (currentPage) {
       if (elements[index]) {
         const pageNum = index + 1;
-        const annotationElements = getAnnotations(annotations, pageNum);
+        const key = `page-${pageNum}`;
 
         elements[index] = (
-          <Page
-            key={`page-${pageNum}`}
-            pageNum={pageNum}
+          <PdfPage
+            key={key}
+            index={pageNum}
             renderingState={renderingIndexQueue.includes(pageNum) ? 'RENDERING' : 'LOADING'}
-            viewport={viewport}
-            getPage={(): Promise<any> => getPdfPage(pdf, pageNum)}
-            rotation={rotation}
-            annotations={annotationElements}
           />
         );
       }

+ 19 - 11
containers/PdfViewer.tsx

@@ -9,11 +9,12 @@ import { fetchPdf } from '../helpers/pdf';
 import { ProgressType, ScrollStateType } from '../constants/type';
 import { scrollIntoView } from '../helpers/utility';
 import { parseAnnotationFromXml } from '../helpers/annotation';
+import { parseWatermarkFromXml } from '../helpers/watermark';
 
 import useActions from '../actions';
 import useStore from '../store';
 
-const PdfViewer: React.FunctionComponent = () => {
+const PdfViewer: React.FC = () => {
   const [{
     viewport,
     pdf,
@@ -28,8 +29,9 @@ const PdfViewer: React.FunctionComponent = () => {
     setViewport,
     setCurrentPage,
     setInfo,
-    addAnnotation,
+    addAnnots,
     changeScale,
+    updateWatermark,
   } = useActions(dispatch);
   const currentPageRef = useRef(0);
 
@@ -49,6 +51,20 @@ const PdfViewer: React.FunctionComponent = () => {
     setViewport(iViewport);
   };
 
+  const getXfdfFile = (token: string): void => {
+    fetchXfdf(token)
+      .then((xfdf) => {
+        const annotations = parseAnnotationFromXml(xfdf);
+        const watermark = parseWatermarkFromXml(xfdf);
+
+        addAnnots(annotations);
+        updateWatermark(watermark);
+      })
+      .catch((error) => {
+        console.log(error);
+      });
+  };
+
   const pdfGenerator = async (): Promise<any> => {
     const parsed = queryString.parse(window.location.search);
 
@@ -56,15 +72,7 @@ const PdfViewer: React.FunctionComponent = () => {
       const token = parsed.token as string;
       const result = await initialPdfFile(token);
 
-      fetchXfdf(token)
-        .then((xfdf) => {
-          const annot = parseAnnotationFromXml(xfdf);
-          addAnnotation(annot, false);
-        })
-        .catch((error) => {
-          console.log(error);
-        });
-
+      getXfdfFile(token);
       setInfo({
         token: parsed.token,
         id: result.data.transaction_id,

+ 1 - 1
containers/Placeholder.tsx

@@ -5,7 +5,7 @@ import useStore from '../store';
 
 import { delay } from '../helpers/time';
 
-const Placeholder: React.FunctionComponent = () => {
+const Placeholder: React.FC = () => {
   const [{ progress }] = useStore();
   const [done, setDone] = useState(false);
 

+ 1 - 1
containers/Search.tsx

@@ -10,7 +10,7 @@ import { normalize, calcFindPhraseMatch, convertMatches } from '../helpers/pdf';
 import { scrollIntoView } from '../helpers/utility';
 import { delay } from '../helpers/time';
 
-const Search: React.FunctionComponent = () => {
+const Search: React.FC = () => {
   const queryString = useRef('');
   const pageIndex = useRef(0);
   const matchIndex = useRef(-1);

+ 191 - 0
containers/ShapeTools.tsx

@@ -0,0 +1,191 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import { ANNOTATION_TYPE } from '../constants';
+import Button from '../components/Button';
+import ExpansionPanel from '../components/ExpansionPanel';
+import Icon from '../components/Icon';
+import ShapeOption from '../components/ShapeOption';
+import { OptionPropsType } from '../constants/type';
+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 Shape: React.FC<Props> = ({
+  title,
+  isActive,
+  onClick,
+}: Props) => {
+  const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
+  const [uuid, setUuid] = useState('');
+  const [data, setData] = useState({
+    shape: 'circle',
+    type: 'border',
+    color: '#FF1B89',
+    opacity: 100,
+    width: 6,
+  });
+  const [cursorPosition, setRef] = useCursorPosition();
+
+  const [{ viewport, scale, annotations }, dispatch] = useStore();
+  const { addAnnots, updateAnnots } = useActions(dispatch);
+
+  const setDataState = (obj: OptionPropsType): void => {
+    setData(prev => ({
+      ...prev,
+      ...obj,
+    }));
+  };
+
+  const convertPosition = (type: string, x1: number, y1: number, x2: number, y2: number): any => {
+    switch (type) {
+      case 'Line':
+        return {
+          start: {
+            x: x1,
+            y: y1,
+          },
+          end: {
+            x: x2,
+            y: y2,
+          },
+        };
+      default:
+        return {
+          top: y2 > y1 ? y1 : y2,
+          left: x2 > x1 ? x1 : x2,
+          right: x2 > x1 ? x2 : x1,
+          bottom: y2 > y1 ? y2 : y1,
+        };
+    }
+  };
+
+  const addShape = useCallback((
+    pageEle: HTMLElement,
+    event: MouseEvent,
+    attributes: OptionPropsType,
+  ): void => {
+    const {
+      shape = '', type, opacity, color, width = 0,
+    } = attributes;
+    const pageNum = pageEle.getAttribute('data-page-num') || 0;
+    const coordinate = getAbsoluteCoordinate(pageEle, event);
+    const id = uuidv4();
+
+    setUuid(id);
+    setStartPosition(coordinate);
+
+    const shapeType = ANNOTATION_TYPE[shape];
+    const position = convertPosition(
+      shapeType, coordinate.x, coordinate.y, coordinate.x, coordinate.y,
+    );
+    const annoteData = {
+      id,
+      obj_type: shapeType,
+      obj_attr: {
+        page: pageNum as number,
+        position,
+        bdcolor: color,
+        fcolor: type === 'fill' ? color : undefined,
+        transparency: opacity,
+        ftransparency: type === 'fill' ? opacity : undefined,
+        bdwidth: width,
+        is_arrow: shape === 'arrow',
+      },
+    };
+    const shapeAnnotation = parseAnnotationObject(annoteData, viewport.height, scale);
+
+    addAnnots([shapeAnnotation], true);
+  }, [viewport, scale, data]);
+
+  const handleMouseDown = (event: MouseEvent): void => {
+    event.preventDefault();
+
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      addShape(pageEle, event, data);
+      setRef(pageEle);
+    }
+  };
+
+  const handleMouseUp = (): void => {
+    setRef(null);
+    setUuid('');
+    setStartPosition({ x: 0, y: 0 });
+  };
+
+  useEffect(() => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
+    if (isActive && pdfViewer) {
+      pdfViewer.addEventListener('mousedown', handleMouseDown);
+      pdfViewer.addEventListener('mouseup', handleMouseUp);
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        pdfViewer.removeEventListener('mousedown', handleMouseDown);
+        pdfViewer.removeEventListener('mouseup', handleMouseUp);
+      }
+    };
+  }, [isActive, addShape]);
+
+  const handleUpdate = useCallback((start: Record<string, any>, end: Record<string, any>): void => {
+    const index = annotations.length - 1;
+    const { x: x1, y: y1 } = start;
+    const { x: x2, y: y2 } = end;
+
+    if (annotations[index] && annotations[index].id === uuid) {
+      const type = annotations[index].obj_type;
+      const position = convertPosition(type, x1, y1, x2, y2);
+
+      annotations[index].obj_attr.position = parsePositionForBackend(
+        type, position, viewport.height, scale,
+      );
+      updateAnnots([...annotations]);
+    }
+  }, [annotations, viewport, scale, uuid]);
+
+  useEffect(() => {
+    if (
+      startPosition.x && startPosition.y
+      && cursorPosition.x && cursorPosition.y
+    ) {
+      handleUpdate(startPosition, cursorPosition);
+    }
+  }, [startPosition, cursorPosition]);
+
+  return (
+    <ExpansionPanel
+      isActive={isActive}
+      label={(
+        <Button
+          shouldFitContainer
+          align="left"
+          onClick={onClick}
+          isActive={isActive}
+        >
+          <Icon glyph="shape" style={{ marginRight: '10px' }} />
+          {title}
+        </Button>
+      )}
+    >
+      <ShapeOption
+        {...data}
+        setDataState={setDataState}
+      />
+    </ExpansionPanel>
+  );
+};
+
+export default Shape;

+ 15 - 6
containers/Sidebar.tsx

@@ -1,11 +1,12 @@
 import React from 'react';
+import { withTranslation } from '../i18n';
 
 import Button from '../components/Button';
 import Typography from '../components/Typography';
 import Icon from '../components/Icon';
 import CreateForm from './CreateForm';
 import MarkupTools from './MarkupTools';
-import Watermark from './Watermark';
+import WatermarkTool from './WatermarkTool';
 
 import useActions from '../actions';
 import useStore from '../store';
@@ -13,7 +14,13 @@ import useStore from '../store';
 import { BtnWrapper } from '../global/toolStyled';
 import { SidebarWrapper } from '../global/otherStyled';
 
-const Sidebar: React.FunctionComponent = () => {
+type Props = {
+  t: (key: string) => string;
+}
+
+const Sidebar: React.FC<Props> = ({
+  t,
+}: Props) => {
   const [{ sidebarState, displayMode }, dispatch] = useStore();
   const { setSidebar } = useActions(dispatch);
 
@@ -27,18 +34,20 @@ const Sidebar: React.FunctionComponent = () => {
 
   return (
     <SidebarWrapper isHidden={displayMode === 'full'}>
-      <Typography light style={{ marginLeft: '30px', marginTop: '46px' }} align="left">Main Menu</Typography>
+      <Typography light style={{ marginLeft: '30px', marginTop: '46px' }} align="left">
+        {t('mainMenu')}
+      </Typography>
       <MarkupTools />
       <CreateForm />
       <BtnWrapper>
         <Button shouldFitContainer align="left" onClick={(): void => { onClick('add-image'); }}>
           <Icon glyph="add-image" style={{ marginRight: '10px' }} />
-          Add Image
+          {t('addImages')}
         </Button>
       </BtnWrapper>
-      <Watermark />
+      <WatermarkTool />
     </SidebarWrapper>
   );
 };
 
-export default Sidebar;
+export default withTranslation('sidebar')(Sidebar);

+ 83 - 0
containers/StickyNoteTools.tsx

@@ -0,0 +1,83 @@
+import React, { useEffect, useCallback } from 'react';
+
+import { ANNOTATION_TYPE } from '../constants';
+import Button from '../components/Button';
+import Icon from '../components/Icon';
+import { getAbsoluteCoordinate } from '../helpers/position';
+import { parseAnnotationObject } from '../helpers/annotation';
+import { BtnWrapper } from '../global/toolStyled';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+type Props = {
+  title: string;
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const StickyNoteTools: React.FC<Props> = ({
+  title,
+  isActive,
+  onClick,
+}: Props) => {
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
+
+  const addStickyNote = (pageEle: HTMLElement, event: MouseEvent): void => {
+    const pageNum = pageEle.getAttribute('data-page-num') || 0;
+    const coordinate = getAbsoluteCoordinate(pageEle, event);
+
+    const annotData = {
+      obj_type: ANNOTATION_TYPE.text,
+      obj_attr: {
+        page: pageNum as number,
+        position: {
+          top: coordinate.y,
+          left: coordinate.x,
+          bottom: coordinate.y,
+          right: coordinate.x,
+        },
+        content: '',
+      },
+    };
+    const stickyNote = parseAnnotationObject(annotData, viewport.height, scale);
+
+    addAnnots([stickyNote], true);
+  };
+
+  const handleMouseDown = useCallback((event: MouseEvent): void => {
+    event.preventDefault();
+
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      addStickyNote(pageEle, event);
+    }
+  }, [viewport, scale]);
+
+  useEffect(() => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
+    if (isActive && pdfViewer) {
+      window.addEventListener('mousedown', handleMouseDown);
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        window.removeEventListener('mousedown', handleMouseDown);
+      }
+    };
+  }, [isActive, handleMouseDown]);
+
+  return (
+    <BtnWrapper>
+      <Button shouldFitContainer align="left" onClick={onClick} isActive={isActive}>
+        <Icon glyph="sticky-note" style={{ marginRight: '10px' }} />
+        {title}
+      </Button>
+    </BtnWrapper>
+  );
+};
+
+export default StickyNoteTools;

+ 1 - 1
containers/Thumbnails.tsx

@@ -5,7 +5,7 @@ import ThumbnailsComponent from '../components/ThumbnailViewer';
 import useActions from '../actions';
 import useStore from '../store';
 
-const Thumbnails: React.FunctionComponent = () => {
+const Thumbnails: React.FC = () => {
   const [{
     pdf, totalPage, currentPage, navbarState, viewport,
   }, dispatch] = useStore();

+ 1 - 1
containers/Toolbar.tsx

@@ -7,7 +7,7 @@ import useStore from '../store';
 
 import { scrollIntoView } from '../helpers/utility';
 
-const Toolbar: React.FunctionComponent = () => {
+const Toolbar: React.FC = () => {
   const [{
     totalPage, currentPage, scale, rotation, viewport, displayMode,
   }, dispatch] = useStore();

+ 13 - 0
global/otherStyled.ts

@@ -21,3 +21,16 @@ export const SidebarWrapper = styled('div')<{isHidden: boolean}>`
     left: 0;
   `)}
 `;
+
+export const AnnotationContainer = styled('div')<{top: string; left: string; width: string; height: string}>`
+  position: absolute;
+  overflow: hidden;
+  cursor: pointer;
+  
+  ${props => css`
+    top: ${props.top};
+    left: ${props.left};
+    width: ${props.width};
+    height: ${props.height};
+  `}
+`;

+ 2 - 2
global/toolStyled.ts

@@ -3,9 +3,9 @@ import styled, { css } from 'styled-components';
 import { color } from '../constants/style';
 
 export const Wrapper = styled('div')<{width?: string}>`
-  margin-bottom: 12px;
+  margin-bottom: 10px;
   display: inline-block;
-  height: 70px;
+  height: auto;
   vertical-align: bottom;
   width: ${props => props.width || '100%'};
 `;

+ 85 - 194
helpers/annotation.ts

@@ -1,179 +1,40 @@
-/* eslint-disable @typescript-eslint/camelcase */
 import _ from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
 
-import { LINE_TYPE } from '../constants';
-import { AnnotationType, PositionType, ViewportType } from '../constants/type';
-import { xmlParser } from './dom';
+import { ANNOTATION_TYPE } from '../constants';
+import {
+  AnnotationType, PositionType, ViewportType,
+} from '../constants/type';
 import { getPdfPage, renderTextLayer } from './pdf';
+import { getPosition, parsePositionForBackend } from './position';
+import { normalizeRound, floatToHex } from './utility';
+import { xmlParser, getElementsByTagname } from './dom';
 
-const EXTEND_RANGE = 2;
-const FRACTIONDIGITS = 2;
+type GetFontAttributeFunc = (type: string, element: Record<string, any>) => Record<string, any>;
 
-const normalizeRound = (num: number, fractionDigits?: number): number => {
-  const frac = fractionDigits || FRACTIONDIGITS;
-  return Math.round(num * (10 ** frac)) / (10 ** frac);
-};
-
-type Props = {
-  color: string;
-  type: string;
-  opacity: number;
-  scale: number;
-}
-
-const resetPositionValue = (
-  {
-    top, left, bottom, right,
-  }: PositionType,
-  scale: number,
-): PositionType => ({
-  top: top / scale,
-  left: left / scale,
-  bottom: bottom / scale,
-  right: right / scale,
-});
-
-export const getAnnotationWithSelection = ({
-  color, type, opacity, scale,
-}: Props): AnnotationType[] | null => {
-  const selection: any = document.getSelection();
-  if (!selection.rangeCount) return [];
-
-  const {
-    startContainer,
-    startOffset,
-    endContainer,
-    endOffset,
-  } = selection.getRangeAt(0);
-  const appendInfo: AnnotationType[] = [];
-  const startElement = startContainer.parentNode as HTMLElement;
-  const endElement = endContainer.parentNode as HTMLElement;
-  const startPage = startElement?.parentNode?.parentNode as HTMLElement;
-  const endPage = endElement?.parentNode?.parentNode as HTMLElement;
-  const startPageNum = parseInt(startPage.getAttribute('data-page-num') as string, 10);
-  const endPageNum = parseInt(endPage.getAttribute('data-page-num') as string, 10);
-  const textLayer = startPage.querySelector('[data-id="text-layer"]') as HTMLElement;
-  const pageHeight = startPage.offsetHeight;
-
-  if (startPageNum !== endPageNum) return null;
-  if (startOffset === endOffset && startOffset === endOffset) return null;
-
-  const startEle = startElement.cloneNode(true) as HTMLElement;
-  const endEle = endElement.cloneNode(true) as HTMLElement;
-
-  const startText = startElement.innerText.substring(0, startOffset);
-  const endText = endEle.innerText.substring(endOffset);
-
-  startEle.innerText = startText;
-  endEle.innerText = endText;
-  textLayer.appendChild(startEle);
-  textLayer.appendChild(endEle);
-
-  const startEleWidth = startEle.offsetWidth;
-  const endEleWidth = endEle.offsetWidth;
-
-  textLayer.removeChild(startEle);
-  textLayer.removeChild(endEle);
-
-  const info: AnnotationType = {
-    obj_type: '',
-    obj_attr: {
-      page: 0,
-      bdcolor: '',
-      position: [],
-      transparency: 0,
-    },
-  };
-  const position: PositionType[] = [];
-
-  // left to right and up to down select
-  let startX = startElement.offsetLeft + startEleWidth;
-  let startY = startElement.offsetTop - EXTEND_RANGE;
-  let endX = endElement.offsetLeft + endElement.offsetWidth - endEleWidth;
-  let endY = endElement.offsetTop + endElement.offsetHeight + EXTEND_RANGE;
-
-  if (startX > endX && startY >= endY) {
-    // right to left and down to up select
-    startX = endElement.offsetLeft + startEleWidth;
-    startY = endElement.offsetTop - EXTEND_RANGE;
-    endX = startElement.offsetLeft + startElement.offsetWidth - endEleWidth;
-    endY = startElement.offsetTop + startElement.offsetHeight + EXTEND_RANGE;
-  }
-  // @ts-ignore
-  const textElements = [...textLayer.childNodes];
-
-  textElements.forEach((ele: any) => {
-    const {
-      offsetTop, offsetLeft, offsetHeight, offsetWidth,
-    } = ele;
-    const offsetRight = offsetLeft + offsetWidth;
-    const offsetBottom = offsetTop + offsetHeight;
-    let coords = {
-      top: 0, left: 0, right: 0, bottom: 0,
-    };
-
-    if (offsetTop >= startY && offsetBottom <= endY) {
-      if (startElement === endElement) {
-        // start and end same element
-        coords = {
-          top: offsetTop,
-          bottom: offsetBottom,
-          left: startX,
-          right: endX,
-        };
-      } else if (startElement === ele) {
-        // start element
-        coords = {
-          top: offsetTop,
-          bottom: offsetBottom,
-          left: startX,
-          right: offsetRight,
-        };
-      } else if (endElement === ele) {
-        // end element
-        coords = {
-          top: offsetTop,
-          bottom: offsetBottom,
-          left: offsetLeft,
-          right: endX,
-        };
-      } else if (
-        (offsetLeft >= startX && offsetRight <= endX)
-        || (offsetTop > (startY + 5) && offsetBottom < (endY - 5))
-        || (offsetLeft >= startX && offsetBottom <= startY + offsetHeight + 5)
-        || (offsetRight <= endX && offsetTop >= endX - offsetHeight - 5)
-      ) {
-        // middle element
-        coords = {
-          top: offsetTop,
-          bottom: offsetBottom,
-          left: offsetLeft,
-          right: offsetRight,
-        };
-      }
+const getContent = (type: string, element: Record<string, any>): string => {
+  if (type !== 'Text' && type !== 'FreeText') return '';
 
-      if (coords.top && coords.left) {
-        coords = {
-          ...coords,
-          top: pageHeight - coords.top,
-          bottom: pageHeight - coords.bottom,
-        };
-
-        position.push(resetPositionValue(coords, scale));
-      }
+  let content = '';
+  const nodes: any = element.childNodes;
+  nodes.forEach((ele: HTMLElement) => {
+    if (ele.tagName === 'contents') {
+      content = ele.innerHTML;
     }
   });
+  return content;
+};
 
-  info.obj_type = LINE_TYPE[type];
-  info.obj_attr = {
-    page: startPageNum - 1,
-    bdcolor: color,
-    position,
-    transparency: opacity * 0.01,
-  };
-  appendInfo.push(info);
+const getFontAttribute: GetFontAttributeFunc = (type, element) => {
+  if (type !== 'FreeText') return {};
+  const appearanceString = element.childNodes[1].innerHTML;
+  const arr = appearanceString.split(' ');
 
-  return appendInfo;
+  return {
+    fontsize: parseInt(arr[5], 10),
+    fontname: arr[4].substr(1),
+    textcolor: floatToHex(parseFloat(arr[0]), parseFloat(arr[1]), parseFloat(arr[2])),
+  };
 };
 
 export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
@@ -181,44 +42,36 @@ export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
 
   const xmlDoc = xmlParser(xmlString);
   const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
+  const element = getElementsByTagname(elements, 'annots') || [];
+  const annotations: any[] = Array.prototype.slice.call(element);
 
-  let annotations = elements.childNodes[1].childNodes;
-  annotations = Array.prototype.slice.call(annotations);
-
-  const filterAnnotations = annotations.reduce((acc: any[], cur: any) => {
-    if (
-      cur.tagName === 'highlight'
-      || cur.tagName === 'underline'
-      || cur.tagName === 'strikeout'
-      || cur.tagName === 'squiggly'
-    ) {
-      let tempArray: any[] = [];
-      if (cur.attributes.coords) {
-        const coords = cur.attributes.coords.value.split(',');
-        tempArray = _.chunk(coords, 8);
-      }
-
-      const position = tempArray.map((ele: string[]) => ({
-        top: parseInt(ele[5] as string, 10),
-        bottom: parseInt(ele[1], 10),
-        left: parseInt(ele[0], 10),
-        right: parseInt(ele[2], 10),
-      }));
+  const filterAnnots = annotations.reduce((acc: any[], cur: any) => {
+    const type = ANNOTATION_TYPE[cur.tagName];
 
+    if (type) {
+      const page = parseInt(cur.attributes.page.value, 10);
       acc.push({
-        obj_type: LINE_TYPE[cur.tagName],
+        id: uuidv4(),
+        obj_type: type,
         obj_attr: {
-          page: parseInt(cur.attributes.page.value, 10),
-          bdcolor: cur.attributes.color.value,
-          position,
-          transparency: parseFloat(cur.attributes.opacity.value),
+          page,
+          position: getPosition(type, cur),
+          bdcolor: cur.attributes.color ? cur.attributes.color.value : undefined,
+          bdwidth: cur.attributes.width ? parseInt(cur.attributes.width.value, 10) : 0,
+          transparency: cur.attributes.opacity ? parseFloat(cur.attributes.opacity.value) : 1,
+          content: getContent(type, cur) || undefined,
+          fcolor: cur.attributes['interior-color'] ? cur.attributes['interior-color'].value : undefined,
+          ftransparency: cur.attributes['interior-opacity'] ? cur.attributes['interior-opacity'].value : undefined,
+          is_arrow: cur.attributes.tail,
+          ...getFontAttribute(type, cur),
         },
       });
     }
+
     return acc;
   }, []);
 
-  return filterAnnotations;
+  return filterAnnots;
 };
 
 // eslint-disable-next-line consistent-return
@@ -301,3 +154,41 @@ export const getAnnotationText = async ({
 
   return text;
 };
+
+export const parseAnnotationObject = ({
+  id,
+  obj_type,
+  obj_attr: {
+    page,
+    bdcolor,
+    transparency,
+    fcolor,
+    ftransparency,
+    position,
+    content,
+    style,
+    bdwidth,
+    fontname,
+    fontsize,
+    textcolor,
+    is_arrow,
+  },
+}: AnnotationType, pageHeight: number, scale: number): AnnotationType => ({
+  id,
+  obj_type,
+  obj_attr: {
+    page: page - 1,
+    bdcolor,
+    position: parsePositionForBackend(obj_type, position, pageHeight, scale),
+    transparency: transparency ? transparency * 0.01 : 0,
+    content: content || undefined,
+    style,
+    fcolor,
+    ftransparency: ftransparency ? ftransparency * 0.01 : 0,
+    bdwidth,
+    fontname,
+    fontsize,
+    textcolor,
+    is_arrow,
+  },
+});

+ 5 - 2
helpers/brush.ts

@@ -4,18 +4,21 @@ type Coordinates = {
 };
 
 type CreatePolylineType = (
-  (penCoordinates: Coordinates, color: string, strokeWidth: number) => SVGPolylineElement
+  (
+    penCoordinates: Coordinates, color: string, opacity: number, strokeWidth: number,
+  ) => SVGPolylineElement
 );
 
 type CompletePathType = (
   (pathElement: SVGPolylineElement | null, penCoordinates: Coordinates) => void
 );
 
-export const createPolyline: CreatePolylineType = (penCoordinates, color, strokeWidth) => {
+export const createPolyline: CreatePolylineType = (penCoordinates, color, opacity, strokeWidth) => {
   const path = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
   path.setAttribute('fill', 'none');
   path.setAttribute('stroke', color);
   path.setAttribute('stroke-width', strokeWidth.toString());
+  path.setAttribute('stroke-opacity', (opacity * 0.01).toString());
   path.setAttribute('points', `${penCoordinates.x},${penCoordinates.y}`);
   return path;
 };

+ 11 - 0
helpers/dom.ts

@@ -8,4 +8,15 @@ export const xmlParser = (xmlString: string): any => {
   return xmlDoc;
 };
 
+export const getElementsByTagname = (elements: any, tagname: string): NodeList => {
+  const array = [...elements.childNodes];
+  let element: any = null;
+  array.forEach((ele) => {
+    if (ele.tagName === tagname) {
+      element = ele.childNodes;
+    }
+  });
+  return element;
+};
+
 export default canUseDOM;

+ 166 - 0
helpers/markup.ts

@@ -0,0 +1,166 @@
+import { ANNOTATION_TYPE } from '../constants';
+import { AnnotationType, PositionType } from '../constants/type';
+import { parseAnnotationObject } from './annotation';
+
+const EXTEND_RANGE = 2;
+
+type ScalePositionFunc = (
+  ({
+    top, left, bottom, right,
+  }: PositionType, scale: number) => PositionType
+);
+
+type GetMarkupWithSelectionFunc = (
+  ({
+    color, type, opacity, scale,
+  }: {
+    color: string;
+    type: string;
+    opacity: number;
+    scale: number;
+  }) => AnnotationType | null
+);
+
+const scalePosition: ScalePositionFunc = ({
+  top, left, bottom, right,
+}, scale) => ({
+  top: top / scale,
+  left: left / scale,
+  bottom: bottom / scale,
+  right: right / scale,
+});
+
+export const getMarkupWithSelection: GetMarkupWithSelectionFunc = ({
+  color, type, opacity, scale,
+}) => {
+  const selection: any = document.getSelection();
+  if (!selection.rangeCount) return null;
+
+  const {
+    startContainer,
+    startOffset,
+    endContainer,
+    endOffset,
+  } = selection.getRangeAt(0);
+  const startElement = startContainer.parentNode as HTMLElement;
+  const endElement = endContainer.parentNode as HTMLElement;
+  const startPage = startElement?.parentNode?.parentNode as HTMLElement;
+  const endPage = endElement?.parentNode?.parentNode as HTMLElement;
+  const startPageNum = parseInt(startPage.getAttribute('data-page-num') as string, 10);
+  const endPageNum = parseInt(endPage.getAttribute('data-page-num') as string, 10);
+  const textLayer = startPage.querySelector('[data-id="text-layer"]') as HTMLElement;
+  const pageHeight = startPage.offsetHeight;
+
+  if (startPageNum !== endPageNum) return null;
+  if (startOffset === endOffset && startOffset === endOffset) return null;
+
+  const startEle = startElement.cloneNode(true) as HTMLElement;
+  const endEle = endElement.cloneNode(true) as HTMLElement;
+
+  const startText = startElement.innerText.substring(0, startOffset);
+  const endText = endEle.innerText.substring(endOffset);
+
+  startEle.innerText = startText;
+  endEle.innerText = endText;
+  textLayer.appendChild(startEle);
+  textLayer.appendChild(endEle);
+
+  const startEleWidth = startEle.offsetWidth;
+  const endEleWidth = endEle.offsetWidth;
+
+  textLayer.removeChild(startEle);
+  textLayer.removeChild(endEle);
+
+  const info: AnnotationType = {
+    obj_type: '',
+    obj_attr: {
+      page: 0,
+      bdcolor: '',
+      position: [],
+      transparency: 0,
+    },
+  };
+  const position: PositionType[] = [];
+
+  // left to right and up to down select
+  let startX = startElement.offsetLeft + startEleWidth;
+  let startY = startElement.offsetTop - EXTEND_RANGE;
+  let endX = endElement.offsetLeft + endElement.offsetWidth - endEleWidth;
+  let endY = endElement.offsetTop + endElement.offsetHeight + EXTEND_RANGE;
+
+  if (startX > endX && startY >= endY) {
+    // right to left and down to up select
+    startX = endElement.offsetLeft + startEleWidth;
+    startY = endElement.offsetTop - EXTEND_RANGE;
+    endX = startElement.offsetLeft + startElement.offsetWidth - endEleWidth;
+    endY = startElement.offsetTop + startElement.offsetHeight + EXTEND_RANGE;
+  }
+  // @ts-ignore
+  const textElements = [...textLayer.childNodes];
+
+  textElements.forEach((ele: any) => {
+    const {
+      offsetTop, offsetLeft, offsetHeight, offsetWidth,
+    } = ele;
+    const offsetRight = offsetLeft + offsetWidth;
+    const offsetBottom = offsetTop + offsetHeight;
+    let coords = {
+      top: 0, left: 0, right: 0, bottom: 0,
+    };
+
+    if (offsetTop >= startY && offsetBottom <= endY) {
+      if (startElement === endElement) {
+        // start and end same element
+        coords = {
+          top: offsetTop,
+          bottom: offsetBottom,
+          left: startX,
+          right: endX,
+        };
+      } else if (startElement === ele) {
+        // start element
+        coords = {
+          top: offsetTop,
+          bottom: offsetBottom,
+          left: startX,
+          right: offsetRight,
+        };
+      } else if (endElement === ele) {
+        // end element
+        coords = {
+          top: offsetTop,
+          bottom: offsetBottom,
+          left: offsetLeft,
+          right: endX,
+        };
+      } else if (
+        (offsetLeft >= startX && offsetRight <= endX)
+        || (offsetTop > (startY + 5) && offsetBottom < (endY - 5))
+        || (offsetLeft >= startX && offsetBottom <= startY + offsetHeight + 5)
+        || (offsetRight <= endX && offsetTop >= endX - offsetHeight - 5)
+      ) {
+        // middle element
+        coords = {
+          top: offsetTop,
+          bottom: offsetBottom,
+          left: offsetLeft,
+          right: offsetRight,
+        };
+      }
+
+      position.push(scalePosition(coords, scale));
+    }
+  });
+
+  info.obj_type = ANNOTATION_TYPE[type];
+  info.obj_attr = {
+    page: startPageNum,
+    bdcolor: color,
+    position,
+    transparency: opacity,
+  };
+
+  return parseAnnotationObject(info, pageHeight, scale);
+};
+
+export default parseAnnotationObject;

+ 186 - 0
helpers/position.ts

@@ -0,0 +1,186 @@
+import _ from 'lodash';
+
+import {
+  PositionType, HTMLCoordinateType, PointType, AnnotationPositionType, LinePositionType,
+} from '../constants/type';
+
+export const rectCalc = (
+  position: PositionType, viewHeight: number, scale: number,
+): HTMLCoordinateType => ({
+  top: viewHeight - position.top * scale,
+  left: position.left * scale,
+  width: (position.right - position.left) * scale,
+  height: (position.top - position.bottom) * scale,
+});
+
+export const getPosition = (type: string, element: Record<string, any>): any => {
+  switch (type) {
+    case 'Ink': {
+      const gestures: any[] = [];
+      let nodes: any = element.childNodes[1].childNodes;
+
+      nodes = Array.prototype.slice.call(nodes);
+      nodes.forEach((ele: HTMLElement) => {
+        if (ele.tagName === 'gesture') {
+          const points: PointType[] = [];
+          const gestureArray = ele.innerHTML.split(';');
+          gestureArray.forEach((ele1: string) => {
+            const pointArr = ele1.split(',');
+            const point = {
+              x: parseInt(pointArr[0], 10),
+              y: parseInt(pointArr[1], 10),
+            };
+            points.push(point);
+          });
+          gestures.push(points);
+        }
+      });
+      return gestures;
+    }
+    case 'Line': {
+      const start = element.attributes.start.value.split(',');
+      const end = element.attributes.end.value.split(',');
+      return {
+        start: {
+          x: start[0],
+          y: start[1],
+        },
+        end: {
+          x: end[0],
+          y: end[1],
+        },
+      };
+    }
+    case 'Square':
+    case 'Circle': {
+      const width = parseInt(element.attributes.width.value, 10);
+      const rect = element.attributes.rect.value.split(',');
+      return {
+        left: parseInt(rect[0], 10) + width,
+        bottom: parseInt(rect[1], 10) + width,
+        right: parseInt(rect[2], 10) - width,
+        top: parseInt(rect[3], 10) - width,
+      };
+    }
+    case 'Text':
+    case 'FreeText': {
+      const rect = element.attributes.rect.value.split(',');
+      return {
+        left: parseInt(rect[0], 10),
+        bottom: parseInt(rect[1], 10),
+        right: parseInt(rect[2], 10),
+        top: parseInt(rect[3], 10),
+      };
+    }
+    case 'Highlight':
+    case 'Underline':
+    case 'Squiggly':
+    case 'StrikeOut': {
+      let tempArray: any[] = [];
+
+      if (element.attributes.coords) {
+        const coords = element.attributes.coords.value.split(',');
+        tempArray = _.chunk(coords, 8);
+      }
+
+      const position = tempArray.map((ele: string[]) => ({
+        left: parseInt(ele[0], 10),
+        bottom: parseInt(ele[1], 10),
+        right: parseInt(ele[2], 10),
+        top: parseInt(ele[5], 10),
+      }));
+
+      return position;
+    }
+    default:
+      return '';
+  }
+};
+
+export const parsePositionForBackend = (
+  type: string, position: AnnotationPositionType, pageHeight: number, scale: number,
+): AnnotationPositionType => {
+  switch (type) {
+    case 'Highlight':
+    case 'Underline':
+    case 'Squiggly':
+    case 'StrikeOut': {
+      const coercionPositionTypeArray = position as PositionType[];
+
+      return coercionPositionTypeArray.map((ele: PositionType) => ({
+        left: ele.left / scale,
+        bottom: (pageHeight - ele.bottom) / scale,
+        right: ele.right / scale,
+        top: (pageHeight - ele.top) / scale,
+      }));
+    }
+    case 'Square':
+    case 'Circle':
+    case 'FreeText':
+    case 'Text': {
+      const coercionPositionType = position as PositionType;
+
+      return {
+        left: coercionPositionType.left / scale,
+        bottom: (pageHeight - coercionPositionType.bottom) / scale,
+        right: coercionPositionType.right / scale,
+        top: (pageHeight - coercionPositionType.top) / scale,
+      };
+    }
+    case 'Line': {
+      const { start, end } = position as LinePositionType;
+
+      return {
+        start: {
+          x: start.x / scale,
+          y: (pageHeight - start.y) / scale,
+        },
+        end: {
+          x: end.x / scale,
+          y: (pageHeight - end.y) / scale,
+        },
+      };
+    }
+    case 'Ink': {
+      const points = position as PointType;
+      return {
+        x: points.x / scale,
+        y: (pageHeight - points.y) / scale,
+      };
+    }
+    default:
+      return {
+        left: 0,
+        bottom: 0,
+        right: 0,
+        top: 0,
+      };
+  }
+};
+
+type GetAbsoluteCoordinateType = (
+  (
+    parentElement: HTMLElement | SVGSVGElement | SVGCircleElement | null,
+    clickEvent: MouseEvent | React.MouseEvent
+  ) => { x: number; y: number }
+);
+
+export const getAbsoluteCoordinate: GetAbsoluteCoordinateType = (parentElement, clickEvent) => {
+  if (!parentElement) return { x: 0, y: 0 };
+
+  const {
+    pageX,
+    pageY,
+  } = clickEvent;
+  const rect = parentElement.getBoundingClientRect();
+  const offsetX = window.pageXOffset || window.scrollX || 0;
+  const offsetY = window.pageYOffset || window.scrollY || 0;
+  const coordinate = {
+    x: pageX - rect.left - offsetX,
+    y: pageY - rect.top - offsetY,
+  };
+
+  return coordinate;
+};
+
+export default rectCalc;

+ 80 - 0
helpers/svgBezierCurve.ts

@@ -0,0 +1,80 @@
+type PointType = { x: number; y: number };
+
+type LineFunc = (
+  (pointA: PointType, pointB: PointType) => { length: number; angle: number}
+);
+
+type ControlPointClosure = (
+  (current: PointType, previous: PointType, next: PointType, reverse?: boolean) => number[]
+);
+
+type ControlPointFunc = (
+  (lineCale: LineFunc, smooth: number) => ControlPointClosure
+);
+
+type BezierCommandClosure = (
+  (point: PointType, i: number, a: PointType[]) => string
+);
+
+type BezierCommandFunc = (
+  (controlPoint: ControlPointClosure) => BezierCommandClosure
+);
+
+type SvgPathFunc = (
+  (points: PointType[], command: BezierCommandClosure) => string
+);
+
+export const line: LineFunc = (pointA, pointB) => {
+  const lengthX = pointB.x - pointA.x;
+  const lengthY = pointB.y - pointA.y;
+
+  return {
+    length: Math.sqrt((lengthX ** 2) + (lengthY ** 2)),
+    angle: Math.atan2(lengthY, lengthX),
+  };
+};
+
+export const controlPoint: ControlPointFunc = (
+  lineCale, smooth,
+) => (
+  current, previous, next, reverse,
+): number[] => {
+  // When 'current' is the first or last point of the array
+  // 'previous' or 'next' don't exist.
+  // Replace with 'current'
+  const p = previous || current;
+  const n = next || current;
+  // Properties of the opposed-line
+  const o = lineCale(p, n);
+  // If is end-control-point, add PI to the angle to go backward
+  const angle = o.angle + (reverse ? Math.PI : 0);
+  const length = o.length * smooth;
+  // The control point position is relative to the current point
+  const x = current.x + Math.cos(angle) * length;
+  const y = current.y + Math.sin(angle) * length;
+
+  return [x, y];
+};
+
+export const bezierCommand: BezierCommandFunc = controlPointCalc => (point, i, a): string => {
+  // start control point
+  const [cpsX, cpsY] = controlPointCalc(a[i - 1], a[i - 2], point);
+  // end control point
+  const [cpeX, cpeY] = controlPointCalc(point, a[i - 1], a[i + 1], true);
+
+  return `C ${cpsX},${cpsY} ${cpeX},${cpeY} ${point.x},${point.y}`;
+};
+
+export const svgPath: SvgPathFunc = (points, command) => {
+  const d = points.reduce(
+    (acc, point, i, a) => (i === 0 ? (
+      `M ${point.x},${point.y}`
+    ) : (
+      `${acc} ${command(point, i, a)}`
+    )),
+    '',
+  );
+  return d;
+};
+
+export default line;

+ 0 - 0
helpers/utility.ts


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików