universal-readonly-rest 0.1.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/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # universal-readonly
2
+
3
+ Universal readonly / interaction-control layer for web applications.
4
+
5
+ This package lets you lock a page or a section of the DOM into a read-only state while still allowing safe viewing actions (scrolling, reading, selection, copying, etc.) and optionally permitting some interactions such as links or keyboard input.
6
+
7
+ It is framework-agnostic and works in plain browser code, React, Vue, or any app that can run JavaScript in the DOM.
8
+
9
+ Installation
10
+
11
+ ```bash
12
+ npm install universal-readonly-rest
13
+ ```
14
+
15
+ Basic usage
16
+
17
+ ```ts
18
+ import { readonlyMode } from 'universal-readonly-rest';
19
+
20
+ // Enable globally for the entire page
21
+ readonlyMode.enable();
22
+
23
+ // Enable only for one form or section
24
+ readonlyMode.enable({ selector: '#order-form' });
25
+
26
+ // Enable with custom permissions
27
+ readonlyMode.enable({
28
+ selector: '#order-form',
29
+ allow: {
30
+ scroll: true,
31
+ selection: true,
32
+ copy: true,
33
+ links: false,
34
+ download: false,
35
+ focus: true,
36
+ contextMenu: false,
37
+ keyboard: false
38
+ }
39
+ });
40
+
41
+ // Disable all readonly behavior
42
+ readonlyMode.disable();
43
+
44
+ // Toggle between on/off
45
+ readonlyMode.toggle();
46
+
47
+ // Check if the readonly mode is active
48
+ console.log(readonlyMode.isEnabled());
49
+ ```
50
+
51
+ Options
52
+
53
+ `readonlyMode.enable(options?)` and `readonlyMode.toggle(options?)` accept:
54
+
55
+ ```ts
56
+ interface ReadonlyOptions {
57
+ selector?: string | string[];
58
+ allow?: {
59
+ scroll?: boolean;
60
+ selection?: boolean;
61
+ copy?: boolean;
62
+ links?: boolean;
63
+ download?: boolean;
64
+ focus?: boolean;
65
+ contextMenu?: boolean;
66
+ keyboard?: boolean;
67
+ };
68
+ }
69
+ ```
70
+
71
+ - `selector`: CSS selector or list of selectors to scope readonly mode. If omitted, the package applies to the whole document.
72
+ - `allow`: fine-grained control over what behavior is still permitted while readonly is active.
73
+
74
+ Default allow configuration
75
+
76
+ ```ts
77
+ {
78
+ scroll: true,
79
+ selection: true,
80
+ copy: true,
81
+ links: true,
82
+ download: true,
83
+ focus: true,
84
+ contextMenu: true,
85
+ keyboard: false
86
+ }
87
+ ```
88
+
89
+ What gets blocked
90
+
91
+ When readonly mode is active, the library listens to DOM events in the capture phase and prevents state-changing actions such as:
92
+
93
+ - form submit
94
+ - button click
95
+ - input/change events
96
+ - paste/cut/drop
97
+ - context menu
98
+ - keyboard editing while `keyboard: false`
99
+
100
+ The default behavior is to keep viewing and reading actions available while blocking edits and interactive mutations.
101
+
102
+ React example
103
+
104
+ ```tsx
105
+ import React, { useEffect } from 'react';
106
+ import { readonlyMode } from 'universal-readonly-rest';
107
+
108
+ export function OrderFormDemo() {
109
+ useEffect(() => {
110
+ readonlyMode.enable({
111
+ selector: '#order-form',
112
+ allow: {
113
+ scroll: true,
114
+ selection: true,
115
+ copy: true,
116
+ links: false,
117
+ download: false,
118
+ focus: true,
119
+ contextMenu: false,
120
+ keyboard: false
121
+ }
122
+ });
123
+
124
+ return () => readonlyMode.disable();
125
+ }, []);
126
+
127
+ return (
128
+ <form id="order-form">
129
+ <input name="name" placeholder="Name" />
130
+ <button type="submit">Submit</button>
131
+ </form>
132
+ );
133
+ }
134
+ ```
135
+
136
+ In this example, only the form with id `order-form` is locked. Other parts of the app remain interactive unless they match the selector.
137
+
138
+ Notes
139
+
140
+ - The package is intentionally small and browser-oriented.
141
+ - It hooks into DOM events on the capture phase to stop interaction before it reaches the target element.
142
+ - Because it works with selectors, you can apply readonly mode to a single form, a section, or the entire page.
143
+ - If you want all interactions disabled, set all `allow` values to `false` for a strict lock.
144
+
145
+ Example: strict read-only mode
146
+
147
+ ```ts
148
+ readonlyMode.enable({
149
+ allow: {
150
+ scroll: false,
151
+ selection: false,
152
+ copy: false,
153
+ links: false,
154
+ download: false,
155
+ focus: false,
156
+ contextMenu: false,
157
+ keyboard: false
158
+ }
159
+ });
160
+ ```
161
+
162
+ This effectively prevents most user-driven state changes while leaving the page visually readable (depending on browser behavior and the element set you target).
@@ -0,0 +1,16 @@
1
+ import { ReadonlyOptions } from './types';
2
+ declare class ReadonlyMode {
3
+ private enabled;
4
+ private roots;
5
+ private options;
6
+ private listeners;
7
+ enable(options?: ReadonlyOptions): void;
8
+ disable(): void;
9
+ toggle(options?: ReadonlyOptions): void;
10
+ isEnabled(): boolean;
11
+ private normalizeSelectors;
12
+ private isEventInRoots;
13
+ private isEditable;
14
+ }
15
+ export declare const readonlyMode: ReadonlyMode;
16
+ export default readonlyMode;
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ const DEFAULT_ALLOW = {
2
+ scroll: true,
3
+ selection: true,
4
+ copy: true,
5
+ links: true,
6
+ download: true,
7
+ focus: true,
8
+ contextMenu: true,
9
+ keyboard: false
10
+ };
11
+ class ReadonlyMode {
12
+ constructor() {
13
+ this.enabled = false;
14
+ this.roots = [];
15
+ this.options = {};
16
+ this.listeners = new Map();
17
+ }
18
+ enable(options) {
19
+ if (this.enabled)
20
+ return;
21
+ this.enabled = true;
22
+ this.options = options || {};
23
+ const allow = { ...DEFAULT_ALLOW, ...(this.options.allow || {}) };
24
+ // Resolve roots
25
+ const selectors = this.normalizeSelectors(this.options.selector);
26
+ if (selectors.length === 0) {
27
+ this.roots = [document.documentElement];
28
+ }
29
+ else {
30
+ const set = new Set();
31
+ for (const sel of selectors) {
32
+ try {
33
+ document.querySelectorAll(sel).forEach(el => set.add(el));
34
+ }
35
+ catch (err) {
36
+ // ignore invalid selectors
37
+ }
38
+ }
39
+ this.roots = Array.from(set);
40
+ }
41
+ // Attach capturing listeners to intercept state-changing interactions
42
+ const capture = (e) => {
43
+ if (!this.isEventInRoots(e))
44
+ return;
45
+ if (e.type === 'submit') {
46
+ // Block form submissions
47
+ e.preventDefault();
48
+ e.stopImmediatePropagation();
49
+ return;
50
+ }
51
+ if (e.type === 'click') {
52
+ // Allow links optionally
53
+ const t = e.target;
54
+ if (!t)
55
+ return;
56
+ const anchor = t.closest('a[href]');
57
+ if (anchor) {
58
+ if (allow.links)
59
+ return; // allow
60
+ e.preventDefault();
61
+ e.stopImmediatePropagation();
62
+ return;
63
+ }
64
+ const btn = t.closest('button, input[type="button"], input[type="submit"], input[type="checkbox"], input[type="radio"]');
65
+ if (btn) {
66
+ // block clicking that could change state
67
+ e.preventDefault();
68
+ e.stopImmediatePropagation();
69
+ return;
70
+ }
71
+ }
72
+ if (e.type === 'keydown' || e.type === 'keypress' || e.type === 'beforeinput') {
73
+ if (allow.keyboard)
74
+ return;
75
+ const t = e.target;
76
+ if (!t)
77
+ return;
78
+ if (this.isEditable(t)) {
79
+ e.preventDefault();
80
+ e.stopImmediatePropagation();
81
+ return;
82
+ }
83
+ }
84
+ if (e.type === 'input' || e.type === 'change' || e.type === 'paste' || e.type === 'cut' || e.type === 'drop') {
85
+ // Block state-changing input events
86
+ e.preventDefault();
87
+ e.stopImmediatePropagation();
88
+ return;
89
+ }
90
+ if (e.type === 'contextmenu') {
91
+ if (allow.contextMenu)
92
+ return;
93
+ e.preventDefault();
94
+ e.stopImmediatePropagation();
95
+ return;
96
+ }
97
+ };
98
+ const events = ['submit', 'click', 'keydown', 'keypress', 'beforeinput', 'input', 'change', 'paste', 'cut', 'drop', 'contextmenu'];
99
+ for (const ev of events) {
100
+ const listener = capture.bind(this);
101
+ document.addEventListener(ev, listener, { capture: true, passive: false });
102
+ this.listeners.set(ev, listener);
103
+ }
104
+ }
105
+ disable() {
106
+ if (!this.enabled)
107
+ return;
108
+ this.enabled = false;
109
+ for (const [ev, listener] of this.listeners.entries()) {
110
+ document.removeEventListener(ev, listener, { capture: true });
111
+ }
112
+ this.listeners.clear();
113
+ this.roots = [];
114
+ this.options = {};
115
+ }
116
+ toggle(options) {
117
+ if (this.enabled)
118
+ this.disable();
119
+ else
120
+ this.enable(options);
121
+ }
122
+ isEnabled() {
123
+ return this.enabled;
124
+ }
125
+ normalizeSelectors(selector) {
126
+ if (!selector)
127
+ return [];
128
+ if (typeof selector === 'string') {
129
+ // support comma-separated lists
130
+ return selector.split(',').map(s => s.trim()).filter(Boolean);
131
+ }
132
+ return selector.map(s => s.trim()).filter(Boolean);
133
+ }
134
+ isEventInRoots(e) {
135
+ const target = e.target;
136
+ if (!target)
137
+ return false;
138
+ for (const root of this.roots) {
139
+ if (root.contains(target))
140
+ return true;
141
+ }
142
+ return false;
143
+ }
144
+ isEditable(el) {
145
+ const tag = el.tagName.toLowerCase();
146
+ if (tag === 'input' || tag === 'textarea' || el.isContentEditable)
147
+ return true;
148
+ if (el.closest && el.closest('input, textarea, [contenteditable="true"]'))
149
+ return true;
150
+ return false;
151
+ }
152
+ }
153
+ export const readonlyMode = new ReadonlyMode();
154
+ export default readonlyMode;
@@ -0,0 +1,16 @@
1
+ import { ReadonlyOptions } from './types';
2
+ declare class ReadonlyMode {
3
+ private enabled;
4
+ private roots;
5
+ private options;
6
+ private listeners;
7
+ enable(options?: ReadonlyOptions): void;
8
+ disable(): void;
9
+ toggle(options?: ReadonlyOptions): void;
10
+ isEnabled(): boolean;
11
+ private normalizeSelectors;
12
+ private isEventInRoots;
13
+ private isEditable;
14
+ }
15
+ export declare const readonlyMode: ReadonlyMode;
16
+ export default readonlyMode;
@@ -0,0 +1,154 @@
1
+ const DEFAULT_ALLOW = {
2
+ scroll: true,
3
+ selection: true,
4
+ copy: true,
5
+ links: true,
6
+ download: true,
7
+ focus: true,
8
+ contextMenu: true,
9
+ keyboard: false
10
+ };
11
+ class ReadonlyMode {
12
+ constructor() {
13
+ this.enabled = false;
14
+ this.roots = [];
15
+ this.options = {};
16
+ this.listeners = new Map();
17
+ }
18
+ enable(options) {
19
+ if (this.enabled)
20
+ return;
21
+ this.enabled = true;
22
+ this.options = options || {};
23
+ const allow = { ...DEFAULT_ALLOW, ...(this.options.allow || {}) };
24
+ // Resolve roots
25
+ const selectors = this.normalizeSelectors(this.options.selector);
26
+ if (selectors.length === 0) {
27
+ this.roots = [document.documentElement];
28
+ }
29
+ else {
30
+ const set = new Set();
31
+ for (const sel of selectors) {
32
+ try {
33
+ document.querySelectorAll(sel).forEach(el => set.add(el));
34
+ }
35
+ catch (err) {
36
+ // ignore invalid selectors
37
+ }
38
+ }
39
+ this.roots = Array.from(set);
40
+ }
41
+ // Attach capturing listeners to intercept state-changing interactions
42
+ const capture = (e) => {
43
+ if (!this.isEventInRoots(e))
44
+ return;
45
+ if (e.type === 'submit') {
46
+ // Block form submissions
47
+ e.preventDefault();
48
+ e.stopImmediatePropagation();
49
+ return;
50
+ }
51
+ if (e.type === 'click') {
52
+ // Allow links optionally
53
+ const t = e.target;
54
+ if (!t)
55
+ return;
56
+ const anchor = t.closest('a[href]');
57
+ if (anchor) {
58
+ if (allow.links)
59
+ return; // allow
60
+ e.preventDefault();
61
+ e.stopImmediatePropagation();
62
+ return;
63
+ }
64
+ const btn = t.closest('button, input[type="button"], input[type="submit"], input[type="checkbox"], input[type="radio"]');
65
+ if (btn) {
66
+ // block clicking that could change state
67
+ e.preventDefault();
68
+ e.stopImmediatePropagation();
69
+ return;
70
+ }
71
+ }
72
+ if (e.type === 'keydown' || e.type === 'keypress' || e.type === 'beforeinput') {
73
+ if (allow.keyboard)
74
+ return;
75
+ const t = e.target;
76
+ if (!t)
77
+ return;
78
+ if (this.isEditable(t)) {
79
+ e.preventDefault();
80
+ e.stopImmediatePropagation();
81
+ return;
82
+ }
83
+ }
84
+ if (e.type === 'input' || e.type === 'change' || e.type === 'paste' || e.type === 'cut' || e.type === 'drop') {
85
+ // Block state-changing input events
86
+ e.preventDefault();
87
+ e.stopImmediatePropagation();
88
+ return;
89
+ }
90
+ if (e.type === 'contextmenu') {
91
+ if (allow.contextMenu)
92
+ return;
93
+ e.preventDefault();
94
+ e.stopImmediatePropagation();
95
+ return;
96
+ }
97
+ };
98
+ const events = ['submit', 'click', 'keydown', 'keypress', 'beforeinput', 'input', 'change', 'paste', 'cut', 'drop', 'contextmenu'];
99
+ for (const ev of events) {
100
+ const listener = capture.bind(this);
101
+ document.addEventListener(ev, listener, { capture: true, passive: false });
102
+ this.listeners.set(ev, listener);
103
+ }
104
+ }
105
+ disable() {
106
+ if (!this.enabled)
107
+ return;
108
+ this.enabled = false;
109
+ for (const [ev, listener] of this.listeners.entries()) {
110
+ document.removeEventListener(ev, listener, { capture: true });
111
+ }
112
+ this.listeners.clear();
113
+ this.roots = [];
114
+ this.options = {};
115
+ }
116
+ toggle(options) {
117
+ if (this.enabled)
118
+ this.disable();
119
+ else
120
+ this.enable(options);
121
+ }
122
+ isEnabled() {
123
+ return this.enabled;
124
+ }
125
+ normalizeSelectors(selector) {
126
+ if (!selector)
127
+ return [];
128
+ if (typeof selector === 'string') {
129
+ // support comma-separated lists
130
+ return selector.split(',').map(s => s.trim()).filter(Boolean);
131
+ }
132
+ return selector.map(s => s.trim()).filter(Boolean);
133
+ }
134
+ isEventInRoots(e) {
135
+ const target = e.target;
136
+ if (!target)
137
+ return false;
138
+ for (const root of this.roots) {
139
+ if (root.contains(target))
140
+ return true;
141
+ }
142
+ return false;
143
+ }
144
+ isEditable(el) {
145
+ const tag = el.tagName.toLowerCase();
146
+ if (tag === 'input' || tag === 'textarea' || el.isContentEditable)
147
+ return true;
148
+ if (el.closest && el.closest('input, textarea, [contenteditable="true"]'))
149
+ return true;
150
+ return false;
151
+ }
152
+ }
153
+ export const readonlyMode = new ReadonlyMode();
154
+ export default readonlyMode;
@@ -0,0 +1,14 @@
1
+ export interface ReadonlyAllowOptions {
2
+ scroll?: boolean;
3
+ selection?: boolean;
4
+ copy?: boolean;
5
+ links?: boolean;
6
+ download?: boolean;
7
+ focus?: boolean;
8
+ contextMenu?: boolean;
9
+ keyboard?: boolean;
10
+ }
11
+ export interface ReadonlyOptions {
12
+ selector?: string | string[];
13
+ allow?: ReadonlyAllowOptions;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export interface ReadonlyAllowOptions {
2
+ scroll?: boolean;
3
+ selection?: boolean;
4
+ copy?: boolean;
5
+ links?: boolean;
6
+ download?: boolean;
7
+ focus?: boolean;
8
+ contextMenu?: boolean;
9
+ keyboard?: boolean;
10
+ }
11
+ export interface ReadonlyOptions {
12
+ selector?: string | string[];
13
+ allow?: ReadonlyAllowOptions;
14
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "universal-readonly-rest",
3
+ "version": "0.1.0",
4
+ "description": "Universal readonly / interaction-control layer for web applications (framework-agnostic).",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc --project tsconfig.json",
13
+ "test": "vitest run --environment jsdom",
14
+ "test:watch": "vitest --environment jsdom"
15
+ },
16
+ "keywords": [
17
+ "readonly",
18
+ "readonly-mode",
19
+ "ui",
20
+ "accessibility",
21
+ "interaction-control"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@types/jsdom": "^30.0.0",
27
+ "@types/node": "^26.4.1",
28
+ "jsdom": "^30.0.1",
29
+ "typescript": "^7.0.2",
30
+ "vitest": "^4.1.11"
31
+ }
32
+ }