slim-select 3.3.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +205 -193
- package/dist/react/index.umd.js +2 -2
- package/dist/slimselect.cjs.js +1 -1
- package/dist/slimselect.cjs.js.map +1 -1
- package/dist/slimselect.es.js +201 -189
- package/dist/slimselect.es.js.map +1 -1
- package/dist/slimselect.iife.js +1 -1
- package/dist/slimselect.iife.js.map +1 -1
- package/dist/slimselect.js +1 -1
- package/dist/slimselect.js.map +1 -1
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +51 -39
- package/dist/vue/index.umd.js +1 -1
- package/package.json +23 -23
- package/sitemap.js +21 -2
- package/src/slim-select/render.test.ts +216 -0
- package/src/slim-select/render.ts +63 -8
- package/src/slim-select/settings.test.ts +2 -1
- package/src/slim-select/settings.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slim-select",
|
|
3
3
|
"description": "Slim advanced select dropdown",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Brian Voelker <brian@webiswhatido.com> (http://webiswhatido.com)",
|
|
7
7
|
"homepage": "https://slimselectjs.com",
|
|
@@ -68,43 +68,43 @@
|
|
|
68
68
|
"release": "node release.js"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@testing-library/react": "^16.3.
|
|
71
|
+
"@testing-library/react": "^16.3.2",
|
|
72
72
|
"@testing-library/user-event": "^14.6.1",
|
|
73
73
|
"@types/downloadjs": "^1.4.6",
|
|
74
|
-
"@types/node": "^25.
|
|
75
|
-
"@types/react": "^19.2.
|
|
74
|
+
"@types/node": "^25.2.1",
|
|
75
|
+
"@types/react": "^19.2.13",
|
|
76
76
|
"@types/react-dom": "^19.2.3",
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
78
|
-
"@typescript-eslint/parser": "^8.
|
|
79
|
-
"@vitejs/plugin-react": "^5.1.
|
|
80
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
81
|
-
"@vitest/coverage-v8": "^4.0.
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
78
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
79
|
+
"@vitejs/plugin-react": "^5.1.3",
|
|
80
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
81
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
82
82
|
"@vue/test-utils": "^2.4.6",
|
|
83
|
-
"axe-core": "^4.11.
|
|
83
|
+
"axe-core": "^4.11.1",
|
|
84
84
|
"clipboard": "^2.0.11",
|
|
85
85
|
"downloadjs": "^1.4.7",
|
|
86
86
|
"eslint": "^9.39.1",
|
|
87
87
|
"eslint-config-prettier": "^10.1.8",
|
|
88
|
-
"eslint-plugin-prettier": "^5.5.
|
|
88
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
89
89
|
"highlight.js": "^11.0.0",
|
|
90
90
|
"identity-obj-proxy": "^3.0.0",
|
|
91
|
-
"jsdom": "^
|
|
91
|
+
"jsdom": "^28.0.0",
|
|
92
92
|
"pinia": "^3.0.4",
|
|
93
|
-
"prettier": "^3.
|
|
94
|
-
"react": "^19.2.
|
|
95
|
-
"react-dom": "^19.2.
|
|
93
|
+
"prettier": "^3.8.1",
|
|
94
|
+
"react": "^19.2.4",
|
|
95
|
+
"react-dom": "^19.2.4",
|
|
96
96
|
"rimraf": "^6.1.2",
|
|
97
|
-
"sass": "^1.
|
|
98
|
-
"shiki": "^3.
|
|
99
|
-
"terser": "^5.
|
|
97
|
+
"sass": "^1.97.3",
|
|
98
|
+
"shiki": "^3.22.0",
|
|
99
|
+
"terser": "^5.46.0",
|
|
100
100
|
"tslib": "^2.8.1",
|
|
101
101
|
"typescript": "^5.9.3",
|
|
102
|
-
"vite": "^7.
|
|
102
|
+
"vite": "^7.3.1",
|
|
103
103
|
"vite-plugin-dts": "^4.5.4",
|
|
104
|
-
"vitest": "^4.0.
|
|
105
|
-
"vue": "^3.5.
|
|
106
|
-
"vue-router": "^
|
|
107
|
-
"vue-tsc": "^3.
|
|
104
|
+
"vitest": "^4.0.18",
|
|
105
|
+
"vue": "^3.5.27",
|
|
106
|
+
"vue-router": "^5.0.2",
|
|
107
|
+
"vue-tsc": "^3.2.4",
|
|
108
108
|
"vuex": "^4.0.2"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
package/sitemap.js
CHANGED
|
@@ -8,6 +8,7 @@ const __filename = fileURLToPath(import.meta.url)
|
|
|
8
8
|
const __dirname = path.dirname(__filename)
|
|
9
9
|
|
|
10
10
|
// Define all routes and their metadata
|
|
11
|
+
// Based on routes defined in src/docs/router.ts
|
|
11
12
|
const routes = [
|
|
12
13
|
{
|
|
13
14
|
loc: 'https://slimselectjs.com/',
|
|
@@ -33,12 +34,30 @@ const routes = [
|
|
|
33
34
|
changefreq: 'monthly',
|
|
34
35
|
priority: '0.8'
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
loc: 'https://slimselectjs.com/examples',
|
|
39
|
+
lastmod: new Date().toISOString().split('T')[0],
|
|
40
|
+
changefreq: 'monthly',
|
|
41
|
+
priority: '0.8'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
loc: 'https://slimselectjs.com/privacy',
|
|
45
|
+
lastmod: new Date().toISOString().split('T')[0],
|
|
46
|
+
changefreq: 'yearly',
|
|
47
|
+
priority: '0.3'
|
|
48
|
+
},
|
|
36
49
|
{
|
|
37
50
|
loc: 'https://slimselectjs.com/settings',
|
|
38
51
|
lastmod: new Date().toISOString().split('T')[0],
|
|
39
52
|
changefreq: 'monthly',
|
|
40
53
|
priority: '0.8'
|
|
41
54
|
},
|
|
55
|
+
{
|
|
56
|
+
loc: 'https://slimselectjs.com/style',
|
|
57
|
+
lastmod: new Date().toISOString().split('T')[0],
|
|
58
|
+
changefreq: 'monthly',
|
|
59
|
+
priority: '0.8'
|
|
60
|
+
},
|
|
42
61
|
{
|
|
43
62
|
loc: 'https://slimselectjs.com/events',
|
|
44
63
|
lastmod: new Date().toISOString().split('T')[0],
|
|
@@ -52,13 +71,13 @@ const routes = [
|
|
|
52
71
|
priority: '0.7'
|
|
53
72
|
},
|
|
54
73
|
{
|
|
55
|
-
loc: 'https://slimselectjs.com/
|
|
74
|
+
loc: 'https://slimselectjs.com/react',
|
|
56
75
|
lastmod: new Date().toISOString().split('T')[0],
|
|
57
76
|
changefreq: 'monthly',
|
|
58
77
|
priority: '0.8'
|
|
59
78
|
},
|
|
60
79
|
{
|
|
61
|
-
loc: 'https://slimselectjs.com/
|
|
80
|
+
loc: 'https://slimselectjs.com/vue',
|
|
62
81
|
lastmod: new Date().toISOString().split('T')[0],
|
|
63
82
|
changefreq: 'monthly',
|
|
64
83
|
priority: '0.8'
|
|
@@ -1658,4 +1658,220 @@ describe('render module', () => {
|
|
|
1658
1658
|
expect(render.content.main.classList.contains(render.classes.dirBelow)).toBe(true)
|
|
1659
1659
|
})
|
|
1660
1660
|
})
|
|
1661
|
+
|
|
1662
|
+
describe('setContentPosition (via moveContentBelow)', () => {
|
|
1663
|
+
// Helper: mock getBoundingClientRect on an element to return specific values
|
|
1664
|
+
function mockRect(el: HTMLElement, rect: Partial<DOMRect>) {
|
|
1665
|
+
el.getBoundingClientRect = vi.fn(
|
|
1666
|
+
() =>
|
|
1667
|
+
({
|
|
1668
|
+
top: 0,
|
|
1669
|
+
left: 0,
|
|
1670
|
+
right: 0,
|
|
1671
|
+
bottom: 0,
|
|
1672
|
+
width: 0,
|
|
1673
|
+
height: 0,
|
|
1674
|
+
x: 0,
|
|
1675
|
+
y: 0,
|
|
1676
|
+
toJSON: () => {},
|
|
1677
|
+
...rect
|
|
1678
|
+
}) as DOMRect
|
|
1679
|
+
)
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
test('absolute (default): content aligns below trigger using document coords', () => {
|
|
1683
|
+
// Simulate trigger at viewport (100, 50) with height 40 and width 200
|
|
1684
|
+
mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
|
|
1685
|
+
|
|
1686
|
+
// No scroll
|
|
1687
|
+
Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
|
|
1688
|
+
Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
|
|
1689
|
+
|
|
1690
|
+
render.settings.contentPosition = 'absolute'
|
|
1691
|
+
render.moveContentBelow()
|
|
1692
|
+
|
|
1693
|
+
// top = viewportTop(100) + scrollY(0) + height(40) = 140
|
|
1694
|
+
// left = viewportLeft(50) + scrollX(0) = 50
|
|
1695
|
+
expect(render.content.main.style.top).toBe('140px')
|
|
1696
|
+
expect(render.content.main.style.left).toBe('50px')
|
|
1697
|
+
expect(render.content.main.style.width).toBe('200px')
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
test('absolute: content accounts for page scroll', () => {
|
|
1701
|
+
// Trigger at viewport (100, 50) but page is scrolled down 300px and right 20px
|
|
1702
|
+
mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
|
|
1703
|
+
|
|
1704
|
+
Object.defineProperty(window, 'scrollY', { value: 300, writable: true })
|
|
1705
|
+
Object.defineProperty(window, 'scrollX', { value: 20, writable: true })
|
|
1706
|
+
|
|
1707
|
+
render.settings.contentPosition = 'absolute'
|
|
1708
|
+
render.moveContentBelow()
|
|
1709
|
+
|
|
1710
|
+
// top = viewportTop(100) + scrollY(300) + height(40) = 440
|
|
1711
|
+
// left = viewportLeft(50) + scrollX(20) = 70
|
|
1712
|
+
expect(render.content.main.style.top).toBe('440px')
|
|
1713
|
+
expect(render.content.main.style.left).toBe('70px')
|
|
1714
|
+
expect(render.content.main.style.width).toBe('200px')
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
test('absolute: content aligns correctly when trigger is near top of page', () => {
|
|
1718
|
+
// Trigger is at very top, no scroll
|
|
1719
|
+
mockRect(render.main.main, { top: 0, left: 10, height: 30, width: 150 })
|
|
1720
|
+
|
|
1721
|
+
Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
|
|
1722
|
+
Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
|
|
1723
|
+
|
|
1724
|
+
render.settings.contentPosition = 'absolute'
|
|
1725
|
+
render.moveContentBelow()
|
|
1726
|
+
|
|
1727
|
+
// top = 0 + 0 + 30 = 30
|
|
1728
|
+
// left = 10 + 0 = 10
|
|
1729
|
+
expect(render.content.main.style.top).toBe('30px')
|
|
1730
|
+
expect(render.content.main.style.left).toBe('10px')
|
|
1731
|
+
expect(render.content.main.style.width).toBe('150px')
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
test('fixed: content uses viewport coords without scroll offset', () => {
|
|
1735
|
+
// Same trigger position and scroll as the absolute+scroll test
|
|
1736
|
+
mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
|
|
1737
|
+
|
|
1738
|
+
Object.defineProperty(window, 'scrollY', { value: 300, writable: true })
|
|
1739
|
+
Object.defineProperty(window, 'scrollX', { value: 20, writable: true })
|
|
1740
|
+
|
|
1741
|
+
render.settings.contentPosition = 'fixed'
|
|
1742
|
+
render.moveContentBelow()
|
|
1743
|
+
|
|
1744
|
+
// Fixed ignores scroll: top = viewportTop(100) + height(40) = 140
|
|
1745
|
+
// left = viewportLeft(50) = 50
|
|
1746
|
+
expect(render.content.main.style.top).toBe('140px')
|
|
1747
|
+
expect(render.content.main.style.left).toBe('50px')
|
|
1748
|
+
expect(render.content.main.style.width).toBe('200px')
|
|
1749
|
+
})
|
|
1750
|
+
|
|
1751
|
+
test('relative: content position styles are not set', () => {
|
|
1752
|
+
mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
|
|
1753
|
+
|
|
1754
|
+
render.settings.contentPosition = 'relative'
|
|
1755
|
+
|
|
1756
|
+
// Reset styles to confirm they aren't changed
|
|
1757
|
+
render.content.main.style.top = ''
|
|
1758
|
+
render.content.main.style.left = ''
|
|
1759
|
+
render.content.main.style.width = ''
|
|
1760
|
+
|
|
1761
|
+
render.moveContentBelow()
|
|
1762
|
+
|
|
1763
|
+
// setContentPosition returns early for relative, so no inline positioning
|
|
1764
|
+
expect(render.content.main.style.top).toBe('')
|
|
1765
|
+
expect(render.content.main.style.left).toBe('')
|
|
1766
|
+
expect(render.content.main.style.width).toBe('')
|
|
1767
|
+
})
|
|
1768
|
+
|
|
1769
|
+
test('absolute vs fixed diverge only by scroll offset', () => {
|
|
1770
|
+
const triggerRect = { top: 200, left: 80, height: 36, width: 250 }
|
|
1771
|
+
|
|
1772
|
+
Object.defineProperty(window, 'scrollY', { value: 500, writable: true })
|
|
1773
|
+
Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
|
|
1774
|
+
|
|
1775
|
+
// Absolute
|
|
1776
|
+
mockRect(render.main.main, triggerRect)
|
|
1777
|
+
render.settings.contentPosition = 'absolute'
|
|
1778
|
+
render.moveContentBelow()
|
|
1779
|
+
const absTop = render.content.main.style.top
|
|
1780
|
+
const absLeft = render.content.main.style.left
|
|
1781
|
+
|
|
1782
|
+
// Fixed
|
|
1783
|
+
mockRect(render.main.main, triggerRect)
|
|
1784
|
+
render.settings.contentPosition = 'fixed'
|
|
1785
|
+
render.moveContentBelow()
|
|
1786
|
+
const fixedTop = render.content.main.style.top
|
|
1787
|
+
const fixedLeft = render.content.main.style.left
|
|
1788
|
+
|
|
1789
|
+
// Absolute top should be exactly scrollY more than fixed top
|
|
1790
|
+
expect(parseFloat(absTop)).toBe(parseFloat(fixedTop) + 500)
|
|
1791
|
+
// Left should differ by scrollX (which is 0 here, so equal)
|
|
1792
|
+
expect(parseFloat(absLeft)).toBe(parseFloat(fixedLeft))
|
|
1793
|
+
})
|
|
1794
|
+
|
|
1795
|
+
describe('contentWidth setting', () => {
|
|
1796
|
+
beforeEach(() => {
|
|
1797
|
+
mockRect(render.main.main, { top: 100, left: 50, height: 40, width: 200 })
|
|
1798
|
+
Object.defineProperty(window, 'scrollY', { value: 0, writable: true })
|
|
1799
|
+
Object.defineProperty(window, 'scrollX', { value: 0, writable: true })
|
|
1800
|
+
render.settings.contentPosition = 'absolute'
|
|
1801
|
+
})
|
|
1802
|
+
|
|
1803
|
+
test('default (empty): width matches trigger', () => {
|
|
1804
|
+
render.settings.contentWidth = ''
|
|
1805
|
+
render.moveContentBelow()
|
|
1806
|
+
|
|
1807
|
+
expect(render.content.main.style.width).toBe('200px')
|
|
1808
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1809
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1810
|
+
})
|
|
1811
|
+
|
|
1812
|
+
test('exact value: sets width to given value', () => {
|
|
1813
|
+
render.settings.contentWidth = '500px'
|
|
1814
|
+
render.moveContentBelow()
|
|
1815
|
+
|
|
1816
|
+
expect(render.content.main.style.width).toBe('500px')
|
|
1817
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1818
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
test('> prefix: sets min-width, no fixed width', () => {
|
|
1822
|
+
render.settings.contentWidth = '>300px'
|
|
1823
|
+
render.moveContentBelow()
|
|
1824
|
+
|
|
1825
|
+
expect(render.content.main.style.minWidth).toBe('300px')
|
|
1826
|
+
expect(render.content.main.style.width).toBe('')
|
|
1827
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1828
|
+
})
|
|
1829
|
+
|
|
1830
|
+
test('< prefix: sets max-width, no fixed width', () => {
|
|
1831
|
+
render.settings.contentWidth = '<600px'
|
|
1832
|
+
render.moveContentBelow()
|
|
1833
|
+
|
|
1834
|
+
expect(render.content.main.style.maxWidth).toBe('600px')
|
|
1835
|
+
expect(render.content.main.style.width).toBe('')
|
|
1836
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1837
|
+
})
|
|
1838
|
+
|
|
1839
|
+
test('exact value with percentage', () => {
|
|
1840
|
+
render.settings.contentWidth = '100%'
|
|
1841
|
+
render.moveContentBelow()
|
|
1842
|
+
|
|
1843
|
+
expect(render.content.main.style.width).toBe('100%')
|
|
1844
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1845
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1846
|
+
})
|
|
1847
|
+
|
|
1848
|
+
test('clears previous width styles when switching modes', () => {
|
|
1849
|
+
// First set min-width
|
|
1850
|
+
render.settings.contentWidth = '>300px'
|
|
1851
|
+
render.moveContentBelow()
|
|
1852
|
+
expect(render.content.main.style.minWidth).toBe('300px')
|
|
1853
|
+
|
|
1854
|
+
// Now switch to exact width — min-width should be cleared
|
|
1855
|
+
render.settings.contentWidth = '500px'
|
|
1856
|
+
render.moveContentBelow()
|
|
1857
|
+
expect(render.content.main.style.width).toBe('500px')
|
|
1858
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1859
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1860
|
+
})
|
|
1861
|
+
|
|
1862
|
+
test('clears previous width styles when switching to default', () => {
|
|
1863
|
+
// First set max-width
|
|
1864
|
+
render.settings.contentWidth = '<600px'
|
|
1865
|
+
render.moveContentBelow()
|
|
1866
|
+
expect(render.content.main.style.maxWidth).toBe('600px')
|
|
1867
|
+
|
|
1868
|
+
// Now switch to default — max-width should be cleared, width matches trigger
|
|
1869
|
+
render.settings.contentWidth = ''
|
|
1870
|
+
render.moveContentBelow()
|
|
1871
|
+
expect(render.content.main.style.width).toBe('200px')
|
|
1872
|
+
expect(render.content.main.style.minWidth).toBe('')
|
|
1873
|
+
expect(render.content.main.style.maxWidth).toBe('')
|
|
1874
|
+
})
|
|
1875
|
+
})
|
|
1876
|
+
})
|
|
1661
1877
|
})
|
|
@@ -1614,26 +1614,81 @@ export default class Render {
|
|
|
1614
1614
|
return
|
|
1615
1615
|
}
|
|
1616
1616
|
|
|
1617
|
+
// getBoundingClientRect() returns viewport-relative coordinates.
|
|
1617
1618
|
const containerRect = this.main.main.getBoundingClientRect()
|
|
1618
1619
|
let top: number
|
|
1619
1620
|
let left: number
|
|
1620
1621
|
|
|
1621
1622
|
if (this.settings.contentPosition === 'fixed') {
|
|
1622
|
-
//
|
|
1623
|
+
// position:fixed is relative to the viewport, so viewport coords work directly.
|
|
1623
1624
|
top = containerRect.top + containerRect.height
|
|
1624
1625
|
left = containerRect.left
|
|
1625
1626
|
} else {
|
|
1626
|
-
//
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1627
|
+
// position:absolute is relative to the containing block. By default the content
|
|
1628
|
+
// is appended to document.body (static), so the containing block is the initial
|
|
1629
|
+
// containing block (document origin). We must convert viewport coords to document
|
|
1630
|
+
// coords by adding scroll offsets. Using offsetParent subtraction was unreliable
|
|
1631
|
+
// because offsetParent for body children varies across browsers (body vs html)
|
|
1632
|
+
// and body margin caused misalignment.
|
|
1633
|
+
top = containerRect.top + window.scrollY + containerRect.height
|
|
1634
|
+
left = containerRect.left + window.scrollX
|
|
1632
1635
|
}
|
|
1633
1636
|
|
|
1634
1637
|
this.content.main.style.top = top + 'px'
|
|
1635
1638
|
this.content.main.style.left = left + 'px'
|
|
1636
|
-
|
|
1639
|
+
|
|
1640
|
+
// Apply content width based on contentWidth setting
|
|
1641
|
+
// "" → width matches trigger (default/current behavior)
|
|
1642
|
+
// "500px" → exact width of 500px
|
|
1643
|
+
// ">500px" → min-width of 500px, width auto (content can grow)
|
|
1644
|
+
// "<500px" → max-width of 500px, width auto (content can shrink)
|
|
1645
|
+
const cw = this.settings.contentWidth
|
|
1646
|
+
this.content.main.style.width = ''
|
|
1647
|
+
this.content.main.style.minWidth = ''
|
|
1648
|
+
this.content.main.style.maxWidth = ''
|
|
1649
|
+
|
|
1650
|
+
if (!cw) {
|
|
1651
|
+
// Default: match trigger width exactly
|
|
1652
|
+
this.content.main.style.width = containerRect.width + 'px'
|
|
1653
|
+
} else if (cw.startsWith('>')) {
|
|
1654
|
+
// Min-width mode: at least this wide, can grow to fit content
|
|
1655
|
+
this.content.main.style.minWidth = cw.slice(1)
|
|
1656
|
+
} else if (cw.startsWith('<')) {
|
|
1657
|
+
// Max-width mode: no wider than this, content wraps if longer
|
|
1658
|
+
this.content.main.style.maxWidth = cw.slice(1)
|
|
1659
|
+
} else {
|
|
1660
|
+
// Exact width
|
|
1661
|
+
this.content.main.style.width = cw
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// If content overflows the right side of the viewport, shift it left so the right edge
|
|
1665
|
+
// lines up with the trigger (or stays inside the viewport). Use rAF so we measure after
|
|
1666
|
+
// layout (and after width:auto / min-width have been applied).
|
|
1667
|
+
const padding = 20
|
|
1668
|
+
const viewportRight = window.innerWidth - padding
|
|
1669
|
+
|
|
1670
|
+
const applyOverflowShift = (): void => {
|
|
1671
|
+
const contentRect = this.content.main.getBoundingClientRect()
|
|
1672
|
+
const contentRight = contentRect.right
|
|
1673
|
+
if (contentRight <= viewportRight) return
|
|
1674
|
+
|
|
1675
|
+
const overflow = contentRight - viewportRight
|
|
1676
|
+
const currentLeft = parseFloat(this.content.main.style.left) || 0
|
|
1677
|
+
|
|
1678
|
+
if (this.settings.contentPosition === 'fixed') {
|
|
1679
|
+
const newLeft = Math.max(padding, currentLeft - overflow)
|
|
1680
|
+
this.content.main.style.left = newLeft + 'px'
|
|
1681
|
+
} else {
|
|
1682
|
+
const newLeft = Math.max(window.scrollX + padding, currentLeft - overflow)
|
|
1683
|
+
this.content.main.style.left = newLeft + 'px'
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// First rAF: layout done (width/min-width applied). Second rAF: catch scrollbar etc.
|
|
1688
|
+
requestAnimationFrame(() => {
|
|
1689
|
+
applyOverflowShift()
|
|
1690
|
+
requestAnimationFrame(applyOverflowShift)
|
|
1691
|
+
})
|
|
1637
1692
|
}
|
|
1638
1693
|
|
|
1639
1694
|
public moveContentAbove(): void {
|
|
@@ -36,7 +36,8 @@ const defaultSettings: { [key: string]: any } = {
|
|
|
36
36
|
maxSelected: 1000,
|
|
37
37
|
timeoutDelay: 200,
|
|
38
38
|
maxValuesShown: 20,
|
|
39
|
-
maxValuesMessage: '{number} selected'
|
|
39
|
+
maxValuesMessage: '{number} selected',
|
|
40
|
+
contentWidth: ''
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
describe('Settings module', () => {
|
|
@@ -25,6 +25,7 @@ export default class Settings {
|
|
|
25
25
|
public closeOnSelect: boolean
|
|
26
26
|
public contentLocation: HTMLElement | null
|
|
27
27
|
public contentPosition: 'relative' | 'absolute' | 'fixed'
|
|
28
|
+
public contentWidth: string
|
|
28
29
|
public openPosition: 'auto' | 'up' | 'down'
|
|
29
30
|
public placeholderText: string
|
|
30
31
|
public allowDeselect: boolean
|
|
@@ -60,6 +61,7 @@ export default class Settings {
|
|
|
60
61
|
this.closeOnSelect = settings.closeOnSelect !== undefined ? settings.closeOnSelect : true
|
|
61
62
|
this.contentLocation = settings.contentLocation || document.body
|
|
62
63
|
this.contentPosition = settings.contentPosition || 'absolute'
|
|
64
|
+
this.contentWidth = settings.contentWidth || ''
|
|
63
65
|
this.openPosition = settings.openPosition || 'auto' // options: auto, up, down
|
|
64
66
|
this.placeholderText = settings.placeholderText !== undefined ? settings.placeholderText : 'Select Value'
|
|
65
67
|
this.allowDeselect = settings.allowDeselect !== undefined ? settings.allowDeselect : false
|