@recogito/recogito-client-core
Advanced tools
Comparing version 0.1.9 to 0.2.0
{ | ||
"name": "@recogito/recogito-client-core", | ||
"version": "0.1.9", | ||
"version": "0.2.0", | ||
"description": "Core functions, classes and components for RecogitoJS", | ||
@@ -34,2 +34,3 @@ "main": "src/index.js", | ||
"mocha": "^7.1.1", | ||
"node-forge": ">=0.10.0", | ||
"node-sass": "^4.14.1", | ||
@@ -36,0 +37,0 @@ "sass-loader": "^8.0.0", |
import React from 'preact/compat'; | ||
import { useState, useRef, useEffect } from 'preact/hooks'; | ||
import Environment from '../Environment'; | ||
import { getWidget, DEFAULT_WIDGETS } from './widgets'; | ||
import setPosition from './setPosition'; | ||
@@ -141,2 +142,6 @@ import i18n from '../i18n'; | ||
// Use default comment + tag widget unless host app overrides | ||
const widgets = props.config.widgets ? | ||
props.config.widgets.map(getWidget) : DEFAULT_WIDGETS; | ||
return ( | ||
@@ -146,16 +151,16 @@ <div ref={element} className="r6o-editor"> | ||
<div className="inner"> | ||
{React.Children.map(props.children, child => | ||
React.cloneElement(child, { | ||
...child.props, | ||
{widgets.map(widget => | ||
React.cloneElement(widget, { | ||
annotation : currentAnnotation, | ||
readOnly : props.readOnly, | ||
onAppendBody : onAppendBody, | ||
onUpdateBody : onUpdateBody, | ||
onRemoveBody : onRemoveBody, | ||
onSaveAndClose : onOk | ||
})) | ||
} | ||
config: props.config, | ||
onAppendBody, | ||
onUpdateBody, | ||
onRemoveBody, | ||
onSaveAndClose: onOk | ||
}) | ||
)} | ||
{ props.readOnly ? ( | ||
<div className="footer"> | ||
<div className="r6o-footer"> | ||
<button | ||
@@ -166,3 +171,3 @@ className="r6o-btn" | ||
) : ( | ||
<div className="footer"> | ||
<div className="r6o-footer"> | ||
<button | ||
@@ -169,0 +174,0 @@ className="r6o-btn outline" |
@@ -1,2 +0,2 @@ | ||
import React, { useState, useEffect, useRef } from 'react' | ||
import React, { useState, useRef } from 'react' | ||
import { useCombobox } from 'downshift' | ||
@@ -9,9 +9,5 @@ | ||
const [ inputItems, setInputItems ] = useState(props.vocabulary); | ||
useEffect(() => | ||
element.current?.querySelector('input')?.focus(), []); | ||
const onInputValueChange = ({ inputValue }) => { | ||
if (inputValue.length > 0) { | ||
// Set suggestions to prefix matches... | ||
const prefixMatches = props.vocabulary.filter(item => { | ||
@@ -68,2 +64,3 @@ return item.toLowerCase().startsWith(inputValue.toLowerCase()); | ||
{...getInputProps({ onKeyDown })} | ||
onChange={evt => props.onChange && props.onChange(evt)} | ||
placeholder={props.placeholder} | ||
@@ -70,0 +67,0 @@ defaultValue={props.initialValue} |
@@ -32,3 +32,3 @@ import React from 'preact/compat'; | ||
<div className="lastmodified"> | ||
<span className="lastmodified-by">{props.body.creator.name}</span> | ||
<span className="lastmodified-by">{props.body.creator.name || props.body.creator.id}</span> | ||
{ props.body.created && | ||
@@ -43,3 +43,3 @@ <span className="lastmodified-at"> | ||
<div className="r6o-widget comment"> | ||
<div className="value">{props.body.value}</div> | ||
<div className="r6o-readonly-comment">{props.body.value}</div> | ||
{ creatorInfo } | ||
@@ -46,0 +46,0 @@ </div> |
@@ -5,2 +5,3 @@ import React from 'preact/compat'; | ||
import i18n from '../../../i18n'; | ||
import Environment from '../../../Environment'; | ||
@@ -53,2 +54,27 @@ /** | ||
// A comment should be read-only if: | ||
// - the global read-only flag is set | ||
// - the current rule is 'MINE_ONLY' and the creator ID differs | ||
// The 'editable' config flag overrides the global setting, if any | ||
const isReadOnly = body => { | ||
if (props.editable === true) | ||
return false; | ||
if (props.editable === false) | ||
return true; | ||
if (props.editable === 'MINE_ONLY') { | ||
// The original creator of the body | ||
const creator = body.creator?.id; | ||
// The current user | ||
const me = Environment.user?.id; | ||
return me !== creator; | ||
} | ||
// Global setting as last possible option | ||
return props.readOnly; | ||
} | ||
return ( | ||
@@ -59,3 +85,3 @@ <> | ||
key={idx} | ||
readOnly={props.readOnly} | ||
readOnly={isReadOnly(body)} | ||
body={body} | ||
@@ -62,0 +88,0 @@ onUpdate={props.onUpdateBody} |
@@ -23,3 +23,3 @@ import React, { Component } from 'preact/compat'; | ||
if (ref && this.props.editable) | ||
ref.focus(); | ||
setTimeout(() => ref.focus(), 1); | ||
} | ||
@@ -26,0 +26,0 @@ |
@@ -8,19 +8,30 @@ import React from 'preact/compat'; | ||
const getDraftTag = existingDraft => | ||
existingDraft ? existingDraft : { | ||
type: 'TextualBody', value: '', purpose: 'tagging', draft: true | ||
}; | ||
/** The basic freetext tag control from original Recogito **/ | ||
const TagWidget = props => { | ||
// All tags (draft + non-draft) | ||
const all = props.annotation ? | ||
props.annotation.bodies.filter(b => b.type === 'TextualBody' && b.purpose === 'tagging') : []; | ||
// Last draft tag goes into the input field | ||
const draftTag = getDraftTag(all.slice().reverse().find(b => b.draft)); | ||
// All except draft tag | ||
const tags = all.filter(b => b != draftTag); | ||
const [ showDelete, setShowDelete ] = useState(false); | ||
// Every body with a 'tagging' purpose is considered a tag | ||
const tagBodies = props.annotation ? | ||
props.annotation.bodies.filter(b => b.purpose === 'tagging') : []; | ||
const toggle = tag => _ => { | ||
if (showDelete === tag) // Removes delete button | ||
setShowDelete(false); | ||
else | ||
else | ||
setShowDelete(tag); // Sets delete button on a different tag | ||
} | ||
const onDelete = tag => evt => { | ||
const onDelete = tag => evt => { | ||
evt.stopPropagation(); | ||
@@ -30,4 +41,19 @@ props.onRemoveBody(tag); | ||
const onDraftChange = evt => { | ||
const prev = draftTag.value.trim(); | ||
const updated = evt.target.value.trim(); | ||
if (prev.length === 0 && updated.length > 0) { | ||
props.onAppendBody({ ...draftTag, value: updated }); | ||
} else if (prev.length > 0 && updated.length === 0) { | ||
props.onRemoveBody(draftTag); | ||
} else { | ||
props.onUpdateBody(draftTag, { ...draftTag, value: updated }); | ||
} | ||
} | ||
const onSubmit = tag => { | ||
props.onAppendBody({ type: 'TextualBody', purpose: 'tagging', value: tag.trim() }); | ||
// Just 'undraft' the current draft tag | ||
const { draft, ...undrafted } = draftTag; | ||
props.onUpdateBody(draftTag, undrafted); | ||
} | ||
@@ -37,25 +63,29 @@ | ||
<div className="r6o-widget tag"> | ||
{ tagBodies.length > 0 && | ||
<ul className="r6o-taglist"> | ||
{ tagBodies.map(tag => | ||
<li key={tag.value} onClick={toggle(tag.value)}> | ||
<span className="label">{tag.value}</span> | ||
<div> | ||
{ tags.length > 0 && | ||
<ul className="r6o-taglist"> | ||
{ tags.map(tag => | ||
<li key={tag.value} onClick={toggle(tag.value)}> | ||
<span className="label">{tag.value}</span> | ||
{!props.readOnly && | ||
<CSSTransition in={showDelete === tag.value} timeout={200} classNames="delete"> | ||
<span className="delete-wrapper" onClick={onDelete(tag)}> | ||
<span className="delete"> | ||
<CloseIcon width={12} /> | ||
{!props.readOnly && | ||
<CSSTransition in={showDelete === tag.value} timeout={200} classNames="delete"> | ||
<span className="delete-wrapper" onClick={onDelete(tag)}> | ||
<span className="delete"> | ||
<CloseIcon width={12} /> | ||
</span> | ||
</span> | ||
</span> | ||
</CSSTransition> | ||
} | ||
</li> | ||
)} | ||
</ul> | ||
} | ||
</CSSTransition> | ||
} | ||
</li> | ||
)} | ||
</ul> | ||
} | ||
</div> | ||
{ !props.readOnly && | ||
<Autocomplete | ||
{!props.readOnly && | ||
<Autocomplete | ||
placeholder={i18n.t('Add tag...')} | ||
initialValue={draftTag.value} | ||
onChange={onDraftChange} | ||
onSubmit={onSubmit} | ||
@@ -69,2 +99,2 @@ vocabulary={props.vocabulary || []} /> | ||
export default TagWidget; | ||
export default TagWidget; |
@@ -98,5 +98,5 @@ const TEXT = 3; // HTML DOM node type for text nodes | ||
/** | ||
* Forces a new ID on the given annotation (or annotation with the given ID). | ||
* This method handles the ID update within the Highlighter ONLY. It's up to | ||
* the application to keep the RelationsLayer in sync! | ||
* Forces a new ID on the annotation with the given ID. This method handles | ||
* the ID update within the Highlighter ONLY. It's up to the application to | ||
* keep the RelationsLayer in sync! | ||
* | ||
@@ -103,0 +103,0 @@ * @returns the updated annotation for convenience |
@@ -0,1 +1,2 @@ | ||
export { default as Editor } from './editor/Editor'; | ||
export { default as Environment } from './Environment'; | ||
@@ -6,3 +7,2 @@ export { default as I18n } from './i18n'; | ||
export * from './editor'; | ||
export * from './highlighter'; | ||
@@ -9,0 +9,0 @@ export * from './relations'; |
import { trimRange, rangeToSelection, enableTouch, getExactOverlaps } from './SelectionUtils'; | ||
import { isInternetExplorer } from '../utils'; | ||
import EventEmitter from 'tiny-emitter'; | ||
@@ -8,3 +7,3 @@ | ||
const IS_INTERNET_EXPLORER = | ||
window?.navigator.userAgent.match(/(MSIE|Trident)/); | ||
navigator?.userAgent.match(/(MSIE|Trident)/); | ||
@@ -11,0 +10,0 @@ /** Tests whether maybeChildEl is contained in containerEl **/ |
@@ -7,6 +7,3 @@ import React, { Component } from 'preact/compat'; | ||
import RelationEditor from './relations/editor/RelationEditor'; | ||
import { addPolyfills } from './utils'; | ||
addPolyfills(); // For Microsoft Edge | ||
/** | ||
@@ -41,5 +38,5 @@ * Pulls the strings between the annotation highlight layer | ||
componentDidMount() { | ||
this.highlighter = new Highlighter(this.props.contentEl, this.props.formatter); | ||
this.highlighter = new Highlighter(this.props.contentEl, this.props.config.formatter); | ||
this.selectionHandler = new SelectionHandler(this.props.contentEl, this.highlighter, this.props.readOnly); | ||
this.selectionHandler = new SelectionHandler(this.props.contentEl, this.highlighter, this.props.config.readOnly); | ||
this.selectionHandler.on('select', this.handleSelect); | ||
@@ -274,4 +271,3 @@ | ||
selectedElement={this.state.selectedDOMElement} | ||
readOnly={this.props.readOnly} | ||
headless={this.state.headless} | ||
config={this.props.config} | ||
applyTemplate={this.state.applyTemplate} | ||
@@ -281,7 +277,3 @@ onAnnotationCreated={this.onCreateOrUpdateAnnotation('onAnnotationCreated')} | ||
onAnnotationDeleted={this.onDeleteAnnotation} | ||
onCancel={this.onCancelAnnotation}> | ||
{this.props.children} | ||
</Editor> | ||
onCancel={this.onCancelAnnotation} /> | ||
} | ||
@@ -288,0 +280,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1212113
68
2681
18