fl-readonly-manager
v3.1.0
Published
A lightweight utility to manage readonly/disabled states for form controls based on CSS classes. Supports nested containers, exemptions, and Shadow DOM.
Downloads
595
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
- Where To Use It
- JavaScript API
- Performance
- 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
- 🧭 CRUD Page Guardrail - Reusable CRUD page mode integration for shared views
- ⚡ 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
New in v3.1.0
- 💾 Auto-Save Detection - Track form changes and prevent data loss with beforeunload warnings
- ⏮️ Undo/Redo Stack - Full history management with keyboard shortcuts (Ctrl+Z/Ctrl+Y)
- 📊 Form Diff Viewer - Visual diff tracking to highlight changes with color-coded indicators
- ⌨️ Keyboard Shortcuts - Power-user shortcuts for toggling readonly states and navigation
- 📋 Smart Field Copying - One-click copy buttons for readonly fields
- 🎯 Conditional Readonly - Dynamic readonly rules based on expressions or functions
- 🎨 Readonly Presets - Pre-configured profiles for common scenarios (view/edit modes)
- 🔗 Smart Dependencies - Automatic field dependencies with cascading readonly states
- ✨ Auto-Format Values - 10+ formatters (date, currency, phone, SSN, credit card, etc.)
- 📦 Field Groups - Manage related fields as cohesive units
Installation
NPM / Yarn
npm install fl-readonly-manager
# or
yarn add fl-readonly-manager
# or
pnpm add fl-readonly-managerPackage:
www.npmjs.com/package/fl-readonly-manager
CDN (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();
const controller = ReadonlyManager.createCrudController(document, {
pageSelector: '.crud-page',
modeAttribute: 'data-crud-mode',
autoRefresh: true
});
controller.setMode('edit');
});
</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) |
| .crud-page | Page container | Guardrail class for shared CRUD views |
| .crud-view | Page container | Page is in readonly/view mode |
| .crud-edit | Page container | Page is in editable/edit mode |
| .crud-create | Page container | Page is in create mode |
| .crud-delete | Page container | Page is in delete mode |
CRUD Page Guardrails
Use createCrudPageReadonlyController, useCrudReadonly, and applyCrudReadonlyGuardrails to keep shared CRUD page views consistent across view/edit/create/delete states.
data-crud-modecontrols the current page state.crud-pagemarks the page container for guardrail policy.readonlyis applied automatically in view/delete/readonly modes.canBeReadOnlyensures the readonly manager manages the page container.
This makes the readonly manager reusable across all CRUD operation pages that share the same page view.
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-for-roles="viewer" value="Readonly for viewers">ReadonlyManager.configure({ RBAC: { enabled: true } });
ReadonlyManager.setRole('admin');Example 10: CRUD Page Guardrail + Shared View Hook
Use the same page view for shared CRUD operations and let the manager enforce page mode state.
<div class="crud-page canBeReadOnly" data-crud-mode="view">
<form>
<input type="text" name="name">
<button type="submit">Save</button>
</form>
</div>import { ReadonlyManager, createCrudPageReadonlyController } from 'fl-readonly-manager';
const controller = createCrudPageReadonlyController(document, {
pageSelector: '.crud-page',
modeAttribute: 'data-crud-mode',
autoRefresh: true
});
// Switch page mode in shared CRUD views
controller.setMode('edit');
controller.setMode('view');const hook = ReadonlyManager.useCrudReadonly(document, {
pageSelector: '.crud-page',
autoRefresh: true
});
hook.setMode('create');Example 12: Blazor Page Integration (v3.0.0)
Integrate fl-readonly-manager in a Blazor page using JS interop.
<!-- _Host.cshtml or wwwroot/index.html -->
<script src="https://unpkg.com/fl-readonly-manager/dist/readonly.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
ReadonlyManager.init();
const controller = ReadonlyManager.createCrudController(document, {
pageSelector: '.crud-page',
modeAttribute: 'data-crud-mode',
autoRefresh: true
});
window.FlReadonlyCrud = {
setMode: (mode) => controller.setMode(mode),
applyGuardrails: () => ReadonlyManager.applyCrudReadonlyGuardrails(document)
};
});
</script>@page "/crud"
@inject IJSRuntime JS
<div class="crud-page canBeReadOnly" data-crud-mode="view">
<form>
<input type="text" name="name" placeholder="Name" />
<input type="email" name="email" placeholder="Email" />
<button type="submit">Save</button>
</form>
<button @onclick="() => SetModeAsync('view')">View</button>
<button @onclick="() => SetModeAsync('edit')">Edit</button>
<button @onclick="() => SetModeAsync('create')">Create</button>
<button @onclick="() => SetModeAsync('delete')">Delete</button>
</div>
@code {
private async Task SetModeAsync(string mode)
{
await JS.InvokeVoidAsync("FlReadonlyCrud.setMode", mode);
}
}Example 12: 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 13: Scheduler Manager (v3.0.0)
Time-based readonly scheduling:
const element = document.getElementById('scheduledField');
// Schedule readonly during specific hours
const taskId = 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(taskId);Example 14: 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.isReadonly ? 'readonly' : 'editable'}`);
});
// Prevent readonly change
ReadonlyManager.events.on('beforeReadonly', (data) => {
if (data.element.id === 'protectedField') {
return false; // Cancel the change
}
});Example 15: 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 16: 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 17: 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 18: 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
}
});Example 17: Auto-Save Detection (v3.1.0)
Track form changes and prevent data loss:
// Get auto-save manager
const autoSave = ReadonlyManager.autoSave;
// Configure and initialize form
autoSave.configure({
enabled: true,
detectChanges: true,
warnBeforeUnload: true,
onDirtyChange: (form, isDirty) => {
console.log(`Form is ${isDirty ? 'dirty' : 'clean'}`);
}
});
autoSave.initializeForm(form);
// Check if form is dirty
if (autoSave.isDirty(form)) {
console.log('Form has unsaved changes');
}
// Get changed fields (returns array of Elements)
const changedFields = autoSave.getChangedFields(form);
const originalValues = autoSave.getOriginalValues(form);
changedFields.forEach(field => {
const name = field.name || field.id;
console.log(`${name}: "${originalValues[name]}" → "${field.value}"`);
});
// Reset form to original values
autoSave.resetForm(form);
// Mark as clean after save
autoSave.markClean(form);Example 18: Undo/Redo Stack (v3.1.0)
History management for readonly state changes (not field values):
// Get history manager
const history = ReadonlyManager.history;
// Configure with keyboard shortcuts
history.configure({
enabled: true,
maxSize: 50,
shortcuts: { undo: 'Ctrl+Z', redo: 'Ctrl+Y' },
onUndo: (entry) => console.log('Undid:', entry.action),
onRedo: (entry) => console.log('Redid:', entry.action)
});
// Record a readonly state change
history.record({
action: 'makeReadonly',
elements: [{ selector: '#myField', id: 'myField' }],
previousState: [false],
newState: [true],
source: 'user'
});
// Undo (or press Ctrl+Z) - restores previous readonly state
history.undo();
// Redo (or press Ctrl+Y) - reapplies readonly state
history.redo();
// Check if can undo/redo
if (history.canUndo()) {
console.log(`Can undo ${history.getHistory().length} actions`);
}
if (history.canRedo()) {
console.log(`Can redo ${history.getRedoStack().length} actions`);
}
// Clear history
history.clear();Example 19: Form Diff Viewer (v3.1.0)
Visual diff tracking to highlight changes:
// Get diff tracker manager
const diffTracker = ReadonlyManager.diffTracker;
// Configure
diffTracker.configure({
enabled: true,
highlightChanges: true,
showDiffIndicator: true
});
// Take snapshot before changes
diffTracker.takeSnapshot(form);
// Make changes to form...
// Get diff (returns object with fields property)
const diff = diffTracker.getDiff(form);
let totalChanges = 0;
Object.entries(diff.fields).forEach(([name, fieldDiff]) => {
if (fieldDiff.changed) {
totalChanges++;
console.log(`${name}: "${fieldDiff.before}" → "${fieldDiff.after}"`);
}
});
console.log(`${totalChanges} fields changed`);
// Show visual diff with highlighting
diffTracker.showDiff(form);
// Export diff as JSON
const diffJson = diffTracker.exportDiff(form);
// Reset diff and clear highlights
diffTracker.resetDiff(form);Example 20: Keyboard Shortcuts (v3.1.0)
Power-user shortcuts for productivity:
// Enable shortcuts
ReadonlyManager.configure({ SHORTCUTS: { enabled: true } });
// Register custom shortcut
ReadonlyManager.shortcuts.register('Ctrl+E', () => {
// Toggle edit mode
document.querySelector('.form-container').classList.toggle('readonly');
}, {
description: 'Toggle edit mode'
});
// Built-in shortcuts work automatically:
// Ctrl+Shift+L - Toggle readonly on focused field
// Ctrl+Shift+A - Toggle all fieldsExample 21: Smart Forms - Conditional Readonly (v3.1.0)
Dynamic readonly rules based on conditions:
// Enable smart forms
ReadonlyManager.configure({ SMART_FORMS: { enabled: true } });
// Add conditional rule
ReadonlyManager.smartForms.addConditionalRule({
selector: '#billingAddress',
condition: (context) => context.sameAsShipping === true,
readonly: true
});
// Update context to trigger rules
ReadonlyManager.smartForms.updateContext({ sameAsShipping: true });Example 22: Auto-Format Values (v3.1.0)
Formatters for common data types (requires value storage for round-trip):
// Enable smart forms with auto-format
const smartForms = ReadonlyManager.smartForms;
smartForms.enable({ autoFormat: true });
// Store original values for round-trip formatting
const originalValues = new Map();
// Format phone number
const phoneField = document.getElementById('phone');
originalValues.set(phoneField, phoneField.value);
smartForms.setFormat(phoneField, { type: 'phone' });
// Format currency
const priceField = document.getElementById('price');
originalValues.set(priceField, priceField.value);
smartForms.setFormat(priceField, {
type: 'currency',
locale: 'en-US'
});
// Format date
const dateField = document.getElementById('date');
originalValues.set(dateField, dateField.value);
smartForms.setFormat(dateField, {
type: 'date',
locale: 'en-US'
});
// Apply formats when field becomes readonly
function onReadonlyChange(field) {
if (field.readOnly) {
// Store value before formatting if not already stored
if (!originalValues.has(field)) {
originalValues.set(field, field.value);
}
smartForms.applyFormatToField(field);
} else {
// Restore original value when editable
if (originalValues.has(field)) {
field.value = originalValues.get(field);
}
}
}
// Available formatters: date, datetime, currency, phone, number, percent, custom
// Note: Formatters automatically handle already-formatted values (remove $, commas, etc.)Example 23: Field Groups (v3.1.0)
Manage related fields as cohesive units:
// Enable smart forms with groups
const smartForms = ReadonlyManager.smartForms;
smartForms.enable({ groups: true });
// Add field group
smartForms.addGroup({
name: 'personal-info',
selector: '[data-group="personal"]'
});
// Set entire group readonly
smartForms.setGroupReadonly('personal-info', true);
// Get all groups
const groups = smartForms.getGroups();
console.log(`Registered groups: ${groups.map(g => g.name).join(', ')}`);
// Remove a group
smartForms.removeGroup('personal-info');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]' | 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 | 50 | 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 lintPublishing
Publish from the fl-readonly-manager package folder.
1. Prepare the release
# Install dependencies
npm install
# Update package version if needed
npm version patch
# or
npm version minor
# or
npm version major
# Verify the package
npm run lint
npm run typecheck
npm test
npm run build2. Preview the package contents
# Review what npm will publish
npm pack --dry-runThe package publishes the files declared in package.json:
distexamplesREADME.mdUSE_CASES.mdLICENSE
3. Publish to npm
# Authenticate if needed
npm login
# Publish the package
npm publishprepublishOnly runs automatically during publish and executes:
npm run build && npm testIf this is the first public publish for the package, use:
npm publish --access publicLicense
MIT © FL Team
