How to fix slider issue? - css

I have my Tabs working just fine. If you click on the plus symbol (+) you will be able to add Tabs and maybe after adding 3 or 4 tabs you'll see tabBar scroller buttons on both ends. This has been done using ExtJS and it works great!
Working Code:
Example Code
Basically, after certain width I would like to show those the scroller that will allow the user to navigate from right-left or left-right. Does anyone know how to accomplish this? Thanks a lot in advance!
Here's my code:
PLUNKER
<p-tabView>
<p-tabPanel header="one">
<button (click)="showSecondTab()">Show Second Tab</button>
</p-tabPanel>
<p-tabPanel header="two">
</p-tabPanel>
<p-tabPanel header="three">
</p-tabPanel>
<p-tabPanel header="fourt">
</p-tabPanel>
<p-tabPanel header="five">
</p-tabPanel>
.......
.......
.......
</p-tabView>

For now there is no built-in feature in primeng package. So I would consider writing some directive which will implement all desired behavior and take all hard work on itself.
This directive is going to have all stuff for handling such events such as window:scroll, click|long click on arrows, mousewhell on tabs. For such things i'm goung to use rxjs streams.
Also it's going to do some DOM manipulation like wrapping tabs in container and creating arrows.
It also will handle changes for p-tabPanel components through ContentChildren.changes
Finally, the result should look like:
tab-scroller.directive.ts
import { Directive, ElementRef, NgZone, Input, ContentChildren, QueryList } from '#angular/core';
import { TabPanel } from 'primeng/primeng';
import { interval } from 'rxjs/observable/interval';
import { of } from 'rxjs/observable/of';
import { fromEvent } from 'rxjs/observable/fromEvent';
import { Subject } from 'rxjs/Subject';
import { takeUntil } from 'rxjs/operators/takeUntil';
import { mergeMap } from 'rxjs/operators/mergeMap';
import { take } from 'rxjs/operators/take';
import { delay } from 'rxjs/operators/delay';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/observable/of';
#Directive({
selector: '[tabScroller]',
})
export class TabScrollerDirective {
#Input() arrowWidth = 30;
#Input() shiftWidth = 25;
#ContentChildren(TabPanel) tabPanels: QueryList<TabPanel>;
private _container: HTMLElement;
private _nav: HTMLElement;
private _shift = 0;
private _scrollable: boolean;
private _leftArrow: HTMLElement;
private _rightArrow: HTMLElement;
private readonly _destroyed$ = new Subject<void>();
constructor(
private elRef: ElementRef,
private zone: NgZone) { }
get rightBorder() {
return -(this._nav.scrollWidth - this._nav.offsetWidth);
}
ngAfterContentInit() {
this.tabPanels.changes
.pipe(takeUntil(this._destroyed$))
.subscribe(() => {
this.zone.onStable.asObservable()
.pipe(take(1))
.subscribe(() => this._refreshScroller());
})
}
ngAfterViewInit() {
this.zone.runOutsideAngular(() => this.init());
}
init() {
this._nav = this.elRef.nativeElement.querySelector('[role=tablist]');
this._container = wrap(this._nav, 'nav-wrapper');
this._initEvents();
this._leftArrow = this._createArrow('left');
this._rightArrow = this._createArrow('right');
this._refreshScroller();
}
scroll(shift: number) {
this._shift += shift;
const rightBorder = this.rightBorder;
if (this._shift < rightBorder) {
this._shift = rightBorder;
}
if (this._shift >= 0) {
this._shift = 0;
}
this._leftArrow.classList.toggle('nav-arrow--disabled', this._shift >= 0);
this._rightArrow.classList.toggle('nav-arrow--disabled', this._shift <= rightBorder);
this._nav.style.transform = `translateX(${this._shift}px)`;
}
ngOnDestroy() {
this._destroyed$.next();
this._destroyed$.complete();
}
private _initEvents() {
fromEvent(this._container, 'mousewheel')
.pipe(takeUntil(this._destroyed$))
.subscribe((e: any) => this._onMouseWheel(e));
// Firefox
fromEvent(this._container, 'DOMMouseScroll')
.pipe(takeUntil(this._destroyed$))
.subscribe((e: any) => this._onMouseWheel(e));
fromEvent(window, 'resize')
.pipe(takeUntil(this._destroyed$))
.subscribe(() => {
this._refreshScroller();
});
}
private _onMouseWheel(e: any) {
const delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
this.scroll(delta * 25);
}
private _createArrow(direction: string) {
const arrow = el(`nav-arrow nav-arrow--${direction}`);
this._container.insertBefore(arrow, this._nav);
arrow.style.width = this.arrowWidth + 'px';
fromEvent(arrow, 'click')
.pipe(takeUntil(this._destroyed$))
.subscribe(() => {
this.scroll(direction === 'left' ? this.shiftWidth : -this.shiftWidth);
});
const upStream$ = fromEvent(arrow, 'mouseup');
// handle long press
fromEvent(arrow, 'mousedown')
.pipe(
takeUntil(this._destroyed$),
mergeMap((event) => interval(100).pipe(delay(100), takeUntil(upStream$)))
)
.subscribe(() => {
this.scroll(direction === 'left' ? this.shiftWidth : -this.shiftWidth);
});
return arrow;
}
private _refreshScroller() {
const compareWith = (this._scrollable ? -this.arrowWidth * 2 : 0);
this._container.classList.toggle('nav-wrapper--scrollable', this.rightBorder < compareWith);
this._scrollable = this.rightBorder < compareWith;
this.scroll(0);
}
}
function wrap(elem, wrapperClass: string) {
const wrapper = el('nav-wrapper');
elem.parentNode.insertBefore(wrapper, elem);
wrapper.appendChild(elem);
return wrapper;
}
function el(className: string): HTMLElement {
const div = document.createElement('div');
div.className = className;
return div;
}
The last thing you need to do is to apply this directive on p-tabView element:
<p-tabView tabScroller>
...
</p-tabView>
Plunker Example
Ng-run Example

Related

Ionic iOS Keyboard Causing Footer To Snap Up

I have an object for a little chat app. When the input is focused on this footer, the keyboard triggers and after the keyboard has fully shown, the footer then jarringly snaps to the top of the keyboard.
So the footer isn’t hidden after the keyboard has fully moved up, but it is not a good experience because snapping feels awkward.
Is there a way to make my footer smoothly move with the iOS keyboard?
The html of the page is basically this:
<ion-page>
<ion-content>
</ion-content>
<ion-footer>
<my-input></my-input>
</ion-footer>
</ion-page>
ion-inputs handle this keyboard interaction. Maybe you can try using an ion-input & see if it solves your problem.
I have implemented an imperfect solution where I listen to Keyboard open & close events and then scroll the Ion-content accordingly. Here's the code for the directive:
import { Directive, ElementRef, HostListener, Input, NgModule, OnDestroy, OnInit } from "#angular/core";
import { IonContent, Platform } from "#ionic/angular";
import { Subject } from "rxjs";
import { takeUntil } from "rxjs/operators";
const SCROLL_DELAY = 100;
const TOOLBAR_HEIGHT = 44; // todo Can be different for some pages
#Directive({
selector: "[appInputScrollIntoView]",
})
export class InputScrollIntoViewDirective implements OnInit, OnDestroy {
#Input("appInputScrollIntoView") content: IonContent;
keyboardHeight: number = 0;
target: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
focused = false;
closed$ = new Subject();
constructor(private elementRef: ElementRef, private platform: Platform) {}
#HostListener("focus", ["$event"])
onFocus(event: Event) {
this.focused = true;
}
#HostListener("focusout", ["$event"])
onFocusOut(event: Event) {
this.focused = false;
}
ngOnInit(): void {
this.target = this.elementRef.nativeElement as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
// Android handles keyboard scenario on HTML automatically
const matchPlatform = this.platform.is("ios") && this.platform.is("capacitor");
if (!matchPlatform) return;
this.platform.keyboardDidShow.pipe(takeUntil(this.closed$)).subscribe((ev) => {
const { keyboardHeight } = ev;
this.keyboardHeight = keyboardHeight;
this.adjustScrollPosition();
});
this.platform.keyboardDidHide.pipe(takeUntil(this.closed$)).subscribe(() => {
this.keyboardHeight = 0;
// maybe move back to original position here..
});
}
adjustScrollPosition() {
if (!this.focused) {
return;
}
setTimeout(() => {
this.scrollUpIfBehindKeyboard();
this.scrollDownIfBehindSafeArea();
}, 50);
}
scrollUpIfBehindKeyboard() {
const windowHeight = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const targetHeightFromBottom = windowHeight - this.target.getBoundingClientRect().bottom;
const requiredScroll = this.keyboardHeight - targetHeightFromBottom;
if (requiredScroll > 0) {
this.content.scrollToPoint(0, this.keyboardHeight, SCROLL_DELAY);
}
}
scrollDownIfBehindSafeArea() {
const safeArea = +getComputedStyle(document.documentElement)
.getPropertyValue("--ion-safe-area-top")
.replace("px", "");
const headerHeigth = TOOLBAR_HEIGHT;
const top = this.target.getBoundingClientRect().top;
const requiredScroll = safeArea + headerHeigth - top;
if (requiredScroll > 0) {
const y = this.target.offsetHeight - safeArea - headerHeigth;
this.content.scrollToPoint(0, y, SCROLL_DELAY);
}
}
ngOnDestroy(): void {
this.closed$.next();
this.closed$.complete();
}
}

move scrollbar based on searching text inside div and it has scrollbar in angular

When I search text in div and it has scrollbar, and when it search least word scrollbar should go down automatically based on search in angular
We could for example, on each keystroke search of a matching value from the text. And if we find it, we replace it with different styling.
But I bet there are packages designed just for this.
Here is a small example that might help you: https://stackblitz.com/edit/angular-textarea-highlight-f5jvj3?file=src%2Fapp%2Ftextarea-highlight%2Ftextarea-highlight.component.ts
#Component({
selector: "app-textarea-highlight",
templateUrl: "./textarea-highlight.component.html",
styleUrls: ["./textarea-highlight.component.css"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextareaHighlightComponent),
multi: true
}
]
})
export class TextareaHighlightComponent
implements ControlValueAccessor {
constructor() {}
#Input() highlightTexts: string[] = [];
#ViewChild("backdrop") $backdrop: ElementRef<HTMLDivElement>;
#ViewChild("textarea") $textarea: ElementRef<HTMLTextAreaElement>;
textValue: string = "";
get highlightedText () {
return this.applyHighlights(this.textValue)
}
applyHighlights(text) {
text = text ? text
.replace(/\n$/g, "\n\n") : '';
this.highlightTexts.forEach(x => {
text = text
.replace(new RegExp(x, 'g'), "<mark>$&</mark>");
})
return text;
}
handleScroll() {
var scrollTop = this.$textarea.nativeElement.scrollTop;
this.$backdrop.nativeElement.scrollTop = scrollTop;
var scrollLeft = this.$textarea.nativeElement.scrollLeft;
this.$backdrop.nativeElement.scrollLeft = scrollLeft;
}
onChanges: ($value: any) => void;
onTouched: () => void;
writeValue(value: any): void {
if (value !== undefined) {
this.textValue = value;
}
}
registerOnChange(fn: any): void {
this.onChanges = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}
Or maybe something like this is what you are looking for: https://dev.to/juniordevforlife/highlight-search-results-with-an-angular-pipe-42cb

Youtube iframe api: interact with MediaSession

I have an angular app with the Youtube IFrame API. When the website/pwa is used on an android device, and a Youtube video is playing, the app automatically creates a notification in the android notification tray, allowing you to see what video is currently playing.
I created my own playlist-controller, so I'm not using queueVideoById since I don't like the way the default youtube-iframe playlist-controller works (you cannot remove queued videos afterwards).
From my AppComponent I'm loading the YouTube IFrame API
app.component.ts
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
constructor(private youtubeHelper: YoutubeHelper) {
this.youtubeHelper.loadApi().then((isLoaded) => {
this.player.playSong(song.playerInfo.id);
});
}
#ViewChild('player') player: YoutubePlayerComponent;
}
app.component.html
<body>
<youtube-player #player [domId]="'player'" ... (previousPressed)="onPreviousPressed()" (nextPressed)="onNextPressed()"></youtube-player>
</body>
This helper is used to load the IFrame API script. LoadApi inserts a script-tag with the src set to the youtube-iframe-url:
import { Injectable } from '#angular/core';
import { Promise } from 'q';
import { BehaviorSubject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class YoutubeHelper {
static scriptTag: HTMLScriptElement = null;
public loadApi() {
return Promise<boolean>((resolve, reject) => {
if (typeof window !== 'undefined') {
if (YoutubeHelper.scriptTag === null) {
window['onYouTubeIframeAPIReady'] = () => {
this.apiReady.next(true);
resolve(true);
};
YoutubeHelper.scriptTag = window.document.createElement('script');
YoutubeHelper.scriptTag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = window.document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(YoutubeHelper.scriptTag, firstScriptTag);
} else {
resolve(true);
}
} else {
resolve(false);
}
});
}
public unloadApi() {
return Promise((resolve) => {
if (typeof window !== 'undefined') {
if (YoutubeHelper.scriptTag !== null) {
YoutubeHelper.scriptTag.parentNode.removeChild(YoutubeHelper.scriptTag);
YoutubeHelper.scriptTag = null;
//console.log('Removed YouTube iframe api');
this.apiReady.next(false);
}
}
});
}
public apiReady = new BehaviorSubject<boolean>(
(typeof window === 'undefined')
? false
: window['YT'] !== undefined
);
}
When the iframe api script is inserted in the dom, the YoutubePlayerComponent deals with this by creating an instance of YT.Player (this happens at the this.youtubeHelper.apiReady subscription):
/// <reference path="../../../../node_modules/#types/youtube/index.d.ts" />
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
import { YoutubeHelper } from '../../helpers/youtube-api.helper';
import { SongProgress } from '../../entities/song-progress';
import { Song } from '../../entities/song';
import { BehaviorSubject } from 'rxjs';
#Component({
selector: 'youtube-player',
templateUrl: './youtube-player.component.html',
styleUrls: ['./youtube-player.component.scss']
})
export class YoutubePlayerComponent implements OnInit {
constructor(private youtubeHelper: YoutubeHelper) {
}
#Input() domId: string;
// Properties omitted for breverity
#Output() previousPressed: EventEmitter<any> = new EventEmitter();
#Output() nextPressed: EventEmitter<any> = new EventEmitter();
private player: YT.Player;
private isPlayerReady = new BehaviorSubject<boolean>(false);
ngOnInit() {
}
ngAfterViewInit() {
this.youtubeHelper.apiReady.subscribe((ready) => {
if (ready && !this.player) {
this.player = new YT.Player(this.domId, {
width: this.width,
height: this.height,
playerVars: {
autoplay: this._autoplay
},
events: {
onReady: (event) => {
this.isPlayerReady.next(true);
},
onStateChange: (state: { data: YT.PlayerState }) => {
}
}
});
// Attempt to give the player methods my very own implementation
this.player.previousVideo = () => {
this.previousPressed.emit();
};
this.player.nextVideo = () => {
this.nextPressed.emit();
};
console.log('player', this.player);
}
});
}
static mediaInit = false;
public playSong(youtubeId: string) {
if (this.isPlayerReady.value) {
this.player.loadVideoById(youtubeId);
} else {
console.log('Player not yet ready');
this.isPlayerReady.subscribe((value) => {
if (value === true) {
this.player.loadVideoById(youtubeId);
}
});
}
// Here is the code that's supposed to interact with the media session in the iframe.
if (!YoutubePlayerComponent.mediaInit) {
YoutubePlayerComponent.mediaInit = true;
let frame = <HTMLIFrameElement>document.getElementById(this.domId);
frame.contentWindow.navigator.mediaSession.setActionHandler('previoustrack', () => {
this.previousPressed.emit();
});
frame.contentWindow.navigator.mediaSession.setActionHandler('nexttrack', () => {
this.nextPressed.emit();
});
}
}
public play() { this.player.playVideo(); }
// Pause, stop methods omitted for breverity
}
After that the isPlayerReady is triggered, allowing a video to be played:
When calling YoutubePlayerComponent.playSong, it either waits for the YoutubePlayer to be ready (isPlayerReady) or immediately calls this.player.loadVideoById, which plays the requested video.
Having said this, the question is:
By default when a video is playing inside my website/pwa it displays a media notification as shown in the first image. I want to be able to navigate to the previous/next track in my own playlist. So I want to intercept these events:
According to this link it's possible to modify the mediaSession of the Window after playing a video, attaching custom handlers to the previous/next buttons in the android tray. But since the video (and off course the mediaSession) is being played from the iframe, it's not possible to modify the metadata:
navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
album: 'Whenever You Need Somebody'
});
would become:
document.getElementById('player').contentWindow.navigator.mediaSession.metadata = new MediaMetadata({
title: 'Never Gonna Give You Up',
artist: 'Rick Astley',
album: 'Whenever You Need Somebody'
});
And just to check, while a video is playing the mediaSession of the mainWindow is empty:
navigator.mediaSession.metadata
> null
And the actual mediaSession should be available in:
document.getElementById("player").contentWindow.navigator.mediaSession
> Blocked a frame with origin "https://example.com" from accessing a cross-origin frame
So is there any way I can add previous/next buttons to the notification while using the Youtube iframe API, and handle these events?
If necessary, the entire project is available on GitHub

How to optimally apply resize on large data in ACE editor?

I am using ace-editor in my angular app as a JSON editor, ace editor has a feature to dynamically resize based on the data.
When I have data within 1.5k lines it works seamlessly post that the chrome browser get hang, noticed that the CPU utilization is also high
Can any help me to identify how can I resolve this issue or any work around?
If it is an styling issue can anyone suggest how to fit the editor?
Resize logic:
ngAfterViewChecked() {
this.codeEditor.setOptions({
maxLines: this.codeEditor.getSession().getScreenLength(),
autoScrollEditorIntoView: true
});
this.codeEditor.resize();
}
editor.ts:
import {
Component, ViewChild, ElementRef, Input, Output, EventEmitter,
OnChanges, SimpleChanges
} from '#angular/core';
import * as ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/theme-github';
const THEME = 'ace/theme/github';
const LANG = 'ace/mode/json';
export interface EditorChangeEventArgs {
newValue: any;
}
#Component({
selector: 'app-editor',
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.css']
})
export class EditorComponent implements OnChanges {
#ViewChild('codeEditor') codeEditorElmRef: ElementRef;
private codeEditor: ace.Ace.Editor;
#Input() jsonObject;
#Input() readMode;
#Output() change = new EventEmitter();
data: any;
mode: any;
constructor() { }
ngOnChanges(changes: SimpleChanges) {
for (const properties of Object.keys(changes)) {
if (properties == 'jsonObject') {
const currentJSONObject = changes[properties];
if (currentJSONObject.currentValue && currentJSONObject.firstChange == false)
this.codeEditor.setValue(JSON.stringify(currentJSONObject.currentValue, null, '\t'), -1);
else if (currentJSONObject.currentValue == null && currentJSONObject.firstChange == false)
this.codeEditor.setValue("");
else
this.data = currentJSONObject.currentValue
}
if (properties == 'readMode') {
const currentReadMode = changes[properties];
if (currentReadMode.firstChange == false)
this.codeEditor.setReadOnly(currentReadMode.currentValue);
else
this.mode = currentReadMode.currentValue
}
}
}
ngAfterViewInit() {
const element = this.codeEditorElmRef.nativeElement;
const editorOptions: Partial<ace.Ace.EditorOptions> = {
highlightActiveLine: true,
displayIndentGuides: true,
highlightSelectedWord: true,
};
this.codeEditor = ace.edit(element, editorOptions);
this.codeEditor.setTheme(THEME);
this.codeEditor.getSession().setMode(LANG);
this.codeEditor.setShowFoldWidgets(true);
this.codeEditor.setHighlightActiveLine(true);
this.codeEditor.setShowPrintMargin(false);
if (this.data)
this.codeEditor.setValue(JSON.stringify(this.data, null, '\t'), -1);
this.codeEditor.setReadOnly(this.readMode);
if (this.mode)
this.codeEditor.setReadOnly(this.mode);
}
ngAfterViewChecked() {
this.codeEditor.setOptions({
maxLines: this.codeEditor.getSession().getScreenLength(),
autoScrollEditorIntoView: true
});
this.codeEditor.resize();
}
onChange(updatedJSON) {
this.change.emit({ newValue: updatedJSON });
}
}
editor.html:
<div ace-editor #codeEditor [autoUpdateContent]="true" [durationBeforeCallback]="1000" (textChanged)="onChange($event)"
(change)="onChange(codeEditor.value)" class="editor">
</div>
Editor CSS property:
.editor {
min-height: 750px;
width: 100%;
}
Using editor component across the application by simply adding following tag in HTML body
<app-editor [jsonObject]="data" [readMode]="readMode" (change)="onChange($event)"></app-editor>

How to pass CSS styles to my Angular 2 component?

I've built an Angular2/4 component that is, basically, a masked date input. I use it in place of a textbox input, and have some code behind it to treat date conversions. It works well enough, but now i want to apply a CSS style and have it changing the input.
I want to write <app-date-input class='someCssClass'></app-date-input> and that class be attributed to my internal input.
My code for the component follows:
date-input.component.html
import { Component, Input, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
import { DatePipe } from "#angular/common";
import * as moment from 'moment';
date-input.component.ts
import { AppConstantsService } from "../../_services";
#Component({
selector: 'app-date-input',
templateUrl: './date-input.component.html',
styleUrls: ['./date-input.component.css'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateInputComponent),
multi: true
}]
})
export class DateInputComponent implements ControlValueAccessor {
#Input()
public valor: Date;
#Input()
public readonly: boolean;
public dataString: string;
constructor(public appConstants: AppConstantsService) {
}
atribuirData(dataEntrada: string) {
if (!dataEntrada || dataEntrada == '') {
this.valor = null;
this.propagateChange(this.valor);
return;
}
let parts = dataEntrada.split('/');
try {
let newDate = moment(parts[2] + '-' + parts[1] + '-' + parts[0]).toDate();
// let newDate = new Date(+parts[2], +parts[1]-1, +parts[0]);
this.valor = newDate;
this.propagateChange(this.valor);
} catch (error) {
// return dataEntrada;
console.log(error);
}
}
writeValue(value: any) {
this.valor = value;
const datePipe = new DatePipe('pt-BR');
this.dataString = datePipe.transform(this.valor, 'dd/MM/yyyy');
}
propagateChange = (_: any) => { };
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched() {
}
}
Although there are multiple ways to achieve this
Setting styles in the Child Component
<app-date-input [childClass]='someCssClass'></app-date-input>
Now in DateInputComponent, create an #Input() property and in the HTML for the component, you can do something like
'<input [class]="input Variable Name">'

Resources