Repository: h2oai/wave
File: ui/src/text_annotator.tsx (TypeScript)

Code before:
      getMark = (text: S, idx: U, tag: S) => {
        const
          color = tagColorMap.get(tag)!,
          removeIconStyle = { visibility: shouldShowRemoveIcon(idx, tag) ? 'visible' : 'hidden' },
          isFirst = tokens[idx - 1]?.tag !== tag,
          isLast = tokens[idx + 1]?.tag !== tag
        return (
          <mark
            onMouseOver={onMarkHover(idx)}
            onMouseOut={onMarkMouseOut}
            className={clas(css.mark, isFirst ? css.firstMark : '', isLast ? css.lastMark : '')}
            style={{ backgroundColor: cssVar(color), color: getContrast(color) }}>
            {text}
            <Fluent.Icon iconName='CircleAdditionSolid' styles={{ root: removeIconStyle }} className={clas(css.removeIcon, 'wave-w6')} onMouseUp={removeAnnotation(idx)} />
            {/* HACK: Put color underlay under remove icon because its glyph is transparent and doesn't look good on tags. */}
            <span style={removeIconStyle as React.CSSProperties} className={css.iconUnderlay}></span>
          </mark>
        )
      },
      handleOnMouseUp = (ev: React.MouseEvent<HTMLSpanElement>) => {
        if (smart_selection) annotate()
        else if (timeoutRef.current) annotate(ev.currentTarget) // double-click
        else if (new Date().getTime() - mouseDownTimeRef.current > 250) annotate() // dragging (long click)
        else timeoutRef.current = setTimeout(() => annotate(), 250) // click
      },
      Token = ({ idx, tokenProps: { start, end, tag, text } }: { idx: number, tokenProps: TextAnnotatorItem & { start: number, end: number } }) =>
        <span
          data-key={idx}
          data-start={start}
          data-end={end}
          onMouseDown={() => { mouseDownTimeRef.current = new Date().getTime() }}
          onMouseUp={handleOnMouseUp}
          onMouseLeave={(ev: React.MouseEvent<HTMLSpanElement>) => {
            if ((ev.relatedTarget as any)?.nodeName !== 'SPAN') handleOnMouseUp(ev) // nodeName prop is not typed yet
          }}
        >
          {tag ? getMark(text, idx, tag) : text}
        </span>,
      text = React.useMemo(() => tokens.reduce((acc, token, idx) => {
        if (smart_selection || token.text === " ") acc.push(<Token key={idx} idx={idx} tokenProps={token} />)
        else if (!idx || tokens[idx - 1].text === " ") {
          const word = []
          for (let i = idx; i < tokens.length; i++) {
            word.push(<Token key={i} idx={i} tokenProps={tokens[i]} />)
            if (i === tokens.length - 1 || tokens[i + 1].text === " ") {
              // placing word broken into characters into span with display: 'inline-block' prevents it from wrapping in the EOL
              acc.push(<span key={`w${i}`} style={{ display: 'inline-block' }}>{word}</span>)
              break
            }
          }
        }

Review comment (refactor):
`TextAnnotatorItem & { ... }` is used in more places, extract a custom type instead of inlining it everywhere.

Code after:
      },
      getMark = (text: S, idx: U, tag: S) => {
        const color = tagColorMap.get(tag)
        // Handle invalid tags entered by user.
        if (!color) return text
        const
          removeIconStyle = { visibility: shouldShowRemoveIcon(idx, tag) ? 'visible' : 'hidden' },
          isFirst = tokens[idx - 1]?.tag !== tag,
          isLast = tokens[idx + 1]?.tag !== tag
        return (
          <mark
            onMouseOver={onMarkHover(idx)}
            onMouseOut={onMarkMouseOut}
            className={clas(css.mark, isFirst ? css.firstMark : '', isLast ? css.lastMark : '')}
            style={{ backgroundColor: cssVar(color), color: getContrast(color) }}>
            {text}
            <Fluent.Icon iconName='CircleAdditionSolid' styles={{ root: removeIconStyle }} className={clas(css.removeIcon, 'wave-w6')} onMouseUp={removeAnnotation(idx)} />
            {/* HACK: Put color underlay under remove icon because its glyph is transparent and doesn't look good on tags. */}
            <span style={removeIconStyle as React.CSSProperties} className={css.iconUnderlay}></span>
          </mark>
        )
      },
      handleMouseDown = (startElProps: TokenMouseEventProps) => () => startElPropsRef.current = startElProps,
      handleMouseUp = (endElProps: TokenMouseEventProps) => () => annotate(endElProps),
      handleMouseLeave = (endElProps: TokenMouseEventProps) => (ev: React.MouseEvent<HTMLSpanElement>) => {
        const nodeName = (ev.relatedTarget as any)?.nodeName
        if (startElPropsRef.current !== undefined && nodeName !== 'P' && nodeName !== 'MARK') annotate(endElProps) // nodeName prop is not typed yet
      }

    // eslint-disable-next-line react-hooks/exhaustive-deps
    React.useEffect(() => { wave.args[name] = items as unknown as Rec[] }, [])

    return (
      <div data-test={name} className={readonly ? css.readonly : ''}>
        <div className={clas('wave-s16 wave-w6', css.title)}>{title}</div>
        <AnnotatorTags tags={tags} activateTag={activateTag} activeTag={activeTag} />
        <Fluent.Toggle
          data-test='smart-selection'
          label='Smart selection'
          defaultChecked={smartSelection}
          onChange={() => setSmartSelection(prevSelection => !prevSelection)}
          onText="On"
          offText="Off"
          inlineLabel
        />
        <div className={clas(css.content, 'wave-s16 wave-t7 wave-w3')}>{
          tokens.map(({ start, end, text, tag }, idx) => {
            const activeColor = activeTag ? tagColorMap.get(activeTag) : undefined
            return (
              // Use paragraph instead of span to support text highlight.
              <p