Skip to content

Commit cf5eab8

Browse files
committed
feat: inject css from js
1 parent 8750dfa commit cf5eab8

File tree

9 files changed

+152
-337
lines changed

9 files changed

+152
-337
lines changed

package-lock.json

Lines changed: 28 additions & 239 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@
6767
"lint-staged": "^15.2.10",
6868
"semantic-release": "^24.1.3",
6969
"vite": "^5.4.9",
70-
"vite-css-modules": "^1.6.0",
7170
"vite-plugin-banner": "^0.8.0",
7271
"vite-plugin-compression": "^0.5.1",
7372
"vite-plugin-dts": "^4.3.0",
@@ -116,5 +115,8 @@
116115
]
117116
]
118117
}
118+
},
119+
"dependencies": {
120+
"vite-plugin-css-injected-by-js": "^3.5.2"
119121
}
120122
}

src/css/demo.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ body {
1313
padding: 2rem;
1414
}
1515

16-
.tooltip-trigger {
16+
button {
1717
padding: 0.5rem 1rem;
1818
background: #4f46e5;
1919
color: white;

src/css/tooltip.module.css

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
.tooltip-trigger {
2-
padding: 0.5rem 1rem;
3-
background: #4f46e5;
4-
color: white;
5-
border: none;
6-
border-radius: 0.25rem;
7-
cursor: pointer;
8-
}
9-
101
.tooltip {
112
position: absolute;
123
background: #1f2937;

src/css/tooltip.module.css.d.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
* Generated by vite-css-modules
66
* https://npmjs.com/vite-css-modules
77
*/
8-
const tooltipTrigger: string
98
const tooltip: string
109
const visible: string
1110

12-
export { tooltipTrigger, tooltip, visible }
11+
export { tooltip, visible }
1312

1413
export default {
15-
'tooltip-trigger': tooltipTrigger,
16-
tooltipTrigger,
1714
tooltip,
1815
visible,
1916
}

src/index.html

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,26 @@
66
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
77
<meta name="viewport"
88
content="user-scalable=no, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
9-
<meta name="description" content="Language files for formeo drag and drop form editor" />
9+
<meta name="description" content="Pure JS, zero dependency tooltip" />
1010
<meta name="theme-color" content="#232323" />
11-
<title></title>
11+
<title>Tooltip</title>
1212
</head>
1313

1414
<body>
1515
<div class="container">
16-
<button class="tooltip-trigger"
17-
data-tooltip="<strong>Hello!</strong><br>This is a tooltip with <em>HTML</em> content.">
16+
<button data-tooltip="<strong>Hello!</strong><br>This is a tooltip with <em>HTML</em> content.">
1817
Hover me (center)
1918
</button>
2019

21-
<button class="tooltip-trigger" style="align-self: flex-start;"
22-
data-tooltip="This tooltip will adjust its position when near the top">
20+
<button style="align-self: flex-start;" data-tooltip="This tooltip will adjust its position when near the top">
2321
Hover me (top)
2422
</button>
2523

26-
<button class="tooltip-trigger" style="align-self: flex-end;"
27-
data-tooltip="This tooltip will only show on click" data-tooltip-type="click">
24+
<button style="align-self: flex-end;" data-tooltip="This tooltip will only show on click" data-tooltip-type="click">
2825
Click Me
2926
</button>
3027

31-
<button class="tooltip-trigger" style="align-self: flex-end;"
32-
data-tooltip="This tooltip will adjust when near the bottom">
28+
<button style="align-self: flex-end;" data-tooltip="This tooltip will adjust when near the bottom">
3329
Hover me (bottom)
3430
</button>
3531

src/js/index.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ describe('SmartTooltip', () => {
1818

1919
test('should initialize with default options', () => {
2020
const instance = new SmartTooltip()
21-
assert.equal(instance['options'].triggerName, 'tooltip')
21+
assert.equal(instance.triggerName, 'data-tooltip')
2222
})
2323

2424
test('should initialize with custom options', () => {
2525
const instance = new SmartTooltip({ triggerName: 'custom-tooltip' })
26-
assert.equal(instance['options'].triggerName, 'custom-tooltip')
26+
assert.equal(instance.triggerName, 'data-custom-tooltip')
2727
})
2828

2929
test('should show tooltip on hover', () => {
@@ -72,7 +72,18 @@ describe('SmartTooltip', () => {
7272

7373
test('should fit tooltip in viewport', () => {
7474
const instance = new SmartTooltip()
75-
assert.ok(instance['fitsInViewport'](10, 10, 100, 100))
76-
assert.ok(!instance['fitsInViewport'](-10, -10, 100, 100))
75+
const rect: DOMRect = {
76+
x: 400,
77+
y: 400,
78+
width: 250,
79+
height: 50,
80+
top: 400,
81+
right: 400,
82+
bottom: 400,
83+
left: 400,
84+
toJSON: () => ({}),
85+
}
86+
assert.ok(instance['fitsInViewport']({ name: 'top', x: 100, y: 100 }, rect))
87+
assert.ok(!instance['fitsInViewport']({ name: 'top', x: -100, y: -100 }, rect))
7788
})
7889
})

src/js/index.ts

Lines changed: 93 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
import styles from '../css/tooltip.module.css'
22

3-
console.log(styles)
4-
53
interface SmartTooltipOptions {
64
triggerName: string
75
}
86

7+
interface Position {
8+
name: 'top' | 'bottom' | 'left' | 'right'
9+
x: number
10+
y: number
11+
}
12+
913
const defaultOptions = {
1014
triggerName: 'tooltip',
1115
}
1216

1317
export class SmartTooltip {
14-
private readonly options: SmartTooltipOptions
18+
readonly triggerName: string
1519
private readonly tooltip: HTMLDivElement
1620
private activeTriggerType: string | null = null
21+
private readonly spacing = 12
1722

1823
constructor(options: SmartTooltipOptions = defaultOptions) {
19-
this.options = options
24+
this.triggerName = `data-${options.triggerName}`
2025
this.tooltip = document.createElement('div')
2126
this.tooltip.className = styles.tooltip
2227
document.body.appendChild(this.tooltip)
@@ -25,89 +30,102 @@ export class SmartTooltip {
2530
}
2631

2732
private setupEventListeners() {
28-
const triggerName = `data-${this.options.triggerName}`
29-
// Handle hover-based tooltips
30-
document.addEventListener('mouseover', e => {
31-
const trigger = (e.target as Element).closest(`[${triggerName}]`)
32-
if (this.activeTriggerType !== 'click' && trigger?.getAttribute(`${triggerName}-type`) !== 'click') {
33-
const content = trigger?.getAttribute(`${triggerName}`)
34-
if (content) {
35-
this.show(trigger as HTMLElement, content)
36-
this.activeTriggerType = 'hover'
37-
}
38-
}
39-
})
33+
document.addEventListener('mouseover', this.handleMouseOver)
34+
document.addEventListener('mouseout', this.handleMouseOut)
35+
document.addEventListener('click', this.handleClick)
36+
window.addEventListener('resize', this.handleResize)
37+
window.addEventListener('scroll', this.handleScroll, true)
38+
}
4039

41-
document.addEventListener('mouseout', e => {
42-
const trigger = (e.target as Element).closest(`[${triggerName}]`)
43-
if (this.activeTriggerType !== 'click' && trigger?.getAttribute(`${triggerName}-type`) !== 'click') {
40+
private readonly handleClick = (e: Event): void => {
41+
const triggerName = this.triggerName
42+
const trigger = (e.target as Element).closest(`[${triggerName}][${triggerName}-type="click"]`)
43+
if (trigger) {
44+
if (this.isVisible()) {
4445
this.hide()
45-
}
46-
})
47-
48-
// Handle click-based tooltips
49-
document.addEventListener('click', e => {
50-
const trigger = (e.target as Element).closest(`[${triggerName}][${triggerName}-type="click"]`)
51-
if (trigger) {
52-
if (this.isVisible()) {
53-
this.hide()
54-
} else {
55-
const content = trigger.getAttribute(`${triggerName}`)
56-
this.show(trigger as HTMLElement, content)
57-
this.activeTriggerType = 'click'
58-
}
5946
} else {
60-
this.hide()
47+
const content = trigger.getAttribute(`${triggerName}`)
48+
this.show(trigger as HTMLElement, content)
49+
this.activeTriggerType = 'click'
50+
}
51+
} else {
52+
this.hide()
53+
}
54+
}
55+
56+
private readonly handleMouseOver = (e: Event): void => {
57+
const triggerName = this.triggerName
58+
const trigger = (e.target as Element).closest(`[${triggerName}]`)
59+
if (this.activeTriggerType !== 'click' && trigger?.getAttribute(`${triggerName}-type`) !== 'click') {
60+
const content = trigger?.getAttribute(`${triggerName}`)
61+
if (content) {
62+
this.show(trigger as HTMLElement, content)
63+
this.activeTriggerType = 'hover'
6164
}
62-
})
65+
}
66+
}
67+
68+
private readonly handleMouseOut = (e: Event): void => {
69+
const triggerName = this.triggerName
70+
const trigger = (e.target as Element).closest(`[${triggerName}]`)
71+
if (this.activeTriggerType !== 'click' && trigger?.getAttribute(`${triggerName}-type`) !== 'click') {
72+
this.hide()
73+
}
74+
}
75+
76+
private readonly handleResize = (): void => {
77+
if (this.isVisible()) {
78+
this.hide()
79+
}
80+
}
81+
82+
private readonly handleScroll = (): void => {
83+
if (this.isVisible()) {
84+
this.hide()
85+
}
6386
}
6487

6588
private isVisible(): boolean {
6689
return this.tooltip.classList.contains(styles.visible)
6790
}
6891

69-
calculatePosition(trigger: Element) {
92+
private calculatePosition(trigger: HTMLElement): Position {
7093
const triggerRect = trigger.getBoundingClientRect()
7194
const tooltipRect = this.tooltip.getBoundingClientRect()
72-
const spacing = 12 // Space between tooltip and trigger
7395

74-
// Try positions in order of preference
75-
const positions = [
96+
const positions: Position[] = [
7697
{
7798
name: 'top',
7899
x: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2,
79-
y: triggerRect.top - tooltipRect.height - spacing,
100+
y: triggerRect.top - tooltipRect.height - this.spacing,
80101
},
81102
{
82103
name: 'bottom',
83104
x: triggerRect.left + (triggerRect.width - tooltipRect.width) / 2,
84-
y: triggerRect.bottom + spacing,
105+
y: triggerRect.bottom + this.spacing,
85106
},
86107
{
87-
name: 'right',
88-
x: triggerRect.right + spacing,
108+
name: 'left',
109+
x: triggerRect.left - tooltipRect.width - this.spacing,
89110
y: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
90111
},
91112
{
92-
name: 'left',
93-
x: triggerRect.left - tooltipRect.width - spacing,
113+
name: 'right',
114+
x: triggerRect.right + this.spacing,
94115
y: triggerRect.top + (triggerRect.height - tooltipRect.height) / 2,
95116
},
96117
]
97118

98-
// Find first position that fits in viewport
99-
for (const pos of positions) {
100-
if (this.fitsInViewport(pos.x, pos.y, tooltipRect.width, tooltipRect.height)) {
101-
return pos
102-
}
103-
}
104-
105-
// If no position fits perfectly, default to top
106-
return positions[0]
119+
return positions.find(pos => this.fitsInViewport(pos, tooltipRect)) || positions[0]
107120
}
108121

109-
fitsInViewport(x: number, y: number, width: number, height: number): boolean {
110-
return x >= 0 && y >= 0 && x + width <= window.innerWidth && y + height <= window.innerHeight
122+
private fitsInViewport(pos: Position, tooltipRect: DOMRect): boolean {
123+
return (
124+
pos.x >= 0 &&
125+
pos.y >= 0 &&
126+
pos.x + tooltipRect.width <= window.innerWidth &&
127+
pos.y + tooltipRect.height <= window.innerHeight
128+
)
111129
}
112130

113131
private show(trigger: HTMLElement, content: string | null) {
@@ -124,6 +142,25 @@ export class SmartTooltip {
124142
this.tooltip.classList.remove(styles.visible)
125143
this.activeTriggerType = null
126144
}
145+
146+
public destroy(): void {
147+
document.removeEventListener('mouseover', this.handleMouseOver)
148+
document.removeEventListener('mouseout', this.handleMouseOut)
149+
document.removeEventListener('click', this.handleClick)
150+
window.removeEventListener('resize', this.handleResize)
151+
window.removeEventListener('scroll', this.handleScroll, true)
152+
this.tooltip.remove()
153+
}
154+
}
155+
156+
declare global {
157+
interface Window {
158+
SmartTooltip: typeof SmartTooltip
159+
}
160+
}
161+
162+
if (window !== undefined) {
163+
window.SmartTooltip = SmartTooltip
127164
}
128165

129166
export default SmartTooltip

vite.config.lib.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import { defineConfig } from 'vite'
33
import type { UserConfigExport } from 'vite'
44
import banner from 'vite-plugin-banner'
55
import compression from 'vite-plugin-compression'
6-
import { patchCssModules } from 'vite-css-modules'
7-
6+
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
87
import dts from 'vite-plugin-dts'
98

109
import { bannerTemplate, camelCaseName, isProduction, shortName } from './env'
1110

1211
const config: UserConfigExport = defineConfig({
1312
root: './',
1413
plugins: [
14+
cssInjectedByJsPlugin(),
1515
banner(bannerTemplate),
1616
compression({
1717
algorithm: 'brotliCompress',
@@ -20,14 +20,12 @@ const config: UserConfigExport = defineConfig({
2020
dts({
2121
insertTypesEntry: true,
2222
}),
23-
patchCssModules({
24-
generateSourceTypes: true,
25-
}),
2623
],
2724
css: {
2825
modules: {
2926
localsConvention: 'camelCase',
30-
generateScopedName: isProduction ? '[hash:base64:8]' : '[local]_[hash:base64:5]',
27+
scopeBehaviour: 'local',
28+
generateScopedName: isProduction ? '[hash:base64:8]' : 'st_[local]_[hash:base64:5]',
3129
},
3230
},
3331
build: {
@@ -55,13 +53,7 @@ const config: UserConfigExport = defineConfig({
5553
output: {
5654
banner: bannerTemplate,
5755
exports: 'named',
58-
// intro: 'import "./src/css/tooltip.module.css";',
59-
assetFileNames: assetInfo => {
60-
if (assetInfo.name === 'style.css') {
61-
return `${shortName}.css`
62-
}
63-
return `${assetInfo.name}`
64-
},
56+
inlineDynamicImports: true,
6557
},
6658
},
6759
sourcemap: isProduction ? false : 'inline',

0 commit comments

Comments
 (0)