diff --git a/public/js/components/Bootstrap/Alert.js b/public/js/components/Bootstrap/Alert.js
new file mode 100644
index 000000000..b261eeaef
--- /dev/null
+++ b/public/js/components/Bootstrap/Alert.js
@@ -0,0 +1,44 @@
+import BsModal from './Modal.js';
+
+export default {
+ components: {
+ BsModal
+ },
+ mixins: [
+ BsModal
+ ],
+ props: {
+ dialogClass: {
+ type: [String,Array,Object],
+ default: 'modal-dialog-centered'
+ },
+ /*
+ * NOTE(chris):
+ * Hack to expose in "emits" declared events to $props which we use
+ * in the v-bind directive to forward all events.
+ * @see: https://github.com/vuejs/core/issues/3432
+ */
+ onHideBsModal: Function,
+ onHiddenBsModal: Function,
+ onHidePreventedBsModal: Function,
+ onShowBsModal: Function,
+ onShownBsModal: Function
+ },
+ data: () => ({
+ result: true
+ }),
+ mounted() {
+ this.modal = this.$refs.modalContainer.modal;
+ },
+ popup(msg, options) {
+ return BsModal.popup.bind(this)(msg, options);
+ },
+ template: `
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Bootstrap/Confirm.js b/public/js/components/Bootstrap/Confirm.js
new file mode 100644
index 000000000..1c609d457
--- /dev/null
+++ b/public/js/components/Bootstrap/Confirm.js
@@ -0,0 +1,22 @@
+import BsAlert from './Alert';
+
+export default {
+ mixins: [
+ BsAlert
+ ],
+ data: () => ({
+ result: false
+ }),
+ popup(msg, options) {
+ return BsAlert.popup.bind(this)(msg, options);
+ },
+ template: `
+
+
+
+
+
+
+
+ `
+}
diff --git a/public/js/components/Bootstrap/Modal.js b/public/js/components/Bootstrap/Modal.js
new file mode 100644
index 000000000..c99aabb58
--- /dev/null
+++ b/public/js/components/Bootstrap/Modal.js
@@ -0,0 +1,103 @@
+export default {
+ data: () => ({
+ modal: null
+ }),
+ props: {
+ backdrop: {
+ type: [Boolean,String],
+ default: true,
+ validator(value) {
+ return ['static', true, false].includes(value);
+ }
+ },
+ focus: {
+ type: Boolean,
+ default: true
+ },
+ keyboard: {
+ type: Boolean,
+ default: true
+ },
+ noCloseBtn: Boolean,
+ dialogClass: [String,Array,Object]
+ },
+ emits: [
+ "hideBsModal",
+ "hiddenBsModal",
+ "hidePreventedBsModal",
+ "showBsModal",
+ "shownBsModal"
+ ],
+ methods: {
+ dispose() {
+ return this.modal.dispose();
+ },
+ handleUpdate() {
+ return this.modal.handleUpdate();
+ },
+ hide() {
+ return this.modal.hide();
+ },
+ show(relatedTarget) {
+ return this.modal.show(relatedTarget);
+ },
+ toggle() {
+ return this.modal.toggle();
+ }
+ },
+ mounted() {
+ this.modal = new bootstrap.Modal(this.$refs.modal, {
+ backdrop: this.backdrop,
+ focus: this.focus,
+ keyboard: this.keyboard
+ });
+ },
+ popup(body, options, title, footer) {
+ const BsModal = this;
+ return new Promise((resolve,reject) => {
+ const instance = Vue.createApp({
+ setup() {
+ return () => Vue.h(BsModal, {...{
+ class: 'fade'
+ },...options, ...{
+ ref: 'modal',
+ 'onHidden.bs.modal': instance.unmount
+ }}, {
+ title: () => title,
+ default: () => body,
+ footer: () => footer
+ });
+ },
+ mounted() {
+ this.$refs.modal.show();
+ },
+ beforeUnmount() {
+ if (this.$refs.modal)
+ this.$refs.modal.result !== false ? resolve(this.$refs.modal.result) : reject();
+ },
+ unmounted() {
+ wrapper.parentElement.removeChild(wrapper);
+ }
+ });
+ const wrapper = document.createElement("div");
+ instance.mount(wrapper);
+ document.body.appendChild(wrapper);
+ });
+ },
+ template: `
`
+}
diff --git a/public/js/components/Bootstrap/Prompt.js b/public/js/components/Bootstrap/Prompt.js
new file mode 100644
index 000000000..c056d4237
--- /dev/null
+++ b/public/js/components/Bootstrap/Prompt.js
@@ -0,0 +1,36 @@
+import BsAlert from './Alert';
+
+export default {
+ mixins: [
+ BsAlert
+ ],
+ props: {
+ placeholder: String,
+ default: String
+ },
+ data: () => ({
+ value: '',
+ result: false
+ }),
+ created() {
+ if (this.default)
+ this.value = this.default;
+ },
+ popup(msg, options) {
+ if (typeof options === 'string')
+ options = { default: options };
+ return BsAlert.popup.bind(this)(msg, options);
+ },
+ template: `
+
+
+
+
+
+
+
+
+
+
+ `
+}