Repository navigation
Expand file tree
/
Copy pathapp.js
More file actions
304 lines (255 loc) · 13 KB
/
Copy pathapp.js
File metadata and controls
304 lines (255 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/* =========================================================================
Perfectly Backwards — shared behaviour
-------------------------------------------------------------------------
All user-facing strings live in I18N and are keyed by <html lang>. Nothing
in here injects untranslated copy.
========================================================================= */
(function () {
'use strict';
var LANG = (document.documentElement.getAttribute('lang') || 'en').slice(0, 2) === 'ar' ? 'ar' : 'en';
var I18N = {
en: {
switchToFixed: 'Fix the direction',
switchToBroken: 'Break it again',
switchHintBroken: 'This page is forced right-to-left. Press to read it the way it was designed.',
switchHintFixed: 'Reading normally. Press to go back to the right-to-left version.',
announceFixed: 'Direction corrected. The page now reads the way it was designed.',
announceBroken: 'Direction reversed. The page is now deliberately backwards.',
knobBroken: 'RTL',
knobFixed: 'LTR',
filterAll: 'All',
filterDone: 'Shipped',
filterPending: 'Pending',
noRows: 'No products match this filter.',
labDefault: 'Deploy v2.1 on 12/03, then ping فريق التشغيل (المناوبة) before 09:00.'
},
ar: {
switchToFixed: 'أصلح الاتجاه',
switchToBroken: 'أعد كسره',
switchHintBroken: 'اتجاه الصفحة معكوس الآن. اضغط لإعادتها إلى وضعها الصحيح.',
switchHintFixed: 'الصفحة تُقرأ بشكل صحيح. اضغط لعكس الاتجاه ورؤية المشكلة.',
/* Direction-neutral: on this page "fixed" is RTL, on the English
page it is LTR. Naming a side here would be wrong on one of them. */
announceFixed: 'تم تصحيح الاتجاه. الصفحة تُقرأ الآن كما صُمِّمت.',
announceBroken: 'تم عكس الاتجاه. الصفحة الآن معكوسة عن قصد.',
knobBroken: 'RTL',
knobFixed: 'LTR',
filterAll: 'الكل',
filterDone: 'مدعوم',
filterPending: 'قيد الانتظار',
noRows: 'لا توجد منتجات مطابقة لهذا الخيار.',
labDefault: 'حدّثت الطلب رقم 4821 بتاريخ 12/03، والرجاء مراجعة Ahmed قبل الساعة 09:00.'
}
};
function t(key) {
return (I18N[LANG] && I18N[LANG][key]) || I18N.en[key] || '';
}
var root = document.documentElement;
/* Keyed per language: choosing "broken" on the English page must never
sabotage the Arabic edition, which is the reference implementation. */
var STORAGE_KEY = 'pb:direction-mode:' + LANG;
/* ---------------------------------------------------------------------
Direction switch
------------------------------------------------------------------- */
function initDirectionSwitch() {
var button = document.querySelector('[data-dir-switch]');
if (!button) { return; }
var brokenDir = button.getAttribute('data-broken-dir') || 'rtl';
var fixedDir = button.getAttribute('data-fixed-dir') || 'ltr';
var label = button.querySelector('[data-dir-label]');
var knob = button.querySelector('[data-dir-knob]');
var live = document.getElementById('dir-announcer');
function apply(mode, announce) {
var fixed = mode === 'fixed';
root.setAttribute('data-mode', mode);
root.setAttribute('dir', fixed ? fixedDir : brokenDir);
button.setAttribute('aria-pressed', String(fixed));
button.setAttribute('title', fixed ? t('switchHintFixed') : t('switchHintBroken'));
if (label) { label.textContent = fixed ? t('switchToBroken') : t('switchToFixed'); }
if (knob) { knob.textContent = fixed ? t('knobFixed') : t('knobBroken'); }
if (announce && live) { live.textContent = fixed ? t('announceFixed') : t('announceBroken'); }
}
var stored = null;
try { stored = window.localStorage.getItem(STORAGE_KEY); } catch (err) { stored = null; }
apply(stored === 'fixed' || stored === 'broken' ? stored : (root.getAttribute('data-mode') || 'broken'), false);
button.addEventListener('click', function () {
var next = root.getAttribute('data-mode') === 'fixed' ? 'broken' : 'fixed';
apply(next, true);
try { window.localStorage.setItem(STORAGE_KEY, next); } catch (err) { /* private mode */ }
});
}
/* ---------------------------------------------------------------------
Bidi lab — the same string, rendered naively vs. rendered correctly
------------------------------------------------------------------- */
function initBidiLab() {
var input = document.querySelector('[data-lab-input]');
var broken = document.querySelector('[data-lab-broken]');
var correct = document.querySelector('[data-lab-correct]');
if (!input || !broken || !correct) { return; }
if (!input.value.trim()) { input.value = t('labDefault'); }
function render() {
var value = input.value;
/* Naive: the whole string inherits one direction, no isolation.
This is exactly what a product does when it sets dir on the
container and stops thinking. */
broken.textContent = value;
/* Correct: base direction comes from the content (dir="auto" on
the container) and each embedded run is isolated, so the Bidi
algorithm resolves neutrals against the right neighbours. */
correct.textContent = '';
splitRuns(value).forEach(function (run) {
/* Whitespace at a run boundary must sit OUTSIDE the isolate,
or it collapses and the two runs weld together. */
var lead = run.text.match(/^\s*/)[0];
var trail = run.text.slice(lead.length).match(/\s*$/)[0];
var core = run.text.slice(lead.length, run.text.length - trail.length);
if (lead) { correct.appendChild(document.createTextNode(lead)); }
if (core) {
var span = document.createElement('span');
span.className = 'sample__run';
span.setAttribute('dir', run.dir);
span.style.unicodeBidi = 'isolate';
span.textContent = core;
correct.appendChild(span);
}
if (trail) { correct.appendChild(document.createTextNode(trail)); }
});
}
input.addEventListener('input', render);
render();
}
var RTL_CHAR = /[--ۿ܀-ݏݐ-ݿࢠ-ࣿיִ-﷿ﹰ-]/;
var LTR_CHAR = /[A-Za-zÀ-ɏ]/;
function splitRuns(text) {
var runs = [];
var current = null;
for (var i = 0; i < text.length; i += 1) {
var ch = text[i];
var dir = RTL_CHAR.test(ch) ? 'rtl' : (LTR_CHAR.test(ch) ? 'ltr' : null);
if (dir === null) {
if (current) { current.text += ch; } else { runs.push({ dir: 'auto', text: ch }); }
continue;
}
if (!current || current.dir !== dir) {
current = { dir: dir, text: ch };
runs.push(current);
} else {
current.text += ch;
}
}
return runs;
}
/* ---------------------------------------------------------------------
Tracker filters
------------------------------------------------------------------- */
function initTracker() {
var trackers = document.querySelectorAll('[data-tracker]');
Array.prototype.forEach.call(trackers, function (tracker) {
var chips = tracker.querySelectorAll('[data-filter]');
var rows = tracker.querySelectorAll('tbody tr[data-status]');
if (!chips.length || !rows.length) { return; }
/* Count once, label the chips from real data. */
var counts = { all: rows.length, done: 0, pending: 0 };
Array.prototype.forEach.call(rows, function (row) {
var status = row.getAttribute('data-status');
if (counts[status] !== undefined) { counts[status] += 1; }
});
Array.prototype.forEach.call(chips, function (chip) {
var key = chip.getAttribute('data-filter');
var count = chip.querySelector('[data-filter-count]');
if (count && counts[key] !== undefined) { count.textContent = String(counts[key]); }
chip.addEventListener('click', function () {
Array.prototype.forEach.call(chips, function (other) {
other.setAttribute('aria-pressed', String(other === chip));
});
var visible = 0;
Array.prototype.forEach.call(rows, function (row) {
var show = key === 'all' || row.getAttribute('data-status') === key;
row.hidden = !show;
if (show) { visible += 1; }
});
var empty = tracker.querySelector('[data-tracker-empty]');
if (empty) {
empty.hidden = visible > 0;
empty.textContent = t('noRows');
}
});
});
});
}
/* ---------------------------------------------------------------------
Scroll progress — fills from the reading-start edge
------------------------------------------------------------------- */
function initProgress() {
var bar = document.querySelector('[data-progress]');
if (!bar) { return; }
var ticking = false;
function update() {
var max = document.documentElement.scrollHeight - window.innerHeight;
var ratio = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
bar.style.setProperty('--progress', (ratio * 100).toFixed(2) + '%');
ticking = false;
}
window.addEventListener('scroll', function () {
if (!ticking) {
ticking = true;
window.requestAnimationFrame(update);
}
}, { passive: true });
update();
}
/* ---------------------------------------------------------------------
Reveals — enhance content that is already visible by default
------------------------------------------------------------------- */
function initReveals() {
var targets = document.querySelectorAll('.js-reveal');
if (!targets.length) { return; }
var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduced || !('IntersectionObserver' in window)) { return; }
var observer = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) { return; }
entry.target.setAttribute('data-reveal', 'in');
observer.unobserve(entry.target);
});
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.05 });
Array.prototype.forEach.call(targets, function (target, index) {
var rect = target.getBoundingClientRect();
/* Anything already on screen at load stays visible — never gate
content behind a transition that may not fire. */
if (rect.top < window.innerHeight * 0.9) {
target.setAttribute('data-reveal', 'in');
return;
}
target.setAttribute('data-reveal', 'pending');
target.style.setProperty('--reveal-delay', (index % 4) * 70 + 'ms');
observer.observe(target);
});
/* Failsafe. A reveal is decoration; the content underneath is the
point. If the observer never fires — hidden tab, headless render,
print, a browser we did not anticipate — show everything anyway
rather than ship a blank section. */
function revealAll() {
Array.prototype.forEach.call(targets, function (target) {
if (target.getAttribute('data-reveal') !== 'in') {
target.setAttribute('data-reveal', 'in');
}
});
observer.disconnect();
}
window.setTimeout(revealAll, 4000);
window.addEventListener('beforeprint', revealAll);
}
function boot() {
initDirectionSwitch();
initBidiLab();
initTracker();
initProgress();
initReveals();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
}());