Greasy Fork

🐭️ MouseHunt Utils

MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.

目前为 2023-02-14 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.cloud/scripts/460027/1149774/%F0%9F%90%AD%EF%B8%8F%20MouseHunt%20Utils.js

// ==UserScript==
// ==UserLibrary==
// @name         🐭️ MouseHunt Utils
// @author       bradp
// @version      1.1.1
// @description  MouseHunt Utils is a library of functions that can be used to make other MouseHunt userscripts easily.
// @license      MIT
// @namespace    bradp
// @match        https://www.mousehuntgame.com/*
// @icon         https://brrad.com/mouse.png
// @grant        none
// ==/UserLibrary==
// ==/UserScript==

/**
 * Add styles to the page.
 *
 * @param {string} styles The styles to add.
 */
const addStyles = (styles) => {
	// Check to see if the existing element exists.
	const existingStyles = document.getElementById('mh-mouseplace-custom-styles');

	// If so, append our new styles to the existing element.
	if (existingStyles) {
		existingStyles.innerHTML += styles;
		return;
	}

	// Otherwise, create a new element and append it to the head.
	const style = document.createElement('style');
	style.id = 'mh-mouseplace-custom-styles';
	style.innerHTML = styles;
	document.head.appendChild(style);
};

/**
 * Do something when ajax requests are completed.
 *
 * @param {Function} callback    The callback to call when an ajax request is completed.
 * @param {string}   url         The url to match. If not provided, all ajax requests will be matched.
 * @param {boolean}  skipSuccess Skip the success check.
 */
const onAjaxRequest = (callback, url = null, skipSuccess = false) => {
	const req = XMLHttpRequest.prototype.open;
	XMLHttpRequest.prototype.open = function () {
		this.addEventListener('load', function () {
			if (this.responseText) {
				let response = {};
				try {
					response = JSON.parse(this.responseText);
				} catch (e) {
					return;
				}

				if (response.success || skipSuccess) {
					if (! url) {
						callback(response);
						return;
					}

					if (this.responseURL.indexOf(url) !== -1) {
						callback(response);
					}
				}
			}
		});
		req.apply(this, arguments);
	};
};

/**
 * Run the callbacks depending on visibility.
 *
 * @param {Object} settings   Settings object.
 * @param {Node}   parentNode The parent node.
 * @param {Object} callbacks  The callbacks to run.
 *
 * @return {Object} The settings.
 */
const runCallbacks = (settings, parentNode, callbacks) => {
	// Loop through the keys on our settings object.
	Object.keys(settings).forEach((key) => {
		// If the parentNode that's passed in contains the selector for the key.
		if (parentNode.classList.contains(settings[ key ].selector)) {
			// Set as visible.
			settings[ key ].isVisible = true;

			// If there is a show callback, run it.
			if (callbacks[ key ] && callbacks[ key ].show) {
				callbacks[ key ].show();
			}
		} else if (settings[ key ].isVisible) {
			// Mark as not visible.
			settings[ key ].isVisible = false;

			// If there is a hide callback, run it.
			if (callbacks[ key ] && callbacks[ key ].hide) {
				callbacks[ key ].hide();
			}
		}
	});

	return settings;
};

/**
 * Do something when the overlay is shown or hidden.
 *
 * @param {Object}   callbacks
 * @param {Function} callbacks.show   The callback to call when the overlay is shown.
 * @param {Function} callbacks.hide   The callback to call when the overlay is hidden.
 * @param {Function} callbacks.change The callback to call when the overlay is changed.
 */
const onOverlayChange = (callbacks) => {
	// Track the different overlay states.
	let overlayData = {
		map: {
			isVisible: false,
			selector: 'treasureMapPopup'
		},
		item: {
			isVisible: false,
			selector: 'itemViewPopup'
		},
		mouse: {
			isVisible: false,
			selector: 'mouseViewPopup'
		},
		image: {
			isVisible: false,
			selector: 'largerImage'
		},
		convertible: {
			isVisible: false,
			selector: 'convertibleOpenViewPopup'
		},
		adventureBook: {
			isVisible: false,
			selector: 'adventureBookPopup'
		},
		marketplace: {
			isVisible: false,
			selector: 'marketplaceViewPopup'
		},
		gifts: {
			isVisible: false,
			selector: 'giftSelectorViewPopup'
		},
		support: {
			isVisible: false,
			selector: 'supportPageContactUsForm'
		},
		premiumShop: {
			isVisible: false,
			selector: 'MHCheckout'
		}
	};

	// Observe the overlayPopup element for changes.
	const observer = new MutationObserver(() => {
		if (callbacks.change) {
			callbacks.change();
		}

		// Grab the overlayPopup element and make sure it has classes on it.
		const overlayType = document.getElementById('overlayPopup');
		if (overlayType && overlayType.classList.length <= 0) {
			return;
		}

		// Grab the overlayBg and check if it is visible or not.
		const overlayBg = document.getElementById('overlayBg');
		if (overlayBg && overlayBg.classList.length > 0) {
			// If there's a show callback, run it.
			if (callbacks.show) {
				callbacks.show();
			}
		} else if (callbacks.hide) {
			// If there's a hide callback, run it.
			callbacks.hide();
		}

		// Run all the specific callbacks.
		overlayData = runCallbacks(overlayData, overlayType, callbacks);
	});

	// Observe the overlayPopup element for changes.
	const observeTarget = document.getElementById('overlayPopup');
	if (observeTarget) {
		observer.observe(observeTarget, {
			attributes: true,
			attributeFilter: ['class']
		});
	}
};

/**
 * Do something when the page or tab changes.
 *
 * @param {Object}   callbacks
 * @param {Function} callbacks.show   The callback to call when the overlay is shown.
 * @param {Function} callbacks.hide   The callback to call when the overlay is hidden.
 * @param {Function} callbacks.change The callback to call when the overlay is changed.
 */
const onPageChange = (callbacks) => {
	// Track our page tab states.
	let tabData = {
		blueprint: {
			isVisible: null,
			selector: 'showBlueprint'
		},
		tem: {
			isVisible: false,
			selector: 'showTrapEffectiveness'
		},
		trap: {
			isVisible: false,
			selector: 'editTrap'
		},
		camp: {
			isVisible: false,
			selector: 'PageCamp'
		},
		travel: {
			isVisible: false,
			selector: 'PageTravel'
		},
		inventory: {
			isVisible: false,
			selector: 'PageInventory'
		},
		shop: {
			isVisible: false,
			selector: 'PageShops'
		},
		mice: {
			isVisible: false,
			selector: 'PageAdversaries'
		},
		friends: {
			isVisible: false,
			selector: 'PageFriends'
		},
		sendSupplies: {
			isVisible: false,
			selector: 'PageSupplyTransfer'
		},
		team: {
			isVisible: false,
			selector: 'PageTeam'
		},
		tournament: {
			isVisible: false,
			selector: 'PageTournament'
		},
		news: {
			isVisible: false,
			selector: 'PageNews'
		},
		scoreboards: {
			isVisible: false,
			selector: 'PageScoreboards'
		},
		discord: {
			isVisible: false,
			selector: 'PageJoinDiscord'
		}
	};

	// Observe the mousehuntContainer element for changes.
	const observer = new MutationObserver(() => {
		// If there's a change callback, run it.
		if (callbacks.change) {
			callbacks.change();
		}

		// Grab the container element and make sure it has classes on it.
		const mhContainer = document.getElementById('mousehuntContainer');
		if (mhContainer && mhContainer.classList.length > 0) {
			// Run the callbacks.
			tabData = runCallbacks(tabData, mhContainer, callbacks);
		}
	});

	// Observe the mousehuntContainer element for changes.
	const observeTarget = document.getElementById('mousehuntContainer');
	if (observeTarget) {
		observer.observe(observeTarget, {
			attributes: true,
			attributeFilter: ['class']
		});
	}
};

/**
 * Do something when the trap tab is changed.
 *
 * @param {Object} callbacks
 */
const onTrapChange = (callbacks) => {
	// Track our trap states.
	let trapData = {
		bait: {
			isVisible: false,
			selector: 'bait'
		},
		base: {
			isVisible: false,
			selector: 'base'
		},
		weapon: {
			isVisible: false,
			selector: 'weapon'
		},
		charm: {
			isVisible: false,
			selector: 'trinket'
		},
		skin: {
			isVisible: false,
			selector: 'skin'
		}
	};

	// Observe the trapTabContainer element for changes.
	const observer = new MutationObserver(() => {
		// Fire the change callback.
		if (callbacks.change) {
			callbacks.change();
		}

		// If we're not viewing a blueprint tab, bail.
		const mhContainer = document.getElementById('mousehuntContainer');
		if (mhContainer.classList.length <= 0 || ! mhContainer.classList.contains('showBlueprint')) {
			return;
		}

		// If we don't have the container, bail.
		const trapContainerParent = document.querySelector('.campPage-trap-blueprintContainer');
		if (! trapContainerParent || ! trapContainerParent.children || ! trapContainerParent.children.length > 0) {
			return;
		}

		// If we're not in the itembrowser, bail.
		const trapContainer = trapContainerParent.children[ 0 ];
		if (! trapContainer || trapContainer.classList.length <= 0 || ! trapContainer.classList.contains('campPage-trap-itemBrowser')) {
			return;
		}

		// Run the callbacks.
		trapData = runCallbacks(trapData, trapContainer, callbacks);
	});

	// Grab the campPage-trap-blueprintContainer element and make sure it has children on it.
	const observeTargetParent = document.querySelector('.campPage-trap-blueprintContainer');
	if (! observeTargetParent || ! observeTargetParent.children || ! observeTargetParent.children.length > 0) {
		return;
	}

	// Observe the first child of the campPage-trap-blueprintContainer element for changes.
	const observeTarget = observeTargetParent.children[ 0 ];
	if (observeTarget) {
		observer.observe(observeTarget, {
			attributes: true,
			attributeFilter: ['class']
		});
	}
};

/**
 * Get the current page slug.
 *
 * @return {string} The page slug.
 */
const getCurrentPage = () => {
	// Grab the container element and make sure it has classes on it.
	const container = document.getElementById('mousehuntContainer');
	if (! container || container.classList.length <= 0) {
		return null;
	}

	// Use the page class as a slug.
	return container.classList[ 0 ].replace('Page', '').toLowerCase();
};

/**
 * Get the saved settings.
 *
 * @param {string}  key          The key to get.
 * @param {boolean} defaultValue The default value.
 *
 * @return {Object} The saved settings.
 */
const getSetting = (key = null, defaultValue = null) => {
	// Grab the local storage data.
	const settings = JSON.parse(localStorage.getItem('mh-mouseplace-settings')) || {};

	// If we didn't get a key passed in, we want all the settings.
	if (! key) {
		return settings;
	}

	// If the setting doesn't exist, return the default value.
	if (Object.prototype.hasOwnProperty.call(settings, key)) {
		return settings[ key ];
	}

	return defaultValue;
};

/**
 * Save a setting.
 *
 * @param {string}  key   The setting key.
 * @param {boolean} value The setting value.
 *
 */
const saveSetting = (key, value) => {
	// Grab all the settings, set the new one, and save them.
	const settings = getSetting();
	settings[ key ] = value;

	localStorage.setItem('mh-mouseplace-settings', JSON.stringify(settings));
};

/**
 * Save a setting and toggle the class in the settings UI.
 *
 * @param {Node}    node  The setting node to animate.
 * @param {string}  key   The setting key.
 * @param {boolean} value The setting value.
 */
const saveSettingAndToggleClass = (node, key, value) => {
	// Toggle the state of the checkbox.
	node.classList.toggle('active');

	// Save the setting.
	saveSetting(key, value);

	// Add the completed class & remove it in a second.
	node.parentNode.classList.add('completed');
	setTimeout(() => {
		node.parentNode.classList.remove('completed');
	}, 1000);
};

/**
 * Add a setting to the preferences page.
 *
 * @param {string}  name         The setting name.
 * @param {string}  key          The setting key.
 * @param {boolean} defaultValue The default value.
 * @param {string}  description  The setting description.
 */
const addSetting = (name, key, defaultValue, description) => {
	// If we're not currently on the preferences page, bail.
	if ('preferences' !== getCurrentPage()) {
		return;
	}

	// Make sure we have the container for our settings.
	const container = document.querySelector('.mousehuntHud-page-tabContent.game_settings');
	if (! container) {
		return;
	}

	// If we don't have our custom settings section, then create it.
	const sectionExists = document.querySelector('#mh-mouseplace-settings');
	if (! sectionExists) {
		// Make the element, add the ID and class.
		const title = document.createElement('div');
		title.id = 'mh-mouseplace-settings';
		title.classList.add('gameSettingTitle');

		// Set the title of our section.
		title.textContent = 'Userscript Settings';

		// Append it.
		container.appendChild(title);

		// Add a separator.
		const seperator = document.createElement('div');
		seperator.classList.add('separator');

		// Append the separator.
		container.appendChild(seperator);
	}

	// If we already have a setting visible for our key, bail.
	const settingExists = document.getElementById(`mh-mouseplace-setting-${ key }`);
	if (settingExists) {
		return;
	}

	// Create the markup for the setting row.
	const settings = document.createElement('div');
	settings.classList.add('settingRowTable');
	settings.id = `mh-mouseplace-setting-${ key }`;

	const settingRow = document.createElement('div');
	settingRow.classList.add('settingRow');

	const settingRowLabel = document.createElement('div');
	settingRowLabel.classList.add('settingRow-label');

	const settingName = document.createElement('div');
	settingName.classList.add('name');
	settingName.innerHTML = name;

	const defaultSettingText = document.createElement('div');
	defaultSettingText.classList.add('defaultSettingText');
	defaultSettingText.textContent = defaultValue ? 'Enabled' : 'Disabled';

	const settingDescription = document.createElement('div');
	settingDescription.classList.add('description');
	settingDescription.innerHTML = description;

	settingRowLabel.appendChild(settingName);
	settingRowLabel.appendChild(defaultSettingText);
	settingRowLabel.appendChild(settingDescription);

	const settingRowAction = document.createElement('div');
	settingRowAction.classList.add('settingRow-action');

	const settingRowInput = document.createElement('div');
	settingRowInput.classList.add('settingRow-action-inputContainer');

	const settingRowInputCheckbox = document.createElement('div');
	settingRowInputCheckbox.classList.add('mousehuntSettingSlider');

	// Depending on the current state of the setting, add the active class.
	const currentSetting = getSetting(key);
	let isActive = false;
	if (currentSetting) {
		settingRowInputCheckbox.classList.add('active');
		isActive = true;
	} else if (null === currentSetting && defaultValue) {
		settingRowInputCheckbox.classList.add('active');
		isActive = true;
	}

	// Event listener for when the setting is clicked.
	settingRowInputCheckbox.onclick = (event) => {
		saveSettingAndToggleClass(event.target, key, ! isActive);
	};

	// Add the input to the settings row.
	settingRowInput.appendChild(settingRowInputCheckbox);
	settingRowAction.appendChild(settingRowInput);

	// Add the label and action to the settings row.
	settingRow.appendChild(settingRowLabel);
	settingRow.appendChild(settingRowAction);

	// Add the settings row to the settings container.
	settings.appendChild(settingRow);
	container.appendChild(settings);
};

/**
 * POST a request to the server and return the response.
 *
 * @param {string} url      The url to post to, not including the base url.
 * @param {Object} formData The form data to post.
 *
 * @return {Promise} The response.
 */
const doRequest = async (url, formData) => {
	// If we don't have the needed params, bail.
	if ('undefined' === typeof lastReadJournalEntryId || 'undefined' === typeof user) {
		return;
	}

	// If our needed params are empty, bail.
	if (! lastReadJournalEntryId || ! user || ! user.unique_hash) { // eslint-disable-line no-undef
		return;
	}

	// Build the form for the request.
	const form = new FormData();
	form.append('sn', 'Hitgrab');
	form.append('hg_is_ajax', 1);
	form.append('last_read_journal_entry_id', lastReadJournalEntryId ? lastReadJournalEntryId : 0); // eslint-disable-line no-undef
	form.append('uh', user.unique_hash ? user.unique_hash : ''); // eslint-disable-line no-undef

	// Add in the passed in form data.
	for (const key in formData) {
		form.append(key, formData[ key ]);
	}

	// Convert the form to a URL encoded string for the body.
	const requestBody = new URLSearchParams(form).toString();

	// Send the request.
	const response = await fetch(
		callbackurl ? callbackurl + url : 'https://www.mousehuntgame.com/' + url, // eslint-disable-line no-undef
		{
			method: 'POST',
			body: requestBody,
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded',
			},
		}
	);

	// Wait for the response and return it.
	const data = await response.json();
	return data;
};

/**
 *  Add a submenu item to a menu.
 *
 * @param {Object} options The options for the submenu item.
 */
const addSubmenuItem = (options) => {
	// Default to sensible values.
	const settings = Object.assign({}, {
		menu: 'kingdom',
		label: '',
		icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/prize_shoppe.png',
		href: '',
		callback: null,
		external: false,
	}, options);

	// Grab the menu item we want to add the submenu to.
	const menuTarget = document.querySelector(`.mousehuntHud-menu .${ settings.menu }`);
	if (! menuTarget) {
		return;
	}

	// If the menu already has a submenu, just add the item to it.
	if (! menuTarget.classList.contains('hasChildren')) {
		menuTarget.classList.add('hasChildren');
	}

	let submenu = menuTarget.querySelector('ul');
	if (! submenu) {
		submenu = document.createElement('ul');
		menuTarget.appendChild(submenu);
	}

	// Create the item.
	const item = document.createElement('li');
	item.classList.add('custom-submenu-item');
	const cleanLabel = settings.label.toLowerCase().replace(/[^a-z0-9]/g, '-');

	const exists = document.querySelector(`#custom-submenu-item-${ cleanLabel }`);
	if (exists) {
		return;
	}

	item.id = `custom-submenu-item-${ cleanLabel }`;

	// Create the link.
	const link = document.createElement('a');
	link.href = settings.href || '#';

	if (settings.callback) {
		link.addEventListener('click', (e) => {
			e.preventDefault();
			settings.callback();
		});
	}

	// Create the icon.
	const icon = document.createElement('div');
	icon.classList.add('icon');
	icon.styles = `background-image: url(${ settings.icon });`;

	// Create the label.
	const name = document.createElement('div');
	name.classList.add('name');
	name.innerText = settings.label;

	// Add the icon and label to the link.
	link.appendChild(icon);
	link.appendChild(name);

	// If it's an external link, also add the icon for it.
	if (settings.external) {
		const externalLinkIcon = document.createElement('div');
		externalLinkIcon.classList.add('external_icon');
		link.appendChild(externalLinkIcon);

		// Set the target to _blank so it opens in a new tab.
		link.target = '_blank';
		link.rel = 'noopener noreferrer';
	}

	// Add the link to the item.
	item.appendChild(link);

	// Add the item to the submenu.
	submenu.appendChild(item);
};

const addMouseripLink = () => {
	addSubmenuItem({
		menu: 'kingdom',
		label: 'mouse.rip',
		icon: 'https://www.mousehuntgame.com/images/ui/hud/menu/prize_shoppe.png',
		href: 'https://mouse.rip',
		external: true,
	});
};

/**
 * Build a popup.
 *
 * Templates:
 *   ajax: no close button in lower right, 'prefix' instead of title. 'suffix' for close button area.
 *   default: {*title*} {*content*}
 *   error: in red, with error icon{*title*} {*content*}
 *   largerImage: full width image {*title*} {*image*}
 *   largerImageWithClass: smaller than larger image, with caption {*title*} {*image*} {*imageCaption*} {*imageClass*} (goes on the img tag)
 *   loading: Just says loading
 *   multipleItems: {*title*} {*content*} {*items*}
 *   singleItemLeft: {*title*} {*content*} {*items*}
 *   singleItemRight: {*title*} {*content*} {*items*}
 *
 * @param {Object} options The popup options.
 */
const createPopup = (options) => {
	// If we don't have jsDialog, bail.
	if ('undefined' === typeof jsDialog || ! jsDialog) { // eslint-disable-line no-undef
		return;
	}

	// Default to sensible values.
	const settings = Object.assign({}, {
		title: '',
		content: '',
		hasCloseButton: true,
		template: 'default',
		show: true,
	}, options);

	// Initiate the popup.
	const popup = new jsDialog(); // eslint-disable-line no-undef
	popup.setIsModal(! settings.hasCloseButton);

	// Set the template & add in the content.
	popup.setTemplate(settings.template);
	popup.addToken('{*title*}', settings.title);
	popup.addToken('{*content*}', settings.content);

	// If we want to show the popup, show it.
	if (settings.show) {
		popup.show();
	}

	return popup;
};

/**
 * Create a popup with an image.
 *
 * @param {Object} options Popup options.
 */
const createImagePopup = (options) => {
	// Default to sensible values.
	const settings = Object.assign({}, {
		title: '',
		image: '',
		show: true,
	}, options);

	// Create the popup.
	const popup = createPopup({
		title: settings.title,
		template: 'largerImage',
		show: false,
	});

	// Add the image to the popup.
	popup.addToken('{*image*}', settings.image);

	// If we want to show the popup, show it.
	if (settings.show) {
		popup.show();
	}

	return popup;
};

/**
 * Show a map-popup.
 *
 * @param {Object} options The popup options.
 */
const createMapPopup = (options) => {
	// Check to make sure we can call the hg views.
	if (! (hg && hg.views && hg.views.TreasureMapDialogView)) { // eslint-disable-line no-undef
		return;
	}

	// Default to sensible values.
	const settings = Object.assign({}, {
		title: '',
		content: '',
		closeClass: 'acknowledge',
		closeText: 'ok',
		show: true,
	}, options);

	// Initiate the popup.
	const dialog = new hg.views.TreasureMapDialogView(); // eslint-disable-line no-undef

	// Set all the content and options.
	dialog.setTitle(options.title);
	dialog.setContent(options.content);
	dialog.setCssClass(options.closeClass);
	dialog.setContinueAction(options.closeText);

	// If we want to show & we can show, show it.
	if (settings.show && hg.controllers && hg.controllers.TreasureMapDialogController) { // eslint-disable-line no-undef
		hg.controllers.TreasureMapController.show(); // eslint-disable-line no-undef
		hg.controllers.TreasureMapController.showDialog(dialog); // eslint-disable-line no-undef
	}

	return dialog;
};

/**
 * Make an element draggable. Saves the position to local storage.
 *
 * @param {string} dragTarget The selector for the element that should be dragged.
 * @param {string} dragHandle The selector for the element that should be used to drag the element.
 * @param {number} defaultX   The default X position.
 * @param {number} defaultY   The default Y position.
 * @param {string} storageKey The key to use for local storage.
 */
const makeElementDraggable = (dragTarget, dragHandle, defaultX = null, defaultY = null, storageKey = null) => {
	const modal = document.querySelector(dragTarget);
	if (! modal) {
		return;
	}

	const handle = document.querySelector(dragHandle);
	if (! handle) {
		return;
	}

	/**
	 * Make sure the coordinates are within the bounds of the window.
	 *
	 * @param {string} type  The type of coordinate to check.
	 * @param {number} value The value of the coordinate.
	 *
	 * @return {number} The value of the coordinate, or the max/min value if it's out of bounds.
	 */
	const keepWithinLimits = (type, value) => {
		if ('top' === type) {
			return value < -20 ? -20 : value;
		}

		if (value < (handle.offsetWidth * -1) + 20) {
			return (handle.offsetWidth * -1) + 20;
		}

		if (value > document.body.clientWidth - 20) {
			return document.body.clientWidth - 20;
		}

		return value;
	};

	/**
	 * When the mouse is clicked, add the class and event listeners.
	 *
	 * @param {Object} e The event object.
	 */
	const onMouseDown = (e) => {
		e.preventDefault();

		// Get the current mouse position.
		x1 = e.clientX;
		y1 = e.clientY;

		// Add the class to the element.
		modal.classList.add('mh-is-dragging');

		// Add the onDrag and finishDrag events.
		document.onmousemove = onDrag;
		document.onmouseup = finishDrag;
	};

	/**
	 * When the drag is finished, remove the dragging class and event listeners, and save the position.
	 */
	const finishDrag = () => {
		document.onmouseup = null;
		document.onmousemove = null;

		// Remove the class from the element.
		modal.classList.remove('mh-is-dragging');

		if (storageKey) {
			localStorage.setItem(storageKey, JSON.stringify({ x: modal.offsetLeft, y: modal.offsetTop }));
		}
	};

	/**
	 * When the mouse is moved, update the element's position.
	 *
	 * @param {Object} e The event object.
	 */
	const onDrag = (e) => {
		e.preventDefault();

		// Calculate the new cursor position.
		x2 = x1 - e.clientX;
		y2 = y1 - e.clientY;

		x1 = e.clientX;
		y1 = e.clientY;

		const newLeft = keepWithinLimits('left', modal.offsetLeft - x2);
		const newTop = keepWithinLimits('top', modal.offsetTop - y2);

		// Set the element's new position.
		modal.style.left = `${ newLeft }px`;
		modal.style.top = `${ newTop }px`;
	};

	// Set the default position.
	let startX = defaultX || 0;
	let startY = defaultY || 0;

	// If the storageKey was passed in, get the position from local storage.
	if (storageKey) {
		const storedPosition = localStorage.getItem(storageKey);
		if (storedPosition) {
			const position = JSON.parse(storedPosition);

			// Make sure the position is within the bounds of the window.
			startX = keepWithinLimits('left', position.x);
			startY = keepWithinLimits('top', position.y);
		}
	}

	// Set the element's position.
	modal.style.left = `${ startX }px`;
	modal.style.top = `${ startY }px`;

	// Set up our variables to track the mouse position.
	let x1 = 0,
		y1 = 0,
		x2 = 0,
		y2 = 0;

	// Add the event listener to the handle.
	handle.onmousedown = onMouseDown;
};

/**
 * Log to the console.
 *
 * @param {string|Object} message The message to log.
 */
const clog = (message) => {
	// If a string is passed in, log it in line with our prefix.
	if ('string' === typeof message) {
		console.log(`%c[MousePlace] %c${ message }`, 'color: #ff0000; font-weight: bold;', 'color: #000000;'); // eslint-disable-line no-console
	} else {
		// Otherwise, log it separately.
		console.log('%c[MousePlace]', 'color: #ff0000; font-weight: bold;', 'color: #000000;'); // eslint-disable-line no-console
		console.log(message); // eslint-disable-line no-console
	}
};