Skip to content

Commit 1230464

Browse files
committed
Add row-linker directive in Alpine.js
1 parent 8b1c791 commit 1230464

3 files changed

Lines changed: 316 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ with.
1010

1111
## My JavaScript Demos - I Love JavaScript!
1212

13+
* [Table Row Linker Directive In Alpine.js](https://bennadel.github.io/JavaScript-Demos/demos/row-linker)
1314
* [Sending Messages Across Documents With The Broadcast Channel API](https://bennadel.github.io/JavaScript-Demos/demos/broadcast-api)
1415
* [Animating DOM Rectangles Over Focused Elements In JavaScript](https://bennadel.github.io/JavaScript-Demos/demos/focus-box)
1516
* [Linking To A Disclosure (Details) Element](https://bennadel.github.io/JavaScript-Demos/demos/scroll-to-details)

demos/row-linker/index.htm

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>
6+
Table Row Linker Directive In Alpine.js
7+
</title>
8+
<link rel="stylesheet" type="text/css" href="./main.css" />
9+
</head>
10+
<body x-data> <!-- Note: you need the x-data directive to "activate" the document. -->
11+
12+
<h1>
13+
Table Row Linker Directive In Alpine.js
14+
</h1>
15+
16+
<table x-table-row-linker>
17+
<thead>
18+
<tr>
19+
<th>Name</th>
20+
<th>Info</th>
21+
<th>Category</th>
22+
<th>Actions</th>
23+
</tr>
24+
</thead>
25+
<tbody>
26+
<template x-for="i in 10">
27+
<tr>
28+
<td>
29+
<a :href="`#row-${i}-link-1`" class="isRowLinker">Row Link</a>
30+
</td>
31+
<td>
32+
More row info here...
33+
</td>
34+
<td>
35+
<a :href="`#row-${i}-link-2`">Another Anchor</a>
36+
</td>
37+
<td>
38+
<a :href="`#row-${i}-action-1`">Action 1</a> -
39+
<a :href="`#row-${i}-action-2`">Action 2</a>
40+
</td>
41+
</tr>
42+
</template>
43+
</tbody>
44+
</table>
45+
46+
<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>
47+
<script type="text/javascript">
48+
49+
/**
50+
* I make the rows in the table clickable. The primary gesture is to make the
51+
* `isRowLinker` anchor the one that is activated on row-click. But, a secondary
52+
* gesture, a whole cell will be make clickable if it contains a single link and no
53+
* other actionable elements (such as buttons).
54+
*/
55+
function TableRowLinkerDirective( element, metadata, framework ) {
56+
57+
var attribute = "x-table-row-linker";
58+
var selector = ".isRowLinker";
59+
60+
init();
61+
62+
// ---
63+
// LIFE-CYCLE METHODS.
64+
// ---
65+
66+
/**
67+
* I setup the directive.
68+
*/
69+
function init() {
70+
71+
framework.cleanup( destroy );
72+
element.addEventListener( "click", handleMouseEvent );
73+
74+
}
75+
76+
77+
/**
78+
* I tear down the directive.
79+
*/
80+
function destroy() {
81+
82+
element.removeEventListener( "click", handleMouseEvent );
83+
84+
}
85+
86+
// ---
87+
// PRIVATE METHODS.
88+
// ---
89+
90+
/**
91+
* I handle the mouse event on the table.
92+
*/
93+
function handleMouseEvent( event ) {
94+
95+
// If the event was fired programmatically, ignore it.
96+
if ( ! event.isTrusted ) {
97+
98+
return;
99+
100+
}
101+
102+
// Only process the main mouse button (ie, not if right-clicking).
103+
if ( event.button !== 0 ) {
104+
105+
return;
106+
107+
}
108+
109+
// Since we're using event delegation in this directive, it's possible
110+
// that another directive or event binding will have already been applied
111+
// (and should take precedence). If the event's default behavior has been
112+
// canceled, we don't want to interfere with that control flow.
113+
if ( event.defaultPrevented ) {
114+
115+
return;
116+
117+
}
118+
119+
var target = event.target;
120+
var cell = target.closest( "td" );
121+
122+
// If the event didn't originate from within a table cell interaction,
123+
// ignore. It must have come from a header or a caption.
124+
if ( ! cell ) {
125+
126+
console.warn( "IGNORE: Click outside TD." );
127+
return;
128+
129+
}
130+
131+
// If the event originated from an actionable element, ignore - the given
132+
// element already has a native behavior associated with it.
133+
if ( target.closest( "a, button" ) ) {
134+
135+
console.warn( "IGNORE: Target is an actionable element." );
136+
return;
137+
138+
}
139+
140+
var selection = window.getSelection();
141+
142+
// If the user highlights text in the table, it will often trigger a click
143+
// event. Let's ignore events in which a non-empty selection exists.
144+
if ( selection && ! selection.isCollapsed ) {
145+
146+
console.warn( "IGNORE: Selection detected." );
147+
return;
148+
149+
}
150+
151+
var linkNodes = cell.querySelectorAll( "a" );
152+
var actionableNodes = cell.querySelectorAll( "a, button" );
153+
154+
// If there's only a SINGLE LINK in the cell and NO OTHER actionable
155+
// elements, let's activate the link regardless of whether or not it's the
156+
// row-linker. This allows for slight miss-clicks on an isolated link.
157+
if (
158+
( linkNodes.length === 1 ) &&
159+
( actionableNodes.length === 1 )
160+
) {
161+
162+
console.info( "EXECUTE: Auto-clicking isolated link." );
163+
linkNodes[ 0 ].click();
164+
return;
165+
166+
}
167+
168+
// If there are any actionable elements in the cell, don't do anything at
169+
// this point since this event represents a miss-click in a non-isolated
170+
// context. We don't want to activate the wrong link or button.
171+
if ( actionableNodes.length ) {
172+
173+
console.warn( "IGNORE: Multiple miss-click targets available." );
174+
return;
175+
176+
}
177+
178+
var row = cell.closest( "tr" );
179+
var rowLinker = row.querySelector( selector );
180+
181+
// If there's no row linker, then no further action can be taken.
182+
if ( ! rowLinker ) {
183+
184+
return;
185+
186+
}
187+
188+
// Translate row event into link event.
189+
console.info( "EXECUTE: Auto-clicking row-linker." );
190+
rowLinker.click();
191+
192+
}
193+
194+
}
195+
196+
// --------------------------------------------------------------------------- //
197+
// --------------------------------------------------------------------------- //
198+
199+
// Register Alpine.js directive.
200+
document.addEventListener(
201+
"alpine:init",
202+
function setupAlpineBindings() {
203+
204+
Alpine.directive( "table-row-linker", TableRowLinkerDirective );
205+
206+
}
207+
);
208+
209+
// For the UX of the demo, I want to highlight whichever link matches the hash.
210+
// This way, as the user clicks around, it will be clear which link has been
211+
// activated (either naturally or programmatically).
212+
window.addEventListener( "hashchange", ( event ) => {
213+
214+
for ( var node of document.querySelectorAll( "a[href]" ) ) {
215+
216+
if ( location.hash === node.getAttribute( "href" ) ) {
217+
218+
node.classList.add( "active" );
219+
220+
} else {
221+
222+
node.classList.remove( "active" );
223+
224+
}
225+
226+
}
227+
228+
});
229+
230+
</script>
231+
232+
</body>
233+
</html>

demos/row-linker/main.css

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
2+
:where( html ) {
3+
box-sizing: border-box ;
4+
5+
& *,
6+
& *:before,
7+
& *:after {
8+
box-sizing: inherit ;
9+
}
10+
}
11+
12+
:where( * ) {
13+
&:focus,
14+
&:focus-visible {
15+
animation-duration: 200ms ;
16+
animation-fill-mode: forwards ;
17+
animation-iteration-count: 1 ;
18+
animation-name: outlineEnter ;
19+
animation-timing-function: ease-out ;
20+
outline-color: hotpink ;
21+
outline-offset: 4px ;
22+
outline-width: 2px ;
23+
}
24+
}
25+
26+
@keyframes outlineEnter {
27+
from {
28+
outline-offset: 8px ;
29+
}
30+
to {
31+
outline-offset: 4px ;
32+
}
33+
}
34+
35+
body {
36+
font-family: Avenir, Montserrat, Corbel, URW Gothic, source-sans-pro, sans-serif ;
37+
font-size: 18px ;
38+
line-height: 1.4 ;
39+
}
40+
41+
button,
42+
input:where([type="text"]),
43+
select,
44+
textarea {
45+
color: inherit ;
46+
font-family: inherit ;
47+
font-size: 20px ;
48+
line-height: inherit ;
49+
padding: 5px 10px ;
50+
}
51+
52+
button {
53+
padding: 5px 15px ;
54+
}
55+
56+
a {
57+
color: red ;
58+
}
59+
60+
table {
61+
border: 1px solid #333333 ;
62+
border-collapse: collapse ;
63+
width: 100% ;
64+
65+
& :where(th, td) {
66+
border: 1px solid #333333 ;
67+
padding: 5px 10px ;
68+
69+
&:not([align]) {
70+
text-align: left ;
71+
}
72+
}
73+
}
74+
75+
tbody tr:hover {
76+
background-color: #e7f9ff ;
77+
}
78+
79+
a.active {
80+
background-color: #333333 ;
81+
color: #fafafa ;
82+
}

0 commit comments

Comments
 (0)