blumbaben 1.0.0
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/LICENSE.md +9 -0
- package/README.md +452 -0
- package/dist/index.d.ts +306 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ragaeeb Haq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# blumbaben
|
|
2
|
+
|
|
3
|
+
A lightweight TypeScript React hook library for adding formatting toolbars to text inputs and textareas. Show contextual formatting options when users focus on input fields.
|
|
4
|
+
|
|
5
|
+
[](https://wakatime.com/badge/user/a0b906ce-b8e7-4463-8bce-383238df6d4b/project/897368ef-62b9-48be-bba9-7c530f10e3da)
|
|
6
|
+

|
|
7
|
+
[](https://github.com/ragaeeb/blumbaben/actions/workflows/build.yml)
|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
[](https://bundlejs.com/?q=blumbaben%40latest)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
[](https://badge.fury.io/js/blumbaben)
|
|
16
|
+
[](https://opensource.org/licenses/MIT)
|
|
17
|
+
|
|
18
|
+
## ✨ Features
|
|
19
|
+
|
|
20
|
+
- **🎯 Global State Management** - Single toolbar instance shared across all inputs
|
|
21
|
+
- **📱 Smart Positioning** - Automatic toolbar positioning with customizable placement
|
|
22
|
+
- **🎨 Flexible Styling** - Bring your own UI components and styles
|
|
23
|
+
- **⚡ TypeScript Support** - Full type safety and excellent DX
|
|
24
|
+
- **🪶 Lightweight** - Minimal bundle size with no external dependencies
|
|
25
|
+
- **🔧 Configurable** - Customizable behavior and positioning
|
|
26
|
+
- **♿ Accessible** - Built with accessibility in mind
|
|
27
|
+
|
|
28
|
+
## 📦 Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install blumbaben
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun add blumbaben
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm add blumbaben
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 🚀 Quick Start
|
|
43
|
+
|
|
44
|
+
### Basic Usage with Hook
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import React, { useState } from 'react';
|
|
48
|
+
import { useFormattingToolbar } from 'blumbaben';
|
|
49
|
+
|
|
50
|
+
function MyComponent() {
|
|
51
|
+
const [content, setContent] = useState('');
|
|
52
|
+
const { getInputProps, isVisible, applyFormat } = useFormattingToolbar();
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<textarea
|
|
57
|
+
{...getInputProps()}
|
|
58
|
+
value={content}
|
|
59
|
+
onChange={(e) => setContent(e.target.value)}
|
|
60
|
+
placeholder="Focus me to see the toolbar!"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{isVisible && (
|
|
64
|
+
<div className="toolbar">
|
|
65
|
+
<button onClick={() => applyFormat((text) => text.toUpperCase())}>UPPERCASE</button>
|
|
66
|
+
<button onClick={() => applyFormat((text) => `**${text}**`)}>Bold</button>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using the FormattingToolbar Component
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import React, { useState } from 'react';
|
|
78
|
+
import { useFormattingToolbar, FormattingToolbar } from 'blumbaben';
|
|
79
|
+
|
|
80
|
+
function MyComponent() {
|
|
81
|
+
const [content, setContent] = useState('');
|
|
82
|
+
const { getInputProps } = useFormattingToolbar();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
<textarea
|
|
87
|
+
{...getInputProps()}
|
|
88
|
+
value={content}
|
|
89
|
+
onChange={(e) => setContent(e.target.value)}
|
|
90
|
+
placeholder="Focus me to see the toolbar!"
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
<FormattingToolbar>
|
|
94
|
+
{(applyFormat) => (
|
|
95
|
+
<div className="flex gap-2 p-2 bg-white border rounded shadow">
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => applyFormat((text) => text.toUpperCase())}
|
|
98
|
+
className="px-2 py-1 bg-blue-500 text-white rounded"
|
|
99
|
+
>
|
|
100
|
+
UPPERCASE
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => applyFormat((text) => `**${text}**`)}
|
|
104
|
+
className="px-2 py-1 bg-green-500 text-white rounded"
|
|
105
|
+
>
|
|
106
|
+
Bold
|
|
107
|
+
</button>
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => applyFormat((text) => text.replace(/\n/g, ' '))}
|
|
110
|
+
className="px-2 py-1 bg-red-500 text-white rounded"
|
|
111
|
+
>
|
|
112
|
+
Remove Line Breaks
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</FormattingToolbar>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Using the Higher-Order Component
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import React, { useState } from 'react';
|
|
126
|
+
import { withFormattingToolbar, FormattingToolbar } from 'blumbaben';
|
|
127
|
+
|
|
128
|
+
// Enhance your existing textarea component
|
|
129
|
+
const MyTextarea = ({ value, onChange, ...props }) => <textarea value={value} onChange={onChange} {...props} />;
|
|
130
|
+
|
|
131
|
+
const TextareaWithToolbar = withFormattingToolbar(MyTextarea);
|
|
132
|
+
|
|
133
|
+
function App() {
|
|
134
|
+
const [content, setContent] = useState('');
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div>
|
|
138
|
+
<TextareaWithToolbar
|
|
139
|
+
value={content}
|
|
140
|
+
onChange={(e) => setContent(e.target.value)}
|
|
141
|
+
placeholder="Focus me to see the toolbar!"
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<FormattingToolbar>
|
|
145
|
+
{(applyFormat) => (
|
|
146
|
+
<div className="toolbar-buttons">
|
|
147
|
+
<button onClick={() => applyFormat((text) => text.toUpperCase())}>UPPERCASE</button>
|
|
148
|
+
<button onClick={() => applyFormat((text) => text.toLowerCase())}>lowercase</button>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</FormattingToolbar>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 🔧 Configuration
|
|
158
|
+
|
|
159
|
+
### Toolbar Configuration Options
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
interface ToolbarConfig {
|
|
163
|
+
// Custom positioning function
|
|
164
|
+
getPosition?: (element: TextInputElement) => ToolbarPosition;
|
|
165
|
+
|
|
166
|
+
// Delay before hiding toolbar after blur (ms)
|
|
167
|
+
hideDelay?: number; // default: 500
|
|
168
|
+
|
|
169
|
+
// Prevent toolbar from closing when clicked
|
|
170
|
+
preventCloseOnClick?: boolean; // default: true
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Positioning
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({
|
|
178
|
+
getPosition: (element) => {
|
|
179
|
+
const rect = element.getBoundingClientRect();
|
|
180
|
+
return {
|
|
181
|
+
x: rect.left,
|
|
182
|
+
y: rect.top - 50, // Position above the element
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
hideDelay: 300,
|
|
186
|
+
preventCloseOnClick: true,
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Styling the Toolbar
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
<FormattingToolbar
|
|
194
|
+
className="my-custom-toolbar"
|
|
195
|
+
style={{
|
|
196
|
+
backgroundColor: 'white',
|
|
197
|
+
border: '1px solid #ccc',
|
|
198
|
+
borderRadius: '4px',
|
|
199
|
+
padding: '8px'
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{(applyFormat) => (
|
|
203
|
+
// Your toolbar content
|
|
204
|
+
)}
|
|
205
|
+
</FormattingToolbar>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 📚 API Reference
|
|
209
|
+
|
|
210
|
+
### Hooks
|
|
211
|
+
|
|
212
|
+
#### `useFormattingToolbar(config?: ToolbarConfig)`
|
|
213
|
+
|
|
214
|
+
Main hook for managing toolbar functionality.
|
|
215
|
+
|
|
216
|
+
**Returns:**
|
|
217
|
+
|
|
218
|
+
- `getInputProps()` - Props to spread on input/textarea elements
|
|
219
|
+
- `getToolbarProps()` - Props for toolbar container (includes positioning)
|
|
220
|
+
- `applyFormat(formatter)` - Apply formatting to active element
|
|
221
|
+
- `showToolbar(element)` - Manually show toolbar
|
|
222
|
+
- `hideToolbar()` - Manually hide toolbar
|
|
223
|
+
- `isVisible` - Whether toolbar is visible
|
|
224
|
+
- `toolbarState` - Current toolbar state
|
|
225
|
+
|
|
226
|
+
#### `useFormattingToolbarState()`
|
|
227
|
+
|
|
228
|
+
Lightweight hook for toolbar-only components that don't handle input events.
|
|
229
|
+
|
|
230
|
+
**Returns:**
|
|
231
|
+
|
|
232
|
+
- `applyFormat(formatter)` - Apply formatting to active element
|
|
233
|
+
- `isVisible` - Whether toolbar is visible
|
|
234
|
+
- `toolbarState` - Current toolbar state
|
|
235
|
+
|
|
236
|
+
### Components
|
|
237
|
+
|
|
238
|
+
#### `FormattingToolbar`
|
|
239
|
+
|
|
240
|
+
Renders the toolbar when an input is focused.
|
|
241
|
+
|
|
242
|
+
**Props:**
|
|
243
|
+
|
|
244
|
+
- `children` - Render function receiving `applyFormat` callback
|
|
245
|
+
- `as?` - Container element type (default: 'div')
|
|
246
|
+
- `className?` - CSS class name
|
|
247
|
+
- `config?` - Toolbar configuration
|
|
248
|
+
- `style?` - Inline styles
|
|
249
|
+
|
|
250
|
+
#### `withFormattingToolbar(Component, config?)`
|
|
251
|
+
|
|
252
|
+
Higher-order component that adds toolbar functionality to input components.
|
|
253
|
+
|
|
254
|
+
### Types
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
type FormatterFunction = (text: string) => string;
|
|
258
|
+
|
|
259
|
+
type TextInputElement = HTMLInputElement | HTMLTextAreaElement;
|
|
260
|
+
|
|
261
|
+
type ToolbarPosition = {
|
|
262
|
+
x: number;
|
|
263
|
+
y: number;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
type ToolbarState = {
|
|
267
|
+
activeElement: TextInputElement | null;
|
|
268
|
+
isVisible: boolean;
|
|
269
|
+
position: ToolbarPosition | null;
|
|
270
|
+
};
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## 💡 Common Formatting Functions
|
|
274
|
+
|
|
275
|
+
Here are some useful formatting functions you can use:
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// Text transformations
|
|
279
|
+
const toUpperCase = (text: string) => text.toUpperCase();
|
|
280
|
+
const toLowerCase = (text: string) => text.toLowerCase();
|
|
281
|
+
const toTitleCase = (text: string) =>
|
|
282
|
+
text.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
|
|
283
|
+
|
|
284
|
+
// Markdown formatting
|
|
285
|
+
const makeBold = (text: string) => `**${text}**`;
|
|
286
|
+
const makeItalic = (text: string) => `*${text}*`;
|
|
287
|
+
const makeCode = (text: string) => `\`${text}\``;
|
|
288
|
+
|
|
289
|
+
// Text cleaning
|
|
290
|
+
const removeLineBreaks = (text: string) => text.replace(/\n/g, ' ');
|
|
291
|
+
const trimWhitespace = (text: string) => text.trim();
|
|
292
|
+
const removeExtraSpaces = (text: string) => text.replace(/\s+/g, ' ');
|
|
293
|
+
|
|
294
|
+
// Usage
|
|
295
|
+
<button onClick={() => applyFormat(makeBold)}>Bold</button>;
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## 🎨 Styling Examples
|
|
299
|
+
|
|
300
|
+
### With Tailwind CSS
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
<FormattingToolbar className="bg-white border border-gray-200 rounded-lg shadow-lg p-2">
|
|
304
|
+
{(applyFormat) => (
|
|
305
|
+
<div className="flex gap-1">
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => applyFormat(toUpperCase)}
|
|
308
|
+
className="px-3 py-1 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
|
|
309
|
+
>
|
|
310
|
+
ABC
|
|
311
|
+
</button>
|
|
312
|
+
<button
|
|
313
|
+
onClick={() => applyFormat(toLowerCase)}
|
|
314
|
+
className="px-3 py-1 text-sm bg-green-500 text-white rounded hover:bg-green-600"
|
|
315
|
+
>
|
|
316
|
+
abc
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</FormattingToolbar>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### With CSS Modules
|
|
324
|
+
|
|
325
|
+
```css
|
|
326
|
+
/* styles.module.css */
|
|
327
|
+
.toolbar {
|
|
328
|
+
background: white;
|
|
329
|
+
border: 1px solid #e2e8f0;
|
|
330
|
+
border-radius: 8px;
|
|
331
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
332
|
+
padding: 8px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.toolbarButton {
|
|
336
|
+
padding: 4px 12px;
|
|
337
|
+
margin-right: 4px;
|
|
338
|
+
border: none;
|
|
339
|
+
border-radius: 4px;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
transition: background-color 0.2s;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.toolbarButton:hover {
|
|
345
|
+
background-color: #f1f5f9;
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
import styles from './styles.module.css';
|
|
351
|
+
|
|
352
|
+
<FormattingToolbar className={styles.toolbar}>
|
|
353
|
+
{(applyFormat) => (
|
|
354
|
+
<div>
|
|
355
|
+
<button className={styles.toolbarButton} onClick={() => applyFormat(makeBold)}>
|
|
356
|
+
Bold
|
|
357
|
+
</button>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
</FormattingToolbar>;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## 🔍 Advanced Usage
|
|
364
|
+
|
|
365
|
+
### Multiple Input Fields
|
|
366
|
+
|
|
367
|
+
The library automatically manages a single toolbar across multiple inputs:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
function MultiInputForm() {
|
|
371
|
+
const { getInputProps } = useFormattingToolbar();
|
|
372
|
+
const [field1, setField1] = useState('');
|
|
373
|
+
const [field2, setField2] = useState('');
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<div>
|
|
377
|
+
<input
|
|
378
|
+
{...getInputProps()}
|
|
379
|
+
value={field1}
|
|
380
|
+
onChange={(e) => setField1(e.target.value)}
|
|
381
|
+
placeholder="First field"
|
|
382
|
+
/>
|
|
383
|
+
|
|
384
|
+
<textarea
|
|
385
|
+
{...getInputProps()}
|
|
386
|
+
value={field2}
|
|
387
|
+
onChange={(e) => setField2(e.target.value)}
|
|
388
|
+
placeholder="Second field"
|
|
389
|
+
/>
|
|
390
|
+
|
|
391
|
+
<FormattingToolbar>
|
|
392
|
+
{(applyFormat) => (
|
|
393
|
+
<div>
|
|
394
|
+
<button onClick={() => applyFormat(toUpperCase)}>UPPERCASE</button>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
</FormattingToolbar>
|
|
398
|
+
</div>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Custom Formatter with Selection
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
const wrapWithQuotes = (text: string) => `"${text}"`;
|
|
407
|
+
const addPrefix = (text: string) => `• ${text}`;
|
|
408
|
+
|
|
409
|
+
// The library automatically handles whether text is selected or not
|
|
410
|
+
<button onClick={() => applyFormat(wrapWithQuotes)}>Add Quotes</button>;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Conditional Toolbar Content
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<FormattingToolbar>
|
|
417
|
+
{(applyFormat) => {
|
|
418
|
+
const { activeElement } = useFormattingToolbarState().toolbarState;
|
|
419
|
+
const isTextarea = activeElement?.tagName === 'TEXTAREA';
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<div>
|
|
423
|
+
<button onClick={() => applyFormat(toUpperCase)}>UPPERCASE</button>
|
|
424
|
+
{isTextarea && <button onClick={() => applyFormat(removeLineBreaks)}>Remove Line Breaks</button>}
|
|
425
|
+
</div>
|
|
426
|
+
);
|
|
427
|
+
}}
|
|
428
|
+
</FormattingToolbar>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## 🤝 Contributing
|
|
432
|
+
|
|
433
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
434
|
+
|
|
435
|
+
1. Fork the repository
|
|
436
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
437
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
438
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
439
|
+
5. Open a Pull Request
|
|
440
|
+
|
|
441
|
+
## 📄 License
|
|
442
|
+
|
|
443
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
444
|
+
|
|
445
|
+
## 🙏 Acknowledgments
|
|
446
|
+
|
|
447
|
+
- Built with TypeScript and React
|
|
448
|
+
- Inspired by modern text editing interfaces
|
|
449
|
+
- Designed for developer experience and flexibility
|
|
450
|
+
- Asmāʾ for the project name
|
|
451
|
+
|
|
452
|
+
---
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React$1, { JSX } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Function type for text formatting operations.
|
|
5
|
+
* Takes a string input and returns a transformed string.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const boldFormatter: FormatterFunction = (text) => `**${text}**`;
|
|
10
|
+
* const upperCaseFormatter: FormatterFunction = (text) => text.toUpperCase();
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
type FormatterFunction = (text: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Union type representing supported HTML input elements for text formatting.
|
|
16
|
+
* Includes both single-line inputs and multi-line textareas.
|
|
17
|
+
*/
|
|
18
|
+
type TextInputElement = HTMLInputElement | HTMLTextAreaElement;
|
|
19
|
+
/**
|
|
20
|
+
* Configuration options for the formatting toolbar behavior and appearance.
|
|
21
|
+
*/
|
|
22
|
+
type ToolbarConfig = {
|
|
23
|
+
/**
|
|
24
|
+
* Custom positioning function to determine where the toolbar appears relative to the focused element.
|
|
25
|
+
* If not provided, defaults to positioning below the element.
|
|
26
|
+
*
|
|
27
|
+
* @param {TextInputElement} element - The focused input or textarea element
|
|
28
|
+
* @returns {ToolbarPosition} Position coordinates for the toolbar
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const config: ToolbarConfig = {
|
|
33
|
+
* getPosition: (element) => {
|
|
34
|
+
* const rect = element.getBoundingClientRect();
|
|
35
|
+
* return { x: rect.left, y: rect.top - 50 }; // Above the element
|
|
36
|
+
* }
|
|
37
|
+
* };
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
getPosition?: (element: TextInputElement) => ToolbarPosition;
|
|
41
|
+
/**
|
|
42
|
+
* Delay in milliseconds before hiding the toolbar after the input loses focus.
|
|
43
|
+
* This allows users to click on toolbar buttons without the toolbar disappearing.
|
|
44
|
+
*
|
|
45
|
+
* @default 500
|
|
46
|
+
*/
|
|
47
|
+
hideDelay?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Whether to prevent the toolbar from closing when clicked.
|
|
50
|
+
* When true, clicking toolbar buttons won't cause the input to lose focus.
|
|
51
|
+
*
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
preventCloseOnClick?: boolean;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Represents the x,y coordinates for positioning the toolbar on screen.
|
|
58
|
+
*
|
|
59
|
+
* @interface ToolbarPosition
|
|
60
|
+
*/
|
|
61
|
+
type ToolbarPosition = {
|
|
62
|
+
/** Horizontal position in pixels from the left edge of the viewport */
|
|
63
|
+
x: number;
|
|
64
|
+
/** Vertical position in pixels from the top edge of the viewport */
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Current state of the formatting toolbar including visibility, position, and active element.
|
|
69
|
+
*
|
|
70
|
+
* @interface ToolbarState
|
|
71
|
+
*/
|
|
72
|
+
type ToolbarState = {
|
|
73
|
+
/** The currently focused input/textarea element, or null if no element is active */
|
|
74
|
+
activeElement: null | TextInputElement;
|
|
75
|
+
/** Whether the toolbar is currently visible to the user */
|
|
76
|
+
isVisible: boolean;
|
|
77
|
+
/** Current position of the toolbar, or null if not positioned */
|
|
78
|
+
position: null | ToolbarPosition;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Props for the FormattingToolbar component.
|
|
83
|
+
*
|
|
84
|
+
* @interface FormattingToolbarProps
|
|
85
|
+
*/
|
|
86
|
+
type FormattingToolbarProps = {
|
|
87
|
+
/**
|
|
88
|
+
* Container element type for the toolbar.
|
|
89
|
+
* Can be any valid HTML element tag name.
|
|
90
|
+
*
|
|
91
|
+
* @default 'div'
|
|
92
|
+
*/
|
|
93
|
+
as?: keyof JSX.IntrinsicElements;
|
|
94
|
+
/**
|
|
95
|
+
* Render function that receives the applyFormat callback.
|
|
96
|
+
* Use this function to render your toolbar buttons and controls.
|
|
97
|
+
*
|
|
98
|
+
* @param {(formatter: FormatterFunction) => void} applyFormat - Function to apply text formatting
|
|
99
|
+
* @returns {React.ReactNode} The toolbar content to render
|
|
100
|
+
*/
|
|
101
|
+
children: (applyFormat: (formatter: FormatterFunction) => void) => React$1.ReactNode;
|
|
102
|
+
/**
|
|
103
|
+
* Custom CSS class name for styling the toolbar container.
|
|
104
|
+
*/
|
|
105
|
+
className?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Toolbar configuration options.
|
|
108
|
+
* Merged with default configuration.
|
|
109
|
+
*/
|
|
110
|
+
config?: ToolbarConfig;
|
|
111
|
+
/**
|
|
112
|
+
* Custom inline styles for the toolbar container.
|
|
113
|
+
* These styles are merged with the positioning styles (position, top, left, zIndex).
|
|
114
|
+
*/
|
|
115
|
+
style?: React$1.CSSProperties;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Formatting toolbar component that renders when an input is focused.
|
|
119
|
+
* Automatically uses the global toolbar state - no need to pass a toolbar instance.
|
|
120
|
+
* Only renders when the global toolbar is visible and has a position.
|
|
121
|
+
*
|
|
122
|
+
* The toolbar automatically positions itself relative to the focused input element
|
|
123
|
+
* and provides an applyFormat function to child components for text transformation.
|
|
124
|
+
*
|
|
125
|
+
* @param {FormattingToolbarProps} props - Component props
|
|
126
|
+
* @returns {React.ReactElement | null} The toolbar element or null if not visible
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* import { FormattingToolbar } from 'blumbaben';
|
|
131
|
+
* import { Button } from './ui/button';
|
|
132
|
+
*
|
|
133
|
+
* function App() {
|
|
134
|
+
* return (
|
|
135
|
+
* <div>
|
|
136
|
+
* <textarea {...getInputProps()} />
|
|
137
|
+
*
|
|
138
|
+
* <FormattingToolbar className="my-toolbar" as="section">
|
|
139
|
+
* {(applyFormat) => (
|
|
140
|
+
* <>
|
|
141
|
+
* <Button onClick={() => applyFormat(text => text.toUpperCase())}>
|
|
142
|
+
* UPPERCASE
|
|
143
|
+
* </Button>
|
|
144
|
+
* <Button onClick={() => applyFormat(text => `**${text}**`)}>
|
|
145
|
+
* Bold
|
|
146
|
+
* </Button>
|
|
147
|
+
* <Button onClick={() => applyFormat(text => text.replace(/\n/g, ' '))}>
|
|
148
|
+
* Remove Line Breaks
|
|
149
|
+
* </Button>
|
|
150
|
+
* </>
|
|
151
|
+
* )}
|
|
152
|
+
* </FormattingToolbar>
|
|
153
|
+
* </div>
|
|
154
|
+
* );
|
|
155
|
+
* }
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
declare const FormattingToolbar: React$1.FC<FormattingToolbarProps>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Return type for the useFormattingToolbar hook containing all toolbar functionality.
|
|
162
|
+
*
|
|
163
|
+
* @interface UseFormattingToolbarResult
|
|
164
|
+
*/
|
|
165
|
+
type UseFormattingToolbarResult = {
|
|
166
|
+
/**
|
|
167
|
+
* Apply formatting to the currently active element.
|
|
168
|
+
*
|
|
169
|
+
* @param {FormatterFunction} formatter - Function to transform the selected or entire text
|
|
170
|
+
*/
|
|
171
|
+
applyFormat: (formatter: FormatterFunction) => void;
|
|
172
|
+
/**
|
|
173
|
+
* Props to spread on your input/textarea components.
|
|
174
|
+
* Includes onFocus and onBlur handlers for toolbar management.
|
|
175
|
+
*
|
|
176
|
+
* @returns {object} Props object with focus and blur handlers
|
|
177
|
+
*/
|
|
178
|
+
getInputProps: () => {
|
|
179
|
+
onBlur: (e: React.FocusEvent<TextInputElement>) => void;
|
|
180
|
+
onFocus: (e: React.FocusEvent<TextInputElement>) => void;
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Props for the toolbar container element.
|
|
184
|
+
* Includes positioning styles and optional mouse event handlers.
|
|
185
|
+
*
|
|
186
|
+
* @returns {object} Props object with styles and event handlers
|
|
187
|
+
*/
|
|
188
|
+
getToolbarProps: () => {
|
|
189
|
+
onMouseDown?: (e: React.MouseEvent) => void;
|
|
190
|
+
style: React.CSSProperties;
|
|
191
|
+
};
|
|
192
|
+
/** Function to manually hide the toolbar */
|
|
193
|
+
hideToolbar: () => void;
|
|
194
|
+
/** Whether the toolbar is currently visible */
|
|
195
|
+
isVisible: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Function to manually show the toolbar for a specific element.
|
|
198
|
+
*
|
|
199
|
+
* @param {TextInputElement} element - The element to show the toolbar for
|
|
200
|
+
*/
|
|
201
|
+
showToolbar: (element: TextInputElement) => void;
|
|
202
|
+
/** Current toolbar state (shared globally across all instances) */
|
|
203
|
+
toolbarState: ToolbarState;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Hook for managing formatting toolbar functionality.
|
|
207
|
+
* Uses global state so all instances share the same toolbar - only one toolbar
|
|
208
|
+
* can be visible at a time across the entire application.
|
|
209
|
+
*
|
|
210
|
+
* @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior
|
|
211
|
+
* @returns {UseFormattingToolbarResult} Object containing toolbar state and control functions
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* function MyComponent() {
|
|
216
|
+
* const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({
|
|
217
|
+
* hideDelay: 300,
|
|
218
|
+
* getPosition: (element) => ({ x: 100, y: 200 })
|
|
219
|
+
* });
|
|
220
|
+
*
|
|
221
|
+
* return (
|
|
222
|
+
* <div>
|
|
223
|
+
* <textarea {...getInputProps()} />
|
|
224
|
+
* {isVisible && (
|
|
225
|
+
* <div>
|
|
226
|
+
* <button onClick={() => applyFormat(text => text.toUpperCase())}>
|
|
227
|
+
* UPPERCASE
|
|
228
|
+
* </button>
|
|
229
|
+
* </div>
|
|
230
|
+
* )}
|
|
231
|
+
* </div>
|
|
232
|
+
* );
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
declare const useFormattingToolbar: (config?: ToolbarConfig) => UseFormattingToolbarResult;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Applies a formatting function to either selected text or entire content of an element.
|
|
240
|
+
* If text is selected (selectionStart !== selectionEnd), formats only the selected portion.
|
|
241
|
+
* If no text is selected, formats the entire content of the element.
|
|
242
|
+
* Returns the formatted result without modifying the original element.
|
|
243
|
+
*
|
|
244
|
+
* @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element containing the text
|
|
245
|
+
* @param {(text: string) => string} formatter - Function that takes a string and returns a formatted version
|
|
246
|
+
* @returns {string} The formatted text with either selected portion or entire content transformed
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const textarea = document.querySelector('textarea');
|
|
251
|
+
* const uppercaseFormatter = (text: string) => text.toUpperCase();
|
|
252
|
+
* const result = applyFormattingOnSelection(textarea, uppercaseFormatter);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
declare const applyFormattingOnSelection: (element: HTMLInputElement | HTMLTextAreaElement, formatter: (text: string) => string) => string;
|
|
256
|
+
/**
|
|
257
|
+
* Updates the value of an input or textarea element and optionally triggers onChange event.
|
|
258
|
+
* Creates a synthetic React change event if onChange callback is provided.
|
|
259
|
+
* Useful for programmatically updating form elements while maintaining React state consistency.
|
|
260
|
+
*
|
|
261
|
+
* @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element to update
|
|
262
|
+
* @param {string} newValue - The new string value to set on the element
|
|
263
|
+
* @param {(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void} [onChange] - Optional React onChange event handler to call after updating the value
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```typescript
|
|
267
|
+
* const handleChange = (e) => setFormValue(e.target.value);
|
|
268
|
+
* updateElementValue(textareaRef.current, 'New content', handleChange);
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
declare const updateElementValue: (element: HTMLInputElement | HTMLTextAreaElement, newValue: string, onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void) => void;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Higher-order component that adds formatting toolbar functionality to input components.
|
|
275
|
+
* Since the library uses global state, all wrapped components automatically share the same toolbar.
|
|
276
|
+
* Only one toolbar will be visible at a time, appearing for whichever input is currently focused.
|
|
277
|
+
*
|
|
278
|
+
* @template P - The props type of the wrapped component
|
|
279
|
+
* @param {React.ComponentType<P>} Component - The input component to enhance with toolbar functionality
|
|
280
|
+
* @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior
|
|
281
|
+
* @returns {React.ForwardRefExoticComponent} Enhanced component with toolbar functionality
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* import { Textarea } from './ui/textarea';
|
|
286
|
+
* import { withFormattingToolbar } from 'blumbaben';
|
|
287
|
+
*
|
|
288
|
+
* const TextareaWithToolbar = withFormattingToolbar(Textarea, {
|
|
289
|
+
* hideDelay: 300,
|
|
290
|
+
* getPosition: (element) => {
|
|
291
|
+
* const rect = element.getBoundingClientRect();
|
|
292
|
+
* return { x: rect.left, y: rect.top - 50 }; // Above the element
|
|
293
|
+
* }
|
|
294
|
+
* });
|
|
295
|
+
*
|
|
296
|
+
* // Usage
|
|
297
|
+
* <TextareaWithToolbar
|
|
298
|
+
* value={content}
|
|
299
|
+
* onChange={setContent}
|
|
300
|
+
* placeholder="Start typing..."
|
|
301
|
+
* />
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
declare const withFormattingToolbar: <P extends Record<string, any>>(Component: React$1.ComponentType<P>, config?: ToolbarConfig) => React$1.ForwardRefExoticComponent<React$1.PropsWithoutRef<P> & React$1.RefAttributes<TextInputElement>>;
|
|
305
|
+
|
|
306
|
+
export { type FormatterFunction, FormattingToolbar, type TextInputElement, type ToolbarConfig, type ToolbarPosition, type ToolbarState, applyFormattingOnSelection, updateElementValue, useFormattingToolbar, withFormattingToolbar };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import I from"react";import{useCallback as S,useEffect as x,useState as y}from"react";var T=(o,t)=>{let n=o.selectionEnd??0,e=o.selectionStart??0,r=o.value??"";if(n>e){let s=r.substring(0,e),a=r.substring(e,n),m=r.substring(n);return s+t(a)+m}return t(r)},v=(o,t,n)=>{o.value=t,o.dispatchEvent(new Event("input",{bubbles:!0})),n&&n({currentTarget:o,target:o})};var F=o=>{let t=o.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},d=class{activeElementOnChange;hideTimeout=null;state={activeElement:null,isVisible:!1,position:null};subscribers=new Set;applyFormat(t,n){let{activeElement:e}=this.state;if(!e){console.warn("No active element found for formatting");return}let r=T(e,t),s=n||this.activeElementOnChange;v(e,r,s),e.focus()}cancelScheduledHide(){this.clearHideTimeout()}getState(){return{...this.state}}hideToolbar(){this.clearHideTimeout(),this.activeElementOnChange=void 0,this.setState({activeElement:null,isVisible:!1,position:null})}scheduleHide(t){this.clearHideTimeout(),this.hideTimeout=setTimeout(()=>this.hideToolbar(),t)}showToolbar(t,n=F){this.clearHideTimeout();let e=t.props||{};this.activeElementOnChange=e.onChange,this.setState({activeElement:t,isVisible:!0,position:n(t)})}subscribe(t){return this.subscribers.add(t),()=>{this.subscribers.delete(t)}}clearHideTimeout(){this.hideTimeout&&(clearTimeout(this.hideTimeout),this.hideTimeout=null)}setState(t){this.state={...this.state,...t},this.subscribers.forEach(n=>n(this.getState()))}},i=new d;var h=()=>{let[o,t]=y(i.getState());return x(()=>i.subscribe(t),[]),{applyFormat:S(e=>{i.applyFormat(e)},[]),isVisible:o.isVisible,toolbarState:o}};var A=({as:o="div",children:t,className:n="",config:e={},style:r={}})=>{let{applyFormat:s,toolbarState:a}=h(),{preventCloseOnClick:m=!0}=e;if(!a.isVisible||!a.position)return null;let b=m?p=>{p.preventDefault()}:void 0;return I.createElement(o,{className:n,onMouseDown:b,style:{left:a.position.x,position:"fixed",top:a.position.y,zIndex:1e3,...r}},t(s))};import{useCallback as c,useEffect as R,useState as C}from"react";var P=o=>{let t=o.getBoundingClientRect();return{x:t.left,y:t.bottom+5}},f=(o={})=>{let{getPosition:t=P,hideDelay:n=500,preventCloseOnClick:e=!0}=o,[r,s]=C(i.getState());R(()=>i.subscribe(s),[]);let a=c(l=>{i.showToolbar(l,t)},[t]),m=c(()=>{i.hideToolbar()},[]),b=c(l=>{a(l.currentTarget)},[a]),p=c(()=>{i.scheduleHide(n)},[n]),u=c(l=>{i.applyFormat(l)},[]),E=c(()=>({onBlur:p,onFocus:b}),[b,p]),g=c(()=>({style:{left:r.position?.x??0,position:"fixed",top:r.position?.y??0,zIndex:1e3},...e&&{onMouseDown:l=>{l.preventDefault(),i.cancelScheduledHide()}}}),[r.position,e]);return{applyFormat:u,getInputProps:E,getToolbarProps:g,hideToolbar:m,isVisible:r.isVisible,showToolbar:a,toolbarState:r}};import H,{forwardRef as M}from"react";var $=(o,t={})=>{let n=M((e,r)=>{let{getInputProps:s}=f(t),a=s(),p={...e,onBlur:u=>{a.onBlur(u),e.onBlur&&typeof e.onBlur=="function"&&e.onBlur(u)},onFocus:u=>{a.onFocus(u),e.onFocus&&typeof e.onFocus=="function"&&e.onFocus(u)},ref:r};return H.createElement(o,p)});return n.displayName=`withFormattingToolbar(${o.displayName||o.name})`,n};export{A as FormattingToolbar,T as applyFormattingOnSelection,v as updateElementValue,f as useFormattingToolbar,$ as withFormattingToolbar};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/formatting-toolbar.tsx","../src/hooks/useFormattingToolbarState.ts","../src/utils/domUtils.ts","../src/utils/globalToolbarManager.ts","../src/hooks/useFormattingToolbar.ts","../src/withFormattingToolbar.ts"],"sourcesContent":["import React, { type JSX } from 'react';\n\nimport type { FormatterFunction, ToolbarConfig } from './types';\n\nimport { useFormattingToolbarState } from './hooks/useFormattingToolbarState';\n\n/**\n * Props for the FormattingToolbar component.\n *\n * @interface FormattingToolbarProps\n */\ntype FormattingToolbarProps = {\n /**\n * Container element type for the toolbar.\n * Can be any valid HTML element tag name.\n *\n * @default 'div'\n */\n as?: keyof JSX.IntrinsicElements;\n\n /**\n * Render function that receives the applyFormat callback.\n * Use this function to render your toolbar buttons and controls.\n *\n * @param {(formatter: FormatterFunction) => void} applyFormat - Function to apply text formatting\n * @returns {React.ReactNode} The toolbar content to render\n */\n children: (applyFormat: (formatter: FormatterFunction) => void) => React.ReactNode;\n\n /**\n * Custom CSS class name for styling the toolbar container.\n */\n className?: string;\n\n /**\n * Toolbar configuration options.\n * Merged with default configuration.\n */\n config?: ToolbarConfig;\n\n /**\n * Custom inline styles for the toolbar container.\n * These styles are merged with the positioning styles (position, top, left, zIndex).\n */\n style?: React.CSSProperties;\n};\n\n/**\n * Formatting toolbar component that renders when an input is focused.\n * Automatically uses the global toolbar state - no need to pass a toolbar instance.\n * Only renders when the global toolbar is visible and has a position.\n *\n * The toolbar automatically positions itself relative to the focused input element\n * and provides an applyFormat function to child components for text transformation.\n *\n * @param {FormattingToolbarProps} props - Component props\n * @returns {React.ReactElement | null} The toolbar element or null if not visible\n *\n * @example\n * ```typescript\n * import { FormattingToolbar } from 'blumbaben';\n * import { Button } from './ui/button';\n *\n * function App() {\n * return (\n * <div>\n * <textarea {...getInputProps()} />\n *\n * <FormattingToolbar className=\"my-toolbar\" as=\"section\">\n * {(applyFormat) => (\n * <>\n * <Button onClick={() => applyFormat(text => text.toUpperCase())}>\n * UPPERCASE\n * </Button>\n * <Button onClick={() => applyFormat(text => `**${text}**`)}>\n * Bold\n * </Button>\n * <Button onClick={() => applyFormat(text => text.replace(/\\n/g, ' '))}>\n * Remove Line Breaks\n * </Button>\n * </>\n * )}\n * </FormattingToolbar>\n * </div>\n * );\n * }\n * ```\n */\nexport const FormattingToolbar: React.FC<FormattingToolbarProps> = ({\n as: Component = 'div',\n children,\n className = '',\n config = {},\n style = {},\n}) => {\n const { applyFormat, toolbarState } = useFormattingToolbarState();\n const { preventCloseOnClick = true } = config;\n\n if (!toolbarState.isVisible || !toolbarState.position) {\n return null;\n }\n\n const handleMouseDown = preventCloseOnClick\n ? (e: React.MouseEvent) => {\n e.preventDefault(); // Prevent blur event when clicking toolbar\n }\n : undefined;\n\n return React.createElement(\n Component,\n {\n className,\n onMouseDown: handleMouseDown,\n style: {\n left: toolbarState.position.x,\n position: 'fixed',\n top: toolbarState.position.y,\n zIndex: 1000,\n ...style,\n },\n },\n children(applyFormat),\n );\n};\n","import { useCallback, useEffect, useState } from 'react';\n\nimport type { FormatterFunction, ToolbarState } from '@/types';\n\nimport { globalToolbarManager } from '@/utils/globalToolbarManager';\n\n/**\n * Lightweight hook that only subscribes to toolbar state without creating input handlers.\n * Useful for toolbar-only components that don't need to handle input focus/blur events.\n * Perfect for creating separate toolbar components that respond to the global toolbar state.\n *\n * @returns {object} Object containing applyFormat function, visibility state, and toolbar state\n *\n * @example\n * ```typescript\n * function ToolbarComponent() {\n * const { applyFormat, isVisible, toolbarState } = useFormattingToolbarState();\n *\n * if (!isVisible) return null;\n *\n * return (\n * <div style={{ position: 'fixed', top: toolbarState.position?.y, left: toolbarState.position?.x }}>\n * <button onClick={() => applyFormat(text => `**${text}**`)}>\n * Bold\n * </button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useFormattingToolbarState = () => {\n const [toolbarState, setToolbarState] = useState<ToolbarState>(globalToolbarManager.getState());\n\n useEffect(() => {\n const unsubscribe = globalToolbarManager.subscribe(setToolbarState);\n return unsubscribe;\n }, []);\n\n const applyFormat = useCallback((formatter: FormatterFunction) => {\n globalToolbarManager.applyFormat(formatter);\n }, []);\n\n return {\n applyFormat,\n isVisible: toolbarState.isVisible,\n toolbarState,\n };\n};\n","/**\n * Applies a formatting function to either selected text or entire content of an element.\n * If text is selected (selectionStart !== selectionEnd), formats only the selected portion.\n * If no text is selected, formats the entire content of the element.\n * Returns the formatted result without modifying the original element.\n *\n * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element containing the text\n * @param {(text: string) => string} formatter - Function that takes a string and returns a formatted version\n * @returns {string} The formatted text with either selected portion or entire content transformed\n *\n * @example\n * ```typescript\n * const textarea = document.querySelector('textarea');\n * const uppercaseFormatter = (text: string) => text.toUpperCase();\n * const result = applyFormattingOnSelection(textarea, uppercaseFormatter);\n * ```\n */\nexport const applyFormattingOnSelection = (\n element: HTMLInputElement | HTMLTextAreaElement,\n formatter: (text: string) => string,\n): string => {\n const selectionEnd = element.selectionEnd ?? 0;\n const selectionStart = element.selectionStart ?? 0;\n const value = element.value ?? '';\n\n if (selectionEnd > selectionStart) {\n // Format only selected text\n const before = value.substring(0, selectionStart);\n const selected = value.substring(selectionStart, selectionEnd);\n const after = value.substring(selectionEnd);\n\n return before + formatter(selected) + after;\n }\n\n // Format entire text if no selection\n return formatter(value);\n};\n\n/**\n * Updates the value of an input or textarea element and optionally triggers onChange event.\n * Creates a synthetic React change event if onChange callback is provided.\n * Useful for programmatically updating form elements while maintaining React state consistency.\n *\n * @param {HTMLInputElement | HTMLTextAreaElement} element - The HTML input or textarea element to update\n * @param {string} newValue - The new string value to set on the element\n * @param {(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void} [onChange] - Optional React onChange event handler to call after updating the value\n *\n * @example\n * ```typescript\n * const handleChange = (e) => setFormValue(e.target.value);\n * updateElementValue(textareaRef.current, 'New content', handleChange);\n * ```\n */\nexport const updateElementValue = (\n element: HTMLInputElement | HTMLTextAreaElement,\n newValue: string,\n onChange?: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void,\n) => {\n element.value = newValue;\n\n // Dispatch input event for React to detect the change\n element.dispatchEvent(new Event('input', { bubbles: true }));\n\n // Call onChange if provided (for controlled components)\n if (onChange) {\n const syntheticEvent = {\n currentTarget: element,\n target: element,\n } as React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n};\n","import type { FormatterFunction, TextInputElement, ToolbarPosition, ToolbarState } from '@/types';\n\nimport { applyFormattingOnSelection, updateElementValue } from './domUtils';\n\n/**\n * Default positioning function that places the toolbar below the focused element.\n * Positions the toolbar 5 pixels below the bottom edge of the element, aligned to the left.\n *\n * @param {TextInputElement} element - The focused input or textarea element\n * @returns {ToolbarPosition} Position coordinates with toolbar below the element\n */\nconst defaultGetPosition = (element: TextInputElement): ToolbarPosition => {\n const rect = element.getBoundingClientRect();\n return {\n x: rect.left,\n y: rect.bottom + 5,\n };\n};\n\n/**\n * Interface defining the contract for managing global toolbar state.\n * Provides methods for showing/hiding the toolbar and applying formatting.\n *\n * @interface ToolbarStateManager\n */\ntype ToolbarStateManager = {\n /**\n * Apply a formatting function to the currently active element.\n *\n * @param {FormatterFunction} formatter - Function to transform the text\n * @param {(e: React.ChangeEvent<TextInputElement>) => void} [onChange] - Optional change handler\n */\n applyFormat(formatter: FormatterFunction, onChange?: (e: React.ChangeEvent<TextInputElement>) => void): void;\n\n /** Cancel any scheduled toolbar hide operation */\n cancelScheduledHide(): void;\n\n /** Get the current toolbar state */\n getState(): ToolbarState;\n\n /** Immediately hide the toolbar */\n hideToolbar(): void;\n\n /**\n * Schedule the toolbar to hide after a delay.\n *\n * @param {number} delay - Delay in milliseconds\n */\n scheduleHide(delay: number): void;\n\n /**\n * Show the toolbar for a specific element.\n *\n * @param {TextInputElement} element - The element to show the toolbar for\n * @param {(element: TextInputElement) => ToolbarPosition} getPosition - Function to determine toolbar position\n */\n showToolbar(element: TextInputElement, getPosition: (element: TextInputElement) => ToolbarPosition): void;\n\n /**\n * Subscribe to toolbar state changes.\n *\n * @param {(state: ToolbarState) => void} callback - Function called when state changes\n * @returns {() => void} Unsubscribe function\n */\n subscribe(callback: (state: ToolbarState) => void): () => void;\n};\n\n/**\n * Global toolbar state manager - single source of truth for all formatting toolbars.\n * Implements the singleton pattern to ensure only one toolbar is visible at a time\n * across the entire application.\n *\n * @class GlobalToolbarManager\n * @implements {ToolbarStateManager}\n */\nclass GlobalToolbarManager implements ToolbarStateManager {\n private activeElementOnChange: ((e: React.ChangeEvent<TextInputElement>) => void) | undefined;\n\n private hideTimeout: NodeJS.Timeout | null = null;\n private state: ToolbarState = {\n activeElement: null,\n isVisible: false,\n position: null,\n };\n private subscribers: Set<(state: ToolbarState) => void> = new Set();\n\n applyFormat(formatter: FormatterFunction, onChange?: (e: React.ChangeEvent<TextInputElement>) => void): void {\n const { activeElement } = this.state;\n\n if (!activeElement) {\n console.warn('No active element found for formatting');\n return;\n }\n\n const newValue = applyFormattingOnSelection(activeElement, formatter);\n\n // Use provided onChange or the stored one from the element\n const onChangeHandler = onChange || this.activeElementOnChange;\n updateElementValue(activeElement, newValue, onChangeHandler);\n\n // Keep focus on the element after formatting\n activeElement.focus();\n }\n\n cancelScheduledHide(): void {\n this.clearHideTimeout();\n }\n\n getState(): ToolbarState {\n return { ...this.state };\n }\n\n hideToolbar(): void {\n this.clearHideTimeout();\n this.activeElementOnChange = undefined;\n this.setState({\n activeElement: null,\n isVisible: false,\n position: null,\n });\n }\n\n scheduleHide(delay: number): void {\n this.clearHideTimeout();\n this.hideTimeout = setTimeout(() => this.hideToolbar(), delay);\n }\n\n showToolbar(\n element: TextInputElement,\n getPosition: (element: TextInputElement) => ToolbarPosition = defaultGetPosition,\n ): void {\n this.clearHideTimeout();\n\n // Store the onChange handler from the element for later use\n const props = (element as any).props || {};\n this.activeElementOnChange = props.onChange;\n\n this.setState({\n activeElement: element,\n isVisible: true,\n position: getPosition(element),\n });\n }\n\n subscribe(callback: (state: ToolbarState) => void): () => void {\n this.subscribers.add(callback);\n return () => {\n this.subscribers.delete(callback);\n };\n }\n\n private clearHideTimeout(): void {\n if (this.hideTimeout) {\n clearTimeout(this.hideTimeout);\n this.hideTimeout = null;\n }\n }\n\n private setState(newState: Partial<ToolbarState>): void {\n this.state = { ...this.state, ...newState };\n this.subscribers.forEach((callback) => callback(this.getState()));\n }\n}\n\n/**\n * Global singleton instance of the toolbar manager.\n * This instance is shared across all components using the formatting toolbar.\n *\n * @example\n * ```typescript\n * import { globalToolbarManager } from './globalToolbarManager';\n *\n * // Show toolbar for an element\n * globalToolbarManager.showToolbar(textareaElement);\n *\n * // Apply formatting\n * globalToolbarManager.applyFormat((text) => text.toUpperCase());\n * ```\n */\nconst globalToolbarManager = new GlobalToolbarManager();\n\nexport { globalToolbarManager };\n","import { useCallback, useEffect, useState } from 'react';\n\nimport type { FormatterFunction, TextInputElement, ToolbarConfig, ToolbarState } from '@/types';\n\nimport { globalToolbarManager } from '@/utils/globalToolbarManager';\n\n/**\n * Default positioning function that places the toolbar below the focused element.\n *\n * @param {TextInputElement} element - The focused input or textarea element\n * @returns {ToolbarPosition} Position coordinates for the toolbar\n */\nconst defaultGetPosition = (element: TextInputElement) => {\n const rect = element.getBoundingClientRect();\n return {\n x: rect.left,\n y: rect.bottom + 5,\n };\n};\n\n/**\n * Return type for the useFormattingToolbar hook containing all toolbar functionality.\n *\n * @interface UseFormattingToolbarResult\n */\ntype UseFormattingToolbarResult = {\n /**\n * Apply formatting to the currently active element.\n *\n * @param {FormatterFunction} formatter - Function to transform the selected or entire text\n */\n applyFormat: (formatter: FormatterFunction) => void;\n\n /**\n * Props to spread on your input/textarea components.\n * Includes onFocus and onBlur handlers for toolbar management.\n *\n * @returns {object} Props object with focus and blur handlers\n */\n getInputProps: () => {\n onBlur: (e: React.FocusEvent<TextInputElement>) => void;\n onFocus: (e: React.FocusEvent<TextInputElement>) => void;\n };\n\n /**\n * Props for the toolbar container element.\n * Includes positioning styles and optional mouse event handlers.\n *\n * @returns {object} Props object with styles and event handlers\n */\n getToolbarProps: () => {\n onMouseDown?: (e: React.MouseEvent) => void;\n style: React.CSSProperties;\n };\n\n /** Function to manually hide the toolbar */\n hideToolbar: () => void;\n\n /** Whether the toolbar is currently visible */\n isVisible: boolean;\n\n /**\n * Function to manually show the toolbar for a specific element.\n *\n * @param {TextInputElement} element - The element to show the toolbar for\n */\n showToolbar: (element: TextInputElement) => void;\n\n /** Current toolbar state (shared globally across all instances) */\n toolbarState: ToolbarState;\n};\n\n/**\n * Hook for managing formatting toolbar functionality.\n * Uses global state so all instances share the same toolbar - only one toolbar\n * can be visible at a time across the entire application.\n *\n * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior\n * @returns {UseFormattingToolbarResult} Object containing toolbar state and control functions\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const { getInputProps, isVisible, applyFormat } = useFormattingToolbar({\n * hideDelay: 300,\n * getPosition: (element) => ({ x: 100, y: 200 })\n * });\n *\n * return (\n * <div>\n * <textarea {...getInputProps()} />\n * {isVisible && (\n * <div>\n * <button onClick={() => applyFormat(text => text.toUpperCase())}>\n * UPPERCASE\n * </button>\n * </div>\n * )}\n * </div>\n * );\n * }\n * ```\n */\nexport const useFormattingToolbar = (config: ToolbarConfig = {}): UseFormattingToolbarResult => {\n const { getPosition = defaultGetPosition, hideDelay = 500, preventCloseOnClick = true } = config;\n\n // Subscribe to global toolbar state\n const [toolbarState, setToolbarState] = useState<ToolbarState>(globalToolbarManager.getState());\n\n useEffect(() => {\n const unsubscribe = globalToolbarManager.subscribe(setToolbarState);\n return unsubscribe;\n }, []);\n\n const showToolbar = useCallback(\n (element: TextInputElement) => {\n globalToolbarManager.showToolbar(element, getPosition);\n },\n [getPosition],\n );\n\n const hideToolbar = useCallback(() => {\n globalToolbarManager.hideToolbar();\n }, []);\n\n const handleFocus = useCallback(\n (e: React.FocusEvent<TextInputElement>) => {\n showToolbar(e.currentTarget);\n },\n [showToolbar],\n );\n\n const handleBlur = useCallback(() => {\n globalToolbarManager.scheduleHide(hideDelay);\n }, [hideDelay]);\n\n const applyFormat = useCallback((formatter: FormatterFunction) => {\n globalToolbarManager.applyFormat(formatter);\n }, []);\n\n const getInputProps = useCallback(\n () => ({\n onBlur: handleBlur,\n onFocus: handleFocus,\n }),\n [handleFocus, handleBlur],\n );\n\n const getToolbarProps = useCallback(\n () => ({\n style: {\n left: toolbarState.position?.x ?? 0,\n position: 'fixed' as const,\n top: toolbarState.position?.y ?? 0,\n zIndex: 1000,\n },\n ...(preventCloseOnClick && {\n onMouseDown: (e: React.MouseEvent) => {\n e.preventDefault(); // Prevent blur event when clicking toolbar\n globalToolbarManager.cancelScheduledHide();\n },\n }),\n }),\n [toolbarState.position, preventCloseOnClick],\n );\n\n return {\n applyFormat,\n getInputProps,\n getToolbarProps,\n hideToolbar,\n isVisible: toolbarState.isVisible,\n showToolbar,\n toolbarState,\n };\n};\n","import React, { forwardRef } from 'react';\n\nimport type { TextInputElement, ToolbarConfig } from './types';\n\nimport { useFormattingToolbar } from './hooks/useFormattingToolbar';\n\n/**\n * Higher-order component that adds formatting toolbar functionality to input components.\n * Since the library uses global state, all wrapped components automatically share the same toolbar.\n * Only one toolbar will be visible at a time, appearing for whichever input is currently focused.\n *\n * @template P - The props type of the wrapped component\n * @param {React.ComponentType<P>} Component - The input component to enhance with toolbar functionality\n * @param {ToolbarConfig} [config={}] - Optional configuration for toolbar behavior\n * @returns {React.ForwardRefExoticComponent} Enhanced component with toolbar functionality\n *\n * @example\n * ```typescript\n * import { Textarea } from './ui/textarea';\n * import { withFormattingToolbar } from 'blumbaben';\n *\n * const TextareaWithToolbar = withFormattingToolbar(Textarea, {\n * hideDelay: 300,\n * getPosition: (element) => {\n * const rect = element.getBoundingClientRect();\n * return { x: rect.left, y: rect.top - 50 }; // Above the element\n * }\n * });\n *\n * // Usage\n * <TextareaWithToolbar\n * value={content}\n * onChange={setContent}\n * placeholder=\"Start typing...\"\n * />\n * ```\n */\nexport const withFormattingToolbar = <P extends Record<string, any>>(\n Component: React.ComponentType<P>,\n config: ToolbarConfig = {},\n) => {\n const WrappedComponent = forwardRef<TextInputElement, P>((props, ref) => {\n // All instances share the same global toolbar state\n const { getInputProps } = useFormattingToolbar(config);\n const toolbarProps = getInputProps();\n\n const handleFocusEvent = (e: React.FocusEvent<TextInputElement>) => {\n toolbarProps.onFocus(e);\n if (props.onFocus && typeof props.onFocus === 'function') {\n props.onFocus(e);\n }\n };\n\n const handleBlurEvent = (e: React.FocusEvent<TextInputElement>) => {\n toolbarProps.onBlur(e);\n if (props.onBlur && typeof props.onBlur === 'function') {\n props.onBlur(e);\n }\n };\n\n const enhancedProps = {\n ...props,\n onBlur: handleBlurEvent,\n onFocus: handleFocusEvent,\n ref,\n } as P & { ref: React.ForwardedRef<TextInputElement> };\n\n return React.createElement(Component, enhancedProps);\n });\n\n WrappedComponent.displayName = `withFormattingToolbar(${Component.displayName || Component.name})`;\n\n return WrappedComponent;\n};\n"],"mappings":"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","names":["React","useCallback","useEffect","useState","applyFormattingOnSelection","element","formatter","selectionEnd","selectionStart","value","before","selected","after","updateElementValue","newValue","onChange","defaultGetPosition","element","rect","GlobalToolbarManager","formatter","onChange","activeElement","newValue","applyFormattingOnSelection","onChangeHandler","updateElementValue","delay","getPosition","props","callback","newState","globalToolbarManager","useFormattingToolbarState","toolbarState","setToolbarState","useState","globalToolbarManager","useEffect","useCallback","formatter","FormattingToolbar","Component","children","className","config","style","applyFormat","toolbarState","useFormattingToolbarState","preventCloseOnClick","handleMouseDown","e","React","useCallback","useEffect","useState","defaultGetPosition","element","rect","useFormattingToolbar","config","getPosition","hideDelay","preventCloseOnClick","toolbarState","setToolbarState","useState","globalToolbarManager","useEffect","showToolbar","useCallback","hideToolbar","handleFocus","e","handleBlur","applyFormat","formatter","getInputProps","getToolbarProps","React","forwardRef","withFormattingToolbar","Component","config","WrappedComponent","forwardRef","props","ref","getInputProps","useFormattingToolbar","toolbarProps","enhancedProps","e","React"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blumbaben",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/ragaeeb/blumbaben.git"
|
|
6
|
+
},
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.30.1",
|
|
21
|
+
"@types/bun": "^1.2.17",
|
|
22
|
+
"@types/react": "^19.1.8",
|
|
23
|
+
"@types/react-dom": "^19.1.6",
|
|
24
|
+
"eslint": "^9.30.1",
|
|
25
|
+
"eslint-config-prettier": "^10.1.5",
|
|
26
|
+
"eslint-plugin-perfectionist": "^4.15.0",
|
|
27
|
+
"eslint-plugin-prettier": "^5.5.1",
|
|
28
|
+
"eslint-plugin-react": "^7.37.5",
|
|
29
|
+
"globals": "^16.3.0",
|
|
30
|
+
"prettier": "^3.6.2",
|
|
31
|
+
"semantic-release": "^24.2.6",
|
|
32
|
+
"tsup": "^8.5.0",
|
|
33
|
+
"typescript-eslint": "^8.35.1"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": "^19.1.0",
|
|
37
|
+
"react-dom": "^19.1.0"
|
|
38
|
+
},
|
|
39
|
+
"type": "module",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/ragaeeb/blumbaben/issues"
|
|
42
|
+
},
|
|
43
|
+
"description": "A lightweight TypeScript React hook to show a toolbar when an input is focused.",
|
|
44
|
+
"homepage": "https://github.com/ragaeeb/blumbaben",
|
|
45
|
+
"keywords": [
|
|
46
|
+
"textarea",
|
|
47
|
+
"multiline",
|
|
48
|
+
"input",
|
|
49
|
+
"forms",
|
|
50
|
+
"styling",
|
|
51
|
+
"formatting"
|
|
52
|
+
],
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup"
|
|
56
|
+
}
|
|
57
|
+
}
|