Skip to content

Commit

Permalink
add default popup
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew44-mappable committed Apr 17, 2024
1 parent 67c12df commit 7408207
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 21 deletions.
4 changes: 4 additions & 0 deletions src/markers/MMapBalloonMarker/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

--balloon-tail-transform-top: translate(-50%, calc(-100% - var(--offset))) rotate(180deg);
--balloon-tail-transform-bottom: translate(-50%, var(--offset));

&.hide {
display: none;
}
}

.mappable--balloon-marker svg {
Expand Down
77 changes: 65 additions & 12 deletions src/markers/MMapBalloonMarker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,74 @@ export type MMapBalloonPositionProps =
| `${VerticalPosition} ${HorizontalPosition}`
| `${HorizontalPosition} ${VerticalPosition}`;

export type MMapBalloonContentProps = (close: () => void) => HTMLElement;

export type MMapBalloonMarkerProps = MMapMarkerProps & {
// TODO: content props as string or function
content: string;
/**
* The function of creating balloon content
* @param close - A function for hiding balloon content. The `MMapBalloonMarker` is not deleted.
* @returns Balloon content as `HTMLElement`
*/
content: MMapBalloonContentProps;
/** The position of the balloon in relation to the point it is pointing to */
position?: MMapBalloonPositionProps;
/** The offset in pixels between the balloon pointer and the point it is pointing to. */
offset?: number;
/** Hide or show balloon on map */
show?: boolean;
/** Balloon closing callback */
onClose?: () => void;
/** Balloon opening callback */
onOpen?: () => void;
};

const defaultProps = Object.freeze({position: 'top', offset: 0});
const defaultProps = Object.freeze({position: 'top', offset: 0, show: true});
type DefaultProps = typeof defaultProps;

/**
* `MMapBalloonMarker` is a balloon (popup) with customized content.
* @example
* ```js
* const balloon = new MMapBalloonMarker({
* content: (close) => createPopupContentHTMLElement(close),
* position: 'top',
* onOpen:() => console.log('open'),
* onClose:() => console.log('close'),
* // support MMapMarker props
* coordinates: BALLOON_COORD,
* draggable: true,
* });
* map.addChild(balloon);
* ```
*/
export class MMapBalloonMarker extends mappable.MMapComplexEntity<MMapBalloonMarkerProps, DefaultProps> {
static defaultProps = defaultProps;
static [mappable.optionsKeyVuefy] = MMapBalloonMarkerVuefyOptions;

public get isOpen() {
return this._props.show;
}
private _markerElement: HTMLElement;
private _balloonContainer: HTMLElement;
private _balloonTail: HTMLElement;
private _marker: MMapMarker;

private _unwatchThemeContext?: () => void;
private _togglePopup(forceShowBalloon?: boolean): void {
let openBalloon = !this._props.show;
if (forceShowBalloon !== undefined) {
openBalloon = forceShowBalloon;
}

this._markerElement.classList.toggle('hide', !openBalloon);

if (openBalloon) {
this._props.onOpen?.();
} else {
this._props.onClose?.();
}

this._props.show = openBalloon;
}

protected __implGetDefaultProps(): DefaultProps {
return MMapBalloonMarker.defaultProps;
Expand All @@ -43,12 +91,13 @@ export class MMapBalloonMarker extends mappable.MMapComplexEntity<MMapBalloonMar

this._balloonContainer = document.createElement('mappable');
this._balloonContainer.classList.add('mappable--balloon-marker_container');
this._balloonContainer.textContent = this._props.content;
this._balloonContainer.appendChild(this._props.content(this.__closePopup));

this._balloonTail = document.createElement('mappable');
this._balloonTail.classList.add('mappable--balloon-marker_tail');
this._balloonTail.innerHTML = tailSVG;

this._togglePopup(this._props.show);
this._updatePosition();
this._updateOffset();

Expand All @@ -58,7 +107,7 @@ export class MMapBalloonMarker extends mappable.MMapComplexEntity<MMapBalloonMar
this._marker = new mappable.MMapMarker(this._props, this._markerElement);
this.addChild(this._marker);

this._unwatchThemeContext = this._watchContext(mappable.ThemeContext, () => this._updateTheme(), {
this._watchContext(mappable.ThemeContext, () => this._updateTheme(), {
immediate: true
});
}
Expand All @@ -72,15 +121,15 @@ export class MMapBalloonMarker extends mappable.MMapComplexEntity<MMapBalloonMar
}

if (propsDiff.content !== undefined) {
this._balloonContainer.textContent = this._props.content;
this._balloonContainer.innerHTML = '';
this._balloonContainer.appendChild(this._props.content(this.__closePopup));
}

this._marker.update(this._props);
}
if (propsDiff.show !== undefined) {
this._togglePopup(propsDiff.show);
}

protected _onDetach(): void {
this._unwatchThemeContext?.();
this._unwatchThemeContext = undefined;
this._marker.update(this._props);
}

private _updateTheme() {
Expand Down Expand Up @@ -133,4 +182,8 @@ export class MMapBalloonMarker extends mappable.MMapComplexEntity<MMapBalloonMar
// check right position
this._markerElement.classList.toggle('position-right', horizontalPosition === 'right');
}

private __closePopup = () => {
this._togglePopup(false);
};
}
10 changes: 6 additions & 4 deletions src/markers/MMapBalloonMarker/vue/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {MMapFeatureProps, MMapMarkerEventHandler} from '@mappable-world/mappable-types';
import {CustomVuefyOptions} from '@mappable-world/mappable-types/modules/vuefy';
import type TVue from '@vue/runtime-core';
import {MMapBalloonMarker, MMapBalloonPositionProps} from '../';
import {MMapBalloonContentProps, MMapBalloonMarker, MMapBalloonMarkerProps, MMapBalloonPositionProps} from '../';

export const MMapBalloonMarkerVuefyOptions: CustomVuefyOptions<MMapBalloonMarker> = {
props: {
Expand All @@ -22,9 +22,11 @@ export const MMapBalloonMarkerVuefyOptions: CustomVuefyOptions<MMapBalloonMarker
onDoubleClick: Function as TVue.PropType<MMapFeatureProps['onDoubleClick']>,
onClick: Function as TVue.PropType<MMapFeatureProps['onClick']>,
onFastClick: Function as TVue.PropType<MMapFeatureProps['onFastClick']>,
// TODO: content props as string or function
content: {type: String, required: true},
content: {type: Function as TVue.PropType<MMapBalloonContentProps>, required: true},
position: {type: String as TVue.PropType<MMapBalloonPositionProps>},
offset: {type: Number, default: 0}
offset: {type: Number, default: 0},
show: {type: Boolean, default: true},
onClose: {type: Function as TVue.PropType<MMapBalloonMarkerProps['onClose']>},
onOpen: {type: Function as TVue.PropType<MMapBalloonMarkerProps['onOpen']>}
}
};
1 change: 1 addition & 0 deletions src/markers/MMapDefaultPopupMarker/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions src/markers/MMapDefaultPopupMarker/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.mappable--default-popup {
display: flex;
flex-direction: column;
row-gap: 8px;
}

.mappable--default-popup_header {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;

.mappable--default-popup_header_title {
font-size: 16px;
font-weight: 400;
line-height: 22px;
color: #050d33;
}
.mappable--default-popup_header_close {
position: absolute;
top: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
border: none;
background: none;
color: #c8c9cc;
cursor: pointer;
}
}

.mappable--default-popup_description {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #7b7d85;
}

.mappable--default-popup_action {
width: max-content;
padding: 12px 16px;
border-radius: 8px;
border: none;
background-color: #eefd7d;
transition: background-color 0.1s ease-out;
color: #050d33;
text-align: center;
cursor: pointer;
white-space: normal;

font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 16px;
}

.mappable--default-popup_action:hover {
background-color: #e5fd30;
}

.mappable--default-popup__dark {
.header_title {
color: #f2f5fa;
}
.header_close {
color: #46464d;
}
.mappable--default-popup_description {
color: #7b7d85;
}
.mappable--default-popup_action {
background-color: #d6fd63;
}
}
119 changes: 119 additions & 0 deletions src/markers/MMapDefaultPopupMarker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {MMapBalloonMarker, MMapBalloonMarkerProps, MMapBalloonContentProps} from '../MMapBalloonMarker';
import './index.css';
import closeSVG from './close.svg';

export type MMapDefaultPopupMarkerProps = Omit<MMapBalloonMarkerProps, 'content'> & {
/** Displayed title in popup header */
title?: string;
/** Displayed description */
description?: string;
/** The inscription on the action button */
action?: string;
/** Callback of click the action button */
onAction?: () => void;
};
/**
* `MMapDefaultPopupMarker` is a default popup that contains a title, a description, and an action button.
* @example
* ```js
* const defaultPopup = new MMapDefaultPopupMarker({
* title: 'Default title',
* description: 'Default description',
* action: 'Make action',
* // support MMapMarker props
* coordinates: POPUP_COORD,
* draggable: true,
* // support MMapBalloonMarker props
* position: 'top',
* });
* map.addChild(defaultPopup);
* ```
*/
export class MMapDefaultPopupMarker extends mappable.MMapComplexEntity<MMapDefaultPopupMarkerProps> {
private _balloon: MMapBalloonMarker;
private _element: HTMLElement;

public get isOpen() {
return this._balloon.isOpen;
}

protected _onAttach(): void {
const {title, description, action} = this._props;

if (title === undefined && description === undefined && action === undefined) {
throw new Error(
'There is no content to display. Specify one of the parameters: title, description, action'
);
}

this._balloon = new MMapBalloonMarker({
...this._props,
content: this.__createDefaultPopup,
onClose: () => (this._props.show = false),
onOpen: () => (this._props.show = true)
});
this.addChild(this._balloon);

this._watchContext(mappable.ThemeContext, () => this._updateTheme(), {immediate: true});
}

protected _onUpdate(propsDiff: Partial<MMapDefaultPopupMarkerProps>, oldProps: MMapDefaultPopupMarkerProps): void {
const {title, description, action} = this._props;

const isTitleChange = oldProps.title !== title;
const isDescriptionChange = oldProps.description !== description;
const isActionChange = oldProps.action !== action;

if (isTitleChange || isDescriptionChange || isActionChange) {
this._balloon.update({content: (close) => this.__createDefaultPopup(close)});
}

this._balloon.update(this._props);
}

private _updateTheme() {
const themeCtx = this._consumeContext(mappable.ThemeContext);
this._element.classList.toggle('mappable--default-popup__dark', themeCtx.theme === 'dark');
}

private __createDefaultPopup: MMapBalloonContentProps = (close) => {
const {title, description, action} = this._props;
this._element = document.createElement('mappable');
this._element.classList.add('mappable--default-popup');

const popupHeaderElement = document.createElement('mappable');
popupHeaderElement.classList.add('mappable--default-popup_header');
this._element.appendChild(popupHeaderElement);

if (title) {
const titleElement = document.createElement('mappable');
titleElement.classList.add('mappable--default-popup_header_title');
titleElement.textContent = title;
popupHeaderElement.appendChild(titleElement);
}

const closeButton = document.createElement('button');
closeButton.classList.add('mappable--default-popup_header_close');
closeButton.innerHTML = closeSVG;
closeButton.addEventListener('click', () => close());
popupHeaderElement.appendChild(closeButton);

if (description) {
const descriptionElement = document.createElement('mappable');
descriptionElement.classList.add('mappable--default-popup_description');
descriptionElement.textContent = description;
this._element.appendChild(descriptionElement);
}
if (action) {
const actionButton = document.createElement('button');
actionButton.classList.add('mappable--default-popup_action');
actionButton.textContent = action;
actionButton.addEventListener('click', () => {
this._props.onAction?.();
});
this._element.appendChild(actionButton);
}

return this._element;
};
}
7 changes: 7 additions & 0 deletions src/markers/MMapTooltipMarker/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.mappable--default-tooltip {
display: block;
max-width: inherit;
max-height: inherit;
overflow: hidden;
text-overflow: ellipsis;
}
Loading

0 comments on commit 7408207

Please sign in to comment.