# استخدام الحلويات Altert2 لمؤشرات تحميل HTMX HTMX (مؤشرات Hx-indicator)

<datetime class="hidden">2025-04-21T20:05</datetime>

<!--category-- Javascript, HTMX -->
# أولاً

على مشروع عمل كنت استخدمه و أسيء استعمال HTMX لبناء إدارة UI. كجزء من هذا انا استخدم [Ail Altert 2](https://sweetalert2.github.io/#examples)  مستنسخاً [من أجل تأكيدي حوارات](https://www.mostlylucid.net/blog/showingtoastandswappingwithhtmx#additional-htmx-features)/ / / /  إنها تعمل بشكل رائع لكنني أيضاً أردت أن أستخدمها لإستبدال مؤشرات تحميل HTMX الخاصة بي.

هذا أثبت أنه تحدي لذا فكرت أن أوثقه هنا لأنقذك من نفس الألم

 <p style="color:red">Warning I'm a C# coder my Javascript is likely horrible.</p>
[TOC]

# المشكلة

اذاً HTMX ذكي جداً، انه `hx-indicator` يسمح لك عادة بوضع مؤشر تحميل لطلبات HTMX الخاصة بك.  هذا هو عنصر HTML في صفتك مثل

```html

<div id="loading-modal" class="modal htmx-indicator">
    <div class="modal-box flex flex-col items-center justify-center bg-base-200 border border-base-300 shadow-xl rounded-xl text-base-content dark text-center ">
        <div class="flex flex-col items-center space-y-4">
            <h2 class="text-lg font-semibold tracking-wide">Loading...</h2>
            <span class="loading loading-dots loading-xl text-4xl text-stone-200"></span>
        </div>
    </div>
</div>
```

ثم عندما تريد استخدامها كنت تزين طلبك HTMX مع `hx-indicator="#loading-modal"` وسيبين النموذج عندما يكون الطلب قيد التنفيذ ()[انظر هنا للتفاصيل](https://htmx.org/attributes/hx-indicator/)).

الآن HTMX يقوم ببعض السحر الذكي باستخدام `request` شيء [هو يُمْسكُ داخلياً داخلياً](https://github.com/bigskysoftware/htmx/blob/86893ebf4cb0f38484a522044f9a07cdd79398fa/src/htmx.js#L3293)

```javascript
  function addRequestIndicatorClasses(elt) {
    let indicators = /** @type Element[] */ (findAttributeTargets(elt, 'hx-indicator'))
    if (indicators == null) {
      indicators = [elt]
    }
    forEach(indicators, function(ic) {
      const internalData = getInternalData(ic)
      internalData.requestCount = (internalData.requestCount || 0) + 1
      ic.classList.add.call(ic.classList, htmx.config.requestClass)
    })
    return indicators
  }

```

لذا، فإن استبدال هذه التحديات يشكل تحدياً قليلاً. كيف تتبّع الطلبات ثمّ تظهر نمط (سويت أليرت 2) عندما يكون الطلب قيد التنفيذ وتخبئه عندما ينتهي.

# A

لذلك بدأت (ليس لأنني اضطررت لذلك، لأنني كنت بحاجة إلى:) للاستعاضة عن مؤشر تحميل HTMX بمقياس تحميل سويتAlert2.
على أي حال هذا هو القانون الذي توصلت إليه

سوف تبدأ إما باستيراد سويث Altert2 في HTML (كنظام وعلامات أسلوب) / استيراده لحزمة ويب أو ما شابه (أو ما شابه ذلك)[(أ) أن يروا هذه الوثائق لهذا الغرض).
](https://sweetalert2.github.io/#download)

بعد npm تثبيته يمكنك استيراده مثل هذا في ملفك المشترك.

```javascript
import Swal from 'sweetalert2';
```

ثمّ رمزي الرئيسي يَبْدو مثل هذا:

```javascript
import Swal from 'sweetalert2';

const SWEETALERT_PATH_KEY = 'swal-active-path'; // Stores the path of the current SweetAlert
const SWEETALERT_HISTORY_RESTORED_KEY = 'swal-just-restored'; // Flag for navigation from browser history
const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader

let swalTimeoutHandle = null;

export function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;
        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;

        const path = getRequestPath(evt.detail);
        if (!path) return;

        const currentPath = sessionStorage.getItem(SWEETALERT_PATH_KEY);

        // Show SweetAlert only if the current request path differs from the previous one
        if (currentPath !== path) {
            closeSweetAlertLoader();
            sessionStorage.setItem(SWEETALERT_PATH_KEY, path);
            evt.detail.indicator = null; // Disable HTMX's default indicator behavior

            Swal.fire({
                title: 'Loading...',
                allowOutsideClick: false,
                allowEscapeKey: false,
                showConfirmButton: false,
                theme: 'dark',
                didOpen: () => {
                    // Cancel immediately if restored from browser history
                    if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
                        sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
                        Swal.close();
                        return;
                    }

                    Swal.showLoading();
                    document.dispatchEvent(new CustomEvent('sweetalert:opened'));

                    // Set timeout to auto-close if something hangs
                    clearTimeout(swalTimeoutHandle);
                    swalTimeoutHandle = setTimeout(() => {
                        if (Swal.isVisible()) {
                            console.warn('SweetAlert loading modal closed after timeout.');
                            closeSweetAlertLoader();
                        }
                    }, SWEETALERT_TIMEOUT_MS);
                },
                didClose: () => {
                    document.dispatchEvent(new CustomEvent('sweetalert:closed'));
                    sessionStorage.removeItem(SWEETALERT_PATH_KEY);
                    clearTimeout(swalTimeoutHandle);
                    swalTimeoutHandle = null;
                }
            });
        } else {
            // Suppress HTMX indicator if the path is already being handled
            evt.detail.indicator = null;
        }
    });

    //Add events to close the loader
    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
    document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);

    // Set a flag so we can suppress the loader immediately if navigating via browser history
    document.body.addEventListener('htmx:historyRestore', () => {
        sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
    });

    window.addEventListener('popstate', () => {
        sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
    });
}

// Returns the closest element with an indicator attribute
function getIndicatorSource(el) {
    return el.closest('[hx-indicator], [data-hx-indicator]');
}

// Determines the request path, including query string if appropriate
function getRequestPath(detail) {
    const responsePath =
        typeof detail?.pathInfo?.responsePath === 'string'
            ? detail.pathInfo.responsePath
            : (typeof detail?.pathInfo?.path === 'string'
                    ? detail.pathInfo.path
                    : (typeof detail?.path === 'string' ? detail.path : '')
            );

    const elt = detail.elt;

    // If not a form and has an hx-indicator, use the raw path
    if (elt.hasAttribute("hx-indicator") && elt.tagName !== "FORM") {
        return responsePath;
    }

    const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
    const form = elt.closest('form');

    // Append query params for GET form submissions
    if (isGet && form) {
        const params = new URLSearchParams();

        for (const el of form.elements) {
            if (!el.name || el.disabled) continue;

            const type = el.type;
            if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
            if (type === 'submit') continue;

            params.append(el.name, el.value);
        }

        const queryString = params.toString();
        return queryString ? `${responsePath}?${queryString}` : responsePath;
    }

    return responsePath;
}

// Closes the SweetAlert loader if the path matches
function maybeClose(evt) {
    const activePath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
    const path = getRequestPath(evt.detail);

    if (activePath && path && activePath === path) {
        closeSweetAlertLoader();
    }
}

// Close and clean up SweetAlert loader state
function closeSweetAlertLoader() {
    if (Swal.getPopup()) {
        Swal.close();
        document.dispatchEvent(new CustomEvent('sweetalert:closed'));
        sessionStorage.removeItem(SWEETALERT_PATH_KEY);
        clearTimeout(swalTimeoutHandle);
        swalTimeoutHandle = null;
    }
}
```

أنت هذا (إذا كنت تستخدم ESAF) في `main.js` مثل هذا

```javascript

import { registerSweetAlertHxIndicator } from './hx-sweetalert-indicator.js';
registerSweetAlertHxIndicator();
```

### رابعاً - معلومات عن عناصرنا

أنت سَتَرى أَستعملُ `getIndicatorSource` إلى ابحث عنصر مُشغّل HTMX طلب. هذا مهم لأننا بحاجة إلى معرفة أي عنصر تسبب في الطلب حتى نتمكن من إغلاق النسق عندما ينتهي. هذا مهم لأن HTMX لديه "الإرث" لذا تحتاج إلى تسلق الشجرة لتجد العنصر الذي دفع الطلب.

```javascript
function getIndicatorSource(el) {
    return el.closest('[hx-indicator], [data-hx-indicator]');
}

```

ثم بناء على أي طلب HTMX `hx-get` أو `hx-post`(ب) يمكن استخدام `hx-indicator` (ب) تحديد نمط استخدام المادة الحلوة Altert2. لا تحتاج حتى إلى تحديد الفئة كما في السابق، فقط المعامل الموجود يعمل.

دعونا نذهب من خلال كيفية عمل كل هذا:

## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ` registerSweetAlertHxIndicator()`

هذا يَعْملُ كنقطة دخول خارج.  يمكنك أن ترى أنه يوصل إلى `htmx:configRequest` حدث حدث. يتم إطلاق النار هذا عندما HTMX على وشك تقديم طلب.

ثم تحصل على العنصر الذي دفع الحدث في `evt.detail.elt` والتحقق إذا كان لديه `hx-indicator` ........... ،..............................................................................................................

وأخيراً، فإنه يظهر أسلوب SwiteAltert2 باستخدام `Swal.fire()`.

```javascript
rt function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;
        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;
        

```

## الحصول على طلب المسار

إذا كان يعمل، فإنه يحصل على ملف الطلب مستخدم `getRequestPath(evt.detail)` وتخزينه في تخزين الدورة.
Tiw HTMX هو أمر صعب، فإنه يخزن المسار في أماكن مختلفة اعتمادا على مكان وجودك في دورة الحياة. لذا في شيفرتي أقوم بكل ما هو أعلى. (ب) مع `detail?.pathInfo?.path ?? detail?.path ?? '';`

واتضح أن HTMX خزنت *الطلب:* & `detail.path` ومسار الاستجابة (من أجل `document.body.addEventListener('htmx:afterRequest', maybeClose); document.body.addEventListener('htmx:responseError', maybeClose);`في `detail.PathInfo.responsePath` لذلك نحن بحاجة إلى التعامل مع كليهما.

نحتاج أيضاً للتعامل مع `GET` (أ)؛ بما أن استجابتها ستتضمن على الأرجح عناصر العنوان التي تم تمريرها في `<input >` إذاً قيم الاستجابة يمكن لـ(أوريل) أن ينتهي به الأمر إلى أن يكون مختلفاً.

```javascript
// Returns the closest element with an indicator attribute
function getIndicatorSource(el) {
    return el.closest('[hx-indicator], [data-hx-indicator]');
}

// Determines the request path, including query string if appropriate
function getRequestPath(detail) {
    const responsePath =
        typeof detail?.pathInfo?.responsePath === 'string'
            ? detail.pathInfo.responsePath
            : (typeof detail?.pathInfo?.path === 'string'
                    ? detail.pathInfo.path
                    : (typeof detail?.path === 'string' ? detail.path : '')
            );

    const elt = detail.elt;

    // If not a form and has an hx-indicator, use the raw path
    if (elt.hasAttribute("hx-indicator") && elt.tagName !== "FORM") {
        return responsePath;
    }

    const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
    const form = elt.closest('form');

    // Append query params for GET form submissions
    if (isGet && form) {
        const params = new URLSearchParams();

        for (const el of form.elements) {
            if (!el.name || el.disabled) continue;

            const type = el.type;
            if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
            if (type === 'submit') continue;

            params.append(el.name, el.value);
        }

        const queryString = params.toString();
        return queryString ? `${responsePath}?${queryString}` : responsePath;
    }

    return responsePath;
}
```

ملاحظة: هذه هي الحالة بوجه خاص إذا استخدمت `HX-Push-Url` ترويسة إلى تغيير URL من طلب HTMX لـ التاريخ.

### الشكل

`HttpGet` الاستمارات صعبة قليلاً لذا لدينا قطعة شفرة والتي سوف تكتشف إذا كنت قد نقرت على a `submit` زر داخل استمارة وتذييل مؤشّرات سلسلة الاستعلام التي تسبّبها تلك المدخلات الفضفاضة لمقارنةها مع الواجهة URL.

```javascript
const isGet = (detail.verb ?? '').toUpperCase() === 'GET';
    const form = elt.closest('form');

    // Append query params for GET form submissions
    if (isGet && form) {
        const params = new URLSearchParams();

        for (const el of form.elements) {
            if (!el.name || el.disabled) continue;

            const type = el.type;
            if ((type === 'checkbox' || type === 'radio') && !el.checked) continue;
            if (type === 'submit') continue;

            params.append(el.name, el.value);
        }

        const queryString = params.toString();
        return queryString ? `${responsePath}?${queryString}` : responsePath;
    }

    return responsePath;
    ```
    
This is important as HTMX will use the response URL to determine if the request is the same as the previous one. So we need to ensure we have the same URL in both places.

### Extensions
I use this little `Response` extension method to set the `HX-Push-Url` header in my ASP.NET Core app. I also added a second extension which will immediately close the modal (useful if you mess with the request and need to close it immediately). 
```csharp
public static class ResponseExtensions
{
    public static void PushUrl(this HttpResponse response, HttpRequest request)
    {
        response.Headers["HX-Push-Url"] = request.GetEncodedUrl();
    }
}
    public static void CloseSweetAlert(this HttpResponse response)
    {
        response.Headers.Append("HX-Trigger" , JsonSerializer.Serialize(new
        {
            sweetalert = "closed"
        }));

    }
}
```

هذه الثانية يتم التعامل معها هنا:

```javascript
    document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);
```

## يَحْرُس المسار

حسناً، الآن لدينا الطريق، ماذا نفعل به؟ حسناً، لنحافظ على أثر الطلب الذي قام بتفعيل نموذج "سويت أليرت 2" نخزنه في `sessionStorage`  `sessionStorage.setItem(SWEETALERT_PATH_KEY, path);`.

(مجدداً يمكنك جعل هذا أكثر تعقيداً وضمان أن يكون لديك واحد فقط إذا أردت)

## إظهار الواسطة

ثم نعرض ببساطة طريقة Sweet Altert2 باستخدام `Swal.fire()`/ / / / ملاحظة لدينا مجموعة من الخيارات هنا.

عند فتحه يفحص لمفتاح تخزين دورة `SWEETALERT_HISTORY_RESTORED_KEY` الذي يُوضع عندما يُستعاد التاريخ. إذا كان كذلك، نغلق الواسطة فوراً (يحفظ HTMX يعبث بنا مع إدارة التاريخ الفردية).

نحن أيضاً نحرق حدثاً `sweetalert:opened` التي يمكنك استخدامها للقيام بأي منطق مخصص تحتاج إليه.

```javascript
Swal.fire({
    title: 'Loading...',
    allowOutsideClick: false,
    allowEscapeKey: false,
    showConfirmButton: false,
    theme: 'dark',
    didOpen: () => {
        // Cancel immediately if restored from browser history
        if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
            sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
            Swal.close();
            return;
        }

        Swal.showLoading();
        document.dispatchEvent(new CustomEvent('sweetalert:opened'));

        // Set timeout to auto-close if something hangs
        clearTimeout(swalTimeoutHandle);
        swalTimeoutHandle = setTimeout(() => {
            if (Swal.isVisible()) {
                console.warn('SweetAlert loading modal closed after timeout.');
                closeSweetAlertLoader();
            }
        }, SWEETALERT_TIMEOUT_MS);
    },
    didClose: () => {
        document.dispatchEvent(new CustomEvent('sweetalert:closed'));
        sessionStorage.removeItem(SWEETALERT_PATH_KEY);
        clearTimeout(swalTimeoutHandle);
        swalTimeoutHandle = null;
    }
});
```

وبالإضافة إلى ذلك، حددنا فترة زمنية لمعالجة القضايا التي يُعلّق فيها الطلب. هذا مهم جداً لأن HTMX لا تغلق دائماً الواسطة إذا فشل الطلب (خاصة إذا استخدمت `hx-boost`)ع( هذا موجود هنا `const SWEETALERT_TIMEOUT_MS = 10000; // Timeout for automatically closing the loader` اذاً يمكننا اغلاق النسق اذا حدث خطأ ما (سوف يكون ايضاً سجل الى الرافعة).

## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

لذلك الآن لدينا النسق المفتوح، ونحن بحاجة إلى إغلاقه عندما ينتهي الطلب. للقيام بهذا نحن ندعو `maybeClose` الدالة الدالة. هذا هو عندما يكون الطلب قد انتهى (إما بنجاح أو بخطأ).
& بوصة `htmx:afterRequest` وقد عقد مؤتمراً بشأن `htmx:responseError` أحداث أحداث أحداث.  هذه الأحداث تحرق بمجرد الانتهاء من HTMX طلب (ملاحظة، هذه مهمة، خاصية لـ HTMX `HX-Boost` والتي يمكن أن تكون مضحكة قليلاً حول ما هي الأحداث التي تطلق النار.)

```javascript
    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
```

```javascript
    function maybeClose(evt) {
        const activePath = sessionStorage.getItem(SWEETALERT_PATH_KEY);
        const path = getRequestPath(evt.detail);

        if (activePath && path && activePath === path) {
            Swal.close();
            sessionStorage.removeItem(SWEETALERT_PATH_KEY);
        }
    }
```

سترى هذه الدالة التحقق إذا كان المسار في تخزين الجلسة هو نفس مسار الطلب. وإذا كان الأمر كذلك، فإنه يغلق الواسطة ويزيل المسار من تخزين الدورة.

## إنه تاريخ التاريخ

HTMX لديه طريقة صلبة لمعالجة التاريخ الذي يمكن أن يترك وسيط'stuck' مفتوحة على القيام بصفحة خلفية. اذاً نضيف عدة أحداث أخرى للقبض على هذا (معظم الوقت نحتاج الى واحد فقط لكن الحزام و الدعامات).

```javascript
    //Add events to close the loader
    document.body.addEventListener('htmx:afterRequest', maybeClose);
    document.body.addEventListener('htmx:responseError', maybeClose);
    document.body.addEventListener('sweetalert:close', closeSweetAlertLoader);

    // Set a flag so we can suppress the loader immediately if navigating via browser history
    document.body.addEventListener('htmx:historyRestore', () => {
        sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
    });

    window.addEventListener('popstate', () => {
        sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');
    });`
```

سترى نحن أيضاً نضع `sessionStorage.setItem(SWEETALERT_HISTORY_RESTORED_KEY, 'true');` الذي نتحقق منه في `didOpen` حدث:

```javascript
           didOpen: () => {
    // Cancel immediately if restored from browser history
    if (sessionStorage.getItem(SWEETALERT_HISTORY_RESTORED_KEY) === 'true') {
        sessionStorage.removeItem(SWEETALERT_HISTORY_RESTORED_KEY);
        Swal.close();
        return;
    }
```

نحن نفعلها في الحدث كما لو أن الواسطة لا تفتح على الفور على `popstate` \ `htmx:historyRestore`  (خاصة إذا كان لديك الكثير من التاريخ) لذلك نحن بحاجة إلى التحقق من ذلك في `didOpen` الحدث (بمعنى أنه في الدورة الرئيسية، في بعض الأحيان هذا يمكن إعادة تحميل إلى آخره... لذا علينا أن نكون مدركين لذلك).

# من اجل مساعدي الذي استدعي

إذا كنت تستخدم [الطّاف المُنجِج](https://www.mostlylucid.net/blog/category/PagingTagHelper) هناك مشكلة مع `PageSize` (ب) أن هذا يرجع إليه أيضاً معلقاً `  `htmx: حدث configapire. لذلك نحن بحاجة إلى إضافة رمز صغير للتأكد من أنه لا يتعارض مع أسلوب SwiteAltert2.

```javascript
(() => {
    if (window.__pageSizeListenerAdded) return;

    document.addEventListener('htmx:configRequest', event => {
        const { elt } = event.detail;
        if (elt?.matches('[name="pageSize"]')) {
            const params = new URLSearchParams(window.location.search);
            params.set('pageSize', elt.value);

            const paramObj = Object.fromEntries(params.entries());
            event.detail.parameters = paramObj;
            
            const pageSizeEvent = new CustomEvent('pagesize:updated', {
                detail: {
                    params: paramObj,
                    elt,
                },
            });

            document.dispatchEvent(pageSizeEvent);
        }
    });

    window.__pageSizeListenerAdded = true;
})();
```

إلى إصلاح هذا أنت إلى اضف a صغير الدالة:

```javascript
/**
 * Appends the current pageSize from the browser's URL to the HTMX request path,
 * modifying event.detail.path directly.
 *
 * @param {CustomEvent['detail']} detail - The HTMX event.detail object
 */
export function getPathWithPageSize(detail) {
    const originalPath = detail.path;
    if (!originalPath) return null;

    const selectedValue = detail.elt?.value ?? '';
    const url = new URL(originalPath, window.location.origin);

    // Use the current location's query string, update pageSize
    const locationParams = new URLSearchParams(window.location.search);
    locationParams.set('pageSize', selectedValue);
    url.search = locationParams.toString();

    return url.pathname + url.search;
}
```

هذا فقط يحصل على الطلب ويضيف `pagesize` البارامترات إلى ذلك. 

```javascript
import { getPathWithPageSize } from './pagesize-sweetalert'

export function registerSweetAlertHxIndicator() {
    document.body.addEventListener('htmx:configRequest', function (evt) {
        const trigger = evt.detail.elt;

        const indicatorAttrSource = getIndicatorSource(trigger);
        if (!indicatorAttrSource) return;

        // ✅ If this is a pageSize-triggered request, use our custom path
        let path;
        if (evt.detail.headers?.['HX-Trigger-Name'] === 'pageSize') {
            path = getPathWithPageSize(evt.detail);
            console.debug('[SweetAlert] Using custom path with updated pageSize:', path);
        } else {
            path = getRequestPath(evt.detail);
        }
        ...rest of code

```

يمكنك أن ترى هنا أننا نكشف `pageSize` وعندما تجده (إذاً غيرت الصفحات) تستخدم `getPathWithPageSize` إلى get جديد المسار. الـ `pageSize` سيستبدل الرمز هذا لاحقاً في الأنبوب لكن هذا يسمح له بالكشف بشكل صحيح حتى يغلق كما هو متوقع.

# في الإستنتاج

اذاً هكذا يمكنك استخدام سويت اللترت2 كمؤشر تحميل HTMX إنه نوع من الإختراق لكنه يعمل وهي طريقة لطيفة لاستخدام نفس المكتبة لكلا مؤشري التحميل وثابت الحوار 9.