"@angularclass/hmr": "^2.1.3",
"@neos21/bootstrap3-glyphicons": "^1.0.1",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
+ "@ng-select/ng-select": "^5.0.0",
"@ngx-i18nsupport/ngx-i18nsupport": "^1.1.6",
"@ngx-i18nsupport/tooling": "^8.0.3",
"@ngx-loading-bar/core": "^5.0.0",
"linkifyjs": "^2.1.5",
"lodash-es": "^4.17.4",
"markdown-it": "^11.0.0",
- "ngx-chips": "2.1.0",
"ngx-pipes": "^2.6.0",
"node-sass": "^4.9.3",
"npm-font-source-sans-pro": "^1.0.2",
"webtorrent": "^0.108.1",
"whatwg-fetch": "^3.0.0",
"zone.js": "~0.10.2"
- },
- "dependencies": {}
+ }
}
<button i18n class="reset-button reset-button-small" (click)="resetField('tagsAllOf')" *ngIf="advancedSearch.tagsAllOf">
Reset
</button>
- <tag-input
- [(ngModel)]="advancedSearch.tagsAllOf" name="tagsAllOf" id="tagsAllOf"
- [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
- i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
- [maxItems]="5" [modelAsStrings]="true"
- ></tag-input>
+ <my-select-tags labelForId="tagsAllOf" id="tagsAllOf" [(ngModel)]="advancedSearch.tagsAllOf"></my-select-tags>
</div>
<div class="form-group">
<button i18n class="reset-button reset-button-small" (click)="resetField('tagsOneOf')" *ngIf="advancedSearch.tagsOneOf">
Reset
</button>
- <tag-input
- [(ngModel)]="advancedSearch.tagsOneOf" name="tagsOneOf" id="tagsOneOf"
- [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
- i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a tag"
- [maxItems]="5" [modelAsStrings]="true"
- ></tag-input>
+ <my-select-tags labelForId="tagsOneOf" id="tagsOneOf" [(ngModel)]="advancedSearch.tagsOneOf"></my-select-tags>
</div>
<div class="form-group" *ngIf="isSearchTargetEnabled()">
display: flex;
white-space: nowrap;
}
-
-@include ng2-tags;
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
-import { ValidatorFn } from '@angular/forms'
import { ServerService } from '@app/core'
import { VideoValidatorsService } from '@app/shared/shared-forms'
import { AdvancedSearch } from '@app/shared/shared-search'
videoLicences: VideoConstant<number>[] = []
videoLanguages: VideoConstant<string>[] = []
- tagValidators: ValidatorFn[]
- tagValidatorsMessages: { [ name: string ]: string }
-
publishedDateRanges: { id: string, label: string }[] = []
sorts: { id: string, label: string }[] = []
durationRanges: { id: string, label: string }[] = []
private videoValidatorsService: VideoValidatorsService,
private serverService: ServerService
) {
- this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
- this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
this.publishedDateRanges = [
{
id: 'any_published_date',
-import { TagInputModule } from 'ngx-chips'
import { NgModule } from '@angular/core'
import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedMainModule } from '@app/shared/shared-main'
@NgModule({
imports: [
- TagInputModule,
-
SearchRoutingModule,
SharedMainModule,
],
exports: [
- TagInputModule,
SearchComponent
],
<div class="modal-body">
<label i18n for="language">Language</label>
- <div class="peertube-select-container">
- <select id="language" formControlName="language" class="form-control">
- <option></option>
- <option *ngFor="let language of videoCaptionLanguages" [value]="language.id">{{ language.label }}</option>
- </select>
+ <div class="peertube-ng-select-container">
+ <ng-select
+ labelForId="language" [items]="videoCaptionLanguages" formControlName="language"
+ bindLabel="label" bindValue="id"
+ ></ng-select>
</div>
<div *ngIf="formErrors.language" class="form-error">
@import '_variables';
@import '_mixins';
-.peertube-select-container {
- @include peertube-select-container(auto);
+label {
+ font-weight: $font-regular;
+ font-size: 100%;
}
.caption-file {
</ng-template>
</my-help>
- <tag-input
- [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
- i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
- formControlName="tags" [maxItems]="5" [modelAsStrings]="true"
- ></tag-input>
+ <my-select-tags labelForId="label-tags" formControlName="tags"></my-select-tags>
+ <div *ngIf="formErrors.tags" class="form-error">
+ {{ formErrors.tags }}
+ </div>
</div>
<div class="form-group">
<div class="col-video-edit">
<div class="form-group">
- <label i18n>Channel</label>
- <div class="peertube-select-container">
- <select formControlName="channelId" class="form-control">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
+ <label i18n for="channel">Channel</label>
+ <my-select-channel labelForId="channel" [items]="userVideoChannels" formControlName="channelId"></my-select-channel>
</div>
<div class="form-group">
<label i18n for="category">Category</label>
- <div class="peertube-select-container">
- <select id="category" formControlName="category" class="form-control">
- <option></option>
- <option *ngFor="let category of videoCategories" [value]="category.id">{{ category.label }}</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="category" [items]="videoCategories" formControlName="category" [clearable]="true"
+ ></my-select-options>
<div *ngIf="formErrors.category" class="form-error">
{{ formErrors.category }}
<div class="form-group">
<label i18n for="licence">Licence</label>
- <div class="peertube-select-container">
- <select id="licence" formControlName="licence" class="form-control">
- <option></option>
- <option *ngFor="let licence of videoLicences" [value]="licence.id">{{ licence.label }}</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="licence" [items]="videoLicences" formControlName="licence" [clearable]="true"
+ ></my-select-options>
<div *ngIf="formErrors.licence" class="form-error">
{{ formErrors.licence }}
<div class="form-group">
<label i18n for="language">Language</label>
- <div class="peertube-select-container">
- <select id="language" formControlName="language" class="form-control">
- <option></option>
- <option *ngFor="let language of videoLanguages" [value]="language.id">{{ language.label }}</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="language" [items]="videoLanguages" formControlName="language"
+ [clearable]="true" [searchable]="true" [groupBy]="'group'"
+ ></my-select-options>
<div *ngIf="formErrors.language" class="form-error">
{{ formErrors.language }}
<div class="form-group">
<label i18n for="privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="privacy" formControlName="privacy" class="form-control">
- <option></option>
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- <option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="privacy" [items]="videoPrivacies" formControlName="privacy" [clearable]="false"
+ ></my-select-options>
<div *ngIf="formErrors.privacy" class="form-error">
{{ formErrors.privacy }}
<my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
<ng-template ptTemplate="label">
- <ng-container i18n>This video contains mature or explicit content</ng-container>
+ <ng-container i18n>Contains sensitive content</ng-container>
</ng-template>
<ng-template ptTemplate="help">
<my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
<ng-template ptTemplate="label">
- <ng-container i18n>Wait transcoding before publishing the video</ng-container>
+ <ng-container i18n>Publish after transcoding</ng-container>
</ng-template>
<ng-template ptTemplate="help">
}
}
-@include ng2-tags;
-
// columns for the video
.col-video-edit {
@include make-col-ready();
import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models'
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { forkJoin } from 'rxjs'
+import { InstanceService } from '@app/shared/shared-instance'
+
+type VideoLanguages = VideoConstant<string> & { group?: string }
@Component({
selector: 'my-video-edit',
videoPrivacies: VideoConstant<VideoPrivacy>[] = []
videoCategories: VideoConstant<number>[] = []
videoLicences: VideoConstant<number>[] = []
- videoLanguages: VideoConstant<string>[] = []
+ videoLanguages: VideoLanguages[] = []
tagValidators: ValidatorFn[]
tagValidatorsMessages: { [ name: string ]: string }
private videoValidatorsService: VideoValidatorsService,
private videoService: VideoService,
private serverService: ServerService,
+ private instanceService: InstanceService,
private i18nPrimengCalendarService: I18nPrimengCalendarService,
+ private i18n: I18n,
private ngZone: NgZone
) {
- this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
- this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
-
this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
licence: this.videoValidatorsService.VIDEO_LICENCE,
language: this.videoValidatorsService.VIDEO_LANGUAGE,
description: this.videoValidatorsService.VIDEO_DESCRIPTION,
- tags: null,
+ tags: this.videoValidatorsService.VIDEO_TAGS_ARRAY,
previewfile: null,
support: this.videoValidatorsService.VIDEO_SUPPORT,
schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT,
.subscribe(res => this.videoCategories = res)
this.serverService.getVideoLicences()
.subscribe(res => this.videoLicences = res)
- this.serverService.getVideoLanguages()
- .subscribe(res => this.videoLanguages = res)
+ forkJoin([
+ this.instanceService.getAbout(),
+ this.serverService.getVideoLanguages()
+ ]).pipe(map(([ about, languages ]) => ({ about, languages })))
+ .subscribe(res => {
+ this.videoLanguages = res.languages
+ .map(l => res.about.instance.languages.includes(l.id)
+ ? { ...l, group: this.i18n('Instance languages'), groupOrder: 0 }
+ : { ...l, group: this.i18n('All languages'), groupOrder: 1 })
+ .sort((a, b) => a.groupOrder - b.groupOrder)
+ })
this.serverService.getVideoPrivacies()
- .subscribe(privacies => this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies))
+ .subscribe(privacies => {
+ this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)
+ if (this.schedulePublicationPossible) {
+ this.videoPrivacies.push({
+ id: this.SPECIAL_SCHEDULED_PRIVACY,
+ label: this.i18n('Scheduled'),
+ description: this.i18n('Hide the video until a specific date')
+ })
+ }
+ })
this.serverConfig = this.serverService.getTmpConfig()
this.serverService.getConfig()
-import { TagInputModule } from 'ngx-chips'
import { CalendarModule } from 'primeng/calendar'
import { NgModule } from '@angular/core'
import { SharedFormModule } from '@app/shared/shared-forms'
@NgModule({
imports: [
- TagInputModule,
CalendarModule,
SharedMainModule,
],
exports: [
- TagInputModule,
CalendarModule,
SharedMainModule,
<div class="form-group">
<label i18n for="first-step-channel">Channel</label>
- <div class="peertube-select-container">
- <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
+ <my-select-channel
+ labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
+ ></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
+ ></my-select-options>
</div>
<input
<div class="form-group">
<label i18n for="first-step-channel">Channel</label>
- <div class="peertube-select-container">
- <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
+ <my-select-channel
+ labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
+ ></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
+ ></my-select-options>
</div>
<input
.peertube-select-container {
@include peertube-select-container($width-size);
}
+ my-select-options ::ng-deep ng-select,
+ my-select-channel ::ng-deep ng-select {
+ width: $width-size;
+ @media screen and (max-width: $width-size) {
+ width: 100%;
+ }
+ }
input[type=text] {
@include peertube-input-text($width-size);
@Directive()
// tslint:disable-next-line: directive-class-suffix
export abstract class VideoSend extends FormReactive implements OnInit {
- userVideoChannels: { id: number, label: string, support: string }[] = []
+ userVideoChannels: { id: number, label: string, support: string, avatarPath?: string }[] = []
videoPrivacies: VideoConstant<VideoPrivacy>[] = []
videoCaptions: VideoCaptionEdit[] = []
this.serverService.getVideoPrivacies()
.subscribe(
privacies => {
- this.videoPrivacies = privacies
+ this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies)
this.firstStepPrivacyId = this.DEFAULT_VIDEO_PRIVACY
})
<div class="form-group form-group-channel">
<label i18n for="first-step-channel">Channel</label>
- <div class="peertube-select-container">
- <select id="first-step-channel" [(ngModel)]="firstStepChannelId" class="form-control">
- <option *ngFor="let channel of userVideoChannels" [value]="channel.id">{{ channel.label }}</option>
- </select>
- </div>
+ <my-select-channel
+ labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId"
+ ></my-select-channel>
</div>
<div class="form-group">
<label i18n for="first-step-privacy">Privacy</label>
- <div class="peertube-select-container">
- <select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId" class="form-control">
- <option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
- <option i18n [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
- </select>
- </div>
+ <my-select-options
+ labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId"
+ ></my-select-options>
</div>
<ng-container *ngIf="isUploadingAudioFile">
video: VideoEdit
isUpdatingVideo = false
- userVideoChannels: { id: number, label: string, support: string }[] = []
+ userVideoChannels: { id: number, label: string, support: string, avatar?: string }[] = []
schedulePublicationPossible = false
videoCaptions: VideoCaptionEdit[] = []
waitTranscodingEnabled = true
.listAccountVideoChannels(video.account)
.pipe(
map(result => result.data),
- map(videoChannels => videoChannels.map(c => ({ id: c.id, label: c.displayName, support: c.support })))
+ map(videoChannels => videoChannels.map(c => ({
+ id: c.id,
+ label: c.displayName,
+ support: c.support,
+ avatarPath: c.avatar?.path
+ })))
),
this.videoCaptionService
return decodeURIComponent(results[2].replace(/\+/g, ' '))
}
-function populateAsyncUserVideoChannels (authService: AuthService, channel: { id: number, label: string, support?: string }[]) {
+function populateAsyncUserVideoChannels (
+ authService: AuthService,
+ channel: { id: number, label: string, support?: string, avatarPath?: string, recent?: boolean }[]
+) {
return new Promise(res => {
authService.userInformationLoaded
.subscribe(
const videoChannels = user.videoChannels
if (Array.isArray(videoChannels) === false) return
- videoChannels.forEach(c => channel.push({ id: c.id, label: c.displayName, support: c.support }))
+ videoChannels.forEach(c => channel.push({
+ id: c.id,
+ label: c.displayName,
+ support: c.support,
+ avatarPath: c.avatar?.path
+ }))
return res()
}
import { I18n } from '@ngx-translate/i18n-polyfill'
-import { Validators } from '@angular/forms'
+import { Validators, ValidatorFn, ValidationErrors, AbstractControl } from '@angular/forms'
import { Injectable } from '@angular/core'
import { BuildFormValidator } from './form-validator.service'
readonly VIDEO_IMAGE: BuildFormValidator
readonly VIDEO_CHANNEL: BuildFormValidator
readonly VIDEO_DESCRIPTION: BuildFormValidator
- readonly VIDEO_TAGS: BuildFormValidator
+ readonly VIDEO_TAGS_ARRAY: BuildFormValidator
+ readonly VIDEO_TAG: BuildFormValidator
readonly VIDEO_SUPPORT: BuildFormValidator
readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
readonly VIDEO_ORIGINALLY_PUBLISHED_AT: BuildFormValidator
}
}
- this.VIDEO_TAGS = {
+ this.VIDEO_TAG = {
VALIDATORS: [ Validators.minLength(2), Validators.maxLength(30) ],
MESSAGES: {
'minlength': this.i18n('A tag should be more than 2 characters long.'),
}
}
+ this.VIDEO_TAGS_ARRAY = {
+ VALIDATORS: [ Validators.maxLength(5), this.arrayTagLengthValidator() ],
+ MESSAGES: {
+ 'maxlength': this.i18n('A maximum of 5 tags can be used on a video.'),
+ 'arrayTagLength': this.i18n('A tag should be more than 2, and less than 30 characters long.')
+ }
+ }
+
this.VIDEO_SUPPORT = {
VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ],
MESSAGES: {
MESSAGES: {}
}
}
+
+ arrayTagLengthValidator (min = 2, max = 30): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors => {
+ const array = control.value as Array<string>
+
+ if (array.every(e => e.length > min && e.length < max)) {
+ return null
+ }
+
+ return { 'arrayTagLength': true }
+ }
+ }
}
--- /dev/null
+<ng-select
+ [(ngModel)]="selectedId"
+ (ngModelChange)="onModelChange()"
+ [bindLabel]="bindLabel"
+ [bindValue]="bindValue"
+ [clearable]="clearable"
+ [searchable]="searchable"
+>
+ <ng-option *ngFor="let channel of channels" [value]="channel.id">
+ <img
+ class="avatar mr-1"
+ [src]="channel.avatarPath"
+ />
+ {{ channel.label }}
+ </ng-option>
+</ng-select>
--- /dev/null
+import { Component, Input, forwardRef, ViewChild } from '@angular/core'
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
+import { Actor } from '../shared-main'
+
+@Component({
+ selector: 'my-select-channel',
+ styleUrls: [ './select-shared.component.scss' ],
+ templateUrl: './select-channel.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectChannelComponent),
+ multi: true
+ }
+ ]
+})
+export class SelectChannelComponent implements ControlValueAccessor {
+ @Input() items: { id: number, label: string, support: string, avatarPath?: string }[] = []
+
+ selectedId: number
+
+ // ng-select options
+ bindLabel = 'label'
+ bindValue = 'id'
+ clearable = false
+ searchable = false
+
+ get channels () {
+ return this.items.map(c => Object.assign(c, {
+ avatarPath: c.avatarPath ? c.avatarPath : Actor.GET_DEFAULT_AVATAR_URL()
+ }))
+ }
+
+ propagateChange = (_: any) => { /* empty */ }
+
+ writeValue (id: number) {
+ this.selectedId = id
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ onModelChange () {
+ this.propagateChange(this.selectedId)
+ }
+}
--- /dev/null
+<ng-select
+ [items]="items"
+ [groupBy]="groupBy"
+ [(ngModel)]="selectedId"
+ (ngModelChange)="onModelChange()"
+ [bindLabel]="bindLabel"
+ [bindValue]="bindValue"
+ [clearable]="clearable"
+ [searchable]="searchable"
+>
+ <ng-template ng-option-tmp let-item="item" let-index="index">
+ {{ item.label }}
+ <ng-container *ngIf="item.description">
+ <br>
+ <span [title]="item.description" class="text-muted">{{ item.description }}</span>
+ </ng-container>
+ </ng-template>
+</ng-select>
--- /dev/null
+import { Component, Input, forwardRef } from '@angular/core'
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
+
+export type SelectOptionsItem = { id: number | string, label: string, description?: string }
+
+@Component({
+ selector: 'my-select-options',
+ styleUrls: [ './select-shared.component.scss' ],
+ templateUrl: './select-options.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectOptionsComponent),
+ multi: true
+ }
+ ]
+})
+export class SelectOptionsComponent implements ControlValueAccessor {
+ @Input() items: SelectOptionsItem[] = []
+ @Input() clearable = false
+ @Input() searchable = false
+ @Input() bindValue = 'id'
+ @Input() groupBy: string
+
+ selectedId: number | string
+
+ // ng-select options
+ bindLabel = 'label'
+
+ propagateChange = (_: any) => { /* empty */ }
+
+ writeValue (id: number | string) {
+ this.selectedId = id
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ onModelChange () {
+ this.propagateChange(this.selectedId)
+ }
+}
--- /dev/null
+$width-size: auto;
+
+ng-select {
+ width: $width-size;
+ @media screen and (max-width: $width-size) {
+ width: 100%;
+ }
+}
+
+// make sure the image is vertically adjusted
+ng-select ::ng-deep .ng-value-label img {
+ position: relative;
+ top: -1px;
+}
+
+ng-select ::ng-deep img {
+ border-radius: 50%;
+ height: 20px;
+ width: 20px;
+}
--- /dev/null
+<ng-select
+ [items]="items"
+ [(ngModel)]="_items"
+ (ngModelChange)="onModelChange()"
+ i18n-placeholder placeholder="Enter a new tag"
+ [maxSelectedItems]="5"
+ [clearable]="true"
+ [addTag]="true"
+ [multiple]="true"
+ [isOpen]="false"
+ [searchable]="true"
+>
+</ng-select>
--- /dev/null
+ng-select ::ng-deep .ng-arrow-wrapper {
+ display: none;
+}
--- /dev/null
+import { Component, Input, forwardRef } from '@angular/core'
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'
+
+@Component({
+ selector: 'my-select-tags',
+ styleUrls: [ './select-shared.component.scss', './select-tags.component.scss' ],
+ templateUrl: './select-tags.component.html',
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SelectTagsComponent),
+ multi: true
+ }
+ ]
+})
+export class SelectTagsComponent implements ControlValueAccessor {
+ @Input() items: string[] = []
+ @Input() _items: string[] = []
+
+ propagateChange = (_: any) => { /* empty */ }
+
+ writeValue (items: string[]) {
+ this._items = items
+ this.propagateChange(this._items)
+ }
+
+ registerOnChange (fn: (_: any) => void) {
+ this.propagateChange = fn
+ }
+
+ registerOnTouched () {
+ // Unused
+ }
+
+ onModelChange () {
+ this.propagateChange(this._items)
+ }
+}
import { ReactiveFileComponent } from './reactive-file.component'
import { TextareaAutoResizeDirective } from './textarea-autoresize.directive'
import { TimestampInputComponent } from './timestamp-input.component'
+import { SelectChannelComponent } from './select-channel.component'
+import { SelectOptionsComponent } from './select-options.component'
+import { SelectTagsComponent } from './select-tags.component'
@NgModule({
imports: [
PreviewUploadComponent,
ReactiveFileComponent,
TextareaAutoResizeDirective,
- TimestampInputComponent
+ TimestampInputComponent,
+ SelectChannelComponent,
+ SelectOptionsComponent,
+ SelectTagsComponent
],
exports: [
PreviewUploadComponent,
ReactiveFileComponent,
TextareaAutoResizeDirective,
- TimestampInputComponent
+ TimestampInputComponent,
+ SelectChannelComponent,
+ SelectOptionsComponent,
+ SelectTagsComponent
],
providers: [
NgbPopoverModule,
NgbTooltipModule
} from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectModule } from '@ng-select/ng-select'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { SharedGlobalIconModule } from '../shared-icons'
import { AccountService, ActorAvatarInfoComponent, AvatarComponent } from './account'
MultiSelectModule,
InputSwitchModule,
+ NgSelectModule,
+
SharedGlobalIconModule
],
TopMenuDropdownComponent,
UserQuotaComponent,
- UserNotificationsComponent
+ UserNotificationsComponent,
+
+ NgSelectModule
],
providers: [
const base = [
{
id: VideoPrivacy.PRIVATE,
- label: this.i18n('Only I can see this video')
+ description: this.i18n('Only I can see this video')
},
{
id: VideoPrivacy.UNLISTED,
- label: this.i18n('Only people with the private link can see this video')
+ description: this.i18n('Only shareable via a private link')
},
{
id: VideoPrivacy.PUBLIC,
- label: this.i18n('Anyone can see this video')
+ description: this.i18n('Anyone can see this video')
},
{
id: VideoPrivacy.INTERNAL,
- label: this.i18n('Only users of this instance can see this video')
+ description: this.i18n('Only users of this instance can see this video')
}
]
- return base.filter(o => !!privacies.find(p => p.id === o.id))
+ return base
+ .filter(o => !!privacies.find(p => p.id === o.id)) // filter down to privacies that where in the input
+ .map(o => ({ ...privacies[o.id - 1], ...o })) // merge the input privacies that contain a label, and extend them with a description
}
nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) {
@import './bootstrap';
@import './primeng-custom';
+@import './ng-select.scss';
[hidden] {
display: none !important;
}
}
-@mixin ng2-tags {
- ::ng-deep {
- .ng2-tag-input {
- border: none !important;
- }
-
- .ng2-tags-container {
- display: flex;
- align-items: center;
- border: 1px solid #C6C6C6;
- border-radius: 3px;
- padding: 5px !important;
- height: max-content;
-
- &:focus-within {
- box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
- }
- }
-
- tag-input-form {
- input {
- height: 30px !important;
- font-size: 12px !important;
-
- background-color: pvar(--mainBackgroundColor) !important;
- color: pvar(--mainForegroundColor) !important;
- }
- }
-
- tag {
- background-color: $grey-background-color !important;
- color: #000 !important;
- border-radius: 3px !important;
- font-size: 12px !important;
- height: 30px !important;
- line-height: 30px !important;
- margin: 0 5px 0 0 !important;
- cursor: default !important;
- padding: 0 8px 0 10px !important;
-
- div {
- height: 100% !important;
- }
- }
-
- delete-icon {
- cursor: pointer !important;
- height: auto !important;
- vertical-align: middle !important;
- padding-left: 6px !important;
-
- svg {
- position: relative;
- top: -1px;
- height: auto !important;
- vertical-align: middle !important;
-
- path {
- fill: pvar(--greyForegroundColor) !important;
- }
- }
-
- &:hover {
- transform: none !important;
- }
- }
- }
-}
-
@mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) {
width: 95%;
border-top: .05rem solid $color;
--- /dev/null
+@import '_variables';
+
+$ng-select-highlight: #f2690d;
+// $ng-select-primary-text: #333 !default;
+// $ng-select-disabled-text: #f9f9f9 !default;
+// $ng-select-border: #ccc !default;
+// $ng-select-border-radius: 4px !default;
+// $ng-select-bg: #ffffff !default;
+// $ng-select-selected: lighten($ng-select-highlight, 46) !default;
+// $ng-select-marke d: lighten($ng-select-highlight, 48) !default;
+$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
+// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default;
+$ng-select-height: 30px;
+// $ng-select-value-padding-left: 10px !default;
+// $ng-select-value-font-size: 0.9em !default;
+
+@import "~@ng-select/ng-select/scss/default.theme.scss";
+
+.ng-input {
+ font-size: .9em;
+}
+
+.ng-select {
+ &.ng-select-focused {
+ &:not(.ng-select-opened) > .ng-select-container {
+ border-color: #ccc !important;
+ }
+ }
+}
dependencies:
tslib "^2.0.0"
+"@ng-select/ng-select@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@ng-select/ng-select/-/ng-select-5.0.0.tgz#db021e5ca7be3fe6be346c3b63b3f2e109afa0f0"
+ integrity sha512-1mdGNh5xGriSnCLcLF/GWxqCgCehB3Stu7mxhB9K/+BxNCRE365Q1MgpzO61XQBgL0L6ktxiXRhCQWXXtFoq9w==
+ dependencies:
+ tslib "^2.0.0"
+
"@ngtools/webpack@10.1.0-next.4":
version "10.1.0-next.4"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-10.1.0-next.4.tgz#5397417820d110e29c7dc99db8adb538de98676e"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
-ng2-material-dropdown@0.11.0:
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.11.0.tgz#27a402ef3cbdcaf6791ef4cfd4b257e31db7546f"
- integrity sha512-wptBo09qKecY0QPTProAThrc4A3ajJTcHE9LTpCG5XZZUhXLBzhnGK8OW33TN8A+K/jqcs7OB74ppYJiqs3nhQ==
- dependencies:
- tslib "^1.9.0"
-
-ngx-chips@2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/ngx-chips/-/ngx-chips-2.1.0.tgz#aa299bcf40dc3e1f6288bf1d29e2fdfe9a132ed3"
- integrity sha512-OQV4dTfD3nXm5d2mGKUSgwOtJOaMnZ4F+lwXOtd7DWRSUne0JQWwoZNHdOpuS6saBGhqCPDAwq6KxdR5XSgZUQ==
- dependencies:
- ng2-material-dropdown "0.11.0"
- tslib "^1.9.0"
-
ngx-pipes@^2.6.0:
version "2.7.5"
resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.7.5.tgz#22e2e4b7015ae9103210dfa2dacd6f4ae4411639"
export interface VideoConstant<T> {
id: T
label: string
+ description?: string
}