diff --git a/src/fragmentarium/ui/LemmaSearchForm.tsx b/src/fragmentarium/ui/LemmaSearchForm.tsx index 9cd1720e4..6ef5b088b 100644 --- a/src/fragmentarium/ui/LemmaSearchForm.tsx +++ b/src/fragmentarium/ui/LemmaSearchForm.tsx @@ -26,11 +26,13 @@ export const LemmaSearchForm = withData< { wordService: WordService onChange: (name: string) => (name: string) => void + isDisabled?: boolean + placeholder?: string }, { lemmas: string }, LemmaOption[] >( - ({ data, wordService, onChange }) => { + ({ data, wordService, onChange, isDisabled, placeholder }) => { return ( lemma.value).join('+')) }} query={data} + isDisabled={isDisabled} + placeholder={placeholder} /> ) }, diff --git a/src/fragmentarium/ui/fragment/CuneiformFragmentEditor.tsx b/src/fragmentarium/ui/fragment/CuneiformFragmentEditor.tsx index f16bf2c23..e221e3ebc 100644 --- a/src/fragmentarium/ui/fragment/CuneiformFragmentEditor.tsx +++ b/src/fragmentarium/ui/fragment/CuneiformFragmentEditor.tsx @@ -20,6 +20,7 @@ import { FindspotService } from 'fragmentarium/application/FindspotService' import { Session } from 'auth/Session' import ColophonEditor from 'fragmentarium/ui/fragment/ColophonEditor' import { Colophon } from 'fragmentarium/domain/Colophon' +import TabularAnnotationTool from './TabularAnnotationTool' const ContentSection: FunctionComponent = ({ children, @@ -47,6 +48,7 @@ type TabName = | 'references' | 'archaeology' | 'colophon' + | 'annotation' const tabNames: TabName[] = [ 'display', @@ -55,6 +57,7 @@ const tabNames: TabName[] = [ 'references', 'archaeology', 'colophon', + 'annotation', ] function EditorTab({ @@ -92,6 +95,7 @@ function TabContentsMatcher({ references: () => ReferencesContents(props), archaeology: () => ArchaeologyContents(props), colophon: () => ColophonContents(props), + annotation: () => AnnotationContents(props), }[name]() } @@ -127,7 +131,9 @@ export const EditorTabs: FunctionComponent = ({ } + +function AnnotationContents(props: TabsProps): JSX.Element { + const updateFragmentAnnotation = async (fragment: Fragment) => { + console.log('Saved fragment!') + } + return ( +
+ +
+ ) +} diff --git a/src/fragmentarium/ui/fragment/TabularAnnotationTool.tsx b/src/fragmentarium/ui/fragment/TabularAnnotationTool.tsx new file mode 100644 index 000000000..35f920b9d --- /dev/null +++ b/src/fragmentarium/ui/fragment/TabularAnnotationTool.tsx @@ -0,0 +1,124 @@ +import { Fragment } from 'fragmentarium/domain/fragment' +import React, { Component } from 'react' +import { isTextLine } from 'transliteration/domain/type-guards' +// import DisplayToken from 'transliteration/ui/DisplayToken' +import { Token } from 'transliteration/domain/token' +import _ from 'lodash' +import { TextLine } from 'transliteration/domain/text-line' +import { Table } from 'react-bootstrap' +import lineNumberToString from 'transliteration/domain/lineNumberToString' +import './TokenAnnotationTool.sass' +import DisplayToken from 'transliteration/ui/DisplayToken' +import { LineNumber, LineNumberRange } from 'transliteration/domain/line-number' +import { LemmaSearchForm } from '../LemmaSearchForm' +import WordService from 'dictionary/application/WordService' + +type Props = { + fragment: Fragment + wordService: WordService + onSave(fragment: Fragment): void +} + +type AnnotationRow = { + lineNumber: LineNumber | LineNumberRange + lineIndex: number + token: Token + newUniqueLemma: string[] + tokenIndex: number + uniqueId: string +} + +type AnnotationTable = AnnotationRow[] + +export default class TokenAnnotationTool extends Component { + private annotationTable: AnnotationTable + fragment: Fragment + + constructor(props: Props) { + super(props) + this.fragment = props.fragment + this.annotationTable = this.createAnnotationTable() + } + + createAnnotationTable(): AnnotationTable { + const lines = this.props.fragment.text.allLines + + return lines + .map((line, lineIndex) => ({ line, lineIndex })) + .filter((indexedLine) => isTextLine(indexedLine.line)) + .flatMap((indexedLine) => { + const line = indexedLine.line as TextLine + return line.content.map((token, tokenIndex) => ({ + lineNumber: line.lineNumber, + token, + tokenIndex, + uniqueId: _.uniqueId(), + lineIndex: indexedLine.lineIndex, + newUniqueLemma: [], + })) + }) + } + + LemmaEditor({ + row, + wordService, + }: { + row: AnnotationRow + wordService: WordService + }): JSX.Element { + const lemmas = row.token.uniqueLemma || [] + return !row.token.lemmatizable ? ( + <> + ) : ( + () => console.log('something')} + placeholder="Add lemma..." + /> + ) + } + + render(): JSX.Element { + let lastLineNumber = '' + return ( + + + + + + + + + + {this.annotationTable.map((row, index) => { + const lineNumber = lineNumberToString(row.lineNumber) + const displayRow = ( + + + + + + ) + lastLineNumber = lineNumber + return displayRow + })} + +
LineTokenLemma
+ {lineNumber !== lastLineNumber && + `(${lineNumberToString(row.lineNumber)})`} + + + + +
+ ) + } +} diff --git a/src/fragmentarium/ui/fragment/TokenAnnotationTool.sass b/src/fragmentarium/ui/fragment/TokenAnnotationTool.sass new file mode 100644 index 000000000..dfe336386 --- /dev/null +++ b/src/fragmentarium/ui/fragment/TokenAnnotationTool.sass @@ -0,0 +1,33 @@ +.annotation-tool + + &__table-annotator + width: unset + tbody + td:last-child + min-width: 10em + + &__wrapper + max-height: 50% + overflow-y: auto + td + padding: 0 + border-top: 0 + tr.line-separator + height: 1em + border-top: 1px solid grey +.annotation-line + &__source + &__annotation-layer + font-size: .7em + .markable-token + white-space: nowrap + // border: 1px dashed orange + margin-right: 1em + + &__checkbox-column + visibility: hidden + &:hover + visibility: visible !important + &__lemma-column + min-width: 10em + // white-space: nowrap diff --git a/src/fragmentarium/ui/fragment/TokenAnnotationTool.tsx b/src/fragmentarium/ui/fragment/TokenAnnotationTool.tsx new file mode 100644 index 000000000..b6e2c173f --- /dev/null +++ b/src/fragmentarium/ui/fragment/TokenAnnotationTool.tsx @@ -0,0 +1,90 @@ +import React, { Component } from 'react' +import { Fragment } from 'fragmentarium/domain/fragment' +import { AbstractLine } from 'transliteration/domain/abstract-line' +import { isEmptyLine, isTextLine } from 'transliteration/domain/type-guards' +import DisplayControlLine from 'transliteration/ui/DisplayControlLine' +import { TextLine } from 'transliteration/domain/text-line' +import { lineComponents } from 'transliteration/ui/TransliterationLines' +import { AnnotationLine } from 'transliteration/ui/annotation-line-tokens' +import './TokenAnnotationTool.sass' +import { Table } from 'react-bootstrap' +import FragmentService from 'fragmentarium/application/FragmentService' +import WordService from 'dictionary/application/WordService' + +type Props = { + fragment: Fragment + onSave(fragment: Fragment): void + fragmentService: FragmentService + wordService: WordService +} + +export default class TokenAnnotationTool extends Component { + readonly fragment: Fragment + + constructor(props: Props) { + super(props) + this.fragment = props.fragment + } + + displayMarkableLine({ + line, + lineIndex, + fragmentService, + wordService, + }: { + line: TextLine + lineIndex: number + fragmentService: FragmentService + wordService: WordService + }): JSX.Element { + return ( + + ) + } + + render(): JSX.Element { + const text = this.fragment.text + + return ( + + + {text.allLines + .filter((line) => !isEmptyLine(line)) + .map((line: AbstractLine, index) => { + const LineComponent = + lineComponents.get(line.type) || DisplayControlLine + + return ( + + {isTextLine(line) ? ( + <> + + + + ) : ( + + + + )} + + ) + })} + +
+ ) + } +} diff --git a/src/fragmentarium/ui/lemmatization/LemmaSelectionForm.tsx b/src/fragmentarium/ui/lemmatization/LemmaSelectionForm.tsx index 0794270bf..8ad36c9d7 100644 --- a/src/fragmentarium/ui/lemmatization/LemmaSelectionForm.tsx +++ b/src/fragmentarium/ui/lemmatization/LemmaSelectionForm.tsx @@ -38,6 +38,8 @@ type Props = { query?: readonly LemmaOption[] onChange: (query: readonly LemmaOption[]) => void wordService: { searchLemma(query: string): Promise } + placeholder?: string + isDisabled?: boolean } type State = { query: ValueType @@ -97,11 +99,11 @@ class LemmaSelectionForm extends Component { } } - Select = ({ label }: { label: string }): JSX.Element => { + Select = (): JSX.Element => { return ( { isMulti={true} components={{ Option, MultiValueLabel }} isOptionSelected={() => false} + isDisabled={this.props.isDisabled} /> ) } render(): JSX.Element { - return + return } } diff --git a/src/transliteration/domain/columns.ts b/src/transliteration/domain/columns.ts index 263811ddc..eb95bf123 100644 --- a/src/transliteration/domain/columns.ts +++ b/src/transliteration/domain/columns.ts @@ -44,6 +44,7 @@ export function lineAccFromColumns({ showIpa = false, phoneticProps, highlightLemmas = [], + isInPopover, }: { columns: readonly TextLineColumn[] isInLineGroup?: boolean @@ -51,6 +52,7 @@ export function lineAccFromColumns({ showIpa?: boolean phoneticProps?: PhoneticProps highlightLemmas: readonly string[] + isInPopover?: boolean }): LineAccumulator { return columns.reduce((acc: LineAccumulator, column) => { acc.addColumn(column.span) @@ -59,20 +61,18 @@ export function lineAccFromColumns({ acc.addColumnToken( token, index, - isInLineGroup, - showMeter, - showIpa, updatePhoneticPropsContext(column.content, index, phoneticProps), _.isEmpty(_.intersection(token.uniqueLemma, highlightLemmas)) ? [] - : ['highlight'] + : ['highlight'], + isInPopover ) return acc }, acc ) return acc - }, new LineAccumulator()) + }, new LineAccumulator(isInLineGroup, showMeter, showIpa)) } export function numberOfColumns(columns: readonly TextLineColumn[]): number { diff --git a/src/transliteration/ui/LineAccumulator.tsx b/src/transliteration/ui/LineAccumulator.tsx index 10a16b6ec..1e983fe28 100644 --- a/src/transliteration/ui/LineAccumulator.tsx +++ b/src/transliteration/ui/LineAccumulator.tsx @@ -30,34 +30,45 @@ function WordSeparator({ ) } -function isCloseEnclosure(token: Token): boolean { +export function isCloseEnclosure(token: Token): boolean { return isEnclosure(token) && ['CENTER', 'RIGHT'].includes(token.side) } -function isOpenEnclosure(token: Token): boolean { +export function isOpenEnclosure(token: Token): boolean { return isEnclosure(token) && ['CENTER', 'LEFT'].includes(token.side) } -function GlossWrapper({ children }: PropsWithChildren): JSX.Element { +export function GlossWrapper({ + children, +}: PropsWithChildren): JSX.Element { return ( {children} ) } -interface ColumnData { +export interface ColumnData { span: number | null content: React.ReactNode[] } export class LineAccumulator { - private columns: ColumnData[] = [] + readonly columns: ColumnData[] = [] private inGloss = false private language = 'AKKADIAN' private enclosureOpened = false private protocol: Protocol | null = null private isFirstWord = true + private isInLineGroup = false + private showMeter = false + private showIpa = false lemmas: string[] = [] + constructor(isInLineGroup?: boolean, showMeter?: boolean, showIpa?: boolean) { + this.isInLineGroup = isInLineGroup || false + this.showMeter = showMeter || false + this.showIpa = showIpa || false + } + getColumns(maxColumns: number): React.ReactNode[] { return this.columns.map((column: ColumnData, index: number) => ( @@ -87,11 +98,9 @@ export class LineAccumulator { pushToken( token: Token, index: number, - isInLineGroup = false, - showMeter = false, - showIpa = false, phoneticProps?: PhoneticProps, - bemModifiers: string[] = [] + bemModifiers: string[] = [], + isInPopover?: boolean ): void { if (_.isEmpty(this.columns)) { this.addColumn(1) @@ -100,7 +109,7 @@ export class LineAccumulator { this.pushSeparator() } - const DisplayTokenComponent = isInLineGroup + const DisplayTokenComponent = this.isInLineGroup ? DisplayLineGroupToken : DisplayToken @@ -110,9 +119,10 @@ export class LineAccumulator { token={token} bemModifiers={[...this.bemModifiers, ...bemModifiers]} Wrapper={this.inGloss && !isEnclosure(token) ? GlossWrapper : undefined} - showMeter={showMeter} - showIpa={showIpa} + showMeter={this.showMeter} + showIpa={this.showIpa} phoneticProps={phoneticProps} + isInPopover={isInPopover} /> ) this.enclosureOpened = isOpenEnclosure(token) @@ -139,11 +149,9 @@ export class LineAccumulator { addColumnToken( token: Token, index: number, - isInLineGroup?: boolean, - showMeter?: boolean, - showIpa?: boolean, phoneticProps?: PhoneticProps, - bemModifiers: string[] = [] + bemModifiers: string[] = [], + isInPopover?: boolean ): void { switch (token.type) { case 'LanguageShift': @@ -158,15 +166,7 @@ export class LineAccumulator { case 'Column': throw new Error('Unexpected column token.') default: - this.pushToken( - token, - index, - isInLineGroup, - showMeter, - showIpa, - phoneticProps, - bemModifiers - ) + this.pushToken(token, index, phoneticProps, bemModifiers, isInPopover) this.pushLemma(token.uniqueLemma) this.isFirstWord = false } diff --git a/src/transliteration/ui/MarkableToken.tsx b/src/transliteration/ui/MarkableToken.tsx new file mode 100644 index 000000000..76f82146f --- /dev/null +++ b/src/transliteration/ui/MarkableToken.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { Protocol, Token } from 'transliteration/domain/token' +import { isEnclosure } from 'transliteration/domain/type-guards' +import DisplayToken from './DisplayToken' +import { GlossWrapper } from './LineAccumulator' +import _ from 'lodash' + +export class MarkableToken { + readonly token: Token + readonly index: number + lemma: readonly string[] + readonly isInGloss: boolean + readonly protocol: Protocol | null = null + readonly language: string + + constructor( + token: Token, + index: number, + isInGloss: boolean, + protocol: Protocol | null, + language: string + ) { + this.token = token + this.index = index + this.isInGloss = isInGloss + this.protocol = protocol + this.language = language + this.lemma = token.uniqueLemma || [] + } + + get hasLemma(): boolean { + return !_.isEmpty(this.lemma) + } + + display(): JSX.Element { + return ( + <> + + + ) + } +} diff --git a/src/transliteration/ui/TransliterationLines.tsx b/src/transliteration/ui/TransliterationLines.tsx index 41397cbd4..371b2878c 100644 --- a/src/transliteration/ui/TransliterationLines.tsx +++ b/src/transliteration/ui/TransliterationLines.tsx @@ -17,7 +17,7 @@ import DisplayTranslationLine from './DisplayTranslationLine' import DisplayControlLine from './DisplayControlLine' import { DisplayParallelLine } from './parallel-line' -const lineComponents: ReadonlyMap< +export const lineComponents: ReadonlyMap< string, FunctionComponent > = new Map([ diff --git a/src/transliteration/ui/annotation-line-tokens.tsx b/src/transliteration/ui/annotation-line-tokens.tsx new file mode 100644 index 000000000..3ad7e2a1a --- /dev/null +++ b/src/transliteration/ui/annotation-line-tokens.tsx @@ -0,0 +1,187 @@ +import React, { Component } from 'react' +import { TextLineColumn } from 'transliteration/domain/columns' +import { TextLine } from 'transliteration/domain/text-line' +import { isLeftSide, Protocol } from 'transliteration/domain/token' +import { MarkableToken } from './MarkableToken' +import { Form } from 'react-bootstrap' +import lineNumberToString from 'transliteration/domain/lineNumberToString' +import AsyncSelect from 'react-select/async' +import FragmentService from 'fragmentarium/application/FragmentService' +import { + components, + MultiValueProps, + OptionProps, + SingleValueProps, +} from 'react-select' +import InlineMarkdown from 'common/InlineMarkdown' +import { LemmaOption } from 'fragmentarium/ui/lemmatization/LemmaSelectionForm' +import WordService from 'dictionary/application/WordService' + +type Props = { + markable: MarkableToken + fragmentService: FragmentService + wordService: WordService +} + +const Option = ( + props: OptionProps | OptionProps +): JSX.Element => ( + + + +) + +const MultiValueLabel = (props: MultiValueProps): JSX.Element => ( + + + +) + +const SingleValue = (props: SingleValueProps): JSX.Element => ( + + + +) + +type State = { + isComplex: boolean +} + +class LemmaEditForm extends Component { + markable: MarkableToken + lemmatizable: boolean + + constructor(props: Props) { + super(props) + this.markable = props.markable + this.lemmatizable = this.markable.token.lemmatizable || false + const isComplex = this.markable.hasLemma && this.markable.lemma.length === 1 + + this.state = { + isComplex: isComplex, + } + } + + loadOptions = ( + inputValue: string, + callback: (lemmas: LemmaOption[]) => void + ): void => { + this.props.wordService + .searchLemma(inputValue) + .then((words) => words.map((word) => new LemmaOption(word))) + .then(callback) + } + + render(): JSX.Element { + return ( + + ) + } +} + +function createTokenMarkables( + columns: readonly TextLineColumn[] +): MarkableToken[] { + let language = 'AKKADIAN' + let isInGloss = false + let protocol: Protocol | null = null + let markable: MarkableToken + + const markables: MarkableToken[] = [] + + columns.forEach((column) => + column.content.forEach((token, index) => { + switch (token.type) { + case 'LanguageShift': + language = token.language + break + case 'CommentaryProtocol': + protocol = token.value + break + case 'DocumentOrientedGloss': + isInGloss = isLeftSide(token) + break + case 'Column': + throw new Error('Unexpected column token.') + default: + markable = new MarkableToken( + token, + index, + isInGloss, + protocol, + language + ) + markables.push(markable) + } + }) + ) + return markables +} + +function DisplayMarkable({ + markable, +}: { + markable: MarkableToken +}): JSX.Element { + return ( + <> + {markable.display()} + + ) +} + +export function AnnotationLine({ + line, + lineIndex, + fragmentService, + wordService, +}: { + line: TextLine + lineIndex: number + fragmentService: FragmentService + wordService: WordService +}): JSX.Element { + const markables = createTokenMarkables(line.columns) + + const checkbox = ( + + + + ) + + return ( + <> + + {checkbox} + ({lineNumberToString(line.lineNumber)}) + + + {markables.map((markable, index) => { + return ( + + {checkbox} + + + + + + + + ) + })} + + ) +} diff --git a/src/transliteration/ui/line-tokens.tsx b/src/transliteration/ui/line-tokens.tsx index 2699c216d..9a5e37f18 100644 --- a/src/transliteration/ui/line-tokens.tsx +++ b/src/transliteration/ui/line-tokens.tsx @@ -28,9 +28,6 @@ export function LineTokens({ acc.addColumnToken( token, index, - false, - false, - false, {}, highlightTokens.includes(index) ? ['highlight'] : [] )