Skip to content
Snippets Groups Projects
NumericalAnswerDropDownCompontent.js 6.12 KiB
Newer Older
chris's avatar
chris committed
/* eslint-disable react/prop-types */
import React, {
  useMemo,
  useContext,
  useState,
  useEffect,
  useRef,
  createRef,
} from 'react';
import styled from 'styled-components';
chris's avatar
chris committed
import {
  DocumentHelpers,
  WaxContext,
  Icon,
  useOnClickOutside,
} from 'wax-prosemirror-core';
chris's avatar
chris committed

const Wrapper = styled.div`
  opacity: ${props => (props.disabled ? '0.4' : '1')};
chris's avatar
chris committed
  z-index: 999;
chris's avatar
chris committed
`;

const DropDownButton = styled.button`
  background: #fff;
chris's avatar
chris committed
  border: 1px solid #f4f4f4;
chris's avatar
chris committed
  color: #000;
  cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
  display: flex;
  position: relative;
chris's avatar
chris committed
  top: 2px;
  left: 3px;
  width: 235px;
chris's avatar
chris committed
  height: 26px;
chris's avatar
chris committed

  span {
    position: relative;
chris's avatar
chris committed
    top: 4px;
chris's avatar
chris committed
  }
`;

chris's avatar
chris committed
const DropDownMenu = styled.div`
  visibility: ${props => (props.isOpen ? 'visible' : 'hidden')};
  background: #fff;
  display: flex;
  flex-direction: column;
  border: 1px solid #ddd;
  border-radius: 0.25rem;
  box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 10%);
  margin: 2px auto auto;
  position: absolute;
  width: 235px;
chris's avatar
chris committed
  max-height: 150px;
  overflow-y: auto;
  z-index: 2;

  span {
    cursor: pointer;
chris's avatar
chris committed
    border-bottom: 1px solid #f4f4f4;
chris's avatar
chris committed
    font-size: 11px;
    padding: 8px 10px;
  }

chris's avatar
chris committed
  span:focus,
  span:hover {
chris's avatar
chris committed
    background: #f2f9fc;
    outline: 2px solid #f2f9fc;
  }
`;

const StyledIcon = styled(Icon)`
  height: 18px;
  width: 18px;
  margin-left: auto;
  position: relative;
chris's avatar
chris committed
  top: 1px;
chris's avatar
chris committed
`;

const NumericalAnswerDropDownCompontent = ({ node }) => {
chris's avatar
chris committed
  const dropDownOptions = [
    {
      label: 'Exact answer with margin of error',
      value: 'exactAnswer',
    },
    {
      label: 'Answer within a range',
      value: 'rangeAnswer',
    },
    {
      label: 'Precise answer',
      value: 'preciseAnswer',
    },
  ];

chris's avatar
chris committed
  const context = useContext(WaxContext);
  const {
    activeView,
    pmViews: { main },
    setOption,
    options,
chris's avatar
chris committed
  } = context;

chris's avatar
chris committed
  const itemRefs = useRef([]);
  const wrapperRef = useRef();
  const [isOpen, setIsOpen] = useState(false);
  useOnClickOutside(wrapperRef, () => setIsOpen(false));

chris's avatar
chris committed
  const [label, setLabel] = useState('Select Type');
chris's avatar
chris committed

chris's avatar
chris committed
  const isEditable = main.props.editable(editable => {
    return editable;
  });

  useEffect(() => {
    setLabel('Select Type');
    setOption({
      [node.attrs.id]: { numericalAnswer: node.attrs.answerType },
    });

chris's avatar
chris committed
    dropDownOptions.forEach(option => {
      if (options[node.attrs.id]?.numericalAnswer === option.value) {
        setLabel(option.label);
      }
chris's avatar
chris committed
    });
chris's avatar
chris committed

  const isDisabled = !isEditable;
  // if (activeView.props?.type !== 'NumericalAnswer') isDisabled = true;
chris's avatar
chris committed

chris's avatar
chris committed
  useEffect(() => {
    if (isDisabled) setIsOpen(false);
  }, [isDisabled]);

chris's avatar
chris committed
  const openCloseMenu = () => {
    if (!isDisabled) setIsOpen(!isOpen);
    if (isOpen)
      setTimeout(() => {
        activeView.focus();
      });
  };

  const onKeyDown = (e, index) => {
    e.preventDefault();
    // arrow down
    if (e.keyCode === 40) {
      if (index === itemRefs.current.length - 1) {
        itemRefs.current[0].current.focus();
      } else {
        itemRefs.current[index + 1].current.focus();
      }
    }

    // arrow up
    if (e.keyCode === 38) {
      if (index === 0) {
        itemRefs.current[itemRefs.current.length - 1].current.focus();
      } else {
        itemRefs.current[index - 1].current.focus();
      }
    }

    // enter
    if (e.keyCode === 13) {
      itemRefs.current[index].current.click();
    }

    // ESC
    if (e.keyCode === 27) {
      setIsOpen(false);
    }
  };

chris's avatar
chris committed
  const SaveTypeToNode = option => {
    const allNodes = getNodes(context.pmViews.main);
    allNodes.forEach(singleNode => {
      if (singleNode.node.attrs.id === node.attrs.id) {
chris's avatar
chris committed
        context.pmViews.main.dispatch(
          context.pmViews.main.state.tr.setNodeMarkup(
            singleNode.pos,
            undefined,
            {
              ...singleNode.node.attrs,
              answerType: option,
              answersExact: [],
              answersRange: [],
              answersPrecise: [],
            },
          ),
        );
      }
    });
  };

chris's avatar
chris committed
  const onChange = option => {
    context.setOption({ [node.attrs.id]: { numericalAnswer: option.value } });
    setLabel(option.label);
chris's avatar
chris committed
    openCloseMenu();
chris's avatar
chris committed
    SaveTypeToNode(option.value);
    activeView.focus();
chris's avatar
chris committed
  };

  const NumericalAnswerDropDown = useMemo(
    () => (
      <Wrapper disabled={isDisabled} ref={wrapperRef}>
        <DropDownButton
chris's avatar
chris committed
          aria-controls="numerical-answer-list"
chris's avatar
chris committed
          aria-expanded={isOpen}
          aria-haspopup
          disabled={isDisabled}
          onKeyDown={e => {
            if (e.keyCode === 40) {
              itemRefs.current[0].current.focus();
            }
            if (e.keyCode === 27) {
              setIsOpen(false);
            }
            if (e.keyCode === 13 || e.keyCode === 32) {
              setIsOpen(true);
            }
          }}
          onMouseDown={openCloseMenu}
          type="button"
        >
          <span>{label}</span> <StyledIcon name="expand" />
        </DropDownButton>
        <DropDownMenu
          aria-label="Choose an item type"
chris's avatar
chris committed
          id="numerical-list"
chris's avatar
chris committed
          isOpen={isOpen}
          role="menu"
        >
          {dropDownOptions.map((option, index) => {
            itemRefs.current[index] = itemRefs.current[index] || createRef();
            return (
              <span
                key={option.value}
                onClick={() => onChange(option)}
                onKeyDown={e => onKeyDown(e, index)}
                ref={itemRefs.current[index]}
                role="menuitem"
                tabIndex="-1"
              >
                {option.label}
              </span>
            );
          })}
        </DropDownMenu>
      </Wrapper>
    ),
    [isDisabled, isOpen, label],
  );

  return NumericalAnswerDropDown;
chris's avatar
chris committed
};

chris's avatar
chris committed
const getNodes = view => {
  const allNodes = DocumentHelpers.findBlockNodes(view.state.doc);
  const numericalAnswerpContainerNodes = [];
  allNodes.forEach(node => {
    if (node.node.type.name === 'numerical_answer_container') {
      numericalAnswerpContainerNodes.push(node);
    }
  });
  return numericalAnswerpContainerNodes;
};

chris's avatar
chris committed
export default NumericalAnswerDropDownCompontent;