|
2 | 2 |
|
3 | 3 | Tiny, CDN-friendly helpers to make jQuery traversal and ergonomics snappier — no widgets, just micro-utilities.
|
4 | 4 |
|
| 5 | +## Why? |
| 6 | + |
| 7 | +jQuery is great, but sometimes you need: |
| 8 | +- **First match per element** (not all matches or globally first) |
| 9 | +- **Short-circuit traversal** (stop at first match, not collect all) |
| 10 | +- **Predicate functions** (not just CSS selectors) |
| 11 | +- **Modern conveniences** (tap, viewport detection) |
| 12 | + |
| 13 | +This adds ~1KB of focused utilities without the bloat of jQuery UI or larger plugins. |
| 14 | + |
5 | 15 | ## Install
|
6 | 16 |
|
7 | 17 | ```html
|
8 | 18 | <script src="https://code.jquery.com/jquery-3.7.1.slim.min.js"></script>
|
9 |
| -<script src="https://cdn.jsdelivr.net/gh/AnswerDotAI/jquery-micro-utils/src/jquery-micro-utils.js"></script> |
| 19 | +<script src="https://cdn.jsdelivr.net/gh/AnswerDotAI/jquery-micro-utils@0.1.2/src/jquery-micro-utils.js"></script> |
10 | 20 | ```
|
11 | 21 |
|
12 |
| -UMD build works with `<script>`, AMD, or CommonJS. |
| 22 | +Or use npm/yarn: |
| 23 | +```bash |
| 24 | +npm install jquery-micro-utils |
| 25 | +``` |
13 | 26 |
|
14 | 27 | ## API
|
15 | 28 |
|
16 |
| -### `.nextMatch(selectorOrFn)` |
| 29 | +### `.nextMatch(selectorOrFn)` / `.prevMatch(selectorOrFn)` |
17 | 30 |
|
18 |
| -**What:** First following sibling that matches. Short-circuits (faster than `.nextAll().first()`). |
| 31 | +Find the **first** sibling that matches (not all). Accepts CSS selector or predicate function. |
19 | 32 |
|
20 | 33 | ```js
|
21 |
| -const $bar = $('.foo').nextMatch('.bar'); |
22 |
| -if ($bar.exists) { |
23 |
| - $bar.addClass('hit'); |
24 |
| -} |
25 |
| -``` |
26 |
| - |
27 |
| -### `.prevMatch(selectorOrFn)` |
| 34 | +// Find next sibling with 10+ characters (can't do this with jQuery's .next()) |
| 35 | +$('.item').nextMatch(el => el.textContent.length > 10); |
28 | 36 |
|
29 |
| -**What:** First previous sibling that matches (short-circuits). |
| 37 | +// More efficient than .nextAll('.active').first() - stops at first match |
| 38 | +$('.current').nextMatch('.active').addClass('highlight'); |
30 | 39 |
|
31 |
| -```js |
32 |
| -$('#item42').prevMatch('.active').tap($x => console.log($x[0])); |
| 40 | +// Works backwards too |
| 41 | +$('#item-5').prevMatch('.section-header').css('font-weight', 'bold'); |
33 | 42 | ```
|
34 | 43 |
|
| 44 | +**vs jQuery:** `.next()` only checks immediate sibling. `.nextAll()` gets ALL matches. `nextMatch()` finds FIRST match per element. |
| 45 | + |
35 | 46 | ### `.findFirst(selector)`
|
36 | 47 |
|
37 |
| -**What:** First descendant under each root via `querySelector`. |
| 48 | +Get the **first descendant** from each element (not all descendants). |
38 | 49 |
|
39 | 50 | ```js
|
40 |
| -const $btn = $('.card').findFirst('button.primary'); |
| 51 | +// Get first button from EACH card (not just the globally first) |
| 52 | +$('.card').findFirst('button').prop('disabled', true); |
| 53 | + |
| 54 | +// Faster than .find('img').first() when you only need one per container |
| 55 | +$('.gallery-row').findFirst('img').addClass('hero'); |
41 | 56 | ```
|
42 | 57 |
|
| 58 | +**vs jQuery:** `.find()` returns ALL matches. `.find().first()` returns globally first. `findFirst()` returns first match FROM EACH element. |
| 59 | + |
43 | 60 | ### `.containsText(str|RegExp)`
|
44 | 61 |
|
45 |
| -**What:** Filter elements by `textContent` (string contains or RegExp test). |
| 62 | +Filter elements by text content. |
46 | 63 |
|
47 | 64 | ```js
|
48 |
| -const $matches = $('li').containsText(/done|complete/i); |
| 65 | +// String contains |
| 66 | +$('button').containsText('Save').addClass('primary'); |
| 67 | + |
| 68 | +// RegExp for complex matching |
| 69 | +$('tr').containsText(/^(error|warning):/i).css('color', 'red'); |
| 70 | + |
| 71 | +// Find elements with specific price ranges |
| 72 | +$('.price').containsText(/\$[5-9][0-9]\./).addClass('mid-range'); |
49 | 73 | ```
|
50 | 74 |
|
51 | 75 | ### `.inViewport(margin = 0)`
|
52 | 76 |
|
53 |
| -**What:** Elements whose bounding rect intersects the viewport (±margin px). |
| 77 | +Filter to elements currently visible in viewport. |
54 | 78 |
|
55 | 79 | ```js
|
56 |
| -const $visible = $('.section').inViewport(100); |
| 80 | +// Lazy-load images as they come into view |
| 81 | +$('img[data-src]').inViewport(200).each(function() { |
| 82 | + this.src = this.dataset.src; |
| 83 | + delete this.dataset.src; |
| 84 | +}); |
| 85 | + |
| 86 | +// Animate elements when visible |
| 87 | +$('.animate-me').inViewport().addClass('fade-in'); |
| 88 | + |
| 89 | +// Check what sections user can see |
| 90 | +$('section').inViewport().tap(console.log); |
57 | 91 | ```
|
58 | 92 |
|
59 | 93 | ### `.tap(fn)`
|
60 | 94 |
|
61 |
| -**What:** Chain-tap for debugging/side-effects; returns the same jQuery set. |
| 95 | +Debug or side-effect without breaking the chain. |
62 | 96 |
|
63 | 97 | ```js
|
64 |
| -$('.item').tap($it => console.log('count:', $it.length)).addClass('seen'); |
| 98 | +$('.item') |
| 99 | + .tap($els => console.log(`Processing ${$els.length} items`)) |
| 100 | + .addClass('processed') |
| 101 | + .tap($els => analytics.track('items.processed', $els.length)) |
| 102 | + .fadeIn(); |
| 103 | + |
| 104 | +// Quick debugging |
| 105 | +$('.mystery').tap(console.log).remove(); |
65 | 106 | ```
|
66 | 107 |
|
67 | 108 | ### `$.as$(x)`
|
68 | 109 |
|
69 |
| -**What:** Normalize DOM or jQuery input to a jQuery object. |
| 110 | +Safely convert anything to jQuery object. |
70 | 111 |
|
71 | 112 | ```js
|
72 |
| -const $el = $.as$(maybeDomOrJq); |
| 113 | +function process(element) { |
| 114 | + // Don't worry if element is DOM node, jQuery object, or selector string |
| 115 | + const $el = $.as$(element); |
| 116 | + $el.addClass('processed'); |
| 117 | +} |
| 118 | + |
| 119 | +process(document.querySelector('.item')); // DOM node - works |
| 120 | +process($('.item')); // jQuery object - works |
| 121 | +process('.item'); // Selector string - works |
73 | 122 | ```
|
74 | 123 |
|
75 |
| -## Development |
| 124 | +## Real-World Examples |
76 | 125 |
|
77 |
| -```bash |
78 |
| -npm i |
| 126 | +### Accordion behavior |
| 127 | +```js |
| 128 | +$('.accordion-header').on('click', function() { |
| 129 | + $(this) |
| 130 | + .nextMatch('.accordion-content') |
| 131 | + .slideToggle() |
| 132 | + .tap($content => console.log('Toggled:', $content)); |
| 133 | +}); |
79 | 134 | ```
|
80 | 135 |
|
81 |
| -### Manual Demo |
| 136 | +### Progressive enhancement |
| 137 | +```js |
| 138 | +// Add "Read more" only to truncated content |
| 139 | +$('.content').containsText('...').after('<button>Read more</button>'); |
| 140 | +``` |
82 | 141 |
|
83 |
| -Open `examples/index.html` in any browser to try the utilities against a small fixture page (no build or server required). It loads jQuery 3.x from the CDN and the UMD source from `src/`. |
| 142 | +### Performance monitoring |
| 143 | +```js |
| 144 | +// Track which sections users actually see |
| 145 | +const viewed = new Set(); |
| 146 | +setInterval(() => { |
| 147 | + $('section[id]').inViewport().each(function() { |
| 148 | + if (!viewed.has(this.id)) { |
| 149 | + viewed.add(this.id); |
| 150 | + analytics.track('section.viewed', { id: this.id }); |
| 151 | + } |
| 152 | + }); |
| 153 | +}, 1000); |
| 154 | +``` |
84 | 155 |
|
85 |
| -Or run a tiny static server: |
| 156 | +## Development |
86 | 157 |
|
87 | 158 | ```bash
|
88 |
| -npm run serve |
| 159 | +npm install |
| 160 | +npm run serve # localhost:8080/examples/ |
89 | 161 | ```
|
90 | 162 |
|
91 |
| -Then open `http://localhost:8080/examples/`. |
92 |
| - |
93 |
| -## Files |
94 |
| - |
95 |
| -- `src/jquery-micro-utils.js` – source (UMD) |
96 |
| -- `examples/index.html` – manual browser demo |
97 |
| - |
98 |
| -## Metadata |
99 |
| - |
100 |
| -- `$.microUtils.version`: library version string. |
101 |
| - |
102 | 163 | ## License
|
103 | 164 |
|
104 |
| -MIT, © Answer.AI 2025 |
| 165 | +MIT © Answer.AI 2025 |
| 166 | + |
0 commit comments