Skip to content
Commits on Source (7)
......@@ -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",
......@@ -49,7 +49,7 @@
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.12.1",
"@angular-devkit/build-angular": "~0.13.9",
"@angular/cli": "^7.2.1",
"@angular/compiler-cli": "~8.0.3",
"@angular/language-service": "~8.0.3",
......
m-post-autocomplete-item-renderer {
.m-postAutocompleteItemRenderer__avatar {
margin-right: 4px;
height: 32px;
border-radius: 50%;
}
}
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;
}
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 {
}
......@@ -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);
}
}
}
}
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();
}
}
......@@ -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]
})
......
......@@ -15,7 +15,7 @@
left: 0;
height: 48px;
width: 100%;
overflow-y: hidden;
overflow-y: visible;
@include m-theme(){
border-bottom: 1px solid themed($m-grey-50);
}
......
......@@ -94,10 +94,9 @@ describe('TagPipe', () => {
expect(transformedString).toEqual('<a class="tag" href="/test1" target="_blank">@test1</a> <a class="tag" href="/test2" target="_blank">@test2</a>');
});
fit('should transform many adjacent tags', () => {
it('should transform many adjacent tags', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = '@test1 @test2 @test3 @test4 @test5 @test6 @test7 @test8 @test9 @test10 @test11 @test12 @test13 @test14 @test15';
console.log(string);
const transformedString = pipe.transform(<any>string);
expect(transformedString).toEqual(`<a class="tag" href="/test1" target="_blank">@test1</a> <a class="tag" href="/test2" target="_blank">@test2</a> `
+ `<a class="tag" href="/test3" target="_blank">@test3</a> <a class="tag" href="/test4" target="_blank">@test4</a> `
......
// 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;
}
......@@ -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,
......
......@@ -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;
}
......@@ -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">
......
......@@ -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
) {
......
......@@ -12,9 +12,10 @@
width: 90px;
overflow-y: auto;
list-style-type: none;
@include m-theme(){
box-shadow: 0 2px 5px rgba(themed($m-black),0.2);
@media screen and(min-width: $min-desktop) {
@include m-theme(){
box-shadow: 0 2px 5px rgba(themed($m-black),0.2);
}
}
@media screen and(max-width: $min-desktop) {
......
......@@ -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>
......
......@@ -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;
......
......@@ -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.`);
......
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}`;
}
}
......@@ -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 {
}
......