1. مقدمة حول القوائم في HTML
القوائم هي من أهم العناصر في HTML لتنظيم المحتوى بشكل هيكلي ومنطقي. تستخدم لعرض مجموعات من العناصر ذات الصلة، وتحسن من تجربة المستخدم وإمكانية الوصول بشكل كبير.
أنواع القوائم الأساسية:
<ul>
– Unordered List (قائمة غير مرتبة)<ol>
– Ordered List (قائمة مرتبة)<dl>
– Description List (قائمة وصفية)
2. تاغ <ul>
– القائمة غير المرتبة
2.1 التعريف والاستخدام
<!-- الصيغة الأساسية -->
<ul>
<li>العنصر الأول</li>
<li>العنصر الثاني</li>
<li>العنصر الثالث</li>
</ul>
2.2 خصائص تاغ <ul>
الخصائص العامة (Global Attributes)
<ul id="main-menu"
class="navigation-list"
role="navigation"
aria-label="القائمة الرئيسية">
<li><a href="/">الرئيسية</a></li>
<li><a href="/about">من نحن</a></li>
<li><a href="/contact">اتصل بنا</a></li>
</ul>
خصائص CSS القديمة (مهجورة)
<!-- تجنب استخدام هذه الخصائص -->
<ul type="disc"> <!-- استخدم CSS بدلاً منها -->
<ul compact> <!-- مهجورة -->
2.3 أنواع التعداد (List Style Types)
/* CSS لتخصيص أنواع التعداد */
.ul-disc { list-style-type: disc; } /* نقطة مصمتة */
.ul-circle { list-style-type: circle; } /* نقطة مفرغة */
.ul-square { list-style-type: square; } /* مربع */
.ul-none { list-style-type: none; } /* بدون تعداد */
/* أنواع متقدمة */
.ul-custom {
list-style-type: '\1F44D'; /* إيموجي */
list-style-position: inside;
}
.ul-image {
list-style-image: url('bullet-icon.png');
}
3. تاغ <li>
– عنصر القائمة
3.1 التعريف والاستخدام
<ul>
<li>عنصر بسيط</li>
<li>عنصر مع <strong>تنسيق</strong></li>
<li>
<a href="/link">عنصر مع رابط</a>
</li>
<li>
<p>عنصر مع فقرة</p>
<p>فقرة إضافية</p>
</li>
</ul>
3.2 خصائص تاغ <li>
value
(للقوائم المرتبة فقط)
<ol>
<li value="5">العنصر الخامس</li>
<li>العنصر السادس</li>
<li value="10">العنصر العاشر</li>
</ol>
خصائص ARIA للإمكانية
<ul role="list">
<li role="listitem" aria-level="1">عنصر مستوى أول</li>
<li role="listitem" aria-level="2">عنصر مستوى ثاني</li>
</ul>
4. القوائم المتداخلة (Nested Lists)
4.1 التداخل الصحيح
<ul>
<li>
الفواكه
<ul>
<li>التفاح</li>
<li>البرتقال</li>
<li>
الموز
<ul>
<li>موز أخضر</li>
<li>موز أصفر</li>
</ul>
</li>
</ul>
</li>
<li>
الخضروات
<ul>
<li>الجزر</li>
<li>البطاطس</li>
</ul>
</li>
</ul>
4.2 تنسيق القوائم المتداخلة
/* تنسيق القوائم المتداخلة */
ul {
margin: 0;
padding-right: 20px; /* للنص العربي */
}
ul ul {
margin-top: 5px;
margin-bottom: 5px;
}
/* مستويات مختلفة من التعداد */
ul li ul li {
list-style-type: circle;
}
ul li ul li ul li {
list-style-type: square;
}
5. أفضل الممارسات للSEO
5.1 الهيكلة الدلالية (Semantic Structure)
<!-- ممتاز للSEO -->
<nav aria-label="التنقل الرئيسي">
<ul>
<li><a href="/" aria-current="page">الرئيسية</a></li>
<li><a href="/products">المنتجات</a></li>
<li>
<a href="/services">الخدمات</a>
<ul>
<li><a href="/services/web-design">تصميم المواقع</a></li>
<li><a href="/services/seo">تحسين محركات البحث</a></li>
</ul>
</li>
</ul>
</nav>
5.2 العناوين والوصف المحسن
<!-- قائمة مميزات المنتج محسنة للSEO -->
<section>
<h2>مميزات الخدمة</h2>
<ul>
<li>
<h3>سرعة عالية</h3>
<p>تحميل فائق السرعة يصل إلى 10 أضعاف السرعة التقليدية</p>
</li>
<li>
<h3>أمان متقدم</h3>
<p>حماية شاملة بتقنية التشفير المتقدمة SSL</p>
</li>
<li>
<h3>دعم فني 24/7</h3>
<p>فريق دعم متخصص متاح على مدار الساعة</p>
</li>
</ul>
</section>
5.3 البيانات المنظمة (Structured Data)
<!-- قائمة مع Schema.org -->
<div itemscope itemtype="https://schema.org/ItemList">
<h2 itemprop="name">أفضل المطاعم في المدينة</h2>
<ul>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<meta itemprop="position" content="1">
<h3 itemprop="name">مطعم الأصالة</h3>
<p itemprop="description">مطعم متخصص في الأكلات الشعبية الأصيلة</p>
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<meta itemprop="position" content="2">
<h3 itemprop="name">مطعم البحر الأزرق</h3>
<p itemprop="description">مأكولات بحرية طازجة ولذيذة</p>
</li>
</ul>
</div>
5.4 Breadcrumb Navigation
<nav aria-label="مسار التنقل">
<ul class="breadcrumb">
<li><a href="/">الرئيسية</a></li>
<li><a href="/category">الفئة</a></li>
<li><a href="/subcategory">الفئة الفرعية</a></li>
<li aria-current="page">الصفحة الحالية</li>
</ul>
</nav>
<!-- مع البيانات المنظمة -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "الرئيسية",
"item": "https://example.com/"
},
{
"@type": "ListItem",
"position": 2,
"name": "المنتجات",
"item": "https://example.com/products"
}
]
}
</script>
6. إمكانية الوصول (Accessibility)
6.1 استخدام ARIA بشكل صحيح
<!-- قائمة تنقل مع ARIA -->
<nav role="navigation" aria-label="القائمة الرئيسية">
<ul role="menubar">
<li role="none">
<a href="/" role="menuitem">الرئيسية</a>
</li>
<li role="none">
<button role="menuitem"
aria-haspopup="true"
aria-expanded="false"
aria-controls="submenu-1">
الخدمات
</button>
<ul role="menu" id="submenu-1" hidden>
<li role="none">
<a href="/web-design" role="menuitem">تصميم الويب</a>
</li>
<li role="none">
<a href="/seo" role="menuitem">تحسين محركات البحث</a>
</li>
</ul>
</li>
</ul>
</nav>
6.2 قوائم تفاعلية يمكن الوصول إليها
<ul class="interactive-list"
role="listbox"
aria-label="قائمة المدن"
aria-multiselectable="true">
<li role="option"
aria-selected="false"
tabindex="0"
id="city-1">
الرياض
</li>
<li role="option"
aria-selected="true"
tabindex="0"
id="city-2">
جدة
</li>
<li role="option"
aria-selected="false"
tabindex="0"
id="city-3">
الدمام
</li>
</ul>
6.3 قوائم طويلة مع التنقل
<section>
<h2 id="alphabet-list">قائمة أبجدية</h2>
<nav aria-label="التنقل الأبجدي">
<ul>
<li><a href="#section-a">أ</a></li>
<li><a href="#section-b">ب</a></li>
<li><a href="#section-c">ت</a></li>
</ul>
</nav>
<ul aria-labelledby="alphabet-list">
<li id="section-a">
<h3>حرف الألف</h3>
<ul>
<li>أحمد</li>
<li>إبراهيم</li>
</ul>
</li>
</ul>
</section>
7. التصميم والتنسيق المتقدم
7.1 قوائم أفقية (Horizontal Lists)
/* قائمة تنقل أفقية */
.horizontal-nav ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 20px;
}
.horizontal-nav li {
flex: none;
}
/* مع Flexbox للتوسط */
.center-nav ul {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
/* قائمة أفقية مع فواصل */
.separated-list li:not(:last-child)::after {
content: " | ";
color: #ccc;
margin: 0 10px;
}
7.2 قوائم شبكية (Grid Lists)
/* قوائم المنتجات */
.product-grid ul {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
list-style: none;
padding: 0;
}
.product-grid li {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
transition: transform 0.2s;
}
.product-grid li:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
7.3 قوائم تفاعلية متقدمة
/* قائمة قابلة للطي */
.collapsible-list {
list-style: none;
}
.collapsible-list > li {
border-bottom: 1px solid #eee;
}
.collapsible-list summary {
cursor: pointer;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
user-select: none;
}
.collapsible-list summary:hover {
background: #e9e9e9;
}
/* أنيميشن للفتح والإغلاق */
.collapsible-list details[open] summary {
margin-bottom: 10px;
}
.collapsible-list ul {
padding-right: 20px;
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
8. الأخطاء الشائعة والمحاذير
8.1 أخطاء في البنية HTML
❌ عناصر غير صحيحة داخل <ul>
<!-- خطأ: عناصر غير li مباشرة في ul -->
<ul>
<p>نص خاطئ</p>
<li>عنصر صحيح</li>
<div>قسم خاطئ</div>
</ul>
✅ البنية الصحيحة
<ul>
<li>
<p>نص صحيح داخل li</p>
</li>
<li>عنصر آخر</li>
<li>
<div>قسم صحيح داخل li</div>
</li>
</ul>
❌ <li>
خارج القائمة
<!-- خطأ: li يجب أن يكون داخل ul أو ol -->
<li>عنصر خاطئ</li>
<ul>
<li>عنصر صحيح</li>
</ul>
❌ قوائم فارغة
<!-- تجنب القوائم الفارغة -->
<ul></ul>
<ul>
<!-- تعليقات فقط -->
</ul>
8.2 أخطاء في التداخل
❌ تداخل خاطئ
<!-- خطأ: ul متداخلة خارج li -->
<ul>
<li>عنصر أول</li>
<ul>
<li>عنصر فرعي خاطئ</li>
</ul>
</ul>
✅ التداخل الصحيح
<ul>
<li>
عنصر أول
<ul>
<li>عنصر فرعي صحيح</li>
</ul>
</li>
</ul>
8.3 أخطاء في إمكانية الوصول
❌ عدم وجود تسميات مناسبة
<!-- نقص في إمكانية الوصول -->
<ul>
<li><a href="/page1">صفحة</a></li>
<li><a href="/page2">صفحة</a></li>
</ul>
✅ تسميات واضحة
<nav aria-label="التنقل الرئيسي">
<ul>
<li><a href="/products">صفحة المنتجات</a></li>
<li><a href="/services">صفحة الخدمات</a></li>
</ul>
</nav>
❌ إزالة خاطئة لـ list-style
/* مشكلة في إمكانية الوصول */
ul {
list-style: none;
/* بدون بديل بصري مناسب */
}
✅ بديل مرئي مناسب
.custom-list {
list-style: none;
}
.custom-list li::before {
content: "▶ ";
color: #007acc;
font-weight: bold;
}
/* أو استخدام أيقونات */
.icon-list li {
background: url('icon.svg') no-repeat left center;
padding-right: 25px;
list-style: none;
}
8.4 أخطاء في الأداء
❌ قوائم ضخمة بدون تحسين
<!-- قائمة كبيرة جداً بدون تقسيم -->
<ul>
<!-- 10,000 عنصر -->
<li>عنصر 1</li>
<li>عنصر 2</li>
<!-- ... -->
</ul>
✅ قوائم محسنة
<!-- تقسيم أو تحميل تدريجي -->
<ul id="item-list">
<!-- أول 50 عنصر -->
</ul>
<button onclick="loadMore()">تحميل المزيد</button>
<!-- أو استخدام التقسيم -->
<div class="paginated-list">
<ul>
<!-- العناصر 1-20 -->
</ul>
<nav class="pagination">
<a href="?page=1">1</a>
<a href="?page=2">2</a>
</nav>
</div>
9. حالات الاستخدام المتقدمة
9.1 قوائم التنقل المعقدة (Mega Menu)
<nav class="mega-menu" aria-label="التنقل الرئيسي">
<ul class="main-nav">
<li class="has-dropdown">
<button aria-haspopup="true" aria-expanded="false">
المنتجات
</button>
<div class="dropdown-panel" role="region" aria-label="قائمة المنتجات">
<div class="dropdown-content">
<div class="column">
<h3>الفئة الأولى</h3>
<ul>
<li><a href="/product1">منتج 1</a></li>
<li><a href="/product2">منتج 2</a></li>
</ul>
</div>
<div class="column">
<h3>الفئة الثانية</h3>
<ul>
<li><a href="/product3">منتج 3</a></li>
<li><a href="/product4">منتج 4</a></li>
</ul>
</div>
</div>
</div>
</li>
</ul>
</nav>
9.2 قوائم تفاعلية بـ JavaScript
<ul class="sortable-list" id="todo-list">
<li draggable="true" data-id="1">
<span class="handle">≡</span>
<span class="text">مهمة أولى</span>
<button class="delete" aria-label="حذف المهمة">×</button>
</li>
<li draggable="true" data-id="2">
<span class="handle">≡</span>
<span class="text">مهمة ثانية</span>
<button class="delete" aria-label="حذف المهمة">×</button>
</li>
</ul>
<script>
// JavaScript للتفاعل
document.getElementById('todo-list').addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', e.target.dataset.id);
});
document.getElementById('todo-list').addEventListener('drop', function(e) {
e.preventDefault();
const draggedId = e.dataTransfer.getData('text/plain');
// منطق إعادة الترتيب
});
</script>
9.3 قوائم بحث قابلة للتصفية
<div class="filterable-list">
<input type="search"
id="list-filter"
placeholder="بحث في القائمة..."
aria-label="تصفية العناصر">
<ul id="filterable-items" role="listbox" aria-live="polite">
<li role="option" data-category="فواكه">تفاح</li>
<li role="option" data-category="فواكه">برتقال</li>
<li role="option" data-category="خضار">جزر</li>
<li role="option" data-category="خضار">بطاطس</li>
</ul>
<div id="search-results" aria-live="polite" class="sr-only">
<!-- نتائج البحث للقارئات -->
</div>
</div>
<script>
document.getElementById('list-filter').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const items = document.querySelectorAll('#filterable-items li');
let visibleCount = 0;
items.forEach(item => {
const text = item.textContent.toLowerCase();
const isVisible = text.includes(searchTerm);
item.style.display = isVisible ? 'block' : 'none';
if (isVisible) visibleCount++;
});
// إعلام قارئات الشاشة
document.getElementById('search-results').textContent =
`تم العثور على ${visibleCount} عنصر`;
});
</script>
10. تحسينات الأداء
10.1 القوائم الكبيرة والتمرير الافتراضي
<div class="virtual-list" style="height: 400px; overflow-y: auto;">
<ul id="large-list">
<!-- سيتم ملؤها بـ JavaScript -->
</ul>
</div>
<script>
// Virtual Scrolling للقوائم الكبيرة
class VirtualList {
constructor(container, items, itemHeight = 40) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight);
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleCount, this.items.length);
this.container.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const li = document.createElement('li');
li.textContent = this.items[i];
li.style.height = this.itemHeight + 'px';
this.container.appendChild(li);
}
}
}
</script>
10.2 تحميل القوائم التدريجي
<ul id="progressive-list">
<!-- العناصر الأولية -->
<li>عنصر 1</li>
<li>عنصر 2</li>
</ul>
<div class="loading-indicator" hidden>
جاري التحميل...
</div>
<script>
// Intersection Observer للتحميل التدريجي
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreItems();
}
});
});
// مراقبة العنصر الأخير
const lastItem = document.querySelector('#progressive-list li:last-child');
observer.observe(lastItem);
async function loadMoreItems() {
const loadingIndicator = document.querySelector('.loading-indicator');
loadingIndicator.hidden = false;
try {
const response = await fetch('/api/more-items');
const newItems = await response.json();
const list = document.getElementById('progressive-list');
newItems.forEach(item => {
const li = document.createElement('li');
li.textContent = item.title;
list.appendChild(li);
});
} finally {
loadingIndicator.hidden = true;
}
}
</script>
11. اختبار وضمان الجودة
11.1 اختبارات إمكانية الوصول
<!-- قائمة للاختبار -->
<ul role="list" aria-label="قائمة اختبار">
<li role="listitem">
<a href="/test1" aria-describedby="desc1">رابط اختبار</a>
<span id="desc1" class="sr-only">وصف إضافي للرابط</span>
</li>
</ul>
<script>
// اختبار تلقائي لإمكانية الوصول
function testAccessibility() {
const lists = document.querySelectorAll('ul, ol');
const issues = [];
lists.forEach(list => {
// التحقق من وجود عناصر li
const listItems = list.querySelectorAll('li');
if (listItems.length === 0) {
issues.push('قائمة فارغة: ' + list.outerHTML.substring(0, 50));
}
// التحقق من التسميات
if (list.hasAttribute('role') && !list.hasAttribute('aria-label')) {
issues.push('قائمة بدون تسمية: ' + list.outerHTML.substring(0, 50));
}
});
if (issues.length > 0) {
console.warn('مشاكل في إمكانية الوصول:', issues);
}
}
// تشغيل الاختبار عند تحميل الصفحة
document.addEventListener('DOMContentLoaded', testAccessibility);
</script>
11.2 اختبارات الأداء
// قياس أداء عرض القوائم
function measureListPerformance() {
const startTime = performance.now();
// إنشاء قائمة كبيرة
const list = document.createElement('ul');
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `عنصر رقم ${i}`;
list.appendChild(li);
}
document.body.appendChild(list);
const endTime = performance.now();
console.log(`وقت عرض 1000 عنصر: ${endTime - startTime} ميللي ثانية`);
// تنظيف
document.body.removeChild(list);
}
12. أمثلة واقعية متقدمة
12.1 قائمة منتجات التجارة الإلكترونية
<section class="product-listing" itemscope itemtype="https://schema.org/ItemList">
<h2 itemprop="name">منتجاتنا المميزة</h2>
<div class="filters">
<label>
ترتيب حسب:
<select onchange="sortProducts(this.value)">
<option value="name">الاسم</option>
<option value="price">السعر</option>
<option value="rating">التقييم</option>
</select>
</label>
</div>
<ul class="product-grid" itemprop="itemListElement">
<li itemscope itemtype="https://schema.org/Product" class="product-item">
<article>
<img src="product1.jpg" alt="هاتف ذكي عالي الجودة" itemprop="image">
<h3 itemprop="name">هاتف ذكي برو</h3>
<div class="rating" aria-label="تقييم 4.5 من 5">
<span aria-hidden="true">★★★★☆</span>
</div>
<div class="price" itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="price" content="1999">1999</span>
<span itemprop="priceCurrency" content="SAR">ريال</span>
</div>
<button class="add-to-cart" data-product-id="1">إضافة للسلة</button>
</article>
</li>
<li itemscope itemtype="https://schema.org/Product" class="product-item">
<article>
<img src="product2.jpg" alt="حاسوب محمول للألعاب" itemprop="image">
<h3 itemprop="name">لابتوب جيمر</h3>
<div class="rating" aria-label="تقييم 4.8 من 5">
<span aria-hidden="true">★★★★★</span>
</div>
<div class="price" itemprop="offers" itemscope itemtype="https://schema.org/Offer">
<span itemprop="price" content="4999">4999</span>
<span itemprop="priceCurrency" content="SAR">ريال</span>
</div>
<button class="add-to-cart" data-product-id="2">إضافة للسلة</button>
</article>
</li>
</ul>
</section>
### 12.2 قائمة FAQ تفاعلية
```html
<section class="faq-section">
<h2>الأسئلة الشائعة</h2>
<ul class="faq-list" role="list">
<li class="faq-item">
<details>
<summary role="button" aria-expanded="false">
<h3>ما هي مدة التوصيل؟</h3>
<span class="toggle-icon" aria-hidden="true">+</span>
</summary>
<div class="faq-answer">
<p>يتم التوصيل خلال 2-3 أيام عمل داخل المدن الرئيسية، و 5-7 أيام للمناطق الأخرى.</p>
<ul>
<li>التوصيل السريع: خلال 24 ساعة (رسوم إضافية)</li>
<li>التوصيل العادي: 2-3 أيام عمل</li>
<li>التوصيل للمناطق النائية: 5-7 أيام</li>
</ul>
</div>
</details>
</li>
<li class="faq-item">
<details>
<summary role="button" aria-expanded="false">
<h3>هل يمكنني إرجاع المنتج؟</h3>
<span class="toggle-icon" aria-hidden="true">+</span>
</summary>
<div class="faq-answer">
<p>نعم، يمكنك إرجاع المنتج خلال 30 يوماً من تاريخ الاستلام.</p>
<ul>
<li>المنتج يجب أن يكون في حالته الأصلية</li>
<li>يجب الاحتفاظ بالفاتورة الأصلية</li>
<li>التكلفة مجانية للمنتجات المعيبة</li>
</ul>
</div>
</details>
</li>
</ul>
</section>
### 12.3 قائمة التنقل الجانبية (Sidebar Navigation)
```html
<nav class="sidebar-nav" aria-label="التنقل الجانبي">
<ul class="nav-tree" role="tree">
<li role="treeitem" aria-expanded="true">
<a href="/dashboard" class="nav-link active" aria-current="page">
<span class="icon" aria-hidden="true">🏠</span>
لوحة التحكم
</a>
</li>
<li role="treeitem" aria-expanded="false" class="has-children">
<button class="nav-toggle" aria-controls="products-submenu">
<span class="icon" aria-hidden="true">📦</span>
إدارة المنتجات
<span class="toggle-arrow" aria-hidden="true">▶</span>
</button>
<ul role="group" id="products-submenu" class="submenu" hidden>
<li role="treeitem">
<a href="/products/list" class="nav-link">
عرض المنتجات
</a>
</li>
<li role="treeitem">
<a href="/products/add" class="nav-link">
إضافة منتج
</a>
</li>
<li role="treeitem">
<a href="/products/categories" class="nav-link">
الفئات
</a>
</li>
</ul>
</li>
<li role="treeitem" aria-expanded="false" class="has-children">
<button class="nav-toggle" aria-controls="orders-submenu">
<span class="icon" aria-hidden="true">🛒</span>
إدارة الطلبات
<span class="toggle-arrow" aria-hidden="true">▶</span>
</button>
<ul role="group" id="orders-submenu" class="submenu" hidden>
<li role="treeitem">
<a href="/orders/pending" class="nav-link">
الطلبات المعلقة
<span class="badge" aria-label="5 طلبات معلقة">5</span>
</a>
</li>
<li role="treeitem">
<a href="/orders/completed" class="nav-link">
الطلبات المكتملة
</a>
</li>
</ul>
</li>
</ul>
</nav>
### 12.4 قائمة المهام التفاعلية (To-Do List)
```html
<section class="todo-app">
<header>
<h2>قائمة المهام</h2>
<form class="add-todo" onsubmit="addTodo(event)">
<label class="sr-only" for="new-todo">إضافة مهمة جديدة</label>
<input type="text"
id="new-todo"
placeholder="أدخل مهمة جديدة..."
required
maxlength="100">
<button type="submit">إضافة</button>
</form>
</header>
<div class="todo-filters">
<button onclick="filterTodos('all')" class="filter-btn active">الكل</button>
<button onclick="filterTodos('active')" class="filter-btn">النشطة</button>
<button onclick="filterTodos('completed')" class="filter-btn">المكتملة</button>
</div>
<ul id="todo-list"
class="todo-list"
role="list"
aria-live="polite"
aria-label="قائمة المهام">
<li class="todo-item" data-id="1" data-status="active">
<label class="todo-checkbox">
<input type="checkbox"
aria-describedby="task-1-text"
onchange="toggleTodo(1)">
<span class="checkmark"></span>
</label>
<span id="task-1-text" class="todo-text">إكمال تقرير المشروع</span>
<div class="todo-actions">
<button onclick="editTodo(1)" aria-label="تعديل المهمة">✏️</button>
<button onclick="deleteTodo(1)" aria-label="حذف المهمة">🗑️</button>
</div>
</li>
<li class="todo-item completed" data-id="2" data-status="completed">
<label class="todo-checkbox">
<input type="checkbox"
checked
aria-describedby="task-2-text"
onchange="toggleTodo(2)">
<span class="checkmark"></span>
</label>
<span id="task-2-text" class="todo-text">مراجعة الإيميلات</span>
<div class="todo-actions">
<button onclick="editTodo(2)" aria-label="تعديل المهمة">✏️</button>
<button onclick="deleteTodo(2)" aria-label="حذف المهمة">🗑️</button>
</div>
</li>
</ul>
<footer class="todo-summary" aria-live="polite">
<span id="todo-count">مهمة واحدة متبقية</span>
<button onclick="clearCompleted()" class="clear-completed">
مسح المكتملة
</button>
</footer>
</section>
### 12.5 قائمة التعليقات (Comments List)
```html
<section class="comments-section">
<header>
<h3>التعليقات (15)</h3>
<div class="sort-options">
<label for="comment-sort">ترتيب حسب:</label>
<select id="comment-sort" onchange="sortComments(this.value)">
<option value="newest">الأحدث</option>
<option value="oldest">الأقدم</option>
<option value="most-liked">الأكثر إعجاباً</option>
</select>
</div>
</header>
<ul class="comments-list" role="list">
<li class="comment-item" itemscope itemtype="https://schema.org/Comment">
<article class="comment">
<header class="comment-header">
<div class="author-info">
<img src="avatar1.jpg"
alt="صورة أحمد محمد"
class="avatar"
itemprop="author"
itemscope
itemtype="https://schema.org/Person">
<div class="author-details">
<span class="author-name" itemprop="name">أحمد محمد</span>
<time datetime="2024-01-15T10:30:00" itemprop="datePublished">
منذ ساعتين
</time>
</div>
</div>
<button class="comment-menu" aria-label="خيارات التعليق">⋯</button>
</header>
<div class="comment-content" itemprop="text">
<p>مقال رائع ومفيد جداً! شكراً لك على هذه المعلومات القيمة.</p>
</div>
<footer class="comment-actions">
<button class="like-btn" onclick="likeComment(1)">
<span aria-hidden="true">👍</span>
<span class="like-count">12</span>
<span class="sr-only">أعجبني</span>
</button>
<button class="reply-btn" onclick="replyToComment(1)">
<span aria-hidden="true">💬</span>
رد
</button>
<button class="share-btn" aria-label="مشاركة التعليق">
<span aria-hidden="true">📤</span>
مشاركة
</button>
</footer>
<!-- الردود على التعليق -->
<ul class="replies-list" role="list">
<li class="reply-item">
<article class="comment reply">
<header class="comment-header">
<div class="author-info">
<img src="avatar2.jpg" alt="صورة فاطمة أحمد" class="avatar">
<div class="author-details">
<span class="author-name">فاطمة أحمد</span>
<time datetime="2024-01-15T11:00:00">منذ ساعة</time>
</div>
</div>
</header>
<div class="comment-content">
<p>أتفق معك تماماً، معلومات مفيدة فعلاً.</p>
</div>
<footer class="comment-actions">
<button class="like-btn" onclick="likeComment(2)">
<span aria-hidden="true">👍</span>
<span class="like-count">3</span>
</button>
</footer>
</article>
</li>
</ul>
</article>
</li>
</ul>
<div class="load-more">
<button onclick="loadMoreComments()" class="load-more-btn">
تحميل المزيد من التعليقات
</button>
</div>
</section>
## 13. الأنماط CSS المتقدمة للقوائم
### 13.1 قوائم متحركة وتفاعلية
```css
/* تأثيرات الحركة للقوائم */
.animated-list {
list-style: none;
padding: 0;
}
.animated-list li {
opacity: 0;
transform: translateX(-30px);
animation: slideInRight 0.5s ease-out forwards;
animation-delay: calc(var(--item-index) * 0.1s);
}
@keyframes slideInRight {
to {
opacity: 1;
transform: translateX(0);
}
}
/* تأثير التمرير */
.hover-list li {
transition: all 0.3s ease;
padding: 10px;
border-radius: 8px;
}
.hover-list li:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateX(10px);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
/* قوائم بأشكال مخصصة */
.custom-bullets {
list-style: none;
padding: 0;
}
.custom-bullets li {
position: relative;
padding-right: 30px;
margin-bottom: 10px;
}
.custom-bullets li::before {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.custom-bullets li:nth-child(even)::before {
background: linear-gradient(45deg, #4834d4, #686de0);
}
/* قوائم متدرجة اللون */
.gradient-list {
list-style: none;
padding: 0;
}
.gradient-list li {
background: linear-gradient(90deg,
hsl(calc(var(--item-index) * 30), 70%, 50%),
hsl(calc(var(--item-index) * 30 + 60), 70%, 70%)
);
margin-bottom: 5px;
padding: 15px;
border-radius: 10px;
color: white;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
13.2 قوائم مرنة ومتجاوبة
/* قوائم شبكية متجاوبة */
.responsive-grid-list {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
@media (max-width: 600px) {
.responsive-grid-list {
grid-template-columns: 1fr;
gap: 10px;
}
}
/* قوائم مرنة بـ Flexbox */
.flex-list {
display: flex;
flex-wrap: wrap;
gap: 15px;
list-style: none;
padding: 0;
}
.flex-list li {
flex: 1 1 200px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 10px;
transition: transform 0.2s;
}
.flex-list li:hover {
transform: scale(1.05);
}
/* قوائم بتخطيط البطاقات */
.card-list {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.card-list li {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
overflow: hidden;
transition: box-shadow 0.3s ease;
}
.card-list li:hover {
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.card-list .card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
}
.card-list .card-body {
padding: 20px;
}
13.3 قوائم للطباعة
/* تنسيق خاص للطباعة */
@media print {
.print-list {
list-style: decimal;
padding-right: 20px;
}
.print-list li {
page-break-inside: avoid;
margin-bottom: 10px;
line-height: 1.6;
}
.print-list a {
color: black !important;
text-decoration: none;
}
.print-list a::after {
content: " (" attr(href) ")";
font-size: 0.8em;
color: #666;
}
/* إخفاء العناصر التفاعلية */
.print-list button,
.print-list .interactive-elements {
display: none;
}
}
14. JavaScript المتقدم للقوائم
14.1 قوائم قابلة للسحب والإفلات
class DraggableList {
constructor(listElement) {
this.list = listElement;
this.draggedElement = null;
this.init();
}
init() {
this.list.addEventListener('dragstart', this.handleDragStart.bind(this));
this.list.addEventListener('dragover', this.handleDragOver.bind(this));
this.list.addEventListener('drop', this.handleDrop.bind(this));
this.list.addEventListener('dragend', this.handleDragEnd.bind(this));
// جعل العناصر قابلة للسحب
this.list.querySelectorAll('li').forEach(item => {
item.setAttribute('draggable', 'true');
});
}
handleDragStart(e) {
this.draggedElement = e.target;
e.target.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.outerHTML);
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = this.getDragAfterElement(e.clientY);
if (afterElement == null) {
this.list.appendChild(this.draggedElement);
} else {
this.list.insertBefore(this.draggedElement, afterElement);
}
}
handleDrop(e) {
e.preventDefault();
this.updateOrder();
}
handleDragEnd(e) {
e.target.style.opacity = '';
this.draggedElement = null;
}
getDragAfterElement(y) {
const draggableElements = [...this.list.querySelectorAll('li:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
updateOrder() {
const items = this.list.querySelectorAll('li');
const newOrder = Array.from(items).map((item, index) => ({
id: item.dataset.id,
position: index + 1
}));
// إرسال الترتيب الجديد للسيرفر
this.saveOrder(newOrder);
}
async saveOrder(order) {
try {
await fetch('/api/update-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order })
});
} catch (error) {
console.error('خطأ في حفظ الترتيب:', error);
}
}
}
14.2 قوائم بحث وتصفية متقدمة
class AdvancedListFilter {
constructor(listElement, options = {}) {
this.list = listElement;
this.items = Array.from(listElement.querySelectorAll('li'));
this.options = {
searchAttribute: 'data-search',
categoryAttribute: 'data-category',
caseSensitive: false,
...options
};
this.currentFilters = {
search: '',
category: 'all',
sortBy: 'default'
};
this.init();
}
init() {
this.createFilterControls();
this.bindEvents();
}
createFilterControls() {
const controlsContainer = document.createElement('div');
controlsContainer.className = 'list-controls';
controlsContainer.innerHTML = `
<div class="search-container">
<input type="search"
id="list-search"
placeholder="بحث في القائمة..."
aria-label="بحث">
<button type="button" id="clear-search" aria-label="مسح البحث">×</button>
</div>
<div class="filter-container">
<select id="category-filter" aria-label="تصفية حسب الفئة">
<option value="all">جميع الفئات</option>
</select>
</div>
<div class="sort-container">
<select id="sort-options" aria-label="ترتيب حسب">
<option value="default">الترتيب الافتراضي</option>
<option value="name-asc">الاسم (أ-ي)</option>
<option value="name-desc">الاسم (ي-أ)</option>
<option value="date-asc">التاريخ (الأقدم أولاً)</option>
<option value="date-desc">التاريخ (الأحدث أولاً)</option>
</select>
</div>
`;
this.list.parentNode.insertBefore(controlsContainer, this.list);
this.populateCategoryFilter();
}
populateCategoryFilter() {
const categories = new Set();
this.items.forEach(item => {
const category = item.getAttribute(this.options.categoryAttribute);
if (category) categories.add(category);
});
const categorySelect = document.getElementById('category-filter');
categories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
}
bindEvents() {
document.getElementById('list-search').addEventListener('input',
this.debounce(this.handleSearch.bind(this), 300));
document.getElementById('clear-search').addEventListener('click',
this.clearSearch.bind(this));
document.getElementById('category-filter').addEventListener('change',
this.handleCategoryFilter.bind(this));
document.getElementById('sort-options').addEventListener('change',
this.handleSort.bind(this));
}
handleSearch(e) {
this.currentFilters.search = e.target.value;
this.applyFilters();
this.announceResults();
}
handleCategoryFilter(e) {
this.currentFilters.category = e.target.value;
this.applyFilters();
this.announceResults();
}
handleSort(e) {
this.currentFilters.sortBy = e.target.value;
this.applySorting();
}
applyFilters() {
const { search, category } = this.currentFilters;
let visibleCount = 0;
this.items.forEach(item => {
let isVisible = true;
// تصفية البحث
if (search) {
const searchText = item.getAttribute(this.options.searchAttribute) ||
item.textContent;
const searchTarget = this.options.caseSensitive ?
searchText : searchText.toLowerCase();
const searchQuery = this.options.caseSensitive ?
search : search.toLowerCase();
isVisible = searchTarget.includes(searchQuery);
}
// تصفية الفئة
if (isVisible && category !== 'all') {
const itemCategory = item.getAttribute(this.options.categoryAttribute);
isVisible = itemCategory === category;
}
item.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
return visibleCount;
}
applySorting() {
const { sortBy } = this.currentFilters;
const sortedItems = [...this.items];
switch (sortBy) {
case 'name-asc':
sortedItems.sort((a, b) =>
a.textContent.trim().localeCompare(b.textContent.trim()));
break;
case 'name-desc':
sortedItems.sort((a, b) =>
b.textContent.trim().localeCompare(a.textContent.trim()));
break;
case 'date-asc':
sortedItems.sort((a, b) => {
const dateA = new Date(a.dataset.date || 0);
const dateB = new Date(b.dataset.date || 0);
return dateA - dateB;
});
break;
case 'date-desc':
sortedItems.sort((a, b) => {
const dateA = new Date(a.dataset.date || 0);
const dateB = new Date(b.dataset.date || 0);
return dateB - dateA;
});
break;
default:
// الترتيب الافتراضي
return;
}
// إعادة ترتيب العناصر في DOM
sortedItems.forEach(item => this.list.appendChild(item));
}
clearSearch() {
document.getElementById('list-search').value = '';
this.currentFilters.search = '';
this.applyFilters();
this.announceResults();
}
announceResults() {
const visibleCount = this.applyFilters();
const announcement = document.createElement('div');
announcement.textContent = `تم العثور على ${visibleCount} عنصر`;
announcement.className = 'sr-only';
announcement.setAttribute('aria-live', 'polite');
document.body.appendChild(announcement);
setTimeout(() => document.body.removeChild(announcement), 1000);
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// استخدام الفئة
document.addEventListener('DOMContentLoaded', function() {
const listElement = document.getElementById('filterable-list');
if (listElement) {
new AdvancedListFilter(listElement, {
searchAttribute: 'data-search',
categoryAttribute: 'data-category'
});
}
});
14.3 قوائم لا نهائية التمرير
class InfiniteScrollList {
constructor(container, options = {}) {
this.container = container;
this.list = container.querySelector('ul');
this.options = {
itemsPerPage: 20,
threshold: 200, // بيكسل من أسفل الصفحة
loadingTemplate: '<li class="loading-item">جاري التحميل...</li>',
errorTemplate: '<li class="error-item">حدث خطأ في التحميل</li>',
endTemplate: '<li class="end-item">تم تحميل جميع العناصر</li>',
apiEndpoint: '/api/items',
...options
};
this.currentPage = 1;
this.isLoading = false;
this.hasMore = true;
this.loadedItems = new Set();
this.init();
}
init() {
this.createObserver();
this.bindEvents();
}
createObserver() {
// إنشاء عنصر مراقبة في نهاية القائمة
this.sentinel = document.createElement('div');
this.sentinel.className = 'scroll-sentinel';
this.container.appendChild(this.sentinel);
// Intersection Observer للتمرير اللا نهائي
this.observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
this.loadMoreItems();
}
}, {
threshold: 0.1,
rootMargin: `${this.options.threshold}px`
});
this.observer.observe(this.sentinel);
}
bindEvents() {
// معالجة أخطاء الشبكة
window.addEventListener('online', () => {
if (this.hasError) {
this.retryLoad();
}
});
}
async loadMoreItems() {
if (this.isLoading || !this.hasMore) return;
this.isLoading = true;
this.showLoadingIndicator();
try {
const response = await fetch(
`${this.options.apiEndpoint}?page=${this.currentPage}&limit=${this.options.itemsPerPage}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.items && data.items.length > 0) {
this.appendItems(data.items);
this.currentPage++;
this.hasMore = data.hasMore !== false;
} else {
this.hasMore = false;
this.showEndMessage();
}
this.hasError = false;
} catch (error) {
console.error('خطأ في تحميل العناصر:', error);
this.hasError = true;
this.showErrorMessage();
} finally {
this.isLoading = false;
this.hideLoadingIndicator();
}
}
appendItems(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
if (this.loadedItems.has(item.id)) {
return; // تجنب التكرار
}
const li = this.createItemElement(item);
fragment.appendChild(li);
this.loadedItems.add(item.id);
});
this.list.appendChild(fragment);
// إعلان للقارئات
this.announceNewItems(items.length);
}
createItemElement(item) {
const li = document.createElement('li');
li.className = 'list-item';
li.dataset.id = item.id;
li.innerHTML = `
<div class="item-content">
<h3>${this.escapeHtml(item.title)}</h3>
<p>${this.escapeHtml(item.description || '')}</p>
<div class="item-meta">
<span class="item-date">${this.formatDate(item.date)}</span>
<span class="item-category">${this.escapeHtml(item.category || '')}</span>
</div>
</div>
`;
return li;
}
showLoadingIndicator() {
if (!this.loadingElement) {
this.loadingElement = document.createElement('div');
this.loadingElement.innerHTML = this.options.loadingTemplate;
this.loadingElement.setAttribute('aria-live', 'polite');
}
this.container.appendChild(this.loadingElement);
}
hideLoadingIndicator() {
if (this.loadingElement && this.loadingElement.parentNode) {
this.loadingElement.parentNode.removeChild(this.loadingElement);
}
}
showErrorMessage() {
const errorElement = document.createElement('div');
errorElement.innerHTML = `
${this.options.errorTemplate}
<button onclick="this.parentNode.nextElementSibling.retryLoad()"
class="retry-btn">إعادة المحاولة</button>
`;
errorElement.setAttribute('aria-live', 'assertive');
this.container.appendChild(errorElement);
}
showEndMessage() {
const endElement = document.createElement('div');
endElement.innerHTML = this.options.endTemplate;
endElement.setAttribute('aria-live', 'polite');
this.container.appendChild(endElement);
}
retryLoad() {
this.hasError = false;
// إزالة رسائل الخطأ
this.container.querySelectorAll('.error-item').forEach(el => {
el.parentNode.removeChild(el);
});
this.loadMoreItems();
}
announceNewItems(count) {
const announcement = document.createElement('div');
announcement.textContent = `تم تحميل ${count} عنصر جديد`;
announcement.className = 'sr-only';
announcement.setAttribute('aria-live', 'polite');
document.body.appendChild(announcement);
setTimeout(() => document.body.removeChild(announcement), 2000);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ar-SA', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// تنظيف الموارد
destroy() {
if (this.observer) {
this.observer.disconnect();
}
if (this.sentinel && this.sentinel.parentNode) {
this.sentinel.parentNode.removeChild(this.sentinel);
}
}
}
15. أمان وحماية القوائم
15.1 تنظيف وتعقيم المحتوى
class SecureListManager {
constructor() {
this.allowedTags = ['b', 'i', 'em', 'strong', 'span'];
this.allowedAttributes = ['class', 'id', 'data-*'];
}
sanitizeContent(content) {
// إزالة الـ Scripts والمحتوى الخطير
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
// إزالة جميع الـ scripts
tempDiv.querySelectorAll('script').forEach(script =>
script.parentNode.removeChild(script));
// إزالة الأحداث الخطيرة
tempDiv.querySelectorAll('*').forEach(element => {
Array.from(element.attributes).forEach(attr => {
if (attr.name.startsWith('on')) {
element.removeAttribute(attr.name);
}
});
});
return tempDiv.innerHTML;
}
validateListStructure(listElement) {
const errors = [];
// التحقق من البنية الصحيحة
if (!listElement.matches('ul, ol, dl')) {
errors.push('العنصر ليس قائمة صحيحة');
}
// التحقق من العناصر الفرعية
Array.from(listElement.children).forEach((child, index) => {
if (listElement.matches('ul, ol') && !child.matches('li')) {
errors.push(`العنصر ${index + 1} ليس li صحيح`);
}
});
return {
isValid: errors.length === 0,
errors
};
}
preventXSS(input) {
const entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return String(input).replace(/[&<>"'\/]/g, s => entityMap[s]);
}
}
// استخدام الحماية
const secureManager = new SecureListManager();
function addSecureListItem(list, content) {
const sanitizedContent = secureManager.sanitizeContent(content);
const validation = secureManager.validateListStructure(list);
if (!validation.isValid) {
console.error('خطأ في بنية القائمة:', validation.errors);
return false;
}
const li = document.createElement('li');
li.innerHTML = sanitizedContent;
list.appendChild(li);
return true;
}
15.2 حماية من هجمات CSRF
class CSRFProtectedList {
constructor(apiEndpoint) {
this.apiEndpoint = apiEndpoint;
this.csrfToken = this.getCSRFToken();
}
getCSRFToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : null;
}
async secureRequest(url, options = {}) {
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': this.csrfToken,
...options.headers
},
credentials: 'same-origin' // إرسال الكوكيز
};
return fetch(url, { ...defaultOptions, ...options });
}
async addListItem(data) {
try {
const response = await this.secureRequest(`${this.apiEndpoint}/items`, {
method: 'POST',
body: JSON.stringify(data)
});
if (response.status === 419) {
// CSRF token expired
await this.refreshCSRFToken();
return this.addListItem(data); // إعادة المحاولة
}
return response;
} catch (error) {
console.error('خطأ في إضافة العنصر:', error);
throw error;
}
}
async refreshCSRFToken() {
const response = await fetch('/csrf-token');
const data = await response.json();
this.csrfToken = data.token;
// تحديث meta tag
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta) {
meta.setAttribute('content', this.csrfToken);
}
}
}
16. تحسين الأداء للقوائم الكبيرة
16.1 التمرير الافتراضي (Virtual Scrolling)
class VirtualScrollList {
constructor(container, options = {}) {
this.container = container;
this.options = {
itemHeight: 50,
buffer: 5,
...options
};
this.items = [];
this.visibleStartIndex = 0;
this.visibleEndIndex = 0;
this.scrollElement = null;
this.init();
}
init() {
this.createScrollContainer();
this.bindEvents();
this.updateVisibleItems();
}
createScrollContainer() {
this.scrollElement = document.createElement('div');
this.scrollElement.className = 'virtual-scroll-container';
this.scrollElement.style.height = '400px';
this.scrollElement.style.overflow = 'auto';
this.contentElement = document.createElement('div');
this.contentElement.className = 'virtual-scroll-content';
this.listElement = document.createElement('ul');
this.listElement.className = 'virtual-list';
this.contentElement.appendChild(this.listElement);
this.scrollElement.appendChild(this.contentElement);
this.container.appendChild(this.scrollElement);
}
bindEvents() {
this.scrollElement.addEventListener('scroll',
this.throttle(this.handleScroll.bind(this), 16)); // 60fps
}
setItems(items) {
this.items = items;
this.updateScrollHeight();
this.updateVisibleItems();
}
updateScrollHeight() {
const totalHeight = this.items.length * this.options.itemHeight;
this.contentElement.style.height = `${totalHeight}px`;
}
handleScroll() {
this.updateVisibleItems();
}
updateVisibleItems() {
const scrollTop = this.scrollElement.scrollTop;
const containerHeight = this.scrollElement.clientHeight;
const startIndex = Math.floor(scrollTop / this.options.itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / this.options.itemHeight) + this.options.buffer,
this.items.length
);
this.visibleStartIndex = Math.max(0, startIndex - this.options.buffer);
this.visibleEndIndex = endIndex;
this.renderVisibleItems();
}
renderVisibleItems() {
// مسح العناصر الموجودة
this.listElement.innerHTML = '';
// تحديد الإزاحة العلوية
const offsetY = this.visibleStartIndex * this.options.itemHeight;
this.listElement.style.transform = `translateY(${offsetY}px)`;
// رسم العناصر المرئية
for (let i = this.visibleStartIndex; i < this.visibleEndIndex; i++) {
if (i < this.items.length) {
const item = this.items[i];
const li = this.createItemElement(item, i);
this.listElement.appendChild(li);
}
}
}
createItemElement(item, index) {
const li = document.createElement('li');
li.style.height = `${this.options.itemHeight}px`;
li.className = 'virtual-list-item';
li.dataset.index = index;
if (typeof this.options.renderItem === 'function') {
li.innerHTML = this.options.renderItem(item, index);
} else {
li.textContent = item.toString();
}
return li;
}
scrollToIndex(index) {
const scrollTop = index * this.options.itemHeight;
this.scrollElement.scrollTop = scrollTop;
}
throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
}
// مثال على الاستخدام
const virtualList = new VirtualScrollList(document.getElementById('container'), {
itemHeight: 60,
renderItem: (item, index) => `
<div class="item-content">
<h4>${item.title}</h4>
<p>${item.description}</p>
<small>عنصر رقم ${index + 1}</small>
</div>
`
});
// تحميل 10000 عنصر
const items = Array.from({length: 10000}, (_, i) => ({
title: `العنصر ${i + 1}`,
description: `هذا هو وصف العنصر رقم ${i + 1}`
}));
virtualList.setItems(items);
16.2 تحسين الذاكرة وإدارة DOM
class MemoryEfficientList {
constructor(container, options = {}) {
this.container = container;
this.options = {
maxRenderedItems: 100,
itemPool: [],
...options
};
this.itemPool = new Set();
this.activeItems = new Map();
this.recycledItems = [];
this.init();
}
init() {
this.createItemPool();
}
createItemPool() {
// إنشاء مجموعة من العناصر المعاد تدويرها
for (let i = 0; i < this.options.maxRenderedItems; i++) {
const li = document.createElement('li');
li.className = 'pooled-item';
li.style.display = 'none';
this.itemPool.add(li);
this.recycledItems.push(li);
}
}
getItemFromPool() {
if (this.recycledItems.length > 0) {
return this.recycledItems.pop();
}
// إنشاء عنصر جديد إذا نفدت المجموعة
const li = document.createElement('li');
li.className = 'pooled-item';
return li;
}
returnItemToPool(item) {
item.style.display = 'none';
item.innerHTML = '';
this.recycledItems.push(item);
}
renderItem(data, index) {
let item = this.activeItems.get(index);
if (!item) {
item = this.getItemFromPool();
this.activeItems.set(index, item);
this.container.appendChild(item);
}
item.innerHTML = this.options.renderTemplate(data);
item.style.display = 'block';
item.dataset.index = index;
return item;
}
removeItem(index) {
const item = this.activeItems.get(index);
if (item) {
this.container.removeChild(item);
this.returnItemToPool(item);
this.activeItems.delete(index);
}
}
clear() {
this.activeItems.forEach((item, index) => {
this.removeItem(index);
});
}
// تنظيف الذاكرة
cleanup() {
this.clear();
this.itemPool.forEach(item => {
if (item.parentNode) {
item.parentNode.removeChild(item);
}
});
this.itemPool.clear();
this.recycledItems = [];
}
}
17. اختبارات شاملة للقوائم
17.1 اختبارات وحدة (Unit Tests)
// اختبارات Jest للقوائم
describe('List Functions', () => {
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
test('should create valid list structure', () => {
const ul = document.createElement('ul');
const li1 = document.createElement('li');
const li2 = document.createElement('li');
li1.textContent = 'العنصر الأول';
li2.textContent = 'العنصر الثاني';
ul.appendChild(li1);
ul.appendChild(li2);
container.appendChild(ul);
expect(ul.children.length).toBe(2);
expect(ul.children[0].textContent).toBe('العنصر الأول');
expect(ul.querySelector('li:first-child')).toBe(li1);
});
test('should handle nested lists correctly', () => {
const ul = document.createElement('ul');
const li = document.createElement('li');
const nestedUl = document.createElement('ul');
const nestedLi = document.createElement('li');
nestedLi.textContent = 'عنصر متداخل';
nestedUl.appendChild(nestedLi);
li.appendChild(document.createTextNode('عنصر رئيسي'));
li.appendChild(nestedUl);
ul.appendChild(li);
expect(ul.querySelector('ul ul')).toBe(nestedUl);
expect(ul.querySelectorAll('li').length).toBe(2);
});
test('should validate accessibility attributes', () => {
const nav = document.createElement('nav');
const ul = document.createElement('ul');
nav.setAttribute('aria-label', 'التنقل الرئيسي');
ul.setAttribute('role', 'menubar');
nav.appendChild(ul);
expect(nav.getAttribute('aria-label')).toBe('التنقل الرئيسي');
expect(ul.getAttribute('role')).toBe('menubar');
});
test('should filter list items correctly', () => {
const ul = document.createElement('ul');
const items = ['تفاح', 'برتقال', 'موز'];
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.dataset.search = item;
ul.appendChild(li);
});
const filter = new AdvancedListFilter(ul);
filter.currentFilters.search = 'تفاح';
const visibleCount = filter.applyFilters();
expect(visibleCount).toBe(1);
expect(ul.querySelector('li[data-search="تفاح"]').style.display).not.toBe('none');
});
});
// اختبارات الأداء
describe('Performance Tests', () => {
test('should handle large lists efficiently', (done) => {
const startTime = performance.now();
const ul = document.createElement('ul');
// إضافة 1000 عنصر
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `عنصر ${i}`;
ul.appendChild(li);
}
document.body.appendChild(ul);
// قياس الوقت
requestAnimationFrame(() => {
const endTime = performance.now();
const duration = endTime - startTime;
expect(duration).toBeLessThan(100); // أقل من 100ms
expect(ul.children.length).toBe(1000);
document.body.removeChild(ul);
done();
});
});
});
17.2 اختبارات إمكانية الوصول
// اختبارات WAVE وaxe-core
async function testAccessibility() {
const results = [];
// اختبار القوائم بدون تسميات
const unlabeledLists = document.querySelectorAll('ul:not([aria-label]):not([aria-labelledby])');
if (unlabeledLists.length > 0) {
results.push({
type: 'warning',
message: `${unlabeledLists.length} قائمة بدون تسمية مناسبة`,
elements: unlabeledLists
});
}
// اختبار القوائم الفارغة
const emptyLists = document.querySelectorAll('ul:empty, ol:empty');
if (emptyLists.length > 0) {
results.push({
type: 'error',
message: `${emptyLists.length} قائمة فارغة`,
elements: emptyLists
});
}
// اختبار بنية القوائم المتداخلة
const invalidNested = document.querySelectorAll('ul > ul, ol > ol');
if (invalidNested.length > 0) {
results.push({
type: 'error',
message: 'قوائم متداخلة بشكل خاطئ',
elements: invalidNested
});
}
// اختبار الروابط بدون نص
const emptyLinks = document.querySelectorAll('li a:empty:not([aria-label]):not([title])');
if (emptyLinks.length > 0) {
results.push({
type: 'error',
message: 'روابط فارغة في القوائم',
elements: emptyLinks
});
}
return results;
}
// اختبار باستخدام axe-core
if (typeof axe !== 'undefined') {
axe.run(document, {
tags: ['wcag2a', 'wcag2aa'],
rules: {
'list': { enabled: true },
'listitem': { enabled: true },
'definition-list': { enabled: true }
}
}).then(results => {
console.log('نتائج اختبار إمكانية الوصول:', results);
});
}
18. خلاصة أهم المحاذير والممارسات
❌ الأخطاء التي يجب تجنبها مطلقاً
- بنية HTML خاطئة
- وضع عناصر غير
<li>
مباشرة في<ul>
- استخدام
<li>
خارج قائمة - قوائم متداخلة خارج
<li>
- وضع عناصر غير
- إمكانية الوصول
- إزالة
list-style
بدون بديل بصري - عدم وجود تسميات مناسبة للقوائم التفاعلية
- تجاهل قارئات الشاشة
- إزالة
- الأداء
- عرض آلاف العناصر دون تحسين
- عدم استخدام التحميل التدريجي
- تجاهل تحسين الذاكرة
- الأمان
- عدم تنظيف المحتوى المدخل من المستخدم
- تجاهل حماية CSRF
- عدم التحقق من صحة البيانات
✅ أفضل الممارسات الواجب اتباعها
- البنية الدلالية
- استخدام القوائم للمحتوى المنطقي فقط
- تطبيق التداخل الصحيح
- استخدام البيانات المنظمة عند الحاجة
- تحسين SEO
- محتوى وصفي وواضح
- استخدام العناوين المناسبة
- تطبيق Schema.org markup
- إمكانية الوصول
- تسميات واضحة ووصفية
- دعم التنقل بالكيبورد
- إعلانات مناسبة للتغييرات
- الأداء والتجربة
- تحسين القوائم الكبيرة
- استخدام التحميل التدريجي
- تطبيق أنيميشن سلس
- الاختبار والجودة
- اختبار على أجهزة مختلفة
- التحقق من إمكانية الوصول
- قياس الأداء باستمرار
هذا الدليل الشامل يغطي جميع جوانب استخدام تاغات <ul>
و <li>
بشكل احترافي ومتقدم، من الأساسيات إلى التطبيقات المعقدة.