@fleetbase/ember-ui
Version:
Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.
575 lines (497 loc) • 16.1 kB
JavaScript
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action, set, get, getProperties, setProperties } from '@ember/object';
import { assert } from '@ember/debug';
import { isArray } from '@ember/array';
import { defer } from 'rsvp';
import { guidFor } from '@ember/object/internals';
const { assign } = Object;
export default class ModalsManagerService extends Service {
events;
modals = [];
defaultOptions = {
title: null,
body: null,
footer: null,
confirmButtonDefaultText: 'Yes',
confirmButtonFulfilledText: 'Yes',
confirmButtonPendingText: 'Yes',
confirmButtonRejectedText: 'Yes',
declineButtonDefaultText: 'No',
declineButtonFulfilledText: 'No',
declineButtonPendingText: 'No',
declineButtonRejectedText: 'No',
cancel: 'Cancel',
backdrop: true,
backdropClose: true,
backdropTransitionDuration: 150,
fade: true,
keyboard: true,
position: 'top',
renderInPlace: false,
scrollable: false,
size: null,
transitionDuration: 300,
confirmIsActive: false,
confirmButtonSize: 'md',
confirmButtonType: 'primary',
confirmIconActive: '',
confirmIconInactive: '',
declineIsActive: false,
declineButtonSize: 'md',
declineButtonType: 'default',
declineIconActive: '',
declineIconInactive: '',
modalClass: '',
};
// Computed properties for backward compatibility
get modalIsOpened() {
return this.modals.length > 0;
}
get componentToRender() {
const topModal = this.getTopModal();
return topModal ? topModal.componentToRender : null;
}
get options() {
const topModal = this.getTopModal();
return topModal ? topModal.options : {};
}
get modalDefer() {
const topModal = this.getTopModal();
return topModal ? topModal.defer : null;
}
/**
* Get the top-most (most recently opened) modal
* @return {Object|null}
*/
getTopModal() {
return this.modals.length > 0 ? this.modals[this.modals.length - 1] : null;
}
/**
* Get modal by ID
* @param {String} modalId
* @return {Object|null}
*/
getModalById(modalId) {
return this.modals.find((modal) => modal.id === modalId) || null;
}
/**
* Get the z-index for a modal based on its position in the stack
* @param {String} modalId
* @return {Number}
*/
getModalZIndex(modalId) {
const index = this.modals.findIndex((modal) => modal.id === modalId);
return index >= 0 ? 1060 + index * 10 : 1060;
}
/**
* @param componentToRender Component's child-class represents needed modal
* @param options options passed to the rendered modal
*/
show(componentToRender, options) {
const component = componentToRender;
const opts = assign({}, this.defaultOptions, options, { _zIndex: 1060 + this.modals.length * 10 });
const modalId = guidFor({}) + '-' + Date.now(); // Generate unique ID
const modalDefer = defer();
const modal = {
id: modalId,
componentToRender: component,
options: opts,
defer: modalDefer,
isOpen: true,
createdAt: new Date(),
};
// Add modal to the stack
this.modals = [...this.modals, modal];
// Trigger modal opened event
if (this.events) {
this.events.trackEvent('ui.modal.opened', component, opts);
}
return modalDefer.promise;
}
/**
* Shows a confirmation dialog
*
* @method confirm
* @param {object} options
* @return {RSVP.Promise}
*/
confirm(options = {}) {
let modalClass = 'flb--confirm-modal modal-sm';
if (typeof options.modalClass === 'string') {
modalClass = `flb--confirm-modal ${options.modalClass}`;
}
options = assign(options, {
hideTitle: true,
modalClass,
});
return this.show('modal/layouts/confirm', options);
}
/**
* Shows a alert dialog
*/
alert(options = {}) {
let modalClass = 'flb--alert-modal modal-sm';
if (typeof options.modalClass === 'string') {
modalClass = `flb--alert-modal ${options.modalClass}`;
}
options = assign(options, {
hideTitle: true,
hideAcceptButton: true,
declineButtonText: 'OK',
modalClass,
});
return this.show('modal/layouts/alert', options);
}
/**
* Shows a prompt dialog
*/
prompt(options = {}) {
return this.show('modal/layouts/prompt', options);
}
/**
* Shows a bulk action dialog
*/
bulk(options = {}) {
return this.show('modal/layouts/bulk-action', options);
}
/**
* Shows a progress dialog
* @category Default Modals
* @throws {Error} if `options.promises` is not an array
*/
progress(options = {}) {
assert('`options.promises` must be an array', options && isArray(options.promises));
return this.show('modal/layouts/progress', options);
}
/**
* Shows a process dialog
* @category Default Modals
* @throws {Error} if `options.process` is not defined
*/
process(options = {}) {
assert('`options.process` must be defined', !!(options && options?.process));
return this.show('modal/layouts/process', options);
}
/**
* Shows a loading dialog
*
* @method confirm
* @param {object} options
* @return {RSVP.Promise}
*/
async displayLoader(options = {}) {
await this.done();
this.show('modal/layouts/loading', { title: 'Loading...', ...options });
}
/**
* Alias for showing a loading dialog
*
* @method confirm
* @param {object} options
* @return {RSVP.Promise}
*/
async loader(options = {}) {
return this.displayLoader(options);
}
/**
* Shows a dialog that allows user to select options from prompt
*
* @method confirm
* @param {object} options
* @return {RSVP.Promise}
*/
async userSelectOption(title, promptOptions = [], modalOptions = {}) {
await this.done();
return new Promise((resolve) => {
const selected = null;
this.show('modal/layouts/option-prompt', {
title,
promptOptions,
selected,
selectOption: (event) => {
const { value } = event.target;
const topModal = this.getTopModal();
if (topModal) {
this.setOptionForModal(topModal.id, 'selected', value);
}
},
confirm: () => {
const topModal = this.getTopModal();
if (topModal) {
this.startLoadingForModal(topModal.id);
const selected = this.getOptionForModal(topModal.id, 'selected');
this.done(topModal.id);
resolve(selected);
}
},
decline: () => {
const topModal = this.getTopModal();
if (topModal) {
this.done(topModal.id);
}
resolve(null);
},
...modalOptions,
});
});
}
/**
* Same as onClickConfirm but allows a handler to run then resolve by user
*
* @param {String} modalId
*/
onClickConfirmWithDone(modalId) {
const modal = this.getModalById(modalId);
if (!modal) return;
const done = this.done.bind(this, modalId, 'onConfirm');
const { confirm, keepOpen } = modal.options;
if (typeof confirm === 'function') {
const response = confirm(this, done);
// hack keep dialog open until hold is true
if (keepOpen === true) {
return;
}
if (response && typeof response.then === 'function') {
return response.finally(() => {
return done();
});
}
return;
}
return done();
}
/**
* Same as onClickDecline but allows a handler to run then resolve by user
*
* @param {String} modalId
*/
onClickDeclineWithDone(modalId) {
const modal = this.getModalById(modalId);
if (!modal) return;
const done = this.done.bind(this, modalId, 'onDecline');
const { decline, keepOpen } = modal.options;
if (typeof decline === 'function') {
const response = decline(this, done);
// hack keep dialog open until hold is true
if (keepOpen === true) {
return;
}
if (response && typeof response.then === 'function') {
return response.finally(() => {
return done();
});
}
return;
}
return done();
}
/**
* Closes the modal and cleans up
*
* @param {String} modalId - ID of the modal to close, if not provided closes the top modal
* @param {String} action - The action that triggered the close
*/
done(modalId, action) {
return new Promise((resolve) => {
let modal;
if (modalId) {
modal = this.getModalById(modalId);
} else {
// If no modalId provided, close the top modal (backward compatibility)
modal = this.getTopModal();
}
if (!modal) {
resolve(true);
return;
}
const callback = get(modal.options, `${action}`);
const onFinish = modal.options.onFinish;
// Remove modal from the stack
this.modals = this.modals.filter((m) => m.id !== modal.id);
// Trigger modal closed event
if (this.events) {
this.events.trackEvent('ui.modal.closed', modal.componentToRender, action, modal.options);
}
// Resolve the modal's promise
modal.defer?.resolve(this);
if (typeof callback === 'function') {
callback(modal.options);
}
if (typeof onFinish === 'function') {
onFinish(modal.options);
}
resolve(true);
});
}
/**
* Close all modals
*/
closeAll() {
const modalsCopy = [...this.modals];
modalsCopy.forEach((modal) => {
this.done(modal.id);
});
}
/**
* Close the top-most modal
*/
closeTop() {
const topModal = this.getTopModal();
if (topModal) {
this.done(topModal.id);
}
}
/**
* Retrieves an option from the top modal or a specific modal
*
* @param {String} key
* @param {Mixed} defaultValue
* @param {String} modalId - Optional modal ID, defaults to top modal
* @return {Mixed}
*/
getOption(key, defaultValue = null, modalId = null) {
if (isArray(key)) {
return this.getOptions(key, modalId);
}
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (!modal) return defaultValue;
const value = get(modal.options, key);
if (value === undefined) {
return defaultValue;
}
return value;
}
/**
* Retrieves an option from a specific modal
*
* @param {String} modalId
* @param {String} key
* @param {Mixed} defaultValue
* @return {Mixed}
*/
getOptionForModal(modalId, key, defaultValue = null) {
return this.getOption(key, defaultValue, modalId);
}
/**
* Allows multiple options to be get
*
* @param {Array} props
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
getOptions(props = [], modalId = null) {
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (!modal) return {};
if (props?.length === 0) {
return modal.options ?? {};
}
return getProperties(modal.options, props);
}
/**
* Updates an option in the top modal or a specific modal
*
* @param {String} key
* @param {Mixed} value
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
setOption(key, value, modalId = null) {
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (modal) {
set(modal.options, key, value);
}
}
/**
* Updates an option in a specific modal
*
* @param {String} modalId
* @param {String} key
* @param {Mixed} value
*/
setOptionForModal(modalId, key, value) {
this.setOption(key, value, modalId);
}
/**
* Allows multiple options to be updated
*
* @param {Object} options
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
setOptions(options = {}, modalId = null) {
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (modal) {
setProperties(modal.options, options);
}
}
/**
* Executes a function passed in options
*
* @param {String} fn
* @param {String} modalId - Optional modal ID, defaults to top modal
* @param {...any} params
*/
invoke(fn, modalId = null, ...params) {
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (!modal) return null;
const callable = get(modal.options, fn);
if (typeof callable === 'function') {
return callable(...params);
}
return null;
}
/**
* Alias to start loading indicator on modal
*
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
startLoading(modalId = null) {
this.setOption('isLoading', true, modalId);
}
/**
* Alias to start loading indicator on a specific modal
*
* @param {String} modalId
*/
startLoadingForModal(modalId) {
this.startLoading(modalId);
}
/**
* Alias to stop loading indicator on modal
*
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
stopLoading(modalId = null) {
this.setOption('isLoading', false, modalId);
}
/**
* Alias to stop loading indicator on a specific modal
*
* @param {String} modalId
*/
stopLoadingForModal(modalId) {
this.stopLoading(modalId);
}
/**
* Clear modalsManager options for a specific modal or top modal.
*
* @param {String} modalId - Optional modal ID, defaults to top modal
*/
clearOptions(modalId = null) {
const modal = modalId ? this.getModalById(modalId) : this.getTopModal();
if (modal) {
modal.options = {};
}
}
/**
* Handle keyboard events (like ESC key) for the top modal
*
* @param {KeyboardEvent} event
*/
handleKeyboardEvent(event) {
if (event.key === 'Escape') {
const topModal = this.getTopModal();
if (topModal && topModal.options.keyboard !== false) {
this.done(topModal.id);
}
}
}
}