<dialog>
एलिमेंट की मदद से, रंग के हिसाब से अडजस्ट होने वाले, रिस्पॉन्सिव, और ऐक्सेस किए जा सकने वाले मिनी और मेगा मॉडल बनाने का बुनियादी तरीका.
इस पोस्ट में, हम <dialog>
एलिमेंट का इस्तेमाल करके, रंग के हिसाब से अडजस्ट होने वाले, रिस्पॉन्सिव, और सुलभ मिनी और मेगा मॉडल बनाने का तरीका बताना चाहते हैं.
डेमो आज़माएं और सोर्स देखें!
अगर आपको वीडियो देखना पसंद है, तो यहां इस पोस्ट का YouTube वर्शन दिया गया है:
खास जानकारी
<dialog>
एलिमेंट, पेज पर मौजूद संदर्भ के हिसाब से जानकारी या कार्रवाई के लिए बहुत अच्छा है. यह तय करें कि उपयोगकर्ता अनुभव को कई पेजों की कार्रवाई के बजाय, एक ही पेज की कार्रवाई से कब फ़ायदा मिल सकता है: शायद इसलिए, क्योंकि फ़ॉर्म छोटा है या उपयोगकर्ता को सिर्फ़ पुष्टि या रद्द करने की कार्रवाई करनी है.
<dialog>
एलिमेंट हाल ही में सभी ब्राउज़र पर काम करने लगा है:
मुझे पता चला कि एलिमेंट में कुछ चीज़ें मौजूद नहीं हैं. इसलिए, इस जीयूआई चुनौती में मैंने डेवलपर के अनुभव से जुड़े ऐसे आइटम जोड़े हैं जिनकी मुझे उम्मीद है: अतिरिक्त इवेंट, लाइट डिसमिज़, कस्टम ऐनिमेशन, और मिनी और मेगा टाइप.
मार्कअप
<dialog>
एलिमेंट के लिए ज़रूरी शर्तें कम हैं. एलिमेंट अपने-आप छिप जाएगा. साथ ही, इसमें आपके कॉन्टेंट को ओवरले करने के लिए स्टाइल पहले से मौजूद होंगे.
<dialog>
…
</dialog>
हम इस बेसलाइन को बेहतर बना सकते हैं.
आम तौर पर, डायलॉग एलिमेंट और मॉडल के बीच काफ़ी समानता होती है. साथ ही, अक्सर इनके नामों को एक-दूसरे के लिए इस्तेमाल किया जा सकता है. मैंने यहां डायलॉग एलिमेंट का इस्तेमाल, छोटे डायलॉग पॉप-अप (मिनी) और पूरे पेज के डायलॉग (मेगा) दोनों के लिए किया है. मैंने उन्हें मेगा और मिनी नाम दिया है. दोनों डायलॉग को अलग-अलग कामों के हिसाब से थोड़ा बदला गया है.
मैंने modal-mode
एट्रिब्यूट जोड़ा है, ताकि आप टाइप की जानकारी दे सकें:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
आम तौर पर, इंटरैक्शन से जुड़ी कुछ जानकारी इकट्ठा करने के लिए, डायलॉग एलिमेंट का इस्तेमाल किया जाता है. हालांकि, ऐसा हमेशा नहीं होता. डायलॉग एलिमेंट में मौजूद फ़ॉर्म एक साथ काम करते हैं.
अपने डायलॉग कॉन्टेंट को फ़ॉर्म एलिमेंट में रैप करना एक अच्छा आइडिया है, ताकि JavaScript उस डेटा को ऐक्सेस कर सके जिसे उपयोगकर्ता ने डाला है. इसके अलावा, method="dialog"
का इस्तेमाल करने वाले फ़ॉर्म में मौजूद बटन, JavaScript के बिना डायलॉग बॉक्स को बंद कर सकते हैं और डेटा पास कर सकते हैं.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
मेगा डायलॉग
मेगा डायलॉग के फ़ॉर्म में तीन एलिमेंट होते हैं:
<header>
,
<article>
, और
<footer>
.
ये सेमेंटिक कंटेनर के तौर पर काम करते हैं. साथ ही, डायलॉग को दिखाने के लिए स्टाइल टारगेट के तौर पर भी काम करते हैं. हेडर में, मॉडल का टाइटल होता है और बंद करने का बटन होता है. यह लेख, फ़ॉर्म में डाले जाने वाले इनपुट और जानकारी के बारे में है. फ़ुटर में ऐक्शन बटन के <menu>
होते हैं.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
पहले मेन्यू बटन में autofocus
और onclick
इनलाइन इवेंट हैंडलर है. डायलॉग बॉक्स खुलने पर, autofocus
एट्रिब्यूट पर फ़ोकस रहेगा. मेरा सुझाव है कि इसे पुष्टि करने वाले बटन के बजाय, रद्द करने वाले बटन पर डालें. इससे यह पक्का होता है कि पुष्टि गलती से नहीं, बल्कि जान-बूझकर की गई है.
मिनी डायलॉग
मिनी डायलॉग, मेगा डायलॉग से काफ़ी मिलता-जुलता है. इसमें सिर्फ़ <header>
एलिमेंट मौजूद नहीं होता. इससे, इसे छोटा और ज़्यादा इनलाइन बनाया जा सकता है.
<dialog id="MiniDialog" modal-mode="mini">
<form method="dialog">
<article>
<p>Are you sure you want to remove this user?</p>
</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
डायलॉग एलिमेंट, पूरे व्यूपोर्ट एलिमेंट के लिए एक मज़बूत आधार उपलब्ध कराता है. यह एलिमेंट, डेटा और उपयोगकर्ता इंटरैक्शन इकट्ठा कर सकता है. इन ज़रूरी बातों की मदद से, आपकी साइट या ऐप्लिकेशन पर कुछ दिलचस्प और असरदार इंटरैक्शन किए जा सकते हैं.
सुलभता
डायलॉग एलिमेंट में, सुलभता से जुड़ी सुविधाएं पहले से मौजूद होती हैं. आम तौर पर, मैं इन सुविधाओं को जोड़ता हूं. हालांकि, कई सुविधाएं पहले से ही मौजूद हैं.
फ़ोकस को वापस लाना
साइडनेव कॉम्पोनेंट बनाना में हमने मैन्युअल तरीके से ऐसा किया था. यह ज़रूरी है कि किसी चीज़ को खोलने और बंद करने पर, खोलने और बंद करने वाले बटन पर फ़ोकस रहे. साइडनेवि खोलने पर, फ़ोकस 'बंद करें' बटन पर जाता है. 'बंद करें' बटन दबाने पर, फ़ोकस उस बटन पर वापस आ जाता है जिससे उसे खोला गया था.
डायलॉग एलिमेंट के साथ, यह डिफ़ॉल्ट तौर पर पहले से मौजूद है:
माफ़ करें, अगर आपको डायलॉग को ऐनिमेशन के साथ दिखाना है, तो यह सुविधा काम नहीं करेगी. JavaScript सेक्शन में, मैं उस सुविधा को वापस लाऊंगा.
फ़ोकस ट्रैप करना
डायलॉग एलिमेंट, दस्तावेज़ में आपके लिए inert
को मैनेज करता है. inert
से पहले, JavaScript का इस्तेमाल किसी एलिमेंट से फ़ोकस हटने पर उसे रोकने के लिए किया जाता था. इस दौरान, यह एलिमेंट को इंटरसेप्ट करके उसे वापस रख देता था.
inert
के बाद, दस्तावेज़ के किसी भी हिस्से को "फ़्रीज़" किया जा सकता है, ताकि वे अब फ़ोकस टारगेट न हों या माउस के साथ इंटरैक्टिव न हों. फ़ोकस को फ़्रेम में फ़ेच करने के बजाय, दस्तावेज़ के सिर्फ़ इंटरैक्टिव हिस्से पर फ़ोकस किया जाता है.
किसी एलिमेंट को खोलना और उस पर ऑटो फ़ोकस करना
डिफ़ॉल्ट रूप से, डायलॉग एलिमेंट, डायलॉग मार्कअप में फ़ोकस करने लायक पहले एलिमेंट पर फ़ोकस असाइन करेगा. अगर उपयोगकर्ता के लिए यह डिफ़ॉल्ट एलिमेंट सबसे अच्छा नहीं है, तो autofocus
एट्रिब्यूट का इस्तेमाल करें. जैसा कि पहले बताया गया है, हमारा सुझाव है कि इस जानकारी को 'पुष्टि करें' बटन के बजाय, 'रद्द करें' बटन पर डालें. इससे यह पक्का होता है कि पुष्टि गलती से नहीं, बल्कि जान-बूझकर की गई है.
Escape बटन से बंद करना
यह ज़रूरी है कि इस एलिमेंट को आसानी से बंद किया जा सके, ताकि वह आपके काम में रुकावट न डाले. अच्छी बात यह है कि डायलॉग एलिमेंट, आपके लिए escape बटन को मैनेज करेगा. इससे आपको ऑर्केस्ट्रेशन के बोझ से मुक्ति मिलेगी.
स्टाइल
डायलॉग एलिमेंट को स्टाइल करने का एक आसान तरीका और एक मुश्किल तरीका है. डायलॉग की डिसप्ले प्रॉपर्टी में बदलाव किए बिना और उसकी सीमाओं के साथ काम करके, आसान तरीका अपनाया जा सकता है. डायलॉग बॉक्स को खोलने और बंद करने, display
प्रॉपर्टी को मैनेज करने वगैरह के लिए, कस्टम ऐनिमेशन देने के लिए, मैं मुश्किल रास्ते को चुनता हूं.
ओपन प्रॉप की मदद से स्टाइल करना
डिवाइस के हिसाब से रंग बदलने की सुविधा और डिज़ाइन में एकरूपता को बेहतर बनाने के लिए, मैंने अपनी सीएसएस वैरिएबल लाइब्रेरी Open Props को शामिल किया है. मुफ़्त में उपलब्ध वैरिएबल के अलावा, मैंने normalize फ़ाइल और कुछ बटन भी इंपोर्ट किए हैं. Open Props, इन दोनों को वैकल्पिक इंपोर्ट के तौर पर उपलब्ध कराता है. इन इंपोर्ट की मदद से, डायलॉग और डेमो को अपनी पसंद के मुताबिक बनाने पर फ़ोकस किया जा सकता है. साथ ही, इनकी मदद से, डायलॉग और डेमो को बेहतर बनाने के लिए कई स्टाइल इस्तेमाल करने की ज़रूरत नहीं पड़ती.
<dialog>
एलिमेंट की स्टाइल तय करना
डिसप्ले प्रॉपर्टी का मालिकाना हक
डायलॉग एलिमेंट के डिफ़ॉल्ट रूप से दिखने और छिपने की सेटिंग, डिसप्ले प्रॉपर्टी को block
से none
पर टॉगल करती है. माफ़ करें, इसका मतलब है कि इसे अंदर और बाहर ऐनिमेट नहीं किया जा सकता. मुझे इन और आउट, दोनों को ऐनिमेट करना है. इसके लिए, सबसे पहले अपनी display प्रॉपर्टी सेट करनी होगी:
dialog {
display: grid;
}
ऊपर दिए गए सीएसएस स्निपेट में दिखाए गए तरीके से, डिसप्ले प्रॉपर्टी की वैल्यू बदलकर और उस पर मालिकाना हक हासिल करके, उपयोगकर्ता को बेहतर अनुभव देने के लिए, कई स्टाइल मैनेज करने की ज़रूरत होती है. पहला, डायलॉग की डिफ़ॉल्ट स्थिति 'बंद है' होती है. इस स्थिति को विज़ुअल तौर पर दिखाया जा सकता है. साथ ही, इन स्टाइल से डायलॉग बॉक्स के इंटरैक्ट होने से रोका जा सकता है:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
अब यह डायलॉग बॉक्स नहीं दिखता और बंद होने पर उससे इंटरैक्ट नहीं किया जा सकता. बाद में, मैं डॉयलॉग पर inert
एट्रिब्यूट को मैनेज करने के लिए कुछ JavaScript जोड़ूंगा. इससे यह पक्का होगा कि कीबोर्ड और स्क्रीन रीडर का इस्तेमाल करने वाले लोग भी छिपे हुए डॉयलॉग तक न पहुंच पाएं.
डायलॉग बॉक्स को अडैप्टिव कलर थीम देना
color-scheme
, आपके दस्तावेज़ को ब्राउज़र की दी गई, सिस्टम की हल्की और डार्क थीम के हिसाब से अडजस्ट होने वाली कलर थीम में ऑप्ट इन करता है. हालांकि, मुझे डायलॉग एलिमेंट को इससे ज़्यादा पसंद के मुताबिक बनाना था. Open Props में कुछ सर्फ़ेस के रंग उपलब्ध होते हैं. ये रंग, color-scheme
का इस्तेमाल करने की तरह ही, सिस्टम की लाइट और डार्क थीम की सेटिंग के हिसाब से अपने-आप बदल जाते हैं. ये टूल, डिज़ाइन में लेयर बनाने के लिए बहुत अच्छे हैं. मुझे रंगों का इस्तेमाल करके, लेयर के प्लैटफ़ॉर्म को विज़ुअल तौर पर बेहतर बनाने में बहुत मज़ा आता है. बैकग्राउंड का रंग
var(--surface-1)
है. उस लेयर के ऊपर जाने के लिए, var(--surface-2)
का इस्तेमाल करें:
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
हेडर और फ़ुटर जैसे चाइल्ड एलिमेंट के लिए, बाद में ज़्यादा अडैप्टिव कलर जोड़े जाएंगे. मेरे हिसाब से, ये डायलॉग एलिमेंट के लिए ज़रूरी नहीं हैं. हालांकि, ये आकर्षक और बेहतर तरीके से डिज़ाइन किए गए डायलॉग बनाने के लिए ज़रूरी हैं.
रिस्पॉन्सिव डायलॉग का साइज़ तय करना
डायलॉग बॉक्स का साइज़, डिफ़ॉल्ट रूप से उसके कॉन्टेंट के हिसाब से तय होता है. आम तौर पर, यह साइज़ सही होता है. मेरा मकसद यहां max-inline-size
को पढ़ने लायक साइज़ (--size-content-3
= 60ch
) या व्यूपोर्ट की चौड़ाई के 90% तक सीमित करना है. इससे यह पक्का होता है कि डायलॉग बॉक्स, मोबाइल डिवाइस पर किनारों तक न दिखे. साथ ही, डेस्कटॉप स्क्रीन पर इतना चौड़ा न हो कि उसे पढ़ने में परेशानी हो. इसके बाद, मैंने एक
max-block-size
जोड़ा, ताकि डायलॉग बॉक्स की ऊंचाई, पेज की ऊंचाई से ज़्यादा न हो. इसका यह भी मतलब है कि अगर डायलॉग बॉक्स का एलिमेंट लंबा है, तो हमें यह बताना होगा कि डायलॉग बॉक्स का स्क्रोल किया जा सकने वाला हिस्सा कहां है.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
ध्यान दें कि मेरे पास दो बार max-block-size
है? पहले में 80vh
का इस्तेमाल किया जाता है, जो एक फ़िज़िकल व्यूपोर्ट यूनिट है. मेरी असल कोशिश है कि अंतरराष्ट्रीय उपयोगकर्ताओं के लिए, डायलॉग को रिलेटिव फ़्लो में ही रखा जाए. इसलिए, मैं दूसरे एलान में लॉजिकल, नई, और सिर्फ़ कुछ हद तक काम करने वाली dvb
यूनिट का इस्तेमाल करता हूं, ताकि जब यह ज़्यादा स्टेबल हो जाए, तब इसका इस्तेमाल किया जा सके.
मेगा डायलॉग की पोज़िशनिंग
डायलॉग एलिमेंट को सही जगह पर दिखाने के लिए, इसे दो हिस्सों में बांटना ज़रूरी है: फ़ुल स्क्रीन बैकड्रॉप और डायलॉग कंटेनर. बैकग्राउंड में, स्क्रीन पर मौजूद सभी चीज़ें दिखनी चाहिए. साथ ही, बैकग्राउंड के रंग की गहराई बदलने की सुविधा का इस्तेमाल करके, यह दिखाना चाहिए कि डायलॉग बॉक्स स्क्रीन पर सबसे आगे है और उसके पीछे मौजूद कॉन्टेंट को ऐक्सेस नहीं किया जा सकता. डायलॉग कंटेनर, इस बैकग्राउंड के ऊपर अपने-आप सेंटर हो सकता है और अपने कॉन्टेंट के हिसाब से कोई भी आकार ले सकता है.
यहां दी गई स्टाइल, डायलॉग एलिमेंट को विंडो में फ़िक्स करती हैं और उसे हर कोने तक स्ट्रेच करती हैं. साथ ही, कॉन्टेंट को बीच में रखने के लिए margin: auto
का इस्तेमाल करती हैं:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
मोबाइल के लिए मेगा डायलॉग के स्टाइल
छोटे व्यूपोर्ट पर, मैं पूरे पेज वाले इस मेगा मॉडल को थोड़ा अलग तरीके से स्टाइल करता/करती हूं. मैंने सबसे नीचे मौजूद मार्जिन को 0
पर सेट किया है. इससे डायलॉग कॉन्टेंट, व्यूपोर्ट के सबसे नीचे दिखता है. स्टाइल में कुछ बदलाव करके, डायलॉग बॉक्स को ऐक्शनशीट में बदला जा सकता है, ताकि उपयोगकर्ता उसे आसानी से ऐक्सेस कर सके:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
मिनी डायलॉग की पोज़िशनिंग
डेस्कटॉप कंप्यूटर जैसे बड़े व्यूपोर्ट का इस्तेमाल करते समय, मैंने मिनी डायलॉग को उस एलिमेंट के ऊपर रखना चुना है जिसने उन्हें कॉल किया है. इसके लिए, मुझे JavaScript की ज़रूरत है. मैं जिस तकनीक का इस्तेमाल करता हूं वह यहां देखी जा सकती है. हालांकि, मुझे लगता है कि इस लेख में इस बारे में नहीं बताया जा सकता. JavaScript के बिना, स्क्रीन के बीच में, मेगा डायलॉग की तरह ही एक छोटा डायलॉग दिखता है.
इमेज को शानदार बनाएं
आखिर में, डायलॉग में कुछ फ़्लेयर जोड़ें, ताकि यह पेज के ऊपर मौजूद किसी सॉफ़्ट प्लैटफ़ॉर्म की तरह दिखे. डायलॉग के कोनों को गोल करके, उसे सॉफ़्ट लुक दिया जाता है. इस इमेज में गहरे रंग का इस्तेमाल करके, ओपन प्रॉप्स के शेडोज प्रॉप का इस्तेमाल किया गया है:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
बैकड्रॉप के स्यूडो एलिमेंट को पसंद के मुताबिक बनाना
मैंने बैकग्राउंड में बहुत कम बदलाव किया है. मैंने सिर्फ़ backdrop-filter
का इस्तेमाल करके, मेगा डायलॉग में धुंधलापन का इफ़ेक्ट जोड़ा है:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
मैंने backdrop-filter
पर भी ट्रांज़िशन जोड़ा है, ताकि आने वाले समय में ब्राउज़र, बैकग्राउंड एलिमेंट को ट्रांज़िशन कर सकें:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
स्टाइल से जुड़ी अन्य सुविधाएं
मैंने इस सेक्शन को "अतिरिक्त" कहा है, क्योंकि यह डायलॉग एलिमेंट के सामान्य वर्शन के बजाय, मेरे डायलॉग एलिमेंट के डेमो से ज़्यादा जुड़ा है.
स्क्रोल कंटेनमेंट
डायलॉग दिखने के बाद भी, उपयोगकर्ता उस पेज को स्क्रोल कर सकता है जिस पर डायलॉग दिख रहा है. मुझे ऐसा नहीं चाहिए:
आम तौर पर, overscroll-behavior
का इस्तेमाल करके इस समस्या को हल किया जा सकता है. हालांकि, स्पेसिफ़िकेशन के मुताबिक, इसका डायलॉग पर कोई असर नहीं पड़ता, क्योंकि यह स्क्रोल पोर्ट नहीं है. इसका मतलब है कि यह स्क्रोलर नहीं है, इसलिए इसे रोकने के लिए कुछ नहीं किया जा सकता. इस गाइड में दिए गए नए इवेंट, जैसे कि "बंद" और "खुला" देखने के लिए, JavaScript का इस्तेमाल किया जा सकता है. साथ ही, दस्तावेज़ पर overflow: hidden
को टॉगल किया जा सकता है. इसके अलावा, सभी ब्राउज़र में :has()
के सही तरीके से काम करने का इंतज़ार किया जा सकता है:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
अब जब कोई मेगा डायलॉग खुला होता है, तो एचटीएमएल दस्तावेज़ में overflow: hidden
होता है.
<form>
लेआउट
यह यूज़र से इंटरैक्शन की जानकारी इकट्ठा करने के लिए एक बहुत ज़रूरी एलिमेंट है. साथ ही, इसका इस्तेमाल हेडर, फ़ुटर, और लेख के एलिमेंट को व्यवस्थित करने के लिए भी किया जाता है. इस लेआउट की मदद से, मैं लेख के चाइल्ड एलिमेंट को स्क्रोल किए जा सकने वाले एरिया के तौर पर दिखाना चाहता/चाहती हूं. मैंने ऐसा करने के लिए,
grid-template-rows
का इस्तेमाल किया.
लेख एलिमेंट को 1fr
दिया गया है और फ़ॉर्म की ज़्यादा से ज़्यादा ऊंचाई, डायलॉग एलिमेंट की ऊंचाई के बराबर है. फ़र्म ऊंचाई और फ़र्म पंक्ति के साइज़ को सेट करने से, लेख के एलिमेंट को सीमित किया जा सकता है और ओवरफ़्लो होने पर उसे स्क्रोल किया जा सकता है:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
डायलॉग <header>
का स्टाइल तय करना
इस एलिमेंट की भूमिका, डायलॉग कॉन्टेंट के लिए टाइटल देना और आसानी से ढूंढा जा सकने वाला 'बंद करें' बटन देना है. इसे एक रंग भी दिया जाता है, ताकि यह डायलॉग आर्टिकल के कॉन्टेंट के पीछे दिखे. इन ज़रूरी शर्तों के आधार पर, एक फ़्लेक्सबॉक्स कंटेनर बनाया जाता है. इसमें वर्टिकल तौर पर अलाइन किए गए आइटम होते हैं, जिनके किनारों के बीच स्पेस होता है. साथ ही, टाइटल और 'बंद करें' बटन के लिए कुछ पैडिंग और गैप भी होते हैं:
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
हेडर के 'बंद करें' बटन को स्टाइल करना
डेमो में प्रॉप खोलने वाले बटन का इस्तेमाल किया जा रहा है. इसलिए, बंद करने वाले बटन को पसंद के मुताबिक, गोल आइकॉन वाले बटन में बदला गया है. जैसे:
dialog > form > header > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
डायलॉग <article>
का स्टाइल तय करना
इस डायलॉग में, लेख एलिमेंट की खास भूमिका होती है: यह एक ऐसा स्पेस होता है जिसे लंबे या बड़े डायलॉग के मामले में स्क्रोल किया जा सकता है.
ऐसा करने के लिए, पैरंट फ़ॉर्म एलिमेंट ने अपने लिए कुछ तय सीमाएं तय की हैं. इन सीमाओं की वजह से, अगर यह लेख एलिमेंट बहुत लंबा हो जाता है, तो उसे सीमाओं तक पहुंचने से रोका जा सकता है. overflow-y: auto
सेट करें, ताकि स्क्रोलबार सिर्फ़ ज़रूरत पड़ने पर दिखें. साथ ही, overscroll-behavior: contain
की मदद से स्क्रोल किया जा सके. बाकी सभी स्टाइल, प्रज़ेंटेशन के हिसाब से तय किए जा सकते हैं:
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
डायलॉग <footer>
का स्टाइल तय करना
फ़ुटर में ऐक्शन बटन के मेन्यू होते हैं. फ़्लेक्सबॉक्स का इस्तेमाल, कॉन्टेंट को फ़ुटर इनलाइन ऐक्सिस के आखिर में अलाइन करने के लिए किया जाता है. इसके बाद, बटन को कुछ जगह देने के लिए कुछ स्पेस दिया जाता है.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
डायलॉग फ़ुटर मेन्यू को स्टाइल करना
menu
एलिमेंट का इस्तेमाल, डायलॉग के लिए ऐक्शन बटन शामिल करने के लिए किया जाता है. यह बटनों के बीच स्पेस देने के लिए, gap
के साथ रैपिंग
फ़्लेक्सबॉक्स लेआउट का इस्तेमाल करता है. मेन्यू एलिमेंट में पैडिंग होती है, जैसे कि <ul>
. मैं उस स्टाइल को भी हटा देता हूं, क्योंकि मुझे इसकी ज़रूरत नहीं है.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
ऐनिमेशन
डायलॉग एलिमेंट अक्सर ऐनिमेशन के साथ दिखते हैं, क्योंकि वे विंडो में आते और उससे बाहर निकलते हैं. डायलॉग में, इनपुट और आउटपुट के लिए कुछ सहायक मोशन देने से, उपयोगकर्ताओं को फ़्लो में अपने ऑरिएंटेशन को समझने में मदद मिलती है.
आम तौर पर, डायलॉग एलिमेंट को सिर्फ़ अंदर आने के लिए ऐनिमेशन दिया जा सकता है, बाहर जाने के लिए नहीं. ऐसा इसलिए होता है, क्योंकि ब्राउज़र, एलिमेंट पर display
प्रॉपर्टी को टॉगल करता है. पहले, गाइड ने डिसप्ले को ग्रिड पर सेट किया था और इसे कभी भी 'कोई नहीं' पर सेट नहीं किया था. इससे, आइटम को अंदर और बाहर ऐनिमेट करने की सुविधा मिलती है.
Open Props में इस्तेमाल के लिए कई कीफ़्रेम ऐनिमेशन मौजूद होते हैं. इनकी मदद से, ऑर्केस्ट्रेशन को आसानी से समझा जा सकता है. यहां ऐनिमेशन के लक्ष्य और लेयर वाले मेरे तरीके के बारे में बताया गया है:
- 'हलचल कम करें' ट्रांज़िशन डिफ़ॉल्ट रूप से लागू होता है. इसमें, ओपैसिटी धीरे-धीरे कम और ज़्यादा होती है.
- अगर मोशन ठीक है, तो स्लाइड और स्केल ऐनिमेशन जोड़े जाते हैं.
- मेगा डायलॉग के लिए, रिस्पॉन्सिव मोबाइल लेआउट को स्लाइड आउट करने के लिए अडजस्ट किया गया है.
सुरक्षित और काम का डिफ़ॉल्ट ट्रांज़िशन
ओपन प्रॉप में, इन और आउट फ़ेड करने के लिए कीफ़्रेम होते हैं. हालांकि, मुझे ट्रांज़िशन के लिए लेयर वाले इस तरीके को डिफ़ॉल्ट तौर पर इस्तेमाल करना पसंद है. साथ ही, कीफ़्रेम ऐनिमेशन को संभावित अपग्रेड के तौर पर इस्तेमाल करना पसंद है. पहले हमने डायलॉग की पारदर्शिता के साथ स्टाइल को पहले से ही सेट कर दिया था. इसके लिए, [open]
एट्रिब्यूट के आधार पर 1
या 0
को ऑर्केस्ट्रेट किया गया था. 0% से 100% के बीच ट्रांज़िशन करने के लिए, ब्राउज़र को बताएं कि आपको कितनी देर और किस तरह का ट्रांज़िशन चाहिए:
dialog {
transition: opacity .5s var(--ease-3);
}
ट्रांज़िशन में मोशन जोड़ना
अगर उपयोगकर्ता मोशन के साथ सहमत है, तो मेगा और मिनी डायलॉग, स्लाइड अप के तौर पर दिखने चाहिए और स्लाइड आउट के तौर पर दिखने चाहिए. ऐसा करने के लिए, prefers-reduced-motion
मीडिया क्वेरी और कुछ ओपन प्रॉप का इस्तेमाल करें:
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
मोबाइल के लिए, आखिरी के समय इस्तेमाल होने वाले ऐनिमेशन को अडैप्ट करना
स्टाइल सेक्शन में पहले, मोबाइल डिवाइसों के लिए मेगा डायलॉग स्टाइल को ऐक्शन शीट की तरह बनाया गया था. जैसे, स्क्रीन के सबसे नीचे से एक छोटा कागज़ ऊपर की ओर स्लाइड करके, अब भी सबसे नीचे लगा हुआ है. स्केल आउट वाला एक्सिट ऐनिमेशन, इस नए डिज़ाइन के साथ ठीक से फ़िट नहीं होता. हम इसे कुछ मीडिया क्वेरी और कुछ ओपन प्रॉप के साथ अडैप्ट कर सकते हैं:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
JavaScript के साथ कई चीज़ें जोड़ी जा सकती हैं:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
ये बदलाव, डायलॉग बैकग्राउंड पर क्लिक करके डायलॉग बॉक्स को आसानी से बंद करने, ऐनिमेशन, और फ़ॉर्म का डेटा पाने के लिए बेहतर समय तय करने के लिए कुछ अन्य इवेंट की वजह से किए गए हैं.
लाइट बंद करने की सुविधा जोड़ना
यह काम आसान है. साथ ही, यह ऐसे डायलॉग एलिमेंट के लिए एक बेहतरीन विकल्प है जिसे ऐनिमेट नहीं किया जा रहा है. इंटरैक्शन, डायलॉग एलिमेंट पर क्लिक को देखकर हासिल किया जाता है. साथ ही, इवेंट बबल का इस्तेमाल करके यह पता लगाया जाता है कि किस पर क्लिक किया गया. यह इंटरैक्शन सिर्फ़ तब close()
होगा, जब यह सबसे ऊपर वाला एलिमेंट हो:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
सूचना dialog.close('dismiss')
. इवेंट को कॉल किया जाता है और एक स्ट्रिंग दी जाती है.
इस स्ट्रिंग को अन्य JavaScript से वापस पाया जा सकता है, ताकि यह जानकारी मिल सके कि डायलॉग बॉक्स को कैसे बंद किया गया. आपको पता चलेगा कि मैंने हर बार अलग-अलग बटन से फ़ंक्शन को कॉल करते समय, क्लोज़ स्ट्रिंग भी दी है. ऐसा इसलिए किया गया है, ताकि उपयोगकर्ता के इंटरैक्शन के बारे में अपने ऐप्लिकेशन को संदर्भ दिया जा सके.
बंद होने वाले और बंद हो चुके इवेंट जोड़ना
डायलॉग एलिमेंट में एक 'बंद करें' इवेंट होता है: यह डायलॉग close()
फ़ंक्शन के कॉल होने पर तुरंत इमिट होता है. हम इस एलिमेंट को ऐनिमेट कर रहे हैं. इसलिए, डेटा हासिल करने या डायलॉग फ़ॉर्म को रीसेट करने के लिए, ऐनिमेशन से पहले और बाद के इवेंट होना अच्छा होता है. यहां इसका इस्तेमाल, बंद डायलॉग पर inert
एट्रिब्यूट जोड़ने के लिए किया गया है. साथ ही, डेमो में इसका इस्तेमाल, अवतार सूची में बदलाव करने के लिए किया गया है. ऐसा तब किया जाता है, जब उपयोगकर्ता ने कोई नई इमेज सबमिट की हो.
ऐसा करने के लिए, closing
और closed
नाम के दो नए इवेंट बनाएं. इसके बाद, डायलॉग बॉक्स में पहले से मौजूद 'बंद करें' इवेंट के लिए सुनें. यहां से, डायलॉग को inert
पर सेट करें और closing
इवेंट भेजें. अगला टास्क, डायलॉग पर ऐनिमेशन और ट्रांज़िशन के खत्म होने का इंतज़ार करना है. इसके बाद, closed
इवेंट को डिस्पैच करें.
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
export default async function (dialog) {
…
dialog.addEventListener('close', dialogClose)
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
animationsComplete
फ़ंक्शन, टोस्ट कॉम्पोनेंट बनाने में भी इस्तेमाल किया जाता है. यह एनिमेशन और ट्रांज़िशन के पूरा होने के आधार पर एक प्रॉमिस दिखाता है. इसलिए, dialogClose
एक असाइन किए गए फ़ंक्शन है. इसके बाद, यह await
के साथ मिले प्रॉमिस का इस्तेमाल करके, क्लोज़्ड इवेंट पर भरोसे के साथ आगे बढ़ सकता है.
नए और बदलाव करने के लिए खोले गए इवेंट जोड़ना
इन इवेंट को जोड़ना आसान नहीं है, क्योंकि पहले से मौजूद डायलॉग एलिमेंट, 'बंद करें' के साथ खुलने वाला इवेंट नहीं देता. डायलॉग के एट्रिब्यूट में होने वाले बदलावों की अहम जानकारी देने के लिए, मैं MutationObserver का इस्तेमाल करता/करती हूं. इस ऑब्ज़र्वर में, मैं open एट्रिब्यूट में होने वाले बदलावों को देखूंगा और उसके हिसाब से कस्टम इवेंट मैनेज करूंगा.
हमने जिस तरह से बंद होने वाले और बंद हो चुके इवेंट शुरू किए थे उसी तरह, opening
और opened
नाम के दो नए इवेंट बनाएं. जहां हमने पहले डायलॉग बंद होने के इवेंट को सुना था, इस बार डायलॉग के एट्रिब्यूट देखने के लिए, बनाए गए म्यूटेशन ऑब्ज़र्वर का इस्तेमाल करें.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
डायलॉग एट्रिब्यूट में बदलाव होने पर, म्यूटेशन ऑब्ज़र्वर कॉलबैक फ़ंक्शन को कॉल किया जाएगा. इसमें बदलावों की सूची, कलेक्शन के तौर पर दी जाएगी. एट्रिब्यूट में हुए बदलावों पर बार-बार जाएं और देखें कि attributeName
खुला है या नहीं. इसके बाद, देखें कि एलिमेंट में एट्रिब्यूट है या नहीं: इससे यह पता चलता है कि डायलॉग खुला है या नहीं. अगर यह खुला है, तो inert
एट्रिब्यूट को हटाएं. इसके बाद, फ़ोकस को autofocus
का अनुरोध करने वाले किसी एलिमेंट या डायलॉग में मिले पहले button
एलिमेंट पर सेट करें. आखिर में, बंद होने वाले और बंद इवेंट की तरह ही, ओपनिंग इवेंट को तुरंत डिस्पैच करें. इसके बाद, ऐनिमेशन के खत्म होने का इंतज़ार करें और फिर ओपन इवेंट को डिस्पैच करें.
हटाए गए इवेंट को जोड़ना
एक पेज वाले ऐप्लिकेशन में, डायलॉग अक्सर रूट या ऐप्लिकेशन की अन्य ज़रूरतों और स्थिति के आधार पर जोड़े और हटाए जाते हैं. किसी डायलॉग को हटाने पर, इवेंट या डेटा को हटाने के लिए यह सुविधा काम की हो सकती है.
ऐसा करने के लिए, किसी दूसरे म्यूटेशन ऑब्ज़र्वर का इस्तेमाल करें. इस बार, डायलॉग एलिमेंट पर एट्रिब्यूट देखने के बजाय, हम बॉडी एलिमेंट के चाइल्ड एलिमेंट को देखेंगे. साथ ही, हटाए जा रहे डायलॉग एलिमेंट पर भी नज़र रखेंगे.
…
const dialogRemovedEvent = new Event('removed')
export default async function (dialog) {
…
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
}
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
जब भी दस्तावेज़ के मुख्य हिस्से में बच्चे जोड़े जाते हैं या हटाए जाते हैं, तब म्यूटेशन ऑब्ज़र्वर कॉलबैक को कॉल किया जाता है. जिन म्यूटेशन पर नज़र रखी जा रही है वे उन removedNodes
के लिए हैं जिनमें डायलॉग का nodeName
है. अगर कोई डायलॉग हटाया गया है, तो मेमोरी खाली करने के लिए क्लिक और बंद करें इवेंट हटा दिए जाते हैं. साथ ही, हटाया गया कस्टम इवेंट डिस्पैच किया जाता है.
लोडिंग एट्रिब्यूट को हटाना
पेज पर जोड़े जाने या पेज लोड होने पर, डायलॉग ऐनिमेशन के बाहर निकलने का ऐनिमेशन न चलने के लिए, डायलॉग में लोडिंग एट्रिब्यूट जोड़ा गया है. यहां दी गई स्क्रिप्ट, डायलॉग बॉक्स के ऐनिमेशन के खत्म होने का इंतज़ार करती है. इसके बाद, एट्रिब्यूट को हटा देती है. अब डायलॉग को ऐनिमेशन के साथ दिखाया जा सकता है. साथ ही, हमने ध्यान भटकाने वाले ऐनिमेशन को छिपा दिया है.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
पेज लोड होने पर कीफ़्रेम ऐनिमेशन को रोकने से जुड़ी समस्या के बारे में ज़्यादा जानें.
सभी एक साथ
हमने हर सेक्शन के बारे में अलग-अलग बताया है. अब यहां dialog.js
का पूरा एनोटेशन दिया गया है:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
dialog.js
मॉड्यूल का इस्तेमाल करना
मॉड्यूल से एक्सपोर्ट किए गए फ़ंक्शन को कॉल किया जाना चाहिए और उसमें एक डायलॉग एलिमेंट पास किया जाना चाहिए जिसमें ये नए इवेंट और फ़ंक्शन जोड़ने हैं:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
इसी तरह, दोनों डायलॉग को हल्के से खारिज करने की सुविधा, ऐनिमेशन लोड करने से जुड़ी गड़बड़ियों को ठीक करने, और साथ में काम करने के लिए ज़्यादा इवेंट के साथ अपग्रेड किया गया है.
नए कस्टम इवेंट सुनना
अपग्रेड किया गया हर डायलॉग एलिमेंट, अब पांच नए इवेंट सुन सकता है. जैसे:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
यहां उन इवेंट को मैनेज करने के दो उदाहरण दिए गए हैं:
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
डायलॉग एलिमेंट की मदद से बनाए गए डेमो में, मैंने सूची में नया अवतार एलिमेंट जोड़ने के लिए, उस बंद इवेंट और फ़ॉर्म डेटा का इस्तेमाल किया है. समयावधि अच्छी है, क्योंकि डायलॉग के बाहर निकलने का ऐनिमेशन पूरा हो गया है और फिर कुछ स्क्रिप्ट नए अवतार में ऐनिमेट हो गई हैं. नए इवेंट की मदद से, उपयोगकर्ता अनुभव को बेहतर बनाया जा सकता है.
सूचना dialog.returnValue
: इसमें डायलॉग close()
इवेंट को कॉल करने पर पास की गई क्लोज़ स्ट्रिंग शामिल होती है. dialogClosed
इवेंट में यह जानना ज़रूरी है कि डायलॉग बॉक्स को बंद किया गया था, रद्द किया गया था या उसकी पुष्टि की गई थी. पुष्टि होने के बाद, स्क्रिप्ट फ़ॉर्म की वैल्यू हासिल करती है और फ़ॉर्म को रीसेट करती है. रीसेट करने की सुविधा इसलिए काम की है, ताकि जब डायलॉग बॉक्स फिर से दिखे, तो वह खाली हो और नए सबमिशन के लिए तैयार हो.
नतीजा
अब आपको पता है कि मैंने यह कैसे किया, तो आप कैसे करेंगे‽ 🙂
आइए, अलग-अलग तरीकों का इस्तेमाल करके, वेब पर कॉन्टेंट बनाने के सभी तरीके जानें.
डेमो बनाएं और मुझे ट्वीट करें लिंक भेजें. हम इसे कम्यूनिटी रीमिक्स सेक्शन में जोड़ देंगे!
कम्यूनिटी रीमिक्स
- तीन-में-एक डायलॉग बॉक्स के साथ @GrimLink.
- @mikemai2awesome ने एक अच्छा रीमिक्स बनाया है, जो
display
प्रॉपर्टी में बदलाव नहीं करता. - @geoffrich_ ने Svelte और Svelte FLIP को बेहतर बनाया है.
संसाधन
- GitHub पर सोर्स कोड
- डूडल अवतार