kreftjs 0.0.20-beta → 0.0.21-beta
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/README.md +10 -1
- package/bin/backend/http.js +6 -2
- package/bin/backend/index.js +4 -0
- package/bin/backend/stream.js +74 -0
- package/bin/build.js +12 -5
- package/bin/kreftjs.js +35 -11
- package/book/ar/16-virtual-list.md +131 -0
- package/book/ar/17-combobox.md +169 -0
- package/book/ar/18-tabs-offline.md +168 -0
- package/book/ar/19-wizard.md +196 -0
- package/book/ar/20-pipes-match-props.md +75 -0
- package/book/ar/README.md +12 -1
- package/book/kreft-book-ar.docx +0 -0
- package/book/~$eft-book-ar.docx +0 -0
- package/docs/README.md +110 -12
- package/editors/sublime/Comments.tmPreferences +23 -0
- package/editors/sublime/Component.sublime-snippet +20 -0
- package/editors/sublime/Kreft.sublime-syntax +10 -0
- package/editors/sublime/LICENSE +95 -0
- package/editors/sublime/README.md +7 -0
- package/editors/sublime/action.sublime-snippet +10 -0
- package/editors/sublime/derive.sublime-snippet +8 -0
- package/editors/sublime/for.sublime-snippet +12 -0
- package/editors/sublime/k-case.sublime-snippet +8 -0
- package/editors/sublime/k-if.sublime-snippet +8 -0
- package/editors/sublime/k-match.sublime-snippet +12 -0
- package/editors/sublime/kreft-wait.sublime-snippet +12 -0
- package/editors/sublime/layout.sublime-snippet +8 -0
- package/editors/sublime/load.sublime-snippet +8 -0
- package/editors/sublime/on.sublime-snippet +10 -0
- package/editors/sublime/pipe.sublime-snippet +8 -0
- package/editors/sublime/prop-typed.sublime-snippet +8 -0
- package/editors/sublime/saved.sublime-snippet +8 -0
- package/editors/sublime/section.sublime-snippet +10 -0
- package/editors/sublime/state.sublime-snippet +8 -0
- package/editors/sublime/watch.sublime-snippet +10 -0
- package/editors/sublime/when.sublime-snippet +10 -0
- package/editors/sublime/yield.sublime-snippet +8 -0
- package/editors/vscode/snippets/kreftjs.json +143 -15
- package/editors/zed/LICENSE +95 -0
- package/editors/zed/README.md +17 -0
- package/editors/zed/extension.toml +11 -0
- package/editors/zed/languages/kreft/brackets.scm +4 -0
- package/editors/zed/languages/kreft/config.toml +17 -0
- package/editors/zed/languages/kreft/highlights.scm +18 -0
- package/editors/zed/languages/kreft/indents.scm +5 -0
- package/editors/zed/languages/kreft/injections.scm +14 -0
- package/editors/zed/languages/kreft/outline.scm +3 -0
- package/kreftjs.d.ts +1 -1
- package/kreftjs.js +219 -14
- package/package.json +2 -2
- package/server.d.ts +2 -0
- package/templates/app/frontend/kreftjs.d.ts +1 -1
- package/templates/app/frontend/kreftjs.js +219 -14
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# الفصل الثامن عشر (خبير): مزامنة بين التبويبات والعمل دون اتصال
|
|
2
|
+
|
|
3
|
+
Kreft يحفظ الحالة المشتركة بين التبويبات **تلقائيًا** (راجع `saved` في الفصل الثالث): غيّر قيمة في تبويب فتراها في الآخر. هنا نبني فوق ذلك طبقتين احترافيتين:
|
|
4
|
+
|
|
5
|
+
1. **حضور (Presence):** من المفتوح الآن؟ رسائل حيّة بين التبويبات عبر `BroadcastChannel`.
|
|
6
|
+
2. **طابور دون اتصال (Offline queue):** الأوامر تُحفظ وتُرسل عند عودة الاتصال، دون أن يفقد المستخدم ما كتبه.
|
|
7
|
+
|
|
8
|
+
**الشيفرة جُرِّبت** بفتح تبويبين وحجب الشبكة.
|
|
9
|
+
|
|
10
|
+
## 1) المنطق المشترك: الحضور بين التبويبات
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
// logic/tabs.js — من مفتوح الآن في "الغرفة"؟
|
|
14
|
+
export function usePresence(room = 'default') {
|
|
15
|
+
const channel = new BroadcastChannel('kreft-' + room)
|
|
16
|
+
const me = Math.random().toString(36).slice(2, 8)
|
|
17
|
+
const s = Kreftjs.state({ peers: [] })
|
|
18
|
+
|
|
19
|
+
const ping = () => channel.postMessage({ type: 'ping', id: me, at: Date.now() })
|
|
20
|
+
|
|
21
|
+
channel.onmessage = (e) => {
|
|
22
|
+
if (e.data.id === me) return // رسالتنا نحن
|
|
23
|
+
if (e.data.type === 'ping') {
|
|
24
|
+
s.peers = [...s.peers.filter((p) => p.id !== e.data.id), { id: e.data.id, at: e.data.at }]
|
|
25
|
+
} else if (e.data.type === 'bye') {
|
|
26
|
+
s.peers = s.peers.filter((p) => p.id !== e.data.id)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ping()
|
|
31
|
+
const timer = setInterval(() => {
|
|
32
|
+
ping()
|
|
33
|
+
s.peers = s.peers.filter((p) => Date.now() - p.at < 6000) // نسقط الصامت منذ 6 ثوانٍ
|
|
34
|
+
}, 3000)
|
|
35
|
+
|
|
36
|
+
window.addEventListener('beforeunload', () => channel.postMessage({ type: 'bye', id: me }))
|
|
37
|
+
Kreftjs.onDestroy(() => {
|
|
38
|
+
clearInterval(timer)
|
|
39
|
+
channel.postMessage({ type: 'bye', id: me })
|
|
40
|
+
channel.close()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
get peers() { return s.peers },
|
|
45
|
+
get count() { return s.peers.length },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- كل تبويب يبثّ `ping` كل 3 ثوانٍ، والآخرون يجدّدون وجوده.
|
|
51
|
+
- التبويب الصامت (أُغلق أو جُمّد) يختفي بعد 6 ثوانٍ.
|
|
52
|
+
- `Kreftjs.onDestroy` ينظّف المؤقت والقناة حين يزول المكوّن — وكما في الفصل الثالث، المستمع المضاف في `on ready`/`on start` يُنظَّف وحده، أما هنا ننظّف يدويًا لأننا في ملف `.js`.
|
|
53
|
+
|
|
54
|
+
## 2) المنطق المشترك: طابور دون اتصال
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
// logic/offline.js — الأمر يُحفظ ويُرسل عند عودة الاتصال
|
|
58
|
+
export function useOfflineQueue() {
|
|
59
|
+
const s = Kreftjs.state({
|
|
60
|
+
online: navigator.onLine,
|
|
61
|
+
pending: Kreftjs.storage.get('offline-queue') || [],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
async function flush() {
|
|
65
|
+
const list = s.pending
|
|
66
|
+
if (!list.length) return
|
|
67
|
+
const done = []
|
|
68
|
+
for (const item of list) {
|
|
69
|
+
try {
|
|
70
|
+
await api.post(item.url, item.body)
|
|
71
|
+
done.push(item)
|
|
72
|
+
} catch (e) {
|
|
73
|
+
break // ما زلنا دون اتصال: توقف وأبقِ الباقي
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
s.pending = list.filter((x) => !done.includes(x))
|
|
77
|
+
Kreftjs.storage.set('offline-queue', s.pending)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function enqueue(url, body) {
|
|
81
|
+
s.pending = [...s.pending, { url, body }]
|
|
82
|
+
Kreftjs.storage.set('offline-queue', s.pending)
|
|
83
|
+
if (s.online) flush()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
window.addEventListener('online', () => { s.online = true; flush() })
|
|
87
|
+
window.addEventListener('offline', () => { s.online = false })
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
get online() { return s.online },
|
|
91
|
+
get pending() { return s.pending },
|
|
92
|
+
enqueue, flush,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- الطابور محفوظ في `Kreftjs.storage`، فينجو من إغلاق التبويب والجهاز.
|
|
98
|
+
- `flush` يرسل بالترتيب ويتوقف عند أول فشل (لا يرسل ما بعده خارج الترتيب).
|
|
99
|
+
- لا نرسل في كل مرة مباشرة: الأوامر تتكدس ثم تُغسل دفعة عند عودة الشبكة.
|
|
100
|
+
|
|
101
|
+
## 3) تطبيق ملاحظات يجمع الاثنين
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<!-- pages/Sync.kreft -->
|
|
105
|
+
import { usePresence } from '../logic/tabs.js'
|
|
106
|
+
import { useOfflineQueue } from '../logic/offline.js'
|
|
107
|
+
|
|
108
|
+
use presence = usePresence('notes')
|
|
109
|
+
use queue = useOfflineQueue()
|
|
110
|
+
|
|
111
|
+
// saved يتزامن بين التبويبات وحده (الفصل الثالث)
|
|
112
|
+
saved notes = []
|
|
113
|
+
saved draft = ''
|
|
114
|
+
|
|
115
|
+
action add() {
|
|
116
|
+
if (!draft.trim()) return
|
|
117
|
+
const text = draft
|
|
118
|
+
notes = [{ id: Date.now(), text }, ...notes]
|
|
119
|
+
draft = ''
|
|
120
|
+
queue.enqueue('/api/notes', { text })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
<view>
|
|
124
|
+
<h1>مزامنة حيّة</h1>
|
|
125
|
+
|
|
126
|
+
<p>
|
|
127
|
+
التبويبات المفتوحة الآن: <b>{presence.count}</b>
|
|
128
|
+
<span k-if="!queue.online" class="offline">· دون اتصال، ستُرسل لاحقًا</span>
|
|
129
|
+
</p>
|
|
130
|
+
|
|
131
|
+
<div class="row">
|
|
132
|
+
<input k-model="draft" @keydown.enter="add()" placeholder="اكتب ملاحظة…">
|
|
133
|
+
<button @click="add">أضف</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{for n in notes key n.id}
|
|
137
|
+
<p class="note">{n.text}</p>
|
|
138
|
+
{empty}
|
|
139
|
+
<p>لا ملاحظات بعد.</p>
|
|
140
|
+
{/for}
|
|
141
|
+
|
|
142
|
+
<p k-if="queue.pending.length" class="pending">في الانتظار: {queue.pending.length} عنصر</p>
|
|
143
|
+
</view>
|
|
144
|
+
|
|
145
|
+
<style scoped>
|
|
146
|
+
.row { display: flex; gap: 8px; }
|
|
147
|
+
.offline { color: var(--danger, #b33); }
|
|
148
|
+
.pending { opacity: .7; }
|
|
149
|
+
</style>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 4) ما لاحظناه أثناء الاختبار
|
|
153
|
+
|
|
154
|
+
- فتحنا التبويب نفسه مرتين: إضافة ملاحظة في الأول ظهرت في الثاني **فورًا** (بفضل `saved`).
|
|
155
|
+
- عدّاد "التبويبات المفتوحة" أصبح 1 ثم عاد 0 عند إغلاق التبويب الثاني (بعد 6 ثوانٍ كحد أقصى).
|
|
156
|
+
- بحجب الشبكة (DevTools › Network › Offline) ثم الإضافة: ظهرت الملاحظة محليًا وعداد "في الانتظار" = 1، وعند إعادة الاتصال أُرسل الطلب واختفى العداد.
|
|
157
|
+
|
|
158
|
+
## تنبيهات
|
|
159
|
+
|
|
160
|
+
- `BroadcastChannel` يعمل بين تبويبات **نفس الموقع** فقط، ولا يعمل بين متصفحين مختلفين.
|
|
161
|
+
- مزامنة `saved` بين التبويبات تقوم على حدث `storage` القياسي، فلا تغيّر القيمة المفتاحية (`as '…'`) إلا بقصد.
|
|
162
|
+
- الطابور هنا يحفظ الطلبات بلا مصادقة إضافية: في تطبيق حقيقي أضف رمز المستخدم ورتّب الأوامر، وفكّر في تعارض "آخر من يكتب يفوز".
|
|
163
|
+
|
|
164
|
+
## تمارين
|
|
165
|
+
|
|
166
|
+
1. أضف إلى `usePresence` اسم المستخدم ليعرض "سارة تعدّل الآن" بدل معرّف عشوائي.
|
|
167
|
+
2. اجعل الطابور يرسل أيضًا الأوامر المحذوفة (`DELETE`) بترتيبها مع الإنشاء.
|
|
168
|
+
3. أضف مؤشر "قيد المزامنة" دوّارًا يظهر أثناء `flush` وزر "أعد المحاولة الآن".
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# الفصل التاسع عشر (خبير): معالج إعداد متعدد الخطوات مع استئناف وحراسة المسارات
|
|
2
|
+
|
|
3
|
+
المعالج (Wizard) يجمع كل مهارات هذا الكتاب في مثال واحد: مخزن محفوظ للاستئناف، ومكوّنات متعددة تتبدل بـ `keep`، وتحقق لكل خطوة قبل الانتقال، وحراسة للمسارات تمنع القفز، وحركة خفيفة. سنبني "إعداد الحساب" من ثلاث خطوات.
|
|
4
|
+
|
|
5
|
+
**جُرِّبت الشيفرة** خطوة خطوة في المتصفح.
|
|
6
|
+
|
|
7
|
+
## 1) المخزن المحفوظ (القلب النابض)
|
|
8
|
+
|
|
9
|
+
حالة المعالج تعيش في مخزن واحد محفوظ، فتنجو من إعادة التحميل وتُشارك بين الصفحات:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
// logic/wizard.js — أنشئه مرة قبل الموجِّه
|
|
13
|
+
Kreftjs.store('wizard', {
|
|
14
|
+
step: 1,
|
|
15
|
+
data: { name: '', email: '', plan: 'basic' },
|
|
16
|
+
completed: false,
|
|
17
|
+
next() { if (this.step < 3) this.step++ },
|
|
18
|
+
prev() { if (this.step > 1) this.step-- },
|
|
19
|
+
finish() { this.completed = true },
|
|
20
|
+
}, { saved: true })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `saved: true` يحفظ `step` و`data` و`completed` في `localStorage` (الدوال لا تُحفظ).
|
|
24
|
+
- كل مكوّن يكتب `use wizard` فيرى الحالة نفسها ويغيّرها، وكل تغيير يحدّث كل من يقرؤها.
|
|
25
|
+
|
|
26
|
+
## 2) الخطوات الثلاث
|
|
27
|
+
|
|
28
|
+
كل خطوة مكوّن مستقل يربط حقوله مباشرة بالمخزن:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<!-- pages/wizard/Step1.kreft -->
|
|
32
|
+
use wizard
|
|
33
|
+
|
|
34
|
+
<view>
|
|
35
|
+
<h2>١) معلوماتك</h2>
|
|
36
|
+
<label>الاسم
|
|
37
|
+
<input k-model="wizard.data.name" placeholder="اسمك الكامل">
|
|
38
|
+
</label>
|
|
39
|
+
<label>البريد
|
|
40
|
+
<input type="email" k-model="wizard.data.email" placeholder="you@example.com">
|
|
41
|
+
</label>
|
|
42
|
+
</view>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<!-- pages/wizard/Step2.kreft -->
|
|
47
|
+
use wizard
|
|
48
|
+
|
|
49
|
+
<view>
|
|
50
|
+
<h2>٢) خطتك</h2>
|
|
51
|
+
<label><input type="radio" value="basic" k-model="wizard.data.plan"> أساسية</label>
|
|
52
|
+
<label><input type="radio" value="pro" k-model="wizard.data.plan"> احترافية</label>
|
|
53
|
+
</view>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<!-- pages/wizard/Step3.kreft -->
|
|
58
|
+
use wizard
|
|
59
|
+
|
|
60
|
+
<view>
|
|
61
|
+
<h2>٣) راجع بياناتك</h2>
|
|
62
|
+
<dl>
|
|
63
|
+
<dt>الاسم</dt><dd>{wizard.data.name}</dd>
|
|
64
|
+
<dt>البريد</dt><dd>{wizard.data.email}</dd>
|
|
65
|
+
<dt>الخطة</dt><dd>{wizard.data.plan}</dd>
|
|
66
|
+
</dl>
|
|
67
|
+
</view>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 3) صفحة المعالج
|
|
71
|
+
|
|
72
|
+
```html
|
|
73
|
+
<!-- pages/wizard/Wizard.kreft -->
|
|
74
|
+
use wizard
|
|
75
|
+
|
|
76
|
+
state errors = {}
|
|
77
|
+
|
|
78
|
+
const stepRules = {
|
|
79
|
+
1: { name: 'required|min:2', email: 'required|email' },
|
|
80
|
+
2: { plan: 'required' },
|
|
81
|
+
3: {},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
action next() {
|
|
85
|
+
errors = validate(wizard.data, stepRules[wizard.step] || {})
|
|
86
|
+
if (Object.keys(errors).length) return // أبقِ المستخدم في خطوته
|
|
87
|
+
wizard.next()
|
|
88
|
+
}
|
|
89
|
+
action prev() { wizard.prev() }
|
|
90
|
+
action finish() { wizard.finish(); $router.go('/wizard/done') }
|
|
91
|
+
|
|
92
|
+
derive pct = Math.round(((wizard.step - 1) / 2) * 100)
|
|
93
|
+
|
|
94
|
+
<view>
|
|
95
|
+
<h1>إعداد الحساب</h1>
|
|
96
|
+
|
|
97
|
+
<div class="progress" aria-hidden="true">
|
|
98
|
+
<div class="bar" style="width: {pct}%"></div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- keep يبقي حالة كل خطوة حين نعود إليها -->
|
|
102
|
+
<kreft-component :is="'Step' + wizard.step" keep></kreft-component>
|
|
103
|
+
|
|
104
|
+
<p k-if="Object.keys(errors).length" class="errors">
|
|
105
|
+
{Object.values(errors).flat().join(' · ')}
|
|
106
|
+
</p>
|
|
107
|
+
|
|
108
|
+
<div class="actions">
|
|
109
|
+
<button k-if="wizard.step > 1" @click="prev">السابق</button>
|
|
110
|
+
<button k-if="wizard.step < 3" class="primary" @click="next">التالي</button>
|
|
111
|
+
<button k-if="wizard.step === 3" class="primary" @click="finish">إنهاء</button>
|
|
112
|
+
</div>
|
|
113
|
+
</view>
|
|
114
|
+
|
|
115
|
+
<style scoped>
|
|
116
|
+
.progress { height: 8px; background: var(--border, #ddd); border-radius: 4px; overflow: hidden; }
|
|
117
|
+
.bar { height: 100%; background: var(--accent, #4a7); transition: width .25s ease; }
|
|
118
|
+
.actions { display: flex; gap: 8px; margin-block-start: 16px; }
|
|
119
|
+
.errors { color: var(--danger, #b33); }
|
|
120
|
+
</style>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **`validate`** قبل `wizard.next()`: لا ينتقل المستخدم إلا بعد أن تسلم حقول خطوته.
|
|
124
|
+
- **`keep`** على `<kreft-component :is>` يبقي ما كتبه المستخدم في الخطوة 1 حين يعود إليها من الخطوة 2.
|
|
125
|
+
- **`derive pct`** يحرّك شريط التقدم وحده.
|
|
126
|
+
|
|
127
|
+
## 4) التسجيل والحراسة
|
|
128
|
+
|
|
129
|
+
سجّل مكوّنات الخطوات وأنشئ الموجّه (أنشئ المخزن أولًا):
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
Kreftjs.store('wizard', { /* كما أعلاه */ }, { saved: true })
|
|
133
|
+
|
|
134
|
+
const wizard = Kreftjs.store('wizard') // نقرؤه في الحراسة
|
|
135
|
+
|
|
136
|
+
const router = Kreftjs.router({
|
|
137
|
+
routes: [
|
|
138
|
+
{ path: '/wizard', component: 'pages/wizard/Wizard.kreft', guard: () => auth.user ? true : '/login' },
|
|
139
|
+
{ path: '/wizard/done', component: 'pages/wizard/Done.kreft', guard: () => wizard.completed ? true : '/wizard' },
|
|
140
|
+
],
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
Kreftjs.app('App.kreft')
|
|
144
|
+
.component('Step1', 'pages/wizard/Step1.kreft')
|
|
145
|
+
.component('Step2', 'pages/wizard/Step2.kreft')
|
|
146
|
+
.component('Step3', 'pages/wizard/Step3.kreft')
|
|
147
|
+
.use(router)
|
|
148
|
+
.mount('#app')
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- حراسة `/wizard` تتطلب الدخول، وحراسة `/wizard/done` تمنع فتح صفحة الإنجاز قبل `finish()`: من يكتب العنوان مباشرة يُعاد إلى `/wizard`.
|
|
152
|
+
- `guard` يعيد `true` للمرور أو مسارًا للذهاب إليه (راجع الفصل الرابع).
|
|
153
|
+
|
|
154
|
+
## 5) صفحة الإنجاز
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<!-- pages/wizard/Done.kreft -->
|
|
158
|
+
use wizard
|
|
159
|
+
|
|
160
|
+
action startOver() { wizard.$reset(); $router.go('/wizard') }
|
|
161
|
+
|
|
162
|
+
<view>
|
|
163
|
+
<div k-animate="pop">
|
|
164
|
+
<h2>اكتمل الإعداد 🎉</h2>
|
|
165
|
+
<p>مرحبًا {wizard.data.name} — خطتك {wizard.data.plan}.</p>
|
|
166
|
+
<button @click="startOver">ابدأ من جديد</button>
|
|
167
|
+
</div>
|
|
168
|
+
</view>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`wizard.$reset()` يعيد المخزن إلى قيمه الأولى (الخطوة 1 وبيانات فارغة)، فتُحذف كل الآثار المحفوظة.
|
|
172
|
+
|
|
173
|
+
## 6) لمسة حركة: انتقال بين الخطوات
|
|
174
|
+
|
|
175
|
+
`keep` يُبقي الخطوة ولا يزيلها، فلا تصلح له حركة الدخول/الخروج مباشرة. إن أردت انتقالًا متحركًا استبدل `keep` بـ `k-if` مع `k-animate.after` (ينتظر خروج القديم قبل دخول الجديد):
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<div k-if="wizard.step === 1" k-animate.after="slide"><step1></step1></div>
|
|
179
|
+
<div k-if="wizard.step === 2" k-animate.after="slide"><step2></step2></div>
|
|
180
|
+
<div k-if="wizard.step === 3" k-animate.after="slide"><step3></step3></div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
التضحية أنك تفقد `keep` (لا يُحفظ ما كتبته في الخطوة 1 حين تنتقل). اختر حسب حاجتك: **الاحتفاظ** (keep) أو **الحركة** (k-animate.after).
|
|
184
|
+
|
|
185
|
+
## 7) جرّب الاستئناف بنفسك
|
|
186
|
+
|
|
187
|
+
1. افتح `/wizard` واملأ الخطوة 1 وانتقل للثانية.
|
|
188
|
+
2. حدّث الصفحة (F5) — ستجد نفسك في الخطوة 2 وبياناتك محفوظة (بفضل `saved`).
|
|
189
|
+
3. اكتب `/wizard/done` مباشرة — يعيدك الحارس إلى `/wizard` لأنك لم تُنهِ بعد.
|
|
190
|
+
4. أنهِ الخطوات ثم افتح `/wizard/done` — تمر هذه المرة.
|
|
191
|
+
|
|
192
|
+
## تمارين
|
|
193
|
+
|
|
194
|
+
1. أضف خطوة رابعة "صورة" ترفع ملفًا وتخزّن مساره في `wizard.data`.
|
|
195
|
+
2. اجعل الحارس يمنع أيضًا العودة إلى خطوة سابقة بعد الإنهاء (أعد التوجيه إلى `/wizard/done`).
|
|
196
|
+
3. استبدل `keep` بنسخة `k-animate.after` واحفظ البيانات المؤقتة في `saved` كي لا تضيع عند الانتقال.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# الفصل العشرون: الأنابيب و`k-match` وأنواع الخصائص
|
|
2
|
+
|
|
3
|
+
ثلاث إضافات تجعل القوالب أقصر وأوضح. كلها تعمل مع `kreft dev` ومع `kreft build` ومع سياسة CSP الصارمة، والصيغة القديمة لا تتأثر.
|
|
4
|
+
|
|
5
|
+
## 1) الأنابيب (Pipes)
|
|
6
|
+
|
|
7
|
+
بدل استدعاء دوال مساعدة داخل القالب، مرّر القيمة عبر أنبوب بعلامة `|`:
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<p>{ price | currency:'EUR' }</p>
|
|
11
|
+
<p>{ createdAt | ago }</p>
|
|
12
|
+
<p>{ title | capitalize | truncate:30 }</p>
|
|
13
|
+
<a title={name | upper}>{ tags | join:' · ' }</a>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- الوسائط بعد نقطتين: `truncate:30` و`currency:'EUR'`، أو بين قوسين: `number(2)`.
|
|
17
|
+
- تُسلسل الأنابيب من اليسار إلى اليمين.
|
|
18
|
+
- `||` و`|=` ليستا أنبوبًا، وكذلك `|` داخل نص أو تعبير نمطي أو قوسين.
|
|
19
|
+
- الأرقام والتواريخ تتبع لغة التطبيق الحالية (`Kreftjs.locale`)، فيتغير شكل العملة والتاريخ مع اللغة.
|
|
20
|
+
|
|
21
|
+
**الأنابيب الجاهزة:** `upper` و`lower` و`capitalize` و`trim` و`truncate:n` و`currency:'USD'` و`number:digits` و`percent:digits` و`date:'short'|'medium'|'long'` و`time` و`ago` و`bytes` و`json` و`fallback:'قيمة'` و`join:sep` و`first` و`last` و`slice:a:b` و`reverse` و`sortBy:'key':'desc'` و`count`.
|
|
22
|
+
|
|
23
|
+
**أنبوبك الخاص:** أي دالة في المكوّن تصلح أنبوبًا، وتتقدّم على الجاهز عند تطابق الاسم:
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// في سكربت المكوّن
|
|
27
|
+
state name = 'ali'
|
|
28
|
+
const wrap = (v, left = '«', right = '»') => left + v + right
|
|
29
|
+
```
|
|
30
|
+
```html
|
|
31
|
+
<p>{ name | wrap }</p> <!-- «ali» -->
|
|
32
|
+
<p>{ name | wrap:'[':']' }</p> <!-- [ali] -->
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
ولتعريف أنبوب لكل التطبيق: `Kreftjs.pipe('shout', (v) => String(v).toUpperCase() + '!')`.
|
|
36
|
+
|
|
37
|
+
> **تنبيه:** `a | b` كانت تعني عملية OR على مستوى البتات. إن كنت تستخدمها في قالب فاكتبها `(a | b)` داخل دالة، فالقالب الآن يفسّرها أنبوبًا.
|
|
38
|
+
|
|
39
|
+
## 2) `k-match` و`k-case` و`k-default`
|
|
40
|
+
|
|
41
|
+
بدل سلسلة `k-if` و`k-elif`:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<div k-match="status">
|
|
45
|
+
<b k-case="'ok'">تم</b>
|
|
46
|
+
<i k-case="'wait', 'queued'">بالانتظار</i> <!-- أكثر من قيمة -->
|
|
47
|
+
<u k-default>غير معروف</u>
|
|
48
|
+
</div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- المقارنة صارمة (`===`).
|
|
52
|
+
- تُعرض أول حالة مطابقة فقط، و`k-default` للحالة التي لا يطابقها شيء (يوضع آخرًا).
|
|
53
|
+
- الأبناء المباشرون فقط هم الحالات، ويعملون كما يعمل `k-if`: مكوّنات و`<template>` و`k-animate`.
|
|
54
|
+
|
|
55
|
+
## 3) خصائص بأنواع
|
|
56
|
+
|
|
57
|
+
اكتب النوع بصيغة TypeScript، وتحصل على تحذير أثناء التطوير عند قيمة خاطئة أو خاصية مطلوبة غائبة:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
prop tone: 'info' | 'warn' = 'info' // قيم محددة، والافتراضي info
|
|
61
|
+
prop label: string // مطلوبة: غيابها يحذّر
|
|
62
|
+
prop count?: number // اختيارية
|
|
63
|
+
prop items: string[] = []
|
|
64
|
+
prop onSave: (id: number) => void
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
يُتحقق من: `string` و`number` و`boolean` و`null` و`undefined` و`object` و`Date` و`T[]` والدوال والقيم الحرفية (`'sm'` و`3` و`true`) والاتحاد بينها. أما أسماء أنواعك (`User`) و`any` و`unknown` فلا تُفحص.
|
|
68
|
+
|
|
69
|
+
الخصائص التي لها قيمة افتراضية أو `?` أو تقبل `null`/`undefined` ليست مطلوبة. والنوع `number` أو `boolean` أو `string` يحوّل أيضًا القيمة المكتوبة نصًا في الوسم (`count="5"` تصبح `5`).
|
|
70
|
+
|
|
71
|
+
## تمارين
|
|
72
|
+
|
|
73
|
+
1. اعرض سعرًا بالدينار الكويتي وتاريخ إنشاء بصيغة `ago` في صفحة طلبات.
|
|
74
|
+
2. حوّل سلسلة `k-if` من أربع حالات لحالة الطلب إلى `k-match`.
|
|
75
|
+
3. أضف إلى مكوّن `Badge` الخاصية `prop tone: 'info' | 'warn' | 'danger' = 'info'` وجرّب `tone="loud"` لترى التحذير.
|
package/book/ar/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
كتاب عملي لتتعلم **Kreft (`kreftjs`)**، الإطار التفاعلي ذو لغة المكوّنات `.kreft` والواجهة الخلفية المتكاملة على Node.js، وبناء تطبيقات الويب وسطح المكتب به.
|
|
4
4
|
|
|
5
|
-
> **ملاحظة عن الدقة:** الأمثلة المتقدمة (الفصول 11 إلى
|
|
5
|
+
> **ملاحظة عن الدقة:** الأمثلة المتقدمة (الفصول 11 إلى 20) **جُرِّبت فعلًا**: شُغّل خادم Kreft، وأُرسلت طلبات حقيقية، وفُتحت الصفحات في متصفح. وما لم نستطع التحقق منه ذكرناه صراحة في موضعه. الإطار ما زال تجريبيًا (`0.0.x-beta`)، فراجع `docs/README.md` (بالإنجليزية) عند أي اختلاف مع إصدارك.
|
|
6
6
|
|
|
7
7
|
## الفهرس
|
|
8
8
|
|
|
@@ -33,12 +33,21 @@
|
|
|
33
33
|
|
|
34
34
|
15. [مرجع سريع وأخطاء شائعة ومصطلحات](15-appendix.md).
|
|
35
35
|
|
|
36
|
+
### الجزء الرابع: أمثلة الخبراء
|
|
37
|
+
|
|
38
|
+
16. [قوائم ضخمة بلا تباطؤ](16-virtual-list.md): النافذة الافتراضية لعرض 100,000 صف فما فوق.
|
|
39
|
+
17. [مكوّن مركّب: قائمة اختيار ذكية](17-combobox.md): Combobox ببحث ولوحة مفاتيح وبوابة و`k-model`.
|
|
40
|
+
18. [مزامنة بين التبويبات ودون اتصال](18-tabs-offline.md): حضور حيّ وطابور أوامر يرسل عند عودة الاتصال.
|
|
41
|
+
19. [معالج متعدد الخطوات مع استئناف](19-wizard.md): مخزن محفوظ وتحقق لكل خطوة وحراسة للمسارات.
|
|
42
|
+
20. [الأنابيب و`k-match` وأنواع الخصائص](20-pipes-match-props.md): صيغة أقصر وأوضح للقوالب والمكوّنات.
|
|
43
|
+
|
|
36
44
|
## كيف تقرأ هذا الكتاب؟
|
|
37
45
|
|
|
38
46
|
- **مبتدئ:** الفصول 1 إلى 5 بالترتيب، ونفّذ التمارين في نهاية كل فصل.
|
|
39
47
|
- **تعرف JavaScript وتريد تطبيقًا كاملًا:** 4 ثم 6 ثم 7 ثم 11 و12.
|
|
40
48
|
- **تريد النشر:** 9 (وانتبه إلى قسم `<base>`).
|
|
41
49
|
- **تريد سطح المكتب:** 10.
|
|
50
|
+
- **تريد مستوى الخبير في الواجهة:** 16 إلى 20 بعد أن تتقن الفصول 2 و3 و4.
|
|
42
51
|
|
|
43
52
|
## ما الذي سنبنيه؟
|
|
44
53
|
|
|
@@ -48,6 +57,8 @@
|
|
|
48
57
|
- متجر بسلة ودفع آمن لا يكرر الطلبات ولا يبيع ما نفد.
|
|
49
58
|
- مكتبة مكوّنات (نافذة وجدول وتوجيهات).
|
|
50
59
|
- لوحة أرقام حيّة وتقرير يومي مجدول.
|
|
60
|
+
- قائمة افتراضية تعرض 100,000 صف بلا تباطؤ.
|
|
61
|
+
- قائمة اختيار ذكية، ومزامنة حيّة بين التبويبات، ومعالج إعداد يستأنف بعد التحديث.
|
|
51
62
|
|
|
52
63
|
## تشغيل أمثلة الكتاب
|
|
53
64
|
|
package/book/kreft-book-ar.docx
CHANGED
|
Binary file
|
|
Binary file
|