UNPKG

primeng

Version:

PrimeNG is an open source UI library for Angular featuring a rich set of 80+ components, a theme designer, various theme alternatives such as Material, Bootstrap, Tailwind, premium templates and professional support. In addition, it integrates with PrimeB

797 lines (784 loc) 29.5 kB
import { isPlatformBrowser } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, inject, TemplateRef, booleanAttribute, numberAttribute, Input, Directive, NgModule } from '@angular/core'; import { uuid, hasClass, appendChild, fadeIn, getWindowScrollLeft, getWindowScrollTop, findSingle, getOuterWidth, getOuterHeight, getViewport, removeChild } from '@primeuix/utils'; import { BaseComponent } from 'primeng/basecomponent'; import { ConnectedOverlayScrollHandler } from 'primeng/dom'; import { ZIndexUtils } from 'primeng/utils'; import { BaseStyle } from 'primeng/base'; const theme = ({ dt }) => ` .p-tooltip { position: absolute; display: none; max-width: ${dt('tooltip.max.width')}; } .p-tooltip-right, .p-tooltip-left { padding: 0 ${dt('tooltip.gutter')}; } .p-tooltip-top, .p-tooltip-bottom { padding: ${dt('tooltip.gutter')} 0; } .p-tooltip-text { white-space: pre-line; word-break: break-word; background: ${dt('tooltip.background')}; color: ${dt('tooltip.color')}; padding: ${dt('tooltip.padding')}; box-shadow: ${dt('tooltip.shadow')}; border-radius: ${dt('tooltip.border.radius')}; } .p-tooltip-arrow { position: absolute; width: 0; height: 0; border-color: transparent; border-style: solid; scale: 2; } .p-tooltip-right .p-tooltip-arrow { top: 50%; left: 0; margin-top: calc(-1 * ${dt('tooltip.gutter')}); border-width: ${dt('tooltip.gutter')} ${dt('tooltip.gutter')} ${dt('tooltip.gutter')} 0; border-right-color: ${dt('tooltip.background')}; } .p-tooltip-left .p-tooltip-arrow { top: 50%; right: 0; margin-top: calc(-1 * ${dt('tooltip.gutter')}); border-width: ${dt('tooltip.gutter')} 0 ${dt('tooltip.gutter')} ${dt('tooltip.gutter')}; border-left-color: ${dt('tooltip.background')}; } .p-tooltip-top .p-tooltip-arrow { bottom: 0; left: 50%; margin-left: calc(-1 * ${dt('tooltip.gutter')}); border-width: ${dt('tooltip.gutter')} ${dt('tooltip.gutter')} 0 ${dt('tooltip.gutter')}; border-top-color: ${dt('tooltip.background')}; border-bottom-color: ${dt('tooltip.background')}; } .p-tooltip-bottom .p-tooltip-arrow { top: 0; left: 50%; margin-left: calc(-1 * ${dt('tooltip.gutter')}); border-width: 0 ${dt('tooltip.gutter')} ${dt('tooltip.gutter')} ${dt('tooltip.gutter')}; border-top-color: ${dt('tooltip.background')}; border-bottom-color: ${dt('tooltip.background')}; } `; const classes = { root: 'p-tooltip p-component', arrow: 'p-tooltip-arrow', text: 'p-tooltip-text' }; class TooltipStyle extends BaseStyle { name = 'tooltip'; theme = theme; classes = classes; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipStyle, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipStyle }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipStyle, decorators: [{ type: Injectable }] }); /** * * Tooltip directive provides advisory information for a component. * * [Live Demo](https://www.primeng.org/tooltip) * * @module tooltipstyle * */ var TooltipClasses; (function (TooltipClasses) { /** * Class name of the root element */ TooltipClasses["root"] = "p-tooltip"; /** * Class name of the arrow element */ TooltipClasses["arrow"] = "p-tooltip-arrow"; /** * Class name of the text element */ TooltipClasses["text"] = "p-tooltip-text"; })(TooltipClasses || (TooltipClasses = {})); /** * Tooltip directive provides advisory information for a component. * @group Components */ class Tooltip extends BaseComponent { zone; viewContainer; /** * Position of the tooltip. * @group Props */ tooltipPosition; /** * Event to show the tooltip. * @group Props */ tooltipEvent = 'hover'; /** * Target element to attach the overlay, valid values are "body", "target" or a local ng-F variable of another element (note: use binding with brackets for template variables, e.g. [appendTo]="mydiv" for a div element having #mydiv as variable name). * @group Props */ appendTo; /** * Type of CSS position. * @group Props */ positionStyle; /** * Style class of the tooltip. * @group Props */ tooltipStyleClass; /** * Whether the z-index should be managed automatically to always go on top or have a fixed value. * @group Props */ tooltipZIndex; /** * By default the tooltip contents are rendered as text. Set to false to support html tags in the content. * @group Props */ escape = true; /** * Delay to show the tooltip in milliseconds. * @group Props */ showDelay; /** * Delay to hide the tooltip in milliseconds. * @group Props */ hideDelay; /** * Time to wait in milliseconds to hide the tooltip even it is active. * @group Props */ life; /** * Specifies the additional vertical offset of the tooltip from its default position. * @group Props */ positionTop; /** * Specifies the additional horizontal offset of the tooltip from its default position. * @group Props */ positionLeft; /** * Whether to hide tooltip when hovering over tooltip content. * @group Props */ autoHide = true; /** * Automatically adjusts the element position when there is not enough space on the selected position. * @group Props */ fitContent = true; /** * Whether to hide tooltip on escape key press. * @group Props */ hideOnEscape = true; /** * Content of the tooltip. * @group Props */ content; /** * When present, it specifies that the component should be disabled. * @defaultValue false * @group Props */ get disabled() { return this._disabled; } set disabled(val) { this._disabled = val; this.deactivate(); } /** * Specifies the tooltip configuration options for the component. * @group Props */ tooltipOptions; _tooltipOptions = { tooltipLabel: null, tooltipPosition: 'right', tooltipEvent: 'hover', appendTo: 'body', positionStyle: null, tooltipStyleClass: null, tooltipZIndex: 'auto', escape: true, disabled: null, showDelay: null, hideDelay: null, positionTop: null, positionLeft: null, life: null, autoHide: true, hideOnEscape: true, id: uuid('pn_id_') + '_tooltip' }; _disabled; container; styleClass; tooltipText; showTimeout; hideTimeout; active; mouseEnterListener; mouseLeaveListener; containerMouseleaveListener; clickListener; focusListener; blurListener; documentEscapeListener; scrollHandler; resizeListener; _componentStyle = inject(TooltipStyle); interactionInProgress = false; constructor(zone, viewContainer) { super(); this.zone = zone; this.viewContainer = viewContainer; } ngAfterViewInit() { super.ngAfterViewInit(); if (isPlatformBrowser(this.platformId)) { this.zone.runOutsideAngular(() => { const tooltipEvent = this.getOption('tooltipEvent'); if (tooltipEvent === 'hover' || tooltipEvent === 'both') { this.mouseEnterListener = this.onMouseEnter.bind(this); this.mouseLeaveListener = this.onMouseLeave.bind(this); this.clickListener = this.onInputClick.bind(this); this.el.nativeElement.addEventListener('mouseenter', this.mouseEnterListener); this.el.nativeElement.addEventListener('click', this.clickListener); this.el.nativeElement.addEventListener('mouseleave', this.mouseLeaveListener); } if (tooltipEvent === 'focus' || tooltipEvent === 'both') { this.focusListener = this.onFocus.bind(this); this.blurListener = this.onBlur.bind(this); let target = this.el.nativeElement.querySelector('.p-component'); if (!target) { target = this.getTarget(this.el.nativeElement); } target.addEventListener('focus', this.focusListener); target.addEventListener('blur', this.blurListener); } }); } } ngOnChanges(simpleChange) { super.ngOnChanges(simpleChange); if (simpleChange.tooltipPosition) { this.setOption({ tooltipPosition: simpleChange.tooltipPosition.currentValue }); } if (simpleChange.tooltipEvent) { this.setOption({ tooltipEvent: simpleChange.tooltipEvent.currentValue }); } if (simpleChange.appendTo) { this.setOption({ appendTo: simpleChange.appendTo.currentValue }); } if (simpleChange.positionStyle) { this.setOption({ positionStyle: simpleChange.positionStyle.currentValue }); } if (simpleChange.tooltipStyleClass) { this.setOption({ tooltipStyleClass: simpleChange.tooltipStyleClass.currentValue }); } if (simpleChange.tooltipZIndex) { this.setOption({ tooltipZIndex: simpleChange.tooltipZIndex.currentValue }); } if (simpleChange.escape) { this.setOption({ escape: simpleChange.escape.currentValue }); } if (simpleChange.showDelay) { this.setOption({ showDelay: simpleChange.showDelay.currentValue }); } if (simpleChange.hideDelay) { this.setOption({ hideDelay: simpleChange.hideDelay.currentValue }); } if (simpleChange.life) { this.setOption({ life: simpleChange.life.currentValue }); } if (simpleChange.positionTop) { this.setOption({ positionTop: simpleChange.positionTop.currentValue }); } if (simpleChange.positionLeft) { this.setOption({ positionLeft: simpleChange.positionLeft.currentValue }); } if (simpleChange.disabled) { this.setOption({ disabled: simpleChange.disabled.currentValue }); } if (simpleChange.content) { this.setOption({ tooltipLabel: simpleChange.content.currentValue }); if (this.active) { if (simpleChange.content.currentValue) { if (this.container && this.container.offsetParent) { this.updateText(); this.align(); } else { this.show(); } } else { this.hide(); } } } if (simpleChange.autoHide) { this.setOption({ autoHide: simpleChange.autoHide.currentValue }); } if (simpleChange.id) { this.setOption({ id: simpleChange.id.currentValue }); } if (simpleChange.tooltipOptions) { this._tooltipOptions = { ...this._tooltipOptions, ...simpleChange.tooltipOptions.currentValue }; this.deactivate(); if (this.active) { if (this.getOption('tooltipLabel')) { if (this.container && this.container.offsetParent) { this.updateText(); this.align(); } else { this.show(); } } else { this.hide(); } } } } isAutoHide() { return this.getOption('autoHide'); } onMouseEnter(e) { if (!this.container && !this.showTimeout) { this.activate(); } } onMouseLeave(e) { if (!this.isAutoHide()) { const valid = hasClass(e.relatedTarget, 'p-tooltip') || hasClass(e.relatedTarget, 'p-tooltip-text') || hasClass(e.relatedTarget, 'p-tooltip-arrow'); !valid && this.deactivate(); } else { this.deactivate(); } } onFocus(e) { this.activate(); } onBlur(e) { this.deactivate(); } onInputClick(e) { this.deactivate(); } activate() { if (!this.interactionInProgress) { this.active = true; this.clearHideTimeout(); if (this.getOption('showDelay')) this.showTimeout = setTimeout(() => { this.show(); }, this.getOption('showDelay')); else this.show(); if (this.getOption('life')) { let duration = this.getOption('showDelay') ? this.getOption('life') + this.getOption('showDelay') : this.getOption('life'); this.hideTimeout = setTimeout(() => { this.hide(); }, duration); } if (this.getOption('hideOnEscape')) { this.documentEscapeListener = this.renderer.listen('document', 'keydown.escape', () => { this.deactivate(); this.documentEscapeListener(); }); } this.interactionInProgress = true; } } deactivate() { this.interactionInProgress = false; this.active = false; this.clearShowTimeout(); if (this.getOption('hideDelay')) { this.clearHideTimeout(); //life timeout this.hideTimeout = setTimeout(() => { this.hide(); }, this.getOption('hideDelay')); } else { this.hide(); } if (this.documentEscapeListener) { this.documentEscapeListener(); } } create() { if (this.container) { this.clearHideTimeout(); this.remove(); } this.container = document.createElement('div'); this.container.setAttribute('id', this.getOption('id')); this.container.setAttribute('role', 'tooltip'); let tooltipArrow = document.createElement('div'); tooltipArrow.className = 'p-tooltip-arrow'; this.container.appendChild(tooltipArrow); this.tooltipText = document.createElement('div'); this.tooltipText.className = 'p-tooltip-text'; this.updateText(); if (this.getOption('positionStyle')) { this.container.style.position = this.getOption('positionStyle'); } this.container.appendChild(this.tooltipText); if (this.getOption('appendTo') === 'body') document.body.appendChild(this.container); else if (this.getOption('appendTo') === 'target') appendChild(this.container, this.el.nativeElement); else appendChild(this.getOption('appendTo'), this.container); this.container.style.display = 'none'; if (this.fitContent) { this.container.style.width = 'fit-content'; } if (this.isAutoHide()) { this.container.style.pointerEvents = 'none'; } else { this.container.style.pointerEvents = 'unset'; this.bindContainerMouseleaveListener(); } } bindContainerMouseleaveListener() { if (!this.containerMouseleaveListener) { const targetEl = this.container ?? this.container.nativeElement; this.containerMouseleaveListener = this.renderer.listen(targetEl, 'mouseleave', (e) => { this.deactivate(); }); } } unbindContainerMouseleaveListener() { if (this.containerMouseleaveListener) { this.bindContainerMouseleaveListener(); this.containerMouseleaveListener = null; } } show() { if (!this.getOption('tooltipLabel') || this.getOption('disabled')) { return; } this.create(); const nativeElement = this.el.nativeElement; const pDialogWrapper = nativeElement.closest('p-dialog'); if (pDialogWrapper) { setTimeout(() => { this.container && (this.container.style.display = 'inline-block'); this.container && this.align(); }, 100); } else { this.container.style.display = 'inline-block'; this.align(); } fadeIn(this.container, 250); if (this.getOption('tooltipZIndex') === 'auto') ZIndexUtils.set('tooltip', this.container, this.config.zIndex.tooltip); else this.container.style.zIndex = this.getOption('tooltipZIndex'); this.bindDocumentResizeListener(); this.bindScrollListener(); } hide() { if (this.getOption('tooltipZIndex') === 'auto') { ZIndexUtils.clear(this.container); } this.remove(); } updateText() { const content = this.getOption('tooltipLabel'); if (content instanceof TemplateRef) { const embeddedViewRef = this.viewContainer.createEmbeddedView(content); embeddedViewRef.detectChanges(); embeddedViewRef.rootNodes.forEach((node) => this.tooltipText.appendChild(node)); } else if (this.getOption('escape')) { this.tooltipText.innerHTML = ''; this.tooltipText.appendChild(document.createTextNode(content)); } else { this.tooltipText.innerHTML = content; } } align() { let position = this.getOption('tooltipPosition'); const positionPriority = { top: [this.alignTop, this.alignBottom, this.alignRight, this.alignLeft], bottom: [this.alignBottom, this.alignTop, this.alignRight, this.alignLeft], left: [this.alignLeft, this.alignRight, this.alignTop, this.alignBottom], right: [this.alignRight, this.alignLeft, this.alignTop, this.alignBottom] }; for (let [index, alignmentFn] of positionPriority[position].entries()) { if (index === 0) alignmentFn.call(this); else if (this.isOutOfBounds()) alignmentFn.call(this); else break; } } getHostOffset() { if (this.getOption('appendTo') === 'body' || this.getOption('appendTo') === 'target') { let offset = this.el.nativeElement.getBoundingClientRect(); let targetLeft = offset.left + getWindowScrollLeft(); let targetTop = offset.top + getWindowScrollTop(); return { left: targetLeft, top: targetTop }; } else { return { left: 0, top: 0 }; } } get activeElement() { return this.el.nativeElement.nodeName.startsWith('P-') ? findSingle(this.el.nativeElement, '.p-component') : this.el.nativeElement; } alignRight() { this.preAlign('right'); const el = this.activeElement; const offsetLeft = getOuterWidth(el); const offsetTop = (getOuterHeight(el) - getOuterHeight(this.container)) / 2; this.alignTooltip(offsetLeft, offsetTop); } alignLeft() { this.preAlign('left'); let offsetLeft = getOuterWidth(this.container); let offsetTop = (getOuterHeight(this.el.nativeElement) - getOuterHeight(this.container)) / 2; this.alignTooltip(-offsetLeft, offsetTop); } alignTop() { this.preAlign('top'); let offsetLeft = (getOuterWidth(this.el.nativeElement) - getOuterWidth(this.container)) / 2; let offsetTop = getOuterHeight(this.container); this.alignTooltip(offsetLeft, -offsetTop); } alignBottom() { this.preAlign('bottom'); let offsetLeft = (getOuterWidth(this.el.nativeElement) - getOuterWidth(this.container)) / 2; let offsetTop = getOuterHeight(this.el.nativeElement); this.alignTooltip(offsetLeft, offsetTop); } alignTooltip(offsetLeft, offsetTop) { let hostOffset = this.getHostOffset(); let left = hostOffset.left + offsetLeft; let top = hostOffset.top + offsetTop; this.container.style.left = left + this.getOption('positionLeft') + 'px'; this.container.style.top = top + this.getOption('positionTop') + 'px'; } setOption(option) { this._tooltipOptions = { ...this._tooltipOptions, ...option }; } getOption(option) { return this._tooltipOptions[option]; } getTarget(el) { return hasClass(el, 'p-inputwrapper') ? findSingle(el, 'input') : el; } preAlign(position) { this.container.style.left = -999 + 'px'; this.container.style.top = -999 + 'px'; let defaultClassName = 'p-tooltip p-component p-tooltip-' + position; this.container.className = this.getOption('tooltipStyleClass') ? defaultClassName + ' ' + this.getOption('tooltipStyleClass') : defaultClassName; } isOutOfBounds() { let offset = this.container.getBoundingClientRect(); let targetTop = offset.top; let targetLeft = offset.left; let width = getOuterWidth(this.container); let height = getOuterHeight(this.container); let viewport = getViewport(); return targetLeft + width > viewport.width || targetLeft < 0 || targetTop < 0 || targetTop + height > viewport.height; } onWindowResize(e) { this.hide(); } bindDocumentResizeListener() { this.zone.runOutsideAngular(() => { this.resizeListener = this.onWindowResize.bind(this); window.addEventListener('resize', this.resizeListener); }); } unbindDocumentResizeListener() { if (this.resizeListener) { window.removeEventListener('resize', this.resizeListener); this.resizeListener = null; } } bindScrollListener() { if (!this.scrollHandler) { this.scrollHandler = new ConnectedOverlayScrollHandler(this.el.nativeElement, () => { if (this.container) { this.hide(); } }); } this.scrollHandler.bindScrollListener(); } unbindScrollListener() { if (this.scrollHandler) { this.scrollHandler.unbindScrollListener(); } } unbindEvents() { const tooltipEvent = this.getOption('tooltipEvent'); if (tooltipEvent === 'hover' || tooltipEvent === 'both') { this.el.nativeElement.removeEventListener('mouseenter', this.mouseEnterListener); this.el.nativeElement.removeEventListener('mouseleave', this.mouseLeaveListener); this.el.nativeElement.removeEventListener('click', this.clickListener); } if (tooltipEvent === 'focus' || tooltipEvent === 'both') { let target = this.el.nativeElement.querySelector('.p-component'); if (!target) { target = this.getTarget(this.el.nativeElement); } target.removeEventListener('focus', this.focusListener); target.removeEventListener('blur', this.blurListener); } this.unbindDocumentResizeListener(); } remove() { if (this.container && this.container.parentElement) { if (this.getOption('appendTo') === 'body') document.body.removeChild(this.container); else if (this.getOption('appendTo') === 'target') this.el.nativeElement.removeChild(this.container); else removeChild(this.getOption('appendTo'), this.container); } this.unbindDocumentResizeListener(); this.unbindScrollListener(); this.unbindContainerMouseleaveListener(); this.clearTimeouts(); this.container = null; this.scrollHandler = null; } clearShowTimeout() { if (this.showTimeout) { clearTimeout(this.showTimeout); this.showTimeout = null; } } clearHideTimeout() { if (this.hideTimeout) { clearTimeout(this.hideTimeout); this.hideTimeout = null; } } clearTimeouts() { this.clearShowTimeout(); this.clearHideTimeout(); } ngOnDestroy() { this.unbindEvents(); super.ngOnDestroy(); if (this.container) { ZIndexUtils.clear(this.container); } this.remove(); if (this.scrollHandler) { this.scrollHandler.destroy(); this.scrollHandler = null; } if (this.documentEscapeListener) { this.documentEscapeListener(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: Tooltip, deps: [{ token: i0.NgZone }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.5", type: Tooltip, isStandalone: true, selector: "[pTooltip]", inputs: { tooltipPosition: "tooltipPosition", tooltipEvent: "tooltipEvent", appendTo: "appendTo", positionStyle: "positionStyle", tooltipStyleClass: "tooltipStyleClass", tooltipZIndex: "tooltipZIndex", escape: ["escape", "escape", booleanAttribute], showDelay: ["showDelay", "showDelay", numberAttribute], hideDelay: ["hideDelay", "hideDelay", numberAttribute], life: ["life", "life", numberAttribute], positionTop: ["positionTop", "positionTop", numberAttribute], positionLeft: ["positionLeft", "positionLeft", numberAttribute], autoHide: ["autoHide", "autoHide", booleanAttribute], fitContent: ["fitContent", "fitContent", booleanAttribute], hideOnEscape: ["hideOnEscape", "hideOnEscape", booleanAttribute], content: ["pTooltip", "content"], disabled: ["tooltipDisabled", "disabled"], tooltipOptions: "tooltipOptions" }, providers: [TooltipStyle], usesInheritance: true, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: Tooltip, decorators: [{ type: Directive, args: [{ selector: '[pTooltip]', standalone: true, providers: [TooltipStyle] }] }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ViewContainerRef }], propDecorators: { tooltipPosition: [{ type: Input }], tooltipEvent: [{ type: Input }], appendTo: [{ type: Input }], positionStyle: [{ type: Input }], tooltipStyleClass: [{ type: Input }], tooltipZIndex: [{ type: Input }], escape: [{ type: Input, args: [{ transform: booleanAttribute }] }], showDelay: [{ type: Input, args: [{ transform: numberAttribute }] }], hideDelay: [{ type: Input, args: [{ transform: numberAttribute }] }], life: [{ type: Input, args: [{ transform: numberAttribute }] }], positionTop: [{ type: Input, args: [{ transform: numberAttribute }] }], positionLeft: [{ type: Input, args: [{ transform: numberAttribute }] }], autoHide: [{ type: Input, args: [{ transform: booleanAttribute }] }], fitContent: [{ type: Input, args: [{ transform: booleanAttribute }] }], hideOnEscape: [{ type: Input, args: [{ transform: booleanAttribute }] }], content: [{ type: Input, args: ['pTooltip'] }], disabled: [{ type: Input, args: ['tooltipDisabled'] }], tooltipOptions: [{ type: Input }] } }); class TooltipModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.5", ngImport: i0, type: TooltipModule, imports: [Tooltip], exports: [Tooltip] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.5", ngImport: i0, type: TooltipModule, decorators: [{ type: NgModule, args: [{ imports: [Tooltip], exports: [Tooltip] }] }] }); /** * Generated bundle index. Do not edit. */ export { Tooltip, TooltipClasses, TooltipModule, TooltipStyle }; //# sourceMappingURL=primeng-tooltip.mjs.map