انتقالات عرض المستند نفسه لتطبيقات الصفحة الواحدة

عند تشغيل عملية انتقال للعرض على مستند واحد، يُطلق عليها انتقال عرض المستند نفسه. يحدث هذا عادةً في تطبيقات الصفحة الواحدة (SPA) التي يتم فيها استخدام JavaScript لتحديث DOM. تتوفّر عمليات الانتقال بين عرض المستند نفسه في Chrome بدءًا من الإصدار 111 من Chrome.

لتفعيل عملية نقل عرض المستند نفسه، استدعِ document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

عند الاستدعاء، يلتقط المتصفّح تلقائيًا لقطات من كلّ العناصر التي تتضمّن السمة view-transition-name CSS المُعلَن عنها.

بعد ذلك، تُنفذ عملية استدعاء الدالة التي تم تمريرها، والتي تعمل على تحديث نموذج العناصر في المستند (DOM)، وبعد ذلك يحصل على لقطات للحالة الجديدة.

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


الانتقال التلقائي: تلاشي متقاطع

انتقال العرض الافتراضي هو تلاشي متقاطع، لذا فهو بمثابة مقدمة رائعة لواجهة برمجة التطبيقات:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

حيث يغيِّر updateTheDOMSomehow عنصر DOM إلى الحالة الجديدة. يمكنك إجراء ذلك كيفما شئت. على سبيل المثال، يمكنك إضافة عناصر أو إزالتها أو تغيير أسماء الفئات أو تغيير الأنماط.

وبهذه الطريقة، تتلاشى الصفحات:

التلاشي المتداخل التلقائي. الحد الأدنى للعرض التوضيحي: المصدر.

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


آلية عمل هذه الانتقالات

لنقم بتحديث نموذج التعليمة البرمجية السابق.

document.startViewTransition(() => updateTheDOMSomehow(data));

عند استدعاء الدالة .startViewTransition()، تسجِّل واجهة برمجة التطبيقات الحالة الحالية للصفحة. ويتضمن ذلك أخذ لقطة.

بعد اكتمال العملية، يتم استدعاء مكالمة معاودة الاتصال التي تم تمريرها إلى .startViewTransition(). وهذا هو المكان الذي يتغير فيه نموذج العناصر في المستند (DOM). وبعد ذلك، تسجّل واجهة برمجة التطبيقات الحالة الجديدة للصفحة.

وبعد الحصول على الحالة الجديدة، تنشئ واجهة برمجة التطبيقات شجرة عناصر زائفة على النحو التالي:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

يظهر عنصر ::view-transition فوق كلّ جزء في الصفحة. ويكون هذا مفيدًا إذا كنت تريد تعيين لون خلفية لعملية الانتقال.

::view-transition-old(root) هي لقطة شاشة لطريقة العرض القديمة، و::view-transition-new(root) هي تمثيل مباشر لطريقة العرض الجديدة. يتم عرض كليهما على شكل "محتوى مستبدل" في CSS (مثل <img>).

يتحرك العرض القديم من opacity: 1 إلى opacity: 0، بينما يتحرك العرض الجديد من opacity: 0 إلى opacity: 1، ما ينتج عنه تلاشي متداخل.

يتم تنفيذ جميع الرسوم المتحركة باستخدام رسوم CSS المتحركة، بحيث يمكن تخصيصها باستخدام CSS.

تخصيص الانتقال

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

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

باستخدام هذا التغيير، يصبح تلاشي الآن بطيئًا جدًا:

تلاشي متقاطع طويل. الحد الأدنى للعرض التوضيحي: المصدر.

حسنًا، هذا لا يزال غير مثير للإعجاب. بدلاً من ذلك، يُنفذ الرمز البرمجي التالي انتقال المحور المشترك في التصميم المتعدد الأبعاد:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

وإليك النتيجة:

انتقال المحور المشترك. الحد الأدنى للعرض التوضيحي: المصدر.

نقل عناصر متعددة

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

ولتجنب ذلك، يمكنك استخراج العنوان من باقي الصفحة بحيث يمكن تحريكه بشكل منفصل. ويتم ذلك من خلال تحديد view-transition-name للعنصر.

.main-header {
  view-transition-name: main-header;
}

يمكن أن تكون قيمة view-transition-name أي قيمة تريدها (باستثناء none، ما يعني أنّه ما مِن اسم لعملية النقل). يتم استخدامه لتحديد العنصر بشكل فريد عبر عملية الانتقال.

ونتيجة ذلك:

انتقال المحور المشترك برأس ثابت. الحد الأدنى للعرض التوضيحي: المصدر.

الآن يبقى العنوان في مكانه ويتلاشى متقاطعًا.

تسبب إعلان CSS هذا في تغيير شجرة العنصر الزائف:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

هناك الآن مجموعتا نقل. واحد للرأس والآخر لبقية. ويمكن استهدافها بشكل مستقل باستخدام CSS، ومنحها انتقالات مختلفة. وعلى الرغم من ذلك، في هذه الحالة، يتم ترك main-header مع الانتقال التلقائي، وهو تلاشي متقاطع.

حسنًا، إنّ الانتقال التلقائي ليس تأثير تلاشي متقاطع فقط، بل إنّ الانتقال من خلال ::view-transition-group أيضًا:

  • الموضع والتحويل (باستخدام transform)
  • العرض
  • الطول

لم يكن ذلك مهمًا حتى الآن، لأنّ العنوان له نفس الحجم والموضع مع طرفَي تغيير DOM. ولكن يمكنك أيضًا استخراج النص الظاهر في العنوان:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

يتم استخدام fit-content بحيث يكون العنصر بحجم النص، بدلاً من أن يمتد إلى العرض المتبقي. بدون ذلك، يقلل السهم الخلفي حجم عنصر نص العنوان، بدلاً من الحجم نفسه في كلتا الصفحتين.

لدينا الآن ثلاثة أجزاء:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

ولكن مرة أخرى، ما عليك سوى استخدام الإعدادات الافتراضية:

تمرير نص العنوان: الحد الأدنى للعرض التوضيحي: المصدر.

الآن يقوم نص العنوان بتمرير مرضٍ بعض الشيء عبر لتوفير مساحة لزر الرجوع.


تحريك عدة عناصر زائفة بالطريقة نفسها باستخدام view-transition-class

التوافق مع المتصفح

  • 125
  • 125
  • x
  • x

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

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

هل لديك 20 عنصرًا؟ هذا يعني أنّك تحتاج إلى كتابة 20 أداة اختيار. هل تريد إضافة عنصر جديد؟ وبعد ذلك، عليك أيضًا زيادة أداة الاختيار التي تطبّق أنماط الصور المتحركة. غير قابلة للتطوير بشكل دقيق.

يمكن استخدام view-transition-class في العناصر الصورية لانتقال العرض لتطبيق قاعدة النمط نفسها.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

يستفيد مثال البطاقات التالي من مقتطف CSS السابق. يتم تطبيق التوقيت نفسه على جميع البطاقات، بما في ذلك البطاقات التي تمت إضافتها مؤخرًا، باستخدام أداة اختيار واحدة: html::view-transition-group(.card).

تسجيل الإصدار التجريبي من البطاقات: باستخدام السمة view-transition-class، يتم تطبيق السمة animation-timing-function نفسها على جميع البطاقات باستثناء البطاقات التي تمت إضافتها أو إزالتها.

تصحيح أخطاء الانتقالات

نظرًا لأن انتقالات طرق العرض مبنية على الصور المتحركة في CSS، فإن لوحة الصور المتحركة في Chrome DevTool رائعة لتصحيح أخطاء الانتقالات.

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

تصحيح الأخطاء في الانتقالات بين طرق العرض باستخدام "أدوات مطوري البرامج في Chrome"

لا يلزم أن تكون العناصر الانتقالية عنصر DOM نفسه

استخدمنا حتى الآن view-transition-name لإنشاء عناصر انتقال منفصلة للرأس والنص في العنوان. وهي من الناحية النظرية هي العنصر نفسه قبل تغيير DOM وبعده، ولكن يمكنك إنشاء انتقالات عندما لا يكون الأمر كذلك.

على سبيل المثال، يمكن إضافة view-transition-name إلى الفيديو الرئيسي المضمّن:

.full-embed {
  view-transition-name: full-embed;
}

بعد ذلك، عند النقر على الصورة المصغّرة، يمكن منحها القيمة نفسها view-transition-name، وذلك خلال مدة الانتقال فقط:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

والنتيجة:

ينتقل أحد العناصر إلى عنصر آخر. الحد الأدنى للعرض التوضيحي: المصدر.

تنتقل الصورة المصغّرة الآن إلى الصورة الرئيسية. على الرغم من أنّهما عناصر مختلفة من الناحية النظرية (وحرفيًا)، تتعامل واجهة برمجة التطبيقات الخاصة بالانتقال مع هذه العناصر على أنّها العنصر نفسه لأنّهما يتشاركان عنصر view-transition-name نفسه.

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


عمليات الانتقال والخروج المخصّصة

انظر إلى هذا المثال:

الدخول إلى الشريط الجانبي والخروج منه: الحد الأدنى للعرض التوضيحي: المصدر.

الشريط الجانبي هو جزء من عملية الانتقال:

.sidebar {
  view-transition-name: sidebar;
}

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

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

في المقابل، إذا كان الشريط الجانبي على الصفحة الجديدة فقط، لن يظهر العنصر ::view-transition-old(sidebar) الزائف. بسبب عدم توفّر صورة "قديمة" للشريط الجانبي، سيحتوي زوج الصور على ::view-transition-new(sidebar) فقط. وبالمثل، إذا كان الشريط الجانبي على الصفحة القديمة فقط، سيحتوي زوج الصور على ::view-transition-old(sidebar) فقط.

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

لإنشاء انتقالات محدَّدة للانتقال والخروج، يمكنك استخدام الفئة الزائفة من :only-child لاستهداف العناصر الصورية القديمة أو الجديدة عندما تكون هي العنصر الثانوي الوحيد في زوج الصور:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

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

تحديثات DOM غير المتزامنة، وفي انتظار المحتوى

يمكن أن تعرض معاودة الاتصال التي تم ضبطها إلى .startViewTransition() وعدًا، ما يسمح بتحديثات DOM غير المتزامنة، والانتظار إلى أن يصبح المحتوى المهم جاهزًا.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

لن تبدأ عملية النقل إلا بعد الوفاء بالوعد الذي منحته. وخلال هذا الوقت، تكون الصفحة مجمّدة، لذا يجب الحدّ من حالات التأخير هنا. على وجه التحديد، يجب إجراء عمليات استرجاع الشبكة قبل طلب خدمة .startViewTransition()، علمًا أنّ الصفحة لا تزال تفاعلية بالكامل، بدلاً من إجرائها كجزء من معاودة الاتصال بـ .startViewTransition().

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

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

ومع ذلك، في بعض الحالات يكون من الأفضل تجنب التأخير تمامًا واستخدام المحتوى الموجود لديك من قبل.


الاستفادة إلى أقصى حد من المحتوى المتوفّر لديك

في حال انتقال الصورة المصغّرة إلى صورة أكبر:

الصورة المصغّرة التي تتحوّل إلى صورة أكبر: جرِّب الموقع الإلكتروني التجريبي.

الانتقال التلقائي هو تلاشي متقاطع، مما يعني أن الصورة المصغّرة يمكن أن تتداخل مع الصورة مع صورة كاملة لم يتم تحميلها بعد.

إحدى طرق التعامل مع هذا الأمر هي انتظار تحميل الصورة بالكامل قبل بدء عملية الانتقال. من المفترض أن يتم ذلك قبل طلب البيانات من .startViewTransition()، بحيث تظل الصفحة تفاعلية، ويمكن عرض مؤشر سريان العمل لإعلام المستخدمين بأنّه يتم تحميل الصفحة. ولكن في هذه الحالة، هناك طريقة أفضل:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

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

لن ينجح هذا الأمر إذا كان العرض الجديد يعرض الشفافية، لكننا نعلم في هذه الحالة أنه ليس كذلك، لذا يمكننا إجراء هذا التحسين.

التعامل مع التغييرات في نسبة العرض إلى الارتفاع

من الملائم، أن جميع الانتقالات حتى الآن تمت إلى عناصر بنفس نسبة العرض إلى الارتفاع، لكن هذا لن يكون هو الحال دائمًا. ماذا لو كانت الصورة المصغّرة 1:1 وكانت الصورة الرئيسية 16:9؟

ينتقل أحد العناصر إلى عنصر آخر مع تغيير نسبة العرض إلى الارتفاع. الحد الأدنى للعرض التوضيحي: المصدر.

في عملية الانتقال الافتراضية، تتحرك المجموعة من الحجم السابق إلى الحجم التالي. يتم عرض المجموعة القديمة والجديدة بنسبة 100%، مع ضبط الارتفاع التلقائي، ما يعني احتفاظهما بنسبة العرض إلى الارتفاع بغض النظر عن حجم المجموعة.

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

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

وهذا يعني أن الصورة المصغّرة تبقى في وسط العنصر مع توسع العرض، في حين أن الصورة الكاملة "un-اقتصاص" أثناء انتقالها من 1:1 إلى 16:9.

للحصول على المزيد من المعلومات التفصيلية، يمكنك الاطّلاع على (عرض عمليات الانتقال: التعامل مع التغييرات في نسبة العرض إلى الارتفاع)(https://jakercibald.com/2024/view-transitions-handling-aspect-ratio-changes/).


استخدام الاستعلامات عن الوسائط لتغيير الانتقالات لحالات الجهاز المختلفة

قد ترغب في استخدام انتقالات مختلفة على الهاتف المحمول مقابل سطح المكتب، مثل هذا المثال الذي يؤدي شريحة كاملة من الجانب على الهاتف المحمول، ولكن شريحة أكثر دقة على سطح المكتب:

ينتقل أحد العناصر إلى عنصر آخر. الحد الأدنى للعرض التوضيحي: المصدر.

ويمكن تحقيق ذلك باستخدام استعلامات الوسائط العادية:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

ويمكنك أيضًا تغيير العناصر التي تريد تعيينها view-transition-name وفقًا للاستعلامات عن الوسائط المطابقة.


التفاعل مع الخيار المفضّل "الحركة المنخفضة"

يمكن للمستخدمين الإشارة إلى أنهم يفضلون الحركة المنخفضة من خلال نظام التشغيل، وهذا التفضيل يظهر في CSS.

يمكنك منع أي عمليات نقل لهؤلاء المستخدمين:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

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


التعامل مع أنماط الانتقال المتعددة في طرق العرض باستخدام أنواع الانتقالات في طرق العرض

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

تسجيل العرض التوضيحي لتقسيم النتائج على عدّة صفحات. حيث تستخدم انتقالات مختلفة بناءً على الصفحة التي سيتم الانتقال إليها.

لهذا، يمكنك استخدام أنواع انتقالات العرض، التي تسمح لك بتعيين نوع واحد أو أكثر لانتقال طريقة العرض النشطة. على سبيل المثال، عند الانتقال إلى صفحة أعلى في تسلسل التقسيم على صفحات، استخدِم النوع forwards، وعند الانتقال إلى صفحة أقل، استخدِم النوع backwards. لا تكون هذه الأنواع نشطة إلا عند التقاط صورة انتقال أو تنفيذها، ويمكن تخصيص كل نوع من خلال CSS لاستخدام صور متحركة مختلفة.

لاستخدام الأنواع في عملية انتقال عرض المستند نفسه، عليك تمرير types إلى طريقة startViewTransition. للسماح بذلك، تقبل document.startViewTransition أيضًا كائنًا: update هي دالة رد الاتصال التي تعدِّل DOM، وtypes هي مصفوفة تتضمّن الأنواع.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
.

للرد على هذه الأنواع، استخدِم أداة الاختيار :active-view-transition-type(). مرِّر type التي تريد استهدافها إلى أداة الاختيار. ويتيح لك هذا الحفاظ على أنماط انتقالات طرق العرض المتعددة مفصولة عن بعضها البعض، بدون أن تتداخل إعلانات أحدهما مع تعريفات الآخر.

بما أنّ الأنواع لا تنطبق إلا عند التقاط الانتقال بين عناصر العرض أو تنفيذه، يمكنك استخدام أداة الاختيار لضبط أو إلغاء ضبط view-transition-name على عنصر انتقال العرض من هذا النوع فقط.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

في العرض التوضيحي التالي للتقسيم على صفحات، يتم تمرير محتوى الصفحة للأمام أو الخلف بناءً على رقم الصفحة الذي تنتقل إليه. يتم تحديد الأنواع بناءً على النقرات التي يتم نقلها إلى document.startViewTransition.

لاستهداف أي انتقال للعرض النشط، بغض النظر عن نوعه، يمكنك استخدام أداة اختيار الفئة الزائفة في :active-view-transition بدلاً من ذلك.

html:active-view-transition {
    …
}

التعامل مع أنماط الانتقال المتعددة بين طرق العرض باستخدام اسم فئة على جذر انتقال طريقة العرض

في بعض الأحيان، يجب أن يشتمل الانتقال من نوع معين من طرق العرض إلى آخر على انتقال مخصص تحديدًا. أو، يجب أن يكون التنقل "للخلف" مختلفًا عن التنقل "للأمام".

انتقالات مختلفة عند الرجوع: الحد الأدنى للعرض التوضيحي: المصدر.

قبل أنواع النقل، كانت طريقة التعامل مع هذه الحالات هي تحديد اسم فئة مؤقتًا على جذر الانتقال. عند استدعاء document.startViewTransition، يكون جذر الانتقال هذا هو العنصر <html>، ويمكن الوصول إليه باستخدام document.documentElement في JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

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

يمكنك الآن استخدام اسم الفئة في خدمة CSS لتغيير عملية النقل:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

وكما هو الحال مع طلبات البحث عن الوسائط، يمكن أيضًا استخدام هذه الفئات لتغيير العناصر التي تحصل على view-transition-name.


تشغيل الانتقالات بدون تجميد الرسوم المتحركة الأخرى

ألقِ نظرة على هذا العرض التوضيحي حول موضع انتقال الفيديو:

نقل الفيديو: الحد الأدنى للعرض التوضيحي: المصدر.

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

عملية نقل الفيديو أبطأ: الحد الأدنى للعرض التوضيحي: المصدر.

وخلال الفترة الانتقالية، يبدو أنّ الفيديو يتجمّد، ثم يتلاشى إصدار الفيديو الذي يتم تشغيله. ويرجع ذلك إلى أنّ ::view-transition-old(video) هي لقطة شاشة لطريقة العرض القديمة، بينما ::view-transition-new(video) هي صورة مباشرة لطريقة العرض الجديدة.

يمكنك إصلاح ذلك، ولكن اسأل نفسك أولاً عما إذا كان الأمر يستحق الإصلاح. وإذا لم تظهر لك "المشكلة" عندما كان الانتقال يعمل بالسرعة الطبيعية، لما كنت لن أزعج نفسك بتغييره.

وإذا كنت تريد إصلاحها حقًا، يُرجى عدم عرض ::view-transition-old(video) والتبديل مباشرةً إلى ::view-transition-new(video). يمكنك القيام بذلك عن طريق إلغاء الأنماط والرسوم المتحركة الافتراضية:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

وهكذا انتهى كل شيء!

عملية نقل الفيديو أبطأ: الحد الأدنى للعرض التوضيحي: المصدر.

أما الآن، فيتم تشغيل الفيديو طوال فترة الانتقال.


الرسوم المتحركة باستخدام JavaScript

حتى الآن، تم تحديد جميع عمليات الانتقال باستخدام CSS، لكن في بعض الأحيان لا تكون CSS كافية:

نقل الدائرة: الحد الأدنى للعرض التوضيحي: المصدر.

هناك جزءان من هذا الانتقال لا يمكن تحقيقه باستخدام خدمة مقارنة الأسعار (CSS) وحدها:

  • تبدأ الرسوم المتحركة من موقع النقر.
  • تنتهي الرسوم المتحركة بدائرة لها نصف قطر في أبعد زاوية. ونأمل أن يصبح ذلك ممكنًا مع CSS في المستقبل.

لحسن الحظ، يمكنك إنشاء انتقالات باستخدام Web Animation API!

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

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


الانتقالات كتحسين

تم تصميم واجهة برمجة تطبيقات View Transition API "لتضمين" تغيير في نموذج العناصر في المستند (DOM) وإنشاء عملية انتقال له. ومع ذلك، يجب التعامل مع عملية النقل كتحسين، كما هو الحال مع تطبيقك، أنّه يجب عدم إدخال حالة "خطأ" إذا تم تغيير نموذج العناصر في المستند (DOM) بنجاح، ولكن تعذّر الانتقال. من المفترض ألا تفشل عملية النقل، ولكن إذا حدث ذلك، فيجب ألا تعطّل بقية تجربة المستخدم.

من أجل التعامل مع الانتقالات كتحسين، احرص على عدم استخدام وعود النقل بطريقة قد تؤدي إلى إيقاف تطبيقك في حالة فشل الانتقال.

الإجراءات غير المُوصى بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

المشكلة في هذا المثال هي أنّه سيتم رفض switchView() إذا تعذّر على الانتقال إلى الحالة ready، لكن هذا لا يعني تعذّر تبديل العرض. يُحتمَل أنّه تم تعديل نموذج العناصر في المستند (DOM) بنجاح، ولكن تم رصد عناصر view-transition-name مكرّرة، لذلك تم تخطّي عملية النقل.

يمكنك بدلاً من ذلك إجراء الخطوات التالية:

الإجراءات الموصى بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

يستخدم هذا المثال transition.updateCallbackDone لانتظار تحديث DOM والرفض في حال تعذّر التحديث. لن يتم رفض switchView بعد الآن في حال تعذُّر عملية النقل، ويتم حلها عند اكتمال تحديث DOM، ويتم الرفض في حال تعذّر النقل.

إذا كنت تريد أن تتم معالجة switchView عند اكتمال العرض الجديد، كما هو الحال في أي عملية نقل متحرّكة قد اكتملت أو تم تخطيها إلى النهاية، يمكنك استبدال transition.updateCallbackDone بـ transition.finished.


ليس polyfill، ولكن...

هذه ليست ميزة سهلة للتعويض. ومع ذلك، تُسهِّل هذه الدالة المساعدة الأمور بشكل كبير في المتصفحات التي لا تتيح الانتقالات بين طرق العرض:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

ويمكن استخدامه على النحو التالي:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

في المتصفّحات التي لا تتيح الانتقال بين طرق العرض، سيستمر استدعاء updateDOM، ولكن لن يكون هناك تأثير انتقال متحرك.

ويمكنك أيضًا توفير بعض classNames لإضافتها إلى <html> أثناء عملية الانتقال، ما يُسهِّل تغيير عملية النقل استنادًا إلى نوع التنقّل.

يمكنك أيضًا تمرير true إلى skipTransition إذا كنت لا تريد صورة متحركة، حتى في المتصفّحات التي تتيح الانتقالات بين طرق العرض. ويكون هذا الأمر مفيدًا إذا كان هناك مستخدم مفضّل في موقعك الإلكتروني لإيقاف عمليات الانتقال.


العمل على أطر العمل

إذا كنت تعمل مع مكتبة أو إطار عمل يزيل تغييرات DOM، فإن الجزء المعقّد هو معرفة وقت اكتمال تغيير DOM. إليك مجموعة من الأمثلة باستخدام أداة المساعدة أعلاه في أُطر عمل مختلفة.

  • التفاعل: المفتاح هنا هو flushSync، الذي يطبّق مجموعة من تغييرات الحالة بشكل متزامن. نعم، هناك تحذير كبير بشأن استخدام واجهة برمجة التطبيقات هذه، إلا أنّ دان أبراموف يؤكد لي أنّ ذلك مناسب في هذه الحالة. عند استخدام الوعود المختلفة التي يعرضها startViewTransition، يجب التأكّد من أنّ الرمز يعمل بالحالة الصحيحة، وذلك كالعادة عند استخدام رمز التفاعل والتفاعل.
  • Vue.js: المفتاح هنا هو nextTick، ويتم تنفيذه بعد تعديل DOM.
  • Svelte: يشبه إلى حد كبير Vue، إلا أنّ طريقة انتظار التغيير التالي هي tick.
  • Lit: هو وعد this.updateComplete في المكوّنات، ويتم تنفيذه بعد تعديل DOM.
  • Angular: المفتاح هنا هو applicationRef.tick، والذي يعمل على مسح تغييرات DOM المعلّقة. اعتبارًا من الإصدار 17 من Angular، يمكنك استخدام withViewTransitions الذي يأتي مع @angular/router.

مرجع واجهة برمجة التطبيقات

const viewTransition = document.startViewTransition(update)

بدء ViewTransition جديد

update هي دالة يتم استدعاءها بمجرد الحصول على الحالة الحالية للمستند.

بعد ذلك، عند تنفيذ الوعد الذي تم إرجاعه من خلال "updateCallback"، تبدأ عملية النقل في الإطار التالي. في حال تم رفض الوعد الذي قدَّمه updateCallback، يتم إلغاء عملية النقل.

const viewTransition = document.startViewTransition({ update, types })

بدء ViewTransition جديد بالأنواع المحدّدة

يتم استدعاء update بعد الحصول على الحالة الحالية للمستند.

types يحدّد الأنواع النشطة لعملية النقل عند التقاطها أو تنفيذها. يكون فارغًا في البداية. يُرجى الاطّلاع على viewTransition.types في أسفل الصفحة للاطّلاع على مزيد من المعلومات.

أعضاء مثال ViewTransition:

viewTransition.updateCallbackDone

يتم تنفيذ الوعد عند تنفيذه من قِبل "updateCallback"، أو يتم رفضه عند رفضه.

تغطي واجهة برمجة التطبيقات View Transition API تغييرًا في نموذج العناصر في المستند (DOM) وتنشئ عملية انتقال. ومع ذلك، في بعض الأحيان لا تهتم بنجاح أو إخفاق الرسوم المتحركة للانتقال، فأنت تريد فقط معرفة ما إذا كان تغيير DOM ومتى حدث. إنّ السمة updateCallbackDone مخصّصة لحالة الاستخدام هذه.

viewTransition.ready

وعد يفي بمجرد إنشاء العناصر الزائفة للانتقال، وتكون الرسوم المتحركة على وشك البدء.

ويتم رفضها في حال تعذّر بدء عملية النقل. قد يرجع ذلك إلى خطأ في الإعداد، مثل إنشاء قيم view-transition-name مكرّرة أو عرض updateCallback بوعد مرفوض.

ويكون هذا مفيدًا في تحريك العناصر الصورية الانتقالية باستخدام JavaScript.

viewTransition.finished

وعد يتم الوفاء به بمجرد أن تصبح الحالة النهائية مرئية بالكامل وتفاعلية للمستخدم.

يتم رفض العملية فقط في حال عرض updateCallback بوعد مرفوض، لأنّه يشير إلى عدم إنشاء الحالة النهائية.

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

viewTransition.types

كائن يشبه Set يحتفظ بأنواع انتقالات العرض النشط. لمعالجة الإدخالات، استخدِم طرقها clear() وadd() وdelete().

للاستجابة لنوع معيّن في CSS، استخدِم أداة اختيار الفئة الزائفة :active-view-transition-type(type) في جذر الانتقال.

يتم تنظيف الأنواع تلقائيًا عند انتهاء انتقال طريقة العرض.

viewTransition.skipTransition()

يمكنك تخطي جزء الحركة الانتقالية.

لن يؤدي ذلك إلى تخطّي طلب updateCallback، لأنّ تغيير DOM يكون منفصلاً عن عملية الانتقال.


مرجع النمط التلقائي والانتقال

::view-transition
العنصر الصوري الجذري الذي يملأ إطار العرض ويحتوي على كل ::view-transition-group.
::view-transition-group

في موضع قائم على الاستمرارية:

يتم الانتقال إلى width وheight بين الولايتَين "قبل" و"بعد".

الانتقالات transform بين رباعي مسافة إطار العرض "قبل" و"بعد"

::view-transition-image-pair

هو في وضع قوي لملء المجموعة.

لديه isolation: isolate للحدّ من تأثير mix-blend-mode في طرق العرض القديمة والجديدة.

::view-transition-new و::view-transition-old

يتم وضعها تمامًا في أعلى يسار الغلاف.

يملأ العرض 100% من عرض المجموعة، ولكن له ارتفاع تلقائي، لذلك سيحافظ على نسبة العرض إلى الارتفاع بدلاً من ملء المجموعة.

تحتوي على mix-blend-mode: plus-lighter للسماح بتلاشي متداخل فعلي.

تنتقل طريقة العرض القديمة من opacity: 1 إلى opacity: 0. تنتقل طريقة العرض الجديدة من opacity: 0 إلى opacity: 1.


إضافة ملاحظات

ونحن نُقدِّر دائمًا ملاحظات المطوّرين. لتنفيذ ذلك، يمكنك الإبلاغ عن مشكلة في مجموعة عمل CSS على GitHub تحتوي على اقتراحات وأسئلة. يمكنك بدء معالجة مشكلتك في "[css-view-transitions]".

إذا واجهت خطأً، يمكنك الإبلاغ عن خطأ في Chromium بدلاً من ذلك.