lodash#throttle JavaScript Examples

The following examples show how to use lodash#throttle. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: SearchCalendarPopup.js    From airdnd-frontend with MIT License 6 votes vote down vote up
SearchCalendarPopup = forwardRef(
  (
    { type, changeType, searchData, changeFocus, setCheckIn, setCheckOut },
    ref,
  ) => {
    const [isResponsive, setIsResponsive] = useState(window.innerWidth <= 950);
    const handleResize = () => {
      if (window.innerWidth <= 950) setIsResponsive(() => true);
      else if (window.innerWidth > 950) setIsResponsive(() => false);
    };

    useEffect(() => {
      window.addEventListener('resize', throttle(handleResize, 250));
      return () => {
        window.removeEventListener('resize', handleResize);
      };
    }, [handleResize, isResponsive]);

    const { checkIn, checkOut } = searchData;
    return (
      <StSearchCalendarPopupWrapper ref={ref}>
        <StSearchCalendarPopup
          popupState={type === 'checkIn' || type === 'checkOut'}
        >
          <CalendarContainer
            responsiveScreen={isResponsive}
            setCheckinData={setCheckIn}
            setCheckoutData={setCheckOut}
            checkin={checkIn}
            checkout={checkOut}
          ></CalendarContainer>
        </StSearchCalendarPopup>
      </StSearchCalendarPopupWrapper>
    );
  },
)
Example #2
Source File: MainHeaderContainer.js    From airdnd-frontend with MIT License 6 votes vote down vote up
MainHeaderContainer = () => {
  const dispatch = useDispatch();
  const [isScrollTop, setIsScrollTop] = useState(window.scrollY < 40);
  const [isSearchBtnClicked, setIsSearchBtnClicked] = useState(false);
  const [initAnimation, setInitAnimation] = useState(false);
  const onScroll = e => {
    setIsScrollTop(e && window.scrollY < 40);
    setIsSearchBtnClicked(false);
  };

  useEffect(() => {
    if (!initAnimation && window.scrollY > 40) {
      setInitAnimation(true);
    }
    window.addEventListener('scroll', throttle(onScroll, 150));
    return () => {
      window.removeEventListener('scroll', onScroll);
    };
  }, [onScroll, initAnimation]);

  const handleLogoClick = e => {
    e.preventDefault();
    window.scrollTo({ top: 0, behavior: 'smooth' });
    dispatch(reset());
  };

  const handleSearchBtnClick = () => {
    setIsSearchBtnClicked(true);
  };
  return (
    <MainHeader
      initAnimation={initAnimation}
      isScrollTop={isScrollTop}
      isSearchBtnClicked={isSearchBtnClicked}
      handleLogoClick={handleLogoClick}
      handleSearchBtnClick={handleSearchBtnClick}
    ></MainHeader>
  );
}
Example #3
Source File: useDocumentScrollThrottled.js    From andromedev with MIT License 6 votes vote down vote up
function useDocumentScrollThrottled(callback) {
  const [, setScrollPosition] = useState(0);
  let previousScrollTop = 0;

  function handleDocumentScroll() {
    const { scrollTop: currentScrollTop } =
      document.documentElement || document.body;

    setScrollPosition((previousPosition) => {
      previousScrollTop = previousPosition;
      return currentScrollTop;
    });

    callback({ previousScrollTop, currentScrollTop });
  }

  const handleDocumentScrollThrottled = throttle(handleDocumentScroll, 250);

  useEffect(() => {
    window.addEventListener("scroll", handleDocumentScrollThrottled);

    return () =>
      window.removeEventListener("scroll", handleDocumentScrollThrottled);
  }, [handleDocumentScrollThrottled]);
}
Example #4
Source File: normal.js    From django-vue-admin-pro with Apache License 2.0 6 votes vote down vote up
// 生成滚动事件的 handler
function handleMaker (wait) {
  return throttle(e => {
    this.$emit('scroll', {
      x: e.target.scrollLeft,
      y: e.target.scrollTop
    })
  }, wait)
}
Example #5
Source File: dict.js    From d2admin-permission with MIT License 6 votes vote down vote up
doFetch = throttle(async function ({ state, commit }) {
  const names = Array.from(new Set(state.fetchList))
  commit('fetchListClean')
  const dictsData = await Promise.all(names.map(name => api.DICTDATA_ALL({
    page_size: 9999,
    dict_type: name
  })))
  dictsData.forEach((dictDataSource, dictDataIndex) => {
    const dictName = names[dictDataIndex]
    let dictData = []
    if (dictDataSource.list.length > 0) {
      dictData = dictDataSource.list.map(e => ({
        label: e.dict_label,
        value: e[dictDataSource.list[0].dict_value === '' ? 'dict_number' : 'dict_value']
      }))
    }
    commit('set', {
      name: dictName,
      value: dictData
    })
  })
}, 1000, {
  leading: false
})
Example #6
Source File: useResizeWidth.js    From ant-simple-pro with MIT License 6 votes vote down vote up
// eslint-disable-next-line
export function useResizeWidth(fn = () => {}) {
  // eslint-disable-line
  const state = reactive({
    width: getWindowtWidth(),
    height: getWindowHeight()
  })

  function onResize() {
    state.width = getWindowtWidth()
    state.height = getWindowHeight()
    fn()
  }

  const onResizeThrottled = throttle(onResize, 300)

  onMounted(() => {
    window.addEventListener('resize', onResizeThrottled)
  })

  onBeforeUnmount(() => {
    window.removeEventListener('resize', onResizeThrottled)
  })

  return {
    ...toRefs(state)
  }
}
Example #7
Source File: Carousel.js    From petio with MIT License 6 votes vote down vote up
constructor(props) {
    super(props);

    this.state = {
      offset: 0,
      pos: 0,
      init: false,
      width: false,
      inView: false,
    };

    this.carouselRef = React.createRef();
    this.wrapper = React.createRef();
    this.next = this.next.bind(this);
    this.prev = this.prev.bind(this);
    this.init = this.init.bind(this);
    this.scroll = throttle(this.scroll.bind(this), 1000);
    this.isInViewport = throttle(this.isInViewport.bind(this), 1000);
  }
Example #8
Source File: app.js    From bank-client with MIT License 6 votes vote down vote up
// Load and Save redux store to localStorage
store.subscribe(
  throttle(() => {
    saveState({
      language: store.getState().language,
      global: omit(store.getState().global, [
        'messages',
        'notifications',
        'currencies',
        'openedMessage',
        'isOpenedMessage',
      ]),
    });
  }, 1000),
);
Example #9
Source File: DetailHeaderContainer.js    From airdnd-frontend with MIT License 5 votes vote down vote up
DetailHeaderContainer = () => {
  const history = useHistory();
  const dispatch = useDispatch();
  const { homeInfosRef } = useSelector(state => state.home.scrollState);
  const [isScrollTop, setIsScrollTop] = useState(
    window.pageYOffset < (homeInfosRef ? homeInfosRef.offsetTop : 643),
  );
  const [isScrollUnderBox, setIsScrollUnderBox] = useState(
    window.pageYOffset > 2569,
  );
  const [isSearchBtnClicked, setIsSearchBtnClicked] = useState(false);
  const [initAnimation, setInitAnimation] = useState(false);
  const searchForm = useSelector(state => state.searchForm);

  const onScroll = () => {
    setIsScrollTop(
      window.pageYOffset < (homeInfosRef ? homeInfosRef.offsetTop : 643),
    );
    setIsScrollUnderBox(window.pageYOffset > 2569);
    setIsSearchBtnClicked(false);
  };

  console.log(window.pageYOffset, homeInfosRef);

  useEffect(() => {
    // console.log(isSearchBtnClicked);
    if (!initAnimation && isSearchBtnClicked) {
      setInitAnimation(true);
    }
    window.addEventListener('scroll', throttle(onScroll, 100));
    return () => {
      // window.removeEventListener('scroll', onScroll);
      // console.log('컴포넌트 사라진다!!!!');
    };
  }, [onScroll, initAnimation]);

  const handleLogoClick = e => {
    e.preventDefault();
    history.push('/');
    window.scrollTo({ top: 0 });
  };

  const handleSearchBtnClick = () => {
    setIsSearchBtnClicked(true);
  };

  return isScrollTop ? (
    <DetailHeader
      initAnimation={initAnimation}
      isSearchBtnClicked={isSearchBtnClicked}
      handleLogoClick={handleLogoClick}
      handleSearchBtnClick={handleSearchBtnClick}
      searchForm={searchForm}
    />
  ) : (
    <DetailFixedHeaderContainer isScrollUnderBox={isScrollUnderBox} />
  );
}
Example #10
Source File: DetailMainContainer.js    From airdnd-frontend with MIT License 5 votes vote down vote up
DetailMainContainer = () => {
  const { isLoading, home, error } = useSelector(state => state.home.homeState);
  const { isScreenMedium } = useSelector(state => state.home.screenState);
  const { isLoggedIn } = useSelector(state => state.user);
  const dispatch = useDispatch();
  const { id } = useParams();
  const location = useLocation();
  const queryObj = qs.parse(location.search, { ignoreQueryPrefix: true });

  console.log(location, queryObj);

  const resize = () => dispatch(onResize());

  useEffect(() => {
    if (home) return;
    dispatch(getHome(id));
    window.addEventListener('resize', throttle(resize, 150));

    dispatch(setInitialDatas(queryObj));

    return () => {
      window.removeEventListener('resize', resize);
    };
  }, [dispatch]);

  if (error)
    return (
      <div style={{ padding: '30px 30px 300px' }}>
        에러 발생! 잘못된 요청입니다.
      </div>
    );

  return (
    <>
      <Subject isLoading={isLoading || !home} home={home} />
      <HomeInfos
        isLoading={isLoading || !home}
        home={home}
        isScreenMedium={isScreenMedium}
      />
      {!isLoading && home && <FullsizeWrapper home={home} />}
      {isLoggedIn && <BookmarkListModalContainer />}
      {isLoggedIn && <NewBookmarkModalContainer />}
    </>
  );
}
Example #11
Source File: SearchHeaderContainer.js    From airdnd-frontend with MIT License 5 votes vote down vote up
SearchHeaderContainer = () => {
  const { headerState } = useSelector(state => state.search);
  const searchForm = useSelector(state => state.searchForm);
  const history = useHistory();

  const [initAnimation, setInitAnimation] = useState(false);
  const dispatch = useDispatch();
  const onScroll = () => dispatch(closeHeader());
  const handleSearchBtnClick = () => dispatch(openHeader());

  const handleLogoClick = e => {
    e.preventDefault();
    history.push('/');
    window.scrollTo({ top: 0 });
    dispatch(reset());
  };

  console.log(
    'stop rendering!!!!!!!!!!!!!',
    '\n headerState',
    headerState,
    '\n initAnimation',
    initAnimation,
  );

  // 이게 어제 무한 렌더링 고쳤던 부분 ㅠㅠ
  useEffect(() => {
    if (!initAnimation && headerState) {
      setInitAnimation(true);
    } else if (headerState) {
      window.addEventListener('scroll', throttle(onScroll, 150), true);
    }
    return () => {
      window.removeEventListener('scroll', onScroll, true);
    };
  }, [onScroll, initAnimation]);

  return (
    <SearchHeader
      searchForm={searchForm}
      initAnimation={initAnimation}
      isSearchBtnClicked={headerState}
      handleLogoClick={handleLogoClick}
      handleSearchBtnClick={handleSearchBtnClick}
    ></SearchHeader>
  );
}
Example #12
Source File: CarouselContainer.js    From airdnd-frontend with MIT License 4 votes vote down vote up
CarouselContainer = ({
  size,
  theme,
  homeId,
  mapState,
  blockLink,
  isHovered,
  responsive,
  imageArray,
  imageCount,
  isSuperhost,
  getWidth,
  ...rest
}) => {
  const carouselInit = {
    imageCount,
    isSliding: false,
    nextIndex: imageCount > 1 ? 1 : null,
    prevIndex: imageCount > 1 ? imageCount - 1 : null,
    currentIndex: 0,
    renderArray: [0],
    direction: '',
    width: 0,
    marker: 0,
  };

  const [carouselState, dispatch] = useReducer(carouselReducer, carouselInit);
  const [didMount, setDidMount] = useState(false);

  const {
    isSliding,
    direction,
    renderArray,
    width,
    marker,
    currentIndex,
  } = carouselState;

  const onSlideNext = () => dispatch({ type: 'SLIDE_NEXT' });
  const onSlidePrev = () => dispatch({ type: 'SLIDE_PREV' });
  const setWidth = () =>
    dispatch({ type: 'SET_WIDTH', width: !size && getWidth() });
  window.addEventListener('resize', throttle(setWidth, 300));

  useEffect(() => {
    setDidMount(true);
    setWidth();
    dispatch({
      type: 'GET_MARKER',
      marker: getMarkerIndex(imageCount, currentIndex),
    });
    setTimeout(() => isSliding && dispatch({ type: 'END_SLIDE' }), 300);
    return () => {
      setDidMount(false);
      window.removeEventListener('resize', setWidth);
    };
  }, [isSliding, currentIndex, width, mapState]);

  if (!didMount) return null;
  return (
    <Carousel
      size={size}
      theme={theme}
      homeId={homeId}
      blockLink={blockLink}
      marker={marker}
      responsive={responsive}
      direction={direction}
      isSliding={isSliding}
      renderArray={renderArray}
      homeWidth={width - 20}
      imageArray={imageArray}
      imageCount={imageCount}
      isSuperhost={isSuperhost}
      isHovered={isHovered}
      onSlideNext={onSlideNext}
      onSlidePrev={onSlidePrev}
      setWidth={setWidth}
      {...rest}
    />
  );
}
Example #13
Source File: Canvas.js    From hivemind with Apache License 2.0 4 votes vote down vote up
Canvas = ({ data, timestamp, events }) => {
  const { cyWrapper, poppers } = useContext(GlobalContext)
  const [output, setOutput] = useState(null)
  const [els, setEls] = useState([])
  const prevEls = usePrevious(els)

  useEffect(() => {
    if (cyWrapper.cy && prevEls !== els) {
      const commonEls = intersectionBy(prevEls, els, 'data.id')
      const celMap = zipObject(map(commonEls, 'data.id'), commonEls)

      cyWrapper.cy
        .elements()
        .filter((el) => celMap[el.id()])
        .forEach((el) => {
          el.removeData('summary content audio lastUpdatedBy')
          el.data(celMap[el.id()].data)
        })
    }
  }, [cyWrapper.cy, els, prevEls])

  useEffect(() => {
    if (get(data, 'ok') && typeof window !== 'undefined') {
      setEls(CytoscapeComponent.normalizeElements(data.data.elements))
    }
  }, [data])

  useEffect(() => {
    function initCy(cyInternal) {
      cyWrapper.cy = cyInternal

      cyInternal.nodes().forEach((node) => {
        node.scratch('style', node.style())
      })
    }

    const nodes = els.filter((el) => !el.data.id.startsWith('links'))
    const fit = shouldFit(nodes)
    const options = getOptions(fit)

    setOutput(
      <CytoscapeComponent
        cy={initCy}
        style={{ width: '100%', height: '100%' }}
        stylesheet={style}
        layout={options}
        elements={els}
      />
    )
  }, [cyWrapper, els])

  useEffect(() => {
    function configurePlugins(access) {
      function buildMenu() {
        const { viewApi } = cyWrapper

        return function (node) {
          const menu = []

          view(menu, poppers)
          if (!node.data('isRoot')) {
            hide(menu, viewApi)
          }
          if (node.scratch('showReveal')) {
            reveal(menu, viewApi)
          }

          if (access && ['admin', 'write'].includes(access.access)) {
            add(menu, poppers)
            if (!node.data('isRoot')) {
              del(menu, poppers)
            }
            edit(menu, poppers)
          }

          return menu
        }
      }

      const { cy } = cyWrapper
      const minRadius = Math.min(cy.width(), cy.height()) / 8

      const viewOpts = {
        highlightStyles: [
          {
            node: { 'border-color': '#0b9bcd', 'border-width': 3 },
            edge: {
              'line-color': '#0b9bcd',
              'source-arrow-color': '#0b9bcd',
              'target-arrow-color': '#0b9bcd',
              width: 3,
            },
          },
          {
            node: { 'border-color': '#04f06a', 'border-width': 3 },
            edge: {
              'line-color': '#04f06a',
              'source-arrow-color': '#04f06a',
              'target-arrow-color': '#04f06a',
              width: 3,
            },
          },
        ],
        selectStyles: {
          node: {
            'border-color': 'white',
            'border-width': 3,
            'background-color': 'lightgrey',
          },
          edge: {
            'line-color': 'white',
            'source-arrow-color': 'white',
            'target-arrow-color': 'white',
            width: 3,
          },
        },
        setVisibilityOnHide: false, // whether to set visibility on hide/show
        setDisplayOnHide: true, // whether to set display on hide/show
        zoomAnimationDuration: 500, //default duration for zoom animation speed
        neighbor: function (node) {
          return node.successors()
        },
        neighborSelectTime: 500,
      }
      cyWrapper.viewApi = cy.viewUtilities(viewOpts)

      const cxtMenu = {
        menuRadius: minRadius + 50, // the radius of the circular menu in pixels
        selector: 'node', // elements matching this Cytoscape.js selector will trigger cxtmenus
        commands: buildMenu(), // function( ele ){ return [
        // /*...*/ ] }, // a function
        // that returns
        // commands or a promise of commands
        fillColor: 'rgba(0, 0, 0, 0.75)', // the background colour of the menu
        activeFillColor: 'rgba(100, 100, 100, 0.5)', // the colour used to indicate the selected
        // command
        activePadding: 10, // additional size in pixels for the active command
        indicatorSize: 16, // the size in pixels of the pointer to the active command
        separatorWidth: 3, // the empty spacing in pixels between successive commands
        spotlightPadding: 4, // extra spacing in pixels between the element and the spotlight
        minSpotlightRadius: minRadius - 40, // the minimum radius in pixels of the spotlight
        maxSpotlightRadius: minRadius - 20, // the maximum radius in pixels of the spotlight
        openMenuEvents: 'tap', // space-separated cytoscape events that will open the menu; only
        // `cxttapstart` and/or `taphold` work here
        itemColor: 'white', // the colour of text in the command's content
        itemTextShadowColor: 'transparent', // the text shadow colour of the command's content
        // zIndex: 9999, // the z-index of the ui div
        atMouse: false, // draw menu at mouse position
      }
      cyWrapper.menu = cy.cxtmenu(cxtMenu)
    }

    function setHandlers() {
      const { viewApi, cy } = cyWrapper

      cy.on(
        'boxend',
        throttle(() => defer(() => viewApi.zoomToSelected(cy.$(':selected')))),
        1000
      )

      cy.on('mouseover', 'node', () => {
        document.getElementById('cy').style.cursor = 'pointer'
      })

      cy.on('mouseout', 'node', () => {
        document.getElementById('cy').style.cursor = 'default'
      })

      cy.on('mouseover', 'edge', (e) => {
        e.target.style({
          width: 4,
          'line-color': '#007bff',
          'target-arrow-color': '#007bff',
        })
      })

      cy.on('unselect mouseout', 'edge', (e) => {
        const edge = e.target

        if (!edge.selected()) {
          edge.style({
            width: 2,
            'line-color': '#ccc',
            'target-arrow-color': '#ccc',
          })
        }
      })

      cy.on('add', 'node', (e) => {
        const node = e.target

        node.scratch('style', node.style())
      })

      cy.on(
        'add data remove',
        'node',
        throttle(() => {
          if (timestamp) {
            const idx = findIndex(events.data, { lctime: timestamp })
            const event = events.data[idx]
            const { viewApi } = cyWrapper

            viewApi.removeHighlights(cy.elements())
            if (event && event.event !== 'deleted') {
              const nid = event.nids[0]
              const node = cy.$id(nid)

              viewApi.highlight(node)
            }
          }
        }, 100)
      )

      cy.on('mouseover', 'node', (e) => {
        e.target.style('background-color', '#007bff')
      })

      cy.on('unselect mouseout', 'node', (e) => {
        const node = e.target

        viewApi.removeHighlights(node)

        if (!node.selected()) {
          node.style(
            'background-color',
            node.scratch('style')['background-color']
          )
        }
      })
    }

    if (cyWrapper.cy && get(data, 'ok') && get(events, 'ok')) {
      configurePlugins(data.data.access)
      setHandlers()
    }

    return () => {
      if (cyWrapper.menu) {
        cyWrapper.menu.destroy()
      }
    }
  }, [data, events, cyWrapper.menu, cyWrapper, timestamp, poppers, els])

  return (
    <div
      className={`border border-${
        timestamp ? 'secondary' : 'danger'
      } rounded w-100`}
      id="cy-container"
    >
      <div className="m-1" id="cy">
        {output}
      </div>
    </div>
  )
}
Example #14
Source File: anatomyFieldHelpers.jsx    From covid19-testing with Apache License 2.0 4 votes vote down vote up
export function withHighlightableList({
  enableCharacterSearch = false,
  onHighlightThrottleMilliseconds = 25,
} = {}) {
  return (Component) =>
    class extends React.PureComponent {
      static displayName = getDisplayName(Component, 'withHighlightableList');

      static propTypes = {
        /**
         * This is an alias for the options prop.
         */
        choices: PropTypes.arrayOf(PropTypes.array),
        /**
         * Function that determines what string representation of each option should be used to search when inputting
         * alphanumeric characters. It is provided the individual option array (e.g. it contains the key and what to
         * render) as the only argument, and should return the desired string. This is necessary when the option values
         * are not strings.
         */
        getSearchStringFromOption: PropTypes.func,
        options: PropTypes.arrayOf(PropTypes.array),
      };

      static defaultProps = {
        options: [],
      };

      constructor(props) {
        super(props);

        this.keyPressSearcher = new KeyPressSearcher(props.getSearchStringFromOption);
        this.state = {
          highlightedIndex: this.getBoundedIndex(props, this.getInitialIndex()),
          highlightedWithKeyPress: false,
        };
        this.throttledOnHighlight = throttle(
          this.onHighlight,
          onHighlightThrottleMilliseconds,
          {leading: true}
        );
      }

      componentWillReceiveProps(nextProps) {
        if (this.state.highlightedIndex >= nextProps.options.length) {
          this.setState({
            highlightedIndex: this.getBoundedIndex(
              nextProps,
              this.state.highlightedIndex
            ),
            highlightedWithKeyPress: false,
          });
        }
      }

      getBoundedIndex = (props, index) => {
        return Math.max(Math.min(index, this.getOptions(props).length - 1), 0);
      };

      getInitialIndex = () => {
        const currentValues = [].concat(this.props.value);
        return this.getOptions().findIndex(
          ([optionKey]) => currentValues.indexOf(optionKey) !== -1
        );
      };

      getOptions = (props = this.props) => {
        return props.choices || props.options;
      };

      onCharacterKeyPress = (key) => {
        const index = this.keyPressSearcher.addAndFindIndex(
          key,
          this.getOptions(),
          this.state.highlightedIndex
        );

        if (index !== -1) {
          this.setState({
            highlightedIndex: index,
            highlightedWithKeyPress: true,
          });
        }
      };

      onHighlight = (index, simulateKeypress = false) => {
        this.setState({
          highlightedIndex: index,
          highlightedWithKeyPress: simulateKeypress,
        });
      };

      onHighlightedIndexIncrement = (increment) => {
        this.setState((prevState) => ({
          highlightedIndex: this.getBoundedIndex(
            this.props,
            prevState.highlightedIndex + increment
          ),
          highlightedWithKeyPress: true,
        }));
      };

      onKeyDown = (e) => {
        switch (e.which) {
          case KeyCode.KEY_DOWN:
            e.preventDefault();
            this.onHighlightedIndexIncrement(1);
            return;

          case KeyCode.KEY_UP:
            e.preventDefault();
            this.onHighlightedIndexIncrement(-1);
            return;
        }

        if (enableCharacterSearch && e.key.match(/^[\w ]{1}$/)) {
          this.onCharacterKeyPress(e.key);
        }
      };

      render() {
        return (
          <Component
            {...this.state}
            {...this.props}
            onHighlight={this.throttledOnHighlight}
            onKeyDown={this.onKeyDown}
          />
        );
      }
    };
}
Example #15
Source File: pdfdraw.js    From pdfdraw with GNU Affero General Public License v3.0 4 votes vote down vote up
(function() {
"use strict";

// TODO(jojo): Should get this from a viewer.js property.
var CSS_UNITS = 96.0 / 72.0;
var INITAL_COLORS  = [
  "#ff0000",
  "#008080",
  "#00ffff",
  "#00ff00",
  "#008000",
  "#c0c0c0",
  "#f7347a",
  "#990000",
  "#ccff00",
  "#3399ff",
  "#f6546a",
  "#ffff00",
  "#ffa500",
  "#0000ff",
  "#800080",
];

var DEFAULT_STROKE_WIDTH = 5;

var PERMISSION_CREATE = 4;
var PERMISSION_READ = 1;
var PERMISSION_UPDATE = 2;
var PERMISSION_DELETE = 8;
var PERMISSION_SHARE = 16;
var PERMISSION_ALL = 31;

// Taken from https://gist.github.com/jed/982883
function uuidv4() {
  return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
    (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  );
}

function getRandomColor() {
  return INITAL_COLORS[Math.floor(Math.random() * INITAL_COLORS.length)];
}

// Required for running in older versions of Qt (see #31).
// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
if (!String.prototype.padStart) {
  String.prototype.padStart = function padStart(targetLength, padString) {
    targetLength = targetLength >> 0; //truncate if number, or convert non-number to 0;
    padString = String(padString !== undefined ? padString : ' ');
    if (this.length >= targetLength) {
      return String(this);
    } else {
      targetLength = targetLength - this.length;
      if (targetLength > padString.length) {
        padString += padString.repeat(targetLength / padString.length); //append to original to ensure we are longer than needed
      }
      return padString.slice(0, targetLength) + String(this);
    }
  };
}

var uid = uuidv4();
var object_count = 0;

function getObjectId() {
  return uid + ":" + (++object_count);
}

var Storage = function(prefix) {
  this.prefix = prefix;
  this.storage = window.localStorage;
  if (!this.storage) {
    console.warn('LocalStorage not available, saving of settings will be disabled.');
  }
};

Storage.prototype._getKey = function(key) {
  if (!this.prefix) {
    return key;
  }

  return this.prefix + '.' + key;
};

Storage.prototype.get = function(key) {
  if (!this.storage) {
    return null;
  }

  return this.storage.getItem(this._getKey(key));
};

Storage.prototype.set = function(key, value) {
  if (!this.storage) {
    return;
  } else if (!value) {
    this.storage.removeItem(this._getKey(key));
    return;
  }

  this.storage.setItem(this._getKey(key), value);
};

var BaseDrawer = function(annotator) {
  this.annotator = annotator;
};

BaseDrawer.prototype.destroy = function() {};
BaseDrawer.prototype.activate = function(page_annotator) {};
BaseDrawer.prototype.updateSettings = function(page_annotator) {};
BaseDrawer.prototype.onMouseDown = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseUp = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseDrag = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseEnter = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseLeave = function(page_annotator, event) {};
BaseDrawer.prototype.onMouseMove = function(page_annotator, event) {};
BaseDrawer.prototype.onClick = function(page_annotator, event) {};
BaseDrawer.prototype.onKeyUp = function(page_annotator, event) {};
BaseDrawer.prototype.onItemMoved = function(page_annotator, item, event) {};

var NullDrawer = function() {
  BaseDrawer.apply(this, arguments);
};
NullDrawer.prototype = Object.create(BaseDrawer.prototype);

var FreehandDrawer = function(annotator) {
  BaseDrawer.apply(this, arguments);
  this.path = null;
  this.intervalId = null;
};
FreehandDrawer.prototype = Object.create(BaseDrawer.prototype);

FreehandDrawer.prototype.sendPath = function() {
  if (this.path && this.path.segments.length) {
    this.annotator.sendItem(this.page_annotator, this.path);
  }
};

FreehandDrawer.prototype.onMouseDown = function(page_annotator, event) {
  page_annotator.activate();
  this.page_annotator = page_annotator;
  this.intervalId = setInterval(this.sendPath.bind(this), 250);
  this.path = page_annotator.createPath({
    name: getObjectId(),
    strokeColor: this.annotator.color,
    strokeWidth: this.annotator.strokeWidth,
  });
};

FreehandDrawer.prototype.onMouseUp = function(page_annotator, event) {
  if (!this.path) {
    return;
  }
  if (this.path.segments.length) {
    this.path.smooth();
    this.annotator.sendItem(page_annotator, this.path);
  }
  this.path = null;
  clearInterval(this.intervalId);
};

FreehandDrawer.prototype.onMouseDrag = function(page_annotator, event) {
  if (!this.path) {
    return;
  }
  this.path.add(event.point);
};

var LineDrawer = function(annotator) {
  FreehandDrawer.apply(this, arguments);
};

LineDrawer.prototype = Object.create(FreehandDrawer.prototype);

LineDrawer.prototype.onMouseDrag = function(page_annotator, event) {
  if (!this.path) {
    return;
  }
  this.path.removeSegment(1);
  this.path.add(event.point);
};

var RectangleDrawer = function(annotator) {
  BaseDrawer.apply(this, arguments);
  this.options = {};
  this.rect = null;
};

RectangleDrawer.prototype = Object.create(BaseDrawer.prototype);

RectangleDrawer.prototype.onMouseDown = function(page_annotator, event) {
  page_annotator.activate();
  this.options = {
    name: getObjectId(),
    strokeColor: this.annotator.color,
    strokeWidth: this.annotator.strokeWidth,
    from: [event.point.x, event.point.y],
    to: [event.point.x, event.point.y],
  };
  this.update(page_annotator);
};

RectangleDrawer.prototype.update = function(page_annotator) {
  var create = page_annotator.createRectangle.bind(page_annotator);
  if (!this.rect) {
    this.rect = create(this.options);
  } else {
    this.rect = this.rect.replaceWith(create(this.options));
  }
};

RectangleDrawer.prototype.onMouseUp = function(page_annotator, event) {
  if (!this.rect) {
    return;
  }
  this.options.to = [event.point.x, event.point.y];
  this.update(page_annotator);
  this.rect.ready = true;
  this.annotator.sendItem(page_annotator, this.rect);
  this.options = {};
  this.rect = null;
};

RectangleDrawer.prototype.onMouseDrag = function(page_annotator, event) {
  if (!this.rect) {
    return;
  }
  this.options.to = [event.point.x, event.point.y];
  this.update(page_annotator);
};

var EllipseDrawer = function(annotator) {
  BaseDrawer.apply(this, arguments);
  this.options = {};
  this.ellipse = null;
};

EllipseDrawer.prototype = Object.create(BaseDrawer.prototype);

EllipseDrawer.prototype.onMouseDown = function(page_annotator, event) {
  page_annotator.activate();
  this.options = {
    name: getObjectId(),
    strokeColor: this.annotator.color,
    strokeWidth: this.annotator.strokeWidth,
    from: [event.point.x, event.point.y],
    to: [event.point.x, event.point.y],
  };
  this.update(page_annotator);
};

EllipseDrawer.prototype.update = function(page_annotator) {
  var create = page_annotator.createEllipse.bind(page_annotator);
  if (!this.ellipse) {
    this.ellipse = create(this.options);
  } else {
    var temp_ellipse = create(this.options);
    this.ellipse = this.ellipse.replaceWith(temp_ellipse);
  }
};

EllipseDrawer.prototype.onMouseUp = function(page_annotator, event) {
  if (!this.ellipse) {
    return;
  }
  this.options.to = [event.point.x, event.point.y];
  this.update(page_annotator);
  this.ellipse.ready = true;
  this.annotator.sendItem(page_annotator, this.ellipse);
  this.options = {};
  this.ellipse = null;
};

EllipseDrawer.prototype.onMouseDrag = function(page_annotator, event) {
  if (!this.ellipse) {
    return;
  }
  this.options.to = [event.point.x, event.point.y];
  this.update(page_annotator);
};

var PointerDrawer = function(annotator) {
  BaseDrawer.apply(this, arguments);
};
PointerDrawer.prototype = Object.create(BaseDrawer.prototype);

PointerDrawer.prototype.destroy = function() {
  this.annotator.destroyCursors();
};

PointerDrawer.prototype.onMouseMove = function(page_annotator, event) {
  this.annotator.renderCursor(page_annotator, event.point.x, event.point.y);
};

PointerDrawer.prototype.onMouseLeave = function(page_annotator, event) {
  this.annotator.hideCursor();
};

var SelectDrawer = function(annotator) {
  BaseDrawer.apply(this, arguments);
  this.selected_item = null;
  this.prev_settings = null;
  this.pending_items = {};
  this.send_interval = setInterval(this._sendPending.bind(this), 100);
};
SelectDrawer.prototype = Object.create(BaseDrawer.prototype);

SelectDrawer.prototype._select = function(item) {
  if (this.selected_item === item) {
    return;
  }

  this._unselect();
  this.selected_item = item;
  if (this.selected_item.onFocus) {
    var result = this.selected_item.onFocus();
    if (result) {
      this.selected_item = result;
    }
  } else {
    this.selected_item.shadowColor = "black";
    this.selected_item.shadowBlur = 10;
  }

  var color = this.selected_item.strokeColor;
  if (typeof(color) !== 'string') {
    color = color.toCSS(true);
    if (this.selected_item.strokeColor.hasAlpha() && color.length <= 7) {
      color += Math.round(this.selected_item.strokeColor.alpha * 255).toString(16);
    }
  }
  var settings = {
    'color': color,
    'strokeWidth': this.selected_item.strokeWidth,
  };
  this.prev_settings = this.annotator.updateSettings(settings);
};

SelectDrawer.prototype._unselect = function() {
  var reset;
  if (this.prev_settings) {
    if (this.selected_item) {
      reset = {
        'strokeColor': this.selected_item.strokeColor,
        'strokeWidth': this.selected_item.strokeWidth,
      };
    }
    this.annotator.updateSettings(this.prev_settings);
    this.prev_settings = null;
  }

  if (!this.selected_item) {
    return;
  }

  if (this.selected_item.onBlur) {
    this.selected_item.onBlur();
  } else {
    this.selected_item.shadowColor = null;
    this.selected_item.shadowBlur = 0;
  }
  if (reset) {
    this.selected_item.strokeColor = reset.strokeColor;
    this.selected_item.strokeWidth = reset.strokeWidth;
  }
  this.selected_item = null;
};

SelectDrawer.prototype._sendItem = function(page_annotator, item) {
  // Need to remove "selected" layout while generating item JSON.
  var is_selected = (item === this.selected_item);
  if (is_selected) {
    this._unselect();
  }
  this.annotator.sendItem(page_annotator, item);
  if (is_selected) {
    this._select(item);
  }
};

SelectDrawer.prototype._sendPending = function() {
  var now = Date.now();
  for (var name in this.pending_items) {
    if (!this.pending_items.hasOwnProperty(name)) {
      continue;
    }

    var info = this.pending_items[name];
    var page_annotator = info[0];
    var item = info[1];
    var last_change = info[2];
    if (now - last_change < 250) {
      continue;
    }

    this._sendItem(page_annotator, item);
    delete this.pending_items[name];
  }
};

SelectDrawer.prototype.destroy = function() {
  clearInterval(this.send_interval);
  this._sendPending();
  this._unselect();
};

SelectDrawer.prototype._hitItem = function(page_annotator, point) {
  var hit = page_annotator.scope.project.hitTest(point);
  if (!hit) {
    // Check if user clicked on a text annotation.
    each(page_annotator.textAnnotations, function(ta) {
      var elem = ta.textareaContainer;
      if (!elem.is(':visible')) {
        return;
      }

      var rect = elem[0].getBoundingClientRect();
      if (rect.left <= point.x && rect.right >= point.x &&
          rect.top <= point.y && rect.bottom >= point.y) {
        hit = {
          "item": ta,
        };
      }
    });
    if (!hit) {
      this._unselect();
      return;
    }
  }

  this._select(hit.item);
};

SelectDrawer.prototype.onMouseDown = function(page_annotator, event) {
  this._hitItem(page_annotator, event.point);
};

SelectDrawer.prototype.onClick = function(page_annotator, event) {
  this._hitItem(page_annotator, event.point);
};

SelectDrawer.prototype.onKeyUp = function(page_annotator, event) {
  if (!this.selected_item) {
    return;
  }

  if (this.selected_item.ignoreKeyUp && this.selected_item.ignoreKeyUp()) {
    return;
  }

  switch (event.keyCode) {
    case 8: // Backspace
      // Fallthrough
    case 46: // Delete key
      var item = this.selected_item;
      this.annotator.deleteItem(page_annotator, item);
      this._unselect();
      item.remove();
      break;
  }
};

SelectDrawer.prototype.onItemMoved = function(page_annotator, item, event) {
  event.stopPropagation();
  item.position.x += event.delta.x;
  item.position.y += event.delta.y;
  this.addPendingItem(page_annotator, item);
};

SelectDrawer.prototype.addPendingItem = function(page_annotator, item) {
  if (this.pending_items.hasOwnProperty(item.name)) {
    this.pending_items[item.name][0] = page_annotator;
    this.pending_items[item.name][1] = item;
  } else {
    this.pending_items[item.name] = [page_annotator, item, Date.now()];
  }
};

SelectDrawer.prototype.updateSettings = function(page_annotator) {
  if (!this.selected_item) {
    return;
  }

  if (this.selected_item.strokeColor === this.annotator.color &&
      this.selected_item.strokeWidth === this.annotator.strokeWidth) {
    return;
  }

  this.selected_item.strokeColor = this.annotator.color;
  this.selected_item.strokeWidth = this.annotator.strokeWidth;
  this.addPendingItem(page_annotator, this.selected_item);
};

var TextDrawer = function(annotator)  {
  BaseDrawer.apply(this, arguments);
};
TextDrawer.prototype = Object.create(BaseDrawer.prototype);

TextDrawer.prototype.onClick = function(page_annotator, event) {
  var author = this.annotator.displayname;
  var textarea = page_annotator.createTextArea({
    'x': event.point.x,
    'y': event.point.y,
    'author': author,
  });
  textarea.sendData();
  textarea.onFocus();
  textarea.textarea.trigger('focus');
  this.annotator.setDrawMode("select");
};

var Cursor = function(annotator, userid, radius) {
  this.annotator = annotator;
  this.page_annotator = null;
  this.name = getObjectId();
  this.userid = userid;
  this.radius = radius;
  this.circle = null;
  this.label = $("<div class='cursor'></div>");
  this.label.css('position', 'absolute');
};

Cursor.prototype.destroy = function() {
  if (this.page_annotator) {
    this.page_annotator.unregisterPendingActivate(this);
  }
  if (this.circle) {
    this.circle.remove();
    this.circle = null;
  }
  this.label.remove();
};

Cursor.prototype.draw = function(page_annotator, x, y, color, text) {
  this.x = x;
  this.y = y;
  var center = new paper.Point(x, y);
  if (this.page_annotator && page_annotator !== this.page_annotator) {
    this.page_annotator.unregisterPendingActivate(this);
    if (this.page_annotator.container) {
      this.page_annotator.container.remove(this.label);
    }
    this.page_annotator = null;
    this.destroy();
  }
  if (!this.circle) {
    page_annotator.activate();
    this.circle = new paper.Path.Circle(center, this.radius);
    this.circle.name = getObjectId();
  }
  if (!this.page_annotator) {
    this.page_annotator = page_annotator;
    if (this.page_annotator.container) {
      this.page_annotator.container.append(this.label);
    } else {
      this.page_annotator.registerPendingActivate(this);
    }
  }
  this.circle.set({
    fillColor: color,
    shadowColor: color,
    shadowBlur: 10,
    position: center
  });
  this.label.html(text);
  this.update();
};

Cursor.prototype.activate = function(page_annotator) {
  if (page_annotator !== this.page_annotator) {
    return;
  }

  this.label.remove();
  if (page_annotator.container) {
    page_annotator.container.append(this.label);
  }
};

Cursor.prototype.update = function() {
  var radius = this.circle.bounds.width / 2;
  var newRadius = this.radius / this.page_annotator.scale;
  if (radius !== newRadius) {
    this.circle.scale(newRadius / radius);
  }
  var x = this.x * this.page_annotator.scale;
  var y = this.y * this.page_annotator.scale;
  this.label.css('left', x + (this.radius * 2));
  this.label.css('top', y - (this.radius));
};

var TextArea = function(page_annotator, text, color, author) {
  this.page_annotator = page_annotator;
  this.content = text;
  this.color = color;
  this.author = author;
  this.modified = Math.round((new Date()).getTime() / 1000);
  this.name = getObjectId();
  this.textareaContainer = $('<div class="textareaContainer form-group shadow-textarea"></div>');
  this.textareaContainer.draggable({
    start: function(event, ui) {
      if ($('textarea', this.textareaContainer).prop("disabled")) {
        event.preventDefault();
      }
    }.bind(this),
    drag: function(event, ui) {
      this.textareaContainerPos = [
        ui.position.left / this.page_annotator.scale,
        ui.position.top / this.page_annotator.scale
      ];
      this.update();
      this.sendData();
    }.bind(this),
    stop: function(event, ui) {
      this.textareaContainerPos = [
        ui.position.left / this.page_annotator.scale,
        ui.position.top / this.page_annotator.scale
      ];
      this.update();
      this.sendData();
    }.bind(this),
  });
  this.textareaContainer.on('click', function(event) {
    this.page_annotator.annotator.drawer.onClick(this.page_annotator, {
      'point': {
        'x': event.originalEvent.clientX,
        'y': event.originalEvent.clientY,
      },
    });
  }.bind(this));
  this.textareaContainer.on('mouseup', function(event) {
    this.page_annotator.annotator.drawer.onMouseUp(this.page_annotator, {
      'point': {
        'x': event.originalEvent.clientX,
        'y': event.originalEvent.clientY,
      },
    });
  }.bind(this));
  this.page_annotator.container.append(this.textareaContainer);
  this.circle = null;
  this.line = null;
  this.textarea = $('<textarea class="form-control" disabled="disabled"></textarea>');
  this.authorLabel = $('<span class="author"</span>');
  this.closeButton = $('<button type="button" disabled="disabled" class="deleteButton btn btn-danger form-control close" aria-label="Close"> <span aria-hidden="true">&times;</span></button>');
  this.closeButton.on('click', function() {
    this.textareaContainer.hide();
    this.update();
  }.bind(this));
  this.textarea.on('input', function(e) {
    if (this.content === this.textarea.val()) {
      return;
    }
    this.content = this.textarea.val();
    this.author = this.page_annotator.annotator.displayname;
    this.modified = Math.round((new Date()).getTime() / 1000);
    this.update();
    this.sendData();
  }.bind(this));
  this.textarea.on('focus', function(e) {
    this.onFocus();
  }.bind(this));
  this.textarea.on('dragstart', function(e) {
    e.preventDefault();
  }.bind(this));
  this.textareaContainer.append(this.authorLabel, this.closeButton, this.textarea);
  this.textareaContainer.css("background-color", this.color);
  this.textarea.css("font-size", this.page_annotator.annotator.fontSize+"px");

  this._sendData = throttle(function() {
    this.page_annotator.annotator.sendItem(this.page_annotator, this);
  }.bind(this), 250);
};

TextArea.prototype.exportJSON = function() {
  var data = {
    "type": "text-annotation",
    "anchor": [
      this.circle.position.x,
      this.circle.position.y,
    ],
    "author": this.author,
    "color": this.color,
    "content": this.content,
    "modified": this.modified,
    "pos": [
      this.textareaContainerPos[0] / this.page_annotator.scale,
      this.textareaContainerPos[1] / this.page_annotator.scale,
    ],
  };

  return JSON.stringify(data);
};

Object.defineProperty(TextArea.prototype, 'strokeColor', {
  "get": function() {
    return this.color;
  },
  "set": function(value) {
    if (this.color === value) {
      return;
    }

    this.color = value;
    this.textareaContainer.css("background-color", this.color);
    if (!$('textarea', this.textareaContainer).prop("disabled")) {
      this.textareaContainer.css("box-shadow", "0px 0px 15px 5px"+this.color);
    }
    this.authorLabel.css('background-color', this.color);
    if (this.circle) {
      this.circle.fillColor = value;
    }
    if (this.line) {
      this.line.strokeColor = value;
    }
  },
});

Object.defineProperty(TextArea.prototype, 'strokeWidth', {
  "get": function() {
    return this.page_annotator.annotator.strokeWidth;
  },
  "set": function(value) {
    // Not supported by text annotations.
  },
});

TextArea.prototype.sendData = function() {
  this._sendData();
};

TextArea.prototype.draw = function(x, y) {
  this.x = x;
  this.y = y;
  if (!this.circle) {
    this.drawCircle();
  }
  if (!this.textareaContainerPos) {
    this.textareaContainerPos = [x, y];
  }
  this.update();
};

TextArea.prototype.drawCircle = function() {
  var center = new paper.Point(this.x, this.y);
  var circle = new paper.Path.Circle(center, 10);
  circle.name = this.name+"pointer";
  circle.set({
    fillColor: this.color,
    shadowColor: this.color,
    shadowBlur: 2,
    position: center
  });

  circle.onClick = function(e) {
    this.textareaContainer.show();
    this.textarea.trigger('blur');
    this.update();
  }.bind(this);
  circle.onFocus = function() {
    this.onFocus();
    return this;
  }.bind(this);
  circle.onBlur = function() {
    this.onBlur();
    return this;
  }.bind(this);
  circle.onMouseDrag = function(event) {
    event.stopPropagation();
    this.textarea.trigger('blur');
    this.circle.position.x += event.delta.x;
    this.circle.position.y += event.delta.y;
    this.update();
    this.sendData();
  }.bind(this);
  circle.onMouseUp = function(event) {
    this.update();
    this.sendData();
  }.bind(this);
  this.circle = circle;
};

TextArea.prototype.update = function(page_annotator) {
  this.authorLabel.text(this.author || 'Anonymous');
  this.authorLabel.attr('title', this.author || 'Anonymous');
  this.authorLabel.css('background-color', this.color);
  this.textareaContainer.css('left', this.textareaContainerPos[0]*this.page_annotator.scale);
  this.textareaContainer.css('top', this.textareaContainerPos[1]*this.page_annotator.scale);
  this.textareaContainer.css("background-color", this.color);
  this.textarea.css("font-size", this.page_annotator.annotator.fontSize+"px");
  this.textarea.val(this.content);
  if (this.line) {
    this.line.remove();
    this.line = null;
  }
  if (this.textareaContainer.is(':visible')) {
    var from = new paper.Point(
      this.circle.position.x,
      this.circle.position.y);
    var to = new paper.Point(
      this.textareaContainerPos[0],
      this.textareaContainerPos[1]);
    this.line = new paper.Path.Line(from, to);
    this.line.strokeColor = this.color;
    this.line.strokeWidth = 2;
    if (!$('textarea', this.textareaContainer).prop("disabled")) {
      this.line.shadowColor = "black";
      this.line.shadowBlur = 5;
    }
    this.line.onFocus = function() {
      this.onFocus();
      return this;
    }.bind(this);
    this.line.onBlur = function() {
      this.onBlur();
      return this;
    }.bind(this);
  }
};

TextArea.prototype.remove = function() {
  this.circle.remove();
  if (this.line) {
    this.line.remove();
  }
  this.textareaContainer.remove();
  delete this.page_annotator.textAnnotations[this.name];
};

TextArea.prototype.ignoreKeyUp = function() {
  if (this.textarea.is(':focus')) {
    // The textarea currently is focused, ignore keypresses in drawer,
    // otherwise the element gets deleted when the user presses "delete".
    return true;
  }

  return false;
};

TextArea.prototype.onFocus = function() {
  if (this._selecting) {
    return;
  }

  this._selecting = true;
  $('textarea', this.textareaContainer).prop("disabled", false);
  this.closeButton.prop("disabled", false);
  this.textareaContainer.css("box-shadow", "0px 0px 15px 5px"+this.color);
  this.circle.shadowColor = "black";
  this.circle.shadowBlur = 10;
  if (this.line) {
    this.line.shadowColor = "black";
    this.line.shadowBlur = 5;
  }
  this._selecting = false;
};

TextArea.prototype.onBlur = function() {
  if (this._unselecting) {
    return;
  }

  this._unselecting = true;
  $('textarea', this.textareaContainer).prop("disabled", true);
  this.closeButton.prop("disabled", true);
  this.textareaContainer.draggable("disable");
  this.textareaContainer.css("box-shadow", "none");
  this.circle.shadowColor = null;
  this.circle.shadowBlur = 0;
  if (this.line) {
    this.line.shadowColor = null;
    this.line.shadowBlur = 0;
  }
  this.textarea.trigger('blur');
  this._unselecting = false;
};

TextArea.prototype.onResized = function(scale) {
  this.page_annotator.container.append(this.textareaContainer);
  this.draw(this.x, this.y);
};

var PageAnnotator = function(annotator, pagenum, container, page) {
  this.annotator = annotator;
  this.pagenum = pagenum;
  this.container = container;
  this.page = page;
  this.pending_activation = {};
  this.textAnnotations = {};
  this.canvas = document.createElement("canvas");
  this.canvas.style.position = "absolute";
  this.canvas.style.left = 0;
  this.canvas.style.top = 0;
  this.canvas.style.right = 0;
  this.canvas.style.bottom = 0;
  this.scope = new paper.PaperScope();
  this.scope.setup(this.canvas);
  this.scope.project.options.hitTolerance = 5;
  if (page) {
    this.setPage(page, container);
  }
  this.view = this.scope.getView();
  this.view.on('mousedown', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mousedown', [args]);
  }.bind(this));
  this.view.on('mouseup', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mouseup', [args]);
  }.bind(this));
  this.view.on('mousedrag', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mousedrag', [args]);
  }.bind(this));
  this.view.on('mouseenter', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mouseenter', [args]);
  }.bind(this));
  this.view.on('mouseleave', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mouseleave', [args]);
  }.bind(this));
  // Sometimes the "mouseleave" event of paper.js doesn't fire when
  // switching views, also handle "mouseleave" on the canvas itself
  // as a workaround.
  $(this.canvas).on('mouseleave', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mouseleave', [args]);
  }.bind(this));
  this.view.on('mousemove', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('mousemove', [args]);
  }.bind(this));
  this.view.on('click', function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('click', [args]);
  }.bind(this));
  $(window).on('keyup', function(event) {
    if (PageAnnotator.prototype.__active !== this) {
      return;
    }

    var args = Array.prototype.slice.call(arguments);
    args.unshift(this);
    this.activate();
    this.annotator.e.trigger('keyup', [args]);
  }.bind(this));
};

PageAnnotator.prototype.exportSVG = function() {
  // Hide circle and line for text annotations so they don't show up in exported PDFs.
  each(this.textAnnotations, function(ta) {
    ta.circle.remove();
    if (ta.line) {
      ta.line.remove();
    }
  }.bind(this));

  var svg = this.scope.project.exportSVG({
    asString: true
  });

  each(this.textAnnotations, function(ta) {
    this.scope.project.activeLayer.addChild(ta.circle);
    if (ta.line) {
      this.scope.project.activeLayer.addChild(ta.line);
    }
  }.bind(this));
  return svg;
};

PageAnnotator.prototype.activate = function() {
  PageAnnotator.prototype.__active = this;
  for (var drawerName in this.pending_activation) {
    if (!this.pending_activation.hasOwnProperty(drawerName)) {
      continue;
    }

    this.pending_activation[drawerName].activate(this);
  }
  this.pending_activation = {};
  this.scope.activate();
};

PageAnnotator.prototype.registerPendingActivate = function(drawer) {
  this.pending_activation[drawer.name] = drawer;
};

PageAnnotator.prototype.unregisterPendingActivate = function(drawer) {
  delete this.pending_activation[drawer.name];
};

PageAnnotator.prototype.setPage = function(page, container) {
  this.container = container;
  this.page = page;
  var pagesize = page.view;
  this.pagewidth = (pagesize[2] - pagesize[0]) * CSS_UNITS;
  this.pageheight = (pagesize[3] - pagesize[1]) * CSS_UNITS;
  if (this.container) {
    this.container.append(this.canvas);
  }
};

PageAnnotator.prototype.update = function(scale) {
  if (this.container && !this.container.has(this.canvas).length) {
    this.container.append(this.canvas);
  }
  this.scale = scale;
  var width = this.pagewidth * scale;
  var height = this.pageheight * scale;
  this.view.viewSize = new paper.Size(width, height);
  this.view.center = new paper.Point(
    (width + (this.pagewidth * (1 - scale))) / 2,
    (height + (this.pageheight * (1 - scale))) / 2);
  this.view.zoom = scale;
  this.activate();
  each(this.textAnnotations, function(ta) {
    ta.onResized();
  });
  this.view._needsUpdate = true;
  this.view.requestUpdate();
};

PageAnnotator.prototype.createPath = function(options) {
  var path = new paper.Path(options || {});
  path.onMouseDrag = function(event) {
    this.annotator.drawer.onItemMoved(this, path, event);
  }.bind(this);
  return path;
};

PageAnnotator.prototype.createRectangle = function(options) {
  var rect = new paper.Path.Rectangle(options || {});
  rect.onMouseDrag = function(event) {
    if (rect.ready) {
      this.annotator.drawer.onItemMoved(this, rect, event);
    }
  }.bind(this);
  return rect;
};

PageAnnotator.prototype.createEllipse = function(options) {
  var from = new paper.Point(options.from);
  var to = new paper.Point(options.to);
  var rectangle = new paper.Rectangle(
    from, to
  );
  var ellipse = new paper.Path.Ellipse(rectangle);
  ellipse.strokeColor = options.strokeColor;
  ellipse.strokeWidth = options.strokeWidth;
  ellipse.name = options.name;
  ellipse.onMouseDrag = function(event) {
    if (ellipse.ready) {
      this.annotator.drawer.onItemMoved(this, ellipse, event);
    }
  }.bind(this);
  return ellipse;
};

PageAnnotator.prototype.createTextArea = function(options) {
  var text = options.text || "";
  var color = options.color || this.annotator.color;
  var author = options.author || "";
  var textarea = new TextArea(this, text, color, author);
  if (options.name) {
    textarea.name = options.name;
  }
  if (options.modified) {
    textarea.modified = options.modified;
  }
  this.textAnnotations[textarea.name] = textarea;
  textarea.draw(options.x, options.y);
  if (options.anchor && textarea.circle) {
    textarea.circle.position = {
      x: options.anchor[0],
      y: options.anchor[1],
    };
  }
  return textarea;
};

PageAnnotator.prototype.drawItem = function(name, data) {
  this.activate();
  var path = this.scope.project.getItem({"name": name});
  if (!path) {
    path = this.createPath();
  }
  try {
    path.importJSON(data);
  } catch (e) {
    path.remove();
    path.name = name;
    this.annotator.deleteItem(this, path);
    console.log("Could not import item", data, e);
    return;
  }
  path.name = name;
};

PageAnnotator.prototype.drawTextAnnotation = function(userid, name, data) {
  this.activate();
  var textarea;
  if (this.textAnnotations.hasOwnProperty(name)) {
    textarea = this.textAnnotations[name];
    textarea.author = data.author;
    textarea.modified = data.modified;
    textarea.color = data.color;
    textarea.content = data.content;
    textarea.circle.position = {
      x: Math.max(0, data.anchor[0]),
      y: Math.max(0, data.anchor[1]),
    };
    textarea.draw(data.pos[0], data.pos[1]);
  } else {
    textarea = this.createTextArea({
      'x': data.pos[0],
      'y': data.pos[1],
      'text': data.content,
      'color': data.color,
      'name': name,
      'anchor': data.anchor,
      'author': data.author,
      'modified': data.modified,
    });
  }
  textarea.update();
  return textarea;
};

PageAnnotator.prototype.deleteItem = function(name) {
  this.activate();
  var path = this.scope.project.getItem({"name": name});
  if (!path) {
    return;
  }

  path.remove();
};

function parseStrokeWidth(value) {
  if (typeof(value) !== "number") {
    value = parseInt(value, 10);
  }
  if (value <= 0 || isNaN(value)) {
    return DEFAULT_STROKE_WIDTH;
  }

  return value;
}

function Annotator(socketurl, id, userid, displayname, token) {
  this.e = $({});
  this.annotators = {};
  this.cursors = {};
  this.draw_mode = null;
  this.drawer = this.nulldrawer = new NullDrawer();
  this.id = id;
  this.userid = userid;
  this.displayname = displayname;
  this.token = token;
  this.storage = new Storage('pdfdraw');
  var color = this.storage.get('color');
  if (!color) {
    color = getRandomColor();
    this.storage.set('color', color);
  }
  this.color = color;
  var strokeWidth = this.storage.get('strokeWidth');
  if (!strokeWidth) {
    strokeWidth = DEFAULT_STROKE_WIDTH;
    this.storage.set('strokeWidth', strokeWidth);
  } else {
    strokeWidth = parseStrokeWidth(strokeWidth);
  }
  this.strokeWidth = strokeWidth;
  this.fontSize = 12;
  this.pageCount = -1;
  this.currentPage = null;
  this.users = {};
  this.has_document = false;
  this.setDrawMode('pointer');
  this.pending_messages = [];
  this.socketurl = socketurl;
  this.socket = io(socketurl, {
    'transports': ['websocket'],
    'query': {
      'token': token
    }
  });
  // Also allow polling connections when the connection was interrupted once
  // (could be caused by proxy / firewall).
  this.socket.io.on('reconnect_attempt', function() {
    this.socket.io.opts.transports = ['polling', 'websocket'];
  }.bind(this));
  this.socket.io.on('reconnect', this.onReconnected.bind(this));
  this.socket.on('message', this.onMessage.bind(this));
  this.socket.on('user.joined', this.onUserJoined.bind(this));
  this.socket.on('user.left', this.onUserLeft.bind(this));
  this.socket.on('connect', this.onConnected.bind(this));
  this.socket.on('disconnect', this.onDisconnect.bind(this));

  this.colorPicker = new iro.ColorPicker("#colorPicker", {
    width: 320,
    height: 320,
    color: this.color,
    transparency: true,
  });
  var setColor = function(color) {
    this.color = color.hex8String;
    this.storage.set('color', this.color);
    $(".modeButton.colorMode, .modeButton.colorMode:focus")
      .css("background-color", this.color);
    if (!this._updating_settings) {
      this.drawer.updateSettings(PageAnnotator.prototype.__active);
    }
  }.bind(this);
  this.colorPicker.on("color:init", setColor);
  this.colorPicker.on("color:change", setColor);

  $('#inputStrokeWidth').val(this.strokeWidth);
  $('#strokeWidthValue').text(this.strokeWidth);
  $('#inputStrokeWidth').on('input', function(e) {
    var strokeWidth = parseStrokeWidth(e.target.value);
    this.storage.set('strokeWidth', strokeWidth);
    this.strokeWidth = strokeWidth;
    if (!this._updating_settings) {
      this.drawer.updateSettings(PageAnnotator.prototype.__active);
    }
  }.bind(this));

  $('#inputStrokeWidth').on('change', function(e) {
    var strokeWidth = parseStrokeWidth(e.target.value);
    $('#strokeWidthValue').text(strokeWidth);
  }.bind(this));

  this.userlist = $("<div class='userlist'></div>");
  $("#mainContainer").append(this.userlist);
  this.connectionError = $("#connectionError");
  this.connectingMessage = $("#connectingMessage");

  this.e.on('mousedown', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseDown.apply(this.drawer, args);
  }.bind(this));
  this.e.on('mouseup', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseUp.apply(this.drawer, args);
  }.bind(this));
  this.e.on('mousedrag', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseDrag.apply(this.drawer, args);
  }.bind(this));
  this.e.on('mouseenter', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseEnter.apply(this.drawer, args);
  }.bind(this));
  this.e.on('mouseleave', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseLeave.apply(this.drawer, args);
  }.bind(this));
  this.e.on('mousemove', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onMouseMove.apply(this.drawer, args);
  }.bind(this));
  this.e.on('keyup', function(event, args) {
    if (this.settings_open) {
      event.preventDefault();
      return;
    }

    this.drawer.onKeyUp.apply(this.drawer, args);
  }.bind(this));
  this.e.on('click', function(event, args) {
    if (this.settings_open) {
      this.drawer.updateSettings(args[0]);
      this.toggleSettings();
      return;
    }

    this.drawer.onClick.apply(this.drawer, args);
  }.bind(this));
}

Annotator.prototype.updateSettings = function(settings) {
  this._updating_settings = true;
  var prev = {
    'color': this.colorPicker.color.hex8String,
    'strokeWidth': this.strokeWidth,
  };
  this.colorPicker.color.hex8String = settings.color;
  $('#inputStrokeWidth').val(settings.strokeWidth);
  $('#strokeWidthValue').text($('#inputStrokeWidth').val());
  this._updating_settings = false;
  return prev;
};

Annotator.prototype.toggleSettings = function() {
  $("#settingsDialog").toggle();
  this.settings_open = !this.settings_open;
};

Annotator.prototype.onDisconnect = function() {
  this.users = {};
  this.updateUsersList();
  this.connectionError.show();
};

Annotator.prototype.onConnected = function() {
  if (!this.connectingMessage) {
    return;
  }

  this.connectingMessage.hide();
  this.connectingMessage = null;
};

Annotator.prototype.onReconnected = function() {
  this.connectionError.hide();
};

function compareEntries(a, b) {
  // Compare by display name.
  a = a[1].toLocaleLowerCase();
  b = b[1].toLocaleLowerCase();
  return a.localeCompare(b);
}

Annotator.prototype.updateUsersList = function() {
  var entries = [];
  for (var userid in this.users) {
    if (!this.users.hasOwnProperty(userid)) {
      continue;
    }

    entries.push([userid, this.users[userid].displayname || "Anonymous"]);
  }
  this.userlist.empty();
  if (!entries.length) {
    this.userlist.hide();
    return;
  }

  entries.sort(compareEntries);
  for (var i = 0; i < entries.length; ++i) {
    userid = entries[i][0];
    var displayname = entries[i][1];
    var elem = $("<div></div>").text(displayname);
    if (userid === this.socket.id) {
      elem.addClass("own");
    }
    this.userlist.append(elem);
  }
  this.userlist.show();
};

Annotator.prototype.onUserJoined = function(message) {
  for (var i = 0; i < message.length; ++i) {
    var userid = message[i].userid;
    if (!userid) {
      continue;
    }

    this.users[userid] = message[i];
  }
  this.updateUsersList();
};

Annotator.prototype.onUserLeft = function(message) {
  var userid = message.userid;
  if (!userid || !this.users.hasOwnProperty(userid)) {
    return;
  }

  delete this.users[userid];
  this.updateUsersList();
};

Annotator.prototype.documentLoaded = function(pdfDocument) {
  this.pageCount = pdfDocument.numPages;

  this.has_document= true;
  while (this.pending_messages.length) {
    var message = this.pending_messages.shift();
    this.onMessage(message);
  }

  if (this.connectingMessage) {
    this.connectingMessage.show();
  }
};

Annotator.prototype.sendMessage = function(message) {
  // console.log("Send", message);
  this.socket.emit('message', message);
};

Annotator.prototype.sendItem = function(page_annotator, item) {
  var data = item.exportJSON();
  if (!data) {
    console.log("Can't export to JSON", item);
    return;
  }

  this.sendMessage({
    'type': 'item',
    'item': {
      'page': page_annotator.pagenum,
      'name': item.name,
      'data': data
    }
  });
};

Annotator.prototype.deleteItem = function(page_annotator, item) {
  this.sendMessage({
    'type': 'delete',
    'delete': {
      'page': page_annotator.pagenum,
      'name': item.name
    }
  });
};

Annotator.prototype.onMessage = function(message) {
  // console.log("Received", message);
  if (!this.has_document) {
    this.pending_messages.push(message);
    return;
  }

  this.processMessage(message);
};

Annotator.prototype.renderCursor = function(page_annotator, x, y) {
  var cursor = this.getCursor(this.userid, 5);
  cursor.draw(page_annotator, x, y, this.color, this.displayname);
  var data = {
    'type': 'cursor',
    'cursor': {
      'action': 'show',
      'page': page_annotator.pagenum,
      'x': x,
      'y': y,
      'color': this.color
    }
  };
  this.sendMessage(data);
};

Annotator.prototype.getCursor = function(userid, size) {
  var cursor;
  if (!this.cursors.hasOwnProperty(userid)) {
    cursor = this.cursors[userid] = new Cursor(this, userid, size);
  } else {
    cursor = this.cursors[userid];
  }
  return cursor;
};

Annotator.prototype.showCursor = function(userid, data) {
  if (!this.users.hasOwnProperty(userid)) {
    return;
  }

  var displayname = this.users[userid].displayname || "Anonymous";
  var cursor = this.getCursor(userid, 5);
  this.getPage(data.page).then(function(page_annotator) {
    cursor.draw(page_annotator, data.x, data.y, data.color, displayname);
  });
};

Annotator.prototype.hideCursor = function(userid) {
  if (!userid) {
    var data = {
      'type': 'cursor',
      'cursor': {
        'action': 'hide'
      }
    };
    this.sendMessage(data);
    userid = this.userid;
  }
  if (!this.cursors.hasOwnProperty(userid)) {
    return;
  }

  var cursor = this.cursors[userid];
  delete this.cursors[userid];
  cursor.destroy();
};

Annotator.prototype.destroyCursors = function() {
  for (var i in this.cursors) {
    if (!this.cursors.hasOwnProperty(i)) {
      continue;
    }

    this.cursors[i].destroy();
  }
  this.cursors = {};
};

Annotator.prototype.getExistingPage = function(pagenum) {
  if (typeof(pagenum) === "string") {
    pagenum = parseInt(pagenum, 10);
  }
  if (!this.annotators.hasOwnProperty(pagenum)) {
    return null;
  }

  return this.annotators[pagenum];
};

Annotator.prototype.getPage = function(pagenum, page, container) {
  if (typeof(pagenum) === "string") {
    pagenum = parseInt(pagenum, 10);
  }
  return new Promise(function(resolve, reject) {
    var page_annotator;
    if (!this.annotators.hasOwnProperty(pagenum)) {
      page_annotator = this.annotators[pagenum] = new PageAnnotator(this, pagenum, container, page);
    } else {
      page_annotator = this.annotators[pagenum];
    }

    if (page_annotator.page && page_annotator.container) {
      return resolve(page_annotator);
    } else if (page && container) {
      page_annotator.setPage(page, container);
      return resolve(page_annotator);
    }
    if (page_annotator.page || !PDFViewerApplication.pdfViewer.pdfDocument) {
      return resolve(page_annotator);
    }

    PDFViewerApplication.pdfViewer.pdfDocument.getPage(pagenum).then(function(page) {
      page_annotator.setPage(page, page_annotator.container);
      page_annotator.update(PDFViewerApplication.pdfViewer.currentScale);
      return resolve(page_annotator);
    }, function(error) {
      return reject(error);
    });
  }.bind(this));
};

Annotator.prototype.destroyDrawer = function() {
  if (this.drawer && this.drawer !== this.nulldrawer) {
    var drawer = this.drawer;
    this.drawer = this.nulldrawer;
    drawer.destroy();
  }
};

Annotator.prototype.setDrawMode = function(mode) {
  $(".toolbarButton.selected").removeClass('selected');
  this.draw_mode = mode;
  $("#" + (mode || "none") + "Mode").addClass('selected');
  switch (mode) {
    case "select":
    case "color":
    case "pointer":
    case null:
      break;
    default:
      $("#drawModeToolbar").addClass('selected');
      break;
  }

  this.destroyDrawer();
  switch (this.draw_mode) {
    case "freehand":
      this.drawer = new FreehandDrawer(this);
      break;
    case "rectangle":
      this.drawer = new RectangleDrawer(this);
      break;
    case "ellipse":
      this.drawer = new EllipseDrawer(this);
      break;
    case "pointer":
      this.drawer = new PointerDrawer(this);
      break;
    case "select":
      this.drawer = new SelectDrawer(this);
      break;
    case "line":
      this.drawer = new LineDrawer(this);
      break;
    case "text":
      this.drawer = new TextDrawer(this);
      break;
    case null:
      break;
    default:
      console.log("Unknown draw mode", this.draw_mode);
      return;
  }
};

Annotator.prototype.switchPage = function(pagenum) {
  if (pagenum === this.currentPage) {
    return;
  }

  this.currentPage = pagenum;
  this.sendMessage({
    'type': 'control',
    'control': {
      'type': 'page',
      'page': pagenum
    }
  });
};

Annotator.prototype.processMessage = function(message) {
  switch (message.type) {
    case 'cursor':
      this.processCursorMessage(message.userid, message.cursor);
      break;
    case 'item':
      this.processItemMessage(message.userid, message.item);
      break;
    case 'delete':
      this.processDeleteMessage(message.userid, message.delete);
      break;
    case 'control':
      this.processControlMessage(message.userid, message.control);
      break;
    default:
      console.log('Unknown message', message);
      break;
  }
};

Annotator.prototype.processCursorMessage = function(userid, message) {
  switch (message.action) {
    case 'hide':
      this.hideCursor(userid);
      break;
    case 'show':
    default:
      this.showCursor(userid, message);
      break;
  }
};

Annotator.prototype.processItemMessage = function(userid, message) {
  if (typeof message.data === "string") {
    message.data = JSON.parse(message.data);
  }

  this.getPage(message.page).then(function(page_annotator) {
    switch (message.data.type) {
      case "text-annotation":
        page_annotator.drawTextAnnotation(userid, message.name, message.data);
        return;
    }

    page_annotator.drawItem(message.name, message.data);
  });
};

Annotator.prototype.processDeleteMessage = function(userid, message) {
  this.getPage(message.page).then(function(page_annotator) {
    page_annotator.deleteItem(message.name);
  });
};

Annotator.prototype.processControlMessage = function(userid, message) {
  switch (message.type) {
    case 'page':
      var page = message.page;
      if (page >= 1 && page <= PDFViewerApplication.pagesCount && page !== PDFViewerApplication.page) {
        this.currentPage = page;
        PDFViewerApplication.page = page;
      }
      break;
    default:
      console.log('Unsupported control message', message);
      return;
  }
};

Annotator.prototype.exportSVG = function() {
  return new Promise(function(resolve, reject) {
    var result = [];
    var pages = [];

    var continueExport = function() {
      // Remove trailing empty pages.
      while (result.length && !result[result.length-1]) {
        result.pop();
      }

      // No annotations yet?
      if (!result.length) {
        return resolve(result);
      }

      // Need to keep a last empty page to avoid duplicating the last overlay.
      if (result.length < this.pageCount) {
        var page = pages[result.length + 1];
        if (page) {
          var svg = page.exportSVG();
          result.push(svg);
        }
      }

      return resolve(result);
    }.bind(this);

    var remaining = this.pageCount;
    for (var i = 1; i <= this.pageCount; i++) {
      this.getPage(i).then(function(pagenum, page_annotator) {
        pages[pagenum] = page_annotator;
        var svg = null;
        if (page_annotator) {
          svg = page_annotator.exportSVG();
        }
        result[pagenum - 1] = svg;
        remaining -= 1;
        if (remaining === 0) {
          continueExport();
        }
      }.bind(this, i));
    }
  }.bind(this));
};

Annotator.prototype.exportTextAnnotations = function() {
  var list = [];
  each(this.annotators, function(page_annotator) {
    each(page_annotator.textAnnotations, function(annotation) {
      var scaleX = page_annotator.pagewidth / (annotation.circle.project.view.bounds.width * CSS_UNITS);
      var scaleY = page_annotator.pageheight / (annotation.circle.project.view.bounds.height * CSS_UNITS);
      var color = annotation.color;
      if (color.length === 9 && color[0] === '#') {
        // PDF annotations don't support transparency.
        color = color.substr(0, 7);
      }
      var taObj = {
        "page": page_annotator.pagenum - 1,
        "x": annotation.circle.position.x * scaleX,
        "y": annotation.circle.position.y * scaleY,
        "author": annotation.author,
        "modified": annotation.modified,
        "text": annotation.content,
        "color": color,
      };
      list.push(taObj);
    });
  });
  return list;
};

Annotator.prototype.downloadPdf = function() {
  this.exportSVG().then(function(svg) {
    if (!svg.length) {
      // No items drawn yet, download source PDF.
      console.log("Download source PDF");
      return;
    }

    var data = {
      "svg": svg,
      "token": this.token,
      "text": this.exportTextAnnotations(),
    };

    // Download code from https://stackoverflow.com/a/23797348
    var xhr = new XMLHttpRequest();
    var base_url = this.socketurl;
    if (base_url[base_url.length-1] !== '/') {
      base_url += '/';
    }
    xhr.open('POST', base_url + "download/" + this.id, true);
    xhr.responseType = 'arraybuffer';
    xhr.onload = function() {
      $("#downloadInProgress").hide();
      if (this.status !== 200) {
        $("#downloadFailed").show();
        setTimeout(function() {
          $("#downloadFailed").hide();
        }, 4000);
        return;
      }

      var filename = "";
      var disposition = xhr.getResponseHeader('Content-Disposition');
      if (disposition && disposition.indexOf('attachment') !== -1) {
        var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        var matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) {
          filename = matches[1].replace(/['"]/g, '');
        }
      }
      var type = xhr.getResponseHeader('Content-Type');

      var blob = typeof File === 'function'
        ? new File([this.response], filename, { type: type })
        : new Blob([this.response], { type: type });
      if (typeof window.navigator.msSaveBlob !== 'undefined') {
        // IE workaround for "HTML7007: One or more blob URLs were revoked by
        // closing the blob for which they were created. These URLs will no
        // longer resolve as the data backing the URL has been freed."
        window.navigator.msSaveBlob(blob, filename);
      } else {
        var URL = window.URL || window.webkitURL;
        var downloadUrl = URL.createObjectURL(blob);

        if (filename) {
          // use HTML5 a[download] attribute to specify filename
          var a = document.createElement("a");
          // safari doesn't support this yet
          if (typeof a.download === 'undefined') {
            window.location = downloadUrl;
          } else {
            a.href = downloadUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
          }
        } else {
          window.location = downloadUrl;
        }

        setTimeout(function() {
          URL.revokeObjectURL(downloadUrl);
        }, 100); // cleanup
      }
    };
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.send(JSON.stringify(data));
    $("#downloadInProgress").show();
  }.bind(this));
};

$(document).ready(function() {
  var fileid = document.getElementsByTagName('head')[0].getAttribute('data-fileid');
  var userid = document.getElementsByTagName('head')[0].getAttribute('data-userid');
  var displayname = document.getElementsByTagName('head')[0].getAttribute('data-displayname');
  var socketurl = document.getElementsByTagName('head')[0].getAttribute('data-socketurl');
  var token = document.getElementsByTagName('head')[0].getAttribute('data-token');
  var permissions = parseInt(document.getElementsByTagName('head')[0].getAttribute('data-permissions'), 10);
  if (!fileid || !socketurl || !token) {
    window.location.href = '/';
    return;
  }

  var can_update = ((permissions & PERMISSION_UPDATE) === PERMISSION_UPDATE);

  var annotator = new Annotator(socketurl, fileid, userid, displayname, token);

  PDFViewerApplication.initializedPromise.then(function() {
    var eventBus = PDFViewerApplication.eventBus;
    eventBus.on('pagesloaded', function(event) {
      annotator.documentLoaded(PDFViewerApplication.pdfViewer.pdfDocument);
      if (!can_update) {
        $('#viewer .page[data-page-number!=1]').addClass('hiddenPage');
      }
    });

    eventBus.on('pagerendered', function(event) {
      var pagenum = event.pageNumber;
      if (!pagenum) {
        console.log("Rendered event without page number", event);
        return;
      }

      var container = $("#viewer .page[data-page-number='" + pagenum + "']");
      if (!container.length) {
        console.log("Could not get page container", event);
        return;
      }

      PDFViewerApplication.pdfViewer.pdfDocument.getPage(pagenum).then(function(page) {
        annotator.getPage(pagenum, page, container).then(function(page_annotator) {
          page_annotator.update(PDFViewerApplication.pdfViewer.currentScale);
        });
      });
    });

    eventBus.on('pagechanging', function(event) {
      var pagenum = event.pageNumber;
      if (can_update) {
        annotator.switchPage(pagenum);
      } else {
        $('#viewer .page').addClass('hiddenPage');
        var page = $('#viewer .page[data-page-number=' + pagenum +']');
        page.removeClass('hiddenPage');
        // Need to redraw currently visible page to fix any layout issues.
        setTimeout(function() {
          PDFViewerApplication.pdfViewer.update();
        }, 0);
      }
    });
  });

  $(".modeButton").click(function(event) {
    var button = $(event.target);
    var mode = button.data("mode") || null;
    if (mode === "color") {
      annotator.toggleSettings();
    } else {
      annotator.setDrawMode(mode);
    }
  });

  var $btnDrawMode = $(".toolbarButton.drawMode");
  $btnDrawMode.click(function(event) {
    $('#drawMenuToolbar').toggleClass('hidden');
  });
  $('#drawMenuToolbar .toolbarButton').each(function(_, elem) {
    var $elem = $(elem);
    $elem.click(function() {
      var mode = $(this).data('mode');
      // TODO(leon): This is an ugly hack to determine the previous mode
      $btnDrawMode.get(0).classList.forEach(function(c) {
        if (c !== 'drawMode' && c.indexOf('Mode') !== -1) {
          $btnDrawMode.removeClass(c);
        }
      });
      $btnDrawMode.addClass(mode + 'Mode');
      $('#drawMenuToolbar').addClass('hidden');
    });
  });

  $("#downloadPdf").click(function(event) {
    annotator.downloadPdf();
  });

  $("#secondaryToolbarClose").click(function() {
    history.back();
  });

  if (!can_update) {
    // User may not modify the file.
    $('#outerContainer').addClass('readonly');
    annotator.setDrawMode(null);
  }

  if (history.length <= 1) {
    // Annotation was opened in a new window. Closing windows through JS is not
    // possible - that's why we simply hide the button to not confuse users.
    $("#secondaryToolbarClose").hide();
  }
});

function setupPdfJs() {
  console.log("Loaded pdf.js", pdfjsLib.version, pdfjsLib.build);
  PDFViewerApplicationOptions.set("sidebarViewOnLoad", 0);
  PDFViewerApplicationOptions.set("showPreviousViewOnLoad", false);
  PDFViewerApplicationOptions.set("disablePageMode", true);
  PDFViewerApplicationOptions.set("isEvalSupported", false);
  PDFViewerApplicationOptions.set("cMapUrl", document.getElementsByTagName('head')[0].getAttribute('data-cmapurl'));
  PDFViewerApplicationOptions.set("workerSrc", document.getElementsByTagName('head')[0].getAttribute('data-workersrc'));
}

if (document.readyState === 'interactive' || document.readyState === 'complete') {
  setupPdfJs();
} else {
  document.addEventListener('DOMContentLoaded', setupPdfJs, true);
}
})();