UNPKG

@syncfusion/ej2-buttons

Version:

A package of feature-rich Essential JS 2 components such as Button, CheckBox, RadioButton and Switch.

1,341 lines (1,248 loc) 72.8 kB
import { BaseEventArgs, EmitType, Event, ChildProperty, Collection, Complex, Component, INotifyPropertyChanged, NotifyPropertyChanges, Property, getUniqueID, EventHandler, isRippleEnabled, removeClass, addClass, attributes, animationMode } from '@syncfusion/ej2-base'; import { select, extend, deleteObject, KeyboardEvents, append, rippleEffect, remove, closest, selectAll, KeyboardEventArgs, isNullOrUndefined, compile, formatUnit, Animation, AnimationModel, Effect as baseEffect } from '@syncfusion/ej2-base'; import { SpeedDialItemModel, SpeedDialModel, RadialSettingsModel, SpeedDialAnimationSettingsModel } from './speed-dial-model'; import { Fab, FabPosition } from './../floating-action-button/index'; import { IconPosition } from './../button/index'; const topPosition: string[] = ['TopLeft', 'TopCenter', 'TopRight']; const bottomPosition: string[] = ['BottomLeft', 'BottomCenter', 'BottomRight']; const leftPosition: string[] = ['TopLeft', 'MiddleLeft', 'BottomLeft']; const rightPosition: string[] = ['TopRight', 'MiddleRight', 'BottomRight']; const SDHIDDEN: string = 'e-speeddial-hidden'; const FIXEDSD: string = 'e-speeddial-fixed'; const SPEEDDIAL: string = 'e-speeddial'; const RTLCLASS: string = 'e-rtl'; const HOVERSD: string = 'e-speeddial-hover-open'; const RADIALSD: string = 'e-speeddial-radial'; const LINEARSD: string = 'e-speeddial-linear'; const TEMPLATESD: string = 'e-speeddial-template'; const SDTEMPLATECONTAINER: string = 'e-speeddial-template-container'; const SDOVERLAY: string = 'e-speeddial-overlay'; const SDPOPUP: string = 'e-speeddial-popup'; const SDUL: string = 'e-speeddial-ul'; const SDLI: string = 'e-speeddial-li'; const SDACTIVELI: string = 'e-speeddial-li-active'; const SDLIICON: string = 'e-speeddial-li-icon'; const SDLITEXT: string = 'e-speeddial-li-text'; const SDLITEXTONLY: string = 'e-speeddial-text-li'; const DISABLED: string = 'e-disabled'; const SDVERTICALBOTTOM: string = 'e-speeddial-vert-bottom'; const SDVERTICALRIGHT: string = 'e-speeddial-vert-right'; const SDHORIZONTALTOP: string = 'e-speeddial-horz-top'; const SDHORIZONTALLEFT: string = 'e-speeddial-horz-left'; const SDHORIZONTALRIGHT: string = 'e-speeddial-horz-right'; const SDOVERFLOW: string = 'e-speeddial-overflow'; const SDVERTOVERFLOW: string = 'e-speeddial-vert-overflow'; const SDHORZOVERFLOW: string = 'e-speeddial-horz-overflow'; const SDTOP: string = 'e-speeddial-top'; const SDBOTTOM: string = 'e-speeddial-bottom'; const SDRIGHT: string = 'e-speeddial-right'; const SDLEFT: string = 'e-speeddial-left'; const SDMIDDLE: string = 'e-speeddial-middle'; const SDCENTER: string = 'e-speeddial-center'; const SDTOPLEFT: string = 'e-speeddial-top-left'; const SDBOTTOMRIGHT: string = 'e-speeddial-bottom-right'; const SDTOPRIGHT: string = 'e-speeddial-top-right'; const SDBOTTOMLEFT: string = 'e-speeddial-bottom-left'; const SDVERTDIST: string = '--speeddialVertDist'; const SDHORZDIST: string = '--speeddialHorzDist'; const SDRADICALANGLE: string = '--speeddialRadialAngle'; const SDRADICALOFFSET: string = '--speeddialRadialOffset'; const SDRADICALMINHEIGHT: string = '--speeddialRadialMinHeight'; const SDRADICALMINWIDTH: string = '--speeddialRadialMinWidth'; const SDOVERFLOWLIMIT: string = '--speeddialOverflowLimit'; const SDRADICALHORZDIST: string = '--speeddialRadialHorzDist'; /** * Defines the display mode of speed dial action items in SpeedDial */ export enum SpeedDialMode { /** * SpeedDial items are displayed in linear order like list. */ Linear = 'Linear', /** * SpeedDial items are displayed like radial menu in radial direction (circular direction). */ Radial = 'Radial' } /** * Defines the speed dial action items display direction when mode is Linear. */ export enum LinearDirection { /** * Speed dial action items are displayed vertically above the button of Speed Dial. */ Up = 'Up', /** * Speed dial action items are displayed vertically below the button of Speed Dial. */ Down = 'Down', /** * Speed dial action items are displayed horizontally on the button's right side. */ Right = 'Right', /** * Speed dial action items are displayed horizontally on the button's left side. */ Left = 'Left', /** * Speed dial action items are displayed vertically above or below the button of Speed Dial based on the position. * If Position is TopRight, TopLeft, TopCenter, the items are displayed vertically below the button else above the button. */ Auto = 'Auto' } /** * Defines the speed dial action items order, when mode is Radial. */ export enum RadialDirection { /** * SpeedDial items are arranged in clockwise direction. */ Clockwise = 'Clockwise', /** * SpeedDial items are shown in anti-clockwise direction. */ AntiClockwise = 'AntiClockwise', /** * SpeedDial items are shown clockwise or anti-clockwise based on the position. */ Auto = 'Auto' } /** * Defines the animation effect applied when open and close the speed dial items. */ export enum SpeedDialAnimationEffect { /** * SpeedDial open/close actions occur with the Fade animation effect. */ Fade = 'Fade', /** * SpeedDial open/close actions occur with the FadeZoom animation effect. */ FadeZoom = 'FadeZoom', /** * SpeedDial open/close actions occur with the FlipLeftDown animation effect. */ FlipLeftDown = 'FlipLeftDown', /** * SpeedDial open/close actions occur with the FlipLeftUp animation effect. */ FlipLeftUp = 'FlipLeftUp', /** * SpeedDial open/close actions occur with the FlipRightDown animation effect. */ FlipRightDown = 'FlipRightDown', /** * SpeedDial open/close actions occur with the FlipRightUp animation effect. */ FlipRightUp = 'FlipRightUp', /** * SpeedDial open/close actions occur with the FlipXDown animation effect. */ FlipXDown = 'FlipXDown', /** * SpeedDial open/close actions occur with the FlipXUp animation effect. */ FlipXUp = 'FlipXUp', /** * SpeedDial open/close actions occur with the FlipYLeft animation effect. */ FlipYLeft = 'FlipYLeft', /** * SpeedDial open/close actions occur with the FlipYRight animation effect. */ FlipYRight = 'FlipYRight', /** * SpeedDial open/close actions occur with the SlideBottom animation effect. */ SlideBottom = 'SlideBottom', /** * SpeedDial open/close actions occur with the SlideLeft animation effect. */ SlideLeft = 'SlideLeft', /** * SpeedDial open/close actions occur with the SlideRight animation effect. */ SlideRight = 'SlideRight', /** * SpeedDial open/close actions occur with the SlideTop animation effect. */ SlideTop = 'SlideTop', /** * SpeedDial open/close actions occur with the Zoom animation effect. */ Zoom = 'Zoom', /** * SpeedDial open/close actions occur without any animation effect. */ None = 'None' } /** * Provides information about the beforeOpen and beforeClose event callback. */ export interface SpeedDialBeforeOpenCloseEventArgs extends BaseEventArgs { /** * Provides the popup element of the speed dial. */ element: HTMLElement; /** * Provides the original event which triggered the open/close action of speed dial. */ event: Event; /** * Defines whether the to cancel the open/close action of speed dial. */ cancel: boolean; } /** * Provides information about the open and close event callback. */ export interface SpeedDialOpenCloseEventArgs extends BaseEventArgs { /** * Provides the popup element of the speed dial. */ element: HTMLElement; } /** * Provides information about the beforeItemRender and clicked event callback. */ export interface SpeedDialItemEventArgs extends BaseEventArgs { /** * Provides speed dial item element. */ element: HTMLElement; /** * Provides speed dial item. */ item: SpeedDialItemModel; /** * Provides the original event. */ event?: Event; } /** * AProvides options to customize the animation applied while opening and closing the popup of SpeedDial. */ export class SpeedDialAnimationSettings extends ChildProperty<SpeedDialAnimationSettings> { /** * Defines the type of animation effect used for opening and closing of the Speed Dial items. * * @isenumeration true * @default SpeedDialAnimationEffect.Fade * @asptype SpeedDialAnimationEffect */ @Property('Fade') public effect: string | SpeedDialAnimationEffect; /** * Defines the duration in milliseconds that the animation takes to open or close the popup. * * @default 400 * @aspType int */ @Property(400) public duration: number; /** * Defines the delay before starting the animation. * * @default 0 * @aspType int */ @Property(0) public delay: number; } /** * Provides the options to customize the speed dial action buttons when mode of SpeedDial is Radial. */ export class RadialSettings extends ChildProperty<RadialSettings> { /** * Defines speed dial action items placement order. * The possible values are * * Clockwise * * AntiClockwise * * Auto * * @isenumeration true * @default RadialDirection.Auto * @asptype RadialDirection */ @Property('Auto') public direction: string | RadialDirection; /** * Defines end angle of speed dial items placement. The accepted value range is 0 to 360. * When a value is outside the accepted value range, then the provided value is ignored, and the angle is calculated based on the position. * * @default -1 * @aspType int */ @Property(-1) public endAngle: number; /** * Defines distance of speed dial items placement from the button of Speed Dial. * * @default '100px' * @aspType string */ @Property('100px') public offset: string | number; /** * Defines start angle of speed dial items placement. The accepted value range is 0 to 360. * When a value is outside the accepted value range, then the provided value is ignored, and the angle is calculated based on the position. * * @default -1 * @aspType int */ @Property(-1) public startAngle: number; } /** * Defines the items of Floating Action Button. */ export class SpeedDialItem extends ChildProperty<SpeedDialItem> { /** * Defines one or more CSS classes to include an icon or image in speed dial item. * * @default '' */ @Property('') public iconCss: string; /** * Defines a unique value for the SpeedDialItem which can be used to identify the item in event args. * * @default '' */ @Property('') public id: string; /** * Defines the text content of SpeedDialItem. * Text won't be visible when mode is Radial. * Also, in Linear mode, text won't be displayed when direction is Left or Right. * * @default '' */ @Property('') public text: string; /** * Defines the title of SpeedDialItem to display tooltip. * * @default '' */ @Property('') public title: string; /** * Defines whether to enable or disable the SpeedDialItem. * * @default false */ @Property(false) public disabled: boolean; } /** * The SpeedDial component that appears in front of all the contents of the page and displays list of action buttons on click which is an extended version of FAB. * The button of speed dial is positioned in relative to a view port of browser or the . * It can display a menu of related actions or a custom content popupTemplate>. * */ @NotifyPropertyChanges export class SpeedDial extends Component<HTMLButtonElement> implements INotifyPropertyChanged { /** * Provides options to customize the animation applied while opening and closing the popup of speed dial * {% codeBlock src='speeddial/animation/index.md' %}{% endcodeBlock %} * * @default { effect: 'Fade', duration: 400, delay: 0 } */ @Complex<SpeedDialAnimationSettingsModel>({}, SpeedDialAnimationSettings) public animation: SpeedDialAnimationSettingsModel; /** * Defines the content for the button of SpeedDial. * * @default '' */ @Property('') public content: string; /** * Defines one or more CSS classes to include an icon or image to denote the speed dial is opened and displaying menu items. * * @default '' */ @Property('') public closeIconCss: string; /** * Defines one or more CSS classes to customize the appearance of SpeedDial. * * @default '' */ @Property('') public cssClass: string; /** * Defines the speed dial item display direction when mode is linear . * The possible values are * * Up * * Down * * Left * * Right * * Auto * * @isenumeration true * @default LinearDirection.Auto * @asptype LinearDirection */ @Property('Auto') public direction: string | LinearDirection; /** * Defines whether to enable or disable the SpeedDial. * * @default false. */ @Property(false) public disabled: boolean; /** * Defines the position of icon in the button of speed dial. * The possible values are: * * Left * * Right * * @isenumeration true * @default IconPosition.Left * @asptype IconPosition */ @Property('Left') public iconPosition: string | IconPosition; /** * Defines the list of SpeedDial items. * * @default [] */ @Collection<SpeedDialItemModel>([], SpeedDialItem) public items: SpeedDialItemModel[]; /** * Defines the template content for the speed dial item. * {% codeBlock src='speeddial/itemTemplate/index.md' %}{% endcodeBlock %} * * @default '' * @angularType string | object * @reactType string | function | JSX.Element * @vueType string | function * @aspType string */ @Property('') public itemTemplate: string | Function; /** * Defines the display mode of speed dial action items. * The possible values are: * * Linear * * Radial * {% codeBlock src='speeddial/mode/index.md' %}{% endcodeBlock %} * * @isenumeration true * @default SpeedDialMode.Linear * @asptype SpeedDialMode */ @Property('Linear') public mode: string | SpeedDialMode; /** * Defines one or more CSS classes to include an icon or image for the button of SpeedDial when it's closed. * * @default '' */ @Property('') public openIconCss: string; /** * Defines whether to open the popup when the button of SpeedDial is hovered. * By default, SpeedDial opens popup on click action. * * @default false */ @Property(false) public opensOnHover: boolean; /** * Defines the position of the button of Speed Dial relative to target. * Defines the position of the FAB relative to target. * The possible values are: * * TopLeft: Positions the FAB at the target's top left corner. * * TopCenter: Positions the FAB at the target's top left corner. * * TopRight: Positions the FAB at the target's top left corner. * * MiddleLeft: Positions the FAB at the target's top left corner. * * MiddleCenter: Positions the FAB at the target's top left corner. * * MiddleRight: Positions the FAB at the target's top left corner. * * BottomLeft: Positions the FAB at the target's top left corner. * * BottomCenter: Places the FAB on the bottom-center position of the target. * * BottomRight: Positions the FAB at the target's bottom right corner. * * To refresh the position of FAB on target resize, use refreshPosition method. * The position will be refreshed automatically when browser resized. * * @isenumeration true * @default FabPosition.BottomRight * @asptype FabPosition */ @Property('BottomRight') public position: string | FabPosition; /** * Defines whether the speed dial popup can be displayed as modal or modal less. * When enabled, the Speed dial creates an overlay that disables interaction with other elements other than speed dial items. * If user clicks anywhere other than speed dial items then popup will get closed. * * @default false. */ @Property(false) public modal: boolean; /** * Defines a template content for popup of SpeedDial. * * @default '' * @angularType string | object * @reactType string | function | JSX.Element * @vueType string | function * @aspType string */ @Property('') public popupTemplate: string | Function; /** * Provides the options to customize the speed dial action buttons when mode of speed dial is radial * {% codeBlock src='speeddial/radialSettings/index.md' %}{% endcodeBlock %} * * @default { startAngle: null, endAngle: null, direction: 'Auto' } */ @Complex<RadialSettingsModel>({}, RadialSettings) public radialSettings: RadialSettingsModel; /** * Defines the selector that points to the element in which the button of SpeedDial will be positioned. * By default button is positioned based on viewport of browser. * The target element must have relative position, else Button will get positioned based on the closest element which has relative position. * * @default '' */ @Property('') public target: string | HTMLElement; /** * Defines whether the SpeedDial is visible or hidden. * * @default true. */ @Property(true) public visible: boolean; /** * Specifies whether the SpeedDial acts as the primary. * * @default true */ @Property(true) public isPrimary: boolean; /** * Event callback that is raised before the speed dial popup is closed. * * @event beforeClose */ @Event() public beforeClose: EmitType<SpeedDialBeforeOpenCloseEventArgs>; /** * Event callback that is raised before rendering the speed dial item. * * @event beforeItemRender */ @Event() public beforeItemRender: EmitType<SpeedDialItemEventArgs>; /** * Event callback that is raised before the speed dial popup is opened. * * @event beforeOpen */ @Event() public beforeOpen: EmitType<SpeedDialBeforeOpenCloseEventArgs>; /** * Event callback that is raised after rendering the speed dial. * * @event created */ @Event() public created: EmitType<Event>; /** * Event callback that is raised when a speed dial action item is clicked. * * @event clicked */ @Event() public clicked: EmitType<SpeedDialItemEventArgs>; /** * Event callback that is raised when the SpeedDial popup is closed. * * @event onClose */ @Event() public onClose: EmitType<SpeedDialOpenCloseEventArgs>; /** * Event callback that is raised when the SpeedDial popup is opened. * * @event onOpen */ @Event() public onOpen: EmitType<SpeedDialOpenCloseEventArgs>; private fab: Fab; private targetEle: HTMLElement; private isFixed: boolean; private isMenuOpen: boolean = false; private popupEle: HTMLElement; private overlayEle: HTMLElement; private actualLinDirection: string; private isClock: boolean = true; private isVertical: boolean = true; private isControl: boolean = false; private focusedIndex: number = -1; private keyboardModule: KeyboardEvents; private popupKeyboardModule: KeyboardEvents; private documentKeyboardModule: KeyboardEvents; private removeRippleEffect: Function; private keyConfigs: { [key: string]: string }; /** * Constructor for creating the widget * * @param {SpeedDialModel} options - Specifies the floating action button model * @param {string|HTMLButtonElement} element - Specifies the target element */ constructor(options?: SpeedDialModel, element?: string | HTMLButtonElement) { super(options, <string | HTMLButtonElement>element); } /** * Initialize the control rendering * * @returns {void} * @private */ protected render(): void { this.initialize(); } protected preRender(): void { this.keyConfigs = { space: 'space', enter: 'enter', end: 'end', home: 'home', moveDown: 'downarrow', moveLeft: 'leftarrow', moveRight: 'rightarrow', moveUp: 'uparrow', esc: 'escape' }; this.validateDirection(); } /** * Get the properties to be maintained in the persisted state. * * @returns {string} - Persist data */ protected getPersistData(): string { return this.addOnPersist([]); } /** * Get component name. * * @returns {string} - Module name * @private */ protected getModuleName(): string { return 'speed-dial'; } private initialize(): void { if (!this.element.id) { this.element.id = getUniqueID('e-' + this.getModuleName()); } this.fab = new Fab({ content: this.content, cssClass: this.cssClass ? (SPEEDDIAL + ' ' + this.cssClass) : SPEEDDIAL, disabled: this.disabled, enablePersistence: this.enablePersistence, enableRtl: this.enableRtl, iconCss: this.openIconCss, iconPosition: this.iconPosition, position: this.position, target: this.target, visible: this.visible, isPrimary: this.isPrimary }); this.fab.appendTo(this.element); if ((this.items.length > 0) || this.popupTemplate) { this.createPopup(); } this.wireEvents(); } private wireEvents(): void { EventHandler.add(<HTMLElement & Window><unknown>window, 'resize', this.resizeHandler, this); EventHandler.add(document.body, 'click', this.bodyClickHandler, this); if (this.opensOnHover) { this.wireFabHover(); } else { this.wireFabClick(); } } private wirePopupEvents(): void { this.removeRippleEffect = rippleEffect(this.popupEle, { selector: '.' + SDLIICON }); this.keyboardModule = new KeyboardEvents(this.element, { keyAction: this.keyActionHandler.bind(this), keyConfigs: this.keyConfigs, eventName: 'keydown' } ); this.popupKeyboardModule = new KeyboardEvents(this.popupEle, { keyAction: this.popupKeyActionHandler.bind(this), keyConfigs: { esc: 'escape' }, eventName: 'keydown' } ); this.documentKeyboardModule = new KeyboardEvents(document.body, { keyAction: this.popupKeyActionHandler.bind(this), keyConfigs: { enter: 'enter', space: 'space' }, eventName: 'keydown' } ); EventHandler.add(this.popupEle, 'click', this.popupClick, this); EventHandler.add(this.popupEle, 'mouseleave', this.popupMouseLeaveHandle, this); } private wireFabClick(): void { EventHandler.add(this.fab.element, 'click', this.fabClick, this); } private wireFabHover(): void { this.popupEle.classList.add(HOVERSD); EventHandler.add(this.fab.element, 'mouseover', this.mouseOverHandle, this); EventHandler.add(this.element, 'mouseleave', this.mouseLeaveHandle, this); } public createPopup(): void { let className: string = SDPOPUP + ' ' + SDHIDDEN; className = this.enableRtl ? className + ' ' + RTLCLASS : className; className = this.cssClass ? className + ' ' + this.cssClass : className; this.popupEle = this.createElement('div', { className: className, id: this.element.id + '_popup' }); this.element.insertAdjacentElement('afterend', this.popupEle); attributes(this.element, { 'aria-expanded': 'false', 'aria-haspopup': 'true', 'aria-controls': this.popupEle.id }); this.setPopupContent(); if (this.modal) { this.createOverlay(); } this.checkTarget(); this.setPositionProps(); this.wirePopupEvents(); } private createOverlay(): void { this.overlayEle = this.createElement('div', { id: this.element.id + '_overlay', className: (SDOVERLAY + (this.isMenuOpen ? '' : ' ' + SDHIDDEN) + ' ' + this.cssClass).trim() }); this.element.insertAdjacentElement('beforebegin', this.overlayEle); } private popupClick(): void { this.isControl = true; } //Checks and closes the speed dial if the click happened outside this speed dial. private bodyClickHandler(e: Event): void { if (this.isControl) { this.isControl = false; return; } if (this.isMenuOpen) { this.hidePopupEle(e); } } private fabClick(e: Event): void { this.isControl = true; if (this.isMenuOpen) { this.hidePopupEle(e); } else { this.showPopupEle(e); } } private setPopupContent(): void { this.popupEle.classList.remove(RADIALSD, LINEARSD, TEMPLATESD); if (!this.popupTemplate) { this.popupEle.classList.add((this.mode === 'Radial') ? RADIALSD : LINEARSD); this.createUl(); this.createItems(); } else { this.popupEle.classList.add(TEMPLATESD); this.appendTemplate(); } this.renderReactTemplates(); } private appendTemplate(): void { const templateContainer: HTMLElement = this.createElement('div', { className: SDTEMPLATECONTAINER }); append([templateContainer], this.popupEle); const templateFunction: Function = this.getTemplateString(this.popupTemplate); append(templateFunction({}, this, 'fabPopupTemplate', (this.element.id + 'popupTemplate'), this.isStringTemplate), templateContainer); } private getTemplateString(template: string | Function): Function { let stringContent: string | Function = ''; try { const tempEle: HTMLElement = select(template as string); if (typeof template !== 'function' && tempEle) { //Return innerHTML incase of jsrenderer script else outerHTML stringContent = tempEle.tagName === 'SCRIPT' ? tempEle.innerHTML : tempEle.outerHTML; } else { stringContent = template; } } catch (e) { stringContent = template; } return compile(stringContent); } private updatePopupTemplate(): void { if (this.popupEle) { if (this.popupEle.querySelector('.' + SDLI)) { this.clearItems(); this.popupEle.classList.remove(RADIALSD, LINEARSD); this.popupEle.classList.add(TEMPLATESD); } while (this.popupEle.firstElementChild) { remove(this.popupEle.firstElementChild); } this.setPopupContent(); this.updatePositionProperties(); } else { this.createPopup(); } } private createUl(): void { const popupUlEle: HTMLElement = this.createElement('ul', { className: SDUL, id: this.element.id + '_ul', attrs: { 'role': 'menu' } }); this.popupEle.appendChild(popupUlEle); } private createItems(): void { this.focusedIndex = -1; const ul: HTMLElement = this.popupEle.querySelector('.' + SDUL) as HTMLElement; for (let index: number = 0; index < this.items.length; index++) { const item: SpeedDialItemModel = this.items[parseInt(index.toString(), 10)]; const li: HTMLElement = this.createElement('li', { className: SDLI + ' ' + SDHIDDEN, id: item.id ? item.id : (this.element.id + '_li_' + index), attrs: { 'role': 'menuitem' } }); if (item.text) { li.setAttribute('aria-label', item.text); } if (this.itemTemplate) { const templateFunction: Function = this.getTemplateString(this.itemTemplate); append(templateFunction(item, this, 'fabItemTemplate', (this.element.id + 'itemTemplate'), this.isStringTemplate), li); } else { if (item.iconCss) { const iconSpan: HTMLElement = this.createElement('span', { className: SDLIICON + ' ' + item.iconCss }); li.appendChild(iconSpan); } if (item.text) { const textSpan: HTMLElement = this.createElement('span', { className: SDLITEXT }); textSpan.innerText = item.text; li.appendChild(textSpan); if (!item.iconCss) { li.classList.add(SDLITEXTONLY); } } } if (item.disabled) { li.classList.add(DISABLED); li.setAttribute('aria-disabled', 'true'); } else { EventHandler.add(li, 'click', (e: Event) => this.triggerItemClick(e, item), this); } if (item.title) { li.setAttribute('title', item.title); } const eventArgs: SpeedDialItemEventArgs = { element: li, item: item }; this.trigger('beforeItemRender', eventArgs, (args: SpeedDialItemEventArgs) => { ul.appendChild(args.element); }); } } private setRTL(): void { this.popupEle.classList[this.enableRtl ? 'add' : 'remove'](RTLCLASS); this.clearHorizontalPosition(); if (!(this.popupTemplate || (this.mode === 'Radial'))) { this.setLinearHorizontalPosition(); } else { if (!this.popupTemplate && this.mode === 'Radial') { this.setRadialPosition(); } this.setHorizontalPosition(); } } private checkTarget(): void { this.isFixed = true; if (this.target) { this.targetEle = (typeof this.target === 'string') ? select(this.target) : this.target; if (this.targetEle) { this.targetEle.appendChild(this.element); this.isFixed = false; } } if (this.isFixed) { if (this.popupEle) { this.popupEle.classList.add(FIXEDSD); } if (this.overlayEle) { this.overlayEle.classList.add(FIXEDSD); } } else { if (this.popupEle) { this.popupEle.classList.remove(FIXEDSD); } if (this.overlayEle) { this.overlayEle.classList.remove(FIXEDSD); } } } private setVisibility(val: boolean): void { this.setProperties({ visible: val }, true); this.fab.setProperties({ visible: val }); } private popupMouseLeaveHandle(e: MouseEvent): void { const target: HTMLElement = e.relatedTarget as HTMLElement; if (this.opensOnHover && !(target.classList.contains(SPEEDDIAL) || closest(target, '.' + SPEEDDIAL))) { this.hidePopupEle(e); } } private mouseOverHandle(e: MouseEvent): void { this.showPopupEle(e); } private mouseLeaveHandle(e: MouseEvent): void { const target: HTMLElement = e.relatedTarget as HTMLElement; if (!(target.classList.contains(SDPOPUP) || closest(target, '.' + SDPOPUP))) { this.hidePopupEle(e); } } private popupKeyActionHandler(e: KeyboardEventArgs): void { switch (e.action) { case 'esc': this.hidePopupEle(e); break; case 'enter': case 'space': if (this.isMenuOpen && e.target !== this.element) { this.hidePopupEle(e); } break; } } private keyActionHandler(e: KeyboardEventArgs): void { e.preventDefault(); switch (e.action) { case 'enter': case 'space': if (this.isMenuOpen) { if (this.focusedIndex !== -1) { this.triggerItemClick(e, this.items[this.focusedIndex]); } else { this.hidePopupEle(e); } } else { this.showPopupEle(e); } break; case 'esc': this.hidePopupEle(e); break; default: if (this.popupTemplate || !this.isMenuOpen) { break; } switch (e.action) { case 'end': this.focusLastElement(); break; case 'home': this.focusFirstElement(); break; case 'moveRight': if (this.mode === 'Radial') { this.focusLeftRightElement(false); } else { this.focusLinearElement(false); } break; case 'moveDown': if (this.mode === 'Radial') { this.focusUpDownElement(false); } else { this.focusLinearElement(false); } break; case 'moveLeft': if (this.mode === 'Radial') { this.focusLeftRightElement(true); } else { this.focusLinearElement(true); } break; case 'moveUp': if (this.mode === 'Radial') { this.focusUpDownElement(true); } else { this.focusLinearElement(true); } break; } break; } } private focusFirstElement(): void { const ele: HTMLElement[] = selectAll('.' + SDLI, this.popupEle); let index: number = 0; while (ele[parseInt(index.toString(), 10)].classList.contains(DISABLED)) { index++; if (index > (ele.length - 1)) { return; } } this.setFocus(index, ele[parseInt(index.toString(), 10)]); } private focusLastElement(): void { const ele: HTMLElement[] = selectAll('.' + SDLI, this.popupEle); let index: number = ele.length - 1; while (ele[parseInt(index.toString(), 10)].classList.contains(DISABLED)) { index--; if (index < 0) { return; } } this.setFocus(index, ele[parseInt(index.toString(), 10)]); } /*Linear*/ private focusLinearElement(isLeftUp: boolean): void { const isReversed: boolean = this.popupEle.classList.contains(SDVERTICALBOTTOM) || this.popupEle.classList.contains(SDHORIZONTALRIGHT); /* Elements will be in reverse (RTL) order for these classes are present. Reversed and Down or right is previous. Not reversed and Up or left is previous. ((isReversed && !isLeftUp)||(!isReversed && isLeftUp)) ==> isReversed!==isLeftUp */ if (isReversed !== isLeftUp) { this.focusPrevElement(); } else { this.focusNextElement(); } } /*Radial*/ private focusLeftRightElement(isLeft: boolean): void { /*radialTop position and left + anticlock or right + clock is previous other positions and right + anticlock or left + clock is previous ((isLeft && !this.isClock)||(!isLeft && this.isClock)) ==> isLeft!==this.isClock */ const isradialTop: boolean = ['TopLeft', 'TopCenter', 'TopRight', 'MiddleLeft'].indexOf(this.position) !== -1; if ((isradialTop && (isLeft !== this.isClock)) || (!isradialTop && (isLeft === this.isClock))) { this.focusPrevElement(); } else { this.focusNextElement(); } } /*Radial*/ private focusUpDownElement(isUp: boolean): void { /*radialRight position and up + anticlock or down + clock is previous other positions and down + anticlock or up + clock is previous ((isUp && !this.isClock)||(!isUp && this.isClock)) ==> isUp!==this.isClock */ const isradialRight: boolean = ['TopRight', 'MiddleRight', 'BottomRight', 'BottomCenter'].indexOf(this.position) !== -1; if ((isradialRight && (isUp !== this.isClock)) || (!isradialRight && (isUp === this.isClock))) { this.focusPrevElement(); } else { this.focusNextElement(); } } private focusPrevElement(): void { const ele: HTMLElement[] = selectAll('.' + SDLI, this.popupEle); let index: number = this.focusedIndex; do { index--; if (index < 0) { this.setFocus(-1); return; } } while (ele[parseInt(index.toString(), 10)].classList.contains(DISABLED)); this.setFocus(index, ele[parseInt(index.toString(), 10)]); } private focusNextElement(): void { const ele: HTMLElement[] = selectAll('.' + SDLI, this.popupEle); let index: number = this.focusedIndex; do { index++; if (index > (ele.length - 1)) { return; } } while (ele[parseInt(index.toString(), 10)].classList.contains(DISABLED)); this.setFocus(index, ele[parseInt(index.toString(), 10)]); } private setFocus(index: number, ele?: HTMLElement): void { this.removeFocus(); if (ele) { ele.classList.add(SDACTIVELI); } this.focusedIndex = index; } private removeFocus(): void { const preEle: HTMLElement = select('.' + SDACTIVELI, this.popupEle); if (preEle) { preEle.classList.remove(SDACTIVELI); } } private updatePositionProperties(): void { this.hidePopupEle(); this.clearPosition(); this.validateDirection(); this.setPositionProps(); } private setPositionProps(): void { if (this.popupTemplate) { this.setPosition(); } else if ((this.mode === 'Radial')) { this.setRadialPosition(); this.setPosition(); } else { this.setLinearPosition(); this.setMaxSize(); } } private validateDirection(): void { switch (this.direction) { case 'Up': this.actualLinDirection = (topPosition.indexOf(this.position) !== -1) ? 'Auto' : 'Up'; break; case 'Down': this.actualLinDirection = (bottomPosition.indexOf(this.position) !== -1) ? 'Auto' : 'Down'; break; case 'Right': this.actualLinDirection = (rightPosition.indexOf(this.position) !== -1) ? 'Auto' : 'Right'; break; case 'Left': this.actualLinDirection = (leftPosition.indexOf(this.position) !== -1) ? 'Auto' : 'Left'; break; case 'Auto': default: this.actualLinDirection = 'Auto'; break; } this.isVertical = !((this.actualLinDirection === 'Left') || (this.actualLinDirection === 'Right')); } private setMaxSize(): void { const top: number = this.element.offsetTop; const left: number = this.element.offsetLeft; const bottom: number = (this.isFixed ? window.innerHeight : this.targetEle.clientHeight) - this.element.offsetTop - this.element.offsetHeight; const right: number = (this.isFixed ? window.innerWidth : this.targetEle.clientWidth) - this.element.offsetLeft - this.element.offsetWidth; let limit: number = 0; const popupUlEle: HTMLElement = this.popupEle.querySelector('.' + SDUL) as HTMLElement; if (this.isVertical) { limit = ((this.actualLinDirection === 'Up') || ((this.actualLinDirection === 'Auto') && (topPosition.indexOf(this.position) === -1))) ? top : bottom; if (limit < popupUlEle.offsetHeight) { this.popupEle.classList.add(SDOVERFLOW, SDVERTOVERFLOW); popupUlEle.style.setProperty(SDOVERFLOWLIMIT, limit + 'px'); } } else { limit = this.enableRtl ? (this.direction === 'Right' ? left : right) : (this.direction === 'Right' ? right : left); if (limit < popupUlEle.offsetWidth) { this.popupEle.classList.add(SDOVERFLOW, SDHORZOVERFLOW); popupUlEle.style.setProperty(SDOVERFLOWLIMIT, limit + 'px'); } } } private setLinearPosition(): void { let vertDist: number = 0; //Check whether the position value should be in top const isTop: boolean = (this.actualLinDirection === 'Down') || ((this.actualLinDirection === 'Auto') && (topPosition.indexOf(this.position) !== -1)) || (!this.isVertical && (bottomPosition.indexOf(this.position) === -1)); const elementOffSetHeight: number = this.element.offsetHeight / 2; const isMiddle: boolean = ['MiddleRight', 'MiddleCenter', 'MiddleLeft'].indexOf(this.position) !== -1; if (isTop) { vertDist = this.element.offsetTop + (this.isVertical ? this.element.offsetHeight : 0); if (isMiddle) { if (this.actualLinDirection === 'Right' || this.actualLinDirection === 'Left') { vertDist = this.element.offsetTop - elementOffSetHeight; } if (this.actualLinDirection === 'Down') { vertDist = vertDist - elementOffSetHeight; } } if (!this.isVertical) { this.popupEle.classList.add(SDHORIZONTALTOP); } } else { vertDist = this.isFixed ? window.document.documentElement.clientHeight : this.targetEle.clientHeight; vertDist = (vertDist - this.element.offsetTop - (this.isVertical ? 0 : this.element.offsetHeight)); if (isMiddle) { if (this.actualLinDirection === 'Auto' || this.actualLinDirection === 'Up') { vertDist = vertDist + elementOffSetHeight; } } if (this.isVertical) { this.popupEle.classList.add(SDVERTICALBOTTOM); } } this.popupEle.classList.add(isTop ? SDTOP : SDBOTTOM); this.popupEle.style.setProperty(SDVERTDIST, vertDist + 'px'); this.setLinearHorizontalPosition(); } private setLinearHorizontalPosition(): void { //Check whether the position value should be in left if ((this.actualLinDirection === 'Right') || (this.isVertical && (rightPosition.indexOf(this.position) === -1))) { if (this.enableRtl) { this.setRight(); } else { this.setLeft(); } //reverse the direction when RTL enabled if (!this.isVertical) { this.popupEle.classList.add(SDHORIZONTALLEFT); } } else { if (this.enableRtl) { this.setLeft(); } else { this.setRight(); } //reverse the direction when RTL enabled this.popupEle.classList.add(this.isVertical ? SDVERTICALRIGHT : SDHORIZONTALRIGHT); } } private setLeft(): void { const elementOffSetWidth: number = this.element.offsetWidth / 2; const isCenter: boolean = [ 'TopCenter', 'MiddleCenter', 'BottomCenter'].indexOf(this.position) !== -1; let horzDist: number = this.element.offsetLeft + (this.isVertical ? 0 : this.element.offsetWidth); if (isCenter) { if (this.actualLinDirection === 'Auto' || this.actualLinDirection === 'Down' || this.actualLinDirection === 'Up') { horzDist = this.element.offsetLeft - elementOffSetWidth; } else { horzDist = this.actualLinDirection === 'Right' ? this.element.offsetLeft + elementOffSetWidth : horzDist + elementOffSetWidth; } } this.popupEle.style.setProperty(SDHORZDIST, horzDist + 'px'); this.popupEle.classList.add(SDLEFT); } private setRight(): void { const elementOffSetWidth: number = this.element.offsetWidth / 2; const isCenter: boolean = [ 'TopCenter', 'MiddleCenter', 'BottomCenter'].indexOf(this.position) !== -1; let horzDist: number = this.isFixed ? window.document.documentElement.clientWidth : this.targetEle.clientWidth; horzDist = (horzDist - this.element.offsetLeft - (this.isVertical ? this.element.offsetWidth : 0)); if (isCenter && this.actualLinDirection === 'Left') { horzDist = horzDist + elementOffSetWidth; } if (this.popupEle.classList.contains('e-rtl') && isCenter) { horzDist = horzDist - elementOffSetWidth; } this.popupEle.style.setProperty(SDHORZDIST, horzDist + 'px'); this.popupEle.classList.add(SDRIGHT); } private setPosition(): void { //Check for middle Position if (['MiddleLeft', 'MiddleRight', 'MiddleCenter'].indexOf(this.position) !== -1) { this.popupEle.classList.add(SDMIDDLE); const yoffset: number = ((this.isFixed ? window.innerHeight : this.targetEle.clientHeight) - this.popupEle.offsetHeight) / 2; this.popupEle.style.setProperty(SDVERTDIST, yoffset + 'px'); } this.popupEle.classList.add((bottomPosition.indexOf(this.position) === -1) ? SDTOP : SDBOTTOM); this.setHorizontalPosition(); } private setHorizontalPosition(): void { //Check for Center Position if (['TopCenter', 'BottomCenter', 'MiddleCenter'].indexOf(this.position) !== -1) { this.popupEle.classList.add(SDCENTER); const xoffset: number = ((this.isFixed ? window.innerWidth : this.targetEle.clientWidth) - this.popupEle.offsetWidth) / 2; this.popupEle.style.setProperty(SDHORZDIST, xoffset + 'px'); } const isRight: boolean = rightPosition.indexOf(this.position) !== -1; this.popupEle.classList.add((!(this.enableRtl || isRight) || (this.enableRtl && isRight)) ? SDLEFT : SDRIGHT); } private setCustomRadialPosition(): void { const viewportWidth: number = document.documentElement.clientWidth; const viewportHeight: number = document.documentElement.clientHeight; if (['TopLeft', 'BottomLeft', 'MiddleLeft'].indexOf(this.position) !== -1) { let horzDist: number; if (this.enableRtl) { if (this.isFixed) { horzDist = viewportWidth - (this.element.offsetLeft + this.element.offsetWidth); } else { horzDist = this.targetEle.clientWidth - (this.element.offsetLeft + this.element.offsetWidth); } } else { horzDist = this.element.offsetLeft; } this.popupEle.style.setProperty(SDRADICALHORZDIST, horzDist + 'px'); } if (['TopLeft', 'TopCenter', 'TopRight'].indexOf(this.position) !== -1) { this.popupEle.style.top = this.element.offsetTop + 'px'; } if (['TopRight', 'BottomRight', 'MiddleRight'].indexOf(this.position) !== -1) { let horzDist: number; if (this.enableRtl) { horzDist = this.element.offsetLeft; } else { if (this.isFixed) { horzDist = viewportWidth - (this.element.offsetLeft + this.element.offsetWidth); } else { horzDist = this.targetEle.clientWidth - (this.element.offsetLeft + this.element.offsetWidth); } } this.popupEle.style.setProperty(SDRADICALHORZDIST, horzDist + 'px'); } if (['BottomLeft', 'BottomCenter', 'BottomRight'].indexOf(this.position) !== -1) { if (this.isFixed) { this.popupEle.style.bottom = viewportHeight - (this.element.offsetTop + this.element.offsetHeight) + 'px'; } else { this.popupEle.style.bottom = this.targetEle.clientHeight - (this.element.offsetTop + this.element.offsetHeight) + 'px'; } } if (['TopCenter', 'MiddleCenter', 'BottomCenter'].indexOf(this.position) !== -1) { let horzDist: number; if (this.enableRtl) { if (this.isFixed) { horzDist = viewportWidth - (this.element.offsetLeft + this.element.offsetWidth) - this.popupEle.offsetWidth / 2; } else { const targetEleWidth: number = this.targetEle.clientWidth; const popupEleWidth: number = this.popupEle.offsetWidth; horzDist = targetEleWidth - (this.element.offsetLeft + this.element.offsetWidth) - popupEleWidth / 2; } } else { horzDist = ((this.element.offsetLeft) - this.popupEle.offsetWidth / 2); } this.popupEle.style.setProperty(SDRADICALHORZDIST, horzDist + 'px'); } if (['MiddleLeft', 'MiddleCenter', 'MiddleRight'].indexOf(this.position) !== -1) { this.popupEle.style.top = ((this.element.offsetTop) - this.popupEle.offsetHeight / 2) + 'px'; } } private setRadialPosition(): void { this.setRadialCorner(); const range: RadialSettingsModel = this.getActualRange(); this.isClock = range.direction === 'Clockwise'; const offset: string = formatUnit(range.offset as string | number); const li: HTMLElement[] = selectAll('.' + SDLI, this.popupEle); this.popupEle.style.setProperty(SDRADICALOFFSET, offset); this.popupEle.style.setProperty(SDRADICALMINHEIGHT, li[0].offsetHeight + 'px');