תשרי

הרפתקאות מפת בתי הספר באתר אורט – פרק ה’, החלונית

הפוסט הזה הוא חלק מסדרה על הקוד של עמוד בתי הספר של אורט באתר אורט החדש, עמוד שמכיל רשימה של כל בתי הספר של אורט לפי ערים, ומפה עם סימוני כל בתי הספר.
בפרק ההקדמה סיפרתי על הרקע הכללי של האתר ומבנה הסדרה, בפרק הראשון סיפרתי על סינון הערים לפי מחרוזת חיפוש, בפרק השני דיברנו על אתגרי המפה ואיך מתחילים איתה, בפרק השלישי הכנו את התשתית לסנכרון הרשימה והמפה, ובפרק הרביעי צללנו לסנכרון בין המפה לערים ברשימה.
בפרק הזה אנחנו עוזבים את הרשימה ומתמקדים במפה: נדבר על מה שקורה כשלוחצים על אחד מסימוני בתי הספר במפה.

לחצנו על בית ספר במפה

מה קורה כשלוחצים על בית ספר במפת גוגל הרגילה? נפתחת חלונית, ובה ישנה כתובת בית הספר, וכתובת אתר האינטרנט שלו. אלה פרטים שמנהלת המפה הזינה לכל סימון.

לחלונית הרגילה יש כמה חסרונות: היא לא מתאימה לעיצוב של אתר אורט, יש בה מידע שאיננו צריכים (הקואורדינטות) ולכן התבקשתי ליישם עיצוב מתאים יותר, והמידע חוזר פעמיים (פעם בשדה התיאור, ופעם בשדות המיוחדים: כתובת, ואתר בית ספר). מכל הסיבות האלה התבקשתי לייצר חלונית משלנו.

דבר נוסף שהייתי צריכה לעשות בלחיצה על הסמן, היא להחליף את איקון הסמן. קצת קשה לראות בצילום המסך לעיל, אבל האופן שבו מפת גוגל מדגישה את הסמן שלחצנו עליו זה ע”י יצירת “הילה” לבנה סביב הסמן. אצלנו הוחלט שאיקון הסמן יוחלף לגמרי, מסמן קטן וכחול כהה, לסמן גדול וורוד.

 

חלונית מעוצבת כשלוחצים על סימון בי"ס במפה

החלונית היא לכאורה דבר קטן – גם מבחינת המידות שלה, וגם מבחינת החשיבות שלה – אבל לפי חוק ה-80/20, הוא לקח לי ים של זמן…

ושוב אנחנו שונים מדוגמאות הקוד הנפוצות

אמנם אפשר למצוא ברשת דוגמאות קוד של יצירת חלונית, אבל בכולן יוצאים מתוך הנחה שנרצה שאת הטקסט שמופיע בחלונית נכתוב בקוד, ושגם את המיקום נכניס hard coded.

אבל כמו עם יצירת הסימונים במפה, גם זה לא התאים אצלנו – הן מפני שלא הגיוני שמתכנת יצטרך לכתוב טקסטים בקוד ולהכניס מיקומים hard coded, והן מפני שהטקסטים והמיקומים קיימים במפה שאותה אנו מטמיעים, אז למה לא להשתמש במידע הזה?

המידע שנשתמש בו

יחי SO. בסוף הגעתי לתשובה שהסבירה איזה event קורה כשלוחצים על סימון, ואיזה מידע מגיע עם האירוע הזה (וגם ב-codepen הזה יש מידע שימושי):

  • שם בית הספר נמצא ב-event.featureData.name
  • התיאור (שמכיל גם את הכתובת הפיזית של בית הספר וגם את כתובת אתר בית הספר) נמצא ב-event.featureData.description
  • המיקום נמצא ב-event.latLng

קדימה לקוד

בכל הדברים האלה השתמשתי בפונקציה מרכזית שיצרתי, openInfoWindow, שנקראת כאשר לוחצים על סימון. הפונקציה openInfoWindow נקראת מתוך הפונקציה שקישרתי לאירוע לחיצה על המפה, kmlLayerClickedHandler.

האירוע שמפעיל את הפונקציה kmlLayerClickedHandler מוגדר גם הוא בפונקציית האתחול של המפה:

/* When one of the markers is clicked, open a styled popup, using the InfoWindow object created earlier */
kmlLayer.addListener('click', kmlLayerClickedHandler);

הפונקציה הזאת עושה שני דברים: יוצרת את החלונית, ומחליפה את הסמן לאיקון גדול וורוד.

function kmlLayerClickedHandler(event) {
    openInfoWindow(event);
    changeMarkerIcon(event);
}

הפונקציה openInfoWindow עושה כמה דברים:

  • יוצרת את ה-HTML  שעוטף את החלונית
  • קוראת לפונקציה getInfowindowFeaturedData שיוצרת את ה-HTML עם המידע על הסמן – הכתובות של ביתה הספר.
  • ממקמת את החלונית
  • מסתירה את חלונית ברירת המחדל. וואי, כמה ניסיונות עשיתי, וכלום לא עבד. בסוף עשיתי הכי חוראני שיש: איפסתי את  event.featureData.infoWindowHtml
  • היא גם קוראת לפונקציה updateInfowWindowForA11y שמטפלת בהתנגשות עם תוסף נגישות שהתקנו. אפרט על זה בע”ה בפרק על הנגישות. רק אומר כעת שברור שזה גרוע מאוד להכניס את הטיפול בנגישות בתוך הפונקציה הרגילה, והדבר הנכון היה שהפונקציה תחזיר את ה-HTML, ופונקציה אחרת תתערב ב-HTML מבחינה נגישותית. אבל מפאת חוסר זמן וחוסר מחשבה מראש, לא עשיתי את זה כך.
/**
 * This is the function that creates our own info window
 * @param event - has the featureData and latLng fields that are needed to create our window.
 */
function openInfoWindow(event) {
    if (event.featureData.status === "OK") {
        let infoWindowWrapperBegin = "<div class='" + infoWindowWrapperClass + "'" + updateInfowWindowForA11y() + ">";
        let infoWindowWrapperEnd = "</div>";
        let infowindowTitle = "<h3 class='info_window_header'>" + event.featureData.name + "</h3>";
        let infowindowDescription = "<div class='info_window_content'>";
        infowindowDescription += getInfowindowFeaturedData(event.featureData.description);
        infowindowDescription += "</div>"; // + event.featureData.description +
        infowindow.setContent(infoWindowWrapperBegin + infowindowTitle + infowindowDescription + infoWindowWrapperEnd);
        infowindow.setPosition(event.latLng); // even though we set the pixelOffset in the constructor, we also have to set the position.
        /* this is the only way I found to hide the original infowindow because suppressInfoWindows doesn't work and neither does showInfoWindowOnClick.
        Got the idea from here: https://stackoverflow.com/a/22083454/278
        maybe try baloonstyle: https://stackoverflow.com/questions/32557103/kml-file-is-there-a-way-to-completely-disable-description-bubbles*/
        event.featureData.infoWindowHtml = "";
        infowindow.open(map);
    }
}

הפונקציה getInfowindowFeaturedData מקבלת את שדה ה-featureData.description של ה-event ומכניסה אותו מסודר בתוך HTML. כמה דברים שצריך לדעת לפני שנסתכל בקוד:

  • המידע שמגיע מופרד ע”י br-ים. כך נראה שדה ה-description:

    תיאור: כתובת: אורט 2 אשדוד<br>אתר בית ספר: <a href=”https://yami-ashdod.ort.org.il/” target=”_blank”>https://yami-ashdod.ort.org.il/</a><br>כתובת: אורט 2 אשדוד<br>אתר בית ספר: <a href=”https://yami-ashdod.ort.org.il/” target=”_blank”>https://yami-ashdod.ort.org.il/</a>

  • המילים “תיאור” ו”כתובת”, שמגיעות עם המידע, מיותרות ולכן אני מעיפה אותן
  • יש לי שריטה שאני לא סובלת סלאש בסוף כתובת ולכן יש לי פונקציה שמעיפה אותו

וזו הפונקציה:

/**
 * Get the description from the Google window and parse them to display in our window
 * @param description - has the address and the site url of this school
 * @returns string - the HTML of the parsed description
 */
function getInfowindowFeaturedData(description) {
    let ret = "", schoolAddress, schoolUrl;
    let descriptionParts = description.split('<br>');
    if (descriptionParts.length > 0) {
        schoolAddress = getInfoFromInfowindow(descriptionParts, schools_and_map_filter_ajax_obj.strSchoolAddress, schools_and_map_filter_ajax_obj.strSchoolAddress);
        schoolUrl = getInfoFromInfowindow(descriptionParts, '<a', schools_and_map_filter_ajax_obj.strSchoolUrl);

        ret += wrapInHTML(schools_and_map_filter_ajax_obj.strSchoolAddress, schoolAddress);
        if (schoolUrl !== "") {
            schoolUrl = removeTrailingSlashFromURL(schoolUrl);
            ret += wrapInHTML(schools_and_map_filter_ajax_obj.strSchoolUrl, schoolUrl);
        }

    }
    return ret;
}

כמו שאפשר לראות, היא קוראת לפונקציה getInfoFromInfowindow. הפונקציה הזאת עובדת קשה לפענח ולפרסר את המידע שמגיע ב-description:

/**
 * Extract from the array of info the address of the school,
 * by searching for the part that has the string כתובת: or אתר בית ספר or any other string that might indicate our desired info
 * @param descriptionParts - the array of info from the info window
 * @param searchString - the string that might indicate our desired info
 * @param label - the label of the current field. We want to get rid of it
 * @returns {string} - the school addresses, without any other words
 */
function getInfoFromInfowindow(descriptionParts, searchString, label) {
    let ret = "";
    let infoArr = descriptionParts.filter(item => item.indexOf(searchString) > -1);
    if (infoArr.length > 0) {
        for (var infoData of infoArr) {
            let info = decodeURI(encodeURI(infoData)
                .replace(/%E2%80%8E/g, "")) // some strings come with these characters attached and some dont, and it affects the results of indexOf
                .replace(label, "")
                .replace(schools_and_map_filter_ajax_obj.strMarkerDescription, "")
                .replace(/target="_blank"/g, "")
                .trim();
            if (ret.indexOf(info) === -1) {
                if (ret !== "") {
                    ret += ", ";
                }
                ret += info;
            }
        }
    }
    return ret;
}

יש בה כמה נקודות מעניינות:

  • שעה ישבתי כדי להבין למה, גם אחרי שהוצאתי את כל המילים המיותרות, ועשיתי trim, עדיין, כשהמידע חזר פעמיים (כן, גוגל לפעמים מביא מידע ככה) והכנסתי אותו פעם אחת למחרוזת המוחזרת, הוא לא זיהה שהמופע השני לא אמור להיכנס. תיארתי לעצמי שיש איזשהם תווים נסתרים, ולא הבנתי למה trim לא מטפל בהם, ואיך בכל זאת אני יכולה לראות אותם. בסוף נעזרתי בחבר שתמיד עוזר שלי – Unicode code converter. הוא גילה לי שבאחד המופעים יש EOL! אז קידדתי את המחרוזת עם encodeURI והחלפתי את ה-eol הזה (שמופיע כך: %E2%80%8E) במחרוזת ריקה, הפכתי חזרה למחרוזת רגילה (עם decodeURI) והמשכתי עם שאר ההחלפות
  • בתי ספר משתלבים: הכוונה לבתי ספר שונים אבל נמצאים באותה כתובת, ולכן רק אחד מהם נראה. התלבטנו איך להתמודד (חשבנו להפריד את הסימונים, אבל אז אחד הסימונים יישב על כתובת לא נכונה, וזה לא נראה נכון). הוחלט בסוף להשאיר סמן אחד עם כתובת פיזית אחת אבל עם שני אתרי בתי ספר. הפונקציה שוכתבה כדי להתמודד עם זה.
  • מטעמי נגישות הוחלט שהקישורים ייפתחו באותו חלון, ולכן העפתי גם את target="_blank".

והנה הפונקציה השרוטה שלי שמעיפה את הלוכסן בסוף כתובת אתר בית הספר:

/**
 * Removes the trailing slash from the school URL
 * @param schoolUrl - the school URL
 * @returns the schoolUrl without trailing slash
 */
function removeTrailingSlashFromURL(schoolUrl) {
    return schoolUrl
        .replace(/\/<\/a>/g, "</a>")
        .replace(/<a /g, "<a class='info_window_school_link'");
}

בסוף עוטפים הכל ב-HTML ומגישים צונן:

/**
 * Wrap the string with a div, and add the title wrapped in a span with class.
 * @returns {string} - str + title in HTML tags
 */
function wrapInHTML(title, str) {
    str = "<div>" + "<span class='info_window_term'>" + title + "</span> " + str + "</div>";
    return str;
}

כדי שהפונקציה openInfoWindow תוכל להשתמש באובייקט ה-infoWindow, יש עוד פונקציה אחת, שנקראת מתוך פונקציית האתחול של המפה, והיא יוצרת את אוביקט ה-infoWindow, ונותנת לו עוד הגדרה שלא הצלחתי לתת בפונקציה שבונה את החלונית: אם משתמשים במיקום שמקבלים מגוגל, החלונית נפתחת בדיוק על הסמן, ולא רואים אותו. הוחלט שמטעמי שמישות נזיז אותו קצת למעלה. את ההזזה הזאת בשום אופן לא מצאתי איך מגדירים בפונקציה שבונה את החלונית ונאלצתי להגדיר בפונקציית האתחול של המפה (כן,יש פה magic number שהיה עדיף להגדיר כ-const.).

/**
 * The window which pops up on clicking a marker has to be styled,
 * and instead of trying to style the actual popup, we create our own.
 */
function createInfoWindowObject() {
    infowindow = new google.maps.InfoWindow({
        /* The pixelOffset tells the infoWindow to move 35 pixels above its position.
        * This is so the user can still see the marker that was clicked.
        * Taken from:
        * https://stackoverflow.com/questions/31064916/how-to-change-position-of-google-maps-infowindow/31068910#31068910
        * */
        pixelOffset: new google.maps.Size(0, -35)
    });
}

החלפה לאיקון גדול וורוד

להחליף איקון היה קשה יותר למצוא קוד, כי זה דבר הרבה פחות נפוץ, אבל בסוף התשובה הזו ב-SO נתנה לי את הפתרון:

function changeMarkerIcon(event) {
    var eLatLng = event.latLng;

    //this will remove the previous marker
    removeProviouslySelectedMarker();

    // create new marker with the location of the marker that was clicked
    marker = new google.maps.Marker({
        position: eLatLng,
        map: map,

    });

    // give it its special icon
    marker.setIcon('https://mapa-linux-test.ort.org.il/ort-site-2019/wp-content/plugins/schools-and-map/img/selected_marker.png');
}

וגם לסגירה צריך קוד

כשגונמרים להסתכל על הפרטים בחלונית, סוגרים אותה. מאחר שיצרנו אוביקט חלונית משלנו, גם את הסגירה אנחנו צריכים לבצע בעצמנו: את החלונית שפתחנו צריך לסגור, ואת האיקון – להחליף לאיקון הרגיל הקטן וכחול.

החלונית נסגרת בשני מקרים: כשלוחצים על ה-X שלה, וכשלוחצים על המפה. גם כאן, חיפוש ב-SO עזר לי לדעת אילו שני eventListenrs אני צריכה:

google.maps.event.addListener(infowindow, 'closeclick', closeClickHandler);

/* Clicking the map closes anything that had to do with clicking a marker */
google.maps.event.addListener(map, 'click', mapClickHandler);

הפונקציה closeClickHandler מפעילה שתי פונקציות:

/**
 * Run when the infowindow is closed. It changes the marker back to the original one
 */
function closeClickHandler() {
    closeInfoWindow();
    removeProviouslySelectedMarker();
}

הפונקציה שסוגרת את החלונית (פונקציה /קטנה של אובייקט ה-infowindow), והפונקציה שמחזירה את הסמן לצורתו המקורית.


/**
 * Remove the marker that was created a marker was clicked, back to the original marker
 */
function closeInfoWindow() {
    if (infowindow != null) {
        infowindow.close();
    }
}

/**
 * delete the marker that was created for the previously clicked marker
 */
function removeProviouslySelectedMarker() {
    if (marker != null) {
        marker.setMap(null);
    }
}

כאמור, הטיפול בחלונית ושות’ היה ארוך וקצת מאתגר, אך עשינו זאת.

הפרק הזה סיים את החלק של הקוד שעוסק במפה. בפרק הבא נדבר על פונקציה שמתרחשת רק ברזולוציות של טלפונים: כפתורי קפיצה מהמפה ורשימה ובחזרה.

כתבו תגובה

כתובת הדוא"ל שלכם לא תוצג.