הפוסט הזה הוא חלק מסדרה על הקוד של עמוד בתי הספר של אורט באתר אורט החדש, עמוד שמכיל רשימה של כל בתי הספר של אורט לפי ערים, ומפה עם סימוני כל בתי הספר.
בפרק ההקדמה סיפרתי על הרקע הכללי של האתר ומבנה הסדרה, ובפרק הזה כבר צוללים לקוד.
המוטיבציה – מה אנחנו בונים
כמו שהראיתי בהקדמה, עמוד בתי הספר מורכב משני חלקים: המפה, ורשימת בתי הספר. בפרק הזה נתמקד ברשימה.
עוד לפני שמתחילים להתעסק עם מפות גוגל, האיפיון דרש שגולש יוכל לחפש בתי ספר או ערים במסך בתי הספר
את רשימת בתי הספר בנינו באמצעות ווידג’ט אקורדיון של אלמנטור, כך שהכול – הערים ובתי הספר – נמצא ב-HTML של העמוד. לכן כל שעל הקוד לעשות הוא למצוא בתוך אותו HTML את בתי הספר והערים שנמצאת בהם מחרוזת החיפוש, ולהסתיר את השאר.
סינון באמצעות Regular Expression – ניסיון שלא עלה יפה
כדי לא להמציא את הגלגל מחדש במקרה שהוא כבר קיים, חיפשתי קוד שמבצע חיפוש ב-node-ים של HTML. כצפוי, אינני הראשונה שצריכה פונקציונליות כזאת, ומהר מאוד מצאתי פתרון מ-StackOverflow. הפתרון משתמש ב-RegExp:
$("#box").on('keyup', function(){
var matcher = new RegExp($(this).val(), 'gi');
$('.connect-cat').show().not(function(){
return matcher.test($(this).find('.name, .category').text())
}).hide();
});
לכאורה נפלא, אבל אצלנו היה בהתחלה אתגר נוסף, והוא שהסינון צריך להיות דו שלבי / היררכי: להראות ערים שמחרוזת החיפוש נמצאת בהן, וגם להראות בתי ספר שמחרוזת החיפוש נמצאת בהן אבל בערים שלא עונות על מחרוזת החיפוש. אבל בסוף הוחלט, מטעמי שמישות, לא להסתיר את בתי הספר שמחרוזת החיפוש לא נמצאת בהם. הסיבה לזה היא שנוצר מצב מוזר מול המפה, כי היא כן מראה את כל בתי הספר, ואילו הרשימה מסתירה את חלקם. לכן הוחלט שבמקום להראות רק את בתי הספר הרלוונטיים לחיפוש, להראות את כל בתי הספר ורק לסמן את מחרוזת החיפוש בבתי הספר שבהם המחרוזת נמצאת.
לקח לי הרבה זמן לנסות להתאים את הפתרון למצב שלנו, ולצערי לא הצלחתי לעשות את זה עם שימוש ב-RegExp. הנה צילום מסך של קוד שניסיתי:

הקוד שכן עובד – indexOf
כשראיתי ששום התאמה של הקוד עם ה-Regex לא עובדת, החלטתי לנסות להשתמש ב-indexOf הפשוט, ולשמחתי זה עבד. קצת התבאסתי לא להשתמש ב-Regular Expression, אבל עדיף קוד עובד מקוד אלגנטי…
הצמדת הפונקציה לאירועים
מתחילים בהצמדת פונקציית הסינון לאירוע ההקלדה (בקוד יש גם הצמדה לאירוע של פוקוס בשדה הטקסט, ואדבר על זה בהמשך, בתת הכותרת “ומה קורה כשחוזרים לשדה הסינון אחרי אינטראקציה עם המפה?“).
fieldSearchSchools.on('keyup', e => filterSchools($(e)));
פונקציית הסינון העיקרית – filterSchools
הפוקנציה filterSchools
מקבלת כפרמטר את האירוע, ומוציאה מתוכו את מחרוזת החיפוש. היא בונה מערך של ערים שצריך להשאיר, ואח”כ היא מחביאה את כל הערים שלא נמצאות במערך. תוך כדי הסינון היא גם מסמנת בצבע את בתי הספר שמחרוזת החיפוש מופיעה בהם.
הערה יפה שקיבלתי על הפונקציה הזאת היא שהיא ארוכה מדי והיה ראוי לחלק אותה ליחידות קטנות, ובפרט לפונקציות של “לוגיקה טהורה”, שרק מקבלות פרמטר, מבצעות חישוב, ומחזירות ערך (בלי לשנות (mutate) את הפרמטר), ולפונקציות שיש להן “תופעות לוואי” (side-effects) שעוסקות בעדכון התצוגה. הערה נכונה וטובה, רק שלצערי ביקשתי הערות בשלב שכבר לא היה לי זמן ליישם שינויים כה מהותיים. זו הפקת לקחים לפעם הבאה בע”ה.
/**
* Show and hide cities and schools according to the value entered in the text input.
*/
function filterSchools(event) {
let citiesToShow = [];
let elementorAccordionItems = $('.elementor-accordion-item');
let searchValue = $(event.target).val();
// First show all accordion items because some of them might have been hidden by previous search.
elementorAccordionItems.show().each(function () {
let currentCitySchools = $(this).find('.elementor-tab-content li');
// now show all schools (because some of them might have been hidden by previous search)
currentCitySchools.show();
currentCitySchools.toArray().some(school => {
let currSchool = $(school);
currSchool.html(currSchool.html().replace(searhedSchoolMarkTagBegin, '').replace(searhedSchoolMarkTagEnd, ''));
if (searchValue.length > 0 && currSchool.text().indexOf(searchValue) > -1) {
currSchool.html(currSchool.html().replace(searchValue, searhedSchoolMarkTagBegin + searchValue + searhedSchoolMarkTagEnd));
citiesToShow.push(currSchool.parents('.elementor-accordion-item'));
}
});
});
elementorAccordionItems.each(function () {
let isItemInCitiesToShow = false;
let doesItemCityHaveValue = false;
let currCityName = $(this).find('.elementor-tab-title a').html();
$(citiesToShow).each(function () {
let currShownCityName = $(this).find('.elementor-tab-title a').html();
if (currCityName === currShownCityName) {
isItemInCitiesToShow = true;
return true;
}
});
// If the city does match the searched value, turn on the doesItemCityHaveValue flag to leave it showing.*/
if (currCityName.indexOf(searchValue) > -1) {
doesItemCityHaveValue = true;
}
/* Hide all accordion items whose cities don't contain the searched value and who don't have a school that contains that value*/
if (!isItemInCitiesToShow && !doesItemCityHaveValue) {
$(this).hide();
}
});
}
סימון החלק בשם בית הספר שמתאים למחרוזת
כאמור, הפונקציה מסמנת את המחרוזת בתוך שם בית הספר. שימו לב שלפני בדיקה אם המחרוזת נמצאת בשם בית הספר, אנחנו מוחקים את הסימון – אחרת זה משבש את החיפוש, מפני שה-HTML של הסימון קוטע את שם בית הספר.
הקוד שמוסיף את הסימון ושמסיר אותו מורכב מהגדרות של ה-CSS וה-HTML של הסימון, ומשתי פונקציות: אחת שמוסיפה את הסימון ואחת שמוחקת אותו. עדכון 23/9/19: קולגה שלי, ינון אלבז, האיר את עיניי שאת הסימון עשיתי בצורה מסורבלת, ואפשר לעשות אותה באמצעות replace
פשוט. הקוד בפונקציה filterSchools
תוקן בהתאם.
קודם מגדירים כמה משתנים: הקלאס שיינתן לאלמנט שמכיל את מחרוזת החיפוש (ב-CSS הוא יקבל עיצוב של רקע צהוב וטקסט שחור), תגית הפתיחה של האלמנט המקיף, ותגית הסגירה.
let clsSearhedSchooMark = "searched-school-mark"; let searhedSchoolMarkTagBegin =`<span class="${clsSearhedSchooMark}">`; let searhedSchoolMarkTagEnd = '</span>';
ומשתמשים במשתנים האלה בפונקציית ה-replace
שאני מפעילה על ה-HTML של שם בית הספר.
currSchool.html(currSchool.html().replace(searhedSchoolMarkTagBegin, '').replace(searhedSchoolMarkTagEnd, ''));
...
currSchool.html(currSchool.html().replace(searchValue, searhedSchoolMarkTagBegin + searchValue + searhedSchoolMarkTagEnd));
ומה קורה כשחוזרים לשדה הסינון אחרי אינטראקציה עם המפה?
הזכרתי קודם שגם אירוע הפוקוס הוצמד לפונקציה. הפוקציה הזאת קוראת כמובן לפונקציה filterSchools
, אבל היא מטפלת בכמה דברים שקשורים למפה:
- מחזירה את המפה לזום הראשוני שמראה את כל בתי הספר (הפונקציה הזאת,
resetMap
, תידון בפרק ג’, הפרק על הסנכרון בין המפה לרשימת הערים) - הסינון חוזר לקדמותו – מה שנמצא בשדה החיפוש מסנן את רשימת הערים
- קוראת לפונקציה שבודקת אם יש חלונית פתוחה במפה, וסוגרת אותה (הפונקציה <code”>closeClickHandler תידון בפרק ה’, הפרק על החלונית)
fieldSearchSchools.on('focus', e => schoolsFilterTextFocusHandler($(e.target)));
/**
* What to do when user puts focus on the text input of the school filter:
* 1. The map should go back to initial state
* 2. The cities and schools should be filtered acoording to whatever input is in the text box
* 3. Any markers showing on the map should be closed
* @param self - $('#schools_filter_text')
*/
function schoolsFilterTextFocusHandler(self) {
// return map to original bounds and size
resetMap();
// filter the schools
filterSchools(self);
// close any open markers and infowindows
closeClickHandler();
}
וכך עובד לו הסינון, למרבה השמחה. יש רק איזה עניין איפיוני שלא חשבנו עליו – כשמחפשים עיר שאין בה בתי ספר של אורט, הרשימה נעלמת לגמרי. זה טיפה מוזר, ואחד המנהלים שלי הציע שהיה עדיף לו היה כתוב משהו כמו “זאת עיר שטרם הגענו אליה” או משהו כזה. אולי זה עוד יקרה 🙂 .
Peace, L,
NB that your embedded hyperlinks don’t use slugs, but queries, eg “https://leketshibolim.ort.org.il/?p=450197&preview=true”, which is obviously not nice. And “preview” is redundant. Why is this happening? Maybe you’ve inserted them when the target posts were still drafts?
This may seem a minor blemish, but remember URLs are an essential aspect of the UI.
;o)
הי אילן,
תודה ששמת לב ושהארת את עיניי. אכן, יצרתי את הקישורים כשהפוסטים עוד היו בטיוטה. תיקנתי כעת, ואשתדל לזכור להמשיך לתקן ככל שיתפרסמו הפרקים 🙂
מעניין. אהבתי את השימוש בRegExp בדוגמא הראשונה 🙂
מסכים עם מה שאמרת לגבי קוד שעובד על גבי קוד אלגנטי, אבל עדיין סחטיין על הנסיון. אולי פעם הבאה זה יעבוד.
תודה, אלרון!
הלוואי באמת שבפעם כלשהי זה יעבוד…