fl-readonly-manager
v3.0.0
Published
A lightweight utility to manage readonly/disabled states for form controls based on CSS classes. Supports nested containers, exemptions, and Shadow DOM.
Maintainers
Readme
FL Readonly Manager
A lightweight JavaScript/TypeScript utility to manage readonly/disabled states for form controls based on CSS classes. Supports nested containers with granular control, Shadow DOM traversal, and automatic DOM observation.
Table of Contents
- Features
- Installation
- Quick Start
- CSS Classes Reference
- Usage Examples
- JavaScript API
- Configuration Options
- Control Behavior Reference
- Shadow DOM Support
- Logger Utility
- Browser Compatibility
- TypeScript Usage
- Migration from v1
- Contributing
- License
Features
Core Features
- 🎯 CSS Class-Based Control - Toggle readonly/disabled states using simple CSS classes
- 📦 Nested Container Support - Handle complex nested structures with proper inheritance
- 🚫 Granular Exemptions - Exempt specific elements or sections from readonly behavior
- 👁️ Automatic Observation - Uses MutationObserver to detect class changes automatically
- 🌑 Shadow DOM Support - Traverse into web components and shadow roots
- 📝 TypeScript Ready - Full type definitions included
- 🔧 Configurable - Customize selectors, debounce timing, and behavior
- 🪶 Modular & dependency-free - No runtime dependencies; ~42KB minified with all managers enabled (feature managers are opt-in)
- ♿ Accessibility - Properly sets readonly/disabled attributes for screen readers
New in v3.0.0
- 🎨 Theme Manager - Visual themes for readonly elements (subtle, greyed, striped, disabled, bordered)
- 🔐 RBAC Manager - Role-based access control with data attributes
- ⚡ Bulk Operations - Set readonly on multiple elements via selector or array
- ⏰ Scheduler Manager - Time-based readonly scheduling
- 💬 Tooltip Manager - Show reasons why elements are readonly
- ✨ Animation Manager - Animated state transitions
- 🎯 Event Manager - Lifecycle hooks (beforeReadonly, afterReadonly)
- 💾 Persistence Manager - Save/restore states to localStorage
- ✅ Validation Manager - Form validation that skips readonly fields
Installation
NPM / Yarn
npm install fl-readonly-manager
# or
yarn add fl-readonly-manager
# or
pnpm add fl-readonly-managerCDN (unpkg)
<!-- Minified (recommended for production) -->
<script src="https://unpkg.com/[email protected]/dist/readonly.min.js"></script>
<!-- Full version (for debugging) -->
<script src="https://unpkg.com/[email protected]/dist/readonly.js"></script>
<!-- Latest version (auto-updates with new releases) -->
<script src="https://unpkg.com/fl-readonly-manager/dist/readonly.min.js"></script>CDN (jsDelivr)
<!-- Minified -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/readonly.min.js"></script>
<!-- Full version -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/readonly.js"></script>
<!-- Latest -->
<script src="https://cdn.jsdelivr.net/npm/fl-readonly-manager/dist/readonly.min.js"></script>ES Module
import { ReadonlyManager } from 'fl-readonly-manager';CommonJS
const { ReadonlyManager } = require('fl-readonly-manager');ES Module via CDN
<script type="module">
import { ReadonlyManager } from 'https://unpkg.com/[email protected]/dist/index.mjs';
ReadonlyManager.init();
</script>Browser (IIFE)
For direct browser usage without a bundler:
<!-- Minified (17 KB) -->
<script src="https://unpkg.com/fl-readonly-manager/dist/readonly.min.js"></script>
<!-- Or full version (40 KB) -->
<!-- <script src="https://unpkg.com/fl-readonly-manager/dist/readonly.js"></script> -->
<script>
// Library exposes ReadonlyManagerLib global
const { ReadonlyManager, FL_Logger } = ReadonlyManagerLib;
document.addEventListener('DOMContentLoaded', () => {
ReadonlyManager.init();
});
</script>Quick Start
1. Add CSS Classes to Your HTML
<!-- Container with all children readonly -->
<div class="canBeReadOnly readonly">
<input type="text" name="field1"> <!-- Will be readonly -->
<select name="field2"> <!-- Will be disabled -->
<option>Option 1</option>
</select>
<button type="button">Click</button> <!-- Will be disabled -->
</div>2. Include the Script
<!-- Include from CDN -->
<script src="https://unpkg.com/fl-readonly-manager/dist/readonly.min.js"></script>
<script>
// Extract ReadonlyManager from global and initialize
const { ReadonlyManager } = ReadonlyManagerLib;
document.addEventListener('DOMContentLoaded', () => {
ReadonlyManager.init();
});
</script>3. Toggle Readonly Dynamically
// Toggle using CSS classes (observed automatically)
document.getElementById('myForm').classList.toggle('readonly');
// Or use the API
ReadonlyManager.setReadonly(document.getElementById('myForm'), true);CSS Classes Reference
| Class | Target | Description |
|-------|--------|-------------|
| .canBeReadOnly | Any element | Marks element as controllable by this system |
| .readonly | Any element | Activates readonly/disabled state |
| .input-readonly | Any element | Alternative to .readonly (same behavior) |
| .ommitReadOnly | Any element | Exempts element and children from readonly |
| .fl-readonly-applied | Auto-added | Visual indicator (added automatically when readonly) |
Usage Examples
Example 1: Basic Container
All inputs inside become readonly/disabled:
<div class="canBeReadOnly readonly">
<input type="text" name="name"> <!-- readonly="readonly" -->
<input type="email" name="email"> <!-- readonly="readonly" -->
<select name="country"> <!-- disabled -->
<option>USA</option>
</select>
<button type="submit">Submit</button> <!-- disabled -->
</div>Example 2: Container with Exempted Section
Use .ommitReadOnly to keep sections editable:
<div class="canBeReadOnly readonly">
<input type="text" name="locked"> <!-- readonly -->
<div class="ommitReadOnly">
<input type="text" name="editable"> <!-- stays editable! -->
<button type="button">Action</button><!-- stays enabled! -->
</div>
<input type="text" name="also-locked"> <!-- readonly -->
</div>Example 3: Individual Control
Apply to a single element:
<input type="text" class="canBeReadOnly readonly" name="single">
<!-- This single input will be readonly -->Example 4: Exempted Individual Control
Exempt specific controls within a readonly container:
<div class="canBeReadOnly readonly">
<input type="text" name="locked"> <!-- readonly -->
<input type="text" name="exempt" class="ommitReadOnly"> <!-- editable! -->
<input type="text" name="also-locked"> <!-- readonly -->
</div>Example 5: Nested Containers
Handle complex nested structures:
<div class="canBeReadOnly readonly">
<input type="text" name="outer"> <!-- readonly -->
<div class="canBeReadOnly ommitReadOnly">
<input type="text" name="inner1"> <!-- editable (parent has ommitReadOnly) -->
<div class="canBeReadOnly readonly">
<input type="text" name="inner2"> <!-- readonly (own container is readonly) -->
</div>
</div>
</div>Example 6: Dynamic Toggle with JavaScript
<div id="myForm" class="canBeReadOnly">
<input type="text" name="field1">
<input type="text" name="field2">
</div>
<button onclick="toggleReadonly()">Toggle Readonly</button>
<script>
// Method 1: Toggle CSS class (MutationObserver handles the rest)
function toggleReadonly() {
const form = document.getElementById('myForm');
form.classList.toggle('readonly');
}
// Method 2: Use the API
function toggleReadonlyAPI() {
const form = document.getElementById('myForm');
const isReadonly = ReadonlyManager.isReadonly(form);
ReadonlyManager.setReadonly(form, !isReadonly);
}
</script>Example 7: Complete Form with Mixed Controls
<form class="canBeReadOnly readonly">
<!-- Text inputs: readonly="readonly" -->
<input type="text" name="name">
<input type="email" name="email">
<input type="tel" name="phone">
<input type="url" name="website">
<input type="password" name="password">
<input type="number" name="age">
<input type="date" name="birthdate">
<!-- Textarea: readonly="readonly" -->
<textarea name="bio"></textarea>
<!-- Special inputs: disabled -->
<input type="checkbox" name="agree">
<input type="radio" name="choice" value="1">
<input type="radio" name="choice" value="2">
<input type="file" name="avatar">
<input type="color" name="theme">
<input type="range" name="volume">
<!-- Select and buttons: disabled -->
<select name="country">
<option>USA</option>
<option>Canada</option>
</select>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
<!-- Contenteditable: contenteditable="false" -->
<div contenteditable="true" name="richtext">Edit me</div>
</form>Example 8: Theme Manager (v3.0.0)
Apply visual themes to readonly elements:
// Enable themes
ReadonlyManager.configure({ THEME: { enabled: true } });
// Set theme (default, subtle, greyed, striped, disabled, bordered)
ReadonlyManager.setTheme('greyed');Example 9: RBAC - Role-Based Access Control (v3.0.0)
Control field editability based on user roles:
<input data-editable-roles="admin,editor" value="Admin/Editor only">
<input data-readonly-roles="viewer" value="Readonly for viewers">ReadonlyManager.configure({ RBAC: { enabled: true } });
ReadonlyManager.setRole('admin');Example 10: Bulk Operations (v3.0.0)
Set readonly on multiple elements at once:
// Using CSS selector
await ReadonlyManager.setReadonlyBulk('.form-fields', true);
// Using NodeList
await ReadonlyManager.setReadonlyBulk(document.querySelectorAll('input'), false);
// With progress callback
await ReadonlyManager.setReadonlyBulk('.fields', true, {
onProgress: (current, total) => console.log(`${current}/${total}`)
});Example 11: Scheduler Manager (v3.0.0)
Time-based readonly scheduling:
const element = document.getElementById('scheduledField');
// Schedule readonly during specific hours
ReadonlyManager.scheduleReadonly(element, {
startTime: new Date('2026-01-15T09:00:00'), // Go readonly
endTime: new Date('2026-01-15T17:00:00') // Go editable
});
// Cancel scheduled task
ReadonlyManager.cancelSchedule(element);Example 12: Event Manager (v3.0.0)
Lifecycle hooks for readonly changes:
// Listen for readonly state changes
ReadonlyManager.events.on('afterReadonly', (data) => {
console.log(`${data.element.id} is now ${data.readonly ? 'readonly' : 'editable'}`);
});
// Prevent readonly change
ReadonlyManager.events.on('beforeReadonly', (data) => {
if (data.element.id === 'protectedField') {
return false; // Cancel the change
}
});Example 13: Tooltip Manager (v3.0.0)
Show reasons why elements are readonly:
// Enable tooltips
ReadonlyManager.configure({ TOOLTIP: { enabled: true } });
// Set reason for an element
const field = document.getElementById('lockedField');
field.setAttribute('data-readonly-reason', 'Locked by admin');
ReadonlyManager.tooltip.attach(field);Example 14: Animation Manager (v3.0.0)
Animate state transitions:
ReadonlyManager.configure({
ANIMATION: {
enabled: true,
duration: 300,
easing: 'ease-in-out'
}
});
// Transitions will now be animated
ReadonlyManager.setReadonly(element, true);Example 15: Persistence Manager (v3.0.0)
Save and restore readonly states:
// Save state to localStorage
ReadonlyManager.persistence.save(element);
// Load saved state
ReadonlyManager.persistence.load(element);
// Clear saved state
ReadonlyManager.persistence.clear(element);Example 16: Validation Manager (v3.0.0)
Form validation that respects readonly fields:
const form = document.getElementById('myForm');
// Get only editable fields for validation
const editableFields = ReadonlyManager.validation.getEditableFields(form);
// Skip readonly fields in validation
editableFields.forEach(field => {
if (ReadonlyManager.validation.shouldValidate(field)) {
// Perform validation
}
});JavaScript API
ReadonlyManager.init()
Initialize the readonly manager. Called automatically on page load.
ReadonlyManager.init();ReadonlyManager.refresh(rootElement?)
Manually re-process all readonly controls. Useful after dynamic DOM changes.
// Refresh entire document
ReadonlyManager.refresh();
// Refresh specific container
ReadonlyManager.refresh(document.getElementById('myForm'));ReadonlyManager.setReadonly(element, readonly)
Programmatically set readonly state on an element.
const form = document.getElementById('myForm');
// Make readonly
ReadonlyManager.setReadonly(form, true);
// Make editable
ReadonlyManager.setReadonly(form, false);ReadonlyManager.isReadonly(element)
Check if an element is currently readonly.
const form = document.getElementById('myForm');
if (ReadonlyManager.isReadonly(form)) {
console.log('Form is readonly');
}ReadonlyManager.destroy()
Disconnect the observer and stop watching for changes.
// Clean up when done (e.g., SPA navigation)
ReadonlyManager.destroy();ReadonlyManager.configure(options)
Update configuration options at runtime.
ReadonlyManager.configure({
DEBOUNCE_MS: 50, // Faster response
DEBUG: true, // Enable logging
TRAVERSE_SHADOW_DOM: true // Enable Shadow DOM support
});ReadonlyManager.getConfig()
Get current configuration.
const config = ReadonlyManager.getConfig();
console.log(config.DEBOUNCE_MS); // 100ReadonlyManager.logger
Access the logger instance for debugging.
// Enable debug logging
ReadonlyManager.logger.enable();
ReadonlyManager.logger.setLevel(0); // DEBUG level
// Disable logging
ReadonlyManager.logger.disable();ReadonlyManager.features
Check browser feature support.
const features = ReadonlyManager.features;
if (features.shadowDOM) {
console.log('Shadow DOM is supported');
}
if (features.mutationObserver) {
console.log('Auto-updates are available');
}Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| EDITABLE_SELECTOR | string | 'input, select, textarea, button, [contenteditable="true"]' | CSS selector for editable controls |
| CONTAINER_SELECTOR | string | 'div, section, span, ...' | CSS selector for container elements |
| CLASSES.CAN_BE_READONLY | string | 'canBeReadOnly' | Class marking controllable elements |
| CLASSES.READONLY | string | 'readonly' | Class activating readonly state |
| CLASSES.INPUT_READONLY | string | 'input-readonly' | Alternative readonly class |
| CLASSES.OMMIT_READONLY | string | 'ommitReadOnly' | Class for exempted elements |
| CLASSES.READONLY_APPLIED | string | 'fl-readonly-applied' | Visual indicator class |
| DEBOUNCE_MS | number | 100 | Debounce delay for observer (ms) |
| DEBUG | boolean | false | Enable debug logging |
| TRAVERSE_SHADOW_DOM | boolean | true | Enable Shadow DOM traversal |
Custom Configuration Example
ReadonlyManager.configure({
DEBOUNCE_MS: 50,
DEBUG: true,
CLASSES: {
CAN_BE_READONLY: 'my-readonly-container',
READONLY: 'is-locked',
OMMIT_READONLY: 'keep-editable'
}
});Control Behavior Reference
| Element Type | Readonly Behavior |
|--------------|-------------------|
| input[type="text"] | readonly="readonly" |
| input[type="password"] | readonly="readonly" |
| input[type="email"] | readonly="readonly" |
| input[type="number"] | readonly="readonly" |
| input[type="tel"] | readonly="readonly" |
| input[type="url"] | readonly="readonly" |
| input[type="search"] | readonly="readonly" |
| input[type="date"] | readonly="readonly" |
| input[type="time"] | readonly="readonly" |
| input[type="datetime-local"] | readonly="readonly" |
| input[type="month"] | readonly="readonly" |
| input[type="week"] | readonly="readonly" |
| textarea | readonly="readonly" |
| input[type="checkbox"] | disabled |
| input[type="radio"] | disabled |
| input[type="file"] | disabled |
| input[type="color"] | disabled |
| input[type="range"] | disabled |
| input[type="submit"] | disabled |
| input[type="reset"] | disabled |
| input[type="button"] | disabled |
| select | disabled |
| button | disabled |
| [contenteditable] | contenteditable="false" |
Shadow DOM Support
The manager can traverse into Shadow DOM to process elements inside web components.
Enable Shadow DOM (Default)
ReadonlyManager.configure({ TRAVERSE_SHADOW_DOM: true });Web Component Example
<my-custom-form class="canBeReadOnly readonly">
<!-- Light DOM content -->
<input type="text" name="light-input">
</my-custom-form>
<script>
class MyCustomForm extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<div class="canBeReadOnly readonly">
<input type="text" name="shadow-input">
</div>
`;
}
}
customElements.define('my-custom-form', MyCustomForm);
</script>Both light-input and shadow-input will be processed.
Logger Utility
Built-in logger for debugging with enable/disable control.
// Access logger
const logger = ReadonlyManager.logger;
// Enable logging
logger.enable();
// Set log level (0=DEBUG, 1=INFO, 2=WARN, 3=ERROR)
logger.setLevel(0);
// Log messages (when enabled)
logger.debug('Detailed info');
logger.info('General info');
logger.warn('Warning');
logger.error('Error');
// Disable for production
logger.disable();Log Level Constants
import { LogLevel } from 'fl-readonly-manager';
ReadonlyManager.logger.setLevel(LogLevel.DEBUG); // 0
ReadonlyManager.logger.setLevel(LogLevel.INFO); // 1
ReadonlyManager.logger.setLevel(LogLevel.WARN); // 2
ReadonlyManager.logger.setLevel(LogLevel.ERROR); // 3Browser Compatibility
| Browser | Version | Notes | |---------|---------|-------| | Chrome | 54+ | Full support | | Firefox | 63+ | Full support | | Safari | 10.1+ | Full support | | Edge | 79+ | Full support (Chromium) | | Edge Legacy | 15+ | No Shadow DOM | | IE 11 | ❌ | Not supported |
Feature Detection
const features = ReadonlyManager.features;
if (!features.mutationObserver) {
console.warn('Auto-updates not available. Call refresh() manually.');
}
if (!features.shadowDOM) {
console.warn('Shadow DOM not supported. Web components will not be traversed.');
}TypeScript Usage
Full TypeScript support with type definitions included.
Import Types
import {
ReadonlyManager,
ReadonlyManagerConfig,
ILogger,
LogLevel,
FeatureSupport
} from 'fl-readonly-manager';Type-Safe Configuration
import { ReadonlyManagerConfig } from 'fl-readonly-manager';
const config: Partial<ReadonlyManagerConfig> = {
DEBOUNCE_MS: 50,
DEBUG: true
};
ReadonlyManager.configure(config);Using the Core Class Directly
import { ReadonlyManagerCore } from 'fl-readonly-manager';
const manager = new ReadonlyManagerCore({
DEBOUNCE_MS: 50,
TRAVERSE_SHADOW_DOM: true
});
manager.init();
manager.handleReadonlyControls(document.body);Migration from v1
Breaking Changes
- Function renamed:
FL_HandleReadonlyControls()is nowReadonlyManager.refresh() - Configuration: Settings now use
ReadonlyManager.configure()instead of global variables - Logger: Console logging now requires enabling
ReadonlyManager.logger.enable()
Migration Steps
// v1 (old)
FL_HandleReadonlyControls();
// v2 (new)
ReadonlyManager.refresh();
// v1 (old) - no configuration API
// v2 (new)
ReadonlyManager.configure({ DEBOUNCE_MS: 50 });Backward Compatibility
The legacy function is still available for backward compatibility:
// Still works in v2
FL_HandleReadonlyControls();Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
npm test - Submit a pull request
Development
# Install dependencies
npm install
# Run in watch mode
npm run dev
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build for production
npm run build
# Lint code
npm run lintLicense
MIT © FL Team
