מבוא
'גרירה ושחרור' (DnD) היא אחת מהתכונות הרבות והמצוינות של HTML 5, והיא נתמכת ב-Firefox 3.5, ב-Safari, ב-Chrome וב-IE. Google השיקה לאחרונה תכונה חדשה שמאפשרת למשתמשים ב-Google Chrome לגרור ולשחרר קבצים מהדפדפן לשולחן העבודה. זו תכונה נוחה מאוד, אבל היא לא הייתה ידועה לכולם עד שרייאן סדון פרסם מאמר על התגליות שלו מהנדסת ההפך של התכונה החדשה הזו.
אנחנו ב-Box.net שמחים מאוד שהיכולות החדשות האלה מאפשרות לנו לשפר את הפתרון שלנו לניהול תוכן בענן, וגם לתרום יותר לקהילת המפתחים. יש לי חדשות טובות: התכונה DnD Download שולבה במוצר שלנו. עכשיו משתמשי Box יכולים לגרור קבצים ישירות מדפדפן Chrome למחשב כדי להוריד ולשמור את הקובץ.
אני רוצה לשתף איך עברתי כמה גרסאות במהלך הפיתוח של התכונה החדשה הזו.
בדיקת התמיכה ב-API ל'גרור ושחרור'
קודם כול, צריך לבדוק שהדפדפן תומך באופן מלא ב-HTML5 של גרירה ושחרור. דרך קלה לעשות זאת היא להשתמש בספרייה בשם Modernizr כדי לבדוק אם יש תמיכה בתכונה מסוימת:
if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}
איטרציה 1
קודם ניסיתי את הגישה ש-Seddon מצא ב-Gmail. הוספתי מאפיין חדש בשם 'data-downloadurl' לקישורים לאישורי קישור של קבצים. התהליך הזה מתבסס על מאפייני הנתונים המותאמים אישית של HTML5. ב-data-downloadurl, צריך לכלול את סוג ה-MIME של הקובץ, את שם קובץ היעד (שם הקובץ הרצוי של הקובץ שהורדתם) ואת כתובת ה-URL להורדת הקובץ. לכן, הקוד הזה מתווסף לתבנית ה-HTML:
<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>
הפלט שייווצר יהיה דומה לזה:
<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>
על סמך plugin של jQuery שיצר von Schorsch, שמבוסס על המאמר של Seddon, הוספתי יישומון jQuery שמבצע זיהוי חלקי של תכונות הדפדפן. השורות שנוספו לגרסה של von Schorsch מודגשות:
(function($) {
$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
$(files).each(function() {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
if (this.addEventListener) {
this.addEventListener("dragstart", function(e) {
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
e.dataTransfer.setData("DownloadURL", url);
}
},false);
}
});
}
}
});
})(jQuery);
הסיבה לכך היא שבלי זיהוי מראש של הדפדפן, הרצת addEventListener() על רכיב HTML ב-IE תיצור שגיאת JavaScript כי ב-IE נעשה שימוש בשיטה משלו, attachEvent(). e.dataTransfer לא מוגדר ב-IE (נכון לעכשיו), e.dataTransfer.constructor מחזיר את DataTransfer ב-Firefox (Mozilla), ואילו בדפדפני Webkit (Chrome ו-Safari) מוטמע ה-constructor של Clipboard.
ב-Safari, הפונקציה e.dataTransfer.setData('DownloadURL','http://www.box.net')
מחזירה את הערך false, וב-Chrome היא מחזירה את הערך true. אם מבצעים את כל הבדיקות שצוינו למעלה, התכונה תהיה זמינה רק ל-Chrome.
יכול להיות שתטענו שאוכל פשוט לבצע את הפעולות הבאות:
/chrome/.test( navigator.userAgent.toLowerCase() )
אבל אני מעדיף זיהוי תכונות על פני זיהוי דפדפן, למרות שבאופן טכני הוא לא מזהה שההורדה של DnD תפעל.
בעיות של חזרה 1
1) מכיוון שאנחנו מפעילים כרגע DnD בדף להעברה או להעתקה של קבצים בין תיקיות, אנחנו צריכים דרך להבדיל בין DnD להורדה לבין DnD בדף. מבחינה טכנית, אי אפשר לשלב את שתי הפעולות האלה. אנחנו לא יכולים לחזות אם המשתמש רוצה להעביר קובץ לתיקייה אחרת בחשבון Box.net או לגרור אותו למחשב. שתי הפעולות האלה שונות לחלוטין.
בנוסף, אין דרך קלה לזהות אם הסמן נמצא מחוץ לחלון הדפדפן.
אפשר להשתמש ב-window.onmouseout (IE) וב-document.onmouseout (דפדפנים אחרים) כדי לצרף את האירוע mouseout למסמך, ולבדוק אם e.relatedTarget.nodeName == "HTML"
(e הוא האירוע mouseout או window.event, לפי מה שזמין). אבל זה קשה למדי בגלל העלייה של האירועים.
האירוע עשוי להופעל באופן אקראי כשאתם מעל תמונה או שכבה, במיוחד באפליקציית אינטרנט מורכבת כמו Box.net.
2) אנחנו רוצים שהמשתמש יבצע פעולה מפורשת כדי למנוע ממנו לגרור משהו לשולחן העבודה בטעות. עורכים של תיקיות ב-Box יכולים להעלות קובץ הפעלה שגורם למשהו לא רצוי במחשב של מי שמוריד אותו. אנחנו רוצים שהמשתמשים ידעו בדיוק מתי הקובץ יורד למחשב.
איטרציה 2
החלטנו להתנסות בשימוש בשילוב המקשים Ctrl + גרירה (גרירה של קובץ בזמן שלוחצים על מקש Ctrl ב-Windows). הפעולה הזו תואמת למה שאנשים יכולים לעשות במחשב עם Windows כדי ליצור עותק כפול של קובץ. בנוסף, המשתמשים צריכים לבצע עבודה נוספת (אבל לא שלב נוסף) כדי למנוע הורדה של קבצים בטעות.
הפלאגין של jQuery בגרסה 1 הוצא משימוש כי אנחנו צריכים לשלב בצורה הדוקה את הורדת DnD עם DnD בדף. לידיעתכם, אנחנו משתמשים בגרסה שונה של הפלאגין Draggable של jQuery UI. בתוך האירוע mousedown של רכיב היעד, אנחנו מציבים את הקוד הבא:
// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart",function(e) {
// e.dataTransfer in Firefox uses the DataTransfer constructor
// instead of Clipboard
// make sure it's Chrome and not Safari (both webkit-based).
// setData on DownloadURL returns true on Chrome, and false on Safari
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
e.dataTransfer.setData("DownloadURL", url);
}
}, false);
return;
}
}
בנוסף להפעלת מקש Ctrl, הוספנו גם תיבת טיפים קטנה שמופיעה כשהמשתמש מבצע גרירה רגילה בדף. ההודעה הזו מאפשרת למשתמש לדעת שאפשר להוריד קבצים על ידי גרירה של סמל הקובץ לשולחן העבודה תוך כדי לחיצה על מקש Ctrl.
בעיות של חזרה 2
מטעמי אבטחה, Box.net לא חושף כתובות URL קבועות כדי לגשת ישירות לקבצים סטטיים. זה לא מצב ספציפי ל-Box.net. כל שירות אחסון אונליין לא אמור לחשוף כתובות URL קבועות בלי שכבת אבטחה נוספת כדי לבדוק אם הקובץ הוא ציבורי ואם ההורדה המיועדת מבקשת על ידי משתמש עם ההרשאות המתאימות.
כשמעקבים אחרי 'כתובת ה-URL להורדה' (למשל https://www.box.net/box_download_file?file_id=f_60466690
) של פריט, המערכת מחזירה את קוד הסטטוס 302 Found ומפנה לכתובת URL אקראית (למשל https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b
) שהיא 'כתובת ה-URL בפועל' הזמנית של הקובץ. האתגר הוא שהתוקף שלו פג כל כמה דקות, ולכן לא מעשי להציב אותו בפלט ה-HTML. יכול להיות שהיא תחזיר את הערך '404' כשהמשתמש ינסה להוריד את הקובץ מהקישור בפלט ה-HTML שנוצר לפני כמה דקות.
ההורדה של DnD פועלת רק בכתובות URL אמיתיות שמפניות ישירות למשאב. אם יש הפניה אוטומטית, המערכת לא מספיק חכמה כרגע כדי לעקוב אחרי השרשרת (ואף פעם לא צריך לעקוב אחרי השרשרת מסיבות אבטחה). לכן, למרות שהקישור https://www.box.net/box_download_file?file_id=f_60466690 שלמעלה יאפשר לכם להוריד את הקובץ כשמזינים אותו בסרגל המיקום בדפדפן, הוא לא יפעל עם DnD.
כדי להמחיש טוב יותר את ההבדלים בין 'כתובת URL בפועל' לבין 'כתובת URL להפניה אוטומטית', אפשר לעיין בצילומי המסך הבאים:
איטרציה 3
ננסה את Ajax.
שינינו מעט את הקוד מהחזרה הקודמת ויצאנו עם הקוד הבא:
// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
// e.dataTransfer in Firefox uses the DataTransfer constructor
// instead of Clipboard
// make sure it's Chrome and not Safari (both webkit-based).
// setData on DownloadURL returns true on Chrome, and false on Safari
if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
var url = (this.dataset && this.dataset.downloadurl) ||
this.getAttribute("data-downloadurl");
$.ajax({
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type:'GET',
url: url
});
}
}, false);
return;
}
}
זה הגיוני. כשמתחילים לגרור, מתבצעת מיד קריאה של Ajax לשרת כדי לאחזר את כתובת ה-URL העדכנית ביותר להורדת הקובץ. עם זאת, הוא לא פועל.
מסתבר שצריכה להיות זו קריאה סינכרונית (או, כפי שאני אוהב לכנות אותה, Sjax). נראה שצריך לבצע את setData בזמן שמצרפים את מאזין האירועים. בהתאם ל-API של jQuery, השורות המודגשות הופכות ל:
$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});
והוא עובד מצוין עד שמנתקים את החיבור לרשת. מכיוון שהיא מבצעת קריאה סינכרונית, הדפדפן קופא עד שהקריאה מסתיימת בהצלחה. אם קריאת ה-Ajax תיכשל (קוד שגיאה 404 או אם היא לא תגיב בכלל), הדפדפן לא יתעורר בכלל, כאילו הוא קרס.
מומלץ לעשות משהו כזה:
$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});
כדי לראות הדגמה של התכונה הזו, אפשר להעלות קובץ סטטי לחשבון Box.net. גוררים את סמל הקובץ למסך העבודה תוך כדי לחיצה על מקש Ctrl. אם אין לכם חשבון, תוכלו ליצור אותו תוך פחות מ-30 שניות.
התכונה הזו מאפשרת לכם להיות יצירתיים ולעשות הרבה דברים. אם גוררים תמונה לתיבת הדו-שיח של מדפסת ב-Windows, התמונה תודפס באופן מיידי. אפשר להעתיק שיר מ-Box לאחסון בנייד, לגרור קובץ מ-Box לתוכנת האימייל כדי להעביר אותו ישירות לחבר או חברה… יש אינסוף אפשרויות לשיפור הפרודוקטיביות.
מחשבות ושיפורים עתידיים
עדיין מדובר בפתרון פחות אידיאלי, כי קריאה סינכרונית עלולה לנעול את הדפדפן לרגע. גם Web Worker של HTML 5 לא עוזר, כי Web Worker צריך להיות אסינכרוני. נראה שצריך לבצע את setData בזמן שמצרפים את מאזין האירועים.
בפועל, הביצועים די טובים. קריאת ה-Ajax (Sjax) הסינכרונית רק מאחזרת מחרוזת של כתובת URL, והיא אמורה להתבצע במהירות יחסית. עם זאת, יש לו עלות רבה בכותרת ה-HTTP, שאפשר לטפל בה באמצעות WebSockets. עם זאת, עד שנראה שימוש נרחב יותר בטכנולוגיה הזו, לא כדאי להשתמש ב-WebSockets כדי לשלוח ללקוח כל עדכון קטן.
אני גם מקווה שהיכולת להוריד כמה קבצים תתווסף ל-API בעתיד. בשילוב עם תיבות סימון בהתאמה אישית לבחירת כמה קבצים בממשק המשתמש, זה יהיה נהדר. בנוסף, יהיה נחמד עוד יותר אם אפשר יהיה להוריד קובצים שנוצרו על ידי לקוחות, כמו קובצי טקסט שנוצרו מהתוצאה של טופס שנשלח.
- העברת עמודות
- סידור מחדש של הרשימה
- יצירת גלריה של תמונות
- ייצוא תמונה על קנבס
קובצי עזר
- מפרט גרירה ושחרור