@andreaswissel/uiflow
Version:
Adaptive UI density management library with progressive disclosure, element dependencies, A/B testing, and intelligent behavior-based adaptation
1,036 lines (874 loc) • 25.8 kB
Markdown
# Text Editor Example
A comprehensive example of implementing UIFlow in a text editor application.
## Overview
This example demonstrates how to create an adaptive text editor that progressively reveals features based on user expertise:
- **Beginner**: Basic text editing, save/load
- **Intermediate**: Formatting toolbar, find/replace
- **Expert**: Advanced formatting, plugins, custom shortcuts
## HTML Structure
```html
<!DOCTYPE html>
<html>
<head>
<title>Adaptive Text Editor</title>
<link rel="stylesheet" href="editor.css">
</head>
<body>
<div class="editor-app">
<!-- Header with file operations -->
<header class="header">
<div class="file-operations" data-uiflow-area="file-ops">
<!-- Basic file operations -->
<button id="new-btn" class="btn">New</button>
<button id="open-btn" class="btn">Open</button>
<button id="save-btn" class="btn">Save</button>
<!-- Advanced file operations -->
<button id="save-as-btn" class="btn">Save As</button>
<button id="export-btn" class="btn">Export</button>
<!-- Expert file operations -->
<button id="batch-ops-btn" class="btn">Batch Operations</button>
</div>
<!-- Density indicator -->
<div class="density-indicator" id="density-display"></div>
</header>
<!-- Main editing area -->
<main class="main-content">
<!-- Basic text editing -->
<div class="editor-container" data-uiflow-area="editor">
<textarea id="text-editor" placeholder="Start typing..."></textarea>
<!-- Formatting toolbar -->
<div id="formatting-toolbar" class="formatting-toolbar">
<!-- Basic formatting -->
<button id="bold-btn" class="format-btn">B</button>
<button id="italic-btn" class="format-btn">I</button>
<button id="underline-btn" class="format-btn">U</button>
<!-- Advanced formatting -->
<select id="font-family" class="format-select">
<option>Arial</option>
<option>Times</option>
<option>Courier</option>
</select>
<select id="font-size" class="format-select">
<option>12px</option>
<option>14px</option>
<option>16px</option>
<option>18px</option>
</select>
<!-- Expert formatting -->
<button id="styles-btn" class="format-btn">Styles</button>
<button id="themes-btn" class="format-btn">Themes</button>
</div>
<!-- Advanced features panel -->
<div id="advanced-panel" class="advanced-panel">
<div class="feature-group">
<h3>Find & Replace</h3>
<input id="find-input" placeholder="Find...">
<input id="replace-input" placeholder="Replace...">
<button id="replace-btn">Replace All</button>
</div>
<div class="feature-group">
<h3>Statistics</h3>
<div id="word-count">Words: 0</div>
<div id="char-count">Characters: 0</div>
</div>
</div>
<!-- Expert features panel -->
<div id="expert-panel" class="expert-panel">
<div class="feature-group">
<h3>Plugins</h3>
<button id="grammar-check-btn">Grammar Check</button>
<button id="markdown-preview-btn">Markdown Preview</button>
<button id="code-highlight-btn">Code Highlighting</button>
</div>
<div class="feature-group">
<h3>Automation</h3>
<button id="macros-btn">Macros</button>
<button id="auto-save-btn">Auto-save</button>
<button id="templates-btn">Templates</button>
</div>
</div>
</div>
</main>
<!-- Sidebar with tools -->
<aside class="sidebar" data-uiflow-area="tools">
<!-- Basic tools -->
<div class="tool-group">
<h3>Document</h3>
<button id="undo-btn" class="tool-btn">Undo</button>
<button id="redo-btn" class="tool-btn">Redo</button>
</div>
<!-- Advanced tools -->
<div class="tool-group">
<h3>Navigation</h3>
<button id="outline-btn" class="tool-btn">Outline</button>
<button id="bookmarks-btn" class="tool-btn">Bookmarks</button>
</div>
<!-- Expert tools -->
<div class="tool-group">
<h3>Development</h3>
<button id="api-docs-btn" class="tool-btn">API Docs</button>
<button id="custom-css-btn" class="tool-btn">Custom CSS</button>
</div>
<!-- Settings -->
<div class="tool-group">
<h3>Settings</h3>
<div id="manual-density-control"></div>
</div>
</aside>
</div>
<script src="https://unpkg.com/uiflow@latest/dist/uiflow.min.js"></script>
<script src="editor.js"></script>
</body>
</html>
```
## JavaScript Implementation
```javascript
// editor.js
class AdaptiveTextEditor {
constructor() {
this.uiflow = null;
this.currentDocument = '';
this.undoStack = [];
this.redoStack = [];
this.initializeUIFlow();
this.setupEventListeners();
this.setupFeatureTracking();
}
async initializeUIFlow() {
this.uiflow = new UIFlow({
userId: this.getUserId(),
categories: ['basic', 'advanced', 'expert'],
learningRate: 0.15, // Slightly faster learning for editors
timeAcceleration: 1,
dataSources: {
api: {
endpoint: '/api/uiflow',
primary: true
}
}
});
await this.uiflow.init();
this.categorizeElements();
this.setupDensityDisplay();
}
categorizeElements() {
// File operations
this.categorizeFileOperations();
// Editor features
this.categorizeEditorFeatures();
// Sidebar tools
this.categorizeSidebarTools();
}
categorizeFileOperations() {
const area = 'file-ops';
// Basic file operations - always visible
this.uiflow.categorize(
document.getElementById('new-btn'),
'basic',
area
);
this.uiflow.categorize(
document.getElementById('open-btn'),
'basic',
area
);
this.uiflow.categorize(
document.getElementById('save-btn'),
'basic',
area
);
// Advanced file operations
this.uiflow.categorize(
document.getElementById('save-as-btn'),
'advanced',
area,
{ helpText: 'Save document with a new name or format' }
);
this.uiflow.categorize(
document.getElementById('export-btn'),
'advanced',
area,
{ helpText: 'Export to PDF, HTML, or other formats' }
);
// Expert file operations
this.uiflow.categorize(
document.getElementById('batch-ops-btn'),
'expert',
area,
{ helpText: 'Process multiple files simultaneously' }
);
}
categorizeEditorFeatures() {
const area = 'editor';
// Basic text editor - always visible
this.uiflow.categorize(
document.getElementById('text-editor'),
'basic',
area
);
// Basic formatting
this.uiflow.categorize(
document.getElementById('bold-btn'),
'basic',
area
);
this.uiflow.categorize(
document.getElementById('italic-btn'),
'basic',
area
);
this.uiflow.categorize(
document.getElementById('underline-btn'),
'basic',
area
);
// Advanced formatting
this.uiflow.categorize(
document.getElementById('font-family'),
'advanced',
area,
{ helpText: 'Choose from different font families' }
);
this.uiflow.categorize(
document.getElementById('font-size'),
'advanced',
area,
{ helpText: 'Adjust text size' }
);
// Advanced features panel
this.uiflow.categorize(
document.getElementById('advanced-panel'),
'advanced',
area,
{ helpText: 'Find & replace, document statistics' }
);
// Expert formatting
this.uiflow.categorize(
document.getElementById('styles-btn'),
'expert',
area,
{ helpText: 'Custom paragraph and character styles' }
);
this.uiflow.categorize(
document.getElementById('themes-btn'),
'expert',
area,
{ helpText: 'Switch between editor themes' }
);
// Expert features panel
this.uiflow.categorize(
document.getElementById('expert-panel'),
'expert',
area,
{ helpText: 'Plugins, macros, and automation tools' }
);
}
categorizeSidebarTools() {
const area = 'tools';
// Basic tools
this.uiflow.categorize(
document.getElementById('undo-btn'),
'basic',
area
);
this.uiflow.categorize(
document.getElementById('redo-btn'),
'basic',
area
);
// Advanced tools
this.uiflow.categorize(
document.getElementById('outline-btn'),
'advanced',
area,
{ helpText: 'Navigate document structure' }
);
this.uiflow.categorize(
document.getElementById('bookmarks-btn'),
'advanced',
area,
{ helpText: 'Save and jump to specific locations' }
);
// Expert tools
this.uiflow.categorize(
document.getElementById('api-docs-btn'),
'expert',
area,
{ helpText: 'API documentation for custom extensions' }
);
this.uiflow.categorize(
document.getElementById('custom-css-btn'),
'expert',
area,
{ helpText: 'Customize editor appearance with CSS' }
);
}
setupDensityDisplay() {
const densityDisplay = document.getElementById('density-display');
const updateDisplay = () => {
const editorDensity = this.uiflow.getDensityLevel('editor');
const fileOpsDensity = this.uiflow.getDensityLevel('file-ops');
const toolsDensity = this.uiflow.getDensityLevel('tools');
densityDisplay.innerHTML = `
<div class="density-item">
<span>Editor: ${Math.round(editorDensity * 100)}%</span>
${this.uiflow.hasOverride('editor') ? '<span class="override">Override</span>' : ''}
</div>
<div class="density-item">
<span>File: ${Math.round(fileOpsDensity * 100)}%</span>
</div>
<div class="density-item">
<span>Tools: ${Math.round(toolsDensity * 100)}%</span>
</div>
`;
};
// Update on changes
document.addEventListener('uiflow:density-changed', updateDisplay);
document.addEventListener('uiflow:adaptation', updateDisplay);
document.addEventListener('uiflow:override-applied', updateDisplay);
document.addEventListener('uiflow:override-cleared', updateDisplay);
// Initial update
updateDisplay();
// Manual density control
this.setupManualDensityControl();
}
setupManualDensityControl() {
const container = document.getElementById('manual-density-control');
const areas = ['editor', 'file-ops', 'tools'];
areas.forEach(area => {
const control = document.createElement('div');
control.className = 'density-control';
control.innerHTML = `
<label for="${area}-density">${area}:</label>
<input
type="range"
id="${area}-density"
min="0"
max="1"
step="0.1"
value="${this.uiflow.getDensityLevel(area)}"
>
<span class="density-value">${Math.round(this.uiflow.getDensityLevel(area) * 100)}%</span>
`;
const slider = control.querySelector('input');
const valueSpan = control.querySelector('.density-value');
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.uiflow.setDensityLevel(value, area);
valueSpan.textContent = `${Math.round(value * 100)}%`;
});
container.appendChild(control);
});
}
setupEventListeners() {
// File operations
this.setupFileOperations();
// Editor operations
this.setupEditorOperations();
// Feature discovery
this.setupFeatureDiscovery();
}
setupFileOperations() {
// Basic operations
document.getElementById('new-btn').addEventListener('click', () => {
this.newDocument();
});
document.getElementById('save-btn').addEventListener('click', () => {
this.saveDocument();
});
document.getElementById('open-btn').addEventListener('click', () => {
this.openDocument();
});
// Advanced operations
document.getElementById('save-as-btn').addEventListener('click', () => {
this.saveDocumentAs();
});
document.getElementById('export-btn').addEventListener('click', () => {
this.exportDocument();
});
// Expert operations
document.getElementById('batch-ops-btn').addEventListener('click', () => {
this.openBatchOperations();
});
}
setupEditorOperations() {
const textEditor = document.getElementById('text-editor');
// Track typing activity
textEditor.addEventListener('input', () => {
this.updateWordCount();
this.trackEditorUsage();
});
// Formatting operations
document.getElementById('bold-btn').addEventListener('click', () => {
this.formatText('bold');
});
document.getElementById('italic-btn').addEventListener('click', () => {
this.formatText('italic');
});
document.getElementById('underline-btn').addEventListener('click', () => {
this.formatText('underline');
});
// Advanced features
document.getElementById('replace-btn').addEventListener('click', () => {
this.replaceText();
});
// Expert features
document.getElementById('grammar-check-btn').addEventListener('click', () => {
this.runGrammarCheck();
});
document.getElementById('macros-btn').addEventListener('click', () => {
this.openMacroEditor();
});
}
setupFeatureDiscovery() {
// Highlight new features when density increases
document.addEventListener('uiflow:adaptation', (event) => {
const { area, newDensity, oldDensity } = event.detail;
// Significant increase in density - show what's new
if (newDensity - oldDensity > 0.2) {
setTimeout(() => this.discoverNewFeatures(area), 1000);
}
});
}
discoverNewFeatures(area) {
const newElements = document.querySelectorAll(
`[data-uiflow-area="${area}"][data-uiflow-visible="true"]`
);
newElements.forEach(element => {
const category = element.getAttribute('data-uiflow-category');
const helpText = element.getAttribute('data-uiflow-help');
const elementId = element.getAttribute('data-uiflow-id');
if (category !== 'basic' && helpText) {
this.uiflow.flagAsNew(elementId, helpText, 8000);
}
});
}
setupFeatureTracking() {
// Track which features users actually use
document.addEventListener('click', (event) => {
const element = event.target.closest('[data-uiflow-category]');
if (!element) return;
const category = element.getAttribute('data-uiflow-category');
const area = element.getAttribute('data-uiflow-area');
const density = this.uiflow.getDensityLevel(area);
// Analytics tracking
this.trackFeatureUsage({
feature: element.id || 'unknown',
category,
area,
density,
timestamp: Date.now()
});
});
}
// Feature implementations
newDocument() {
if (this.currentDocument && !confirm('Discard current document?')) {
return;
}
document.getElementById('text-editor').value = '';
this.currentDocument = '';
this.updateWordCount();
}
saveDocument() {
this.currentDocument = document.getElementById('text-editor').value;
// Simulate save
this.showNotification('Document saved!');
}
saveDocumentAs() {
const filename = prompt('Save as:', 'document.txt');
if (filename) {
this.saveDocument();
this.showNotification(`Saved as ${filename}`);
}
}
exportDocument() {
const formats = ['PDF', 'HTML', 'Markdown'];
const format = prompt(`Export format (${formats.join(', ')}):`, 'PDF');
if (formats.includes(format)) {
this.showNotification(`Exported as ${format}`);
}
}
openBatchOperations() {
this.showNotification('Batch operations panel opened');
// In a real app, this would open a complex batch processing interface
}
formatText(type) {
// Simple text formatting simulation
const textEditor = document.getElementById('text-editor');
const selection = textEditor.selectionStart !== textEditor.selectionEnd;
if (selection) {
this.showNotification(`Applied ${type} formatting`);
} else {
this.showNotification(`${type} mode activated`);
}
}
replaceText() {
const findText = document.getElementById('find-input').value;
const replaceText = document.getElementById('replace-input').value;
if (findText) {
const textEditor = document.getElementById('text-editor');
const content = textEditor.value;
const replaced = content.split(findText).join(replaceText);
textEditor.value = replaced;
const count = content.split(findText).length - 1;
this.showNotification(`Replaced ${count} instances`);
}
}
runGrammarCheck() {
this.showNotification('Grammar check completed - 3 suggestions');
}
openMacroEditor() {
this.showNotification('Macro editor opened');
}
updateWordCount() {
const text = document.getElementById('text-editor').value;
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
const chars = text.length;
document.getElementById('word-count').textContent = `Words: ${words}`;
document.getElementById('char-count').textContent = `Characters: ${chars}`;
}
trackEditorUsage() {
// Throttled usage tracking
if (!this.trackingTimeout) {
this.trackingTimeout = setTimeout(() => {
if (this.uiflow) {
// Track as basic usage for typing
this.recordUsage('basic', 'editor');
}
this.trackingTimeout = null;
}, 1000);
}
}
recordUsage(category, area) {
// This would normally be handled automatically by UIFlow
// but we can also manually record specific interactions
const element = document.getElementById('text-editor');
if (element && this.uiflow) {
this.uiflow.recordInteraction(element);
}
}
trackFeatureUsage(data) {
// Send to analytics
console.log('Feature usage:', data);
// In a real app, you'd send this to your analytics service
// analytics.track('editor_feature_usage', data);
}
showNotification(message) {
// Simple notification system
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #333;
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 1000;
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
getUserId() {
// Get or generate user ID
let userId = localStorage.getItem('editor-user-id');
if (!userId) {
userId = 'user-' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('editor-user-id', userId);
}
return userId;
}
}
// Initialize the editor when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new AdaptiveTextEditor();
});
```
## CSS Styles
```css
/* editor.css */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f5f5;
}
.editor-app {
display: grid;
grid-template-areas:
"header header"
"main sidebar";
grid-template-columns: 1fr 250px;
grid-template-rows: auto 1fr;
height: 100vh;
gap: 1px;
}
/* Header */
.header {
grid-area: header;
background: white;
padding: 1rem;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-operations {
display: flex;
gap: 0.5rem;
align-items: center;
}
.btn {
padding: 0.5rem 1rem;
background: #007acc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
background: #005a9e;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Density indicator */
.density-indicator {
display: flex;
gap: 1rem;
font-size: 12px;
color: #666;
}
.density-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.override {
background: #ff9500;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
}
/* Main content */
.main-content {
grid-area: main;
background: white;
display: flex;
flex-direction: column;
overflow: hidden;
}
.editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
#text-editor {
flex: 1;
padding: 1rem;
border: none;
outline: none;
font-size: 14px;
line-height: 1.5;
resize: none;
font-family: 'Monaco', 'Consolas', monospace;
}
/* Formatting toolbar */
.formatting-toolbar {
padding: 0.5rem;
background: #f8f8f8;
border-top: 1px solid #ddd;
display: flex;
gap: 0.5rem;
align-items: center;
transition: all 0.3s ease;
}
.format-btn {
padding: 0.25rem 0.5rem;
background: white;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
min-width: 30px;
}
.format-btn:hover {
background: #e8e8e8;
}
.format-select {
padding: 0.25rem;
border: 1px solid #ddd;
border-radius: 3px;
background: white;
}
/* Advanced and expert panels */
.advanced-panel,
.expert-panel {
padding: 1rem;
background: #fafafa;
border-top: 1px solid #ddd;
transition: all 0.3s ease;
}
.feature-group {
margin-bottom: 1rem;
}
.feature-group h3 {
margin-bottom: 0.5rem;
font-size: 14px;
color: #333;
}
.feature-group input {
padding: 0.25rem;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 0.5rem;
width: 120px;
}
/* Sidebar */
.sidebar {
grid-area: sidebar;
background: white;
padding: 1rem;
border-left: 1px solid #ddd;
overflow-y: auto;
}
.tool-group {
margin-bottom: 1.5rem;
}
.tool-group h3 {
margin-bottom: 0.5rem;
font-size: 14px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 0.25rem;
}
.tool-btn {
display: block;
width: 100%;
padding: 0.5rem;
margin-bottom: 0.25rem;
background: #f8f8f8;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
text-align: left;
font-size: 13px;
transition: all 0.3s ease;
}
.tool-btn:hover {
background: #e8e8e8;
}
/* Manual density controls */
.density-control {
margin-bottom: 0.5rem;
}
.density-control label {
display: block;
font-size: 12px;
margin-bottom: 0.25rem;
text-transform: capitalize;
}
.density-control input[type="range"] {
width: 100%;
margin-bottom: 0.25rem;
}
.density-value {
font-size: 11px;
color: #666;
}
/* UIFlow element transitions */
[data-uiflow-category] {
transition: opacity 0.3s ease, transform 0.3s ease;
}
[data-uiflow-visible="false"] {
opacity: 0;
transform: scale(0.95);
pointer-events: none;
}
[data-uiflow-visible="true"] {
opacity: 1;
transform: scale(1);
}
/* Responsive adjustments based on density */
[data-uiflow-area][data-density="high"] .formatting-toolbar {
padding: 0.25rem;
gap: 0.25rem;
}
[data-uiflow-area][data-density="high"] .btn {
padding: 0.25rem 0.5rem;
font-size: 12px;
}
/* Notification */
.notification {
animation: slideIn 0.3s ease;
}
slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Responsive design */
(max-width: 768px) {
.editor-app {
grid-template-areas:
"header"
"main"
"sidebar";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
.sidebar {
max-height: 200px;
overflow-y: auto;
}
.file-operations {
flex-wrap: wrap;
}
.density-indicator {
display: none;
}
}
```
## Usage Patterns
This editor demonstrates several key UIFlow patterns:
### 1. Progressive Disclosure
- Basic: Essential editing and file operations
- Advanced: Formatting, find/replace, statistics
- Expert: Plugins, macros, customization
### 2. Area-Specific Adaptation
- **Editor area**: Text editing and formatting features
- **File-ops area**: File management operations
- **Tools area**: Navigation and utility functions
### 3. User Education
- Help text for advanced features
- Automatic highlighting of new features
- Progressive feature discovery
### 4. Manual Override
- Density sliders for each area
- Visual indicators for admin overrides
- Immediate feedback on changes
## Customization
You can adapt this example by:
1. **Adding more categories**: Introduce domain-specific feature levels
2. **Custom areas**: Create specialized tool areas
3. **Different learning rates**: Adjust adaptation speed per area
4. **Analytics integration**: Track real usage patterns
5. **A/B testing**: Test different categorization strategies
This example provides a solid foundation for implementing adaptive interfaces in text editors, IDEs, or any content creation tool.