]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
refactor: use a Map instead of an Object in dom/data (#32180)
authoralpadev <2838324+alpadev@users.noreply.github.com>
Tue, 2 Mar 2021 14:55:44 +0000 (15:55 +0100)
committerGitHub <noreply@github.com>
Tue, 2 Mar 2021 14:55:44 +0000 (16:55 +0200)
Co-authored-by: XhmikosR <xhmikosr@gmail.com>
Co-authored-by: Rohit Sharma <rohit2sharma95@gmail.com>
15 files changed:
.bundlewatch.config.json
js/src/alert.js
js/src/base-component.js
js/src/button.js
js/src/carousel.js
js/src/collapse.js
js/src/dom/data.js
js/src/dropdown.js
js/src/modal.js
js/src/popover.js
js/src/scrollspy.js
js/src/tab.js
js/src/toast.js
js/src/tooltip.js
js/tests/unit/dom/data.spec.js

index bd9ce671d45e57bd9ccbdfbb57e1e42d78a77014..a68eeb85be487b86e10a7a8bb44ff9d0aaf0143d 100644 (file)
@@ -38,7 +38,7 @@
     },
     {
       "path": "./dist/js/bootstrap.bundle.min.js",
-      "maxSize": "21.75 kB"
+      "maxSize": "22 kB"
     },
     {
       "path": "./dist/js/bootstrap.esm.js",
index 8fc3f12a8d04ae65733fbaf871775454f6d21eab..d10e6c8da3a3f9009402a1dbdf20ee10205edf3c 100644 (file)
@@ -98,7 +98,7 @@ class Alert extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
 
       if (!data) {
         data = new Alert(this)
index 989a6415619557be73cbfeb6aa3978a074b12f38..2a9e29c2aa71371f3daeece26e95fd136eb00dcf 100644 (file)
@@ -24,18 +24,18 @@ class BaseComponent {
     }
 
     this._element = element
-    Data.setData(this._element, this.constructor.DATA_KEY, this)
+    Data.set(this._element, this.constructor.DATA_KEY, this)
   }
 
   dispose() {
-    Data.removeData(this._element, this.constructor.DATA_KEY)
+    Data.remove(this._element, this.constructor.DATA_KEY)
     this._element = null
   }
 
   /** Static */
 
   static getInstance(element) {
-    return Data.getData(element, this.DATA_KEY)
+    return Data.get(element, this.DATA_KEY)
   }
 
   static get VERSION() {
index 4ec48ca0830fd5936dbdebf1614cfbd758f330cc..7a9449f07510765b09c2b3597562811426c54d9d 100644 (file)
@@ -51,7 +51,7 @@ class Button extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
 
       if (!data) {
         data = new Button(this)
@@ -75,7 +75,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, event => {
 
   const button = event.target.closest(SELECTOR_DATA_TOGGLE)
 
-  let data = Data.getData(button, DATA_KEY)
+  let data = Data.get(button, DATA_KEY)
   if (!data) {
     data = new Button(button)
   }
index 75f8a4da79838fab9fbdccf9fe693106d0ecb865..a825aaef48123065a11615cea0b092715e527747 100644 (file)
@@ -527,7 +527,7 @@ class Carousel extends BaseComponent {
   // Static
 
   static carouselInterface(element, config) {
-    let data = Data.getData(element, DATA_KEY)
+    let data = Data.get(element, DATA_KEY)
     let _config = {
       ...Default,
       ...Manipulator.getDataAttributes(element)
@@ -586,7 +586,7 @@ class Carousel extends BaseComponent {
     Carousel.carouselInterface(target, config)
 
     if (slideIndex) {
-      Data.getData(target, DATA_KEY).to(slideIndex)
+      Data.get(target, DATA_KEY).to(slideIndex)
     }
 
     event.preventDefault()
@@ -605,7 +605,7 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
   const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)
 
   for (let i = 0, len = carousels.length; i < len; i++) {
-    Carousel.carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY))
+    Carousel.carouselInterface(carousels[i], Data.get(carousels[i], DATA_KEY))
   }
 })
 
index f86166765924015a007b532591cda6bf19092355..036ffcf248415da1e51a9684006a2dd426b541b2 100644 (file)
@@ -147,7 +147,7 @@ class Collapse extends BaseComponent {
     const container = SelectorEngine.findOne(this._selector)
     if (actives) {
       const tempActiveData = actives.find(elem => container !== elem)
-      activesData = tempActiveData ? Data.getData(tempActiveData, DATA_KEY) : null
+      activesData = tempActiveData ? Data.get(tempActiveData, DATA_KEY) : null
 
       if (activesData && activesData._isTransitioning) {
         return
@@ -166,7 +166,7 @@ class Collapse extends BaseComponent {
         }
 
         if (!activesData) {
-          Data.setData(elemActive, DATA_KEY, null)
+          Data.set(elemActive, DATA_KEY, null)
         }
       })
     }
@@ -332,7 +332,7 @@ class Collapse extends BaseComponent {
   // Static
 
   static collapseInterface(element, config) {
-    let data = Data.getData(element, DATA_KEY)
+    let data = Data.get(element, DATA_KEY)
     const _config = {
       ...Default,
       ...Manipulator.getDataAttributes(element),
@@ -380,7 +380,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
   const selectorElements = SelectorEngine.find(selector)
 
   selectorElements.forEach(element => {
-    const data = Data.getData(element, DATA_KEY)
+    const data = Data.get(element, DATA_KEY)
     let config
     if (data) {
       // update parent attribute
index c93a8dc7c5efb7779a9003dec9bebdfd73040275..1d283d68b8a233ee2d8e0a393a80bbde2f9752cd 100644 (file)
  * ------------------------------------------------------------------------
  */
 
-const mapData = (() => {
-  const storeData = {}
-  let id = 1
-  return {
-    set(element, key, data) {
-      if (typeof element.bsKey === 'undefined') {
-        element.bsKey = {
-          key,
-          id
-        }
-        id++
-      }
+const elementMap = new Map()
 
-      storeData[element.bsKey.id] = data
-    },
-    get(element, key) {
-      if (!element || typeof element.bsKey === 'undefined') {
-        return null
-      }
-
-      const keyProperties = element.bsKey
-      if (keyProperties.key === key) {
-        return storeData[keyProperties.id]
-      }
+export default {
+  set(element, key, instance) {
+    if (!elementMap.has(element)) {
+      elementMap.set(element, new Map())
+    }
 
-      return null
-    },
-    delete(element, key) {
-      if (typeof element.bsKey === 'undefined') {
-        return
-      }
+    const instanceMap = elementMap.get(element)
 
-      const keyProperties = element.bsKey
-      if (keyProperties.key === key) {
-        delete storeData[keyProperties.id]
-        delete element.bsKey
-      }
+    // make it clear we only want one instance per element
+    // can be removed later when multiple key/instances are fine to be used
+    if (!instanceMap.has(key) && instanceMap.size !== 0) {
+      // eslint-disable-next-line no-console
+      console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`)
+      return
     }
-  }
-})()
 
-const Data = {
-  setData(instance, key, data) {
-    mapData.set(instance, key, data)
+    instanceMap.set(key, instance)
   },
-  getData(instance, key) {
-    return mapData.get(instance, key)
+
+  get(element, key) {
+    if (elementMap.has(element)) {
+      return elementMap.get(element).get(key) || null
+    }
+
+    return null
   },
-  removeData(instance, key) {
-    mapData.delete(instance, key)
+
+  remove(element, key) {
+    if (!elementMap.has(element)) {
+      return
+    }
+
+    const instanceMap = elementMap.get(element)
+
+    instanceMap.delete(key)
+
+    // free up element references if there are no instances left for an element
+    if (instanceMap.size === 0) {
+      elementMap.delete(element)
+    }
   }
 }
-
-export default Data
index 590c748012c9d134a3be426510e7d785f25132ac..fea0b1919b4e8aba4d94f8811e482206e664f22f 100644 (file)
@@ -357,7 +357,7 @@ class Dropdown extends BaseComponent {
   // Static
 
   static dropdownInterface(element, config) {
-    let data = Data.getData(element, DATA_KEY)
+    let data = Data.get(element, DATA_KEY)
     const _config = typeof config === 'object' ? config : null
 
     if (!data) {
@@ -387,7 +387,7 @@ class Dropdown extends BaseComponent {
     const toggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE)
 
     for (let i = 0, len = toggles.length; i < len; i++) {
-      const context = Data.getData(toggles[i], DATA_KEY)
+      const context = Data.get(toggles[i], DATA_KEY)
       const relatedTarget = {
         relatedTarget: toggles[i]
       }
index 5afb9791b4752ad6290aff8c02f9afe3bd794353..332d636d0f0589a23b2d093c91c3f9b0107ab2cd 100644 (file)
@@ -508,7 +508,7 @@ class Modal extends BaseComponent {
 
   static jQueryInterface(config, relatedTarget) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
       const _config = {
         ...Default,
         ...Manipulator.getDataAttributes(this),
@@ -556,7 +556,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
     })
   })
 
-  let data = Data.getData(target, DATA_KEY)
+  let data = Data.get(target, DATA_KEY)
   if (!data) {
     const config = {
       ...Manipulator.getDataAttributes(target),
index 0677dafa09450e251292ae4a208a7cf523469887..55354475477fcf89cb5ed7ea2e78b75cd30b6406 100644 (file)
@@ -136,7 +136,7 @@ class Popover extends Tooltip {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
       const _config = typeof config === 'object' ? config : null
 
       if (!data && /dispose|hide/.test(config)) {
@@ -145,7 +145,7 @@ class Popover extends Tooltip {
 
       if (!data) {
         data = new Popover(this, _config)
-        Data.setData(this, DATA_KEY, data)
+        Data.set(this, DATA_KEY, data)
       }
 
       if (typeof config === 'string') {
index 0c51eab0fef6862fd61723d2cbb2f67c4ac80dd7..c7472439be3ec5682dc0f54fb6f9e359efc364a9 100644 (file)
@@ -278,7 +278,7 @@ class ScrollSpy extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
       const _config = typeof config === 'object' && config
 
       if (!data) {
index e60ecddb56b1c393de878bf50f199e5d3a41123d..95968f4f8ba85f0cee09307e7a1151ecbe556254 100644 (file)
@@ -182,7 +182,7 @@ class Tab extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      const data = Data.getData(this, DATA_KEY) || new Tab(this)
+      const data = Data.get(this, DATA_KEY) || new Tab(this)
 
       if (typeof config === 'string') {
         if (typeof data[config] === 'undefined') {
@@ -204,7 +204,7 @@ class Tab extends BaseComponent {
 EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
   event.preventDefault()
 
-  const data = Data.getData(this, DATA_KEY) || new Tab(this)
+  const data = Data.get(this, DATA_KEY) || new Tab(this)
   data.show()
 })
 
index 2f451aab76c9af2cd6f424db5234f253460d8146..ea91163d8470f0efa7a09204073df2ed9aaabebc 100644 (file)
@@ -189,7 +189,7 @@ class Toast extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
       const _config = typeof config === 'object' && config
 
       if (!data) {
index d35b5e0ab102cd04e9f461435a8e8d2abbbc8da1..6f33245f8ee0afbf26f8a5b5e82b424f901226a1 100644 (file)
@@ -275,7 +275,7 @@ class Tooltip extends BaseComponent {
     this._addAttachmentClass(attachment)
 
     const container = this._getContainer()
-    Data.setData(tip, this.constructor.DATA_KEY, this)
+    Data.set(tip, this.constructor.DATA_KEY, this)
 
     if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
       container.appendChild(tip)
@@ -465,11 +465,11 @@ class Tooltip extends BaseComponent {
 
   _initializeOnDelegatedTarget(event, context) {
     const dataKey = this.constructor.DATA_KEY
-    context = context || Data.getData(event.delegateTarget, dataKey)
+    context = context || Data.get(event.delegateTarget, dataKey)
 
     if (!context) {
       context = new this.constructor(event.delegateTarget, this._getDelegateConfig())
-      Data.setData(event.delegateTarget, dataKey, context)
+      Data.set(event.delegateTarget, dataKey, context)
     }
 
     return context
@@ -761,7 +761,7 @@ class Tooltip extends BaseComponent {
 
   static jQueryInterface(config) {
     return this.each(function () {
-      let data = Data.getData(this, DATA_KEY)
+      let data = Data.get(this, DATA_KEY)
       const _config = typeof config === 'object' && config
 
       if (!data && /dispose|hide/.test(config)) {
index c80f32db0f5b2574988e8a4476a90d99eb478147..a00d3b7341e9ebb1abb1dc15e01677bdaf443854 100644 (file)
@@ -4,128 +4,103 @@ import Data from '../../../src/dom/data'
 import { getFixture, clearFixture } from '../../helpers/fixture'
 
 describe('Data', () => {
+  const TEST_KEY = 'bs.test'
+  const UNKNOWN_KEY = 'bs.unknown'
+  const TEST_DATA = {
+    test: 'bsData'
+  }
+
   let fixtureEl
+  let div
 
   beforeAll(() => {
     fixtureEl = getFixture()
   })
 
+  beforeEach(() => {
+    fixtureEl.innerHTML = '<div></div>'
+    div = fixtureEl.querySelector('div')
+  })
+
   afterEach(() => {
+    Data.remove(div, TEST_KEY)
     clearFixture()
   })
 
-  describe('setData', () => {
-    it('should set data in an element by adding a bsKey attribute', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
-
-      Data.setData(div, 'test', data)
-      expect(div.bsKey).toBeDefined()
-    })
+  it('should return null for unknown elements', () => {
+    const data = { ...TEST_DATA }
 
-    it('should change data if something is already stored', () => {
-      fixtureEl.innerHTML = '<div></div>'
+    Data.set(div, TEST_KEY, data)
 
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
-
-      Data.setData(div, 'test', data)
-
-      data.test = 'bsData2'
-      Data.setData(div, 'test', data)
-
-      expect(div.bsKey).toBeDefined()
-    })
+    expect(Data.get(null)).toBeNull()
+    expect(Data.get(undefined)).toBeNull()
+    expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
   })
 
-  describe('getData', () => {
-    it('should return stored data', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
+  it('should return null for unknown keys', () => {
+    const data = { ...TEST_DATA }
 
-      Data.setData(div, 'test', data)
-      expect(Data.getData(div, 'test')).toEqual(data)
-    })
+    Data.set(div, TEST_KEY, data)
 
-    it('should return null on undefined element', () => {
-      expect(Data.getData(null)).toEqual(null)
-      expect(Data.getData(undefined)).toEqual(null)
-    })
-
-    it('should return null when an element have nothing stored', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-
-      expect(Data.getData(div, 'test')).toEqual(null)
-    })
-
-    it('should return null when an element have nothing stored with the provided key', () => {
-      fixtureEl.innerHTML = '<div></div>'
+    expect(Data.get(div, null)).toBeNull()
+    expect(Data.get(div, undefined)).toBeNull()
+    expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
+  })
 
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
+  it('should store data for an element with a given key and return it', () => {
+    const data = { ...TEST_DATA }
 
-      Data.setData(div, 'test', data)
+    Data.set(div, TEST_KEY, data)
 
-      expect(Data.getData(div, 'test2')).toEqual(null)
-    })
+    expect(Data.get(div, TEST_KEY)).toBe(data)
   })
 
-  describe('removeData', () => {
-    it('should do nothing when an element have nothing stored', () => {
-      fixtureEl.innerHTML = '<div></div>'
+  it('should overwrite data if something is already stored', () => {
+    const data = { ...TEST_DATA }
+    const copy = { ...data }
 
-      const div = fixtureEl.querySelector('div')
+    Data.set(div, TEST_KEY, data)
+    Data.set(div, TEST_KEY, copy)
 
-      Data.removeData(div, 'test')
-      expect().nothing()
-    })
+    expect(Data.get(div, TEST_KEY)).not.toBe(data)
+    expect(Data.get(div, TEST_KEY)).toBe(copy)
+  })
 
-    it('should should do nothing if it\'s not a valid key provided', () => {
-      fixtureEl.innerHTML = '<div></div>'
+  it('should do nothing when an element have nothing stored', () => {
+    Data.remove(div, TEST_KEY)
 
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
+    expect().nothing()
+  })
 
-      Data.setData(div, 'test', data)
+  it('should remove nothing for an unknown key', () => {
+    const data = { ...TEST_DATA }
 
-      expect(div.bsKey).toBeDefined()
+    Data.set(div, TEST_KEY, data)
+    Data.remove(div, UNKNOWN_KEY)
 
-      Data.removeData(div, 'test2')
+    expect(Data.get(div, TEST_KEY)).toBe(data)
+  })
 
-      expect(div.bsKey).toBeDefined()
-    })
+  it('should remove data for a given key', () => {
+    const data = { ...TEST_DATA }
 
-    it('should remove data if something is stored', () => {
-      fixtureEl.innerHTML = '<div></div>'
+    Data.set(div, TEST_KEY, data)
+    Data.remove(div, TEST_KEY)
 
-      const div = fixtureEl.querySelector('div')
-      const data = {
-        test: 'bsData'
-      }
+    expect(Data.get(div, TEST_KEY)).toBeNull()
+  })
 
-      Data.setData(div, 'test', data)
+  it('should console.error a message if called with multiple keys', () => {
+    /* eslint-disable no-console */
+    console.error = jasmine.createSpy('console.error')
 
-      expect(div.bsKey).toBeDefined()
+    const data = { ...TEST_DATA }
+    const copy = { ...data }
 
-      Data.removeData(div, 'test')
+    Data.set(div, TEST_KEY, data)
+    Data.set(div, UNKNOWN_KEY, copy)
 
-      expect(div.bsKey).toBeUndefined()
-    })
+    expect(console.error).toHaveBeenCalled()
+    expect(Data.get(div, UNKNOWN_KEY)).toBe(null)
   })
 })