mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 21:55:52 +02:00
Archive commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[agAutoFocus]'
|
||||
})
|
||||
export class AgAutoFocus implements AfterViewInit {
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.element.nativeElement.focus(), 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Directive, OnInit, Input, Output, EventEmitter, ElementRef, OnDestroy } from '@angular/core';
|
||||
import { noop } from 'lodash';
|
||||
import { getButton, getX, getY, AnyEvent } from '../../../common/utils';
|
||||
|
||||
export interface AgDragEvent {
|
||||
event: AnyEvent;
|
||||
type: 'start' | 'drag' | 'end';
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
}
|
||||
|
||||
export interface AgDragOptions {
|
||||
relative?: 'self' | 'parent';
|
||||
prevent?: boolean;
|
||||
}
|
||||
|
||||
export function handleDrag(element: HTMLElement, emit: (event: AgDragEvent) => void, options: AgDragOptions = {}) {
|
||||
// typeof PointerEvent !== 'undefined'
|
||||
const eventSets = window.navigator.pointerEnabled ? [
|
||||
{ down: 'pointerdown', move: 'pointermove', up: 'pointerup' }, // , up2: 'pointercancel' },
|
||||
] : [
|
||||
{ down: 'mousedown', move: 'mousemove', up: 'mouseup' },
|
||||
{ down: 'touchstart', move: 'touchmove', up: 'touchend', up2: 'touchcancel' },
|
||||
];
|
||||
const emptyRect = { left: 0, top: 0 };
|
||||
let rect = emptyRect;
|
||||
let scrollLeft = 0;
|
||||
let scrollTop = 0;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let button = 0;
|
||||
let dragging = false;
|
||||
let lastEvent: any;
|
||||
|
||||
function setupScrollAndRect() {
|
||||
// TODO: fix issue with scroll
|
||||
switch (options.relative) {
|
||||
case 'self':
|
||||
rect = element.getBoundingClientRect();
|
||||
scrollLeft = -(window.scrollX || window.pageXOffset || 0);
|
||||
scrollTop = -(window.scrollY || window.pageYOffset || 0);
|
||||
break;
|
||||
case 'parent':
|
||||
rect = element.parentElement!.getBoundingClientRect();
|
||||
scrollLeft = element.parentElement!.scrollLeft;
|
||||
scrollTop = element.parentElement!.scrollTop;
|
||||
break;
|
||||
default:
|
||||
rect = emptyRect;
|
||||
scrollLeft = 0;
|
||||
scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function send(event: AnyEvent, type: 'start' | 'drag' | 'end') {
|
||||
const x = getX(event);
|
||||
const y = getY(event);
|
||||
|
||||
emit({
|
||||
event,
|
||||
type,
|
||||
x: x - rect.left + scrollLeft,
|
||||
y: y - rect.top + scrollTop,
|
||||
dx: x - startX,
|
||||
dy: y - startY,
|
||||
});
|
||||
}
|
||||
|
||||
const handlers = eventSets.map(events => {
|
||||
function move(e: any) {
|
||||
lastEvent = e;
|
||||
e.preventDefault();
|
||||
send(e, 'drag');
|
||||
}
|
||||
|
||||
function up(e: any) {
|
||||
if (getButton(e) === button) {
|
||||
// touchend event does not have x, y coordinates, use last touchmove event instead
|
||||
if (e.type !== 'touchend' && e.type !== 'touchcancel') {
|
||||
lastEvent = e;
|
||||
}
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
function end() {
|
||||
send(lastEvent, 'end');
|
||||
window.removeEventListener(events.move, move);
|
||||
window.removeEventListener(events.up, up);
|
||||
events.up2 && window.removeEventListener(events.up2, up);
|
||||
window.removeEventListener('blur', end);
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
function handler(e: any) {
|
||||
if (!dragging) {
|
||||
setupScrollAndRect();
|
||||
dragging = true;
|
||||
button = getButton(e);
|
||||
startX = getX(e);
|
||||
startY = getY(e);
|
||||
send(e, 'start');
|
||||
lastEvent = e;
|
||||
|
||||
window.addEventListener(events.move, move);
|
||||
window.addEventListener(events.up, up);
|
||||
events.up2 && window.addEventListener(events.up2, up);
|
||||
window.addEventListener('blur', end);
|
||||
e.stopPropagation();
|
||||
|
||||
if (options.prevent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener(events.down, handler);
|
||||
return () => element.removeEventListener(events.down, handler);
|
||||
});
|
||||
|
||||
return () => handlers.forEach(f => f());
|
||||
}
|
||||
|
||||
@Directive({ selector: '[agDrag]' })
|
||||
export class AgDrag implements OnInit, OnDestroy {
|
||||
@Input('agDragRelative') relative: 'self' | 'parent' | undefined = undefined;
|
||||
@Input('agDragPrevent') prevent = false;
|
||||
@Output('agDrag') drag = new EventEmitter<AgDragEvent>();
|
||||
private unsubscribe = noop;
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.unsubscribe = handleDrag(this.element.nativeElement, e => this.drag.emit(e), this);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Directive, OnInit, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: 'a[href]'
|
||||
})
|
||||
export class Anchor implements OnInit {
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngOnInit() {
|
||||
const a = this.element.nativeElement as HTMLAnchorElement;
|
||||
|
||||
if (/^(https?|mailto):/.test(a.href) && !a.target) {
|
||||
a.setAttribute('target', '_blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Directive, Input, Optional } from '@angular/core';
|
||||
import { NgModel } from '@angular/forms';
|
||||
|
||||
@Directive({
|
||||
selector: '[btnHighlight]',
|
||||
host: {
|
||||
'[class.btn-default]': '!on',
|
||||
'[class.btn-primary]': 'on',
|
||||
},
|
||||
})
|
||||
export class BtnHighlight {
|
||||
@Input() btnHighlight?: boolean = undefined;
|
||||
constructor(@Optional() private model?: NgModel) {
|
||||
}
|
||||
get on() {
|
||||
const value = this.btnHighlight;
|
||||
return (value === true || value === false || !this.model) ? value : !!this.model.value;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[btnHighlightDanger]',
|
||||
host: {
|
||||
'[class.btn-default]': '!btnHighlightDanger',
|
||||
'[class.btn-danger]': 'btnHighlightDanger',
|
||||
},
|
||||
})
|
||||
export class BtnHighlightDanger {
|
||||
@Input() btnHighlightDanger = false;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Component, Directive, Input, Output, EventEmitter, ElementRef, OnInit, Injectable, OnDestroy } from '@angular/core';
|
||||
import { noop } from 'lodash';
|
||||
import { AgDragEvent, handleDrag } from './agDrag';
|
||||
import { clamp, setTransform, removeItem, pointInRect } from '../../../common/utils';
|
||||
import { rect } from '../../../common/rect';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DraggableService {
|
||||
root?: ElementRef;
|
||||
draggedItem?: any;
|
||||
activeDropZone?: DraggableDrop<any>;
|
||||
dropZones: DraggableDrop<any>[] = [];
|
||||
get rootElement(): HTMLElement {
|
||||
return this.root ? this.root.nativeElement : document.body;
|
||||
}
|
||||
setActiveDropZone(dropZone: DraggableDrop<any> | undefined) {
|
||||
if (this.activeDropZone !== dropZone) {
|
||||
if (this.activeDropZone) {
|
||||
this.activeDropZone.setActive(false);
|
||||
}
|
||||
|
||||
this.activeDropZone = dropZone;
|
||||
|
||||
if (this.activeDropZone) {
|
||||
this.activeDropZone.setActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
startMove(element: HTMLElement, item: any) {
|
||||
this.setActiveDropZone(undefined);
|
||||
this.rootElement.appendChild(element);
|
||||
this.draggedItem = item;
|
||||
this.initRects();
|
||||
}
|
||||
endMove() {
|
||||
if (this.activeDropZone) {
|
||||
this.activeDropZone.drop.emit(this.draggedItem);
|
||||
this.setActiveDropZone(undefined);
|
||||
}
|
||||
|
||||
this.draggedItem = undefined;
|
||||
}
|
||||
addDropZone(dropZone: DraggableDrop<any>) {
|
||||
this.dropZones.push(dropZone);
|
||||
|
||||
if (this.draggedItem) {
|
||||
this.initRects();
|
||||
}
|
||||
}
|
||||
removeDropZone(dropZone: DraggableDrop<any>) {
|
||||
removeItem(this.dropZones, dropZone);
|
||||
|
||||
if (this.draggedItem) {
|
||||
this.initRects();
|
||||
}
|
||||
|
||||
if (this.activeDropZone === dropZone) {
|
||||
this.setActiveDropZone(undefined);
|
||||
}
|
||||
}
|
||||
updateHover(x: number, y: number) {
|
||||
if (this.draggedItem) {
|
||||
for (const zone of this.dropZones) {
|
||||
if (pointInRect(x, y, zone.rect)) {
|
||||
this.setActiveDropZone(zone);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setActiveDropZone(undefined);
|
||||
}
|
||||
}
|
||||
private initRects() {
|
||||
this.dropZones.forEach(i => i.initRect());
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'draggable-outlet',
|
||||
template: `<div></div>`,
|
||||
styles: [`:host { position: fixed; top: 0; left: 0; z-index: 10000; }`],
|
||||
})
|
||||
export class DraggableOutlet {
|
||||
constructor(element: ElementRef, service: DraggableService) {
|
||||
service.root = element;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({ selector: '[draggableDrop]' })
|
||||
export class DraggableDrop<T> implements OnInit, OnDestroy {
|
||||
@Input('draggablePad') pad = 0;
|
||||
@Output('draggableDrop') drop = new EventEmitter<T>();
|
||||
rect = rect(0, 0, 0, 0);
|
||||
constructor(private element: ElementRef, private service: DraggableService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.service.addDropZone(this);
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.service.removeDropZone(this);
|
||||
}
|
||||
setActive(active: boolean) {
|
||||
const element = this.element.nativeElement as HTMLElement;
|
||||
|
||||
if (active) {
|
||||
element.classList.add('draggable-hover');
|
||||
} else {
|
||||
element.classList.remove('draggable-hover');
|
||||
}
|
||||
}
|
||||
initRect() {
|
||||
const element = this.element.nativeElement as HTMLElement;
|
||||
const clientBounds = element.getBoundingClientRect();
|
||||
this.rect.x = clientBounds.left - this.pad;
|
||||
this.rect.y = clientBounds.top - this.pad;
|
||||
this.rect.w = clientBounds.width + 2 * this.pad;
|
||||
this.rect.h = clientBounds.height + 2 * this.pad;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[draggableItem]',
|
||||
host: {
|
||||
'[style.touch-action]': `touchAction`,
|
||||
}
|
||||
})
|
||||
export class DraggableItem<T> implements OnInit, OnDestroy {
|
||||
@Input('draggableItem') item: T | undefined;
|
||||
@Output('draggableDrag') dragStarted = new EventEmitter<void>();
|
||||
private startX = 0;
|
||||
private startY = 0;
|
||||
private draggable?: HTMLElement;
|
||||
private width = 0;
|
||||
private height = 0;
|
||||
private unsubscribeDrag = noop;
|
||||
private _disabled = false;
|
||||
constructor(private element: ElementRef, private service: DraggableService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.setupDragEvents();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.unsubscribeDrag();
|
||||
}
|
||||
get touchAction() {
|
||||
return this.disabled ? 'inherit' : 'none';
|
||||
}
|
||||
@Input('draggableDisabled') get disabled() {
|
||||
return this._disabled;
|
||||
}
|
||||
set disabled(value) {
|
||||
if (this._disabled !== value) {
|
||||
this._disabled = value;
|
||||
this.setupDragEvents();
|
||||
}
|
||||
}
|
||||
private setupDragEvents() {
|
||||
this.unsubscribeDrag();
|
||||
this.unsubscribeDrag = noop;
|
||||
|
||||
if (!this.disabled) {
|
||||
this.unsubscribeDrag = handleDrag(this.element.nativeElement, e => this.drag(e), { prevent: true });
|
||||
}
|
||||
}
|
||||
drag(e: AgDragEvent) {
|
||||
if (this.item && !this.disabled && !this.draggable && (Math.abs(e.dx) > 5 || Math.abs(e.dy) > 5)) {
|
||||
const element = this.element.nativeElement as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
this.startX = rect.left;
|
||||
this.startY = rect.top;
|
||||
this.draggable = element.cloneNode(true) as HTMLElement;
|
||||
this.draggable.style.position = 'absolute';
|
||||
this.draggable.style.width = `${rect.width}px`;
|
||||
this.draggable.style.height = `${rect.height}px`;
|
||||
this.draggable.style.margin = '0';
|
||||
this.draggable.classList.add('draggable-dragging');
|
||||
this.width = rect.width;
|
||||
this.height = rect.height;
|
||||
|
||||
const src = element.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
|
||||
const dst = this.draggable.querySelectorAll('canvas') as NodeListOf<HTMLCanvasElement>;
|
||||
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
const context = dst.item(i).getContext('2d');
|
||||
context && context.drawImage(src.item(i), 0, 0);
|
||||
}
|
||||
|
||||
this.service.startMove(this.draggable, this.item!);
|
||||
this.dragStarted.emit();
|
||||
}
|
||||
|
||||
if (this.draggable) {
|
||||
if (e.type === 'end') {
|
||||
this.draggable!.parentNode!.removeChild(this.draggable!);
|
||||
this.draggable = undefined;
|
||||
this.service.endMove();
|
||||
} else {
|
||||
const x = clamp(this.startX + e.dx, 0, window.innerWidth - this.width);
|
||||
const y = clamp(this.startY + e.dy, 0, window.innerHeight - this.height);
|
||||
setTransform(this.draggable, `translate3d(${x}px, ${y}px, 0px)`);
|
||||
this.service.updateHover(e.x, e.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const draggableComponents = [DraggableOutlet, DraggableItem, DraggableDrop];
|
||||
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
Directive, HostListener, Input, Output, EventEmitter, TemplateRef, ViewContainerRef, ContentChild,
|
||||
Renderer2, ElementRef, EmbeddedViewRef, Component, Injectable
|
||||
} from '@angular/core';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { focusFirstElement } from '../../../client/htmlUtils';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DropdownOutletService {
|
||||
viewContainer?: ViewContainerRef;
|
||||
rootElement?: HTMLElement;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'dropdown-outlet',
|
||||
template: `<ng-template></ng-template>`,
|
||||
})
|
||||
export class DropdownOutlet {
|
||||
constructor(service: DropdownOutletService, viewContainer: ViewContainerRef, element: ElementRef) {
|
||||
service.viewContainer = viewContainer;
|
||||
service.rootElement = element.nativeElement.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dropdownMenu]',
|
||||
})
|
||||
export class DropdownMenu {
|
||||
ref?: EmbeddedViewRef<any>;
|
||||
id = uniqueId('dropdown-menu-');
|
||||
private onClose?: () => void;
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private renderer: Renderer2,
|
||||
private service: DropdownOutletService,
|
||||
) {
|
||||
}
|
||||
private get root(): HTMLElement {
|
||||
return this.ref && this.ref.rootNodes[0];
|
||||
}
|
||||
open(useOutlet: boolean, rootElement: HTMLElement) {
|
||||
if (!this.ref) {
|
||||
if (useOutlet) {
|
||||
this.ref = this.service.viewContainer!.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.ref = this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
}
|
||||
|
||||
const { renderer, root } = this;
|
||||
|
||||
renderer.addClass(root, 'show');
|
||||
renderer.setAttribute(root, 'id', this.id);
|
||||
|
||||
if (useOutlet) {
|
||||
const positionMenu = () => {
|
||||
const rect = rootElement.getBoundingClientRect();
|
||||
const menuRect = root.getBoundingClientRect();
|
||||
let transform: string;
|
||||
|
||||
if ((rect.bottom + menuRect.height) > window.innerHeight) {
|
||||
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top - menuRect.height)}px, 0)`;
|
||||
renderer.addClass(root, 'dropdown-menu-up');
|
||||
} else {
|
||||
transform = `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.bottom)}px, 0)`;
|
||||
renderer.removeClass(root, 'dropdown-menu-up');
|
||||
}
|
||||
|
||||
renderer.setStyle(root, 'transform', transform);
|
||||
};
|
||||
|
||||
renderer.addClass(root, 'dropdown-in-outlet');
|
||||
positionMenu();
|
||||
|
||||
const closeDropdown = () => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
document.addEventListener('scroll', closeDropdown, true);
|
||||
window.addEventListener('resize', closeDropdown, true);
|
||||
|
||||
this.onClose = () => {
|
||||
document.removeEventListener('scroll', closeDropdown, true);
|
||||
window.removeEventListener('resize', closeDropdown, true);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
close() {
|
||||
if (this.ref) {
|
||||
this.ref.destroy();
|
||||
this.ref = undefined;
|
||||
}
|
||||
|
||||
if (this.onClose) {
|
||||
this.onClose();
|
||||
this.onClose = undefined;
|
||||
}
|
||||
}
|
||||
checkTarget(e: Event) {
|
||||
return this.root && this.root.contains(e.target as any);
|
||||
}
|
||||
focusFirstElement() {
|
||||
if (this.root) {
|
||||
focusFirstElement(this.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dropdown]',
|
||||
exportAs: 'ag-dropdown',
|
||||
host: {
|
||||
'[class.show]': 'isOpen',
|
||||
},
|
||||
})
|
||||
export class Dropdown {
|
||||
dropdownToggle?: DropdownToggle;
|
||||
@ContentChild(DropdownMenu, { static: false }) menu!: DropdownMenu;
|
||||
@Input() autoClose: boolean | 'outsideClick' = true;
|
||||
@Input() preventAutoCloseOnOutlet = false;
|
||||
@Input() hookToCanvas = false;
|
||||
@Input() focusOnOpen = true;
|
||||
@Input() focusOnClose = true;
|
||||
@Input() useOutlet = false;
|
||||
@Input() isOpen = false;
|
||||
@Output() isOpenChange = new EventEmitter<boolean>();
|
||||
get menuId() {
|
||||
return this.isOpen ? this.menu.id : '';
|
||||
}
|
||||
constructor(private element: ElementRef, private service: DropdownOutletService) {
|
||||
}
|
||||
open() {
|
||||
if (!this.isOpen) {
|
||||
this.isOpen = true;
|
||||
this.isOpenChange.emit(true);
|
||||
this.menu.open(this.useOutlet, this.element.nativeElement);
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.closeHandler);
|
||||
document.addEventListener('keydown', this.closeHandler);
|
||||
|
||||
if (this.focusOnOpen) {
|
||||
this.menu.focusFirstElement();
|
||||
}
|
||||
|
||||
if (this.hookToCanvas) {
|
||||
const canvas = document.getElementById('canvas');
|
||||
|
||||
if (canvas) {
|
||||
canvas.addEventListener('touchstart', this.canvasCloseHandler);
|
||||
canvas.addEventListener('mousedown', this.canvasCloseHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
close() {
|
||||
if (this.isOpen) {
|
||||
this.isOpen = false;
|
||||
this.isOpenChange.emit(false);
|
||||
this.menu.close();
|
||||
|
||||
if (this.focusOnClose && this.dropdownToggle) {
|
||||
this.dropdownToggle.focus();
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this.closeHandler);
|
||||
document.removeEventListener('keydown', this.closeHandler);
|
||||
|
||||
if (this.hookToCanvas) {
|
||||
const canvas = document.getElementById('canvas');
|
||||
|
||||
if (canvas) {
|
||||
canvas.removeEventListener('touchstart', this.canvasCloseHandler);
|
||||
canvas.removeEventListener('mousedown', this.canvasCloseHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
toggle() {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
private closeHandler: any = (e: KeyboardEvent) => {
|
||||
if (
|
||||
!e.keyCode
|
||||
&& (this.autoClose || (this.dropdownToggle && this.dropdownToggle.checkTarget(e)))
|
||||
&& !(this.preventAutoCloseOnOutlet && this.service.rootElement && this.service.rootElement.contains(e.target as any))
|
||||
&& !(this.autoClose === 'outsideClick' && this.menu.checkTarget(e))
|
||||
) {
|
||||
this.close();
|
||||
} else if (this.autoClose && e.keyCode === 27) { // esc
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
private canvasCloseHandler: any = () => this.close();
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: '[dropdownToggle]',
|
||||
host: {
|
||||
'aria-haspopup': 'true',
|
||||
'[attr.aria-expanded]': 'dropdown.isOpen',
|
||||
'[attr.aria-controls]': 'dropdown.isOpen ? dropdown.menuId : undefined',
|
||||
},
|
||||
})
|
||||
export class DropdownToggle {
|
||||
constructor(private element: ElementRef, public dropdown: Dropdown) {
|
||||
dropdown.dropdownToggle = this;
|
||||
}
|
||||
@HostListener('click')
|
||||
click() {
|
||||
this.dropdown.toggle();
|
||||
}
|
||||
checkTarget(e: Event) {
|
||||
return this.element.nativeElement.contains(e.target);
|
||||
}
|
||||
focus() {
|
||||
this.element.nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export const dropdownDirectives = [Dropdown, DropdownToggle, DropdownMenu, DropdownOutlet];
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Directive, ElementRef, Input, HostListener, HostBinding, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[fixToTop]',
|
||||
})
|
||||
export class FixToTop {
|
||||
@Input() fixToTopOffset = 0;
|
||||
@Output() fixToTop = new EventEmitter<boolean>();
|
||||
@HostBinding('class.fixed-to-top') fixed = false;
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
@HostListener('window:scroll')
|
||||
scroll() {
|
||||
const element = this.element.nativeElement as HTMLElement;
|
||||
const { top } = element.getBoundingClientRect();
|
||||
|
||||
if (this.fixed !== top < this.fixToTopOffset) {
|
||||
this.fixed = top < this.fixToTopOffset;
|
||||
this.fixToTop.emit(this.fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Directive, AfterViewInit, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[focusTitle]',
|
||||
host: {
|
||||
'tabindex': '-1',
|
||||
},
|
||||
})
|
||||
export class FocusTitle implements AfterViewInit {
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => this.element.nativeElement.focus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Directive, Input, OnDestroy, ElementRef, OnInit } from '@angular/core';
|
||||
import { isParentOf, focusFirstElement, findFocusableElements } from '../../../client/htmlUtils';
|
||||
import { isMobile } from '../../../client/data';
|
||||
|
||||
@Directive({
|
||||
selector: '[focusTrap]',
|
||||
})
|
||||
export class FocusTrap implements OnInit, OnDestroy {
|
||||
private on = true;
|
||||
private lastActiveElement?: HTMLElement;
|
||||
@Input() set focusTrap(value: boolean) {
|
||||
if (this.on !== value) {
|
||||
this.on = value;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngOnInit() {
|
||||
this.update();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.focusTrap = false;
|
||||
}
|
||||
private update() {
|
||||
if (!isMobile) {
|
||||
if (this.on) {
|
||||
this.lastActiveElement = document.activeElement as HTMLElement;
|
||||
document.addEventListener('focusin', this.focus);
|
||||
|
||||
if (!isParentOf(this.element.nativeElement, this.lastActiveElement)) {
|
||||
setTimeout(() => this.lastActiveElement = focusFirstElement(this.element.nativeElement));
|
||||
}
|
||||
} else {
|
||||
this.lastActiveElement = undefined;
|
||||
document.removeEventListener('focusin', this.focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
private focus = (e: Event) => {
|
||||
if (isParentOf(this.element.nativeElement, e.target as any)) {
|
||||
this.lastActiveElement = e.target as any;
|
||||
} else {
|
||||
const focusable = findFocusableElements(this.element.nativeElement);
|
||||
|
||||
if (focusable.length) {
|
||||
if (this.lastActiveElement === focusable[0]) {
|
||||
this.lastActiveElement = focusable[focusable.length - 1];
|
||||
} else {
|
||||
this.lastActiveElement = focusable[0];
|
||||
}
|
||||
|
||||
this.lastActiveElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Directive, Input, TemplateRef, ViewContainerRef, OnDestroy, EmbeddedViewRef, AfterViewInit } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { hasFeatureFlag, featureFlagsChanged } from '../../../client/clientUtils';
|
||||
import { Model } from '../../services/model';
|
||||
|
||||
@Directive({
|
||||
selector: '[hasFeature]',
|
||||
})
|
||||
export class HasFeature implements AfterViewInit, OnDestroy {
|
||||
private subscriptions: Subscription[] = [];
|
||||
private showing = false;
|
||||
private _flag: string | undefined = undefined;
|
||||
private _orMod = false;
|
||||
private _alsoIf = true;
|
||||
private ref?: EmbeddedViewRef<any>;
|
||||
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef, private model: Model) {
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
this.subscriptions.push(featureFlagsChanged.subscribe(() => this.update()));
|
||||
this.subscriptions.push(this.model.accountChanged.subscribe(() => this.update()));
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(s => s.unsubscribe());
|
||||
}
|
||||
@Input()
|
||||
set hasFeature(value: string | undefined) {
|
||||
if (this._flag !== value) {
|
||||
this._flag = value;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
@Input()
|
||||
set hasFeatureOrMod(value: boolean) {
|
||||
if (this._orMod !== value) {
|
||||
this._orMod = value;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
@Input()
|
||||
set hasFeatureAlso(value: boolean) {
|
||||
if (this._alsoIf !== value) {
|
||||
this._alsoIf = value;
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
private update() {
|
||||
const show = this._alsoIf && (hasFeatureFlag(this._flag as any) || (this._orMod && this.model.isMod));
|
||||
|
||||
if (this.showing !== show) {
|
||||
this.showing = show;
|
||||
|
||||
if (show) {
|
||||
this.ref = this.ref || this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
this.ref = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { findParentElement } from '../../../client/htmlUtils';
|
||||
|
||||
@Directive({
|
||||
selector: '[labelledBy]',
|
||||
})
|
||||
export class LabelledBy implements OnInit {
|
||||
@Input('labelledBy') selector!: string;
|
||||
constructor(private element: ElementRef) {
|
||||
}
|
||||
ngOnInit() {
|
||||
const element = this.element.nativeElement as HTMLElement;
|
||||
const target = findParentElement(element, this.selector);
|
||||
const id = element.id = element.id || uniqueId('labelled-by-');
|
||||
|
||||
if (target) {
|
||||
target.setAttribute('aria-labelledby', id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Directive, HostBinding } from '@angular/core';
|
||||
import { RouterLinkActive } from '@angular/router';
|
||||
|
||||
@Directive({
|
||||
selector: '[linkCurrent]',
|
||||
})
|
||||
export class LinkCurrent {
|
||||
constructor(private routerLinkActive: RouterLinkActive) {
|
||||
}
|
||||
@HostBinding('attr.aria-current')
|
||||
get current() {
|
||||
return this.routerLinkActive.isActive ? 'true' : undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Directive, Input, HostBinding } from '@angular/core';
|
||||
import { getUrl } from '../../../client/rev';
|
||||
|
||||
@Directive({
|
||||
selector: '[revSrc]',
|
||||
})
|
||||
export class RevSrc {
|
||||
@HostBinding() get src() {
|
||||
return this.revSrc && getUrl(this.revSrc);
|
||||
}
|
||||
@Input() revSrc?: string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Directive, Input, Host, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Tabset } from '../tabset/tabset';
|
||||
import { StorageService } from '../../services/storageService';
|
||||
|
||||
@Directive({
|
||||
selector: '[saveActiveTab]',
|
||||
})
|
||||
export class SaveActiveTab implements OnInit, OnDestroy {
|
||||
@Input('saveActiveTab') key!: string;
|
||||
private subscription?: Subscription;
|
||||
constructor(@Host() private tabset: Tabset, private storage: StorageService) {
|
||||
}
|
||||
ngOnInit() {
|
||||
// this.tabset.activeIndex = parseInt(this.storage.getItem(this.key) || '0', 10);
|
||||
this.tabset.select(parseInt(this.storage.getItem(this.key) || '0', 10));
|
||||
this.subscription = this.tabset.activeIndexChange.subscribe((i: number) => {
|
||||
this.storage.setItem(this.key, i.toString());
|
||||
});
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user