commit 4bc995912c81ebe8ddf01a440fb51bb09d4f816e
Author: Lukáš Vlček <vlcek@net-inout.cz>
Date:   Sun Apr 17 15:24:42 2022 +0200

    Adding tag/subtag to selected text

diff --git a/webapp/api/api.ts b/webapp/api/api.ts
index 3478280..b26d5f3 100644
--- a/webapp/api/api.ts
+++ b/webapp/api/api.ts
@@ -76,6 +76,43 @@ export interface AnnotationInfo {
      */
     'tagInstances'?: Array<TagInstanceInfo> | null;
 }
+/**
+ * 
+ * @export
+ * @interface AnnotationInstanceAddRequest
+ */
+export interface AnnotationInstanceAddRequest {
+    /**
+     * 
+     * @type {number}
+     * @memberof AnnotationInstanceAddRequest
+     */
+    'position'?: number;
+    /**
+     * 
+     * @type {number}
+     * @memberof AnnotationInstanceAddRequest
+     */
+    'length'?: number;
+    /**
+     * 
+     * @type {ETagType}
+     * @memberof AnnotationInstanceAddRequest
+     */
+    'type'?: ETagType;
+    /**
+     * 
+     * @type {string}
+     * @memberof AnnotationInstanceAddRequest
+     */
+    'id'?: string;
+    /**
+     * 
+     * @type {string}
+     * @memberof AnnotationInstanceAddRequest
+     */
+    'instanceId'?: string | null;
+}
 /**
  * 
  * @export
@@ -321,6 +358,20 @@ export const EState = {
 export type EState = typeof EState[keyof typeof EState];
 
 
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const ETagType = {
+    Tag: 'TAG',
+    Subtag: 'SUBTAG'
+} as const;
+
+export type ETagType = typeof ETagType[keyof typeof ETagType];
+
+
 /**
  * 
  * @export
@@ -553,10 +604,10 @@ export interface TagInstanceInfo {
     'subTagId'?: string | null;
     /**
      * 
-     * @type {number}
+     * @type {string}
      * @memberof TagInstanceInfo
      */
-    'instance'?: number;
+    'instance'?: string;
     /**
      * 
      * @type {number}
@@ -710,6 +761,43 @@ export const AnnotationApiAxiosParamCreator = function (configuration?: Configur
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {string} annotationId 
+         * @param {AnnotationInstanceAddRequest} [annotationInstanceAddRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        annotationAnnotationIdPost: async (annotationId: string, annotationInstanceAddRequest?: AnnotationInstanceAddRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'annotationId' is not null or undefined
+            assertParamExists('annotationAnnotationIdPost', 'annotationId', annotationId)
+            const localVarPath = `/annotation/{annotationId}`
+                .replace(`{${"annotationId"}}`, encodeURIComponent(String(annotationId)));
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(annotationInstanceAddRequest, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {AnnotationsAddRequest} [annotationsAddRequest] 
@@ -763,6 +851,17 @@ export const AnnotationApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.annotationAnnotationIdGet(annotationId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {string} annotationId 
+         * @param {AnnotationInstanceAddRequest} [annotationInstanceAddRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async annotationAnnotationIdPost(annotationId: string, annotationInstanceAddRequest?: AnnotationInstanceAddRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.annotationAnnotationIdPost(annotationId, annotationInstanceAddRequest, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {AnnotationsAddRequest} [annotationsAddRequest] 
@@ -792,6 +891,16 @@ export const AnnotationApiFactory = function (configuration?: Configuration, bas
         annotationAnnotationIdGet(annotationId: string, options?: any): AxiosPromise<AnnotationInfo> {
             return localVarFp.annotationAnnotationIdGet(annotationId, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {string} annotationId 
+         * @param {AnnotationInstanceAddRequest} [annotationInstanceAddRequest] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        annotationAnnotationIdPost(annotationId: string, annotationInstanceAddRequest?: AnnotationInstanceAddRequest, options?: any): AxiosPromise<void> {
+            return localVarFp.annotationAnnotationIdPost(annotationId, annotationInstanceAddRequest, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {AnnotationsAddRequest} [annotationsAddRequest] 
@@ -822,6 +931,18 @@ export class AnnotationApi extends BaseAPI {
         return AnnotationApiFp(this.configuration).annotationAnnotationIdGet(annotationId, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {string} annotationId 
+     * @param {AnnotationInstanceAddRequest} [annotationInstanceAddRequest] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AnnotationApi
+     */
+    public annotationAnnotationIdPost(annotationId: string, annotationInstanceAddRequest?: AnnotationInstanceAddRequest, options?: AxiosRequestConfig) {
+        return AnnotationApiFp(this.configuration).annotationAnnotationIdPost(annotationId, annotationInstanceAddRequest, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {AnnotationsAddRequest} [annotationsAddRequest] 
diff --git a/webapp/components/annotation/AnnotationItem.tsx b/webapp/components/annotation/AnnotationItem.tsx
index 7dfef34..d67a4bc 100644
--- a/webapp/components/annotation/AnnotationItem.tsx
+++ b/webapp/components/annotation/AnnotationItem.tsx
@@ -19,6 +19,8 @@ import { TagInstanceInfo } from '../../api';
  * @returns The item that represents an annotation.
  */
 export function AnnotationItem(props: { tag: Tag }) {
+    const { markSelectedText } = useContext(AnnotationContext);
+
     /**
      * Should properties of this annotation be visible?
      */
@@ -88,7 +90,10 @@ export function AnnotationItem(props: { tag: Tag }) {
                 <Col sm="auto" className="d-flex align-items-center">
                     <TagOutlined />
                 </Col>
-                <Col className="d-flex align-items-center">{props.tag.name}</Col>
+                <Col className="d-flex align-items-center">
+                    {props.tag.tagName}
+                    {props.tag.subtagName ? ' (' + props.tag.subtagName + ')' : ''}
+                </Col>
                 <Col sm="auto">
                     <Button
                         type="text"
diff --git a/webapp/components/annotation/DocumentAnnotationView.tsx b/webapp/components/annotation/DocumentAnnotationView.tsx
index 8c2090e..f55fe05 100644
--- a/webapp/components/annotation/DocumentAnnotationView.tsx
+++ b/webapp/components/annotation/DocumentAnnotationView.tsx
@@ -15,66 +15,6 @@ export function DocumentAnnotationView() {
 
     return (
         <div>
-            <Button
-                onClick={() => {
-                    const selection = window.getSelection();
-                    if (!selection) {
-                        return;
-                    }
-                    if (!annotation?.tagStartPositions || !annotation.tagLengths) {
-                        console.log('start or lengths not found');
-                        return;
-                    }
-
-                    let startTag = selection.anchorNode;
-                    let endTag = selection.focusNode;
-
-                    if (!startTag || !endTag) {
-                        console.log('Selection not found');
-                        return;
-                    }
-
-                    if (startTag.nodeName.includes('#')) {
-                        startTag = startTag.parentNode;
-                    }
-                    if (endTag.nodeName.includes('#')) {
-                        endTag = endTag.parentNode;
-                    }
-                    if (!startTag || !endTag) {
-                        console.log('Selection element not found');
-                        return;
-                    }
-
-                    if (!(startTag instanceof Element)) {
-                        console.log('StartTag is not instance of Element');
-                        return;
-                    }
-                    if (!(endTag instanceof Element)) {
-                        console.log('EndTag is not instance of Element');
-                        return;
-                    }
-
-                    const startElement = startTag as Element;
-                    const endElement = endTag as Element;
-
-                    const startId: number =
-                        Number(startElement.getAttribute('aswi-tag-id')) ?? -1;
-                    const endId: number =
-                        Number(endElement.getAttribute('aswi-tag-id')) ?? -1;
-
-                    const startPosition =
-                        annotation.tagStartPositions[startId] +
-                        annotation.tagLengths[startId] +
-                        selection.anchorOffset;
-                    const endPosition =
-                        annotation.tagStartPositions[endId] +
-                        annotation.tagLengths[endId] +
-                        selection.focusOffset -
-                        1;
-                }}
-            >
-                Test
-            </Button>
             <div
                 dangerouslySetInnerHTML={{ __html: annotation.documentToRender ?? '' }}
             />
diff --git a/webapp/components/types/tag.tsx b/webapp/components/types/tag.tsx
index 62c23b8..d0a04aa 100644
--- a/webapp/components/types/tag.tsx
+++ b/webapp/components/types/tag.tsx
@@ -1,11 +1,17 @@
-import { TagInstanceInfo } from '../../api';
+import { SubTagInfo, TagInfo, TagInstanceInfo } from '../../api';
 
 /**
  * Special tag used in annotation panel.
  */
 export type Tag = {
-    name: string;
+    tagName: string;
+    subtagName: string | null;
+
     category: string;
     visible: boolean;
     occurrences: TagInstanceInfo[];
+
+    tagId: string;
+    subtagId: string | null;
+    instanceId: string;
 };
diff --git a/webapp/contexts/AnnotationContext.tsx b/webapp/contexts/AnnotationContext.tsx
index 74f28e4..502f1b6 100644
--- a/webapp/contexts/AnnotationContext.tsx
+++ b/webapp/contexts/AnnotationContext.tsx
@@ -1,7 +1,8 @@
 import React, { createContext, useEffect, useState } from 'react';
-import { AnnotationInfo, TagInstanceInfo } from '../api';
+import { AnnotationInfo, ETagType, SubTagInfo, TagInfo, TagInstanceInfo } from '../api';
 import { Tag } from '../components/types/tag';
 import { annotationController } from '../controllers';
+import { GetSelectionInfo } from '../utils/selectionUtils';
 
 /**
  * Interface of an annotation context provider.
@@ -53,6 +54,12 @@ interface IAnnotationContextProvider {
     annotation: AnnotationInfo | null;
     mappedTags: Tag[] | null;
     refreshAnnotation: () => void;
+
+    markSelectedText: (
+        tagId: string,
+        subtagId: string | null,
+        instanceID: string | null
+    ) => void;
 }
 
 /**
@@ -119,6 +126,10 @@ export const AnnotationContext = createContext<IAnnotationContextProvider>({
     refreshAnnotation: () => {
         return;
     },
+
+    markSelectedText: () => {
+        return;
+    },
 });
 
 /**
@@ -139,12 +150,49 @@ const AnnotationProvider = (props: {
 
     const [mappedTags, setMappedTags] = useState<Tag[] | null>(null);
 
+    async function markSelectedText(
+        tagId: string,
+        subtagId: string | null,
+        instanceId: string | null
+    ) {
+        if (!annotation) {
+            console.log('annotation not found');
+            return;
+        }
+
+        const selectionInfo = GetSelectionInfo(annotation);
+        if (!selectionInfo) {
+            console.log(
+                'not able to continue, selection processing not completed successfully'
+            );
+            return;
+        }
+
+        const id = subtagId ?? tagId;
+        const type: ETagType = subtagId == null ? ETagType.Tag : ETagType.Subtag;
+
+        const res = await annotationController.annotationAnnotationIdPost(
+            props.annotationId,
+            {
+                id: id,
+                instanceId,
+                type,
+                position: selectionInfo.startPositionOriginalDocument,
+                length: selectionInfo.selectionLengthOriginalDocument,
+            }
+        );
+        console.log('res');
+        console.log(res);
+
+        await refreshAnnotation();
+    }
+
     /**
      * Default implementation of addOccurrence method.
      * @param tag The tag with new occurrence.
      */
-    const addOccurrence = (tag: Tag) => {
-        //TODO: Implement method (should use objects from server API)
+    const addOccurrence = async (tag: Tag) => {
+        await markSelectedText(tag.tagId, tag.subtagId ?? null, tag.instanceId);
     };
 
     /**
@@ -182,17 +230,21 @@ const AnnotationProvider = (props: {
     };
 
     const remapAnnotations = (data: AnnotationInfo) => {
-        let map = new Map<number, Tag>();
+        let map = new Map<string, Tag>();
         data.tagInstances?.forEach((tagInstance) => {
-            if (map.has(tagInstance.instance ?? 0)) {
-                let tag = map.get(tagInstance.instance ?? 0);
+            if (map.has(tagInstance.instance ?? '-')) {
+                let tag = map.get(tagInstance.instance ?? '-');
                 tag!.occurrences = [...tag!.occurrences, tagInstance];
             } else {
-                map.set(tagInstance.position ?? 0, {
-                    name: tagInstance.tagName ?? '',
+                map.set(tagInstance.instance ?? '-', {
+                    tagName: tagInstance.tagName ?? '',
+                    subtagName: tagInstance.subTagName ?? null,
                     category: tagInstance.tagCategoryName ?? '',
                     visible: true,
                     occurrences: [tagInstance],
+                    tagId: tagInstance.tagId ?? '',
+                    instanceId: tagInstance.instance ?? '',
+                    subtagId: tagInstance.subTagId ?? null,
                 });
             }
         });
@@ -229,6 +281,7 @@ const AnnotationProvider = (props: {
                 refreshAnnotation,
                 annotation,
                 mappedTags,
+                markSelectedText,
             }}
         >
             {props.children}
diff --git a/webapp/contexts/TagCategoryContext.tsx b/webapp/contexts/TagCategoryContext.tsx
index 315149b..f2b4ab3 100644
--- a/webapp/contexts/TagCategoryContext.tsx
+++ b/webapp/contexts/TagCategoryContext.tsx
@@ -1,6 +1,9 @@
-import React, { createContext, useEffect, useState } from 'react';
+import React, { createContext, useContext, useEffect, useState } from 'react';
 import { SubTagInfo, TagCategoryInfo, TagInfo } from '../api';
 import { tagController } from '../controllers';
+import { LoggedUserContext } from './LoggedUserContext';
+import Annotation from '../pages/annotation/[id]';
+import { AnnotationContext } from './AnnotationContext';
 
 /**
  * Interface of a tag context provider.
@@ -78,6 +81,8 @@ export const TagCategoryContext = createContext<ITagCategoryContextProvider>({
  * @returns Prepared html of the provider.
  */
 const TagCategoryProvider = (props: { children: React.ReactNode }) => {
+    const { markSelectedText } = useContext(AnnotationContext);
+
     /**
      * Tags managed by the context.
      */
@@ -99,6 +104,13 @@ const TagCategoryProvider = (props: { children: React.ReactNode }) => {
     const selectTag = (tag: TagInfo, subTag: SubTagInfo | null) => {
         setSelectedTag(tag);
         setSelectedSubTag(subTag);
+
+        if (!tag.id) {
+            console.log('invalid selection');
+            return;
+        }
+
+        markSelectedText(tag.id, subTag?.id ?? null, null);
     };
 
     /**
diff --git a/webapp/pages/annotation/[id].tsx b/webapp/pages/annotation/[id].tsx
index dec1dfe..80afdcf 100644
--- a/webapp/pages/annotation/[id].tsx
+++ b/webapp/pages/annotation/[id].tsx
@@ -5,6 +5,8 @@ import { MainLayout } from '../../layouts/MainLayout';
 import 'antd/dist/antd.css';
 import styles from '/styles/Annotation.module.scss';
 import AnnotationProvider from '../../contexts/AnnotationContext';
+import TagCategoryProvider from '../../contexts/TagCategoryContext';
+
 import { useRouter } from 'next/router';
 
 /**
@@ -27,19 +29,21 @@ function Annotation() {
 
     return (
         <AnnotationProvider annotationId={annotationId}>
-            <MainLayout>
-                <div className={styles.layoutWrapper}>
-                    <div className={styles.tags}>
-                        <TagPanel />
-                    </div>
-                    <div className={styles.document}>
-                        <DocumentAnnotationView />
-                    </div>
-                    <div className={styles.annotations}>
-                        <AnnotationPanel />
+            <TagCategoryProvider>
+                <MainLayout>
+                    <div className={styles.layoutWrapper}>
+                        <div className={styles.tags}>
+                            <TagPanel />
+                        </div>
+                        <div className={styles.document}>
+                            <DocumentAnnotationView />
+                        </div>
+                        <div className={styles.annotations}>
+                            <AnnotationPanel />
+                        </div>
                     </div>
-                </div>
-            </MainLayout>
+                </MainLayout>
+            </TagCategoryProvider>
         </AnnotationProvider>
     );
 }
diff --git a/webapp/utils/selectionUtils.ts b/webapp/utils/selectionUtils.ts
new file mode 100644
index 0000000..8f7e990
--- /dev/null
+++ b/webapp/utils/selectionUtils.ts
@@ -0,0 +1,88 @@
+import { AnnotationInfo } from '../api';
+
+export interface SelectionInfo {
+    startElementId: number;
+    endElementId: number;
+    startPositionOriginalDocument: number;
+    endPositionOriginalDocument: number;
+    selectionLengthOriginalDocument: number;
+}
+export function GetSelectionInfo(annotation: AnnotationInfo): SelectionInfo | null {
+    const selection = window.getSelection();
+    if (!selection) {
+        return null;
+    }
+    if (!annotation?.tagStartPositions || !annotation.tagLengths) {
+        console.log('start or lengths not found');
+        return null;
+    }
+
+    let startTag = selection.anchorNode;
+    let endTag = selection.focusNode;
+
+    if (!startTag || !endTag) {
+        console.log('Selection not found');
+        return null;
+    }
+
+    if (startTag.nodeName.includes('#')) {
+        startTag = startTag.parentNode;
+    }
+    if (endTag.nodeName.includes('#')) {
+        endTag = endTag.parentNode;
+    }
+    if (!startTag || !endTag) {
+        console.log('Selection element not found');
+        return null;
+    }
+
+    if (!(startTag instanceof Element)) {
+        console.log('StartTag is not instance of Element');
+        return null;
+    }
+    if (!(endTag instanceof Element)) {
+        console.log('EndTag is not instance of Element');
+        return null;
+    }
+
+    let startElement = startTag as Element;
+    let endElement = endTag as Element;
+
+    let startId: number = Number(startElement.getAttribute('aswi-tag-id')) ?? -1;
+    let endId: number = Number(endElement.getAttribute('aswi-tag-id')) ?? -1;
+
+    let startPosition =
+        annotation.tagStartPositions[startId] +
+        annotation.tagLengths[startId] +
+        selection.anchorOffset;
+    let endPosition =
+        annotation.tagStartPositions[endId] +
+        annotation.tagLengths[endId] +
+        selection.focusOffset -
+        1;
+
+    // need to switch start and end elements (selection was in the opposite way then expected)
+    if (startPosition > endPosition) {
+        let temp = startPosition;
+        startPosition = endPosition;
+        endPosition = temp;
+
+        temp = startId;
+        startId = endId;
+        endId = temp;
+
+        const tempElement = startElement;
+        startElement = endElement;
+        endElement = tempElement;
+    }
+
+    const length = endPosition - startPosition + 1;
+
+    return {
+        endElementId: endId,
+        startElementId: startId,
+        startPositionOriginalDocument: startPosition,
+        endPositionOriginalDocument: endPosition,
+        selectionLengthOriginalDocument: length,
+    };
+}
