...
 
Commits (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);
}
}
}
}
......@@ -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 {
}
......