Skip to content

Commit 68061cc

Browse files
committed
Refactor core implementation to factory-based API (v0.3.0)
BREAKING CHANGES: - Change from singleton object to factory function pattern - Remove reference counting mechanism - Replace setTimeout() method with lock() options parameter - Remove unlock(abort) parameter - Add scope filtering (inside, outside, self) - Add element-specific locks with WeakMap caching Implementation details: - blokr.ts: Factory function returns cached instances per target - lock.ts: Filter-based event blocking with Set of filter functions - Add JSDoc comments for all public and internal methods
1 parent f845ac7 commit 68061cc

2 files changed

Lines changed: 105 additions & 78 deletions

File tree

src/blokr.ts

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,95 @@
11
import lock from './lock.ts';
2+
import type { Filter } from './lock.ts';
3+
4+
export type Scope = 'inside' | 'outside' | 'self';
5+
6+
export interface Options {
7+
scope?: Scope;
8+
timeout?: number;
9+
}
10+
11+
const blokrs = new WeakMap<Element | typeof globalThis, Blokr>();
212

313
class Blokr {
4-
private _timeout: number;
14+
private _target: Element | undefined;
515

6-
private _timerId: number;
16+
private _timerId: number | undefined;
717

8-
private _counter: number;
18+
private _filter: Filter | undefined;
919

1020
/**
1121
* Creates the Blokr singleton instance.
1222
*/
13-
constructor () {
14-
this._timeout = 10000; // Default timeout of 10 seconds
15-
this._timerId = 0;
16-
this._counter = 0;
23+
constructor (target?: Element) {
24+
this._target = target;
25+
this._timerId = undefined;
1726
}
1827

1928
/**
20-
* Prevents user interactions.
29+
* Locks user interactions with optional timeout and scope configuration.
30+
* Returns false if already locked without making any changes.
31+
* @param [options] - Lock configuration options.
32+
* @returns true if lock was applied, false if already locked.
2133
*/
22-
lock () {
23-
lock.on();
24-
this._counter++;
25-
26-
if (this._timerId) {
27-
self.clearTimeout(this._timerId);
28-
this._timerId = 0;
34+
lock (options?: Options) {
35+
if (this.isLocked()) {
36+
return false;
2937
}
30-
if (this._timeout) {
31-
this._timerId = self.setTimeout(() => {
32-
this._timerId = 0;
33-
this._counter = 0;
34-
lock.off();
35-
}, this._timeout);
38+
39+
const scope = options?.scope ?? 'inside';
40+
const timeout = options?.timeout ?? 0;
41+
42+
this._filter = (eventTarget: Element) => {
43+
if (this._target) {
44+
if (scope === 'self') {
45+
return this._target === eventTarget;
46+
}
47+
const contains = this._target.contains(eventTarget);
48+
// For 'outside' scope, block events outside target; otherwise block events inside target
49+
return scope === 'outside' ? !contains : contains;
50+
}
51+
// No target specified: block all events
52+
return true;
53+
};
54+
lock.register(this._filter);
55+
56+
if (timeout > 0) {
57+
this._timerId = globalThis.setTimeout(() => this.unlock(), timeout);
3658
}
59+
60+
return true;
3761
}
3862

3963
/**
40-
* Checks if user interactions are currently prevented.
41-
* @returns {boolean} Returns true if interactions are blocked, false otherwise.
64+
* Checks if user interactions are currently locked.
65+
* @returns true if locked, false otherwise.
4266
*/
4367
isLocked () {
44-
return this._counter > 0;
68+
return !!this._filter;
4569
}
4670

4771
/**
48-
* Sets the timeout duration for automatic unlock.
49-
* @param timeout - The timeout in milliseconds. Set to 0 to disable automatic unlock. Negative values are treated as 0.
50-
* @returns {boolean} Returns true if the timeout was set successfully, false if currently locked.
72+
* Unlocks user interactions and clears any pending timeout.
73+
* Safe to call even when not locked.
5174
*/
52-
setTimeout(timeout: number) {
53-
if (!this.isLocked()) {
54-
this._timeout = timeout < 0 ? 0 : timeout;
55-
return true;
75+
unlock () {
76+
if (this._timerId) {
77+
globalThis.clearTimeout(this._timerId);
78+
this._timerId = undefined;
5679
}
57-
return false;
58-
}
59-
60-
/**
61-
* Decrements the internal counter and releases the lock when the counter reaches zero.
62-
* If abort is true, the counter is reset to zero immediately, effectively releasing the lock.
63-
* Clears any pending timeout and triggers the unlock event.
64-
* @param abort - If true, immediately resets the counter to zero and releases the lock
65-
*/
66-
unlock (abort = false) {
67-
if (this._counter > 0) {
68-
this._counter--;
69-
70-
if (abort) {
71-
this._counter = 0;
72-
}
73-
if (this._counter === 0) {
74-
if (this._timerId) {
75-
self.clearTimeout(this._timerId);
76-
this._timerId = 0;
77-
}
78-
lock.off();
79-
}
80+
if (this._filter) {
81+
lock.unregister(this._filter);
8082
}
83+
this._filter = undefined;
8184
}
8285
}
8386

84-
export default new Blokr();
87+
const blokr = (target?: Element) => {
88+
return blokrs.get(target ?? globalThis) ?? (() => {
89+
const instance = new Blokr(target);
90+
blokrs.set(target ?? globalThis, instance);
91+
return instance;
92+
})();
93+
};
94+
95+
export default blokr;

src/lock.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,58 @@
1+
export type Filter = (eventTarget: Element) => boolean;
2+
13
const eventNames = [
24
'contextmenu', 'keydown', 'mousedown', 'touchmove', 'touchstart', 'wheel'
35
];
46

5-
const options = { capture: true, passive: false };
6-
77
class Lock {
8-
private _locked: boolean;
8+
private _filters: Set<Filter>;
99

10+
/**
11+
* Creates the Lock singleton instance.
12+
*/
1013
constructor () {
11-
this._locked = false;
12-
this._listener = this._listener.bind(this);
13-
if (typeof self !== 'undefined') {
14-
eventNames.forEach(eventName => self.addEventListener(eventName, this._listener, options));
14+
this._filters = new Set();
15+
16+
if ('addEventListener' in globalThis) {
17+
eventNames.forEach(eventName => globalThis.addEventListener(
18+
eventName,
19+
this._listener.bind(this),
20+
{ capture: true, passive: false }
21+
));
1522
}
1623
}
1724

25+
/**
26+
* Blocks user interactions when the lock is active.
27+
* @param evt - The event to be blocked.
28+
*/
1829
private _listener (evt: Event) {
19-
if (this._locked) {
20-
evt.stopImmediatePropagation();
21-
evt.stopPropagation();
22-
evt.preventDefault();
30+
if (evt.target instanceof Element) {
31+
for (const filter of this._filters.values()) {
32+
if (filter(evt.target)) {
33+
evt.stopImmediatePropagation();
34+
evt.stopPropagation();
35+
evt.preventDefault();
36+
break;
37+
}
38+
}
2339
}
2440
}
2541

26-
on () {
27-
if (this._locked) {
28-
return false;
29-
}
30-
this._locked = true;
31-
32-
return true;
42+
/**
43+
* Registers a filter function to block events matching the filter criteria.
44+
* @param filter - Filter function that determines which events to block.
45+
*/
46+
register (filter: Filter) {
47+
this._filters.add(filter);
3348
}
3449

35-
off () {
36-
if (!this._locked) {
37-
return;
38-
}
39-
this._locked = false;
50+
/**
51+
* Unregisters a previously registered filter function.
52+
* @param filter - The filter function to remove.
53+
*/
54+
unregister (filter: Filter) {
55+
this._filters.delete(filter);
4056
}
4157
}
4258

0 commit comments

Comments
 (0)