recogito / recogito-js

A JavaScript library for text annotation
BSD 3-Clause "New" or "Revised" License
349 stars 38 forks source link

Tags as buttons #78

Closed varna9000 closed 1 year ago

varna9000 commented 1 year ago

Hi, I saw this feature on a video in some of the showcase commercial projects. If we have fixed tags (i.e. vocabulary) could we expose them as buttons instead of input autocomplete the tags all the time? It would make sense if you have just few tags and it would speed up the annotation process.

rsimon commented 1 year ago

There is no commercial product :-) But you are probably referring to the Recogito platform, a system we built in a past project, and parts of which became the precursor to RecogitoJS and Annotorious.

In Annotorious, you would need to create your own Editor widget plugin to have a similar row of buttons. (The docs are for Annotorious. But plugins work in both systems.)

Luckily, such a three button plugin is about as easy as it gets as far as plugins are concerned. I have some existing code here if it helps: https://github.com/machines-reading-maps/mrm-recogito-ui/blob/main/src/widgets/ClassifyWidget.jsx https://github.com/machines-reading-maps/mrm-recogito-ui/blob/main/src/widgets/ClassifyWidget.scss

The only tricky bit about doing your own plugins (especially in React) is the build process. Things need to look different if you are using Recogito inside a React project or not, for example. Do shout out if you have problems with that, and I'll try to help.

krasimir commented 1 year ago

Hey,

sorry, I took a bit of a cowboy approach and implemented quite a hacky solution. I decided to drop it her in case someone else needs the same thing.

So, in order to replace the autocomplete with just buttons I believe we have to touch the node_modules/@recogito/recogito-client-core/src/editor/widgets/tag/TagWidget.jsx file. That's because we don't want to have a new widget but want to replace the default ones. I did the change myself directly into the node_modules folder and compiled a new version of the library. You can find the newly generated recogito.min.js file here https://github.com/krasimir/recogito-js/tree/main/dist

I changed the TagWidget's render method to return the following:

return (
  <div className="r6o-widget r6o-tag">
    <ul className="r6o-taglist">
      {
        props.vocabulary.map(word => {
          const styleButton = {
            border: 'none',
            padding: '0.5em 1em',
            fontSize: '0.9em',
            fontFamily: 'inherit',
            cursor: 'pointer',
            background: 'none',
            display: 'block',
            height: '100%'
          };
          const styleSelected = {
            background: '#4483c4',
            color: '#FFF'
          }
          const tag = tags.find(t => word === tagValue(t));
          if (tag) {
            return (
              <li key={tagValue(tag)} onClick={toggle(tag)}>
                <span className="r6o-label" style={styleSelected}>✔ {tagValue(tag)}</span>

                {!props.readOnly &&
                  <CSSTransition in={showDelete === tag} timeout={200} classNames="r6o-delete">
                    <span className="r6o-delete-wrapper" onClick={onDelete(tag)}>
                      <span className="r6o-delete">
                        <CloseIcon width={12} />
                      </span>
                    </span>
                  </CSSTransition>
                }
              </li>
            );
          }
          return (
            <li>
              <button key={word} style={styleButton} onClick={(e) => {
                e.preventDefault();
                onSubmit(word);
              }}>{word}</button>
            </li>
          );
        })
      }
    </ul>
  </div>
)

Or in other words, printing all the available tags and marking the ones that are selected.

Here's how it works:

https://user-images.githubusercontent.com/528677/210641510-c70252e7-61ba-4992-8ff9-8773230a6594.mp4

For the relations mode I had to touch src/relations/editor/RelationEditor.jsx and amend the render method to:

render() {
  const styleButton = {
    border: 'none',
    padding: '0.5em 1em',
    fontSize: '0.9em',
    fontFamily: 'inherit',
    cursor: 'pointer',
    background: 'none',
    display: 'block',
    height: '100%'
  };
  return(
    <div className="r6o-relation-editor" ref={this.element}>
      <div className="input-wrapper r6o-widget r6o-tag" style={{ marginRight: '34px' }}>
        <ul className="r6o-taglist">
          {
            (this.props.vocabulary || []).map(word => {
              const isSelected = getContent(this.props.relation) === word;
              const style = Object.assign(
                {},
                styleButton,
                isSelected ? { background: '#4483c4', color: '#FFF' } : null
              );
              return (
                <li>
                  <button key={word} style={style} onClick={(e) => {
                    e.preventDefault();
                    this.onSubmit(word);
                  }}>{word}</button>
                </li>
              );
            })
          }
        </ul>
      </div>

      <div className="buttons">
        <span 
          className="r6o-icon delete"
          onClick={this.onDelete}>
          <TrashIcon width={14} />
        </span>
      </div>
    </div>
  )
}