From d4ed5e042ccf4a3e4b714d23c01dc3b29344c485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Ste=CC=A8pien=CC=81?= Date: Tue, 17 Oct 2023 01:26:30 +0200 Subject: [PATCH] Bootstrap dynamic imports POC --- .../theme/utility/_dynamic-import-fix.scss | 4 +- .../dynamic-bootstrap-components.js | 160 +++++++++----- _dev/js/theme/utils/DynamicImportDOMEvents.js | 42 ---- _dev/js/theme/utils/DynamicImportHandler.js | 51 ----- .../theme/utils/DynamicImportJqueryPlugin.js | 41 ---- .../useBootstrapComponentDynamicImport.js | 202 ++++++++++++++++++ .../vendors/bootstrap/bootstrap-imports.js | 13 +- 7 files changed, 324 insertions(+), 189 deletions(-) delete mode 100644 _dev/js/theme/utils/DynamicImportDOMEvents.js delete mode 100644 _dev/js/theme/utils/DynamicImportHandler.js delete mode 100644 _dev/js/theme/utils/DynamicImportJqueryPlugin.js create mode 100644 _dev/js/theme/utils/dynamicImports/useBootstrapComponentDynamicImport.js diff --git a/_dev/css/theme/utility/_dynamic-import-fix.scss b/_dev/css/theme/utility/_dynamic-import-fix.scss index 04e919c7..4e63f495 100644 --- a/_dev/css/theme/utility/_dynamic-import-fix.scss +++ b/_dev/css/theme/utility/_dynamic-import-fix.scss @@ -9,6 +9,8 @@ } //FIX TO TOAST, opacity by default -.toast { +.toast:not(.show), +.fade:not(.show) { opacity: 0; + display: none; } diff --git a/_dev/js/theme/components/dynamic-bootstrap-components.js b/_dev/js/theme/components/dynamic-bootstrap-components.js index ae25b61b..d51710bf 100644 --- a/_dev/js/theme/components/dynamic-bootstrap-components.js +++ b/_dev/js/theme/components/dynamic-bootstrap-components.js @@ -1,80 +1,138 @@ import $ from 'jquery'; -import DynamicImportHandler from '@js/theme/utils/DynamicImportHandler'; +import DOMReady from "../utils/DOMReady"; +import useBootstrapComponentDynamicImport from '../utils/dynamicImports/useBootstrapComponentDynamicImport'; -$(() => { +DOMReady(() => { /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "import" }] */ - const importModal = new DynamicImportHandler({ - jqueryPluginCover: 'modal', - DOMEvents: 'click', - DOMEventsSelector: '[data-bs-toggle="modal"]', - DOMEventsPreventDefault: true, - files: () => [ + const { init: initDynamicImportForModal } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/modal'), import('@css/dynamic/modal/_index.scss'), ], - }); - - const importOffcanvas = new DynamicImportHandler({ - jqueryPluginCover: 'offcanvas', - DOMEvents: 'click', - DOMEventsSelector: '[data-bs-toggle="offcanvas"]', - DOMEventsPreventDefault: true, - files: () => [ + { + events: [ + { + name: 'click', + selector: '[data-bs-toggle="modal"]', + }, + ], + componentName: 'Modal', + }, + ); + + initDynamicImportForModal(); + + // const modal = new bootstrap.Modal('#testModal', { + // keyboard: false, + // }); + // + // const handleTestModal = () => { + // modal.toggle(); + // console.log('toggle'); + // setTimeout(handleTestModal, 4000); + // } + // + // handleTestModal(); + + const { init: initDynamicImportForOffcanvas } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/offcanvas'), import('@css/dynamic/offcanvas/_index.scss'), ], - }); - - const importDropdown = new DynamicImportHandler({ - jqueryPluginCover: 'dropdown', - DOMEvents: 'click', - DOMEventsSelector: '[data-bs-toggle="dropdown"]', - DOMEventsPreventDefault: true, - files: () => [ + { + events: [ + { + name: 'click', + selector: '[data-bs-toggle="offcanvas"]', + }, + ], + componentName: 'Offcanvas', + }, + ); + + initDynamicImportForOffcanvas(); + + const { init: initDynamicImportForDropdown } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/dropdown'), import('@css/dynamic/dropdown/_index.scss'), ], - }); - - const importCollapse = new DynamicImportHandler({ - jqueryPluginCover: 'collapse', - DOMEvents: 'click', - DOMEventsSelector: '[data-bs-toggle="collapse"]', - DOMEventsPreventDefault: true, - files: () => [ + { + events: [ + { + name: 'click', + selector: '[data-bs-toggle="dropdown"]', + }, + ], + componentName: 'Dropdown', + }, + ); + + initDynamicImportForDropdown(); + + const { init: initDynamicImportForCollapse } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/collapse'), ], - }); + { + events: [ + { + name: 'click', + selector: '[data-bs-toggle="collapse"]', + }, + ], + componentName: 'Collapse', + }, + ); + + initDynamicImportForCollapse(); - const importPopover = new DynamicImportHandler({ - jqueryPluginCover: 'popover', - files: () => [ + const { init: initDynamicImportForPopover } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/popover'), import('@css/dynamic/popover/_index.scss'), ], - }); + { + componentName: 'Popover', + events: [ + { + name: 'click', + selector: '[data-bs-toggle="popover"]', + }, + ], + }, + ); - const importScrollspy = new DynamicImportHandler({ - jqueryPluginCover: 'scrollspy', - files: () => [ - import('bootstrap/js/src/scrollspy'), - ], - }); + initDynamicImportForPopover(); - const importToast = new DynamicImportHandler({ - jqueryPluginCover: 'toast', - files: () => [ + const { init: initDynamicImportForToast } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/toast'), import('@css/dynamic/toast/_index.scss'), ], - }); + { + componentName: 'Toast', + }, + ); - const importTooltip = new DynamicImportHandler({ - jqueryPluginCover: 'tooltip', - files: () => [ + initDynamicImportForToast(); + + const { init: initDynamicImportForTooltip } = useBootstrapComponentDynamicImport( + () => [ import('bootstrap/js/src/tooltip'), import('@css/dynamic/tooltip/_index.scss'), ], - }); + { + componentName: 'Tooltip', + events: [ + { + name: 'mouseenter', + selector: '[data-bs-toggle="tooltip"]', + }, + ], + }, + ); + + initDynamicImportForTooltip(); }); diff --git a/_dev/js/theme/utils/DynamicImportDOMEvents.js b/_dev/js/theme/utils/DynamicImportDOMEvents.js deleted file mode 100644 index 5681326a..00000000 --- a/_dev/js/theme/utils/DynamicImportDOMEvents.js +++ /dev/null @@ -1,42 +0,0 @@ -import $ from 'jquery'; - -class DynamicImportDOMEvents { - constructor({ - importer, - events, - eventSelector, - preventDefault, - } = {}) { - this.eventSelector = eventSelector; - this.events = events; - this.eventsArray = events.split(' '); - this.preventDefault = preventDefault; - this.importer = importer; - this.fetchFiles = this.fetchFiles.bind(this); - - this.bindEvents(); - } - - fetchFiles(e = false) { - if (e && this.preventDefault) { - e.preventDefault(); - } - - this.importer.loadFiles(() => { - if (e && this.eventsArray.includes(e.type)) { - $(e.target).trigger(e.type); - this.unbindEvents(); - } - }); - } - - bindEvents() { - $(document).on(this.events, this.eventSelector, this.fetchFiles); - } - - unbindEvents() { - $(document).off(this.events, this.eventSelector, this.fetchFiles); - } -} - -export default DynamicImportDOMEvents; diff --git a/_dev/js/theme/utils/DynamicImportHandler.js b/_dev/js/theme/utils/DynamicImportHandler.js deleted file mode 100644 index 91a9acbd..00000000 --- a/_dev/js/theme/utils/DynamicImportHandler.js +++ /dev/null @@ -1,51 +0,0 @@ -import DynamicImportJqueryPlugin from '@js/theme/utils/DynamicImportJqueryPlugin'; -import DynamicImportDOMEvents from '@js/theme/utils/DynamicImportDOMEvents'; - -export default class DynamicImportHandler { - constructor({ - files, - jqueryPluginCover = null, - enableObserve = false, - observeOptions = false, - DOMEvents = false, - DOMEventsSelector = false, - DOMEventsPreventDefault = false, - onLoadFiles = () => {}, - } = {}) { - this.files = files; - this.jqueryPluginCover = jqueryPluginCover; - this.enableObserve = enableObserve; - this.observeOptions = observeOptions; - this.onLoadFiles = onLoadFiles; - - this.jqueryDynamicImport = false; - this.dynamicDOMEvents = false; - this.filesLoaded = false; - - if (jqueryPluginCover) { - this.jqueryDynamicImport = new DynamicImportJqueryPlugin({ - jqueryPluginCover, - importer: this, - }); - } - if (DOMEvents && DOMEventsSelector) { - this.dynamicDOMEvents = new DynamicImportDOMEvents({ - events: DOMEvents, - eventSelector: DOMEventsSelector, - preventDefault: DOMEventsPreventDefault, - importer: this, - }); - } - } - - loadFiles(callback = () => {}) { - if (!this.filesLoaded) { - Promise.all(this.files()).then((res) => { - callback(); - this.onLoadFiles(res); - }); - - this.filesLoaded = true; - } - } -} diff --git a/_dev/js/theme/utils/DynamicImportJqueryPlugin.js b/_dev/js/theme/utils/DynamicImportJqueryPlugin.js deleted file mode 100644 index 01533c82..00000000 --- a/_dev/js/theme/utils/DynamicImportJqueryPlugin.js +++ /dev/null @@ -1,41 +0,0 @@ -import $ from 'jquery'; - -class DynamicImportJqueryPlugin { - constructor({ - jqueryPluginCover, - importer, - } = {}) { - this.jqueryPluginCover = jqueryPluginCover; - this.importer = importer; - this.jqueryFuncCalled = []; - - this.setJqueryPlugin(); - } - - callJqueryAction() { - for (const fncCall of this.jqueryFuncCalled) { - fncCall.elem[this.jqueryPluginCover](fncCall.args); - } - } - - fetchFiles() { - this.importer.loadFiles(() => this.callJqueryAction()); - } - - setJqueryPlugin() { - const self = this; - - /* eslint-disable func-names */ - $.fn[this.jqueryPluginCover] = function (args) { - self.jqueryFuncCalled.push({ - elem: this, - args, - }); - self.fetchFiles(); - - return this; - }; - } -} - -export default DynamicImportJqueryPlugin; diff --git a/_dev/js/theme/utils/dynamicImports/useBootstrapComponentDynamicImport.js b/_dev/js/theme/utils/dynamicImports/useBootstrapComponentDynamicImport.js new file mode 100644 index 00000000..2cee6e73 --- /dev/null +++ b/_dev/js/theme/utils/dynamicImports/useBootstrapComponentDynamicImport.js @@ -0,0 +1,202 @@ +import useEvent from '../../components/event/useEvent'; + +const { on, off } = useEvent(); + +const useBootstrapComponentDynamicImport = (importFiles, { + componentName = '', + events = [], + onLoad = () => {}, +}) => { + let filesLoaded = false; + const callStack = []; + const jQueryCallStack = []; + + if (!componentName) { + throw new Error('Component name is required'); + } + + const DATA_KEY = 'dynamic_import.' + componentName; + + const getJQueryComponentName = () => componentName.charAt(0).toLowerCase() + componentName.slice(1); + + const isJQueryEnabled = () => { + try { + return !!window.jQuery; + } catch (e) { + return false; + } + } + + const handleComponentLoad = async () => { + if (filesLoaded) { + return; + } + + unbindEvents(); + await loadFiles(); + onLoad(); + executeCallStack(); + }; + + const getOrCreateInstance = (element) => { + const pluginInstance = getComponentInstance(element); + + if (pluginInstance) { + return pluginInstance.instanceProxy; + } + + return new ComponentObjectConstructorFunction(element); + } + + const getComponentInstance = (element) => { + const pluginInstance = callStack.find(({ element: instanceElement }) => instanceElement === element); + + return pluginInstance ? pluginInstance.componentInstance : null; + } + + const proxyFactory = (pluginInstance) => { + const pluginObject = {}; + const proxyHandler = { + get(target, prop, receiver) { + return (...args) => { + if (!filesLoaded) { + pluginInstance.instanceMethodCall.push({ + prop, + args, + }); + + handleComponentLoad(); + } + + if (pluginInstance.componentInstance !== null) { + pluginInstance.componentInstance[prop](...args); + } + + return receiver; + }; + } + } + + return new Proxy(pluginObject, proxyHandler) + } + + const ComponentObjectConstructorFunction = function(args) { + const pluginInstance = { + args, + instanceMethodCall: [], + componentInstance: null, + element: null, + } + + pluginInstance.instanceProxy = proxyFactory(pluginInstance); + + callStack.push(pluginInstance); + + return pluginInstance.instanceProxy; + }; + + ComponentObjectConstructorFunction.getOrCreateInstance = getOrCreateInstance; + ComponentObjectConstructorFunction.getInstance = getComponentInstance; + + const handleJQueryPluginCall = function(args) { + jQueryCallStack.push({ + elem: this, + args, + }); + + handleComponentLoad(); + } + + const executeCallStack = () => { + callStack.forEach(({ args, instanceMethodCall, componentInstance }, i) => { + componentInstance = new window.bootstrap[componentName](args); + + callStack[i].componentInstance = componentInstance; + callStack[i].element = componentInstance._element; + + instanceMethodCall.forEach(({ prop, args }) => { + componentInstance[prop](...args); + }); + }); + + if (isJQueryEnabled()) { + jQueryCallStack.forEach(({ elem, args }) => { + window.jQuery(elem)[getJQueryComponentName()](args); + }); + } + }; + + window.bootstrap = window.bootstrap || {}; + window.bootstrap[componentName] = ComponentObjectConstructorFunction; + + if (isJQueryEnabled()) { + window.jQuery.fn[getJQueryComponentName()] = handleJQueryPluginCall; + } + + const handleEvent = async (e) => { + e.preventDefault(); + await handleComponentLoad(); + + const { currentTarget, type } = e; + + currentTarget.dispatchEvent(new Event(type)); + } + + const loadFiles = () => { + filesLoaded = true; + + return Promise.all(importFiles()).then((files) => { + files.forEach((file) => { + if (file.default) { + window.bootstrap[componentName] = file.default; + } + }); + }); + }; + + const unbindEvents = () => { + events.forEach(({ + name = '', + selector = '', + }) => { + if (!name || !selector) { + throw new Error('Event name and selector are required'); + } + + off( + document, + name, + selector, + handleEvent + ); + }); + }; + + const bindEvents = () => { + events.forEach(({ + name = '', + selector = '', + }) => { + if (!name || !selector) { + throw new Error('Event name and selector are required'); + } + + on( + document, + name, + selector, + handleEvent + ); + }); + }; + + const init = () => { + bindEvents(); + }; + + return { + init, + }; +}; + +export default useBootstrapComponentDynamicImport; diff --git a/_dev/js/theme/vendors/bootstrap/bootstrap-imports.js b/_dev/js/theme/vendors/bootstrap/bootstrap-imports.js index 3735a1d9..55ad91ea 100644 --- a/_dev/js/theme/vendors/bootstrap/bootstrap-imports.js +++ b/_dev/js/theme/vendors/bootstrap/bootstrap-imports.js @@ -1,8 +1,15 @@ -import 'bootstrap/js/dist/alert'; -import 'bootstrap/js/dist/button'; -import 'bootstrap/js/dist/tab'; +import Alert from 'bootstrap/js/src/alert'; +import Button from 'bootstrap/js/src/button'; +import Tab from 'bootstrap/js/src/tab'; +import Scrollspy from 'bootstrap/js/src/scrollspy'; import 'bootstrap/js/dist/util'; +window.bootstrap = window.bootstrap || {}; +window.bootstrap.Alert = Alert; +window.bootstrap.Button = Button; +window.bootstrap.Tab = Tab; +window.bootstrap.Scrollspy = Scrollspy; + // MOVED TO DYNAMIC IMPORTS // import 'bootstrap/js/dist/carousel'; // import 'bootstrap/js/dist/collapse';