` elements indiscriminately.
47 |
48 | ## API
49 |
50 | ### hijack
51 |
52 | `linkHijacker.hijack([options], callback)`
53 |
54 | Returns a function that can be used to remove event listeners, unhijacking links.
55 | Calls the `callback` whenever a link is hijacked.
56 |
57 | #### options
58 |
59 | ##### root
60 |
61 | Type: `HtmlElement`. Default: `document.documentElement`.
62 |
63 | Links will be hijacked within this element.
64 |
65 | ##### skipModifierKeys
66 |
67 | Type: `boolean`. Default: `true`.
68 |
69 | By default, clicks paired with modifiers keys (`ctrlKey`, `altKey`, `metaKey`, `shiftKey`) are *not* hijacked.
70 | If this option is `false`, these clicks *will* be hijacked.
71 |
72 | ##### skipDownload
73 |
74 | Type: `boolean`. Default: `true`.
75 |
76 | By default, links with the `download` attribute are *not* hijacked.
77 | If this option is `false`, these links *will* be hijacked.
78 |
79 | ##### skipTargetBlank
80 |
81 | Type: `boolean`. Default: `true`.
82 |
83 | By default, links with the attribute `target="_blank"` are *not* hijacked.
84 | If this option is `false`, these links *will* be hijacked.
85 |
86 | ##### skipExternal
87 |
88 | Type: `boolean`. Default: `true`.
89 |
90 | By default, links with the attribute `rel="external"` are *not* hijacked.
91 | If this option is `false`, these links *will* be hijacked.
92 |
93 | ##### skipMailTo
94 |
95 | Type: `boolean`. Default: `true`.
96 |
97 | By default, links whose `href` attributes start with `mailto:` are *not* hijacked.
98 | If this option is `false`, these links *will* be hijacked.
99 |
100 | ##### skipOtherOrigin
101 |
102 | Type: `boolean`. Default: `true`.
103 |
104 | By default, links pointing to other origins (protocol + domain) are *not* hijacked.
105 | If this option is `false`, these links *will* be hijacked.
106 |
107 | ##### skipFragment
108 |
109 | Type: `boolean`. Default: `true`.
110 |
111 | By default, links with `href` attributes starting with fragments (e.g. `href="#foo"`) are *not* hijacked.
112 | (Links with `href` attributes that *include* fragments, but don't start with them, will still be hijacked, e.g. `href="some/page#foo"`.)
113 | If this option is `false`, these links *will* be hijacked.
114 |
115 | ##### skipFilter
116 |
117 | Type: `Function`.
118 |
119 | A filter function that receives the clicked link element and returns a truthy or falsey value indicating whether the link should be hijacked or not.
120 | If it returns a falsey value, the link will be hijacked.
121 | If the function returns a truthy value, the link will not be hijacked.
122 |
123 | ##### preventDefault
124 |
125 | Type: `boolean`. Default: `true`.
126 |
127 | By default, `event.preventDefault()` will be called on any click events that are hijacked (are *not* skipped).
128 | If this option is `false`, `event.preventDefault()` will *not* be called.
129 | You could let the event continue as normal, or prevent default behavior yourself.
130 |
131 | #### callback
132 |
133 | Type: `Function`.
134 | **Required.**
135 |
136 | Whenever a link is clicked, the `callback` will be invoked with two arguments:
137 |
138 | - `link`: The link element that was clicked on or within.
139 | - `event`: The `click` event.
140 |
141 | ## Similar work
142 |
143 | - [page.js](https://github.com/visionmedia/page.js/blob/1034c8cbed600ea7da378a73716c885227c03270/index.js#L541-L601)
144 | - [nanohref]( https://github.com/yoshuawuyts/nanohref/blob/4efcc2c0becd2822a31c912364997cf03c66ab8d/index.js)
145 | - [whir-tools/hijack-links](https://github.com/whir-tools/hijack-links)
146 |
147 | ["Similar work"]: #similar-work
148 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function getClosestLink(node, root) {
4 | if (!node || node === root) return;
5 | if ('a' !== node.nodeName.toLowerCase() || !node.href) {
6 | return getClosestLink(node.parentNode, root);
7 | }
8 | return node;
9 | }
10 |
11 | function setDefault(x, d) {
12 | return x === undefined ? d : x;
13 | }
14 |
15 | function hijack(options, callback) {
16 | if (typeof window === 'undefined') return;
17 | if (typeof options === 'function') {
18 | callback = options;
19 | }
20 | if (callback === undefined) {
21 | throw new Error('hijack requires a callback');
22 | }
23 | var root = setDefault(options.root, document.documentElement);
24 | var skipModifierKeys = setDefault(options.skipModifierKeys, true);
25 | var skipDownload = setDefault(options.skipDownload, true);
26 | var skipTargetBlank = setDefault(options.skipTargetBlank, true);
27 | var skipExternal = setDefault(options.skipExternal, true);
28 | var skipMailTo = setDefault(options.skipMailTo, true);
29 | var skipOtherOrigin = setDefault(options.skipOtherOrigin, true);
30 | var skipFragment = setDefault(options.skipFragment, true);
31 | var preventDefault = setDefault(options.preventDefault, true);
32 |
33 | function onClick(e) {
34 | if (e.defaultPrevented) return;
35 | if (e.button && e.button !== 0) return;
36 |
37 | if (
38 | skipModifierKeys &&
39 | (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey)
40 | ) {
41 | return;
42 | }
43 |
44 | var link = getClosestLink(e.target, root);
45 | if (!link) return;
46 |
47 | if (options.skipFilter && options.skipFilter(link)) return;
48 | if (skipFragment && /^#/.test(link.getAttribute('href'))) return;
49 | if (skipDownload && link.hasAttribute('download')) return;
50 | if (skipExternal && link.getAttribute('rel') === 'external') return;
51 | if (skipTargetBlank && link.getAttribute('target') === '_blank') return;
52 | if (skipMailTo && /mailto:/.test(link.getAttribute('href'))) return;
53 | // IE doesn't populate all link properties when setting href with a
54 | // relative URL. However, href will return an absolute URL which then can
55 | // be used on itself to populate these additional fields.
56 | // https://stackoverflow.com/a/13405933/2284669
57 | if (!link.host) link.href = link.href;
58 | if (
59 | skipOtherOrigin &&
60 | /:\/\//.test(link.href) &&
61 | (location.protocol !== link.protocol || location.host !== link.host)
62 | ) {
63 | return;
64 | }
65 |
66 | if (preventDefault) {
67 | e.preventDefault();
68 | }
69 | callback(link, e);
70 | }
71 |
72 | root.addEventListener('click', onClick);
73 | return function unhijackLinks() {
74 | root.removeEventListener('click', onClick);
75 | };
76 | }
77 |
78 | module.exports = {
79 | hijack: hijack
80 | };
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mapbox/link-hijacker",
3 | "version": "1.1.0",
4 | "description": "Hijack clicks on and within links, probably for client-side routing",
5 | "main": "index.js",
6 | "scripts": {
7 | "format": "prettier --single-quote --write '{,test/**/}*.js'",
8 | "precommit": "lint-staged",
9 | "lint": "eslint .",
10 | "test-manual": "budo test/manual/manual.js -l -d test/manual",
11 | "start": "npm run test-manual",
12 | "test-jest": "jest",
13 | "pretest": "npm run lint",
14 | "test": "jest"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/mapbox/link-hijacker.git"
19 | },
20 | "keywords": [
21 | "client-side-routing",
22 | "routing",
23 | "hijack",
24 | "links"
25 | ],
26 | "author": "Mapbox",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/mapbox/link-hijacker/issues"
30 | },
31 | "homepage": "https://github.com/mapbox/link-hijacker#readme",
32 | "devDependencies": {
33 | "budo": "^10.0.3",
34 | "eslint": "^4.1.0",
35 | "husky": "^0.14.1",
36 | "jest": "^20.0.4",
37 | "lint-staged": "^4.0.0",
38 | "prettier": "^1.4.4"
39 | },
40 | "lint-staged": {
41 | "**/*.js": [
42 | "eslint",
43 | "prettier --single-quote --write",
44 | "git add"
45 | ]
46 | },
47 | "jest": {
48 | "coverageReporters": [
49 | "text",
50 | "html"
51 | ],
52 | "clearMocks": true,
53 | "roots": [
54 | "./test"
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/manual/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | link-hijacker test
6 |
7 |
8 |
9 |
10 |
11 | link-hijacker test cases
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | just a link
20 |
21 |
27 |
28 |
29 |
30 |
31 | link with nested spans
32 |
33 |
47 |
48 |
49 |
50 |
51 | button nested inside a link
52 |
53 |
67 |
68 |
69 |
70 |
71 | image nested inside a link
72 |
73 |
85 |
86 |
87 |
88 |
89 | download link
90 |
91 |
97 |
98 |
99 |
100 |
101 | rel="external" link
102 |
103 |
109 |
110 |
111 |
112 |
113 | target="_blank" link
114 |
115 |
121 |
122 |
123 |
124 |
125 | mailto: link
126 |
127 |
133 |
134 |
135 |
136 |
137 | link to other origin
138 |
139 |
145 |
146 |
147 |
148 |
149 | default prevented
150 |
151 |
157 |
158 |
159 |
160 |
161 | matches skipFilter
162 |
163 |
169 |
170 |
171 |
172 |
173 | does not match skipFilter
174 |
175 |
181 |
182 |
183 |
184 |
185 | anchor without href
186 |
187 |
193 |
194 |
195 |
196 |
197 | anchor with fragment href
198 |
199 |
205 |
206 |
207 |
208 |
209 | anchor with href URL containing (but not starting with) fragment
210 |
211 |
217 |
218 |
219 |
220 |
221 | Default behavior is not prevented.
222 |
223 |
229 |
230 |
231 |
232 |
233 |
234 |
--------------------------------------------------------------------------------
/test/manual/manual.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var linkHijacker = require('../..');
4 |
5 | document
6 | .getElementById('default-prevented')
7 | .addEventListener('click', function(e) {
8 | e.preventDefault();
9 | });
10 |
11 | function logHijacking(link) {
12 | var countContainer = document.getElementById(link.id + '-count');
13 | var currentCount = Number(countContainer.innerText);
14 | var nextCount = currentCount + 1;
15 | countContainer.innerText = nextCount;
16 | }
17 |
18 | var stop = linkHijacker.hijack(logHijacking);
19 |
20 | var usingDefaults = true;
21 | var switchButton = document.getElementById('switch-options');
22 | switchButton.addEventListener('click', function() {
23 | if (usingDefaults === true) {
24 | stop();
25 | stop = linkHijacker.hijack(
26 | {
27 | skipModifierKeys: false,
28 | skipDownload: false,
29 | skipTargetBlank: false,
30 | skipExternal: false,
31 | skipMailTo: false,
32 | skipOtherOrigin: false,
33 | skipFragment: false,
34 | skipFilter: function(link) {
35 | return link.hasAttribute('data-no-hijack');
36 | }
37 | },
38 | logHijacking
39 | );
40 | usingDefaults = false;
41 | switchButton.innerText = 'use defaults';
42 | } else {
43 | stop();
44 | stop = linkHijacker.hijack(logHijacking);
45 | usingDefaults = true;
46 | switchButton.innerText = 'set options';
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const linkHijacker = require('..');
4 |
5 | describe('hijack', () => {
6 | let root;
7 | let link;
8 | let mockEvent;
9 | let remove;
10 |
11 | beforeEach(() => {
12 | root = {
13 | addEventListener: jest.fn(),
14 | removeEventListener: jest.fn()
15 | };
16 | link = global.document.createElement('a');
17 | link.href = 'about:/foo/bar';
18 | global.document.body.appendChild(link);
19 | mockEvent = {
20 | preventDefault: jest.fn(),
21 | target: link
22 | };
23 | });
24 |
25 | afterEach(() => {
26 | global.document.body.removeChild(link);
27 | remove();
28 | });
29 |
30 | test('adds a listener', () => {
31 | const options = { root };
32 | const callback = () => {};
33 | remove = linkHijacker.hijack(options, callback);
34 | expect(root.addEventListener).toHaveBeenCalledTimes(1);
35 | expect(root.addEventListener.mock.calls[0][0]).toBe('click');
36 | });
37 |
38 | test('can remove the listener', () => {
39 | const options = { root };
40 | const callback = () => {};
41 | remove = linkHijacker.hijack(options, callback);
42 | remove();
43 | const handler = root.addEventListener.mock.calls[0][1];
44 | expect(root.removeEventListener).toHaveBeenCalledTimes(1);
45 | expect(root.removeEventListener.mock.calls[0][0]).toBe('click');
46 | expect(root.removeEventListener.mock.calls[0][1]).toBe(handler);
47 | });
48 |
49 | test('hijacks links, preventing default', () => {
50 | let callbackCalled = false;
51 | remove = linkHijacker.hijack({ root }, (clickedLink, clickEvent) => {
52 | callbackCalled = true;
53 | expect(clickedLink).toBe(link);
54 | expect(clickEvent).toBe(mockEvent);
55 | });
56 | const handler = root.addEventListener.mock.calls[0][1];
57 | handler(mockEvent);
58 | expect(callbackCalled).toBe(true);
59 | expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
60 | });
61 |
62 | test('hijacks links when click is on nested element', () => {
63 | const nestedEl = global.document.createElement('div');
64 | link.appendChild(nestedEl);
65 | mockEvent.target = nestedEl;
66 | let callbackCalled = false;
67 | remove = linkHijacker.hijack({ root }, (clickedLink, clickEvent) => {
68 | callbackCalled = true;
69 | expect(clickedLink).toBe(link);
70 | expect(clickEvent).toBe(mockEvent);
71 | });
72 | const handler = root.addEventListener.mock.calls[0][1];
73 | handler(mockEvent);
74 | expect(callbackCalled).toBe(true);
75 | });
76 |
77 | test('skips defaultPrevented', () => {
78 | let callbackCalled = false;
79 | remove = linkHijacker.hijack({ root }, () => {
80 | callbackCalled = true;
81 | });
82 | const handler = root.addEventListener.mock.calls[0][1];
83 | mockEvent.defaultPrevented = true;
84 | handler(mockEvent);
85 | expect(callbackCalled).toBe(false);
86 | });
87 |
88 | test('skips right click', () => {
89 | let callbackCalled = false;
90 | remove = linkHijacker.hijack({ root }, () => {
91 | callbackCalled = true;
92 | });
93 | const handler = root.addEventListener.mock.calls[0][1];
94 | mockEvent.button = 1;
95 | handler(mockEvent);
96 | expect(callbackCalled).toBe(false);
97 | });
98 |
99 | test('skips ctrl key', () => {
100 | let callbackCalled = false;
101 | remove = linkHijacker.hijack({ root }, () => {
102 | callbackCalled = true;
103 | });
104 | const handler = root.addEventListener.mock.calls[0][1];
105 | mockEvent.ctrlKey = true;
106 | handler(mockEvent);
107 | expect(callbackCalled).toBe(false);
108 | });
109 |
110 | test('options.skipModifierKeys = false does not skip ctrl key', () => {
111 | let callbackCalled = false;
112 | remove = linkHijacker.hijack(
113 | {
114 | root,
115 | skipModifierKeys: false
116 | },
117 | () => {
118 | callbackCalled = true;
119 | }
120 | );
121 | const handler = root.addEventListener.mock.calls[0][1];
122 | mockEvent.ctrlKey = true;
123 | handler(mockEvent);
124 | expect(callbackCalled).toBe(true);
125 | });
126 |
127 | test('skips meta key', () => {
128 | let callbackCalled = false;
129 | remove = linkHijacker.hijack({ root }, () => {
130 | callbackCalled = true;
131 | });
132 | const handler = root.addEventListener.mock.calls[0][1];
133 | mockEvent.metaKey = true;
134 | handler(mockEvent);
135 | expect(callbackCalled).toBe(false);
136 | });
137 |
138 | test('options.skipModifierKeys = false does not skip meta key', () => {
139 | let callbackCalled = false;
140 | remove = linkHijacker.hijack(
141 | {
142 | root,
143 | skipModifierKeys: false
144 | },
145 | () => {
146 | callbackCalled = true;
147 | }
148 | );
149 | const handler = root.addEventListener.mock.calls[0][1];
150 | mockEvent.metaKey = true;
151 | handler(mockEvent);
152 | expect(callbackCalled).toBe(true);
153 | });
154 |
155 | test('skips alt key', () => {
156 | let callbackCalled = false;
157 | remove = linkHijacker.hijack({ root }, () => {
158 | callbackCalled = true;
159 | });
160 | const handler = root.addEventListener.mock.calls[0][1];
161 | mockEvent.altKey = true;
162 | handler(mockEvent);
163 | expect(callbackCalled).toBe(false);
164 | });
165 |
166 | test('options.skipModifierKeys = false does not skip alt key', () => {
167 | let callbackCalled = false;
168 | remove = linkHijacker.hijack(
169 | {
170 | root,
171 | skipModifierKeys: false
172 | },
173 | () => {
174 | callbackCalled = true;
175 | }
176 | );
177 | const handler = root.addEventListener.mock.calls[0][1];
178 | mockEvent.altKey = true;
179 | handler(mockEvent);
180 | expect(callbackCalled).toBe(true);
181 | });
182 |
183 | test('skips shift key', () => {
184 | let callbackCalled = false;
185 | remove = linkHijacker.hijack({ root }, () => {
186 | callbackCalled = true;
187 | });
188 | const handler = root.addEventListener.mock.calls[0][1];
189 | mockEvent.shiftKey = true;
190 | handler(mockEvent);
191 | expect(callbackCalled).toBe(false);
192 | });
193 |
194 | test('options.skipModifierKeys = false does not skip shift key', () => {
195 | let callbackCalled = false;
196 | remove = linkHijacker.hijack(
197 | {
198 | root,
199 | skipModifierKeys: false
200 | },
201 | () => {
202 | callbackCalled = true;
203 | }
204 | );
205 | const handler = root.addEventListener.mock.calls[0][1];
206 | mockEvent.shiftKey = true;
207 | handler(mockEvent);
208 | expect(callbackCalled).toBe(true);
209 | });
210 |
211 | test('skips elements with no link parent', () => {
212 | let callbackCalled = false;
213 | remove = linkHijacker.hijack({ root }, () => {
214 | callbackCalled = true;
215 | });
216 | const handler = root.addEventListener.mock.calls[0][1];
217 | const el = global.document.createElement('div');
218 | global.document.body.appendChild(el);
219 | mockEvent.target = el;
220 | handler(mockEvent);
221 | expect(callbackCalled).toBe(false);
222 | });
223 |
224 | test('skips download', () => {
225 | let callbackCalled = false;
226 | remove = linkHijacker.hijack({ root }, () => {
227 | callbackCalled = true;
228 | });
229 | const handler = root.addEventListener.mock.calls[0][1];
230 | link.setAttribute('download', true);
231 | handler(mockEvent);
232 | expect(callbackCalled).toBe(false);
233 | });
234 |
235 | test('options.skipDownload = false does not skip download', () => {
236 | let callbackCalled = false;
237 | remove = linkHijacker.hijack(
238 | {
239 | root,
240 | skipDownload: false
241 | },
242 | () => {
243 | callbackCalled = true;
244 | }
245 | );
246 | const handler = root.addEventListener.mock.calls[0][1];
247 | link.setAttribute('download', true);
248 | handler(mockEvent);
249 | expect(callbackCalled).toBe(true);
250 | });
251 |
252 | test('skips rel="external"', () => {
253 | let callbackCalled = false;
254 | remove = linkHijacker.hijack({ root }, () => {
255 | callbackCalled = true;
256 | });
257 | const handler = root.addEventListener.mock.calls[0][1];
258 | link.setAttribute('rel', 'external');
259 | handler(mockEvent);
260 | expect(callbackCalled).toBe(false);
261 | });
262 |
263 | test('options.skipExternal = false does not skip rel="external"', () => {
264 | let callbackCalled = false;
265 | remove = linkHijacker.hijack(
266 | {
267 | root,
268 | skipExternal: false
269 | },
270 | () => {
271 | callbackCalled = true;
272 | }
273 | );
274 | const handler = root.addEventListener.mock.calls[0][1];
275 | link.setAttribute('rel', 'external');
276 | handler(mockEvent);
277 | expect(callbackCalled).toBe(true);
278 | });
279 |
280 | test('skips target="_blank"', () => {
281 | let callbackCalled = false;
282 | remove = linkHijacker.hijack({ root }, () => {
283 | callbackCalled = true;
284 | });
285 | const handler = root.addEventListener.mock.calls[0][1];
286 | link.setAttribute('target', '_blank');
287 | handler(mockEvent);
288 | expect(callbackCalled).toBe(false);
289 | });
290 |
291 | test('options.skipTargetBlank = false does not skip target="_blank"', () => {
292 | let callbackCalled = false;
293 | remove = linkHijacker.hijack(
294 | {
295 | root,
296 | skipTargetBlank: false
297 | },
298 | () => {
299 | callbackCalled = true;
300 | }
301 | );
302 | const handler = root.addEventListener.mock.calls[0][1];
303 | link.setAttribute('target', '_blank');
304 | handler(mockEvent);
305 | expect(callbackCalled).toBe(true);
306 | });
307 |
308 | test('skips mailto', () => {
309 | let callbackCalled = false;
310 | remove = linkHijacker.hijack({ root }, () => {
311 | callbackCalled = true;
312 | });
313 | const handler = root.addEventListener.mock.calls[0][1];
314 | link.setAttribute('href', 'mailto:fake@gmail.com');
315 | handler(mockEvent);
316 | expect(callbackCalled).toBe(false);
317 | });
318 |
319 | test('options.skipMailTo = false does not skip mailto', () => {
320 | let callbackCalled = false;
321 | remove = linkHijacker.hijack(
322 | {
323 | root,
324 | skipMailTo: false
325 | },
326 | () => {
327 | callbackCalled = true;
328 | }
329 | );
330 | const handler = root.addEventListener.mock.calls[0][1];
331 | link.setAttribute('href', 'mailto:fake@gmail.com');
332 | handler(mockEvent);
333 | expect(callbackCalled).toBe(true);
334 | });
335 |
336 | test('skips links to another host', () => {
337 | let callbackCalled = false;
338 | remove = linkHijacker.hijack({ root }, () => {
339 | callbackCalled = true;
340 | });
341 | const handler = root.addEventListener.mock.calls[0][1];
342 | link.setAttribute('href', 'https://google.com');
343 | handler(mockEvent);
344 | expect(callbackCalled).toBe(false);
345 | });
346 |
347 | test('options.skipFilter', () => {
348 | let callbackCalled = false;
349 | remove = linkHijacker.hijack(
350 | {
351 | root,
352 | skipFilter: link => link.hasAttribute('data-no-hijack')
353 | },
354 | () => {
355 | callbackCalled = true;
356 | }
357 | );
358 | const handler = root.addEventListener.mock.calls[0][1];
359 | link.setAttribute('href', 'about:/path/to/place');
360 | link.setAttribute('data-no-hijack', '');
361 | handler(mockEvent);
362 | expect(callbackCalled).toBe(false);
363 | link.removeAttribute('data-no-hijack');
364 | handler(mockEvent);
365 | expect(callbackCalled).toBe(true);
366 | });
367 |
368 | test('skips anchor without href', () => {
369 | let callbackCalled = false;
370 | remove = linkHijacker.hijack({ root }, () => {
371 | callbackCalled = true;
372 | });
373 | const handler = root.addEventListener.mock.calls[0][1];
374 | link.removeAttribute('href');
375 | handler(mockEvent);
376 | expect(callbackCalled).toBe(false);
377 | });
378 |
379 | test('skips fragments', () => {
380 | let callbackCalled = false;
381 | remove = linkHijacker.hijack({ root }, () => {
382 | callbackCalled = true;
383 | });
384 | const handler = root.addEventListener.mock.calls[0][1];
385 | link.setAttribute('href', '#foo');
386 | handler(mockEvent);
387 | expect(callbackCalled).toBe(false);
388 | });
389 |
390 | test('does not skip URLs ending with fragments', () => {
391 | let callbackCalled = false;
392 | remove = linkHijacker.hijack({ root }, () => {
393 | callbackCalled = true;
394 | });
395 | const handler = root.addEventListener.mock.calls[0][1];
396 | link.setAttribute('href', '/foo/bar#baz');
397 | handler(mockEvent);
398 | expect(callbackCalled).toBe(true);
399 | });
400 |
401 | test('does not skip URLs ending with slash + fragments', () => {
402 | let callbackCalled = false;
403 | remove = linkHijacker.hijack({ root }, () => {
404 | callbackCalled = true;
405 | });
406 | const handler = root.addEventListener.mock.calls[0][1];
407 | link.setAttribute('href', '/foo/bar/#baz');
408 | handler(mockEvent);
409 | expect(callbackCalled).toBe(true);
410 | });
411 |
412 | test('options.skipFragment', () => {
413 | let callbackCalled = false;
414 | remove = linkHijacker.hijack({ root, skipFragment: false }, () => {
415 | callbackCalled = true;
416 | });
417 | const handler = root.addEventListener.mock.calls[0][1];
418 | link.setAttribute('href', '#foo');
419 | handler(mockEvent);
420 | expect(callbackCalled).toBe(true);
421 | });
422 |
423 | test('options.preventDefault true (default)', () => {
424 | let callbackCalled = false;
425 | remove = linkHijacker.hijack({ root }, () => {
426 | callbackCalled = true;
427 | });
428 | const handler = root.addEventListener.mock.calls[0][1];
429 | link.setAttribute('href', '/foo');
430 | handler(mockEvent);
431 | expect(callbackCalled).toBe(true);
432 | expect(mockEvent.preventDefault).toHaveBeenCalled();
433 | });
434 |
435 | test('options.preventDefault false', () => {
436 | let callbackCalled = false;
437 | remove = linkHijacker.hijack({ root, preventDefault: false }, () => {
438 | callbackCalled = true;
439 | });
440 | const handler = root.addEventListener.mock.calls[0][1];
441 | link.setAttribute('href', '/foo');
442 | handler(mockEvent);
443 | expect(callbackCalled).toBe(true);
444 | expect(mockEvent.preventDefault).not.toHaveBeenCalled();
445 | });
446 | });
447 |
--------------------------------------------------------------------------------