Explorar el Código

update components and containers

RoyLiu hace 5 años
padre
commit
60fe2ffaf7
Se han modificado 58 ficheros con 1779 adiciones y 266 borrados
  1. 63 0
      components/Annotation/index.tsx
  2. 67 0
      components/Annotation/styled.ts
  3. 62 0
      components/AnnotationList/index.tsx
  4. 49 0
      components/AnnotationList/styled.ts
  5. 89 0
      components/AnnotationSelector/index.tsx
  6. 37 0
      components/AnnotationSelector/styled.ts
  7. 9 2
      components/Box/index.tsx
  8. 0 1
      components/Box/styled.ts
  9. 9 2
      components/Button/index.tsx
  10. 27 21
      components/Button/styled.ts
  11. 18 0
      components/ColorSelector/data.ts
  12. 40 0
      components/ColorSelector/index.tsx
  13. 43 0
      components/DeleteDialog/index.tsx
  14. 11 0
      components/DeleteDialog/styled.ts
  15. 3 1
      components/Dialog/index.tsx
  16. 3 0
      components/Drawer/index.tsx
  17. 2 2
      components/Drawer/styled.ts
  18. 1 5
      components/ExpansionPanel/index.tsx
  19. 18 4
      components/Freehand/index.tsx
  20. 1 0
      components/Head.tsx
  21. 17 0
      components/Icon/data.ts
  22. 5 3
      components/Icon/index.tsx
  23. 3 1
      components/Navbar/index.tsx
  24. 11 3
      components/Navbar/styled.ts
  25. 86 0
      components/Page/index.tsx
  26. 49 0
      components/Page/styled.ts
  27. 1 3
      components/Pagination/index.tsx
  28. 8 1
      components/Pagination/styled.ts
  29. 4 4
      components/PdfSkeleton/styled.ts
  30. 42 14
      components/Search/index.tsx
  31. 2 1
      components/Search/styled.ts
  32. 1 1
      components/SelectBox/styled.ts
  33. 18 4
      components/Shape/index.tsx
  34. 9 4
      components/Sliders/index.tsx
  35. 18 4
      components/TextTools/index.tsx
  36. 2 1
      components/ThumbnailViewer/index.tsx
  37. 29 12
      components/Toolbar/index.tsx
  38. 23 2
      components/Toolbar/styled.ts
  39. 9 95
      components/Viewer/index.tsx
  40. 5 3
      components/Viewer/styled.ts
  41. 3 3
      components/Watermark/index.tsx
  42. 2 0
      constants/apiPath.ts
  43. 7 0
      constants/index.ts
  44. 19 0
      constants/type.ts
  45. 91 0
      containers/Annotation.tsx
  46. 36 0
      containers/AutoSave.tsx
  47. 55 0
      containers/CreateForm.tsx
  48. 20 0
      containers/HighlightTools/data.ts
  49. 99 0
      containers/HighlightTools/index.tsx
  50. 81 0
      containers/MarkupTools.tsx
  51. 9 8
      containers/Navbar.tsx
  52. 119 0
      containers/PdfPages.tsx
  53. 71 41
      containers/PdfViewer.tsx
  54. 245 0
      containers/Search.tsx
  55. 9 14
      containers/Sidebar.tsx
  56. 2 2
      containers/Thumbnails.tsx
  57. 15 2
      containers/Toolbar.tsx
  58. 2 2
      containers/Watermark.tsx

+ 63 - 0
components/Annotation/index.tsx

@@ -0,0 +1,63 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import React from 'react';
+
+import { AnnotationType } from '../../constants/type';
+import AnnotationSelector from '../AnnotationSelector';
+
+import {
+  Markup, Popper,
+} from './styled';
+
+type Props = AnnotationType & {
+  isCovered: boolean;
+  isCollapse: boolean;
+  mousePosition: Record<string, any>;
+  onUpdate: (data: any) => void;
+  onDelete: () => void;
+  scale: number;
+};
+
+const Annotation: React.FunctionComponent<Props> = ({
+  obj_type,
+  obj_attr,
+  isCovered,
+  mousePosition,
+  isCollapse,
+  onUpdate,
+  onDelete,
+  scale,
+}: any) => {
+  const {
+    page, position, bdcolor, transparency,
+  } = obj_attr;
+
+  return (
+    <>
+      {
+        position.map((ele: any, index: number) => (
+          <Markup
+            key={`block_${page + index}`}
+            position={ele}
+            scale={scale}
+            bdcolor={bdcolor}
+            opacity={transparency}
+            markupType={obj_type}
+            isCovered={isCovered}
+          />
+        ))
+      }
+      {
+        !isCollapse ? (
+          <Popper position={mousePosition}>
+            <AnnotationSelector
+              onUpdate={onUpdate}
+              onDelete={onDelete}
+            />
+          </Popper>
+        ) : ''
+      }
+    </>
+  );
+};
+
+export default Annotation;

+ 67 - 0
components/Annotation/styled.ts

@@ -0,0 +1,67 @@
+import styled, { css } from 'styled-components';
+
+const MarkupStyle: Record<string, any> = {
+  Highlight: css<{isCovered: boolean; bdcolor: string}>`
+    background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+  `,
+  Underline: css<{isCovered: boolean; bdcolor: string}>`
+    border-bottom: 2px solid ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+  `,
+  Squiggly: css<{isCovered: boolean; bdcolor: string}>`
+    overflow: hidden;
+    
+    &:before {
+      content: '';
+      position: absolute;
+      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
+      background-size: 22px 26px;
+      width: 100%;
+      height: 5px;
+      left: 0;
+      bottom: 3px;
+    }
+    &:after {
+      content: '';
+      position: absolute;
+      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
+      background-size: 22px 26px;
+      width: 100%;
+      height: 5px;
+      bottom: -1px;
+      left: 11px;
+      background-position: 0px -22px;
+    }
+  `,
+  StrikeOut: css<{isCovered: boolean; bdcolor: string}>`
+    &:after {
+      content: '';
+      position: absolute;
+      height: 2px;
+      width: 100%;
+      left: 0;
+      top: 40%;
+      background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+    };
+  `,
+};
+
+export const Markup = styled('div')<{position: Record<string, any>; bdcolor: string; markupType: string; opacity: number; isCovered: boolean; scale: number}>`
+  position: absolute;
+  cursor: pointer;
+  opacity: ${props => props.opacity};
+
+  ${props => css`
+    top: ${props.position.top * props.scale}px;
+    left: ${props.position.left * props.scale}px;
+    width: ${(props.position.right * props.scale) - (props.position.left * props.scale)}px;
+    height: ${(props.position.bottom * props.scale) - (props.position.top * props.scale) + 2}px;
+  `}
+  ${props => MarkupStyle[props.markupType]}
+`;
+
+export const Popper = styled('div')<{position: Record<string, any>}>`
+  position: absolute;
+  top: ${props => props.position.y}px;
+  left: ${props => props.position.x}px;
+  transform: translate(-50%, -80px);
+`;

+ 62 - 0
components/AnnotationList/index.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+
+import Icon from '../Icon';
+import Drawer from '../Drawer';
+import Typography from '../Typography';
+import Divider from '../Divider';
+
+import {
+  AnnotationBox, PageNumber, Content, Info,
+} from './styled';
+import { Separator } from '../../global/otherStyled';
+import {
+  Wrapper, Head, Body, IconWrapper,
+} from '../../global/sidebarStyled';
+
+type Props = {
+  isActive?: boolean;
+  close: () => void;
+};
+
+const Annotations: React.FunctionComponent<Props> = ({
+  isActive = false,
+  close,
+}: Props) => (
+  <Drawer anchor="right" open={isActive}>
+    <Wrapper>
+      <Head>
+        <IconWrapper>
+          <Icon glyph="right-back" onClick={close} />
+        </IconWrapper>
+        <Separator />
+        <IconWrapper>
+          <Icon glyph="sort" />
+        </IconWrapper>
+        <IconWrapper>
+          <Icon glyph="annotation-export" />
+        </IconWrapper>
+        <IconWrapper>
+          <Icon glyph="import" />
+        </IconWrapper>
+      </Head>
+      <Body>
+        <Typography light>2 Annotations</Typography>
+        <Divider orientation="horizontal" />
+        <PageNumber>Page 1</PageNumber>
+        <AnnotationBox>
+          <Content>
+            If the Photographer fails to appear at the place and time specified above,
+            the deposit shall be refunded to the Client.
+          </Content>
+          <Info>
+            Gameboy
+            <Separator />
+            2016/07/28 18:18
+          </Info>
+        </AnnotationBox>
+      </Body>
+    </Wrapper>
+  </Drawer>
+);
+
+export default Annotations;

+ 49 - 0
components/AnnotationList/styled.ts

@@ -0,0 +1,49 @@
+import styled from 'styled-components';
+
+import { color } from '../../constants/style';
+
+export const Wrapper = styled.div`
+  margin-top: 60px;
+  padding: 24px 8px;
+  background-color: white;
+`;
+
+export const Head = styled.div`
+  display: flex;
+`;
+
+export const Body = styled.div`
+  padding: 8px;
+`;
+
+export const IconWrapper = styled.span`
+  display: inline-block;
+  padding: 8px;
+`;
+
+export const PageNumber = styled.div`
+  font-size: 12px;
+  font-weight: bold;
+  color: ${color.primary};
+  text-align: right;
+  margin-bottom: 5px;
+`;
+
+export const AnnotationBox = styled.div`
+  border-radius: 4px;
+  border: solid 1px ${color.black38};
+  padding: 12px 12px 8px;
+  width: 235px;
+`;
+
+export const Content = styled.div`
+  background-color: ${color['lemon-yellow']};
+`;
+
+export const Info = styled.div`
+  margin-top: 8px;
+  display: flex;
+  color: ${color.black38};
+  font-size: 12px;
+  font-weight: bold;
+`;

+ 89 - 0
components/AnnotationSelector/index.tsx

@@ -0,0 +1,89 @@
+import React, { useState } from 'react';
+
+import ColorSelector from '../ColorSelector';
+import Button from '../Button';
+import Icon from '../Icon';
+import Divider from '../Divider';
+import Box from '../Box';
+import Sliders from '../Sliders';
+import DeleteDialog from '../DeleteDialog';
+
+import {
+  Wrapper, Subtitle, SliderWrapper,
+} from './styled';
+
+type Props = {
+  onUpdate: (data: any) => void;
+  onDelete: () => void;
+  color?: string;
+};
+
+const index: React.FunctionComponent<Props> = ({
+  onUpdate,
+  onDelete,
+}: Props) => {
+  const [openSlider, setSlider] = useState(false);
+  const [openDialog, setDialog] = useState(false);
+  const [opacity, setOpacity] = useState(100);
+
+  const handleClick = (color: string): void => {
+    onUpdate({ color });
+  };
+
+  const handleChange = (value: number): void => {
+    setOpacity(value);
+    onUpdate({ opacity: value });
+  };
+
+  const sliderToggle = (e: React.MouseEvent<HTMLElement>): void => {
+    e.preventDefault();
+    setSlider(!openSlider);
+  };
+
+  const DialogToggle = (e: React.MouseEvent<HTMLElement>): void => {
+    e.preventDefault();
+    setDialog(!openDialog);
+  };
+
+  return (
+    <Wrapper>
+      {
+        openSlider ? (
+          <>
+            <Box w="40px" h="36px" d="flex" j="center">
+              <Icon glyph="right-arrow" onClick={sliderToggle} />
+            </Box>
+            <Divider style={{ margin: '0 10px 0 10px' }} />
+            <SliderWrapper>
+              <Sliders defaultValue={opacity} onChange={handleChange} />
+            </SliderWrapper>
+            <Subtitle>{`${opacity} %`}</Subtitle>
+          </>
+        ) : (
+          <>
+            <ColorSelector onClick={handleClick} />
+            <Subtitle>opacity</Subtitle>
+            <Button
+              appearance="dark"
+              style={{ height: '34px', width: '80px', fontSize: '12px' }}
+              onClick={sliderToggle}
+            >
+              {`${opacity} %`}
+            </Button>
+            <Divider style={{ margin: '0 6px 0 15px' }} />
+            <Box w="48px" h="36px" d="flex" j="center">
+              <Icon glyph="trash" onClick={DialogToggle} />
+            </Box>
+          </>
+        )
+      }
+      <DeleteDialog
+        open={openDialog}
+        onCancel={DialogToggle}
+        onDelete={onDelete}
+      />
+    </Wrapper>
+  );
+};
+
+export default index;

+ 37 - 0
components/AnnotationSelector/styled.ts

@@ -0,0 +1,37 @@
+import styled from 'styled-components';
+
+export const Wrapper = styled.div`
+  min-width: 300px;
+  height: 53px;
+  border-radius: 2px;
+  box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.38);
+  background-color: rgba(0, 0, 0, 0.6);
+  position: relative;
+  display: flex;
+  padding: 0 5px;
+  align-items: center;
+  
+  :before {
+    content: '';
+    border-left: 20px solid transparent;
+    border-right: 20px solid transparent;
+    border-top: 20px solid rgba(0, 0, 0, 0.6);
+    position: absolute;
+    margin: auto;
+    left: 0;
+    right: 0;
+    width: 0px;
+    bottom: -20px;
+  }
+`;
+
+export const Subtitle = styled.div`
+  display: inline-block;
+  color: #ffffff;
+  padding: 0 10px 0 15px;
+  min-width: 70px;
+`;
+
+export const SliderWrapper = styled.div`
+  width: 220px;
+`;

+ 9 - 2
components/Box/index.tsx

@@ -6,9 +6,11 @@ const properties: {[index: string]: string} = {
   m: 'margin',
   p: 'padding',
   d: 'display',
-  jc: 'justify-content',
+  j: 'justify-content',
   a: 'align-items',
-  fd: 'flex-direction',
+  f: 'flex-direction',
+  w: 'width',
+  h: 'height',
 };
 
 const directions: {[index: string]: string} = {
@@ -29,6 +31,11 @@ const Box: React.FunctionComponent<Props> = ({
 }: Props) => {
   const args = Object.keys(rest);
 
+  if (!rest.width) {
+    // eslint-disable-next-line no-param-reassign
+    rest.width = '100%';
+  }
+
   const styles = args.map((ele: string) => {
     const property = properties[ele[0]];
     const direction = directions[ele[1]];

+ 0 - 1
components/Box/styled.ts

@@ -2,5 +2,4 @@ import styled from 'styled-components';
 
 export const Wrapper = styled('div')<{styles?: string[]}>`
   ${props => props.styles}
-  width: 100%;
 `;

+ 9 - 2
components/Button/index.tsx

@@ -6,20 +6,24 @@ import {
 } from './styled';
 
 export type Props = {
-  appearance?: 'default' | 'primary' | 'primary-hollow' | 'default-hollow' | 'link' | 'danger-link';
+  appearance?: 'default' | 'primary' | 'primary-hollow' | 'default-hollow' | 'dark' | 'link' | 'danger-link';
   id?: string;
   isDisabled?: boolean;
-  onClick?: () => void;
+  onClick?: (e: any) => void;
   onBlur?: () => void;
+  onFocus?: () => void;
   shouldFitContainer?: boolean;
   align?: 'left' | 'center' | 'right';
   children: React.ReactNode;
   style?: {};
+  isActive?: boolean;
+  tabIndex?: number;
 };
 
 const Button: React.FunctionComponent<Props> = ({
   children,
   isDisabled,
+  onClick,
   ...rest
 }: Props): React.ReactElement => (
   isDisabled ? (
@@ -29,6 +33,7 @@ const Button: React.FunctionComponent<Props> = ({
   ) : (
     <NormalButton
       {...rest}
+      onMouseDown={onClick}
     >
       {children}
     </NormalButton>
@@ -40,6 +45,8 @@ Button.defaultProps = {
   align: 'center',
   shouldFitContainer: false,
   isDisabled: false,
+  isActive: false,
+  onFocus: (): void => {},
 };
 
 export default Button;

+ 27 - 21
components/Button/styled.ts

@@ -17,6 +17,7 @@ const staticStyles = css`
   cursor: pointer;
   outline: none;
   padding: 5px 20px;
+  min-width: 80px;
 `;
 
 const theme: {[index: string]: any} = {
@@ -28,9 +29,6 @@ const theme: {[index: string]: any} = {
     &:hover {
       background-color: ${color['soft-blue']};
     }
-    &:focus, &:active {
-      background-color: ${color['soft-blue']};
-    }
   `,
   primary: css`
     color: white;
@@ -41,10 +39,6 @@ const theme: {[index: string]: any} = {
       background-color: ${color['french-blue']};
       border: 1.5px solid ${color['french-blue']};
     }
-    &:focus, &:active {
-      background-color: ${color['french-blue']};
-      border: 1.5px solid ${color['french-blue']};
-    }
   `,
   'primary-hollow': css`
     color: black;
@@ -54,9 +48,6 @@ const theme: {[index: string]: any} = {
     &:hover {
       background-color: ${color['primary-light']};
     }
-    &:focus, &:active {
-      background-color: ${color['primary-light']};
-    }
   `,
   'default-hollow': css`
     color: black;
@@ -64,9 +55,7 @@ const theme: {[index: string]: any} = {
     border: 1.5px solid ${color.black56};
 
     &:hover {
-      background-color: ${color.black38};
-    }
-    &:focus, &:active {
+      color: white;
       background-color: ${color.black38};
     }
   `,
@@ -76,11 +65,6 @@ const theme: {[index: string]: any} = {
     border: none;
     text-decoration: underline;
     padding: 5px 0;
-
-    &:hover {
-    }
-    &:focus, &:active {
-    }
   `,
   'danger-link': css`
     color: ${color.error};
@@ -88,20 +72,42 @@ const theme: {[index: string]: any} = {
     border: none;
     text-decoration: underline;
     padding: 5px 0;
+  `,
+  dark: css`
+    color: #ffffff;
+    background-color: #000000;
+    border: 1.5px solid #000000;
 
     &:hover {
+      background-color: #333333;
+      border: 1.5px solid #333333;
     }
-    &:focus, &:active {
-    }
   `,
 };
 
-export const NormalButton = styled('button')<{appearance?: string; shouldFitContainer?: boolean; align?: string}>`
+const activeTheme: {[index: string]: any} = {
+  default: css`
+    background-color: ${color['soft-blue']};
+  `,
+  primary: css`
+    background-color: ${color['french-blue']};
+    border: 1.5px solid ${color['french-blue']};
+  `,
+  'primary-hollow': css`
+    background-color: ${color['primary-light']};
+  `,
+  'default-hollow': css`
+    background-color: ${color.black38};
+  `,
+};
+
+export const NormalButton = styled('div')<{appearance?: string; shouldFitContainer?: boolean; align?: string; isActive?: boolean}>`
   ${staticStyles}
   ${props => theme[props.appearance || 'default']};
   ${props => (props.shouldFitContainer ? css`
     width: 100%;
   ` : null)}
+  ${props => (props.isActive ? activeTheme[props.appearance || 'default'] : null)}
 
   display: inline-flex;
   justify-content: ${props => align[props.align || 'center']};

+ 18 - 0
components/ColorSelector/data.ts

@@ -0,0 +1,18 @@
+export default [
+  {
+    key: 'lemon-yellow',
+    color: '#fcff36',
+  },
+  {
+    key: 'strong-pink',
+    color: '#ff1b89',
+  },
+  {
+    key: 'neon-green',
+    color: '#02ff36',
+  },
+  {
+    key: 'light-blue',
+    color: '#27befd',
+  },
+];

+ 40 - 0
components/ColorSelector/index.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+
+import Icon from '../Icon';
+import Typography from '../Typography';
+
+import { Group, Item, Circle } from '../../global/toolStyled';
+
+import data from './data';
+
+type Props = {
+  showTitle?: boolean;
+  color?: string;
+  onClick: (color: string) => void;
+};
+
+const ColorSelector: React.FunctionComponent<Props> = ({
+  showTitle = false,
+  color = '',
+  onClick,
+}: Props) => (
+  <>
+    { showTitle ? <Typography variant="subtitle" style={{ marginTop: '8px' }}>Color</Typography> : null}
+    <Group>
+      {data.map(ele => (
+        <Item
+          key={ele.key}
+          selected={color === ele.color}
+          onClick={(): void => { onClick(ele.color); }}
+        >
+          <Circle color={ele.color} />
+        </Item>
+      ))}
+      <Item>
+        <Icon glyph="color-picker" />
+      </Item>
+    </Group>
+  </>
+);
+
+export default ColorSelector;

+ 43 - 0
components/DeleteDialog/index.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+
+import Dialog from '../Dialog';
+import Button from '../Button';
+
+import {
+  TextWrapper, BtnWrapper,
+} from './styled';
+
+type Props = {
+  open: boolean;
+  onCancel: (e: any) => void;
+  onDelete: (e: any) => void;
+};
+
+const index: React.FunctionComponent<Props> = ({
+  open,
+  onCancel,
+  onDelete,
+}: Props) => (
+  <Dialog open={open}>
+    <TextWrapper>
+      This will permanently delete the annotation. Do you want to continue?
+    </TextWrapper>
+    <BtnWrapper>
+      <Button
+        appearance="default-hollow"
+        onClick={onCancel}
+      >
+        Cancel
+      </Button>
+      <Button
+        appearance="primary"
+        onClick={onDelete}
+        style={{ marginLeft: '16px' }}
+      >
+        OK
+      </Button>
+    </BtnWrapper>
+  </Dialog>
+);
+
+export default index;

+ 11 - 0
components/DeleteDialog/styled.ts

@@ -0,0 +1,11 @@
+import styled from 'styled-components';
+
+export const TextWrapper = styled.div`
+  width: 324px;
+  margin-bottom: 16px;
+`;
+
+export const BtnWrapper = styled.div`
+  width: 100%;
+  text-align: right;
+`;

+ 3 - 1
components/Dialog/index.tsx

@@ -5,16 +5,18 @@ import Modal from '../Modal';
 import { Wrapper } from './styled';
 
 type Props = {
+  children: React.ReactNode;
   open: boolean;
 };
 
 const Dialog: React.FunctionComponent<Props> = ({
+  children,
   open,
 }: Props) => (
   open ? (
     <Modal>
       <Wrapper>
-        aaaaaa
+        {children}
       </Wrapper>
     </Modal>
   ) : null

+ 3 - 0
components/Drawer/index.tsx

@@ -8,18 +8,21 @@ type Props = {
   anchor?: 'left' | 'top' | 'right' |'bottom';
   children: React.ReactNode;
   open?: boolean;
+  zIndex?: number;
 };
 
 const Drawer: React.FunctionComponent<Props> = ({
   anchor = 'bottom',
   children,
   open = false,
+  zIndex = 3,
 }: Props) => (
   <Portal>
     <Slide
       open={open}
       anchor={anchor}
       data-testid="drawer"
+      zIndex={zIndex}
     >
       <Container>
         {children}

+ 2 - 2
components/Drawer/styled.ts

@@ -42,10 +42,10 @@ const close: {[index: string]: any} = {
   `,
 };
 
-export const Slide = styled('div')<{open: boolean; anchor: string}>`
+export const Slide = styled('div')<{open: boolean; anchor: string; zIndex: number}>`
   position: fixed;
   transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
-  z-index: 3;
+  z-index: ${props => props.zIndex};
   background-color: white;
 
   ${props => position[props.anchor]}

+ 1 - 5
components/ExpansionPanel/index.tsx

@@ -17,17 +17,13 @@ const Collapse: React.FunctionComponent<Props> = ({
 }: Props) => {
   const [isCollapse, setCollapse] = useState(true);
 
-  const handleClick = (): void => {
-    setCollapse(!isCollapse);
-  };
-
   useEffect(() => {
     setCollapse(!isActive);
   }, [isActive]);
 
   return (
     <Wrapper>
-      <Label onClick={handleClick}>
+      <Label>
         {label}
       </Label>
       { children ? (

+ 18 - 4
components/Freehand/index.tsx

@@ -5,7 +5,7 @@ import Button from '../Button';
 import ExpansionPanel from '../ExpansionPanel';
 import Typography from '../Typography';
 import SelectBox from '../SelectBox';
-import ColorSelect from '../ColorSelect';
+import ColorSelect from '../ColorSelector';
 import Sliders from '../Sliders';
 
 import { Group, Item, SliderWrapper } from '../../global/toolStyled';
@@ -23,14 +23,28 @@ const BrushOptions = [
   },
 ];
 
-const Freehand: React.FunctionComponent = () => (
+type Props = {
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const Freehand: React.FunctionComponent<Props> = ({
+  isActive,
+  onClick,
+}: Props) => (
   <ExpansionPanel
     label={(
-      <Button shouldFitContainer align="left">
+      <Button
+        shouldFitContainer
+        align="left"
+        onClick={onClick}
+        isActive={isActive}
+      >
         <Icon glyph="freehand" style={{ marginRight: '10px' }} />
         Freehand
       </Button>
     )}
+    isActive={isActive}
   >
     <Typography variant="subtitle" style={{ marginTop: '4px' }}>Tools</Typography>
     <Group>
@@ -45,7 +59,7 @@ const Freehand: React.FunctionComponent = () => (
         <Icon glyph="undo" />
       </Item>
     </Group>
-    <ColorSelect />
+    <ColorSelect showTitle color="" onClick={(): void => {}} />
     <Typography variant="subtitle" style={{ marginTop: '4px' }}>Opacity</Typography>
     <Group>
       <SliderWrapper>

+ 1 - 0
components/Head.tsx

@@ -44,6 +44,7 @@ const Head: React.StatelessComponent<Props> = ({
     <meta property="og:image" content={ogImage || defaultOGImage} />
     <meta property="og:image:width" content="1200" />
     <meta property="og:image:height" content="630" />
+    <meta httpEquiv="X-UA-Compatible" content="IE=EmulateIE11,chrome=1" />
   </NextHead>
 );
 

+ 17 - 0
components/Icon/data.ts

@@ -1,4 +1,5 @@
 // @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
 import React from 'react';
 
 import Close from '../../static/icons/close.svg';
@@ -67,6 +68,10 @@ import RightBack from '../../static/icons/sidebar/RightBack.svg';
 import Sort from '../../static/icons/annotation/sort.svg';
 import Import from '../../static/icons/annotation/import.svg';
 import AnnotationExport from '../../static/icons/annotation/Export.svg';
+import ToolOpen from '../../static/icons/btn_ToolsOpen.svg';
+import ToolClose from '../../static/icons/btn_ToolsClose.svg';
+import Trash from '../../static/icons/toolbar/delete-00.svg';
+import RightArrow from '../../static/icons/right-arrow.svg';
 
 const data: {[index: string]: any} = {
   close: {
@@ -233,6 +238,18 @@ const data: {[index: string]: any} = {
   'annotation-export': {
     Normal: AnnotationExport,
   },
+  'tool-open': {
+    Normal: ToolOpen,
+  },
+  'tool-close': {
+    Normal: ToolClose,
+  },
+  trash: {
+    Normal: Trash,
+  },
+  'right-arrow': {
+    Normal: RightArrow,
+  },
 };
 
 export default data;

+ 5 - 3
components/Icon/index.tsx

@@ -9,10 +9,11 @@ import data from './data';
 type Props = {
   id?: string;
   glyph: string;
-  onClick?: () => void;
+  onClick?: (e: any) => void;
   onBlur?: () => void;
   style?: {};
   isActive? : boolean;
+  tabIndex?: number;
 };
 
 const Icon: React.FunctionComponent<Props> = ({
@@ -22,6 +23,7 @@ const Icon: React.FunctionComponent<Props> = ({
   onBlur,
   style,
   isActive = false,
+  tabIndex,
 }: Props) => {
   const {
     Normal,
@@ -38,8 +40,8 @@ const Icon: React.FunctionComponent<Props> = ({
       {isActive && Hover ? <Hover data-status="active" /> : null}
       <ClickZone
         id={id}
-        tabIndex={0}
-        onClick={onClick}
+        tabIndex={tabIndex}
+        onMouseDown={onClick}
         onBlur={onBlur}
       />
     </IconWrapper>

+ 3 - 1
components/Navbar/index.tsx

@@ -11,14 +11,16 @@ type Props = {
   onClick: (state: string) => void;
   navbarState: string;
   children: React.ReactNode;
+  displayMode: string;
 };
 
 const Navbar: React.FunctionComponent<Props> = ({
   onClick,
   navbarState,
   children,
+  displayMode,
 }: Props) => (
-  <Wrapper>
+  <Wrapper isHidden={displayMode === 'full'}>
     <Typography variant="title">Title</Typography>
     <Icon glyph="edit" />
     <Separator />

+ 11 - 3
components/Navbar/styled.ts

@@ -1,6 +1,6 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
 
-export const Wrapper = styled.div`
+export const Wrapper = styled('div')<{isHidden: boolean}>`
   position: fixed;
   top: 0;
   height: 60px;
@@ -11,5 +11,13 @@ export const Wrapper = styled.div`
   align-items: center;
   padding: 0 24px;
   background-color: white;
-  z-index: 4;
+  z-index: 100;
+
+  transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
+
+  ${props => (props.isHidden ? css`
+    transform: translate(0, -60px);
+  ` : css`
+    transform: translate(0, 0);
+  `)}
 `;

+ 86 - 0
components/Page/index.tsx

@@ -0,0 +1,86 @@
+import React, {
+  useEffect, useRef,
+} from 'react';
+
+import { renderPdfPage } from '../../helpers/pdf';
+import { TypeRenderingStates, ViewportType } from '../../constants/type';
+
+import {
+  PageWrapper, PdfCanvas, AnnotationLayer, TextLayer, LoadingLayer,
+} from './styled';
+
+type Props = {
+  pageNum: number;
+  renderingState?: TypeRenderingStates;
+  getPage?: () => void;
+  viewport: ViewportType;
+  rotation?: number;
+  annotations?: React.ReactElement[];
+};
+
+const PageView: React.FunctionComponent<Props> = ({
+  pageNum,
+  getPage,
+  viewport,
+  renderingState = 'PAUSED',
+  rotation,
+  annotations = [],
+}: Props) => {
+  const rootEle = useRef<HTMLDivElement | null>(null);
+  let page: any = null;
+
+  const renderPage = async (): Promise<any> => {
+    if (getPage) {
+      page = await getPage();
+
+      if (rootEle.current) {
+        await renderPdfPage({
+          rootEle: rootEle.current,
+          page,
+          viewport,
+        });
+      }
+    }
+  };
+
+  useEffect(() => {
+    if (renderingState === 'RENDERING') {
+      renderPage();
+    } else if (page && renderingState === 'LOADING') {
+      page.cleanup();
+    }
+  }, [renderingState, viewport]);
+
+  useEffect(() => (): void => {
+    if (page) page.cleanup();
+  }, []);
+
+  return (
+    <PageWrapper
+      ref={rootEle}
+      id={`page_${pageNum}`}
+      data-page-num={pageNum}
+      width={viewport.width}
+      height={viewport.height}
+      rotation={rotation}
+    >
+      {
+        renderingState === 'LOADING' ? (
+          <LoadingLayer />
+        ) : (
+          <>
+            <div>
+              <PdfCanvas />
+            </div>
+            <TextLayer data-id="text-layer" />
+            <AnnotationLayer data-id="annotation-layer">
+              {annotations}
+            </AnnotationLayer>
+          </>
+        )
+      }
+    </PageWrapper>
+  );
+};
+
+export default PageView;

+ 49 - 0
components/Page/styled.ts

@@ -0,0 +1,49 @@
+import styled from 'styled-components';
+
+export const PageWrapper = styled('div')<{width: number; height: number; rotation?: number}>`
+  direction: ltr;
+  position: relative;
+  overflow: visible;
+  background-clip: content-box;
+  display: inline-block;
+  margin: 25px auto;
+
+  width: ${props => props.width}px;
+  height: ${props => props.height}px;
+  transform: rotate(${props => props.rotation}deg);
+  background-color: white;
+`;
+
+export const PdfCanvas = styled.canvas`
+  margin: 0;
+  display: block;
+  width: 100%;
+  height: 100%;
+`;
+
+export const AnnotationLayer = styled.div`
+  position: absolute;
+  left: 0;
+  top: 0;
+`;
+
+export const TextLayer = styled.div`
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  margin: auto;
+  overflow: hidden;
+  line-height: 1.0;
+
+  & > span {
+    color: transparent;
+    position: absolute;
+    white-space: pre;
+    cursor: text;
+    transform-origin: 0% 0%;
+  }
+`;
+
+export const LoadingLayer = styled.div``;

+ 1 - 3
components/Pagination/index.tsx

@@ -1,4 +1,4 @@
-import React, { useRef, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
 
 import {
   Wrapper, Text, Input, ArrowButton,
@@ -15,7 +15,6 @@ const Pagination: React.FunctionComponent<Props> = ({
   totalPage = 1,
   onChange,
 }: Props) => {
-  const inputRef = useRef(null);
   const [inputValue, setInputValue] = useState(currentPage);
 
   const handleRightClick = (): void => {
@@ -58,7 +57,6 @@ const Pagination: React.FunctionComponent<Props> = ({
       <ArrowButton onClick={handleLeftClick} variant="left" />
       <Input
         type="tel"
-        ref={inputRef}
         onChange={handleChange}
         onKeyDown={handleKeyDown}
         value={inputValue}

+ 8 - 1
components/Pagination/styled.ts

@@ -41,12 +41,16 @@ export const ArrowButton = styled('button')<{variant: string}>`
     margin-right: 1px;
     :after {
       content: '';
-      width: 0; 
+      top: 0;
+      bottom: 0;
+      margin: auto;
+      width: 0;
       height: 0; 
       border-top: 5px solid transparent;
       border-bottom: 5px solid transparent;
       border-right: 5px solid #000000;
       position: absolute;
+      transform-origin: 50%;
     }
     :hover:after {
       border-right: 5px solid ${color.black38};
@@ -57,6 +61,9 @@ export const ArrowButton = styled('button')<{variant: string}>`
     margin-left: 1px;
     :after {
       content: '';
+      top: 0;
+      bottom: 0;
+      margin: auto;
       width: 0; 
       height: 0; 
       border-top: 5px solid transparent;

+ 4 - 4
components/PdfSkeleton/styled.ts

@@ -3,12 +3,12 @@ import styled from 'styled-components';
 export const Wrapper = styled.div`
   padding: 50px 40px;
   background-color: white;
-  width: 630px;
-  height: 800px;
+  width: 730px;
+  height: 900px;
   position: fixed;
-  left: 267px;
+  left: 0;
   right: 0;
-  top: 60px;
+  top: 0;
   margin: auto;
   margin-top: 60px;
   display: flex;

+ 42 - 14
components/Search/index.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useRef, useEffect, MutableRefObject } from 'react';
 
 import Button from '../Button';
 import Portal from '../Portal';
@@ -8,25 +8,53 @@ import {
 } from './styled';
 
 type Props = {
+  matchesTotal: number;
+  matchIndex: number;
+  onEnter: (val: string) => void;
+  onPrev: () => void;
+  onNext: () => void;
   isActive?: boolean;
   close: () => void;
 };
 
 const Search: React.FunctionComponent<Props> = ({
+  matchesTotal = 0,
+  matchIndex = 1,
+  onPrev,
+  onNext,
+  onEnter,
   isActive = false,
   close,
-}: Props) => (
-  <Portal>
-    <Wrapper open={isActive}>
-      <InputWrapper>
-        <Input />
-        <ResultInfo>2 of 14</ResultInfo>
-      </InputWrapper>
-      <ArrowButton variant="top" />
-      <ArrowButton variant="bottom" />
-      <Button appearance="primary" style={{ marginLeft: '16px' }} onClick={close}>Close</Button>
-    </Wrapper>
-  </Portal>
-);
+}: Props) => {
+  const inputRef = useRef(null) as MutableRefObject<any>;
+
+  const handleKeyDown = (e: React.KeyboardEvent): void => {
+    if (e.keyCode === 13 && inputRef.current.value) {
+      onEnter(inputRef.current.value);
+    }
+  };
+
+  useEffect(() => {
+    if (isActive) {
+      inputRef.current.focus();
+    }
+  }, [isActive]);
+
+  return (
+    <Portal>
+      <Wrapper open={isActive}>
+        <InputWrapper>
+          <Input ref={inputRef} 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>
+      </Wrapper>
+    </Portal>
+  );
+};
 
 export default Search;

+ 2 - 1
components/Search/styled.ts

@@ -13,7 +13,7 @@ export const Wrapper = styled('div')<{open: boolean}>`
   padding: 9px 16px;
   box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.38);
   z-index: 2;
-  transform: translateY(${props => (props.open ? '0' : '-60px')});
+  transform: translateY(${props => (props.open ? '0' : '-120px')});
   transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
 `;
 
@@ -44,6 +44,7 @@ export const Input = styled.input`
 export const ResultInfo = styled.span`
   display: block;
   flex: 1 1 auto;
+  text-align: right;
 `;
 
 export const ArrowButton = styled('button')<{variant: string}>`

+ 1 - 1
components/SelectBox/styled.ts

@@ -49,7 +49,7 @@ export const OptionWrapper = styled.div`
   padding: 6px 0;
   box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.38);
   max-height: 420px;
-  z-index: 200;
+  z-index: 100;
   box-sizing: border-box;
 `;
 

+ 18 - 4
components/Shape/index.tsx

@@ -5,7 +5,7 @@ import ExpansionPanel from '../ExpansionPanel';
 import Icon from '../Icon';
 import SelectBox from '../SelectBox';
 import Typography from '../Typography';
-import ColorSelect from '../ColorSelect';
+import ColorSelect from '../ColorSelector';
 import Sliders from '../Sliders';
 
 import { Group, SliderWrapper } from '../../global/toolStyled';
@@ -45,19 +45,33 @@ const typeOptions = [
   },
 ];
 
-const Shape: React.FunctionComponent = () => (
+type Props = {
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const Shape: React.FunctionComponent<Props> = ({
+  isActive,
+  onClick,
+}: Props) => (
   <ExpansionPanel
     label={(
-      <Button shouldFitContainer align="left">
+      <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 />
+    <ColorSelect showTitle color="" onClick={(): void => {}} />
     <Typography variant="subtitle" style={{ marginTop: '8px' }}>Opacity</Typography>
     <Group>
       <SliderWrapper>

+ 9 - 4
components/Sliders/index.tsx

@@ -1,6 +1,10 @@
 import React, {
   useEffect, useState, useRef, useCallback,
 } from 'react';
+import { fromEvent } from 'rxjs';
+import {
+  throttleTime,
+} from 'rxjs/operators';
 
 import {
   Container, Wrapper, Rail, Track,
@@ -12,7 +16,7 @@ type Props = {
   min?: number;
   defaultValue?: number;
   disabled?: boolean;
-  onChange?: (value: number | number[]) => void;
+  onChange?: (value: number) => void;
 };
 
 const Sliders: React.FunctionComponent<Props> = ({
@@ -22,6 +26,7 @@ const Sliders: React.FunctionComponent<Props> = ({
   const sliderRef = useRef<HTMLDivElement>(null);
   const [valueState, setValueState] = useState(defaultValue);
   const [isActive, setActive] = useState(false);
+  let subscription: any = null;
 
   const parseValueToPercent = (value: number): number => Math.floor(value * 100);
 
@@ -44,7 +49,7 @@ const Sliders: React.FunctionComponent<Props> = ({
     }
   };
 
-  const handleTouchMove = useCallback((event: MouseEvent) => {
+  const handleTouchMove = useCallback((event: any) => {
     event.preventDefault();
     getFingerMoveValue(event);
   }, []);
@@ -53,7 +58,7 @@ const Sliders: React.FunctionComponent<Props> = ({
     event.preventDefault();
 
     setActive(false);
-    document.body.removeEventListener('mousemove', handleTouchMove);
+    subscription.unsubscribe();
   }, []);
 
   const handleMouseDown = useCallback((event: React.MouseEvent<HTMLElement>) => {
@@ -62,7 +67,7 @@ const Sliders: React.FunctionComponent<Props> = ({
     getFingerMoveValue(event);
     setActive(true);
 
-    document.body.addEventListener('mousemove', handleTouchMove);
+    subscription = fromEvent(document.body, 'mousemove').pipe(throttleTime(35)).subscribe(handleTouchMove);
     document.body.addEventListener('mouseup', handleTouchEnd);
   }, []);
 

+ 18 - 4
components/TextTools/index.tsx

@@ -5,7 +5,7 @@ import Button from '../Button';
 import Typography from '../Typography';
 import ExpansionPanel from '../ExpansionPanel';
 import Sliders from '../Sliders';
-import ColorSelect from '../ColorSelect';
+import ColorSelect from '../ColorSelector';
 import SelectBox from '../SelectBox';
 
 import {
@@ -14,14 +14,28 @@ import {
 
 import { fontOptions, sizeOptions } from './data';
 
-const TextTools: React.FunctionComponent = () => (
+type Props = {
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const TextTools: React.FunctionComponent<Props> = ({
+  isActive,
+  onClick,
+}: Props) => (
   <ExpansionPanel
     label={(
-      <Button shouldFitContainer align="left">
+      <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>
@@ -56,7 +70,7 @@ const TextTools: React.FunctionComponent = () => (
       </Group>
     </Wrapper>
     <Wrapper>
-      <ColorSelect />
+      <ColorSelect showTitle color="" onClick={(): void => {}} />
     </Wrapper>
     <Wrapper>
       <Typography variant="subtitle" style={{ marginTop: '8px' }}>Opacity</Typography>

+ 2 - 1
components/ThumbnailViewer/index.tsx

@@ -33,6 +33,7 @@ const Thumbnails: React.FunctionComponent<Props> = ({
   const scrollUpdate = (state: ScrollStateType): void => {
     const height = 220;
     const index = Math.round((state.lastY + height) / height);
+
     setScrollIndex(index);
   };
 
@@ -63,7 +64,7 @@ const Thumbnails: React.FunctionComponent<Props> = ({
     let index = scrollIndex - 5;
     const end = scrollIndex + 5;
 
-    while (index >= 0) {
+    while (scrollIndex) {
       if (elements[index]) {
         const pageNum = index + 1;
 

+ 29 - 12
components/Toolbar/index.tsx

@@ -7,7 +7,7 @@ import Divider from '../Divider';
 import { SelectOptionType, ViewportType } from '../../constants/type';
 import { scaleCheck } from '../../helpers/utility';
 
-import { Wrapper } from './styled';
+import { Wrapper, ToggleButton } from './styled';
 import data from './data';
 
 type Props = {
@@ -19,6 +19,9 @@ type Props = {
   scale: number;
   rotation: number;
   viewport: ViewportType;
+  displayMode: string;
+  toggleDisplayMode: (state: string) => void;
+  handleHandClick: () => void;
 };
 
 const Toolbar: React.FunctionComponent<Props> = ({
@@ -30,6 +33,9 @@ const Toolbar: React.FunctionComponent<Props> = ({
   scale,
   rotation,
   viewport,
+  displayMode,
+  toggleDisplayMode,
+  handleHandClick,
 }: Props) => {
   const handleClockwiseRotation = (): void => {
     const r = rotation + 90;
@@ -61,17 +67,28 @@ const Toolbar: React.FunctionComponent<Props> = ({
   };
 
   return (
-    <Wrapper>
-      <Pagination totalPage={totalPage} currentPage={currentPage} onChange={setCurrentPage} />
-      <Divider />
-      <Icon glyph="hand" style={{ width: '30px' }} />
-      <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)} %`} />
-      <Divider />
-      <Icon glyph="rotate-left" style={{ width: '30px' }} onClick={handleCounterclockwiseRotation} />
-      <Icon glyph="rotate-right" style={{ width: '30px' }} onClick={handleClockwiseRotation} />
-    </Wrapper>
+    <>
+      <Wrapper isHidden={displayMode === 'full'}>
+        <Pagination totalPage={totalPage} currentPage={currentPage} onChange={setCurrentPage} />
+        <Divider />
+        <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)} %`} />
+        <Divider />
+        <Icon glyph="rotate-left" style={{ width: '30px' }} onClick={handleCounterclockwiseRotation} />
+        <Icon glyph="rotate-right" style={{ width: '30px' }} onClick={handleClockwiseRotation} />
+      </Wrapper>
+      <ToggleButton>
+        {
+          displayMode === 'normal' ? (
+            <Icon glyph="tool-close" onClick={(): void => { toggleDisplayMode('full'); }} />
+          ) : (
+            <Icon glyph="tool-open" onClick={(): void => { toggleDisplayMode('normal'); }} />
+          )
+        }
+      </ToggleButton>
+    </>
   );
 };
 

+ 23 - 2
components/Toolbar/styled.ts

@@ -1,6 +1,6 @@
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
 
-export const Wrapper = styled.div`
+export const Wrapper = styled('div')<{isHidden: boolean}>`
   position: fixed;
   top: 86px;
   left: 267px;
@@ -17,4 +17,25 @@ export const Wrapper = styled.div`
   justify-content: space-between;
   padding: 0 8px;
   z-index: 2;
+
+  transition: all 225ms ease-in-out;
+
+  ${props => (props.isHidden ? css`
+    opacity: 0;
+    visibility: hidden;
+  ` : css`
+    opacity: 1;
+    visibility: visible;
+  `)}
+`;
+
+export const ToggleButton = styled.div`
+  position: fixed;
+  right: 20px;
+  bottom: 15px;
+  z-index: 2;
+  box-shadow: 1px 1px 4px 2px rgba(0,0,0,0.32);
+  border-radius: 40px;
+  width: 80px;
+  height: 80px;
 `;

+ 9 - 95
components/Viewer/index.tsx

@@ -1,115 +1,29 @@
-import React, { forwardRef, useEffect, useState } from 'react';
+import React, { forwardRef } from 'react';
 
-import PageView from '../PageView';
 import { ViewportType } from '../../constants/type';
-import { scrollIntoView } from '../../helpers/utility';
 
 import { Container, Wrapper } from './styled';
 
 type Props = {
-  totalPage: number;
-  currentPage: number;
+  children: React.ReactNode;
   viewport: ViewportType;
-  pdf: any;
   rotation: number;
+  displayMode: string;
 };
 type Ref = HTMLDivElement;
 
 const Viewer = forwardRef<Ref, Props>(({
-  totalPage,
-  currentPage,
+  children,
   viewport,
-  pdf,
   rotation,
+  displayMode,
 }: Props, ref) => {
-  const [elements, setElement] = useState<React.ReactNode[]>([]);
-
-  const getPdfPage = async (pageNum: number): Promise<any> => {
-    const page = await pdf.getPage(pageNum);
-    return page;
-  };
-
-  const createPages = (): void => {
-    const pagesContent: React.ReactNode[] = [];
-
-    for (let i = 1; i <= totalPage; i += 1) {
-      const component = (
-        <PageView
-          key={`page-${i}`}
-          pageNum={i}
-          renderingState={[1, 2, 3].includes(i) ? 'RENDERING' : 'LOADING'}
-          viewport={viewport}
-          getPage={(): Promise<any> => getPdfPage(i)}
-          rotation={rotation}
-        />
-      );
-      pagesContent.push(component);
-    }
-
-    setElement(pagesContent);
-  };
-
-  const updatePages = (): void => {
-    const renderingIndexQueue = [currentPage - 1, currentPage, currentPage + 1];
-    let index = currentPage - 2;
-    const end = currentPage + 2;
-
-    while (index >= 0) {
-      if (elements[index]) {
-        const pageNum = index + 1;
-
-        elements[index] = (
-          <PageView
-            key={`page-${pageNum}`}
-            pageNum={pageNum}
-            renderingState={renderingIndexQueue.includes(pageNum) ? 'RENDERING' : 'LOADING'}
-            viewport={viewport}
-            getPage={(): Promise<any> => getPdfPage(pageNum)}
-            rotation={rotation}
-          />
-        );
-      }
-
-      index += 1;
-      if (index >= end) break;
-    }
-
-    if (elements.length) {
-      setElement([...elements]);
-    }
-  };
-
-  const changePdfContainerScale = (): void => {
-    if (elements.length) {
-      for (let i = 1; i <= totalPage; i += 1) {
-        const ele: HTMLDivElement = document.getElementById(`page_${i}`) as HTMLDivElement;
-        ele.style.width = `${viewport.width}px`;
-        ele.style.height = `${viewport.height}px`;
-
-        if (i === currentPage) {
-          updatePages();
-          scrollIntoView(ele);
-        }
-      }
-    }
-  };
-
-  useEffect(() => {
-    createPages();
-  }, [totalPage]);
-
-  useEffect(() => {
-    changePdfContainerScale();
-  }, [viewport]);
-
-  useEffect(() => {
-    updatePages();
-  }, [currentPage, rotation]);
+  const width = (Math.abs(rotation) / 90) % 2 === 1 ? viewport.height : viewport.width;
 
   return (
-    <Container ref={ref}>
-      <Wrapper width={(rotation / 90) % 2 === 1 ? viewport.height : viewport.width}>
-        {elements}
+    <Container ref={ref} isFull={displayMode === 'full'}>
+      <Wrapper width={width}>
+        {children}
       </Wrapper>
     </Container>
   );

+ 5 - 3
components/Viewer/styled.ts

@@ -1,14 +1,16 @@
 import styled from 'styled-components';
 
-export const Container = styled.div`
+export const Container = styled('div')<{isFull: boolean}>`
   position: fixed;
-  left: 267px;
+  left: ${props => (props.isFull ? 0 : '267px')};
   right: 0;
-  top: 60px;
+  top: ${props => (props.isFull ? 0 : '60px')};
   bottom: 0;
   overflow: scroll;
   text-align: center;
   padding: 20px;
+
+  transition: all 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
 `;
 
 export const Wrapper = styled('div')<{width: number}>`

+ 3 - 3
components/Watermark/index.tsx

@@ -7,7 +7,7 @@ import Typography from '../Typography';
 import Tabs from '../Tabs';
 import Sliders from '../Sliders';
 import Divider from '../Divider';
-import ColorSelect from '../ColorSelect';
+import ColorSelect from '../ColorSelector';
 import TextField from '../TextField';
 
 import { BtnWrapper, ContentWrapper } from './styled';
@@ -48,7 +48,7 @@ const Watermark: React.FunctionComponent<Props> = ({
         Watermark
       </Button>
     </BtnWrapper>
-    <Drawer open={isActive} anchor="left">
+    <Drawer open={isActive} anchor="left" zIndex={4}>
       <Wrapper>
         <Head>
           <IconWrapper>
@@ -73,7 +73,7 @@ const Watermark: React.FunctionComponent<Props> = ({
             ]}
           />
           <Divider orientation="horizontal" style={{ margin: '20px 0' }} />
-          <ColorSelect />
+          <ColorSelect showTitle color="" onClick={(): void => {}} />
           <Typography variant="subtitle">Opacity</Typography>
           <Group>
             <SliderWrapper>

+ 2 - 0
constants/apiPath.ts

@@ -1,4 +1,6 @@
 export default {
   initPdf: '/api/v1/init',
   getOriginalPdfFile: '/api/v1/original.pdf',
+  getXfdf: '/api/v1/output.xfdf',
+  saveFile: '/api/v1/save',
 };

+ 7 - 0
constants/index.ts

@@ -1,3 +1,10 @@
 export const MAX_SCALE = 5;
 export const MIN_SCALE = 0.25;
 export const RENDER_RANGE = 3;
+
+export const LINE_TYPE: Record<string, any> = {
+  highlight: 'Highlight',
+  underline: 'Underline',
+  squiggly: 'Squiggly',
+  strikeout: 'StrikeOut',
+};

+ 19 - 0
constants/type.ts

@@ -1,5 +1,7 @@
 export type TypeRenderingStates = 'RENDERING' | 'LOADING' | 'FINISHED' | 'PAUSED';
 
+export type LineType = 'Highlight' | 'Underline' | 'Squiggly' | 'StrikeOut';
+
 export type PayloadType = {
   payload: any;
 };
@@ -26,3 +28,20 @@ export type SelectOptionType = {
   content: React.ReactNode;
   value: number | string;
 };
+
+export type Position = {
+  top: number;
+  bottom: number;
+  left: number;
+  right: number;
+};
+
+export type AnnotationType = {
+  obj_type?: string;
+  obj_attr?: {
+    page?: number;
+    bdcolor?: string;
+    position?: Position[];
+    transparency?: number;
+  };
+};

+ 91 - 0
containers/Annotation.tsx

@@ -0,0 +1,91 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import React, { useState } from 'react';
+
+import { AnnotationType } from '../constants/type';
+import AnnotationComp from '../components/Annotation';
+import useCursorPosition from '../hooks/useCursorPosition';
+
+import useStore from '../store';
+import useActions from '../actions';
+
+type Props = AnnotationType & {
+  index: number;
+  scale: number;
+};
+
+const Annotation: React.FunctionComponent<Props> = ({
+  obj_type,
+  obj_attr,
+  index,
+  scale,
+}: Props) => {
+  const [isCollapse, setCollapse] = useState(true);
+  const [isCovered, setMouseOver] = useState(false);
+  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+  const [cursorPosition, ref] = useCursorPosition();
+  const [{ annotations }, dispatch] = useStore();
+  const { updateAnnotation } = useActions(dispatch);
+
+  const handleClick = (): void => {
+    if (isCollapse) {
+      setCollapse(false);
+      setMousePosition({
+        x: cursorPosition.x || 0,
+        y: cursorPosition.y || 0,
+      });
+    }
+  };
+
+  const handleBlur = (): void => {
+    setCollapse(true);
+  };
+
+  const handleMouseOver = (): void => {
+    setMouseOver(true);
+  };
+
+  const handleMouseOut = (): void => {
+    setMouseOver(false);
+  };
+
+  const handleUpdate = (data: any): void => {
+    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 handleDelete = (): void => {
+    annotations.splice(index, 1);
+    updateAnnotation(annotations);
+  };
+
+  return (
+    <div
+      ref={ref}
+      tabIndex={0}
+      role="button"
+      onFocus={(): void => {}}
+      onBlur={handleBlur}
+      onMouseDown={handleClick}
+      onMouseOver={handleMouseOver}
+      onMouseOut={handleMouseOut}
+    >
+      <AnnotationComp
+        obj_type={obj_type}
+        obj_attr={obj_attr}
+        scale={scale}
+        isCollapse={isCollapse}
+        isCovered={isCovered}
+        mousePosition={mousePosition}
+        onUpdate={handleUpdate}
+        onDelete={handleDelete}
+      />
+    </div>
+  );
+};
+
+export default Annotation;

+ 36 - 0
containers/AutoSave.tsx

@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react';
+import { useSnackbar } from 'notistack';
+
+import useAutoSave from '../hooks/useAutoSave';
+import Icon from '../components/Icon';
+
+const index: React.FunctionComponent = () => {
+  const [isSaved, isSaving] = useAutoSave();
+  const { enqueueSnackbar, closeSnackbar } = useSnackbar();
+
+  useEffect(() => {
+    if (isSaving) {
+      enqueueSnackbar('File saving', {
+        variant: 'info',
+        action: key => (
+          <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />
+        ),
+      });
+    }
+    if (!isSaving && isSaved) {
+      enqueueSnackbar('File is saved', {
+        variant: 'success',
+        action: key => (
+          <Icon glyph="close" onClick={(): void => { closeSnackbar(key); }} />
+        ),
+      });
+    }
+  }, [isSaved, isSaving]);
+
+  return (
+    <>
+    </>
+  );
+};
+
+export default index;

+ 55 - 0
containers/CreateForm.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+
+import Button from '../components/Button';
+import Icon from '../components/Icon';
+import ExpansionPanel from '../components/ExpansionPanel';
+
+import useActions from '../actions';
+import useStore from '../store';
+import { BtnWrapper } from '../global/toolStyled';
+
+const CreateForm: React.FunctionComponent = () => {
+  const [{ sidebarState }, dispatch] = useStore();
+  const { setSidebar } = useActions(dispatch);
+
+  const onClickSidebar = (state: string): void => {
+    if (state === sidebarState) {
+      setSidebar('');
+    } else {
+      setSidebar(state);
+    }
+  };
+
+  return (
+    <ExpansionPanel
+      label={(
+        <Button shouldFitContainer align="left" onClick={(): void => { onClickSidebar('create-form'); }}>
+          <Icon glyph="create-form" style={{ marginRight: '10px' }} />
+          Create Form
+        </Button>
+      )}
+      isActive={sidebarState === 'create-form'}
+    >
+      <BtnWrapper>
+        <Button shouldFitContainer align="left">
+          <Icon glyph="text-field" style={{ marginRight: '10px' }} />
+          Text Field
+        </Button>
+      </BtnWrapper>
+      <BtnWrapper>
+        <Button shouldFitContainer align="left">
+          <Icon glyph="checkbox" style={{ marginRight: '10px' }} />
+          Check box
+        </Button>
+      </BtnWrapper>
+      <BtnWrapper>
+        <Button shouldFitContainer align="left">
+          <Icon glyph="radio-button" style={{ marginRight: '10px' }} />
+          Radio Button
+        </Button>
+      </BtnWrapper>
+    </ExpansionPanel>
+  );
+};
+
+export default CreateForm;

+ 20 - 0
containers/HighlightTools/data.ts

@@ -0,0 +1,20 @@
+export default {
+  lineType: [
+    {
+      key: 'highlight',
+      icon: 'highlight',
+    },
+    {
+      key: 'underline',
+      icon: 'underline',
+    },
+    {
+      key: 'squiggly',
+      icon: 'squiggly',
+    },
+    {
+      key: 'strikeout',
+      icon: 'strikeout',
+    },
+  ],
+};

+ 99 - 0
containers/HighlightTools/index.tsx

@@ -0,0 +1,99 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import Icon from '../../components/Icon';
+import Button from '../../components/Button';
+import Typography from '../../components/Typography';
+import ExpansionPanel from '../../components/ExpansionPanel';
+import Sliders from '../../components/Sliders';
+import ColorSelector from '../../components/ColorSelector';
+
+import useActions from '../../actions';
+import useStore from '../../store';
+
+import { getAnnotationWithSelection } from '../../helpers/annotation';
+import { Group, Item, SliderWrapper } from '../../global/toolStyled';
+import data from './data';
+
+type Props = {
+  isActive: boolean;
+  onClick: () => void;
+};
+
+const Highlight: React.FunctionComponent<Props> = ({
+  isActive,
+  onClick,
+}: Props) => {
+  const [color, setColor] = useState('#fcff36');
+  const [type, setType] = useState('highlight');
+  const [opacity, setOpacity] = useState(40);
+
+  const [{ scale }, dispatch] = useStore();
+  const { addAnnotation } = useActions(dispatch);
+
+  const handleSlide = (value: number): void => {
+    setOpacity(value);
+  };
+
+  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,
+        });
+
+        if (newAnnotations && newAnnotations.length) {
+          addAnnotation(newAnnotations, true);
+        }
+      }
+    }
+  }, [isActive, color, type, opacity, scale]);
+
+  useEffect(() => {
+    document.addEventListener('mouseup', selectRange);
+
+    return (): void => {
+      document.removeEventListener('mouseup', selectRange);
+    };
+  }, [selectRange]);
+
+  return (
+    <ExpansionPanel
+      label={(
+        <Button
+          shouldFitContainer
+          align="left"
+          onClick={onClick}
+          isActive={isActive}
+        >
+          <Icon glyph="highlight" style={{ marginRight: '10px' }} />
+          Highlight Tools
+        </Button>
+      )}
+      isActive={isActive}
+    >
+      <Typography variant="subtitle" style={{ marginTop: '8px' }}>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} />
+      <Typography variant="subtitle" style={{ marginTop: '8px' }}>Opacity</Typography>
+      <Group>
+        <SliderWrapper>
+          <Sliders defaultValue={opacity} onChange={handleSlide} />
+        </SliderWrapper>
+        {`${opacity}%`}
+      </Group>
+    </ExpansionPanel>
+  );
+};
+
+export default Highlight;

+ 81 - 0
containers/MarkupTools.tsx

@@ -0,0 +1,81 @@
+import React, { useEffect } from 'react';
+
+import Button from '../components/Button';
+import ExpansionPanel from '../components/ExpansionPanel';
+import Icon from '../components/Icon';
+import HighlightTools from './HighlightTools';
+import Freehand from '../components/Freehand';
+import TextTools from '../components/TextTools';
+import Shape from '../components/Shape';
+
+import useActions from '../actions';
+import useStore from '../store';
+import { BtnWrapper } from '../global/toolStyled';
+
+const MarkupTools: React.FunctionComponent = () => {
+  const [{ sidebarState, markupToolState }, dispatch] = useStore();
+  const { setSidebar, setMarkupTool } = useActions(dispatch);
+
+  const onClickSidebar = (state: string): void => {
+    if (state === sidebarState) {
+      setSidebar('');
+    } else {
+      setSidebar(state);
+    }
+  };
+
+  const onClickTool = (state: string): void => {
+    if (state === markupToolState) {
+      setMarkupTool('');
+    } else {
+      setMarkupTool(state);
+    }
+  };
+
+  useEffect(() => {
+    if (sidebarState !== 'markup-tools') {
+      setMarkupTool('');
+    }
+  }, []);
+
+  return (
+    <ExpansionPanel
+      label={(
+        <Button
+          shouldFitContainer
+          align="left"
+          onClick={(): void => { onClickSidebar('markup-tools'); }}
+        >
+          <Icon glyph="markup-tools" style={{ marginRight: '10px' }} />
+          Markup Tools
+        </Button>
+      )}
+      isActive={sidebarState === 'markup-tools'}
+    >
+      <HighlightTools
+        isActive={markupToolState === 'highlight'}
+        onClick={(): void => { onClickTool('highlight'); }}
+      />
+      <Freehand
+        isActive={markupToolState === 'freehand'}
+        onClick={(): void => { onClickTool('freehand'); }}
+      />
+      <TextTools
+        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>
+      <Shape
+        isActive={markupToolState === 'shape'}
+        onClick={(): void => { onClickTool('shape'); }}
+      />
+    </ExpansionPanel>
+  );
+};
+
+export default MarkupTools;

+ 9 - 8
containers/Navbar.tsx

@@ -4,19 +4,19 @@ import useStore from '../store';
 import useActions from '../actions';
 
 import NavbarComponent from '../components/Navbar';
-import Search from '../components/Search';
-import Annotations from '../components/Annotations';
+import Search from './Search';
+import AnnotationList from '../components/AnnotationList';
 import Thumbnails from './Thumbnails';
 
 const Navbar: React.FunctionComponent = () => {
-  const [{ navbarState }, dispatch] = useStore();
-  const { switchNavbar } = useActions(dispatch);
+  const [{ navbarState, displayMode }, dispatch] = useStore();
+  const { setNavbar } = useActions(dispatch);
 
   const onClick = (state: string): void => {
     if (state === navbarState) {
-      switchNavbar('');
+      setNavbar('');
     } else {
-      switchNavbar(state);
+      setNavbar(state);
     }
   };
 
@@ -29,9 +29,10 @@ const Navbar: React.FunctionComponent = () => {
     <NavbarComponent
       onClick={onClick}
       navbarState={navbarState}
+      displayMode={displayMode}
     >
-      <Search {...transferProps('search')} />
-      <Annotations {...transferProps('annotations')} />
+      <Search />
+      <AnnotationList {...transferProps('annotations')} />
       <Thumbnails />
     </NavbarComponent>
   );

+ 119 - 0
containers/PdfPages.tsx

@@ -0,0 +1,119 @@
+import React, {
+  useEffect, useState, useRef,
+} from 'react';
+
+import { AnnotationType, ScrollStateType } from '../constants/type';
+import Page from '../components/Page';
+import Viewer from '../components/Viewer';
+import Annotation from './Annotation';
+import { watchScroll } from '../helpers/utility';
+import { getPdfPage } from '../helpers/pdf';
+
+import useStore from '../store';
+
+type Props = {
+  scrollToUpdate: (state: ScrollStateType) => void;
+};
+
+const PdfPages: React.FunctionComponent<Props> = ({
+  scrollToUpdate,
+}: Props) => {
+  const [elements, setElement] = useState<React.ReactNode[]>([]);
+  const containerRef = useRef<HTMLDivElement>(null);
+  const [{
+    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) => {
+      if (ele.obj_attr?.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 component = (
+        <Page
+          key={`page-${i}`}
+          pageNum={i}
+          renderingState={[1, 2, 3].includes(i) ? 'RENDERING' : 'LOADING'}
+          viewport={viewport}
+          getPage={(): Promise<any> => getPdfPage(pdf, i)}
+          rotation={rotation}
+          annotations={annotationElements}
+        />
+      );
+      pagesContent.push(component);
+    }
+
+    setElement(pagesContent);
+  };
+
+  const updatePages = (): void => {
+    const renderingIndexQueue = [currentPage - 1, currentPage, currentPage + 1];
+    let index = currentPage - 4;
+    const end = currentPage + 3;
+
+    while (currentPage) {
+      if (elements[index]) {
+        const pageNum = index + 1;
+        const annotationElements = getAnnotations(annotations, pageNum);
+
+        elements[index] = (
+          <Page
+            key={`page-${pageNum}`}
+            pageNum={pageNum}
+            renderingState={renderingIndexQueue.includes(pageNum) ? 'RENDERING' : 'LOADING'}
+            viewport={viewport}
+            getPage={(): Promise<any> => getPdfPage(pdf, pageNum)}
+            rotation={rotation}
+            annotations={annotationElements}
+          />
+        );
+      }
+
+      index += 1;
+      if (index >= end) break;
+    }
+
+    if (elements.length) {
+      setElement([...elements]);
+    }
+  };
+
+  useEffect(() => {
+    createPages();
+    watchScroll(containerRef.current, scrollToUpdate);
+  }, []);
+
+  useEffect(() => {
+    updatePages();
+  }, [currentPage, viewport, rotation, annotations]);
+
+  return (
+    <Viewer
+      ref={containerRef}
+      viewport={viewport}
+      rotation={rotation}
+      displayMode={displayMode}
+    >
+      {elements}
+    </Viewer>
+  );
+};
+
+export default PdfPages;

+ 71 - 41
containers/PdfViewer.tsx

@@ -1,31 +1,37 @@
-import React, {
-  useEffect, useState, useRef, useCallback,
-} from 'react';
+import React, { useEffect, useRef } from 'react';
 import queryString from 'query-string';
 import config from '../config';
 import apiPath from '../constants/apiPath';
 
 import { initialPdfFile } from '../apis';
-import Viewer from '../components/Viewer';
+import PdfPages from './PdfPages';
 import { fetchPdf } from '../helpers/pdf';
-import { watchScroll } from '../helpers/utility';
 import { ProgressType, ScrollStateType } from '../constants/type';
+import { scrollIntoView } from '../helpers/utility';
+import { parseAnnotationFromXml, fetchXfdf } from '../helpers/annotation';
 
 import useActions from '../actions';
 import useStore from '../store';
 
 const PdfViewer: React.FunctionComponent = () => {
-  const containerRef = useRef<HTMLDivElement>(null);
-  const pageRef = useRef(1);
-  const viewportRef = useRef({ height: 0, width: 0 });
-  const [initialized, setInitialize] = useState(false);
-
   const [{
-    totalPage, currentPage, viewport, pdf, scale, rotation,
+    viewport,
+    pdf,
+    scale,
+    currentPage,
+    totalPage,
   }, dispatch] = useStore();
   const {
-    setTotalPage, setCurrentPage, setPdf, setProgress, setViewport,
+    setTotalPage,
+    setPdf,
+    setProgress,
+    setViewport,
+    setCurrentPage,
+    setInfo,
+    addAnnotation,
+    changeScale,
   } = useActions(dispatch);
+  const currentPageRef = useRef(0);
 
   const setLoadingProgress = (totalSize: number) => (progress: ProgressType): void => {
     setProgress({
@@ -45,45 +51,76 @@ const PdfViewer: React.FunctionComponent = () => {
 
   const pdfGenerator = async (): Promise<any> => {
     const parsed = queryString.parse(window.location.search);
-    const res = await initialPdfFile(parsed.token as string);
+    const result = await initialPdfFile(parsed.token as string);
+
+    fetchXfdf(parsed.token as string).then((res) => {
+      if (res) {
+        const annot = parseAnnotationFromXml(res);
+        addAnnotation(annot, false);
+      }
+    });
+
+    if (result.data) {
+      setInfo({
+        token: parsed.token,
+        id: result.data.transaction_id,
+      });
 
-    if (res.data) {
       const iPdf = await fetchPdf(
-        `${config.API_HOST}${apiPath.getOriginalPdfFile}?transaction_id=${res.data.transaction_id}`,
-        setLoadingProgress(res.data.size),
+        `${config.API_HOST}${apiPath.getOriginalPdfFile}?transaction_id=${result.data.transaction_id}`,
+        setLoadingProgress(result.data.size),
       );
 
-      const totalNum = iPdf.numPages;
-      setTotalPage(totalNum);
+      setTotalPage(iPdf.numPages);
       setPdf(iPdf);
 
-      await getViewport(iPdf, 1);
-      setInitialize(true);
+      const page = await iPdf.getPage(1);
+      const iViewport = page.getViewport({ scale: 1 });
+
+      iViewport.width = Math.round(iViewport.width);
+      iViewport.height = Math.round(iViewport.height);
+      const screenwidth = window.screen.width - 200;
+      const originPdfWidth = iViewport.width / 1;
+      const rate = screenwidth / originPdfWidth;
+      changeScale(rate);
+    }
+  };
+
+  const changePdfContainerScale = (): void => {
+    for (let i = 1; i <= totalPage; i += 1) {
+      const ele: HTMLDivElement = document.getElementById(`page_${i}`) as HTMLDivElement;
+      if (ele) {
+        ele.style.width = `${viewport.width}px`;
+        ele.style.height = `${viewport.height}px`;
+
+        if (i === currentPageRef.current) {
+          scrollIntoView(ele);
+        }
+      }
     }
   };
 
-  const scrollUpdate = useCallback((state: ScrollStateType) => {
-    const iViewport = viewportRef.current;
-    const page = Math.round((state.lastY + iViewport.height / 1.4) / (iViewport.height + 50));
-    if (page !== pageRef.current) {
+  const scrollToUpdate = (state: ScrollStateType): void => {
+    const ele: HTMLDivElement = document.getElementById('page_1') as HTMLDivElement;
+    const page = Math.round((state.lastY + ele.offsetHeight / 1.4) / (ele.offsetHeight + 40));
+    if (page !== currentPageRef.current) {
       setCurrentPage(page);
     }
-  }, [dispatch]);
+  };
 
   useEffect(() => {
     pdfGenerator();
   }, []);
 
   useEffect(() => {
-    if (containerRef.current) {
-      watchScroll(containerRef.current, scrollUpdate);
-    }
-  }, [initialized]);
+    currentPageRef.current = currentPage;
+  }, [currentPage]);
 
   useEffect(() => {
-    pageRef.current = currentPage;
-    viewportRef.current = viewport;
-  }, [currentPage, viewport]);
+    if (pdf) {
+      changePdfContainerScale();
+    }
+  }, [viewport]);
 
   useEffect(() => {
     if (pdf) {
@@ -92,15 +129,8 @@ const PdfViewer: React.FunctionComponent = () => {
   }, [scale]);
 
   return (
-    initialized ? (
-      <Viewer
-        ref={containerRef}
-        totalPage={totalPage}
-        currentPage={currentPage}
-        viewport={viewport}
-        pdf={pdf}
-        rotation={rotation}
-      />
+    viewport.width ? (
+      <PdfPages scrollToUpdate={scrollToUpdate} />
     ) : null
   );
 };

+ 245 - 0
containers/Search.tsx

@@ -0,0 +1,245 @@
+/* eslint-disable @typescript-eslint/no-use-before-define */
+import React, { useState, useEffect, useRef } from 'react';
+
+import useActions from '../actions';
+import useStore from '../store';
+import SearchComponent from '../components/Search';
+import { normalize, calcFindPhraseMatch, convertMatches } from '../helpers/pdf';
+import { scrollIntoView } from '../helpers/utility';
+import { delay } from '../helpers/time';
+
+const Search: React.FunctionComponent = () => {
+  const queryString = useRef('');
+  const pageIndex = useRef(0);
+  const matchIndex = useRef(-1);
+  const pageMatches = useRef<any[]>([]);
+  const textContentItems = useRef<any[]>([]);
+  const textDiv = useRef<any[]>([]);
+  const pageContents: string[] = [];
+  const [matchesTotal, setMatchesTotal] = useState(0);
+  const [selected, setSelected] = useState({
+    last: -1,
+    current: -1,
+  });
+  const [{ navbarState, pdf, totalPage }, dispatch] = useStore();
+  const { setNavbar } = useActions(dispatch);
+
+  const extractPageText = async (pageIdx: number): Promise<any> => {
+    const page = await pdf.getPage(pageIdx + 1);
+    const textContent = await page.getTextContent({
+      normalizeWhitespace: true,
+    });
+    const textItems = textContent.items;
+    const strBuf = [];
+
+    for (let j = 0, jj = textItems.length; j < jj; j += 1) {
+      if (textItems[j].str.match(/[^\s]/)) {
+        strBuf.push(textItems[j].str);
+      }
+    }
+
+    pageContents[pageIdx] = normalize(strBuf.join('').toLowerCase());
+    textContentItems.current[pageIdx] = strBuf;
+  };
+
+  const extractPdfText = async (): Promise<any> => {
+    const extractTextPromises = [];
+    for (let i = 0; i < totalPage; i += 1) {
+      extractTextPromises.push(extractPageText(i));
+    }
+    await Promise.all(extractTextPromises);
+  };
+
+  const appendTextToDiv = (
+    divIdx: number,
+    fromOffset: number,
+    toOffset: number | null,
+    highlight: boolean,
+  ): void => {
+    const textContentItem = textContentItems.current[pageIndex.current];
+    const div = textDiv.current[divIdx];
+    const content = textContentItem[divIdx].substring(fromOffset, toOffset);
+    const node = document.createTextNode(content);
+    const span = document.createElement('span');
+    if (highlight) {
+      span.style.backgroundColor = 'rgba(255, 211, 0, 0.5)';
+      span.appendChild(node);
+      div.appendChild(span);
+    } else {
+      div.appendChild(node);
+    }
+  };
+
+  const cleanMatch = (): void => {
+    if (queryString.current) {
+      const pageMatch = pageMatches.current[pageIndex.current][matchIndex.current];
+      const textContentItem = textContentItems.current[pageIndex.current];
+      const { begin, end } = convertMatches(queryString.current, pageMatch, textContentItem);
+      const len = begin.divIdx + (end.divIdx - begin.divIdx);
+
+      for (let i = begin.divIdx; i <= len; i += 1) {
+        const offset = {
+          divIdx: i,
+          offset: null,
+        };
+        startText(offset);
+      }
+    }
+  };
+
+  const startText = (begin: Record<string, any>): void => {
+    textDiv.current[begin.divIdx].textContent = '';
+    appendTextToDiv(begin.divIdx, 0, begin.offset, false);
+  };
+
+  const renderMatches = async (): Promise<any> => {
+    const pageDiv: HTMLDivElement = document.getElementById(`page_${pageIndex.current + 1}`) as HTMLDivElement;
+    scrollIntoView(pageDiv);
+    await delay(500);
+
+    const textLayer: HTMLDivElement = pageDiv.querySelector('[data-id="text-layer"]') as HTMLDivElement;
+    textDiv.current = Array.from(textLayer.children);
+    await delay(200);
+
+    const pageMatch = pageMatches.current[pageIndex.current][matchIndex.current];
+    const textContentItem = textContentItems.current[pageIndex.current];
+    const { begin, end } = convertMatches(queryString.current, pageMatch, textContentItem);
+
+    startText(begin);
+
+    if (begin.divIdx === end.divIdx) {
+      appendTextToDiv(begin.divIdx, begin.offset, end.offset, true);
+    } else {
+      const len = begin.divIdx + (end.divIdx - begin.divIdx);
+      for (let i = begin.divIdx; i <= len; i += 1) {
+        switch (i) {
+          case begin.divIdx:
+            appendTextToDiv(i, begin.offset, null, true);
+            break;
+          case end.divIdx:
+            appendTextToDiv(end.divIdx, 0, end.offset, true);
+            break;
+          default: {
+            startText(begin);
+            appendTextToDiv(begin.divIdx, 0, null, true);
+            break;
+          }
+        }
+      }
+      // append end text
+      appendTextToDiv(end.divIdx, end.offset, null, true);
+    }
+  };
+
+  const advanceOffsetPage = (previous: boolean): void => {
+    if (previous && pageIndex.current > 0) {
+      pageIndex.current -= 1;
+      const numPageMatches = pageMatches.current[pageIndex.current].length;
+      matchIndex.current = numPageMatches;
+      prevMatch();
+    } else if (!previous && pageIndex.current < totalPage) {
+      pageIndex.current += 1;
+      matchIndex.current = -1;
+      nextMatch();
+    }
+  };
+
+  const prevMatch = (): void => {
+    const numPageMatches = pageMatches.current[pageIndex.current].length;
+
+    if (numPageMatches && matchIndex.current - 1 >= 0) {
+      matchIndex.current -= 1;
+      renderMatches();
+    } else {
+      advanceOffsetPage(true);
+    }
+  };
+
+  const nextMatch = (): void => {
+    const numPageMatches = pageMatches.current[pageIndex.current].length;
+
+    if (numPageMatches && matchIndex.current + 1 < numPageMatches) {
+      matchIndex.current += 1;
+      renderMatches();
+    } else {
+      advanceOffsetPage(false);
+    }
+  };
+
+  const clickPrev = (): void => {
+    cleanMatch();
+
+    setSelected((prev) => {
+      if (prev.current - 1 >= 0) {
+        return {
+          last: prev.current,
+          current: prev.current - 1,
+        };
+      }
+      return prev;
+    });
+  };
+
+  const clickNext = (): void => {
+    cleanMatch();
+
+    setSelected((prev) => {
+      if (prev.current + 1 < matchesTotal) {
+        return {
+          last: prev.current,
+          current: prev.current + 1,
+        };
+      }
+      return prev;
+    });
+  };
+
+  const handleSearch = async (val: string): Promise<any> => {
+    if (!pageContents.length) {
+      await extractPdfText();
+    }
+
+    const query = val.toLowerCase();
+    queryString.current = query;
+    for (let i = 0; i < totalPage; i += 1) {
+      const matches = calcFindPhraseMatch(pageContents[i], query);
+      pageMatches.current[i] = matches;
+    }
+
+    if (pageMatches.current.length) {
+      const total = pageMatches.current.reduce((prev, curr) => prev + curr.length, 0);
+      setMatchesTotal(total);
+      setSelected({
+        last: -1,
+        current: 0,
+      });
+    }
+  };
+
+  const handleClose = (): void => {
+    cleanMatch();
+    setNavbar('');
+  };
+
+  useEffect(() => {
+    if (selected.current > selected.last) {
+      nextMatch();
+    } else if (selected.current < selected.last) {
+      prevMatch();
+    }
+  }, [selected]);
+
+  return (
+    <SearchComponent
+      matchesTotal={matchesTotal}
+      matchIndex={selected.current}
+      onPrev={clickPrev}
+      onNext={clickNext}
+      onEnter={handleSearch}
+      isActive={navbarState === 'search'}
+      close={handleClose}
+    />
+  );
+};
+
+export default Search;

+ 9 - 14
containers/Sidebar.tsx

@@ -3,8 +3,8 @@ import React from 'react';
 import Button from '../components/Button';
 import Typography from '../components/Typography';
 import Icon from '../components/Icon';
-import MarkupTools from '../components/MarkupTools';
-import CreateForm from '../components/CreateForm';
+import CreateForm from './CreateForm';
+import MarkupTools from './MarkupTools';
 import Watermark from './Watermark';
 
 import useActions from '../actions';
@@ -14,27 +14,22 @@ import { BtnWrapper } from '../global/toolStyled';
 import { SidebarWrapper } from '../global/otherStyled';
 
 const Sidebar: React.FunctionComponent = () => {
-  const [{ sidebarState }, dispatch] = useStore();
-  const { switchSidebar } = useActions(dispatch);
+  const [{ sidebarState, displayMode }, dispatch] = useStore();
+  const { setSidebar } = useActions(dispatch);
 
   const onClick = (state: string): void => {
     if (state === sidebarState) {
-      switchSidebar('');
+      setSidebar('');
     } else {
-      switchSidebar(state);
+      setSidebar(state);
     }
   };
 
-  const transferProps = {
-    onClick,
-    sidebarState,
-  };
-
   return (
-    <SidebarWrapper>
+    <SidebarWrapper isHidden={displayMode === 'full'}>
       <Typography light style={{ marginLeft: '30px', marginTop: '46px' }}>Main Menu</Typography>
-      <MarkupTools {...transferProps} />
-      <CreateForm {...transferProps} />
+      <MarkupTools />
+      <CreateForm />
       <BtnWrapper>
         <Button shouldFitContainer align="left" onClick={(): void => { onClick('add-image'); }}>
           <Icon glyph="add-image" style={{ marginRight: '10px' }} />

+ 2 - 2
containers/Thumbnails.tsx

@@ -9,7 +9,7 @@ const Thumbnails: React.FunctionComponent = () => {
   const [{
     pdf, totalPage, currentPage, navbarState, viewport,
   }, dispatch] = useStore();
-  const { switchNavbar } = useActions(dispatch);
+  const { setNavbar } = useActions(dispatch);
 
   const getPdfImage = async (pageNum: number): Promise<any> => {
     let dataUrl = '';
@@ -40,7 +40,7 @@ const Thumbnails: React.FunctionComponent = () => {
         totalPage={totalPage}
         currentPage={currentPage}
         isActive={navbarState === 'thumbnails'}
-        close={(): void => { switchNavbar(''); }}
+        close={(): void => { setNavbar(''); }}
       />
     )
   );

+ 15 - 2
containers/Toolbar.tsx

@@ -9,9 +9,15 @@ import { scrollIntoView } from '../helpers/utility';
 
 const Toolbar: React.FunctionComponent = () => {
   const [{
-    totalPage, currentPage, scale, rotation, viewport,
+    totalPage, currentPage, scale, rotation, viewport, displayMode,
   }, dispatch] = useStore();
-  const { setCurrentPage: setCurrentPageAction, changeScale, changeRotate } = useActions(dispatch);
+  const {
+    setCurrentPage: setCurrentPageAction,
+    changeScale,
+    changeRotate,
+    toggleDisplayMode,
+    setMarkupTool,
+  } = useActions(dispatch);
 
   const setCurrentPage = (num: number): void => {
     if (num > 0) {
@@ -21,6 +27,10 @@ const Toolbar: React.FunctionComponent = () => {
     setCurrentPageAction(num);
   };
 
+  const handleHandClick = (): void => {
+    setMarkupTool('');
+  };
+
   return (
     <ToolbarComponent
       totalPage={totalPage}
@@ -28,9 +38,12 @@ const Toolbar: React.FunctionComponent = () => {
       setCurrentPage={setCurrentPage}
       changeScale={changeScale}
       changeRotate={changeRotate}
+      handleHandClick={handleHandClick}
       scale={scale}
       rotation={rotation}
       viewport={viewport}
+      displayMode={displayMode}
+      toggleDisplayMode={toggleDisplayMode}
     />
   );
 };

+ 2 - 2
containers/Watermark.tsx

@@ -8,11 +8,11 @@ import useStore from '../store';
 const Watermark: React.FunctionComponent = () => {
   const [isActive, setActive] = useState(false);
   const [, dispatch] = useStore();
-  const { switchSidebar } = useActions(dispatch);
+  const { setSidebar } = useActions(dispatch);
 
   const handleClick = (): void => {
     setActive(!isActive);
-    switchSidebar(!isActive ? 'watermark' : '');
+    setSidebar(!isActive ? 'watermark' : '');
   };
 
   return (