Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
'space-before-function-paren': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'no-new': 'off',
"@typescript-eslint/consistent-type-imports": [
'@typescript-eslint/consistent-type-imports': [
"error",
{
disallowTypeAnnotations: false
Expand Down
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,32 @@ Or use it directly through our CDN:
<script src="https://unpkg.com/bootstrap-compatibility-layer@1"></script>
[...]
```
To be sure that the scripts are all operational try to load the compatibility layer before the other scripts!

## Utilisation
### Package
L'utilisation en tant que package est très simple il suffit de le charger sur votre script
```ts
import BSCompatibilityLayer from 'bootstrap-compatibility-layer';
```
The compatibility layer initializes automatically; however, you can call the methods independently if you need to.
```ts
BSCompatibilityLayer.updateAllDataAttributes();
```
### CDN
If you only want to use attribute modifiers, you can place the tag wherever you want in your code. However, if you want to use jQuery commands relating to Bootstrap, it is advisable to put it first in the list of your scripts and to wait for the DOM to be loaded before using them like the following code:
```html
[...]
<link href="https://unpkg.com/bootstrap-compatibility-layer@1/dist/bootstrap-compatibility-layer.min.css" rel="stylesheet">
[...]
<script src="https://unpkg.com/bootstrap-compatibility-layer@1"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('[data-toggle="popover"]').popover()

$('[data-toggle="tooltip"]').tooltip()
});
</script>
```

## Contributing
Contributions are welcome!
Expand All @@ -27,4 +52,4 @@ Contributions are welcome!
PrestaShop is a free and Open Source e-commerce web application written in PHP, committed to providing the best shopping cart experience for both merchants and customers. [Learn more about PrestaShop](https://www.prestashop-project.org/)

## License
This Bootstrap compatibility layer is released under the [MIT License](LICENSE).
This Bootstrap compatibility layer is released under the [MIT License](LICENSE).
6 changes: 4 additions & 2 deletions example/bootstrap-4.html
Original file line number Diff line number Diff line change
Expand Up @@ -946,9 +946,11 @@ <h3 class="code">.sr-only-focusable</h3>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"></script>
<script>
$('[data-toggle="popover"]').popover()
document.addEventListener('DOMContentLoaded', function() {
$('[data-toggle="popover"]').popover()

$('[data-toggle="tooltip"]').tooltip()
$('[data-toggle="tooltip"]').tooltip()
});

// Table of content
const headings = document.querySelectorAll('h1.title, h2.title');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bootstrap-compatibility-layer",
"version": "1.0.2",
"version": "1.1.0",
"description": "Bootstrap compatibility layer to help using a module based on bootstrap 4 with a theme based on bootstrap 5",
"keywords": [
"prestashop",
Expand Down
100 changes: 74 additions & 26 deletions src/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,97 @@ import '../styles/index.scss';
import { type Popover, type Tooltip } from 'bootstrap';

// The BSCompatibilityLayer class is responsible for updating the data attributes of HTML elements to be compatible with Bootstrap 5. It also initializes the popover and tooltip components of Bootstrap 5.
class BSCompatibilityLayer {
readonly dataToUpdate: Record<string, string>;
export class BSCompatibilityLayer {
readonly dataToUpdate: Map<string, string>;

constructor() {
// a readonly object that maps the old data attributes to the new data attributes in Bootstrap 5
this.dataToUpdate = {
'data-autohide': 'data-bs-autohide',
'data-content': 'data-bs-content',
'data-dismiss': 'data-bs-dismiss',
'data-html': 'data-bs-html',
'data-offset': 'data-bs-offset',
'data-parent': 'data-bs-parent',
'data-placement': 'data-bs-placement',
'data-reference': 'data-bs-reference',
'data-ride': 'data-bs-ride',
'data-slide': 'data-bs-slide',
'data-slide-to': 'data-bs-slide-to',
'data-spy': 'data-bs-spy',
'data-target': 'data-bs-target',
'data-toggle': 'data-bs-toggle',
'data-trigger': 'data-bs-trigger'
};
this.dataToUpdate = new Map([
['data-autohide', 'data-bs-autohide'],
['data-content', 'data-bs-content'],
['data-dismiss', 'data-bs-dismiss'],
['data-html', 'data-bs-html'],
['data-offset', 'data-bs-offset'],
['data-parent', 'data-bs-parent'],
['data-placement', 'data-bs-placement'],
['data-reference', 'data-bs-reference'],
['data-ride', 'data-bs-ride'],
['data-slide', 'data-bs-slide'],
['data-slide-to', 'data-bs-slide-to'],
['data-spy', 'data-bs-spy'],
['data-target', 'data-bs-target'],
['data-toggle', 'data-bs-toggle'],
['data-trigger', 'data-bs-trigger']
]);
this.init();
}

init(): void {
this.updateDataAttributes();
this.updateAllDataAttributes();
this.attachObserver();

this.waitingForJQuery(() => {
this.attachJQueryMethods();
});
}

public updateDataAttributes(): void {
for (const [key, value] of Object.entries(this.dataToUpdate)) {
Array.from(document.querySelectorAll(`[${key}]`)).forEach((el) => {
const dataValue = el.getAttribute(key);
if (dataValue !== null && dataValue !== '') {
el.setAttribute(value, dataValue);
// Updates all data attributes in the HTML document that match the keys in the `dataToUpdate` map.
public updateAllDataAttributes(): void {
const elements = document.querySelectorAll(`[${Array.from(this.dataToUpdate.keys()).join('], [')}]`);
elements.forEach((el) => {
Array.from(this.dataToUpdate.keys()).forEach((key) => {
if (el.hasAttribute(key)) {
this.updateDataAttributes(el, key);
}
});
});
}

// Updates a specific data attribute of an HTML element.
public updateDataAttributes(el: Element, dataAttribute: string): void {
const dataValue = el.getAttribute(dataAttribute);
if (dataValue !== null && dataValue !== '' && this.dataToUpdate.has(dataAttribute)) {
const newDataAttribute = this.dataToUpdate.get(dataAttribute);
if (newDataAttribute !== undefined) {
el.setAttribute(newDataAttribute, dataValue);
}
}
}

// Attaches a mutation observer to the HTML document to detect changes in data attributes.
public attachObserver(): void {
if ('MutationObserver' in window) {
const observer = new MutationObserver(this.observerCallback.bind(this));
const config = { attributes: true, childList: true, subtree: true };
const targetNode = document.documentElement;

observer.observe(targetNode, config);
}
}

// Callback function for the mutation observer, updates data attributes of added or modified elements.
public observerCallback(mutationsList: MutationRecord[], observer: MutationObserver): void {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node instanceof Element) {
for (const key of this.dataToUpdate.keys()) {
if (node.hasAttribute(key)) {
this.updateDataAttributes(node, key);
}
}
}
});
} else if (mutation.type === 'attributes' && mutation.attributeName !== null && this.dataToUpdate.has(mutation.attributeName)) {
const targetElement = mutation.target;
if (targetElement instanceof Element) {
this.updateDataAttributes(targetElement, mutation.attributeName);
}
}
}
}

// Waits for jQuery to be loaded before calling a callback function.
public waitingForJQuery(callback: () => void): void {
const checkJQueryLoaded = (): void => {
if (typeof $ !== 'undefined') {
Expand All @@ -57,6 +104,7 @@ class BSCompatibilityLayer {
requestAnimationFrame(checkJQueryLoaded);
}

// Extends the jQuery object with BS methods.
public attachJQueryMethods(): void {
$.fn.extend({
popover: function(params: Partial<Popover.Options> | undefined) {
Expand Down
163 changes: 136 additions & 27 deletions tests/unit/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,145 @@
import BSCompatibilityLayer from '../../src/js/index';

describe('test for test', () => {
test('test data attributes updated correctly', () => {
const el = document.createElement('div');
el.setAttribute('data-autohide', 'true');
el.setAttribute('data-content', 'content');
el.setAttribute('data-toggle', 'popover');
document.body.appendChild(el);
BSCompatibilityLayer.updateDataAttributes();
expect(el.getAttribute('data-autohide')).toBe('true');
expect(el.getAttribute('data-content')).toBe('content');
expect(el.getAttribute('data-toggle')).toBe('popover');
expect(el.getAttribute('data-bs-autohide')).toBe('true');
expect(el.getAttribute('data-bs-content')).toBe('content');
expect(el.getAttribute('data-bs-toggle')).toBe('popover');
describe('BSCompatibilityLayer', () => {
// BSCompatibilityLayer updates all data attributes in the HTML document that match the keys in the `dataToUpdate` map.
it('should update all data attributes in the HTML document that match the keys in the `dataToUpdate` map', () => {
// Arrange
const mockElement = document.createElement('div');
const mockDataAttribute = 'data-autohide';
const mockDataValue = 'true';
mockElement.setAttribute(mockDataAttribute, mockDataValue);
document.body.appendChild(mockElement);

// Act
BSCompatibilityLayer.updateAllDataAttributes();

// Assert
expect(mockElement.hasAttribute('data-bs-autohide')).toBe(true);
expect(mockElement.getAttribute('data-bs-autohide')).toBe(mockDataValue);
});

// BSCompatibilityLayer attaches a mutation observer to the HTML document to detect changes in data attributes.
it('should attach a mutation observer to the HTML document to detect changes in data attributes', () => {
// Arrange
const mockObserver = jest.fn();
const mockMutationObserver = jest.fn(() => ({
observe: jest.fn()
}));
/**
* Too hard to type mutation observer and unecessary for this test
*
* @ts-expect-error */
window.MutationObserver = mockMutationObserver;

// Act
BSCompatibilityLayer.attachObserver();

// Assert
expect(mockMutationObserver).toHaveBeenCalledWith(expect.any(Function));
});

// BSCompatibilityLayer extends the jQuery object with BS methods.
it('should extend the jQuery object with BS methods', () => {
// Arrange
const mockExtend = jest.fn();
const mockJQuery = {
fn: {
extend: mockExtend
}
};
/**
* The global jQuery $ is not added loaded into jest
*
* @ts-expect-error */
global.$ = mockJQuery;

// Act
BSCompatibilityLayer.attachJQueryMethods();

// Assert
expect(mockExtend).toHaveBeenCalledWith(expect.any(Object));
});

// BSCompatibilityLayer updates a specific data attribute of an HTML element.
it('should update a specific data attribute of an HTML element', () => {
// Arrange
const mockElement = document.createElement('div');
const mockDataAttribute = 'data-autohide';
const mockDataValue = 'true';
mockElement.setAttribute(mockDataAttribute, mockDataValue);

// Act
BSCompatibilityLayer.updateDataAttributes(mockElement, mockDataAttribute);

// Assert
expect(mockElement.hasAttribute('data-bs-autohide')).toBe(true);
expect(mockElement.getAttribute('data-bs-autohide')).toBe(mockDataValue);
});

test('test data attributes not updated if not in data to update object', () => {
const el = document.createElement('div');
el.setAttribute('data-foo', 'bar');
BSCompatibilityLayer.updateDataAttributes();
expect(el.hasAttribute('data-foo')).toBe(true);
expect(el.hasAttribute('data-bs-foo')).toBe(false);
// BSCompatibilityLayer updates data attributes of added or modified elements.
it('should update data attributes of added or modified elements', () => {
// Arrange
const mockElement = document.createElement('div');
const mockDataAttribute = 'data-autohide';
const mockDataValue = 'true';
mockElement.setAttribute(mockDataAttribute, mockDataValue);

// Act
/**
* Can't recreate the needed type with mock
*
* @ts-expect-error */
BSCompatibilityLayer.observerCallback([{ type: 'childList', addedNodes: [mockElement] }], new MutationObserver(() => {}));

// Assert
expect(mockElement.hasAttribute('data-bs-autohide')).toBe(true);
expect(mockElement.getAttribute('data-bs-autohide')).toBe(mockDataValue);

const mockModifiedAttribute = 'data-content';
const mockModifiedValue = 'hello world';

mockElement.setAttribute(mockModifiedAttribute, mockModifiedValue);

/**
* Can't recreate the needed type with moke
*
* @ts-expect-error */
BSCompatibilityLayer.observerCallback([{ type: 'attributes', attributeName: mockModifiedAttribute, target: mockElement }], new MutationObserver(() => {}));

expect(mockElement.hasAttribute('data-bs-content')).toBe(true);
expect(mockElement.getAttribute('data-bs-content')).toBe(mockModifiedValue);
});

test('test data attributes not updated if value is undefined', () => {
const el = document.createElement('div');
el.setAttribute('data-autohide', '');
document.body.appendChild(el);
BSCompatibilityLayer.updateDataAttributes();
expect(el.hasAttribute('data-autohide')).toBe(true);
expect(el.hasAttribute('data-bs-autohide')).toBe(false);
// BSCompatibilityLayer does not update data attributes that are not in the `dataToUpdate` map.
it('should not update data attributes that are not in the `dataToUpdate` map', () => {
// Arrange
const mockElement = document.createElement('div');
const mockDataAttribute = 'data-unknown';
const mockDataValue = 'unknown';
mockElement.setAttribute(mockDataAttribute, mockDataValue);

// Act
BSCompatibilityLayer.updateDataAttributes(mockElement, mockDataAttribute);

// Assert
expect(mockElement.hasAttribute(mockDataAttribute)).toBe(true);
expect(mockElement.hasAttribute('data-bs-unknown')).toBe(false);
});

// BSCompatibilityLayer does not update data attributes with empty or null values.
it('should not update data attributes with empty or null values', () => {
// Arrange
const mockElement = document.createElement('div');
const mockDataAttribute = 'data-autohide';
const mockDataValue = '';
mockElement.setAttribute(mockDataAttribute, mockDataValue);

// Act
BSCompatibilityLayer.updateDataAttributes(mockElement, mockDataAttribute);

// Assert
expect(mockElement.hasAttribute(mockDataAttribute)).toBe(true);
expect(mockElement.hasAttribute('data-bs-autohide')).toBe(false);
});
});

Expand Down