Search typeahead initial design
This commit is contained in:
parent
36f2981f7d
commit
f409f0c3b9
|
@ -9,7 +9,7 @@ import 'focus-visible'
|
||||||
import { AppRoutingModule } from './app-routing.module'
|
import { AppRoutingModule } from './app-routing.module'
|
||||||
import { AppComponent } from './app.component'
|
import { AppComponent } from './app.component'
|
||||||
import { CoreModule } from './core'
|
import { CoreModule } from './core'
|
||||||
import { HeaderComponent } from './header'
|
import { HeaderComponent, SearchTypeaheadComponent } from './header'
|
||||||
import { LoginModule } from './login'
|
import { LoginModule } from './login'
|
||||||
import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
|
import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu'
|
||||||
import { SharedModule } from './shared'
|
import { SharedModule } from './shared'
|
||||||
|
@ -41,6 +41,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
|
||||||
LanguageChooserComponent,
|
LanguageChooserComponent,
|
||||||
AvatarNotificationComponent,
|
AvatarNotificationComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
|
SearchTypeaheadComponent,
|
||||||
|
|
||||||
WelcomeModalComponent,
|
WelcomeModalComponent,
|
||||||
InstanceConfigWarningModalComponent
|
InstanceConfigWarningModalComponent
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<input
|
<my-search-typeahead>
|
||||||
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
|
<input
|
||||||
[(ngModel)]="searchValue" (keyup.enter)="doSearch()"
|
type="text" id="search-video" name="search-video" [attr.aria-label]="ariaLabelTextForSearch" i18n-placeholder placeholder="Search videos, channels…"
|
||||||
>
|
[(ngModel)]="searchValue" (keyup.enter)="doSearch()"
|
||||||
<span (click)="doSearch()" class="icon icon-search"></span>
|
>
|
||||||
|
<span (click)="doSearch()" class="icon icon-search"></span>
|
||||||
|
</my-search-typeahead>
|
||||||
|
|
||||||
<a class="upload-button" routerLink="/videos/upload">
|
<a class="upload-button" routerLink="/videos/upload">
|
||||||
<my-global-icon iconName="upload"></my-global-icon>
|
<my-global-icon iconName="upload"></my-global-icon>
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
@import '_variables';
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
|
my-search-typeahead {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
#search-video {
|
#search-video {
|
||||||
@include peertube-input-text($search-input-width);
|
@include peertube-input-text($search-input-width);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin-right: 15px;
|
|
||||||
padding-right: 40px; // For the search icon
|
padding-right: 40px; // For the search icon
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
transition: box-shadow .3s ease;
|
|
||||||
|
|
||||||
/* light border style */
|
|
||||||
border: 1px solid var(--mainBackgroundColor);
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--inputPlaceholderColor);
|
color: var(--inputPlaceholderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus::placeholder {
|
|
||||||
opacity: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
width: calc(100% - 150px);
|
width: calc(100% - 150px);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +33,7 @@
|
||||||
|
|
||||||
// yolo
|
// yolo
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: -50px;
|
margin-left: -35px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from './header.component'
|
export * from './header.component'
|
||||||
|
export * from './search-typeahead.component'
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
<div class="d-inline-flex position-relative" id="typeahead-container" #contentWrapper>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
|
||||||
|
<div class="position-absolute jump-to-suggestions">
|
||||||
|
<!-- suggestions -->
|
||||||
|
<ul id="jump-to-results" role="listbox" class="p-0 m-0" #optionsList>
|
||||||
|
<li *ngFor="let res of results" class="d-flex flex-justify-start flex-items-center p-0 f5" role="option" aria-selected="true">
|
||||||
|
<ng-container *ngTemplateOutlet="result; context: {$implicit: res}"></ng-container>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- search instructions, when search input is empty -->
|
||||||
|
<div *ngIf="!hasSearch" 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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<em>@username@domain</em> <span class="flex-auto text-muted" i18n>account or channel</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<em>URL</em> <span class="text-muted" i18n>account or channel</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<em>URL</em> <span class="text-muted" i18n>video</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<span class="text-muted" i18n>Any other text will return matching video, channel or account names.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #result let-result>
|
||||||
|
<a tabindex="0" class="d-flex flex-auto flex-items-center p-2"
|
||||||
|
data-target-type="Repository"
|
||||||
|
[routerLink]="result.routerLink"
|
||||||
|
>
|
||||||
|
<div class="flex-shrink-0 mr-2 text-center">
|
||||||
|
<my-global-icon *ngIf="result.type !== 'channel'" iconName="search"></my-global-icon>
|
||||||
|
<my-global-icon *ngIf="result.type === 'channel'" iconName="folder"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img class="avatar mr-2 flex-shrink-0 d-none" alt="" aria-label="Team" src="" width="28" height="28">
|
||||||
|
|
||||||
|
<div class="flex-auto overflow-hidden text-left no-wrap css-truncate css-truncate-target" [attr.aria-label]="result.text" [innerHTML]="result.text | highlight : searchInput.value"></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-global'" [attr.aria-label]="inAllText">
|
||||||
|
{{ inAllText }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="result.type === 'search-any'" aria-hidden="true" class="d-inline-block ml-1 v-align-middle">↵</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="result.type === 'channel'" aria-hidden="true" class="border rounded flex-shrink-0 px-1 bg-gray text-gray-light ml-1 f6 d-on-nav-focus" i18n>
|
||||||
|
Jump to channel
|
||||||
|
<span class="d-inline-block ml-1 v-align-middle">↵</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,121 @@
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
.jump-to-suggestions {
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 35;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#typeahead-instructions,
|
||||||
|
#jump-to-results {
|
||||||
|
border: 1px solid var(--mainBackgroundColor);
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
background: var(--mainBackgroundColor);
|
||||||
|
transition: .3s ease;
|
||||||
|
transition-property: box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
#typeahead-instructions {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
em {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#typeahead-container {
|
||||||
|
::ng-deep input {
|
||||||
|
border: 1px solid var(--mainBackgroundColor) !important;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 20px 0px;
|
||||||
|
flex-grow: 1;
|
||||||
|
transition: box-shadow .3s ease, width .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep span {
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div:last-child {
|
||||||
|
// we have to switch the display and not the opacity,
|
||||||
|
// to avoid clashing with the rest of the interface.
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
::ng-deep &:focus-within {
|
||||||
|
& > div:last-child {
|
||||||
|
display: initial !important;
|
||||||
|
|
||||||
|
#typeahead-instructions,
|
||||||
|
#jump-to-results {
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 10px 20px -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep input {
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 20px 0px;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
|
||||||
|
@media screen and (min-width: 900px) {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.focus-visible {
|
||||||
|
background-color: var(--mainHoverColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@include disable-default-a-behaviour;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&, &:hover {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray {
|
||||||
|
background-color: var(--mainBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-light {
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon {
|
||||||
|
top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-search-status {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-title {
|
||||||
|
@include in-content-small-title;
|
||||||
|
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 17px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
@include apply-svg-color(var(--mainForegroundColor))
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'
|
||||||
|
import { Router, NavigationEnd } from '@angular/router'
|
||||||
|
import { AuthService } from '@app/core'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { filter } from 'rxjs/operators'
|
||||||
|
import { ListKeyManager, ListKeyManagerOption } from '@angular/cdk/a11y'
|
||||||
|
import { UP_ARROW, DOWN_ARROW, ENTER } from '@angular/cdk/keycodes'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-search-typeahead',
|
||||||
|
templateUrl: './search-typeahead.component.html',
|
||||||
|
styleUrls: [ './search-typeahead.component.scss' ]
|
||||||
|
})
|
||||||
|
export class SearchTypeaheadComponent implements OnInit, AfterViewInit {
|
||||||
|
@ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef
|
||||||
|
@ViewChild('optionsList', { static: true }) optionsList: ElementRef
|
||||||
|
|
||||||
|
hasChannel = false
|
||||||
|
inChannel = false
|
||||||
|
keyboardEventsManager: ListKeyManager<ListKeyManagerOption>
|
||||||
|
|
||||||
|
searchInput: HTMLInputElement
|
||||||
|
URIPolicy: 'only-followed' | 'any' = 'any'
|
||||||
|
|
||||||
|
URIPolicyText: string
|
||||||
|
inAllText: string
|
||||||
|
inThisChannelText: string
|
||||||
|
|
||||||
|
results: any[] = []
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router,
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
this.URIPolicyText = this.i18n('Determines whether you can resolve any distant content from its URL, 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')
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.router.events
|
||||||
|
.pipe(filter(event => event instanceof NavigationEnd))
|
||||||
|
.subscribe((event: NavigationEnd) => {
|
||||||
|
this.hasChannel = event.url.startsWith('/videos/watch')
|
||||||
|
this.inChannel = event.url.startsWith('/video-channels')
|
||||||
|
this.computeResults()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
this.searchInput = this.contentWrapper.nativeElement.childNodes[0]
|
||||||
|
this.searchInput.addEventListener('input', this.computeResults.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasSearch () {
|
||||||
|
return !!this.searchInput && !!this.searchInput.value
|
||||||
|
}
|
||||||
|
|
||||||
|
computeResults () {
|
||||||
|
let results = [
|
||||||
|
{
|
||||||
|
text: 'Maître poney',
|
||||||
|
type: 'channel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.hasSearch) {
|
||||||
|
results = [
|
||||||
|
{
|
||||||
|
text: this.searchInput.value,
|
||||||
|
type: 'search-channel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.searchInput.value,
|
||||||
|
type: 'search-global'
|
||||||
|
},
|
||||||
|
...results
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.results = results.filter(
|
||||||
|
result => {
|
||||||
|
// 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')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
isUserLoggedIn () {
|
||||||
|
return this.authService.isLoggedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp (event: KeyboardEvent) {
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
if (this.keyboardEventsManager) {
|
||||||
|
if (event.keyCode === DOWN_ARROW || event.keyCode === UP_ARROW) {
|
||||||
|
// passing the event to key manager so we get a change fired
|
||||||
|
this.keyboardEventsManager.onKeydown(event)
|
||||||
|
return false
|
||||||
|
} else if (event.keyCode === ENTER) {
|
||||||
|
// when we hit enter, the keyboardManager should call the selectItem method of the `ListItemComponent`
|
||||||
|
// this.keyboardEventsManager.activeItem
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { PipeTransform, Pipe } from '@angular/core'
|
||||||
|
import { SafeHtml } from '@angular/platform-browser'
|
||||||
|
|
||||||
|
// Thanks https://gist.github.com/adamrecsko/0f28f474eca63e0279455476cc11eca7#gistcomment-2917369
|
||||||
|
@Pipe({ name: 'highlight' })
|
||||||
|
export class HighlightPipe implements PipeTransform {
|
||||||
|
/* use this for single match search */
|
||||||
|
static SINGLE_MATCH: string = "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"
|
||||||
|
/* use this for global search */
|
||||||
|
static MULTI_MATCH: string = "Multi-Match"
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
transform(
|
||||||
|
contentString: string = null,
|
||||||
|
stringToHighlight: string = null,
|
||||||
|
option: string = "Single-And-StartsWith-Match",
|
||||||
|
caseSensitive: boolean = false,
|
||||||
|
highlightStyleName: string = "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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,6 +89,7 @@ import { NumberFormatterPipe } from '@app/shared/angular/number-formatter.pipe'
|
||||||
import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
|
import { VideoDurationPipe } from '@app/shared/angular/video-duration-formatter.pipe'
|
||||||
import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
|
import { ObjectLengthPipe } from '@app/shared/angular/object-length.pipe'
|
||||||
import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
|
import { FromNowPipe } from '@app/shared/angular/from-now.pipe'
|
||||||
|
import { HighlightPipe }from '@app/shared/angular/highlight.pipe'
|
||||||
import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
|
import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
|
||||||
import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
|
import { VideoActionsDropdownComponent } from '@app/shared/video/video-actions-dropdown.component'
|
||||||
import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
|
import { VideoBlacklistComponent } from '@app/shared/video/modals/video-blacklist.component'
|
||||||
|
@ -149,6 +150,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
ObjectLengthPipe,
|
ObjectLengthPipe,
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
|
HighlightPipe,
|
||||||
PeerTubeTemplateDirective,
|
PeerTubeTemplateDirective,
|
||||||
VideoDurationPipe,
|
VideoDurationPipe,
|
||||||
|
|
||||||
|
@ -254,6 +256,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
ObjectLengthPipe,
|
ObjectLengthPipe,
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
|
HighlightPipe,
|
||||||
PeerTubeTemplateDirective,
|
PeerTubeTemplateDirective,
|
||||||
VideoDurationPipe
|
VideoDurationPipe
|
||||||
],
|
],
|
||||||
|
|
|
@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
|
||||||
animation: spin .7s infinite linear;
|
animation: spin .7s infinite linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-auto {
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from {
|
from {
|
||||||
transform: scale(1) rotate(0deg);
|
transform: scale(1) rotate(0deg);
|
||||||
|
|
|
@ -11,11 +11,6 @@
|
||||||
&:focus:not(.focus-visible) {
|
&:focus:not(.focus-visible) {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
padding: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue