إنشاء أحد مكونات المبدل

نظرة عامة أساسية حول كيفية إنشاء مكوِّن مبدل سريع الاستجابة ويمكن الوصول إليه.

في هذه المشاركة، أود مشاركة التفكير حول طريقة لإنشاء مكونات المبدلات. جرِّب الإصدار التجريبي.

إصدار تجريبي

في ما يلي إصدار YouTube من هذه المشاركة إذا كنت تفضّل الفيديوهات:

نظرة عامة

يعمل switch بشكل مشابه لمربّع اختيار ولكنه يمثل بشكل صريح حالات التشغيل والإيقاف المنطقية.

يستخدم هذا العرض التوضيحي <input type="checkbox" role="switch"> لمعظم وظائفه، وهو ما يعني أنّه لا حاجة إلى CSS أو JavaScript ليكون مؤهّلاً لجميع وظائفه ويتيح إمكانية الوصول إليه. يوفر تحميل CSS دعمًا للغات التي تُكتب من اليمين إلى اليسار، والوضع العمودي، والرسوم المتحركة، والمزيد. فإن تحميل JavaScript يجعل التبديل قابلاً للسحب وملموسًا.

الخصائص المخصّصة

تمثل المتغيرات التالية الأجزاء المختلفة من المبدل وخياراتها. باعتبارها الفئة ذات المستوى الأعلى، تحتوي .gui-switch على خصائص مخصّصة يتم استخدامها في جميع العناصر الثانوية الخاصة بالمكوِّنات ونقاط دخول للتخصيص المركزي.

الأغنية

الطول (--track-size) والمساحة المتروكة واللونان:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

موسيقى آلة الأمبيرا

يبرز الحجم ولون الخلفية والتفاعل ألوانًا مختلفة:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

حركة منخفضة

لإضافة اسم مستعار واضح وتقليل التكرار، يمكن وضع طلب بحث وسائط المستخدم لتفضيل الحركة المخفّض في خاصية مخصّصة باستخدام المكوّن الإضافي PostCSS بناءً على مواصفات المسودة في استعلامات الوسائط 5:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

Markup

اخترتُ إحاطة عنصر <input type="checkbox" role="switch"> بالرمز <label> ودمج علاقته لتجنُّب الالتباس بين علامة الاختيار وربط التصنيف، مع منح المستخدم القدرة على التفاعل مع التصنيف لتبديل الإدخال.

تسمية ومربع اختيار طبيعي وغير نمطي.

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

يتوفّر <input type="checkbox"> مع API وstate. يُدير المتصفح موقع checked وأحداث الإدخال مثل oninputوonchanged.

التنسيقات

وFlexbox وgrid والخصائص المخصَّصة ضرورية في الحفاظ على أنماط هذا المكوِّن. فهي تقوم بتمركز القيم، وتعطي أسماء إلى حسابات أو مناطق غامضة، وتتيح واجهة برمجة تطبيقات صغيرة للخصائص المخصصة لإجراء عمليات تخصيص سهلة للمكونات.

.gui-switch

التصميم ذو المستوى الأعلى لمفتاح التبديل هو flexbox. تحتوي الفئة .gui-switch على الخصائص المخصصة الخاصة والعامة التي يستخدمها الأطفال لحساب تخطيطاتهم.

أدوات مطوّري البرامج Flexbox Devtools تتراكب على تصنيف أفقي ومفتاح تبديل، وتعرض كيفية توزيع التخطيط للمساحة

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

يشبه تمديد تنسيق flexbox وتعديله أيّ تنسيق flexbox. على سبيل المثال، لوضع تصنيفات أعلى أو أسفل مفتاح تحكّم، أو لتغيير flex-direction:

أدوات مطوّري برامج Flexbox التي تتراكب على تصنيف عمودي ومفتاح تحكّم

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

الأغنية

يتم تصميم إدخال مربّع الاختيار كمسار تبديل من خلال إزالة appearance: checkbox العادي وتقديم حجمه بدلاً من ذلك:

&quot;Grid DevTools&quot; (أدوات مطوري الشبكة) تظهر على مسار التبديل، وتعرض مناطق مسار الشبكة
المسماة باسم &#39;track&#39;.

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

ينشئ المسار أيضًا منطقة مسار شبكة خلية واحدة تلو الأخرى لإبهامك للمطالبة بها.

موسيقى آلة الأمبيرا

ويزيل النمط appearance: none أيضًا علامة الاختيار المرئية التي يوفّرها المتصفّح. يستخدم هذا المكوِّن عنصرًا زائفًا و:checked الفئة الزائفة في الإدخال لاستبدال هذا المؤشر المرئي.

الإبهام هو عنصر ثانوي مرتبط بالسمة input[type="checkbox"] ويرتبط أعلى المسار بدلاً من أسفله من خلال المطالبة بمساحة الشبكة track:

أدوات مطوري البرامج تعرض الصورة المصغّرة والعناصر الزائفة في حال وضعها داخل شبكة CSS.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

الأنماط

تعمل الخصائص المخصصة على تفعيل مكون تبديل متعدد الاستخدامات يتكيف مع أنظمة الألوان واللغات التي تُكتب من اليمين إلى اليسار وتفضيلات الحركة.

مقارنة جنبًا إلى جنب بين المظهر الفاتح والداكن للمفتاح وحالاته.

أنماط تفاعل اللمس

على الهاتف المحمول، تضيف المتصفحات ميزات تمييز النقر وميزات تحديد النص إلى التسميات والإدخالات. أثرت هذه سلبًا على ملاحظات النمط والتفاعل المرئي التي يحتاجها هذا التبديل. باستخدام بضعة أسطر من CSS، يمكنني إزالة هذه التأثيرات وإضافة نمط cursor: pointer الخاص بي:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

لا يُنصح دائمًا بإزالة هذه الأنماط، لأنها يمكن أن تكون ملاحظات قيّمة على التفاعل المرئي. تأكَّد من تقديم بدائل مخصّصة في حال إزالتها.

الأغنية

تتعلق أنماط العنصر في الغالب بشكله ولونه، الذي يمكن الوصول إليه من العنصر الرئيسي .gui-switch من خلال الشلال.

يمكنك تبديل الصيغ بأحجام وألوان للمسارات المخصّصة.

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

تتوفر مجموعة متنوعة من خيارات التخصيص لمسار التبديل من أربع خصائص مخصصة. تمت إضافة border: none لأن appearance: none لا يزيل الحدود من مربع الاختيار على جميع المتصفحات.

موسيقى آلة الأمبيرا

عنصر الإبهام موجود من قبل على track الأيمن ولكنه يحتاج إلى أنماط دائرة:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

أدوات مطوّري البرامج معروضة تبرز العنصر الزائف للإبهام على شكل دائرة.

تفاعل

استخدم الخصائص المخصصة للتحضير للتفاعلات التي ستظهر لك من حيث التمرير السريع وتغييرات موضع الإبهام. يتم أيضًا تحديد تفضيل المستخدم قبل نقل أنماط التمييز بالحركة أو التمرير.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

موضع الإبهام

توفر الخصائص المخصصة آلية مصدر واحد لوضع الرمز المصغرة في المسار. تتوفّر لدينا مقاسات المضمار والإبهام التي سنستخدمها في العمليات الحسابية للحفاظ على إزاحة الإبهام بشكل صحيح وبين المسار: 0% و100%.

يملك العنصر input متغير الموضع --thumb-position، ويستخدمه العنصر الزائف كموضع translateX:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

يمكننا الآن تغيير --thumb-position من CSS والفئات الزائفة المتوفرة في عناصر مربّعات الاختيار. وبما أنّنا ضبطنا transition: transform var(--thumb-transition-duration) ease بشكل مشروط في هذا العنصر، قد تتحرك هذه التغييرات عند تغييرها:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

اعتقدت أن هذه الأوركسترا المنفصلة قد أفادت. يرتبط عنصر الإبهام بنمط واحد فقط، وهو موضع translateX. يمكن للمدخل إدارة جميع التعقيدات والعمليات الحسابية.

رأسي

وتم إجراء الدعم باستخدام فئة التعديل -vertical التي تضيف دوران باستخدام عمليات تحويل CSS إلى العنصر input.

ومع ذلك، لا يغير العنصر الذي يتم تدويره ثلاثي الأبعاد الارتفاع الإجمالي للمكون، مما قد يؤدي إلى التخلص من تخطيط الكتلة. عليك مراعاة ذلك باستخدام المتغيّرَين --track-size و--track-padding. احسب الحد الأدنى من المساحة المطلوبة لتدفق الزر الرأسي في التخطيط كما هو متوقع:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) من اليمين إلى اليسار

أنشأت أنا، أحد أصدقاء خدمة CSS، إيلاد شكتر، نموذجًا أوّليًا لقائمة جانبية من الشريحة باستخدام عمليات تحويل CSS التي تتعامل مع اللغات من اليمين إلى اليسار من خلال قلب متغير واحد. لقد فعلنا ذلك نظرًا لعدم وجود عمليات تحويل للخصائص المنطقية في CSS، وربما لا يحدث ذلك أبدًا. كان لدى "إلد" فكرة رائعة عن استخدام قيمة خاصية مخصصة لقلب النسب المئوية، للسماح بإدارة موقع فردي لمنطقنا المخصص للتحويلات المنطقية. لقد استخدمت نفس الطريقة في هذا التبديل وأعتقد أنه نجح بشكل رائع:

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

إنّ السمة المخصّصة التي تُسمى --isLTR تحتفظ في البداية بالقيمة 1، ما يعني أنّ القيمة هي true لأنّ التنسيق يكون تلقائيًا من اليسار إلى اليمين. وبعد ذلك، باستخدام الفئة الزائفة في CSS :dir()، يتم ضبط القيمة على -1 عندما يكون المكوِّن في تنسيق من اليمين إلى اليسار.

ضع --isLTR موضع التنفيذ من خلال استخدامه في calc() داخل تحويل:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

الآن يراعي تدوير مفتاح التبديل العمودي موضع الجانب الآخر الذي يتطلبه التنسيق من اليمين إلى اليسار.

يجب أيضًا تعديل عمليات التحويل translateX على العنصر الزائف للإبهام لمراعاة متطلبات الجانب الآخر:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

ومع أنّ هذا المنهج لن ينجح في تلبية كل الاحتياجات المتعلقة بمفهوم مثل عمليات تحويل CSS المنطقية، فهو يوفّر بعض مبادئ DRY للعديد من حالات الاستخدام.

الولايات

ولن يكتمل استخدام ميزة input[type="checkbox"] المضمَّنة بدون معالجة الحالات المختلفة التي يمكن أن تكون فيها: :checked و:disabled و:indeterminate و:hover. تم ترك :focus وحده عن قصد، مع تعديل إزاحةه فقط؛ بدت حلقة التركيز رائعة في Firefox وSafari:

لقطة شاشة لحلقة التركيز التي تركّز على مفتاح تبديل في Firefox وSafari

تم التحقّق منها

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

وتمثل هذه الولاية الحالة on. في هذه الحالة، يتم تعيين خلفية "المسار" للإدخال على اللون النشط ويتم تعيين موضع الإبهام على "النهاية".

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

غير مفعَّل

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

لا يبدو الزر :disabled مختلفًا من الناحية المرئية فحسب، بل يمكنه أيضًا أن يجعل العنصر غير قابل للتغيير.إنّ قابلية التغيّر للتفاعل خالية من المتصفِّح، ولكن تحتاج الحالات المرئية إلى أنماط بسبب استخدام appearance: none.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

مفتاح التبديل ذو النمط الداكن في الحالات التي تم إيقافها، ووضع علامة عليها،
وعدم التحقق منها.

هذه الحالة صعبة نظرًا لأنها تحتاج إلى مظاهر داكنة وفاتحة مع كل من الحالة المعطلة والمحددة. اخترت أسلوبًا الحد الأدنى من الأنماط لهذه الحالات لتخفيف عبء الصيانة لمجموعات الأنماط.

غير محدد

غالبًا ما يتم نسيان الحالة :indeterminate، حيث لا يتم وضع علامة في مربّع الاختيار أو إزالة العلامة من المربّع. هذه حالة مرحة، وجذابة ومتواضعة. تذكير جيد بأن الحالات المنطقية يمكن أن تكون مخادعة بين الحالات.

من الصعب تعيين مربع اختيار غير محدد، ولا يمكن لأحد سوى JavaScript تعيينه:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

الحالة غير المحددة التي يوجد بها إصبع الإبهام في المنتصف، للإشارة إلى عدم اتخاذ قرار.

نظرًا لأن الولاية، بالنسبة لي، متواضعة وجذابة، كان من المناسب وضع مفتاح إبهام التبديل في المنتصف:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

المرور فوق

يجب أن توفر تفاعلات التمرير دعمًا مرئيًا لواجهة المستخدم المتصلة وكذلك تقدم توجيهًا نحو واجهة المستخدم التفاعلية. يبرز مفتاح التبديل هذا الإبهام بحلقة شبه شفافة عند التمرير فوق الملصق أو الإدخال. توفر هذه الرسوم المتحركة التمرير بعد ذلك اتجاهًا نحو عنصر الإبهام التفاعلي.

يتم تطبيق تأثير "التمييز" باستخدام box-shadow. عند التمرير فوق أي إدخال غير مفعَّل، يمكنك زيادة حجم --highlight-size. إذا كان المستخدم يوافق على الحركة، ننقل عنصر box-shadow ونلاحظ تطوّره، وإذا كان لا يمانع من الحركة، سيظهر التمييز على الفور:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

بالنسبة لي، قد تبدو واجهة المبدل غامضة عند محاولة محاكاة واجهة مادية، وخاصةً عند وجود دائرة داخل المسار. لقد تمكن نظام iOS من ذلك بشكل صحيح من خلال مفتاح التبديل الخاص بهم، ويمكنك سحبها من جانب إلى آخر، وهذا أمر ممتع للغاية. وعلى العكس من ذلك، يمكن أن يبدو عنصر واجهة المستخدم غير نشط إذا تمت تجربة إيماءة السحب ولم يحدث شيء.

يمكن سحب الإبهام

يتلقّى العنصر الزائف للإبهام موضعه من var(--thumb-position) بنطاق .gui-switch > input، ويمكن أن توفّر لغة JavaScript قيمة نمط مضمّن في الإدخال لتعديل موضع الإبهام ديناميكيًا، ما يجعله يبدو يتّبع إيماءة المؤشر. عند رفع المؤشر عن المؤشر، أزِل الأنماط المضمّنة وحدِّد ما إذا كان السحب أقرب إلى "إيقاف" أو "مفعَّل" باستخدام السمة المخصّصة --thumb-position. هذا هو العمود الفقري للحلّ، إذ تتتبّع أحداث المؤشر مواضع المؤشرات مشروطًا لتعديل خصائص CSS المخصّصة.

نظرًا لأن المكوِّن كان يعمل بنسبة 100% بالفعل قبل ظهور هذا النص البرمجي، يتطلب الأمر قدرًا كبيرًا من العمل للحفاظ على السلوك الحالي، مثل النقر على تصنيف لتبديل الإدخال. لا ينبغي أن تضيف JavaScript ميزات على حساب الميزات الحالية.

touch-action

السحب هو إيماءة مخصّصة، ما يجعلها مرشحة جدًا للحصول على مزايا touch-action. في حالة مفتاح التبديل هذا، يجب التعامل مع الإيماءة الأفقية من خلال النص البرمجي، أو الإيماءة العمودية التي يتم تسجيلها لصيغة المفتاح العمودي. باستخدام touch-action، يمكننا إخبار المتصفح بالإيماءات التي يجب التعامل معها على هذا العنصر، حتى يتمكن النص البرمجي من التعامل مع إيماءة بدون أي منافسة.

توجه خدمة CSS التالية المتصفح إلى أنه عند بدء إيماءة المؤشر من داخل مسار التبديل هذا، يجب أن تتعامل مع الإيماءات الرأسية، ولن يتم اتخاذ أي إجراء باستخدام الإيماءات الأفقية:

.gui-switch > input {
  touch-action: pan-y;
}

والنتيجة المطلوبة هي إيماءة أفقية لا تعمل أيضًا على تحريك الصفحة أو تمريرها. يمكن للمؤشر التمرير عموديًا بدءًا من داخل الإدخال وتمرير الصفحة، في حين تتم معالجة المؤشرات الأفقية بشكلٍ مخصّص.

أدوات مساعدة بشأن نمط قيمة وحدة البكسل

عند الإعداد وأثناء السحب، يجب سحب قيم الأرقام المحسوبة المختلفة من العناصر. تعرض دوال JavaScript التالية قيم البكسل المحسوبة وفقًا لخاصية CSS. وتُستخدَم في النص البرمجي للإعداد مثل getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

لاحِظ كيف يقبل window.getComputedStyle() وسيطة ثانية، وهي عنصر زائف مستهدَف. من الرائع أن لغة JavaScript يمكنها قراءة العديد من القيم من العناصر، حتى من العناصر الزائفة.

dragging

هذه لحظة أساسية لمنطق السحب وهناك بعض الأشياء التي يجب ملاحظتها من معالج أحداث الدالة:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

جزء النص الرئيسي هو state.activethumb، وهي الدائرة الصغيرة التي يضعها هذا النص مع مؤشر. الكائن switches هو Map() حيث تكون المفاتيح هي .gui-switch والقيم هي حدود وأحجام تخزين مؤقت تحافظ على فعالية النص البرمجي. يتم التعامل مع الترميز من اليمين إلى اليسار باستخدام الخاصية المخصّصة نفسها التي تكون فيها CSS هي --isLTR، ويمكن استخدامها لعكس المنطق ومواصلة إتاحة RTL. قيمة event.offsetX هي أيضًا قيّمة، لأنّها تحتوي على قيمة دلتا مفيدة لتحديد موضع الصورة المصغّرة.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

يحدد السطر الأخير من CSS الخاصية المخصصة التي يستخدمها عنصر الصورة المصغرة. قد تتغيّر عملية تحديد القيمة هذه بمرور الوقت، إلا أنّ حدثًا سابقًا للمؤشر ضبط --thumb-transition-duration مؤقتًا على 0s، ما أدى إلى إزالة ما كان من الممكن أن يكون تفاعلاً بطيئًا.

dragEnd

لكي يتم السماح للمستخدم بالسحب بعيدًا عن مفتاح التبديل وإفلاته، يلزم تسجيل حدث نافذة عمومي:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

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

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

اكتمل التفاعل مع العنصر، ووقت ضبط خاصية الإدخال التي تم وضع علامة عليها وإزالة جميع أحداث الإيماءات. تم تغيير مربّع الاختيار باستخدام state.activethumb.checked = determineChecked().

determineChecked()

تحدد هذه الدالة، التي تُسمى dragEnd، مكان وجود تيار الإبهام داخل حدود المسار وترجع إلى القيمة "صحيح" إذا كان يساوي أو يزيد عن المنتصف على طول المسار:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

أفكار إضافية

تكبدت إيماءة السحب قدرًا قليلاً من الديون في التعليمات البرمجية بسبب بنية HTML الأولية التي تم اختيارها، وغالبًا ما يكون تضمين الإدخال في تصنيف. ستتلقى التسمية، كونها عنصرًا رئيسيًا، تفاعلات النقر بعد الإدخال. في نهاية فعالية dragEnd، ربما تكون قد لاحظت السمة padRelease() كدالة مفردة.

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

ويرجع هذا إلى حساب التصنيف الذي يحصل على هذه النقرة الأخيرة، حيث يؤدي ذلك إلى إلغاء تحديد أو تحديد التفاعل الذي قام به المستخدم.

في حال إجراء ذلك مرة أخرى، قد أفكّر في تعديل نموذج العناصر في المستند (DOM) باستخدام JavaScript أثناء ترقية تجربة المستخدم، لإنشاء عنصر يعالج النقرات على التصنيف نفسه ولا يتعارض مع السلوك المضمَّن.

هذا النوع من JavaScript هو الأقل ما أحب كتابته، ولا أريد إدارة فقاعات الأحداث المشروطة:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

الخلاصة

انتهى الأمر بعنصر التبديل الصغير هذا بين كل تحديات واجهة المستخدم الرسومية حتى الآن! الآن بعد أن عرفت كيف فعلت ذلك، كيف يمكنك‽ 🙂

دعونا ننويع أساليبنا ونتعلم جميع طرق الإنشاء على الويب. يمكنك إنشاء عرض توضيحي وروابط تغريدات لي وسنضيفها إلى قسم الريمكسات في المنتدى أدناه.

ريمكسات من المنتدى

المراجِع

يمكنك العثور على رمز المصدر على GitHub من خلال .gui-switch.