]> Untitled Git - lemmy.git/commitdiff
Adding autocomplete to post, community, message, and site forms. Fixes #453
authorDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 18:59:50 +0000 (13:59 -0500)
committerDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 18:59:50 +0000 (13:59 -0500)
ui/src/components/comment-form.tsx
ui/src/components/community-form.tsx
ui/src/components/post-form.tsx
ui/src/components/private-message-form.tsx
ui/src/components/site-form.tsx
ui/src/utils.ts

index b8ea0a5a3f510adb6100e1e248551755e0ef58f6..f4eb118153f7565d0992ad869495ec8b531f4453 100644 (file)
@@ -2,28 +2,20 @@ import { Component, linkEvent } from 'inferno';
 import {
   CommentNode as CommentNodeI,
   CommentForm as CommentFormI,
-  SearchForm,
-  SearchType,
-  SortType,
-  UserOperation,
-  SearchResponse,
 } from '../interfaces';
-import { Subscription } from 'rxjs';
 import {
-  wsJsonToRes,
   capitalizeFirstLetter,
-  mentionDropdownFetchLimit,
   mdToHtml,
   randomStr,
   markdownHelpUrl,
   toast,
+  setupTribute,
 } from '../utils';
 import { WebSocketService, UserService } from '../services';
 import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
-import Tribute from 'tributejs/src/Tribute.js';
-import emojiShortName from 'emoji-short-name';
 
 interface CommentFormProps {
   postId?: number;
@@ -42,9 +34,7 @@ interface CommentFormState {
 
 export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   private id = `comment-form-${randomStr()}`;
-  private userSub: Subscription;
-  private communitySub: Subscription;
-  private tribute: any;
+  private tribute: Tribute;
   private emptyState: CommentFormState = {
     commentForm: {
       auth: null,
@@ -68,55 +58,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
   constructor(props: any, context: any) {
     super(props, context);
 
-    this.tribute = new Tribute({
-      collection: [
-        // Emojis
-        {
-          trigger: ':',
-          menuItemTemplate: (item: any) => {
-            let emoji = `:${item.original.key}:`;
-            return `${item.original.val} ${emoji}`;
-          },
-          selectTemplate: (item: any) => {
-            return `:${item.original.key}:`;
-          },
-          values: Object.entries(emojiShortName).map(e => {
-            return { key: e[1], val: e[0] };
-          }),
-          allowSpaces: false,
-          autocompleteMode: true,
-          menuItemLimit: mentionDropdownFetchLimit,
-        },
-        // Users
-        {
-          trigger: '@',
-          selectTemplate: (item: any) => {
-            return `[/u/${item.original.key}](/u/${item.original.key})`;
-          },
-          values: (text: string, cb: any) => {
-            this.userSearch(text, (users: any) => cb(users));
-          },
-          allowSpaces: false,
-          autocompleteMode: true,
-          menuItemLimit: mentionDropdownFetchLimit,
-        },
-
-        // Communities
-        {
-          trigger: '#',
-          selectTemplate: (item: any) => {
-            return `[/c/${item.original.key}](/c/${item.original.key})`;
-          },
-          values: (text: string, cb: any) => {
-            this.communitySearch(text, (communities: any) => cb(communities));
-          },
-          allowSpaces: false,
-          autocompleteMode: true,
-          menuItemLimit: mentionDropdownFetchLimit,
-        },
-      ],
-    });
-
+    this.tribute = setupTribute();
     this.state = this.emptyState;
 
     if (this.props.node) {
@@ -297,68 +239,4 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
         toast(error, 'danger');
       });
   }
-
-  userSearch(text: string, cb: any) {
-    if (text) {
-      let form: SearchForm = {
-        q: text,
-        type_: SearchType[SearchType.Users],
-        sort: SortType[SortType.TopAll],
-        page: 1,
-        limit: mentionDropdownFetchLimit,
-      };
-
-      WebSocketService.Instance.search(form);
-
-      this.userSub = WebSocketService.Instance.subject.subscribe(
-        msg => {
-          let res = wsJsonToRes(msg);
-          if (res.op == UserOperation.Search) {
-            let data = res.data as SearchResponse;
-            let users = data.users.map(u => {
-              return { key: u.name };
-            });
-            cb(users);
-            this.userSub.unsubscribe();
-          }
-        },
-        err => console.error(err),
-        () => console.log('complete')
-      );
-    } else {
-      cb([]);
-    }
-  }
-
-  communitySearch(text: string, cb: any) {
-    if (text) {
-      let form: SearchForm = {
-        q: text,
-        type_: SearchType[SearchType.Communities],
-        sort: SortType[SortType.TopAll],
-        page: 1,
-        limit: mentionDropdownFetchLimit,
-      };
-
-      WebSocketService.Instance.search(form);
-
-      this.communitySub = WebSocketService.Instance.subject.subscribe(
-        msg => {
-          let res = wsJsonToRes(msg);
-          if (res.op == UserOperation.Search) {
-            let data = res.data as SearchResponse;
-            let communities = data.communities.map(u => {
-              return { key: u.name };
-            });
-            cb(communities);
-            this.communitySub.unsubscribe();
-          }
-        },
-        err => console.error(err),
-        () => console.log('complete')
-      );
-    } else {
-      cb([]);
-    }
-  }
 }
index 4dc7bfcbb23411417128a6f0118ed1aa68ff0851..33c63c897130bba3db31460621f78295d8cb7986 100644 (file)
@@ -11,7 +11,14 @@ import {
   WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
+import {
+  wsJsonToRes,
+  capitalizeFirstLetter,
+  toast,
+  randomStr,
+  setupTribute,
+} from '../utils';
+import Tribute from 'tributejs/src/Tribute.js';
 import autosize from 'autosize';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -36,6 +43,8 @@ export class CommunityForm extends Component<
   CommunityFormProps,
   CommunityFormState
 > {
+  private id = `community-form-${randomStr()}`;
+  private tribute: Tribute;
   private subscription: Subscription;
 
   private emptyState: CommunityFormState = {
@@ -53,6 +62,7 @@ export class CommunityForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
+    this.tribute = setupTribute();
     this.state = this.emptyState;
 
     if (this.props.community) {
@@ -80,7 +90,14 @@ export class CommunityForm extends Component<
   }
 
   componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
+    var textarea: any = document.getElementById(this.id);
+    autosize(textarea);
+    this.tribute.attach(textarea);
+    textarea.addEventListener('tribute-replaced', () => {
+      this.state.communityForm.description = textarea.value;
+      this.setState(this.state);
+      autosize.update(textarea);
+    });
   }
 
   componentWillUnmount() {
@@ -130,6 +147,7 @@ export class CommunityForm extends Component<
           </label>
           <div class="col-12">
             <textarea
+              id={this.id}
               value={this.state.communityForm.description}
               onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
               class="form-control"
index 440617743e3bd1a904e5eb754e67025c8608b767..677007caf2bc8b9ee50d04b0ea508b52168c1bcb 100644 (file)
@@ -30,8 +30,11 @@ import {
   debounce,
   isImage,
   toast,
+  randomStr,
+  setupTribute,
 } from '../utils';
 import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -56,6 +59,8 @@ interface PostFormState {
 }
 
 export class PostForm extends Component<PostFormProps, PostFormState> {
+  private id = `post-form-${randomStr()}`;
+  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: PostFormState = {
     postForm: {
@@ -82,6 +87,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
     this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
     this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
 
+    this.tribute = setupTribute();
     this.state = this.emptyState;
 
     if (this.props.post) {
@@ -126,7 +132,14 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
   }
 
   componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
+    var textarea: any = document.getElementById(this.id);
+    autosize(textarea);
+    this.tribute.attach(textarea);
+    textarea.addEventListener('tribute-replaced', () => {
+      this.state.postForm.body = textarea.value;
+      this.setState(this.state);
+      autosize.update(textarea);
+    });
   }
 
   componentWillUnmount() {
@@ -238,6 +251,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
             </label>
             <div class="col-sm-10">
               <textarea
+                id={this.id}
                 value={this.state.postForm.body}
                 onInput={linkEvent(this, this.handlePostBodyChange)}
                 className={`form-control ${this.state.previewMode && 'd-none'}`}
index 170c2ab8ea29d7fcbed2848ec599e33f7cdbffbd..c8627845d00ae13ac6f71d62a463c5ee7156cbbd 100644 (file)
@@ -24,7 +24,10 @@ import {
   pictshareAvatarThumbnail,
   wsJsonToRes,
   toast,
+  randomStr,
+  setupTribute,
 } from '../utils';
+import Tribute from 'tributejs/src/Tribute.js';
 import autosize from 'autosize';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -49,6 +52,8 @@ export class PrivateMessageForm extends Component<
   PrivateMessageFormProps,
   PrivateMessageFormState
 > {
+  private id = `message-form-${randomStr()}`;
+  private tribute: Tribute;
   private subscription: Subscription;
   private emptyState: PrivateMessageFormState = {
     privateMessageForm: {
@@ -64,6 +69,7 @@ export class PrivateMessageForm extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
+    this.tribute = setupTribute();
     this.state = this.emptyState;
 
     if (this.props.privateMessage) {
@@ -93,7 +99,14 @@ export class PrivateMessageForm extends Component<
   }
 
   componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
+    var textarea: any = document.getElementById(this.id);
+    autosize(textarea);
+    this.tribute.attach(textarea);
+    textarea.addEventListener('tribute-replaced', () => {
+      this.state.privateMessageForm.content = textarea.value;
+      this.setState(this.state);
+      autosize.update(textarea);
+    });
   }
 
   componentWillUnmount() {
@@ -136,6 +149,7 @@ export class PrivateMessageForm extends Component<
             <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
             <div class="col-sm-10">
               <textarea
+                id={this.id}
                 value={this.state.privateMessageForm.content}
                 onInput={linkEvent(this, this.handleContentChange)}
                 className={`form-control ${this.state.previewMode && 'd-none'}`}
index 5d8ff0b59be5de93c4f73f89abf49a4643c0fd29..936a9d53ebc1c4894f203b6155daa17b410ae770 100644 (file)
@@ -1,8 +1,9 @@
 import { Component, linkEvent } from 'inferno';
 import { Site, SiteForm as SiteFormI } from '../interfaces';
 import { WebSocketService } from '../services';
-import { capitalizeFirstLetter } from '../utils';
+import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
 import autosize from 'autosize';
+import Tribute from 'tributejs/src/Tribute.js';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -17,6 +18,8 @@ interface SiteFormState {
 }
 
 export class SiteForm extends Component<SiteFormProps, SiteFormState> {
+  private id = `site-form-${randomStr()}`;
+  private tribute: Tribute;
   private emptyState: SiteFormState = {
     siteForm: {
       enable_downvotes: true,
@@ -29,7 +32,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
 
   constructor(props: any, context: any) {
     super(props, context);
+
+    this.tribute = setupTribute();
     this.state = this.emptyState;
+
     if (this.props.site) {
       this.state.siteForm = {
         name: this.props.site.name,
@@ -42,7 +48,14 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
   }
 
   componentDidMount() {
-    autosize(document.querySelectorAll('textarea'));
+    var textarea: any = document.getElementById(this.id);
+    autosize(textarea);
+    this.tribute.attach(textarea);
+    textarea.addEventListener('tribute-replaced', () => {
+      this.state.siteForm.description = textarea.value;
+      this.setState(this.state);
+      autosize.update(textarea);
+    });
   }
 
   render() {
@@ -75,6 +88,7 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
           </label>
           <div class="col-12">
             <textarea
+              id={this.id}
               value={this.state.siteForm.description}
               onInput={linkEvent(this, this.handleSiteDescriptionChange)}
               class="form-control"
index 0e4cd9d55e076c7f31edbd78b5e1dda75e4b8978..6a0ffdb3e3d12368ecb1dead58c9b66a6f22ee2e 100644 (file)
@@ -18,13 +18,17 @@ import {
   SearchType,
   WebSocketResponse,
   WebSocketJsonResponse,
+  SearchForm,
+  SearchResponse,
 } from './interfaces';
-import { UserService } from './services/UserService';
+import { UserService, WebSocketService } from './services';
+
+import Tribute from 'tributejs/src/Tribute.js';
 import markdown_it from 'markdown-it';
 import markdownitEmoji from 'markdown-it-emoji/light';
 import markdown_it_container from 'markdown-it-container';
-import * as twemoji from 'twemoji';
-import * as emojiShortName from 'emoji-short-name';
+import twemoji from 'twemoji';
+import emojiShortName from 'emoji-short-name';
 import Toastify from 'toastify-js';
 
 export const repoUrl = 'https://github.com/dessalines/lemmy';
@@ -33,7 +37,7 @@ export const archiveUrl = 'https://archive.is';
 
 export const postRefetchSeconds: number = 60 * 1000;
 export const fetchLimit: number = 20;
-export const mentionDropdownFetchLimit = 6;
+export const mentionDropdownFetchLimit = 10;
 
 export function randomStr() {
   return Math.random()
@@ -380,3 +384,118 @@ export function toast(text: string, background: string = 'success') {
     backgroundColor: backgroundColor,
   }).showToast();
 }
+
+export function setupTribute(): Tribute {
+  return new Tribute({
+    collection: [
+      // Emojis
+      {
+        trigger: ':',
+        menuItemTemplate: (item: any) => {
+          let emoji = `:${item.original.key}:`;
+          return `${item.original.val} ${emoji}`;
+        },
+        selectTemplate: (item: any) => {
+          return `:${item.original.key}:`;
+        },
+        values: Object.entries(emojiShortName).map(e => {
+          return { key: e[1], val: e[0] };
+        }),
+        allowSpaces: false,
+        autocompleteMode: true,
+        menuItemLimit: mentionDropdownFetchLimit,
+      },
+      // Users
+      {
+        trigger: '@',
+        selectTemplate: (item: any) => {
+          return `[/u/${item.original.key}](/u/${item.original.key})`;
+        },
+        values: (text: string, cb: any) => {
+          userSearch(text, (users: any) => cb(users));
+        },
+        allowSpaces: false,
+        autocompleteMode: true,
+        menuItemLimit: mentionDropdownFetchLimit,
+      },
+
+      // Communities
+      {
+        trigger: '#',
+        selectTemplate: (item: any) => {
+          return `[/c/${item.original.key}](/c/${item.original.key})`;
+        },
+        values: (text: string, cb: any) => {
+          communitySearch(text, (communities: any) => cb(communities));
+        },
+        allowSpaces: false,
+        autocompleteMode: true,
+        menuItemLimit: mentionDropdownFetchLimit,
+      },
+    ],
+  });
+}
+
+function userSearch(text: string, cb: any) {
+  if (text) {
+    let form: SearchForm = {
+      q: text,
+      type_: SearchType[SearchType.Users],
+      sort: SortType[SortType.TopAll],
+      page: 1,
+      limit: mentionDropdownFetchLimit,
+    };
+
+    WebSocketService.Instance.search(form);
+
+    this.userSub = WebSocketService.Instance.subject.subscribe(
+      msg => {
+        let res = wsJsonToRes(msg);
+        if (res.op == UserOperation.Search) {
+          let data = res.data as SearchResponse;
+          let users = data.users.map(u => {
+            return { key: u.name };
+          });
+          cb(users);
+          this.userSub.unsubscribe();
+        }
+      },
+      err => console.error(err),
+      () => console.log('complete')
+    );
+  } else {
+    cb([]);
+  }
+}
+
+function communitySearch(text: string, cb: any) {
+  if (text) {
+    let form: SearchForm = {
+      q: text,
+      type_: SearchType[SearchType.Communities],
+      sort: SortType[SortType.TopAll],
+      page: 1,
+      limit: mentionDropdownFetchLimit,
+    };
+
+    WebSocketService.Instance.search(form);
+
+    this.communitySub = WebSocketService.Instance.subject.subscribe(
+      msg => {
+        let res = wsJsonToRes(msg);
+        if (res.op == UserOperation.Search) {
+          let data = res.data as SearchResponse;
+          let communities = data.communities.map(u => {
+            return { key: u.name };
+          });
+          cb(communities);
+          this.communitySub.unsubscribe();
+        }
+      },
+      err => console.error(err),
+      () => console.log('complete')
+    );
+  } else {
+    cb([]);
+  }
+}