Improve search typeahead performance and use native events
authorRigel Kent <sendmemail@rigelk.eu>
Tue, 4 Feb 2020 15:44:53 +0000 (16:44 +0100)
committerRigel Kent <sendmemail@rigelk.eu>
Thu, 13 Feb 2020 15:35:24 +0000 (16:35 +0100)
19 files changed:
client/src/app/+accounts/accounts.component.html
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.scss
client/src/app/header/header.component.html
client/src/app/header/header.component.scss
client/src/app/header/header.component.ts
client/src/app/header/search-typeahead.component.html
client/src/app/header/search-typeahead.component.scss
client/src/app/header/search-typeahead.component.ts
client/src/app/header/suggestion.component.html
client/src/app/header/suggestion.component.ts
client/src/app/header/suggestions.component.html [new file with mode: 0644]
client/src/app/header/suggestions.component.ts
client/src/app/shared/angular/highlight.pipe.ts
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/include/_bootstrap-variables.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss

index b982fba9a9e299fe755eafa50e2885cbc338ff07..6a76393b91b9c72aca28ddecca3e5ce24fdb08c0 100644 (file)
@@ -7,14 +7,13 @@
       <div class="actor-info">
         <div class="actor-names">
           <div class="actor-display-name">{{ account.displayName }}</div>
-          <div class="actor-name">{{ account.nameWithHost }}
-
-          <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
-                  class="btn btn-outline-secondary btn-sm copy-button"
-          >
-            <span class="glyphicon glyphicon-copy"></span>
-          </button>
-
+          <div class="actor-name">
+            <span>{{ account.nameWithHost }}</span>
+            <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
+                    class="btn btn-outline-secondary btn-sm copy-button"
+            >
+              <span class="glyphicon glyphicon-copy"></span>
+            </button>
           </div>
           <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
           <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
index 735a8f2c8f558dce804703e1d4eba58ab01fa484..1087de113aba42014184af8729ad75e889219861 100644 (file)
@@ -7,25 +7,29 @@
       <div class="actor-info">
         <div class="actor-names">
           <div class="actor-display-name">{{ videoChannel.displayName }}</div>
-          <div class="actor-name">{{ videoChannel.nameWithHost }}
+          <div class="actor-name">
+            <span>{{ videoChannel.nameWithHost }}</span>
             <button [cdkCopyToClipboard]="videoChannel.nameWithHost" (click)="activateCopiedMessage()"
                     class="btn btn-outline-secondary btn-sm copy-button"
             >
               <span class="glyphicon glyphicon-copy"></span>
             </button>
           </div>
+        </div>
 
-          <div class="right-buttons">
-            <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
-            <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
-          </div>
+        <div class="right-buttons">
+          <a *ngIf="isChannelManageable" [routerLink]="[ '/my-account/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n>Manage</a>
+          <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
         </div>
-        <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
 
-        <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
-          <span i18n>Created by {{ videoChannel.ownerBy }}</span>
-          <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
-        </a>
+        <div class="actor-lower">
+          <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
+  
+          <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
+            <span i18n>Created by {{ videoChannel.ownerBy }}</span>
+            <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
+          </a>
+        </div>
       </div>
     </div>
 
index 50b69e7acf7e1489d523b3904db63aa57a125a4c..aa26a7e7b7fc4b977e3d09a8b46131b349c46f5b 100644 (file)
@@ -8,6 +8,23 @@
     width: 100%;
   }
 
+  .actor-info {
+    display: grid !important;
+    grid-template-columns: 1fr auto;
+    grid-template-rows: 1fr auto / 1fr auto;
+    grid-template-areas: "name buttons"
+                         "lower buttons";
+
+    @media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
+      grid-template-areas: "name name"
+                           "lower buttons";
+    }
+  }
+
+  .actor-names {
+    grid-area: name;
+  }
+
   .actor-name {
     flex-grow: 1;
 
@@ -25,6 +42,9 @@
   margin-left: auto;
   margin-top: 20px;
 
+  grid-row: buttons-start / span buttons-end;
+  grid-column: buttons-start;
+
   a {
     @include peertube-button-outline;
     line-height: 1.8;
index 8e1d24ea87b005c500ef3a5517f8115fb96e8663..49e219187405c398ee903cd12036604955e9a88c 100644 (file)
@@ -1,10 +1,4 @@
-<my-search-typeahead class="w-100 d-flex justify-content-end">
-  <input
-    type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch"
-    i18n-placeholder placeholder="Search videos, channels…" [(ngModel)]="searchValue"
-  >
-  <span class="icon icon-search"></span>
-</my-search-typeahead>
+<my-search-typeahead class="w-100 d-flex justify-content-end"></my-search-typeahead>
 
 <a class="upload-button" routerLink="/videos/upload">
   <my-global-icon iconName="upload"></my-global-icon>
index 2f0a407fc9cd022440a363d78a8c0d9a559c18c6..91b39077367ba2b638ea0c2de687a21a824afdff 100644 (file)
@@ -5,30 +5,6 @@ my-search-typeahead {
   margin-right: 15px;
 }
 
-#search-video {
-  @include peertube-input-text($search-input-width);
-  padding-left: 10px;
-  padding-right: 40px; // For the search icon
-  font-size: 14px;
-
-  &::placeholder {
-    color: var(--inputPlaceholderColor);
-  }
-}
-
-.icon.icon-search {
-  @include icon(25px);
-  height: 21px;
-
-  background-color: var(--mainForegroundColor);
-  mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
-
-  // yolo
-  position: absolute;
-  margin-left: -35px;
-  margin-top: 5px;
-}
-
 .upload-button {
   @include peertube-button-link;
   @include orange-button;
index d9311c554a0934dd6d34e10f0461f99848cc2f6a..cce76b0d1272c82eb72ae8349b9835d1ea4c693b 100644 (file)
@@ -1,5 +1,4 @@
-import { Component, OnInit } from '@angular/core'
-import { I18n } from '@ngx-translate/i18n-polyfill'
+import { Component } from '@angular/core'
 
 @Component({
   selector: 'my-header',
@@ -7,15 +6,4 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './header.component.scss' ]
 })
 
-export class HeaderComponent implements OnInit {
-  searchValue = ''
-  ariaLabelTextForSearch = ''
-
-  constructor (
-    private i18n: I18n
-  ) {}
-
-  ngOnInit () {
-    this.ariaLabelTextForSearch = this.i18n('Search videos, channels')
-  }
-}
+export class HeaderComponent {}
index 2623ba33734968e80252ce6897c5703a8fae4121..428246585f536c0e91ff46d86d5487eefca74b8a 100644 (file)
@@ -1,9 +1,13 @@
-<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper>
-  <ng-content></ng-content>
+<div class="d-inline-flex position-relative" id="typeahead-container">
+  <input
+    type="text" id="search-video" name="search-video" #searchVideo i18n-placeholder placeholder="Search videos, channels…"
+    [(ngModel)]="search" (ngModelChange)="onSearchChange()" (keyup)="handleKeyUp($event)"
+  >
+  <span class="icon icon-search" (click)="doSearch()"></span>
 
   <div class="position-absolute jump-to-suggestions">
     <!-- suggestions -->
-    <my-suggestions *ngIf="hasSearch && newSearch" [results]="results" [highlight]="searchInput.value" (init)="initKeyboardEventsManager($event)" #suggestions></my-suggestions>
+    <my-suggestions *ngIf="search && newSearch" [results]="results" [highlight]="search" (init)="initKeyboardEventsManager($event)"></my-suggestions>
 
     <!-- suggestion help, not shown until one of the suggestions is selected and specific to that suggestion -->
     <div *ngIf="showHelp" id="typeahead-help" class="overflow-hidden">
@@ -11,7 +15,7 @@
         <div class="d-flex justify-content-between">
           <label class="small-title" i18n>Global search</label>
           <div class="advanced-search-status text-muted">
-            <span class="mr-1" i18n>using {{ globalSearchIndex }}</span>
+            <span *ngIf="serverConfig" class="mr-1" i18n>using {{ serverConfig.followings.instance.autoFollowIndex.indexUrl }}</span>
             <i class="glyphicon glyphicon-globe"></i>
           </div>
         </div>
     </div>
 
     <!-- search instructions, when search input is empty -->
-    <div *ngIf="!hasSearch" id="typeahead-instructions" class="overflow-hidden">
+    <div *ngIf="showInstructions" id="typeahead-instructions" class="overflow-hidden">
       <div class="d-flex justify-content-between">
         <label class="small-title" i18n>Advanced search</label>
         <div class="advanced-search-status c-help">
-          <span [ngClass]="URIPolicy === 'any' ? 'text-success' : 'text-muted'" [title]="URIPolicyText">
-            <span class="mr-1" i18n>{URIPolicy, select, only-followed {only followed instances} other {any instance}} </span>
-            <i [ngClass]="URIPolicy === 'any' ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
+          <span [ngClass]="anyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
+            <span *ngIf="anyURI" class="mr-1" i18n>any instance</span>
+            <span *ngIf="!anyURI" class="mr-1" i18n>only followed instances</span>
+            <i [ngClass]="anyURI ? 'glyphicon glyphicon-ok-sign' : 'glyphicon glyphicon-exclamation-sign'"></i>
           </span>
         </div>
       </div>
index 6d7511c70070f9847cf9bb904cbb8be9b97b5cbf..a55e78326694e666ced390d422bb04ac2a0e5101 100644 (file)
@@ -3,6 +3,30 @@
 @import '_bootstrap-variables';
 @import '~bootstrap/scss/mixins/_breakpoints';
 
+#search-video {
+  @include peertube-input-text($search-input-width);
+  padding-left: 10px;
+  padding-right: 40px; // For the search icon
+  font-size: 14px;
+
+  &::placeholder {
+    color: var(--inputPlaceholderColor);
+  }
+}
+
+.icon.icon-search {
+  @include icon(25px);
+  height: 21px;
+
+  background-color: var(--mainForegroundColor);
+  mask: url('../../assets/images/header/search.svg') no-repeat 50% 50%;
+
+  // yolo
+  position: absolute;
+  margin-left: -35px;
+  margin-top: 5px;
+}
+
 .jump-to-suggestions {
   top: 100%;
   left: 0;
@@ -42,7 +66,7 @@ my-suggestions ::ng-deep ul {
 }
 
 #typeahead-container {
-  ::ng-deep input {
+  input {
     border: 1px solid var(--mainBackgroundColor) !important;
     box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
     flex-grow: 1;
@@ -56,12 +80,12 @@ my-suggestions ::ng-deep ul {
   @media screen and (max-width: $small-view) {
     flex: 1;
 
-    ::ng-deep input {
+    input {
       width: unset;
     }
   }
 
-  ::ng-deep span {
+  span {
     right: 10px;
   }
 
index 9b414bc568b3496333f206a67e86c4325ea12238..c265f2c83d16777777621a8ba3205adffbed7471 100644 (file)
@@ -1,16 +1,15 @@
 import {
   Component,
-  ViewChild,
-  ElementRef,
   AfterViewInit,
   OnInit,
   OnDestroy,
-  QueryList
+  QueryList,
+  ViewChild,
+  ElementRef
 } from '@angular/core'
-import { Router, NavigationEnd, Params, ActivatedRoute } from '@angular/router'
+import { Router, Params, ActivatedRoute } from '@angular/router'
 import { AuthService, ServerService } from '@app/core'
-import { I18n } from '@ngx-translate/i18n-polyfill'
-import { filter, first, tap, map } from 'rxjs/operators'
+import { first, tap } from 'rxjs/operators'
 import { ListKeyManager } from '@angular/cdk/a11y'
 import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
 import { SuggestionComponent, Result } from './suggestion.component'
@@ -24,19 +23,16 @@ import { ServerConfig } from '@shared/models'
   styleUrls: [ './search-typeahead.component.scss' ]
 })
 export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewInit {
-  @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
+  @ViewChild('searchVideo', { static: true }) searchInput: ElementRef<HTMLInputElement>
 
   hasChannel = false
   inChannel = false
   newSearch = true
 
-  searchInput: HTMLInputElement
+  search = ''
   serverConfig: ServerConfig
 
-  URIPolicyText: string
-  inAllText: string
   inThisChannelText: string
-  globalSearchIndex = 'https://index.joinpeertube.org'
 
   keyboardEventsManager: ListKeyManager<SuggestionComponent>
   results: any[] = []
@@ -45,30 +41,10 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
     private authService: AuthService,
     private router: Router,
     private route: ActivatedRoute,
-    private serverService: ServerService,
-    private i18n: I18n
-  ) {
-    this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content, or if your instance only allows doing so for instances it follows.')
-    this.inAllText = this.i18n('In all PeerTube')
-    this.inThisChannelText = this.i18n('In this channel')
-  }
+    private serverService: ServerService
+  ) {}
 
   ngOnInit () {
-    this.router.events
-      .pipe(filter(e => e instanceof NavigationEnd))
-      .subscribe((event: NavigationEnd) => {
-        this.hasChannel = event.url.startsWith('/videos/watch')
-        this.inChannel = event.url.startsWith('/video-channels')
-        this.computeResults()
-      })
-
-    this.router.events
-      .pipe(
-        filter(e => e instanceof NavigationEnd),
-        map(() => getParameterByName('search', window.location.href))
-      )
-      .subscribe(searchQuery => this.searchInput.value = searchQuery || '')
-
     this.serverService.getConfig()
       .subscribe(config => this.serverConfig = config)
   }
@@ -78,53 +54,52 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
   }
 
   ngAfterViewInit () {
-    this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
-    this.searchInput.addEventListener('input', this.computeResults.bind(this))
-    this.searchInput.addEventListener('keyup', this.handleKeyUp.bind(this))
-  }
-
-  get hasSearch () {
-    return !!this.searchInput && !!this.searchInput.value
+    this.search = getParameterByName('search', window.location.href) || ''
   }
 
   get activeResult () {
     return this.keyboardEventsManager && this.keyboardEventsManager.activeItem && this.keyboardEventsManager.activeItem.result
   }
 
+  get showInstructions () {
+    return !this.search
+  }
+
   get showHelp () {
-    return this.hasSearch && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
+    return this.search && this.newSearch && this.activeResult && this.activeResult.type === 'search-global' || false
   }
 
-  get URIPolicy (): 'only-followed' | 'any' {
-    return (
-      this.authService.isLoggedIn()
-        ? this.serverConfig.search.remoteUri.users
-        : this.serverConfig.search.remoteUri.anonymous
-    )
-      ? 'any'
-      : 'only-followed'
+  get anyURI () {
+    if (!this.serverConfig) return false
+    return this.authService.isLoggedIn()
+      ? this.serverConfig.search.remoteUri.users
+      : this.serverConfig.search.remoteUri.anonymous
+  }
+
+  onSearchChange () {
+    this.computeResults()
   }
 
   computeResults () {
     this.newSearch = true
     let results: Result[] = []
 
-    if (this.hasSearch) {
+    if (this.search) {
       results = [
         /* Channel search is still unimplemented. Uncomment when it is.
         {
-          text: this.searchInput.value,
+          text: this.search,
           type: 'search-channel'
         },
         */
         {
-          text: this.searchInput.value,
+          text: this.search,
           type: 'search-instance',
           default: true
         },
         /* Global search is still unimplemented. Uncomment when it is.
         {
-          text: this.searchInput.value,
+          text: this.search,
           type: 'search-global'
         },
         */
@@ -137,7 +112,8 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
         // if we're not in a channel or one of its videos/playlits, show all channel-related results
         if (!(this.hasChannel || this.inChannel)) return !result.type.includes('channel')
         // if we're in a channel, show all channel-related results except for the channel redirection itself
-        if (this.inChannel) return !(result.type === 'channel')
+        if (this.inChannel) return result.type !== 'channel'
+        // all other result types are kept
         return true
       }
     )
@@ -187,7 +163,7 @@ export class SearchTypeaheadComponent implements OnInit, OnDestroy, AfterViewIni
       Object.assign(queryParams, this.route.snapshot.queryParams)
     }
 
-    Object.assign(queryParams, { search: this.searchInput.value })
+    Object.assign(queryParams, { search: this.search })
 
     const o = this.authService.isLoggedIn()
       ? this.loadUserLanguagesIfNeeded(queryParams)
index 894cacb95423632e899123af2b2c96cd9ae97c2a..edde2023afc377965ecdc4b858d212b4ec95ce72 100644 (file)
@@ -9,15 +9,9 @@
   <div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : highlight"></div>
 
   <div *ngIf="result.type !== 'channel' && result.type !== 'suggestion'" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6">
-    <span *ngIf="result.type === 'search-channel'" [attr.aria-label]="inThisChannelText">
-      {{ inThisChannelText }}
-    </span>
-    <span *ngIf="result.type === 'search-instance'" [attr.aria-label]="inThisInstanceText">
-      {{ inThisInstanceText }}
-    </span>
-    <span *ngIf="result.type === 'search-global'" [attr.aria-label]="inAllText">
-      {{ inAllText }}
-    </span>
+    <span *ngIf="result.type === 'search-channel'" i18n>In this channel</span>
+    <span *ngIf="result.type === 'search-instance'" i18n>In this instance</span>
+    <span *ngIf="result.type === 'search-global'" i18n>In the vidiverse</span>
     <span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
   </div>
 
index bdcb3e03feff8c898e83e14bec60b3c6af57db97..69641b511b9612f55c40fe3155bb980f301b2808 100644 (file)
@@ -1,6 +1,5 @@
-import { Input, Component, Output, EventEmitter, OnInit } from '@angular/core'
+import { Input, Component, Output, EventEmitter, OnInit, ChangeDetectionStrategy } from '@angular/core'
 import { RouterLink } from '@angular/router'
-import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ListKeyManagerOption } from '@angular/cdk/a11y'
 
 export type Result = {
@@ -13,28 +12,17 @@ export type Result = {
 @Component({
   selector: 'my-suggestion',
   templateUrl: './suggestion.component.html',
-  styleUrls: [ './suggestion.component.scss' ]
+  styleUrls: [ './suggestion.component.scss' ],
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class SuggestionComponent implements OnInit, ListKeyManagerOption {
   @Input() result: Result
   @Input() highlight: string
   @Output() selected = new EventEmitter()
 
-  inAllText: string
-  inThisChannelText: string
-  inThisInstanceText: string
-
   disabled = false
   active = false
 
-  constructor (
-    private i18n: I18n
-  ) {
-    this.inAllText = this.i18n('In the vidiverse')
-    this.inThisChannelText = this.i18n('In this channel')
-    this.inThisInstanceText = this.i18n('In this instance')
-  }
-
   getLabel () {
     return this.result.text
   }
diff --git a/client/src/app/header/suggestions.component.html b/client/src/app/header/suggestions.component.html
new file mode 100644 (file)
index 0000000..8d017d7
--- /dev/null
@@ -0,0 +1,6 @@
+<ul role="listbox" class="p-0 m-0">
+  <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
+      role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
+    <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
+  </li>
+</ul>
\ No newline at end of file
index fac7fe2f97b00fe5b80b37e510bef938a3ee4f75..ee3ef73c2c8f617e37c662b2a41f7b053571ff29 100644 (file)
@@ -1,16 +1,10 @@
-import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren } from '@angular/core'
+import { Input, QueryList, Component, Output, AfterViewInit, EventEmitter, ViewChildren, ChangeDetectionStrategy } from '@angular/core'
 import { SuggestionComponent } from './suggestion.component'
 
 @Component({
   selector: 'my-suggestions',
-  template: `
-    <ul role="listbox" class="p-0 m-0">
-      <li *ngFor="let result of results; let i = index" class="d-flex flex-justify-start flex-items-center p-0 f5"
-          role="option" aria-selected="true" (mouseenter)="hoverItem(i)">
-        <my-suggestion [result]="result" [highlight]="highlight"></my-suggestion>
-      </li>
-    </ul>
-  `
+  templateUrl: './suggestions.component.html',
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class SuggestionsComponent implements AfterViewInit {
   @Input() results: any[]
@@ -20,7 +14,7 @@ export class SuggestionsComponent implements AfterViewInit {
 
   ngAfterViewInit () {
     this.listItems.changes.subscribe(
-      val => this.init.emit({ items: this.listItems })
+      _ => this.init.emit({ items: this.listItems })
     )
   }
 
index 4199d833e0b58d8bd677c2bdd118c2e7e447ecfb..e219b3823a3e98b231ee40d688523bff4bfdb51f 100644 (file)
@@ -5,48 +5,50 @@ import { SafeHtml } from '@angular/platform-browser'
 @Pipe({ name: 'highlight' })
 export class HighlightPipe implements PipeTransform {
   /* use this for single match search */
-  static SINGLE_MATCH: string = "Single-Match"
+  static SINGLE_MATCH = 'Single-Match'
   /* use this for single match search with a restriction that target should start with search string */
-  static SINGLE_AND_STARTS_WITH_MATCH: string = "Single-And-StartsWith-Match"
+  static SINGLE_AND_STARTS_WITH_MATCH = 'Single-And-StartsWith-Match'
   /* use this for global search */
-  static MULTI_MATCH: string = "Multi-Match"
+  static MULTI_MATCH = 'Multi-Match'
 
-  constructor() {}
-  transform(
+  // tslint:disable-next-line:no-empty
+  constructor () {}
+
+  transform (
       contentString: string = null,
       stringToHighlight: string = null,
-      option: string = "Single-And-StartsWith-Match",
-      caseSensitive: boolean = false,
-      highlightStyleName: string = "search-highlight"
+      option = 'Single-And-StartsWith-Match',
+      caseSensitive = false,
+      highlightStyleName = 'search-highlight'
   ): SafeHtml {
-      if (stringToHighlight && contentString && option) {
-          let regex: any = ""
-          let caseFlag: string = !caseSensitive ? "i" : ""
-          switch (option) {
-              case "Single-Match": {
-                  regex = new RegExp(stringToHighlight, caseFlag)
-                  break
-              }
-              case "Single-And-StartsWith-Match": {
-                  regex = new RegExp("^" + stringToHighlight, caseFlag)
-                  break
-              }
-              case "Multi-Match": {
-                  regex = new RegExp(stringToHighlight, "g" + caseFlag)
-                  break
-              }
-              default: {
-                  // default will be a global case-insensitive match
-                  regex = new RegExp(stringToHighlight, "gi")
-              }
-          }
-          const replaced = contentString.replace(
-              regex,
-              (match) => `<span class="${highlightStyleName}">${match}</span>`
-          )
-          return replaced
-      } else {
-          return contentString
+    if (stringToHighlight && contentString && option) {
+      let regex: any = ''
+      const caseFlag: string = !caseSensitive ? 'i' : ''
+      switch (option) {
+        case 'Single-Match': {
+          regex = new RegExp(stringToHighlight, caseFlag)
+          break
+        }
+        case 'Single-And-StartsWith-Match': {
+          regex = new RegExp("^" + stringToHighlight, caseFlag)
+          break
+        }
+        case 'Multi-Match': {
+          regex = new RegExp(stringToHighlight, 'g' + caseFlag)
+          break
+        }
+        default: {
+          // default will be a global case-insensitive match
+          regex = new RegExp(stringToHighlight, 'gi')
+        }
       }
+      const replaced = contentString.replace(
+          regex,
+          (match) => `<span class="${highlightStyleName}">${match}</span>`
+      )
+      return replaced
+    } else {
+      return contentString
+    }
   }
 }
index 6bf3457895c4f1c9ff8b788cd43cabd59f699582..560414e90fa60200356f4afb2c4b2321adf8afd1 100644 (file)
@@ -1,5 +1,6 @@
 $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 
+@import '_bootstrap-variables';
 @import '_variables';
 @import '_mixins';
 
@@ -234,7 +235,7 @@ table {
   }
 }
 
-@media screen and (max-width: 1600px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, xxl)}) {
   .main-col {
     &.expanded {
       .margin-content {
@@ -245,7 +246,7 @@ table {
   }
 }
 
-@media screen and (max-width: 900px) {
+@media screen and (max-width: #{map-get($grid-breakpoints, lg)}) {
   .main-col {
     &.expanded {
       .margin-content {
index be5879b506fda7242c0c656eb6cb3bc13fe9d009..e10b8459635dc69938cc01ad8dd448ec0f7d4018 100644 (file)
@@ -52,7 +52,7 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 
-@media screen and (min-width: 768px) {
+@media screen and (min-width: #{map-get($grid-breakpoints, md)}) {
   .modal:before {
     vertical-align: middle;
     content: " ";
index 7f413836b56388cf32d93582f55c4bcfa5a4c749..b3ab0eb2b6b3586f3be437727502dad14a88e22f 100644 (file)
@@ -13,8 +13,9 @@ $grid-breakpoints: (
   md: 768px,
   // Large screen / desktop
   lg: 900px,
-  // Extra large screen / wide desktop
-  xl: 1200px
+  // Extra large screens / wide desktops
+  xl: 1200px,
+  xxl: 1600px
 );
 
 $container-max-widths: (
index ed2cacdd1803ef5ca7ecd5d10ce3d60589816093..4766e449085f77657c8f51ec22d904387d9c48b7 100644 (file)
 @mixin actor-owner {
   @include disable-default-a-behaviour;
 
-  display: inline-table;
   font-size: 13px;
   margin-top: 4px;
   color: var(--mainForegroundColor);
       .actor-names {
         display: flex;
         align-items: center;
+        flex-wrap: wrap;
 
         .actor-display-name {
           font-size: 23px;
           font-weight: $font-bold;
+          margin-right: 7px;
         }
 
         .actor-name {
-          margin-left: 7px;
           position: relative;
           top: 3px;
           font-size: 14px;
         }
       }
 
+      .actor-lower {
+        grid-area: lower;
+      }
+
       .actor-followers {
         font-size: 15px;
       }
       margin-bottom: 0;
       text-transform: uppercase;
       font-weight: 600;
+      font-size: 110%;
+
+      @media screen and (max-width: $mobile-view) {
+        font-size: 130%;
+      }
     }
   }
 }
index d0e1a8c9caf2f32bda493977cf4e430fa9a26184..d8db3f3f84de1ca80a21be5b4fc275e501cb074b 100644 (file)
@@ -1,3 +1,5 @@
+@import '_bootstrap-variables';
+
 $small-view: 800px;
 $mobile-view: 500px;