@sc4rfurryx/proteusjs 1.0.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/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,545 @@
|
|
1
|
+
/**
|
2
|
+
* Flow Layout Engine for ProteusJS
|
3
|
+
* Natural content flow with reading pattern optimization
|
4
|
+
*/
|
5
|
+
|
6
|
+
export interface FlowConfig {
|
7
|
+
pattern: 'z-pattern' | 'f-pattern' | 'gutenberg' | 'natural' | 'custom' | 'auto';
|
8
|
+
direction: 'ltr' | 'rtl' | 'auto';
|
9
|
+
language: string;
|
10
|
+
accessibility: boolean;
|
11
|
+
focusManagement: boolean;
|
12
|
+
skipLinks: boolean;
|
13
|
+
landmarks: boolean;
|
14
|
+
readingOrder: 'visual' | 'logical' | 'auto';
|
15
|
+
customFlow?: FlowStep[];
|
16
|
+
}
|
17
|
+
|
18
|
+
export interface FlowStep {
|
19
|
+
selector: string;
|
20
|
+
priority: number;
|
21
|
+
region: 'header' | 'main' | 'content' | 'footer' | 'navigation' | 'sidebar';
|
22
|
+
tabIndex?: number;
|
23
|
+
ariaLabel?: string;
|
24
|
+
landmark?: string;
|
25
|
+
}
|
26
|
+
|
27
|
+
export interface FlowState {
|
28
|
+
currentPattern: string;
|
29
|
+
direction: 'ltr' | 'rtl';
|
30
|
+
focusableElements: Element[];
|
31
|
+
tabOrder: Element[];
|
32
|
+
landmarks: Map<string, Element>;
|
33
|
+
skipTargets: Element[];
|
34
|
+
}
|
35
|
+
|
36
|
+
export class FlowLayout {
|
37
|
+
private element: Element;
|
38
|
+
private config: Required<FlowConfig>;
|
39
|
+
private state: FlowState;
|
40
|
+
private mutationObserver: MutationObserver | null = null;
|
41
|
+
|
42
|
+
private static readonly READING_PATTERNS = {
|
43
|
+
'z-pattern': [
|
44
|
+
{ selector: 'header, [role="banner"]', region: 'header' as const, priority: 1 },
|
45
|
+
{ selector: 'nav, [role="navigation"]', region: 'navigation' as const, priority: 2 },
|
46
|
+
{ selector: 'main, [role="main"]', region: 'main' as const, priority: 3 },
|
47
|
+
{ selector: 'aside, [role="complementary"]', region: 'sidebar' as const, priority: 4 },
|
48
|
+
{ selector: 'footer, [role="contentinfo"]', region: 'footer' as const, priority: 5 }
|
49
|
+
],
|
50
|
+
'f-pattern': [
|
51
|
+
{ selector: 'header, [role="banner"]', region: 'header' as const, priority: 1 },
|
52
|
+
{ selector: 'main, [role="main"]', region: 'main' as const, priority: 2 },
|
53
|
+
{ selector: 'aside, [role="complementary"]', region: 'sidebar' as const, priority: 3 },
|
54
|
+
{ selector: 'nav, [role="navigation"]', region: 'navigation' as const, priority: 4 },
|
55
|
+
{ selector: 'footer, [role="contentinfo"]', region: 'footer' as const, priority: 5 }
|
56
|
+
],
|
57
|
+
'gutenberg': [
|
58
|
+
{ selector: 'nav, [role="navigation"]', region: 'navigation' as const, priority: 1 },
|
59
|
+
{ selector: 'header, [role="banner"]', region: 'header' as const, priority: 2 },
|
60
|
+
{ selector: 'main, [role="main"]', region: 'main' as const, priority: 3 },
|
61
|
+
{ selector: 'aside, [role="complementary"]', region: 'sidebar' as const, priority: 4 },
|
62
|
+
{ selector: 'footer, [role="contentinfo"]', region: 'footer' as const, priority: 5 }
|
63
|
+
],
|
64
|
+
'natural': [
|
65
|
+
{ selector: 'header, [role="banner"]', region: 'header' as const, priority: 1 },
|
66
|
+
{ selector: 'main, [role="main"]', region: 'main' as const, priority: 2 },
|
67
|
+
{ selector: 'nav, [role="navigation"]', region: 'navigation' as const, priority: 3 },
|
68
|
+
{ selector: 'aside, [role="complementary"]', region: 'sidebar' as const, priority: 4 },
|
69
|
+
{ selector: 'footer, [role="contentinfo"]', region: 'footer' as const, priority: 5 }
|
70
|
+
]
|
71
|
+
};
|
72
|
+
|
73
|
+
private static readonly RTL_LANGUAGES = [
|
74
|
+
'ar', 'he', 'fa', 'ur', 'yi', 'ji', 'iw', 'ku', 'ps', 'sd'
|
75
|
+
];
|
76
|
+
|
77
|
+
constructor(element: Element, config: Partial<FlowConfig> = {}) {
|
78
|
+
this.element = element;
|
79
|
+
this.config = {
|
80
|
+
pattern: 'natural',
|
81
|
+
direction: 'auto',
|
82
|
+
language: 'en',
|
83
|
+
accessibility: true,
|
84
|
+
focusManagement: true,
|
85
|
+
skipLinks: true,
|
86
|
+
landmarks: true,
|
87
|
+
readingOrder: 'auto',
|
88
|
+
customFlow: [],
|
89
|
+
...config
|
90
|
+
};
|
91
|
+
|
92
|
+
this.state = this.createInitialState();
|
93
|
+
this.activate();
|
94
|
+
}
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Activate the flow layout
|
98
|
+
*/
|
99
|
+
public activate(): void {
|
100
|
+
this.analyzeContent();
|
101
|
+
this.applyFlowPattern();
|
102
|
+
this.setupAccessibility();
|
103
|
+
this.setupObservers();
|
104
|
+
}
|
105
|
+
|
106
|
+
/**
|
107
|
+
* Deactivate and clean up
|
108
|
+
*/
|
109
|
+
public deactivate(): void {
|
110
|
+
this.cleanupObservers();
|
111
|
+
this.removeFlowStyles();
|
112
|
+
this.removeAccessibilityFeatures();
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Update flow configuration
|
117
|
+
*/
|
118
|
+
public updateConfig(newConfig: Partial<FlowConfig>): void {
|
119
|
+
this.config = { ...this.config, ...newConfig };
|
120
|
+
this.activate();
|
121
|
+
}
|
122
|
+
|
123
|
+
/**
|
124
|
+
* Get current flow state
|
125
|
+
*/
|
126
|
+
public getState(): FlowState {
|
127
|
+
return { ...this.state };
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Manually set tab order
|
132
|
+
*/
|
133
|
+
public setTabOrder(elements: Element[]): void {
|
134
|
+
elements.forEach((element, index) => {
|
135
|
+
(element as HTMLElement).tabIndex = index + 1;
|
136
|
+
});
|
137
|
+
this.state.tabOrder = elements;
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Add skip link
|
142
|
+
*/
|
143
|
+
public addSkipLink(target: Element, label: string): void {
|
144
|
+
if (!this.config.skipLinks) return;
|
145
|
+
|
146
|
+
const skipLink = document.createElement('a');
|
147
|
+
skipLink.href = `#${this.ensureId(target)}`;
|
148
|
+
skipLink.textContent = label;
|
149
|
+
skipLink.className = 'proteus-skip-link';
|
150
|
+
skipLink.style.cssText = `
|
151
|
+
position: absolute;
|
152
|
+
top: -40px;
|
153
|
+
left: 6px;
|
154
|
+
background: #000;
|
155
|
+
color: #fff;
|
156
|
+
padding: 8px;
|
157
|
+
text-decoration: none;
|
158
|
+
z-index: 1000;
|
159
|
+
transition: top 0.3s;
|
160
|
+
`;
|
161
|
+
|
162
|
+
// Show on focus
|
163
|
+
skipLink.addEventListener('focus', () => {
|
164
|
+
skipLink.style.top = '6px';
|
165
|
+
});
|
166
|
+
skipLink.addEventListener('blur', () => {
|
167
|
+
skipLink.style.top = '-40px';
|
168
|
+
});
|
169
|
+
|
170
|
+
document.body.insertBefore(skipLink, document.body.firstChild);
|
171
|
+
this.state.skipTargets.push(target);
|
172
|
+
}
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Analyze content structure
|
176
|
+
*/
|
177
|
+
private analyzeContent(): void {
|
178
|
+
// Detect reading direction
|
179
|
+
this.state.direction = this.detectReadingDirection();
|
180
|
+
|
181
|
+
// Find focusable elements
|
182
|
+
this.state.focusableElements = this.findFocusableElements();
|
183
|
+
|
184
|
+
// Identify landmarks
|
185
|
+
this.state.landmarks = this.identifyLandmarks();
|
186
|
+
|
187
|
+
// Determine optimal pattern
|
188
|
+
this.state.currentPattern = this.determineOptimalPattern();
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Apply flow pattern
|
193
|
+
*/
|
194
|
+
private applyFlowPattern(): void {
|
195
|
+
let pattern;
|
196
|
+
if (this.config.pattern === 'custom') {
|
197
|
+
pattern = this.config.customFlow;
|
198
|
+
} else if (this.config.pattern === 'auto') {
|
199
|
+
const optimalPattern = this.determineOptimalPattern();
|
200
|
+
pattern = FlowLayout.READING_PATTERNS[optimalPattern as keyof typeof FlowLayout.READING_PATTERNS];
|
201
|
+
} else {
|
202
|
+
pattern = FlowLayout.READING_PATTERNS[this.config.pattern];
|
203
|
+
}
|
204
|
+
|
205
|
+
if (!pattern) return;
|
206
|
+
|
207
|
+
// Sort elements by pattern priority
|
208
|
+
const sortedElements = this.sortElementsByPattern(pattern);
|
209
|
+
|
210
|
+
// Apply visual flow
|
211
|
+
this.applyVisualFlow(sortedElements);
|
212
|
+
|
213
|
+
// Set logical tab order
|
214
|
+
if (this.config.focusManagement) {
|
215
|
+
this.setLogicalTabOrder(sortedElements);
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
/**
|
220
|
+
* Setup accessibility features
|
221
|
+
*/
|
222
|
+
private setupAccessibility(): void {
|
223
|
+
if (!this.config.accessibility) return;
|
224
|
+
|
225
|
+
// Add landmarks
|
226
|
+
if (this.config.landmarks) {
|
227
|
+
this.addLandmarks();
|
228
|
+
}
|
229
|
+
|
230
|
+
// Add skip links
|
231
|
+
if (this.config.skipLinks) {
|
232
|
+
this.addDefaultSkipLinks();
|
233
|
+
}
|
234
|
+
|
235
|
+
// Set reading order
|
236
|
+
this.setReadingOrder();
|
237
|
+
}
|
238
|
+
|
239
|
+
/**
|
240
|
+
* Detect reading direction
|
241
|
+
*/
|
242
|
+
private detectReadingDirection(): 'ltr' | 'rtl' {
|
243
|
+
if (this.config.direction !== 'auto') {
|
244
|
+
return this.config.direction;
|
245
|
+
}
|
246
|
+
|
247
|
+
const langCode = this.config.language?.split('-')[0]?.toLowerCase();
|
248
|
+
return langCode && FlowLayout.RTL_LANGUAGES.includes(langCode) ? 'rtl' : 'ltr';
|
249
|
+
}
|
250
|
+
|
251
|
+
/**
|
252
|
+
* Find all focusable elements
|
253
|
+
*/
|
254
|
+
private findFocusableElements(): Element[] {
|
255
|
+
const focusableSelectors = [
|
256
|
+
'a[href]',
|
257
|
+
'button:not([disabled])',
|
258
|
+
'input:not([disabled])',
|
259
|
+
'select:not([disabled])',
|
260
|
+
'textarea:not([disabled])',
|
261
|
+
'[tabindex]:not([tabindex="-1"])',
|
262
|
+
'[contenteditable="true"]'
|
263
|
+
].join(', ');
|
264
|
+
|
265
|
+
return Array.from(this.element.querySelectorAll(focusableSelectors));
|
266
|
+
}
|
267
|
+
|
268
|
+
/**
|
269
|
+
* Identify semantic landmarks
|
270
|
+
*/
|
271
|
+
private identifyLandmarks(): Map<string, Element> {
|
272
|
+
const landmarks = new Map<string, Element>();
|
273
|
+
|
274
|
+
const landmarkSelectors = {
|
275
|
+
'banner': 'header, [role="banner"]',
|
276
|
+
'main': 'main, [role="main"]',
|
277
|
+
'navigation': 'nav, [role="navigation"]',
|
278
|
+
'complementary': 'aside, [role="complementary"]',
|
279
|
+
'contentinfo': 'footer, [role="contentinfo"]',
|
280
|
+
'search': '[role="search"]',
|
281
|
+
'form': 'form, [role="form"]'
|
282
|
+
};
|
283
|
+
|
284
|
+
Object.entries(landmarkSelectors).forEach(([role, selector]) => {
|
285
|
+
const element = this.element.querySelector(selector);
|
286
|
+
if (element) {
|
287
|
+
landmarks.set(role, element);
|
288
|
+
}
|
289
|
+
});
|
290
|
+
|
291
|
+
return landmarks;
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Determine optimal reading pattern
|
296
|
+
*/
|
297
|
+
private determineOptimalPattern(): string {
|
298
|
+
if (this.config.pattern !== 'auto') {
|
299
|
+
return this.config.pattern;
|
300
|
+
}
|
301
|
+
|
302
|
+
// Analyze layout structure to suggest pattern
|
303
|
+
const hasHeader = this.state.landmarks.has('banner');
|
304
|
+
const hasSidebar = this.state.landmarks.has('complementary');
|
305
|
+
const hasNav = this.state.landmarks.has('navigation');
|
306
|
+
|
307
|
+
if (hasHeader && hasSidebar && hasNav) {
|
308
|
+
return 'z-pattern';
|
309
|
+
} else if (hasHeader && hasSidebar) {
|
310
|
+
return 'f-pattern';
|
311
|
+
} else {
|
312
|
+
return 'natural';
|
313
|
+
}
|
314
|
+
}
|
315
|
+
|
316
|
+
/**
|
317
|
+
* Sort elements by pattern priority
|
318
|
+
*/
|
319
|
+
private sortElementsByPattern(pattern: FlowStep[]): Element[] {
|
320
|
+
const elements: Array<{ element: Element; priority: number }> = [];
|
321
|
+
|
322
|
+
pattern.forEach(step => {
|
323
|
+
const stepElements = Array.from(this.element.querySelectorAll(step.selector));
|
324
|
+
stepElements.forEach(element => {
|
325
|
+
elements.push({ element, priority: step.priority });
|
326
|
+
});
|
327
|
+
});
|
328
|
+
|
329
|
+
// Sort by priority, then by DOM order
|
330
|
+
return elements
|
331
|
+
.sort((a, b) => {
|
332
|
+
if (a.priority !== b.priority) {
|
333
|
+
return a.priority - b.priority;
|
334
|
+
}
|
335
|
+
// Use DOM order as tiebreaker
|
336
|
+
return a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
|
337
|
+
})
|
338
|
+
.map(item => item.element);
|
339
|
+
}
|
340
|
+
|
341
|
+
/**
|
342
|
+
* Apply visual flow styles
|
343
|
+
*/
|
344
|
+
private applyVisualFlow(elements: Element[]): void {
|
345
|
+
elements.forEach((element, index) => {
|
346
|
+
const htmlElement = element as HTMLElement;
|
347
|
+
htmlElement.style.setProperty('--flow-order', index.toString());
|
348
|
+
htmlElement.classList.add('proteus-flow-item');
|
349
|
+
});
|
350
|
+
|
351
|
+
// Add CSS for visual flow
|
352
|
+
this.addFlowCSS();
|
353
|
+
}
|
354
|
+
|
355
|
+
/**
|
356
|
+
* Set logical tab order
|
357
|
+
*/
|
358
|
+
private setLogicalTabOrder(elements: Element[]): void {
|
359
|
+
const focusableInOrder = elements.filter(el =>
|
360
|
+
this.state.focusableElements.includes(el)
|
361
|
+
);
|
362
|
+
|
363
|
+
focusableInOrder.forEach((element, index) => {
|
364
|
+
(element as HTMLElement).tabIndex = index + 1;
|
365
|
+
});
|
366
|
+
|
367
|
+
this.state.tabOrder = focusableInOrder;
|
368
|
+
}
|
369
|
+
|
370
|
+
/**
|
371
|
+
* Add landmark roles and labels
|
372
|
+
*/
|
373
|
+
private addLandmarks(): void {
|
374
|
+
this.state.landmarks.forEach((element, role) => {
|
375
|
+
const htmlElement = element as HTMLElement;
|
376
|
+
|
377
|
+
if (!htmlElement.getAttribute('role')) {
|
378
|
+
htmlElement.setAttribute('role', role);
|
379
|
+
}
|
380
|
+
|
381
|
+
if (!htmlElement.getAttribute('aria-label') && !htmlElement.getAttribute('aria-labelledby')) {
|
382
|
+
const label = this.generateLandmarkLabel(role);
|
383
|
+
htmlElement.setAttribute('aria-label', label);
|
384
|
+
}
|
385
|
+
});
|
386
|
+
}
|
387
|
+
|
388
|
+
/**
|
389
|
+
* Add default skip links
|
390
|
+
*/
|
391
|
+
private addDefaultSkipLinks(): void {
|
392
|
+
const mainContent = this.state.landmarks.get('main');
|
393
|
+
if (mainContent) {
|
394
|
+
this.addSkipLink(mainContent, 'Skip to main content');
|
395
|
+
}
|
396
|
+
|
397
|
+
const navigation = this.state.landmarks.get('navigation');
|
398
|
+
if (navigation) {
|
399
|
+
this.addSkipLink(navigation, 'Skip to navigation');
|
400
|
+
}
|
401
|
+
}
|
402
|
+
|
403
|
+
/**
|
404
|
+
* Set reading order for screen readers
|
405
|
+
*/
|
406
|
+
private setReadingOrder(): void {
|
407
|
+
if (this.config.readingOrder === 'visual') {
|
408
|
+
// Use CSS order for screen readers
|
409
|
+
(this.element as HTMLElement).style.setProperty('--reading-order', 'visual');
|
410
|
+
} else {
|
411
|
+
// Maintain logical DOM order
|
412
|
+
(this.element as HTMLElement).style.setProperty('--reading-order', 'logical');
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
/**
|
417
|
+
* Generate landmark label
|
418
|
+
*/
|
419
|
+
private generateLandmarkLabel(role: string): string {
|
420
|
+
const labels = {
|
421
|
+
'banner': 'Site header',
|
422
|
+
'main': 'Main content',
|
423
|
+
'navigation': 'Site navigation',
|
424
|
+
'complementary': 'Sidebar',
|
425
|
+
'contentinfo': 'Site footer',
|
426
|
+
'search': 'Search',
|
427
|
+
'form': 'Form'
|
428
|
+
};
|
429
|
+
|
430
|
+
return labels[role as keyof typeof labels] || role;
|
431
|
+
}
|
432
|
+
|
433
|
+
/**
|
434
|
+
* Add CSS for flow layout
|
435
|
+
*/
|
436
|
+
private addFlowCSS(): void {
|
437
|
+
const styleId = 'proteus-flow-styles';
|
438
|
+
if (document.getElementById(styleId)) return;
|
439
|
+
|
440
|
+
const style = document.createElement('style');
|
441
|
+
style.id = styleId;
|
442
|
+
style.textContent = `
|
443
|
+
.proteus-flow-item {
|
444
|
+
order: var(--flow-order, 0);
|
445
|
+
}
|
446
|
+
|
447
|
+
.proteus-skip-link:focus {
|
448
|
+
clip: auto !important;
|
449
|
+
height: auto !important;
|
450
|
+
margin: 0 !important;
|
451
|
+
overflow: visible !important;
|
452
|
+
position: absolute !important;
|
453
|
+
width: auto !important;
|
454
|
+
}
|
455
|
+
|
456
|
+
[dir="rtl"] .proteus-flow-item {
|
457
|
+
direction: rtl;
|
458
|
+
}
|
459
|
+
`;
|
460
|
+
|
461
|
+
document.head.appendChild(style);
|
462
|
+
}
|
463
|
+
|
464
|
+
/**
|
465
|
+
* Ensure element has an ID
|
466
|
+
*/
|
467
|
+
private ensureId(element: Element): string {
|
468
|
+
if (!element.id) {
|
469
|
+
element.id = `proteus-flow-${Math.random().toString(36).substring(2, 11)}`;
|
470
|
+
}
|
471
|
+
return element.id;
|
472
|
+
}
|
473
|
+
|
474
|
+
/**
|
475
|
+
* Setup observers
|
476
|
+
*/
|
477
|
+
private setupObservers(): void {
|
478
|
+
this.mutationObserver = new MutationObserver(() => {
|
479
|
+
this.analyzeContent();
|
480
|
+
this.applyFlowPattern();
|
481
|
+
});
|
482
|
+
|
483
|
+
this.mutationObserver.observe(this.element, {
|
484
|
+
childList: true,
|
485
|
+
subtree: true,
|
486
|
+
attributes: true,
|
487
|
+
attributeFilter: ['role', 'tabindex']
|
488
|
+
});
|
489
|
+
}
|
490
|
+
|
491
|
+
/**
|
492
|
+
* Clean up observers
|
493
|
+
*/
|
494
|
+
private cleanupObservers(): void {
|
495
|
+
if (this.mutationObserver) {
|
496
|
+
this.mutationObserver.disconnect();
|
497
|
+
this.mutationObserver = null;
|
498
|
+
}
|
499
|
+
}
|
500
|
+
|
501
|
+
/**
|
502
|
+
* Remove flow styles
|
503
|
+
*/
|
504
|
+
private removeFlowStyles(): void {
|
505
|
+
const flowItems = this.element.querySelectorAll('.proteus-flow-item');
|
506
|
+
flowItems.forEach(item => {
|
507
|
+
const htmlItem = item as HTMLElement;
|
508
|
+
htmlItem.classList.remove('proteus-flow-item');
|
509
|
+
htmlItem.style.removeProperty('--flow-order');
|
510
|
+
});
|
511
|
+
|
512
|
+
const styleElement = document.getElementById('proteus-flow-styles');
|
513
|
+
if (styleElement) {
|
514
|
+
styleElement.remove();
|
515
|
+
}
|
516
|
+
}
|
517
|
+
|
518
|
+
/**
|
519
|
+
* Remove accessibility features
|
520
|
+
*/
|
521
|
+
private removeAccessibilityFeatures(): void {
|
522
|
+
// Remove skip links
|
523
|
+
const skipLinks = document.querySelectorAll('.proteus-skip-link');
|
524
|
+
skipLinks.forEach(link => link.remove());
|
525
|
+
|
526
|
+
// Reset tab indices
|
527
|
+
this.state.tabOrder.forEach(element => {
|
528
|
+
(element as HTMLElement).removeAttribute('tabindex');
|
529
|
+
});
|
530
|
+
}
|
531
|
+
|
532
|
+
/**
|
533
|
+
* Create initial state
|
534
|
+
*/
|
535
|
+
private createInitialState(): FlowState {
|
536
|
+
return {
|
537
|
+
currentPattern: 'natural',
|
538
|
+
direction: 'ltr',
|
539
|
+
focusableElements: [],
|
540
|
+
tabOrder: [],
|
541
|
+
landmarks: new Map(),
|
542
|
+
skipTargets: []
|
543
|
+
};
|
544
|
+
}
|
545
|
+
}
|