From 1696aa6c317e7d1ae4151eeec1f4482e354c2e22 Mon Sep 17 00:00:00 2001
From: Marcelo Rivera <malrogf@gmail.com>
Date: Mon, 15 Jul 2019 10:45:28 -0300
Subject: [PATCH] (feat): add support for minds-textarea (feat): added
 posts-autocomplete component (fix): reformat textinput autocomplete (feat):
 moved findSuggestions to a service

---
 package.json                                  |   2 +-
 .../posts-autocomplete.component.scss         |   7 +
 .../posts-autocomplete.component.ts           |  34 ++
 ...-input-autocomplete-container.component.ts |  20 +-
 .../text-input-autocomplete-menu.component.ts | 186 +++----
 .../text-input-autocomplete.directive.ts      | 458 ++++++++++--------
 .../text-input-autocomplete.module.ts         |   7 +-
 src/app/helpers/contenteditable-caret.ts      |  58 +++
 src/app/modules/comments/comments.module.ts   |   2 +
 .../modules/comments/list/list.component.scss |   5 +-
 .../comments/poster/poster.component.html     |  30 +-
 .../comments/poster/poster.component.ts       |   3 +
 .../newsfeed/poster/poster.component.html     |  29 +-
 .../newsfeed/poster/poster.component.scss     |   5 -
 .../newsfeed/poster/poster.component.ts       |  32 +-
 .../autocomplete-suggestions.service.ts       |  40 ++
 .../modules/suggestions/suggestions.module.ts |   4 +
 17 files changed, 545 insertions(+), 377 deletions(-)
 create mode 100644 src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.scss
 create mode 100644 src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.ts
 create mode 100644 src/app/helpers/contenteditable-caret.ts
 create mode 100644 src/app/modules/suggestions/services/autocomplete-suggestions.service.ts

diff --git a/package.json b/package.json
index 3ef32395a2..e5c541d526 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
     "prebuild": "gulp build.sass",
     "build": "ng build --prod",
     "prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
-    "build-dev": "ng build --output-path dist/en --deploy-url=/en/ --watch=true",
+    "build-dev": "ng build --output-path dist/en --deploy-url=/en/ --watch=true --poll=800",
     "test": "ng test",
     "lint": "ng lint",
     "e2e": "cypress run --debug",
diff --git a/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.scss b/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.scss
new file mode 100644
index 0000000000..d0c94a6874
--- /dev/null
+++ b/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.scss
@@ -0,0 +1,7 @@
+m-post-autocomplete-item-renderer {
+  .m-postAutocompleteItemRenderer__avatar {
+    margin-right: 4px;
+    height: 32px;
+    border-radius: 50%;
+  }
+}
diff --git a/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.ts b/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.ts
new file mode 100644
index 0000000000..e15d175a9f
--- /dev/null
+++ b/src/app/common/components/autocomplete/item-renderers/posts-autocomplete.component.ts
@@ -0,0 +1,34 @@
+import { Component, Input, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'm-post-autocomplete-item-renderer',
+  template: `
+      <ng-container *ngIf="choice?.type == 'user'; else hashtagBlock">
+          <a
+              href="javascript:;"
+              (click)="selectChoice.next(choice.username)"
+          >
+              <img
+                  class="m-postAutocompleteItemRenderer__avatar mdl-shadow--2dp"
+                  [src]="minds.cdn_url + 'icon/' + choice.guid + '/medium/' + choice.icontime">
+              {{ choice.username }}
+          </a>
+      </ng-container>
+
+      <ng-template #hashtagBlock>
+          <a
+              href="javascript:;"
+              (click)="selectChoice.next(choice)"
+          >
+              #{{ choice }}
+          </a>
+      </ng-template>
+  `
+})
+
+export class PostsAutocompleteItemRendererComponent {
+  @Input() choice;
+  @Input() selectChoice;
+
+  minds = window.Minds;
+}
diff --git a/src/app/common/components/autocomplete/text-input-autocomplete-container.component.ts b/src/app/common/components/autocomplete/text-input-autocomplete-container.component.ts
index 3fa83529af..1a9a09be3b 100644
--- a/src/app/common/components/autocomplete/text-input-autocomplete-container.component.ts
+++ b/src/app/common/components/autocomplete/text-input-autocomplete-container.component.ts
@@ -1,16 +1,16 @@
 import { Component } from '@angular/core';
 
 @Component({
-    selector: 'm-text-input--autocomplete-container',
-    styles: [
-            `
-            :host {
-                position: relative;
-                display: block;
-            }
-        `
-    ],
-    template: '<ng-content></ng-content>'
+  selector: 'm-text-input--autocomplete-container',
+  styles: [
+    `
+          :host {
+              position: relative;
+              display: block;
+          }
+    `
+  ],
+  template: '<ng-content></ng-content>'
 })
 export class TextInputAutocompleteContainerComponent {
 }
diff --git a/src/app/common/components/autocomplete/text-input-autocomplete-menu.component.ts b/src/app/common/components/autocomplete/text-input-autocomplete-menu.component.ts
index 9247db8770..fcc16bc0a0 100644
--- a/src/app/common/components/autocomplete/text-input-autocomplete-menu.component.ts
+++ b/src/app/common/components/autocomplete/text-input-autocomplete-menu.component.ts
@@ -2,113 +2,113 @@ import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular
 import { Subject } from 'rxjs';
 
 @Component({
-    selector: 'm-text-input--autocomplete-menu',
-    template: `
-        <ul
-            *ngIf="choices?.length > 0"
-            #dropdownMenu
-            class="dropdown-menu"
-            [style.top.px]="position?.top"
-            [style.left.px]="position?.left">
-            <li
-                *ngFor="let choice of choices; trackBy:trackById"
-                [class.active]="activeChoice === choice"
-            >
-                <ng-container
-                    [ngTemplateOutlet]="itemTemplate"
-                    [ngTemplateOutletContext]="{choice: choice, selectChoice: selectChoice}"
-                >
-                </ng-container>
-            </li>
-        </ul>
+  selector: 'm-text-input--autocomplete-menu',
+  template: `
+      <ul
+          *ngIf="choices?.length > 0"
+          #dropdownMenu
+          class="dropdown-menu"
+          [style.top.px]="position?.top"
+          [style.left.px]="position?.left">
+          <li
+              *ngFor="let choice of choices; trackBy:trackById"
+              [class.active]="activeChoice === choice"
+          >
+              <ng-container
+                  [ngTemplateOutlet]="itemTemplate"
+                  [ngTemplateOutletContext]="{choice: choice, selectChoice: selectChoice}"
+              >
+              </ng-container>
+          </li>
+      </ul>
 
-        <ng-template #defaultItemTemplate let-choice="choice" let-selectChoice="selectChoice">
-            <a
-                href="javascript:;"
-                (click)="selectChoice.next(choice)"
-            >
-                {{ choice }}
-            </a>
-        </ng-template>
-    `,
-    styles: [
-            `
-            .dropdown-menu {
-                display: block;
-                max-height: 200px;
-                overflow-y: auto;
-            }
-        `
-    ]
+      <ng-template #defaultItemTemplate let-choice="choice" let-selectChoice="selectChoice">
+          <a
+              href="javascript:;"
+              (click)="selectChoice.next(choice)"
+          >
+              {{ choice }}
+          </a>
+      </ng-template>
+  `,
+  styles: [
+    `
+          .dropdown-menu {
+              display: block;
+              max-height: 200px;
+              overflow-y: auto;
+          }
+    `
+  ]
 })
 export class TextInputAutocompleteMenuComponent implements OnInit {
-    @ViewChild('dropdownMenu', { static: true }) dropdownMenuElement: ElementRef<HTMLUListElement>;
-    @ViewChild('defaultItemTemplate', { static: true }) defaultItemTemplate;
-    itemTemplate: any;
-    position: { top: number; left: number };
-    selectChoice = new Subject();
-    activeChoice: any;
-    searchText: string;
-    choiceLoadError: any;
-    choiceLoading = false;
-    private _choices: any[];
-    trackById = (index: number, choice: any) =>
-        typeof choice.id !== 'undefined' ? choice.id : choice;
+  @ViewChild('dropdownMenu', { static: true }) dropdownMenuElement: ElementRef<HTMLUListElement>;
+  @ViewChild('defaultItemTemplate', { static: true }) defaultItemTemplate;
+  itemTemplate: any;
+  position: { top: number; left: number };
+  selectChoice = new Subject();
+  activeChoice: any;
+  searchText: string;
+  choiceLoadError: any;
+  choiceLoading = false;
+  private _choices: any[];
+  trackById = (index: number, choice: any) =>
+    typeof choice.id !== 'undefined' ? choice.id : choice;
 
-    set choices(choices: any[]) {
-        this._choices = choices;
-        if (choices.indexOf(this.activeChoice) === -1 && choices.length > 0) {
-            this.activeChoice = choices[0];
-        }
+  set choices(choices: any[]) {
+    this._choices = choices;
+    if (choices.indexOf(this.activeChoice) === -1 && choices.length > 0) {
+      this.activeChoice = choices[0];
     }
+  }
 
-    get choices() {
-        return this._choices;
-    }
+  get choices() {
+    return this._choices;
+  }
 
-    ngOnInit() {
-        if (!this.itemTemplate) {
-            this.itemTemplate = this.defaultItemTemplate;
-        }
+  ngOnInit() {
+    if (!this.itemTemplate) {
+      this.itemTemplate = this.defaultItemTemplate;
     }
+  }
 
-    @HostListener('document:keydown.ArrowDown', ['$event'])
-    onArrowDown(event: KeyboardEvent) {
-        event.preventDefault();
-        const index = this.choices.indexOf(this.activeChoice);
-        if (this.choices[index + 1]) {
-            this.scrollToChoice(index + 1);
-        }
+  @HostListener('document:keydown.ArrowDown', ['$event'])
+  onArrowDown(event: KeyboardEvent) {
+    event.preventDefault();
+    const index = this.choices.indexOf(this.activeChoice);
+    if (this.choices[index + 1]) {
+      this.scrollToChoice(index + 1);
     }
+  }
 
-    @HostListener('document:keydown.ArrowUp', ['$event'])
-    onArrowUp(event: KeyboardEvent) {
-        event.preventDefault();
-        const index = this.choices.indexOf(this.activeChoice);
-        if (this.choices[index - 1]) {
-            this.scrollToChoice(index - 1);
-        }
+  @HostListener('document:keydown.ArrowUp', ['$event'])
+  onArrowUp(event: KeyboardEvent) {
+    event.preventDefault();
+    const index = this.choices.indexOf(this.activeChoice);
+    if (this.choices[index - 1]) {
+      this.scrollToChoice(index - 1);
     }
+  }
 
-    @HostListener('document:keydown.Enter', ['$event'])
-    onEnter(event: KeyboardEvent) {
-        if (this.choices.indexOf(this.activeChoice) > -1) {
-            event.preventDefault();
-            this.selectChoice.next(this.activeChoice);
-        }
+  @HostListener('document:keydown.Enter', ['$event'])
+  onEnter(event: KeyboardEvent) {
+    if (this.choices.indexOf(this.activeChoice) > -1) {
+      event.preventDefault();
+      this.selectChoice.next(this.activeChoice);
     }
+  }
 
-    private scrollToChoice(index: number) {
-        this.activeChoice = this._choices[index];
-        if (this.dropdownMenuElement) {
-            const ulPosition = this.dropdownMenuElement.nativeElement.getBoundingClientRect();
-            const li = this.dropdownMenuElement.nativeElement.children[index];
-            const liPosition = li.getBoundingClientRect();
-            if (liPosition.top < ulPosition.top) {
-                li.scrollIntoView();
-            } else if (liPosition.bottom > ulPosition.bottom) {
-                li.scrollIntoView(false);
-            }
-        }
+  private scrollToChoice(index: number) {
+    this.activeChoice = this._choices[index];
+    if (this.dropdownMenuElement) {
+      const ulPosition = this.dropdownMenuElement.nativeElement.getBoundingClientRect();
+      const li = this.dropdownMenuElement.nativeElement.children[index];
+      const liPosition = li.getBoundingClientRect();
+      if (liPosition.top < ulPosition.top) {
+        li.scrollIntoView();
+      } else if (liPosition.bottom > ulPosition.bottom) {
+        li.scrollIntoView(false);
+      }
     }
+  }
 }
diff --git a/src/app/common/components/autocomplete/text-input-autocomplete.directive.ts b/src/app/common/components/autocomplete/text-input-autocomplete.directive.ts
index d93ee81147..270ace9456 100644
--- a/src/app/common/components/autocomplete/text-input-autocomplete.directive.ts
+++ b/src/app/common/components/autocomplete/text-input-autocomplete.directive.ts
@@ -1,230 +1,280 @@
 import {
-    ComponentFactoryResolver,
-    ComponentRef,
-    Directive,
-    ElementRef,
-    EventEmitter,
-    HostListener,
-    Injector,
-    Input,
-    OnDestroy,
-    Output,
-    ViewContainerRef
+  ComponentFactoryResolver,
+  ComponentRef,
+  Directive,
+  ElementRef,
+  EventEmitter,
+  HostListener,
+  Injector,
+  Input,
+  OnDestroy,
+  Output,
+  ViewContainerRef
 } from '@angular/core';
-import getCaretCoordinates from 'textarea-caret';
 import { takeUntil } from 'rxjs/operators';
 import { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
 import { Subject } from 'rxjs';
+import getCaretCoordinates from 'textarea-caret';
+import { getContentEditableCaretCoordinates } from "../../../helpers/contenteditable-caret";
 
 export interface ChoiceSelectedEvent {
-    choice: any;
-    insertedAt: {
-        start: number;
-        end: number;
-    };
+  choice: any;
+  insertedAt: {
+    start: number;
+    end: number;
+  };
 }
 
 @Directive({
-    selector:
-        'textarea[mTextInputAutocomplete],input[type="text"][mTextInputAutocomplete]'
+  selector:
+    'minds-textarea[mTextInputAutocomplete],textarea[mTextInputAutocomplete],input[type="text"][mTextInputAutocomplete]'
 })
 export class TextInputAutocompleteDirective implements OnDestroy {
-    triggerCharacter: string;
-    /**
-     * The character that will trigger the menu to appear
-     */
-    @Input() triggerCharacters = ['@'];
-
-    /**
-     * The regular expression that will match the search text after the trigger character
-     */
-    @Input() searchRegexp = /^\w*$/;
-
-    /**
-     * The menu component to show with available options.
-     * You can extend the built in `TextInputAutocompleteMenuComponent` component to use a custom template
-     */
-    @Input() menuComponent = TextInputAutocompleteMenuComponent;
-
-    @Input() itemTemplate: any;
-
-    /**
-     * Called when the options menu is shown
-     */
-    @Output() menuShown = new EventEmitter();
-
-    /**
-     * Called when the options menu is hidden
-     */
-    @Output() menuHidden = new EventEmitter();
-
-    /**
-     * Called when a choice is selected
-     */
-    @Output() choiceSelected = new EventEmitter<ChoiceSelectedEvent>();
-
-    /**
-     * A function that accepts a search string and returns an array of choices. Can also return a promise.
-     */
-    @Input()
-    findChoices: (
-        searchText: string,
-        triggerCharacter?: string
-    ) => any[] | Promise<any[]>;
-
-    /**
-     * A function that formats the selected choice once selected.
-     */
-    @Input()
-    getChoiceLabel: (choice: any, triggerCharacter?: any) => string = choice => choice;
-
-    /* tslint:disable member-ordering */
-    private menu:
-        | {
-        component: ComponentRef<TextInputAutocompleteMenuComponent>;
-        triggerCharacterPosition: number;
-        lastCaretPosition?: number;
-    }
-        | undefined;
-
-    private menuHidden$ = new Subject();
-
-    constructor(
-        private componentFactoryResolver: ComponentFactoryResolver,
-        private viewContainerRef: ViewContainerRef,
-        private injector: Injector,
-        private elm: ElementRef
-    ) {}
-
-    @HostListener('keypress', ['$event.key'])
-    onKeypress(key: string) {
-        const index: number = this.triggerCharacters.indexOf(key);
-        if (index !== -1) {
-            this.triggerCharacter = this.triggerCharacters[index];
-            this.showMenu();
-        }
+  triggerCharacter: string;
+  /**
+   * The character that will trigger the menu to appear
+   */
+  @Input() triggerCharacters = ['@'];
+
+  /**
+   * The regular expression that will match the search text after the trigger character
+   */
+  @Input() searchRegexp = /^\w*$/;
+
+  /**
+   * The menu component to show with available options.
+   * You can extend the built in `TextInputAutocompleteMenuComponent` component to use a custom template
+   */
+  @Input() menuComponent = TextInputAutocompleteMenuComponent;
+
+  @Input() itemTemplate: any;
+
+  /**
+   * Called when the options menu is shown
+   */
+  @Output() menuShown = new EventEmitter();
+
+  /**
+   * Called when the options menu is hidden
+   */
+  @Output() menuHidden = new EventEmitter();
+
+  /**
+   * Called when a choice is selected
+   */
+  @Output() choiceSelected = new EventEmitter<ChoiceSelectedEvent>();
+
+  /**
+   * A function that accepts a search string and returns an array of choices. Can also return a promise.
+   */
+  @Input()
+  findChoices: (
+    searchText: string,
+    triggerCharacter?: string
+  ) => any[] | Promise<any[]>;
+
+  /**
+   * A function that formats the selected choice once selected.
+   */
+  @Input()
+  getChoiceLabel: (choice: any, triggerCharacter?: any) => string = choice => choice;
+
+  /* tslint:disable member-ordering */
+  private menu:
+    | {
+    component: ComponentRef<TextInputAutocompleteMenuComponent>;
+    triggerCharacterPosition: number;
+    lastCaretPosition?: number;
+  }
+    | undefined;
+
+  private menuHidden$ = new Subject();
+
+  constructor(
+    private componentFactoryResolver: ComponentFactoryResolver,
+    private viewContainerRef: ViewContainerRef,
+    private injector: Injector,
+    private elm: ElementRef
+  ) {
+  }
+
+  @HostListener('keypress', ['$event.key'])
+  onKeypress(key: string) {
+    const index: number = this.triggerCharacters.indexOf(key);
+    if (index !== -1) {
+      this.triggerCharacter = this.triggerCharacters[index];
+      this.showMenu();
     }
+  }
 
-    @HostListener('input', ['$event.target.value'])
-    onChange(value: string) {
-        if (this.menu) {
-            if (
-                this.triggerCharacters.indexOf(
-                    value[this.menu.triggerCharacterPosition]
-                ) === -1
-            ) {
-                this.hideMenu();
-            } else {
-                const cursor = this.elm.nativeElement.selectionStart;
-                if (cursor < this.menu.triggerCharacterPosition) {
-                    this.hideMenu();
-                } else {
-                    const searchText = value.slice(
-                        this.menu.triggerCharacterPosition + 1,
-                        cursor
-                    );
-                    if (!searchText.match(this.searchRegexp)) {
-                        this.hideMenu();
-                    } else {
-                        this.menu.component.instance.searchText = searchText;
-                        this.menu.component.instance.choices = [];
-                        this.menu.component.instance.choiceLoadError = undefined;
-                        this.menu.component.instance.choiceLoading = true;
-                        this.menu.component.changeDetectorRef.detectChanges();
-                        Promise.resolve(this.findChoices(searchText, this.triggerCharacter))
-                            .then(choices => {
-                                if (this.menu) {
-                                    this.menu.component.instance.choices = choices;
-                                    this.menu.component.instance.choiceLoading = false;
-                                    this.menu.component.changeDetectorRef.detectChanges();
-                                }
-                            })
-                            .catch(err => {
-                                if (this.menu) {
-                                    this.menu.component.instance.choiceLoading = false;
-                                    this.menu.component.instance.choiceLoadError = err;
-                                    this.menu.component.changeDetectorRef.detectChanges();
-                                }
-                            });
-                    }
+  @HostListener('input', ['$event'])
+  onChange(event: any) {
+    const value: string = event.target.value || event.target.textContent;
+    if (this.menu) {
+      if (
+        this.triggerCharacters.indexOf(
+          value[this.menu.triggerCharacterPosition]
+        ) === -1
+      ) {
+        this.hideMenu();
+      } else {
+        const cursor = this.elm.nativeElement.selectionStart;
+        if (cursor < this.menu.triggerCharacterPosition) {
+          this.hideMenu();
+        } else {
+          const searchText = value.slice(
+            this.menu.triggerCharacterPosition + 1,
+            cursor
+          );
+          if (!searchText.match(this.searchRegexp)) {
+            this.hideMenu();
+          } else {
+            this.menu.component.instance.searchText = searchText;
+            this.menu.component.instance.choices = [];
+            this.menu.component.instance.choiceLoadError = undefined;
+            this.menu.component.instance.choiceLoading = true;
+            this.menu.component.changeDetectorRef.detectChanges();
+            Promise.resolve(this.findChoices(searchText, this.triggerCharacter))
+              .then(choices => {
+                if (this.menu) {
+                  this.menu.component.instance.choices = choices;
+                  this.menu.component.instance.choiceLoading = false;
+                  this.menu.component.changeDetectorRef.detectChanges();
                 }
-            }
+              })
+              .catch(err => {
+                if (this.menu) {
+                  this.menu.component.instance.choiceLoading = false;
+                  this.menu.component.instance.choiceLoadError = err;
+                  this.menu.component.changeDetectorRef.detectChanges();
+                }
+              });
+          }
         }
+      }
     }
+  }
 
-    @HostListener('blur')
-    onBlur() {
-        if (this.menu) {
-            this.menu.lastCaretPosition = this.elm.nativeElement.selectionStart;
-        }
+  @HostListener('blur')
+  onBlur() {
+    if (this.menu) {
+      this.menu.lastCaretPosition = this.getTriggerCharPosition(this.elm.nativeElement);
     }
+  }
 
-    private showMenu() {
-        if (!this.menu) {
-            const menuFactory = this.componentFactoryResolver.resolveComponentFactory<
-                TextInputAutocompleteMenuComponent
-                >(this.menuComponent);
-            this.menu = {
-                component: this.viewContainerRef.createComponent(
-                    menuFactory,
-                    0,
-                    this.injector
-                ),
-                triggerCharacterPosition: this.elm.nativeElement.selectionStart
-            };
-            const lineHeight = +getComputedStyle(
-                this.elm.nativeElement
-            ).lineHeight!.replace(/px$/, '');
-            const { top, left } = getCaretCoordinates(
-                this.elm.nativeElement,
-                this.elm.nativeElement.selectionStart
-            );
-            this.menu.component.instance.itemTemplate = this.itemTemplate;
-            this.menu.component.instance.position = {
-                top: top + lineHeight,
-                left
-            };
-            this.menu.component.changeDetectorRef.detectChanges();
-            this.menu.component.instance.selectChoice
-                .pipe(takeUntil(this.menuHidden$))
-                .subscribe(choice => {
-                    const label = this.getChoiceLabel(choice, this.triggerCharacter);
-                    const textarea: HTMLTextAreaElement = this.elm.nativeElement;
-                    const value: string = textarea.value;
-                    const startIndex = this.menu!.triggerCharacterPosition;
-                    const start = value.slice(0, startIndex);
-                    const caretPosition =
-                        this.menu!.lastCaretPosition || textarea.selectionStart;
-                    const end = value.slice(caretPosition);
-                    textarea.value = start + label + end;
-                    // force ng model / form control to update
-                    textarea.dispatchEvent(new Event('input'));
-                    this.hideMenu();
-                    const setCursorAt = (start + label).length;
-                    textarea.setSelectionRange(setCursorAt, setCursorAt);
-                    textarea.focus();
-                    this.choiceSelected.emit({
-                        choice,
-                        insertedAt: {
-                            start: startIndex,
-                            end: startIndex + label.length
-                        }
-                    });
-                });
-            this.menuShown.emit();
-        }
+  private getTriggerCharPosition(element) {
+    if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
+      return this.elm.nativeElement.selectionStart;
+    } else {
+      return getContentEditableCaretCoordinates(element).start;
     }
+  }
 
-    private hideMenu() {
-        if (this.menu) {
-            this.menu.component.destroy();
-            this.menuHidden$.next();
-            this.menuHidden.emit();
-            this.menu = undefined;
-        }
+  private showMenu() {
+    if (!this.menu) {
+      const menuFactory = this.componentFactoryResolver.resolveComponentFactory<TextInputAutocompleteMenuComponent>(this.menuComponent);
+      this.menu = {
+        component: this.viewContainerRef.createComponent(
+          menuFactory,
+          0,
+          this.injector
+        ),
+        triggerCharacterPosition: this.getTriggerCharPosition(this.elm.nativeElement)
+      };
+      const lineHeight = +getComputedStyle(
+        this.elm.nativeElement
+      ).lineHeight!.replace(/px$/, '');
+
+      const { top, left } = this.elm.nativeElement instanceof HTMLTextAreaElement ?
+        <any>getCaretCoordinates(
+          this.elm.nativeElement,
+          this.elm.nativeElement.selectionStart
+        ) :
+        getContentEditableCaretCoordinates(this.elm.nativeElement)
+      ;
+      this.menu.component.instance.itemTemplate = this.itemTemplate;
+      this.menu.component.instance.position = {
+        top: top + lineHeight,
+        left
+      };
+      this.menu.component.changeDetectorRef.detectChanges();
+      this.menu.component.instance.selectChoice
+        .pipe(takeUntil(this.menuHidden$))
+        .subscribe(choice => {
+          const label = this.getChoiceLabel(choice, this.triggerCharacter);
+          let element: any = this.elm.nativeElement;
+
+          if (element.nodeName === 'MINDS-TEXTAREA') {
+            element = element.firstChild;
+          }
+          let value: string;
+          let selectionStart;
+          if (element instanceof HTMLTextAreaElement) {
+            value = element.value;
+            selectionStart = element.selectionStart;
+          } else {
+            value = element.textContent;
+            selectionStart = getContentEditableCaretCoordinates(element).start;
+          }
+
+          const startIndex = this.menu!.triggerCharacterPosition;
+
+          const start = value.slice(0, startIndex);
+
+          const caretPosition =
+            this.menu!.lastCaretPosition || selectionStart || 0;
+
+          const end = value.slice(caretPosition);
+
+          value = start + label + end;
+
+          if (element instanceof HTMLDivElement) {
+            element.textContent = value;
+          } else {
+            element.value = value;
+          }
+          // force ng model / form control to update
+          element.dispatchEvent(new Event('input'));
+          this.hideMenu();
+          const setCursorAt = (start + label).length;
+
+          if (element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement) {
+            element.setSelectionRange(setCursorAt, setCursorAt);
+          } else {
+            const range = document.createRange();
+            const sel = window.getSelection();
+
+            range.setStart(element.firstChild, setCursorAt);
+            range.setEnd(element.firstChild, setCursorAt);
+            //range.collapse(true);
+            sel.removeAllRanges();
+            sel.addRange(range);
+          }
+
+          element.focus();
+
+          this.choiceSelected.emit({
+            choice,
+            insertedAt: {
+              start: startIndex,
+              end: startIndex + label.length
+            }
+          });
+        });
+      this.menuShown.emit();
     }
+  }
 
-    ngOnDestroy() {
-        this.hideMenu();
+  private hideMenu() {
+    if (this.menu) {
+      this.menu.component.destroy();
+      this.menuHidden$.next();
+      this.menuHidden.emit();
+      this.menu = undefined;
     }
+  }
+
+  ngOnDestroy() {
+    this.hideMenu();
+  }
 }
diff --git a/src/app/common/components/autocomplete/text-input-autocomplete.module.ts b/src/app/common/components/autocomplete/text-input-autocomplete.module.ts
index c7867ff850..7d2820b463 100644
--- a/src/app/common/components/autocomplete/text-input-autocomplete.module.ts
+++ b/src/app/common/components/autocomplete/text-input-autocomplete.module.ts
@@ -4,18 +4,21 @@ import { CommonModule } from '@angular/common';
 import { TextInputAutocompleteDirective } from './text-input-autocomplete.directive';
 import { TextInputAutocompleteContainerComponent } from './text-input-autocomplete-container.component';
 import { TextInputAutocompleteMenuComponent } from './text-input-autocomplete-menu.component';
+import { PostsAutocompleteItemRendererComponent } from "./item-renderers/posts-autocomplete.component";
 
 @NgModule({
     declarations: [
         TextInputAutocompleteDirective,
         TextInputAutocompleteContainerComponent,
-        TextInputAutocompleteMenuComponent
+        TextInputAutocompleteMenuComponent,
+        PostsAutocompleteItemRendererComponent,
     ],
     imports: [CommonModule],
     exports: [
         TextInputAutocompleteDirective,
         TextInputAutocompleteContainerComponent,
-        TextInputAutocompleteMenuComponent
+        TextInputAutocompleteMenuComponent,
+        PostsAutocompleteItemRendererComponent,
     ],
     entryComponents: [TextInputAutocompleteMenuComponent]
 })
diff --git a/src/app/helpers/contenteditable-caret.ts b/src/app/helpers/contenteditable-caret.ts
new file mode 100644
index 0000000000..94a7e137c6
--- /dev/null
+++ b/src/app/helpers/contenteditable-caret.ts
@@ -0,0 +1,58 @@
+// node_walk: walk the element tree, stop when func(node) returns false
+function node_walk(node, func) {
+  var result = func(node);
+  for (node = node.firstChild; result !== false && node; node = node.nextSibling)
+    result = node_walk(node, func);
+  return result;
+};
+
+// getCaretPosition: return [start, end] as offsets to elem.textContent that
+//   correspond to the selected portion of text
+//   (if start == end, caret is at given position and no text is selected)
+export function getContentEditableCaretCoordinates(elem) {
+  var sel: any = window.getSelection();
+  var cum_length = [0, 0];
+
+  if (sel.anchorNode == elem)
+    cum_length = [sel.anchorOffset, sel.extentOffset];
+  else {
+    var nodes_to_find = [sel.anchorNode, sel.extentNode];
+    if (!elem.contains(sel.anchorNode) || !elem.contains(sel.extentNode))
+      return undefined;
+    else {
+      var found: any = [0, 0];
+      var i;
+      node_walk(elem, function (node) {
+        for (i = 0; i < 2; i++) {
+          if (node == nodes_to_find[i]) {
+            found[i] = true;
+            if (found[i == 0 ? 1 : 0])
+              return false; // all done
+          }
+        }
+
+        if (node.textContent && !node.firstChild) {
+          for (i = 0; i < 2; i++) {
+            if (!found[i])
+              cum_length[i] += node.textContent.length;
+          }
+        }
+      });
+      cum_length[0] += sel.anchorOffset;
+      cum_length[1] += sel.extentOffset;
+    }
+  }
+  let coordinates = {
+    start: cum_length[0],
+    end: cum_length[1],
+  };
+
+  if (cum_length[0] <= cum_length[1])
+    return coordinates;
+
+  // it's reversed
+  coordinates.start = cum_length[1];
+  coordinates.end = cum_length[0];
+
+  return coordinates;
+}
diff --git a/src/app/modules/comments/comments.module.ts b/src/app/modules/comments/comments.module.ts
index 9af11055d2..32fe7a8184 100644
--- a/src/app/modules/comments/comments.module.ts
+++ b/src/app/modules/comments/comments.module.ts
@@ -15,6 +15,7 @@ import { CommentPosterComponent } from './poster/poster.component';
 import { CommentsTreeComponent } from './tree/tree.component';
 import { CommentsThreadComponent } from './thread/thread.component';
 import { CommentsService } from './comments.service';
+import { TextInputAutocompleteModule } from "../../common/components/autocomplete";
 
 @NgModule({
   imports: [
@@ -25,6 +26,7 @@ import { CommentsService } from './comments.service';
     VideoModule,
     TranslateModule,
     ModalsModule,
+    TextInputAutocompleteModule,
   ],
   declarations: [
     CommentsScrollDirective,
diff --git a/src/app/modules/comments/list/list.component.scss b/src/app/modules/comments/list/list.component.scss
index 897fc0f043..a304a67a82 100644
--- a/src/app/modules/comments/list/list.component.scss
+++ b/src/app/modules/comments/list/list.component.scss
@@ -245,6 +245,9 @@ minds-comments, m-comments__tree, .m-comment-wrapper {
     }
   }
 
+  .m-comments-composer form m-text-input--autocomplete-container {
+    width: 100%;
+  }
   .m-comments-composer form minds-textarea,
   .minds-editable-container textarea {
     width: 100%;
@@ -423,6 +426,6 @@ minds-comments, m-comments__tree, .m-comment-wrapper {
 }
 
 .m-comment--poster .minds-body {
-  overflow: hidden;
+  overflow: visible;
   min-height: 50px;
 }
diff --git a/src/app/modules/comments/poster/poster.component.html b/src/app/modules/comments/poster/poster.component.html
index 205647c716..93b416b511 100644
--- a/src/app/modules/comments/poster/poster.component.html
+++ b/src/app/modules/comments/poster/poster.component.html
@@ -10,14 +10,28 @@
         <div class="m-comments-composer">
 
             <form (submit)="post($event)">
-                <minds-textarea
-                  #message="Textarea"
-                  [(mModel)]="content"
-                  [disabled]="(ascendingInProgress || descendingInProgress) && attachment.hasFile()"
-                  (keyup)="getPostPreview(content)"
-                  (keypress)="keypress($event)"
-                  [placeholder]="conversation ? 'Enter your message' : 'Enter your comment'"
-                ></minds-textarea>
+                <ng-template #itemTemplate let-choice="choice" let-selectChoice="selectChoice">
+                    <m-post-autocomplete-item-renderer
+                        [choice]="choice"
+                        [selectChoice]="selectChoice"
+                    ></m-post-autocomplete-item-renderer>
+                </ng-template>
+                <m-text-input--autocomplete-container>
+                    <minds-textarea
+                        #message="Textarea"
+                        [(mModel)]="content"
+                        [disabled]="(ascendingInProgress || descendingInProgress) && attachment.hasFile()"
+                        (keyup)="getPostPreview(content)"
+                        (keypress)="keypress($event)"
+                        [placeholder]="conversation ? 'Enter your message' : 'Enter your comment'"
+                        mTextInputAutocomplete
+                        [findChoices]="suggestions.findSuggestions"
+                        [getChoiceLabel]="suggestions.getChoiceLabel"
+                        [itemTemplate]="itemTemplate"
+                        [triggerCharacters]="['#', '@']"
+                    ></minds-textarea>
+                </m-text-input--autocomplete-container>
+
             </form>
 
             <div class="minds-comment-span mdl-color-text--red-500" *ngIf="!canPost && triedToPost">
diff --git a/src/app/modules/comments/poster/poster.component.ts b/src/app/modules/comments/poster/poster.component.ts
index c835915d08..541b975b37 100644
--- a/src/app/modules/comments/poster/poster.component.ts
+++ b/src/app/modules/comments/poster/poster.component.ts
@@ -16,6 +16,8 @@ import { Upload } from '../../../services/api/upload';
 import { AttachmentService } from '../../../services/attachment';
 import { Textarea } from '../../../common/components/editors/textarea.component';
 import { SocketsService } from '../../../services/sockets';
+import autobind from "../../../helpers/autobind";
+import { AutocompleteSuggestionsService } from "../../suggestions/services/autocomplete-suggestions.service";
 
 @Component({
   selector: 'm-comment__poster',
@@ -53,6 +55,7 @@ export class CommentPosterComponent {
     public client: Client,
     public attachment: AttachmentService,
     public sockets: SocketsService,
+    public suggestions: AutocompleteSuggestionsService,
     private renderer: Renderer,
     private cd: ChangeDetectorRef
   ) {
diff --git a/src/app/modules/newsfeed/poster/poster.component.html b/src/app/modules/newsfeed/poster/poster.component.html
index 08d7ff30b1..60bcf61308 100644
--- a/src/app/modules/newsfeed/poster/poster.component.html
+++ b/src/app/modules/newsfeed/poster/poster.component.html
@@ -2,27 +2,10 @@
   <div class="mdl-card__supporting-text">
     <form (submit)="post()">
       <ng-template #itemTemplate let-choice="choice" let-selectChoice="selectChoice">
-        <ng-container *ngIf="choice?.type == 'user'; else hashtagBlock">
-          <a
-              href="javascript:;"
-              (click)="selectChoice.next(choice.username)"
-          >
-            <img
-                class="m-poster__userSuggestionAvatar mdl-shadow--2dp"
-                [src]="minds.cdn_url + 'icon/' + choice.guid + '/medium/' + choice.icontime">
-            {{ choice.username }}
-          </a>
-        </ng-container>
-
-        <ng-template #hashtagBlock>
-          <a
-              href="javascript:;"
-              (click)="selectChoice.next(choice)"
-          >
-            #{{ choice }}
-          </a>
-        </ng-template>
-
+        <m-post-autocomplete-item-renderer
+            [choice]="choice"
+            [selectChoice]="selectChoice"
+        ></m-post-autocomplete-item-renderer>
       </ng-template>
 
       <m-text-input--autocomplete-container>
@@ -39,8 +22,8 @@
           i18n-placeholder="@@MINDS__POSTER__SPEAK_YOUR_MIND"
           [autoGrow]
           mTextInputAutocomplete
-          [findChoices]="findSuggestions"
-          [getChoiceLabel]="getChoiceLabel"
+          [findChoices]="suggestions.findSuggestions"
+          [getChoiceLabel]="suggestions.getChoiceLabel"
           [itemTemplate]="itemTemplate"
           [triggerCharacters]="['#', '@']"
         ></textarea>
diff --git a/src/app/modules/newsfeed/poster/poster.component.scss b/src/app/modules/newsfeed/poster/poster.component.scss
index b19b91dcce..a949c51f33 100644
--- a/src/app/modules/newsfeed/poster/poster.component.scss
+++ b/src/app/modules/newsfeed/poster/poster.component.scss
@@ -257,11 +257,6 @@ m-hashtags-selector {
 }
 
 .m-poster {
-  .m-poster__userSuggestionAvatar {
-    margin-right: 4px;
-    height: 32px;
-    border-radius: 50%;
-  }
   .m-poster__ActionBar {
     display: flex;
     flex-direction: row;
diff --git a/src/app/modules/newsfeed/poster/poster.component.ts b/src/app/modules/newsfeed/poster/poster.component.ts
index b1541c0b27..c198e1795e 100644
--- a/src/app/modules/newsfeed/poster/poster.component.ts
+++ b/src/app/modules/newsfeed/poster/poster.component.ts
@@ -11,6 +11,7 @@ import { Subject, Subscription } from "rxjs";
 import { debounceTime } from "rxjs/operators";
 import { Router } from "@angular/router";
 import { InMemoryStorageService } from "../../../services/in-memory-storage.service";
+import { AutocompleteSuggestionsService } from "../../suggestions/services/autocomplete-suggestions.service";
 
 @Component({
   moduleId: module.id,
@@ -60,6 +61,7 @@ export class PosterComponent {
     public client: Client,
     public upload: Upload,
     public attachment: AttachmentService,
+    public suggestions: AutocompleteSuggestionsService,
     protected elementRef: ElementRef,
     protected router: Router,
     protected inMemoryStorageService: InMemoryStorageService
@@ -259,36 +261,6 @@ export class PosterComponent {
     this.attachment.preview(message.value);
   }
 
-  @autobind()
-  async findSuggestions(searchText: string, triggerCharacter: string) {
-    if (searchText == '')
-      return;
-
-    let url = 'api/v2/search/suggest';
-
-    if (triggerCharacter === '#') {
-      url += '/tags';
-    }
-    const response: any = await this.client.get(url, { q: searchText });
-
-    let result;
-    switch (triggerCharacter) {
-      case '#':
-        result = response.tags.filter(item => item.toLowerCase().includes(searchText.toLowerCase()));
-        break;
-      case '@':
-        result = response.entities
-          .filter(item => item.username.toLowerCase().includes(searchText.toLowerCase()));
-        break;
-    }
-
-    return result.slice(0,5);
-  }
-
-  getChoiceLabel(text: string, triggerCharacter: string) {
-    return `${triggerCharacter}${text}`;
-  }
-
   createBlog() {
     if (this.meta && this.meta.message) {
       const shouldNavigate = confirm(`Are you sure? The content will be moved to the blog editor.`);
diff --git a/src/app/modules/suggestions/services/autocomplete-suggestions.service.ts b/src/app/modules/suggestions/services/autocomplete-suggestions.service.ts
new file mode 100644
index 0000000000..afa92f3108
--- /dev/null
+++ b/src/app/modules/suggestions/services/autocomplete-suggestions.service.ts
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core';
+import { Client } from "../../../services/api/client";
+import autobind from "../../../helpers/autobind";
+
+@Injectable()
+export class AutocompleteSuggestionsService {
+
+  constructor(private client: Client) {
+  }
+
+  @autobind()
+  async findSuggestions(searchText: string, triggerCharacter: string) {
+    if (searchText == '')
+      return;
+
+    let url = 'api/v2/search/suggest';
+
+    if (triggerCharacter === '#') {
+      url += '/tags';
+    }
+    const response: any = await this.client.get(url, { q: searchText });
+
+    let result;
+    switch (triggerCharacter) {
+      case '#':
+        result = response.tags.filter(item => item.toLowerCase().includes(searchText.toLowerCase()));
+        break;
+      case '@':
+        result = response.entities
+          .filter(item => item.username.toLowerCase().includes(searchText.toLowerCase()));
+        break;
+    }
+
+    return result.slice(0,5);
+  }
+
+  getChoiceLabel(text: string, triggerCharacter: string) {
+    return `${triggerCharacter}${text}`;
+  }
+}
diff --git a/src/app/modules/suggestions/suggestions.module.ts b/src/app/modules/suggestions/suggestions.module.ts
index df4a947b96..bbaa1b24e5 100644
--- a/src/app/modules/suggestions/suggestions.module.ts
+++ b/src/app/modules/suggestions/suggestions.module.ts
@@ -7,6 +7,7 @@ import { LegacyModule } from '../legacy/legacy.module';
 import { CommonModule } from '../../common/common.module';
 import { SuggestionsSidebar } from './channel/sidebar.component';
 import { GroupSuggestionsSidebarComponent } from "./groups/sidebar.component";
+import { AutocompleteSuggestionsService } from "./services/autocomplete-suggestions.service";
 
 @NgModule({
   imports: [
@@ -25,6 +26,9 @@ import { GroupSuggestionsSidebarComponent } from "./groups/sidebar.component";
     SuggestionsSidebar,
     GroupSuggestionsSidebarComponent,
   ],
+  providers: [
+    AutocompleteSuggestionsService,
+  ]
 })
 export class SuggestionsModule {
 }
-- 
GitLab