pconkie Posted May 7, 2017 Share Posted May 7, 2017 (edited) The frog timetable widget does the job..... But it's big, contains a lot of unused white space and you only get to see one day at a time. It's a matter of opinion, but we probably had more usable timetable widgets in earlier versions of frog. After spending the last week or so trying to get up to speed with FrogLearn development (thank you Graham and @Chris.Smith for your helpful and speedy responses!) I thought I would give re-designing this widget a go. Here is what I came up with.... As with the frog version you can lookup and display any student or teachers timetable. This version is much more compact, has a week to view, supports two week timetables and is a bit more 'colourful'. There are a few improvements that need to be made which i hope the community can help with, but i'm fairly pleased with the result and what is possible with the frog api's and built in access to jquery and bootstrap. I plan to share and explain my approach so that the code can be improved (i'm a science teacher with no formal coding training) and perhaps others will find it useful for their own projects? Edited May 7, 2017 by pconkie 3 Link to comment Share on other sites More sharing options...
pconkie Posted May 7, 2017 Author Share Posted May 7, 2017 (edited) STEP 1: Analyse the frog timetable widget What information does it send and receive when it loads and as various parts of it are interacted with? How to do this is already explained on this forum (https://frog.frogcommunity.com/understanding-api), but in Chrome you can right click any element of the screen and select "inspect" (or ctrl+shift+I). This opens the Chrome Developer Tools (select the 'network' tab). You can see from the screen shot that as the widget loads it calls a frog api - timetables.getTimetable. This is a GET request (rather than a POST) which is important for any code that we will write. You can tell this because all of the parameters have been sent as a query string (if our parameters were contained in a form section then a POST would have been involved). The following parameters must be supplied with this call to timetables.getTimetable: 1. offset (number) - 0 = current week. Allows you to specify what week of timetable information you would like returned. e.g. 5 = 5 weeks in the future, -8 = 8 weeks in the past. 2. limit (number) - (use in conjunction with offset) the number of weeks of timetable data to return e.g. 3 = 3 weeks of data from the offset value is returned. 3. week_starts_on (number) - what should the first day of the week be? 1 = use Monday as first day of week. 4. user_uuid (string) - the id of the currently logged in user 5. target[] (array) - the id of the user whose timetable you want to look up. If omitted or left null the user_uuid is used for this purpose. Haven't tested, but assume you can supply more than one uuid. From using other examples on this forum here is a function for calling this api: var getTimetable = function(anyuuid) { Frog.Model .api( 'timetables.getTimetable', { limit: 2, offset: 0, week_starts_on: 1, //Monday user_uuid: currentuser.uuid, target: [anyuuid] //uuid for user we are looking up } ).done(function(thisResponse) { //error handling if (thisResponse.status.code == 0) { //more code here } }); }; //usage getTimetable(); //returns the timetable of the currently logged in user getTimetable('ABC12345'); //returns the timetable of the user with uuid ABC1234 Now back to Chrome to look at the structure of the data returned (preview tab). This is some highly nested JSON data. I find it useful to copy and paste it into a JSON viewer like this... The data section is of most interest. For each user whose timetable we are requesting (in our example just one) there is information about the user, the days in the timetable cycle and the timetable week (or weeks) requested. For each week there is information about the days in that week, the periods in that day and the group, room and teacher allocated to that period. That's USER->WEEK->DAY->PERIOD. If we want to display the timetable information for a day, we would need to select a day and loop through all of the periods. If we want to display the timetable information for a week we would need to select a week and loop through each day AND loop through each period. //this code inside the done() of the api call //would print details of every lesson in the entire response regardless of offset or how many weeks returned (limit) var data = thisResponse.data[0]; $.each(data.weeks, function(i,weeks) { $.each(weeks.days, function(p,days) { $.each(days.periods, function(q,periods) { console.log(periods.name+' '+periods.subject+' '+periods.teacher.displayname); }); }); }); //feels like there should be a better way of doing this? The frog timetable widget also uses a second api to search for other users and return their uuid. We will look at this call (textsearch.search) in step 2. Edited May 7, 2017 by pconkie Link to comment Share on other sites More sharing options...
pconkie Posted May 7, 2017 Author Share Posted May 7, 2017 (edited) STEP 2: Analyse the other frog timetable widget api call. The second api the frog timetable widget makes use of is textsearch.search Above you can see that I am searching for 'smith' by typing that into the search box. Each time a key is pressed (once there is more than one letter typed) a call to the api is made. Again it is a GET request with the following parameters: 1. profile (string) - which profiles to include in the search 2. type (string) - presume type of entity (user, site? resource?) So that the api is re-usable in different contexts. 3. include_associations (boolean?) - no idea 4. exclude_sources (array) - no idea 5. find (string) - the term to search for Example function attached to a keyup event: $('input').keyup(function() { var s = this.val(); if(s.length > 1) { Frog.Model .api( 'textsearch.search', { profile: 'student,staff', find: s, type: 'user', include_associations: true, exclude_sources: ['temporary'] } ).done(function(thisResponse) { if (thisResponse.status.code == 0) { //empty some ul //looop through the users returned //re-fill the ul with li containing user information } }); } }); The information returned by this api is much more straight forward. If we look in data we find a user collection containing all of the users that match my search for 'smith'. You can see that I have 17 returned matches. I can loop through the users and print out their profile, display name and uuid (which is what we need to pass to tables.getTimetable something like this.... //again this should go inside the done() of the api call, that way it will wait for a response if (thisResponse.status.code == 0) { $(".mywrapper ul").empty(); var rusers = thisResponse.data.user; if (rusers.length) { $.each(rusers, function(i,users) { $(".mywrapper ul").append('<li data-value="'+users.uuid+'">'+users.profile.name+':'+users.displayname+'</li>'); }); } } So we now have a fairly good idea how the frog timetable viewer works and in particular the mechanics of the two api's it uses. We are now ready in step 3 to make an alternative timetable viewer! Edited May 7, 2017 by pconkie Link to comment Share on other sites More sharing options...
Graham Quince Posted May 8, 2017 Share Posted May 8, 2017 That's amazing. Thanks for sharing. Link to comment Share on other sites More sharing options...
pconkie Posted May 8, 2017 Author Share Posted May 8, 2017 Step 3: Basic mark-up and CSS for the 'week to view' We have to bear in mind that different schools have different timetable structures. In the final version the layout html is generated dynamically based on the number of days in the cycle and the number of periods in each day. For now we will keep it simple and fix the layout for a 5 day timetable with 7 periods each day. <style> .mywrapper .table tbody tr { height: 50px; } .mywrapper .table th { text-align: center; } </style> <div class="mywrapper"> <table class="table table-bordered"> <thead> <tr> <th>This Week</th> <th>Monday</th> <th>Tuesday</th> <th>Wednesday</th> <th>Thursday</th> <th>Friday</th> </tr> </thead> <tbody> <tr> <th>REG</th> <td id="1Mon REG"></td> <td id="1Tue REG"></td> <td id="1Wed REG"></td> <td id="1Thu REG"></td> <td id="1Fri REG"></td> </tr> <tr> <th>LESSON 1</th> <td id="1Mon 1"></td> <td id="1Tue 1"></td> <td id="1Wed 1"></td> <td id="1Thu 1"></td> <td id="1Fri 1"></td> </tr> ///////////////////// ///Repeat <tr> to </tr> ///for additional periods ///////////////////// </tbody> </table> </div> This gives the following output on a frog page: <div class="mywrapper"> ......</div> Everything is inside this div. I've noticed that code in a HTML widget on a page has the potential to affect other open pages and sometimes have even worse consequences! By having everything inside a div with a (undefined) class we can attempt to better control the consequences of our code. For example, i chose to use a table (I know, i'm a bit old fashioned) and as frog utilities bootstrap css (http://getbootstrap.com/css/#tables) I have taken advantage of the built in styles available.... <table class="table table-bordered"> Those two classes (table and table-bordered) style the table producing the thin border and the bold column and 'row' headings. I wanted to over-ride the table class a little bit by adding a fixed row height and center the text, but was concerned that I might affect other tables on the frog platform. So I made use of the div everything is inside with the class - mywrapper (which i assume is unique, but could change it to make it 'more unique'). <style> .mywrapper .table tbody tr { height: 50px; } .mywrapper .table th { text-align: center; } </style> The space between the classes is the descendant selector. So this line reads.... .mywrapper .table tbody tr select all the table rows (tr) that are in all the body's (tbody) of all the tables with a class of table (.table) as long as those tables are inside an element with the mywrapper class (.mywarpper). As far as I am aware this is pretty precise styling and should cause problems elsewhere, but please chip-in if you disagree or think there is a better way etc The ids on the table cells (td) are crucial and must exactly match the period labels used by the school/MIS. Step 4 when i get a chance - filling the table with this weeks timetable for the currently logged in user. Link to comment Share on other sites More sharing options...
pconkie Posted May 8, 2017 Author Share Posted May 8, 2017 Step 4: Filling the table with this weeks timetable for the currently logged in user This javascript should go before the code in step 3. <script> var currentuser = FrogOS.getUser(); //currentuser.uuid now contains the logged in users uuid //this function sends a request for the selected users timetable and is called as soon as the document is ready (below) var getTimetable = function(anyuuid) { $(".mywrapper table td").html(''); //these two lines act as a 're-set' for the timetable grid (clear text and background colour) $(".mywrapper table td").css("background-color",''); //not strictly required but as soon as we want to show another users timetable will be handy! Frog.Model .api( 'timetables.getTimetable', { limit: 2, //how many weeks of timetable data to return? offset: 0, //how many weeks ahead of current week? 0=current week (can be negative value) week_starts_on: 1, //what day of the week is the first day 1=Monday user_uuid: currentuser.uuid, //uuid of current user target: [anyuuid] //uuid for timetable we are looking up } ).done(function(thisResponse) { if (thisResponse.status.code == 0) { //check the response returned without error. Should this be ===? var data = thisResponse.data[0]; //get the bit of the reponse we want $.each(data.weeks, function(i,weeks) { //here loop through each week of data look at each period of each day of each of those weeks. $.each(weeks.days, function(p,days) { $.each(days.periods, function(q,periods) { $(".mywrapper table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room); //line above matches correct cell in table to correct period in data returned using periods.name }); }); }); } }); }; $(document).ready(function() { //when the document is ready getTimetable(); //call the api passing no parameter. Default behaviour is that we will get back the current users timetable. }); </script> If there are issues make sure that the cells in your table (td) have an id that exactly match the period names that your MIS (and therefore frog) are using. It doesn't look very colourfull yet! In the next step - the mathematics of colour Link to comment Share on other sites More sharing options...
pconkie Posted May 8, 2017 Author Share Posted May 8, 2017 (edited) Step 5: Giving each subject their own background colour Each subject gets in own background colour (actually if a student has two different teachers for the same subject those lessons get different background colours too, but you could change that). What I wanted to avoid was listed every possible subject in frog and assigning a corresponding colour. If possible this should have zero configuration. I wondered if i could somehow turn unique strings into unique colours e.g "science" --> blue, but "maths" --> green. There are a few ways of setting a colour in html5 but one way is using a hex. 3 pairs of code, the first for the red content, the next for the green content and the last for the blue content e.g. #FF0000 (Red) FF=255 red, 00=0 green, 00=0 blue. So can i turn strings of different length into a hex? Random wouldn't work as I would want the same colour each time the subject appeared. Didn't have to write this code, the answer (and credit) was found here: http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript //this is a function to consistently generate a colour for any string passed to it e.g. a subject name var stringToColour = function(str) { var hash = 0; for (var i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var colour = '#'; for (var p = 0; p < 3; p++) { var value = (hash >> (p * 8)) & 0xFF; colour += ('00' + value.toString(16)).substr(-2); } return colour; }; This works great, but there is an issue. All of the font is black, and so can not be read if the background colour is black (or dark). Any font colour would have the same issue! To fix this issue we need to test the brightness of the colour generated from the function above and set the font to either black or white based on which has the best contrast. Ideas found here: https://24ways.org/2010/calculating-color-contrast I preferred the results when calculating the YIQ (https://en.wikipedia.org/wiki/YIQ) rather than comparing the hex to the hex which is halfway between pure black and pure white. var getForegroundColor = function(hexcolor) { var r = parseInt(hexcolor.substr(1,2),16); var g = parseInt(hexcolor.substr(3,2),16); var b = parseInt(hexcolor.substr(5,2),16); var yiq = ((r*299)+(g*587)+(b*114))/1000; return (yiq >= 128) ? 'black' : 'white'; }; I could possibly put these two functions together, but at the moment I have them separate and use them to colour the timetable like so... //this is a pre-existing line in the done() of the api call $(".mywrapper table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3)); /////////////////////////////////////////////////////////////// //add these three lines and the two functions from above var bgcolor = stringToColour(periods.subject+periods.teacher.username); $(".mywrapper table tr td[id='"+periods.name+"']").css("background-color", bgcolor); $(".mywrapper table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor)); //interested in more efficient ways of doing this. So you won't have any control over what colours are used, but this requires no configuration and you will get a nice consistently coloured timetable. Edited May 8, 2017 by pconkie Link to comment Share on other sites More sharing options...
bernadette fagan Posted May 9, 2017 Share Posted May 9, 2017 This is great Paul! Thanks for sharing. 1 Link to comment Share on other sites More sharing options...
ADT Posted May 9, 2017 Share Posted May 9, 2017 @Graham Quince @Chris.Smith I take it you boys are converting this into a Frog Code widget right? Link to comment Share on other sites More sharing options...
pconkie Posted May 9, 2017 Author Share Posted May 9, 2017 Step 6: Searching and displaying other users timetables I just stole the mark-up from the frog timetable viewer which has a hidden search box, which once revealed allows you to start typing a users name and displays the results just below. Once a user is selected from the list their timetable loads. Mark-up code is: <div class="widget_timetable widget_timetable_user widget_timetable_user_search"> <div class="theme-headerbg clearfix"> <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2> <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button> </div> <div class="frogui_components_search" data-component="search" style="display: none;"> <form class="search"> <input autocomplete="off" type="text" name="needle" placeholder=""> <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button> </form> </div> </div> Keeping the frog classes in meant that it looked really good straight away! The it was *just* the case of attached all of the events again... 1. Toggling the visibility of the search box when the search icon is clicked 2. Calling the textsearch.search api and filling the dropdown-menu ul 3. Re-setting the search box when the clear-search button is clicked 4. Hiding the dropdown-menu when it has lost focus 5. Calling the getTimetable api when a user is selected from the drop-down menu and displaying their timetable and displayname in the widget title 6. Preventing the default behaviour of the return key (which wants to submit the form which would navigate us away from the page) Code to follow.... Link to comment Share on other sites More sharing options...
Chris.Smith Posted May 9, 2017 Share Posted May 9, 2017 4 hours ago, ADT said: @Graham Quince @Chris.Smith I take it you boys are converting this into a Frog Code widget right? Well Adrian, I think that is a brilliant idea! If only I knew an ICT teacher and VLE coordinator whose Year 11 and 13s where about to leave school thus freeing up some of his famously busy schedule allowing him to make said widget Honest answer is we are busy getting some surprises ready in time for conference; so we don't have a great deal of time. @pconkie are you up for a challenge? 1 Link to comment Share on other sites More sharing options...
ADT Posted May 9, 2017 Share Posted May 9, 2017 2 minutes ago, Chris.Smith said: Well Adrian, I think that is a brilliant idea! If only I knew an ICT teacher and VLE coordinator whose Year 11 and 13s where about to leave school thus freeing up some of his famously busy schedule allowing him to make said widget Honest answer is we are busy getting some surprises ready in time for conference; so we don't have a great deal of time. @pconkie are you up for a challenge? ICT teacher and VLE coordinator who unlike most schools... timetable changes after half term to our year groups move up.... new classes new courses.......... extra time 0!! Link to comment Share on other sites More sharing options...
pconkie Posted May 9, 2017 Author Share Posted May 9, 2017 Step 6b The code (re-written following advice from Chris) 1. Toggling the visibility of the search box when the search icon is clicked var $mywrapper = this.element.find(".mywrapper"); $mywrapper.find("#toggleSearch").click(function(e) { $mywrapper.find(".frogui_components_search").toggle(); e.stopPropagation(); }); 2. Calling the textsearch.search api and filling the dropdown-menu ul $mywrapper.find('.search input').keyup(function() { var s = $mywrapper.find('.search input').val(); if(s.length > 1) { Frog.Model .api( 'textsearch.search', { profile: 'student,staff', find: s, type: 'user', include_associations: true, exclude_sources: ['temporary'] } ).done(function(thisResponse) { if (thisResponse.status.code == 0) { $mywrapper.find("ul.typeahead.dropdown-menu").empty(); var rusers = thisResponse.data.user; if (rusers.length) { $.each(rusers, function(i,users) { $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>'); }); $mywrapper.find('.dropdown-menu').show(); } else { $mywrapper.find('.dropdown-menu').hide(); } } }); } }); 3. Re-setting the search box when the clear-search button is clicked $mywrapper.find("#btn-clear").click(function() { $mywrapper.find('.search input').val(''); }); 4. Hiding the dropdown-menu when it has lost focus $(this.body).click(function() { $mywrapper.find('.dropdown-menu').hide(); }); 5. Calling the getTimetable api when a user is selected from the drop-down menu and displaying their timetable and displayname in the widget title $mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) { e.preventDefault(); $mywrapper.find('.search input').val($mywrapper.find("span").attr('title')); getTimetable($(this).data("value")); }); 6. Preventing the default behaviour of the return key (which wants to submit the form which would navigate us away from the page) $mywrapper.find('.search input').keypress(function(e) { if (e.which == 13) { e.preventDefault(); return false; } }); //this seems to be needed for IE to do anything right! $mywrapper.find('.search input').on('click', function(e) { e.stopPropagation(); }); That's it. I'm done. I'll post the entire code when i have had a chance to change everything suggested by @Chris.Smith. As for turning it into a FrogCode widget - sure, how hard could that be?! Especially if that means getting to use the FrogCode area of my FrogDrive that just appeared?! Link to comment Share on other sites More sharing options...
pconkie Posted May 9, 2017 Author Share Posted May 9, 2017 Alternative Timetable Viewer Here is all the code in one go: <style> .frogui_components_search { border: 0 !important; } .table tbody tr { height: 50px; } .table th { text-align: center; } .table { background-color: #FFFFFF; } .table td { line-height: 15px; padding: 4px; } img { margin: 0px auto; display: block; } </style> <div class="mywrapper"> <div class="widget_timetable widget_timetable_user widget_timetable_user_search"> <div class="theme-headerbg clearfix"> <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2> <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button> </div> <div class="frogui_components_search" data-component="search" style="display: none;"> <form class="search"> <input autocomplete="off" type="text" name="needle" placeholder=""> <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button> </form> </div> </div> <img id="loader" src="../app/public/icon/UI/loading.gif"> <div id="week1" class="table-responsive"> <table class="table table-bordered"> <thead> <tr> <th class="span1">WEEK 1</th> <th class="span2">Monday</th> <th class="span2">Tuesday</th> <th class="span2">Wednesday</th> <th class="span2">Thursday</th> <th class="span2">Friday</th> </tr> </thead> <tbody></tbody> </table><br /> <button type="button" id="wk1btn" class="btn btn-primary" style="display: none;">Click for Week 2</button> </div> <div id="week2" class="table-responsive"> <table class="table table-bordered"> <thead> <tr> <th class="span1">WEEK 2</th> <th class="span2">Monday</th> <th class="span2">Tuesday</th> <th class="span2">Wednesday</th> <th class="span2">Thursday</th> <th class="span2">Friday</th> </tr> </thead> <tbody></tbody> </table><br /> <button type="button" id="wk2btn" class="btn btn-primary">Click for Week 1</button> </div> </div> <script> ///////////these vars should become the preferences in the eventual forg code widget //////// //////////change these to match the timetable used by your school or nothing will work!/////// var school_weeks = 2; var school_periods_in_day = 7; //total including any reg/tutor that will appear in your achool_lesson_labels var school_lesson_labels = ["REG","LESSON 1","LESSON 2","LESSON 3","LESSON 4","LESSON 5","LESSON 6"]; var school_period_labels = ["1Mon REG","1Mon 1","1Mon 2","1Mon 3","1Mon 4","1Mon 5","1Mon 6","1Tue REG","1Tue 1","1Tue 2","1Tue 3","1Tue 4","1Tue 5","1Tue 6","1Wed REG","1Wed 1","1Wed 2","1Wed 3","1Wed 4","1Wed 5","1Wed 6","1Thu REG","1Thu 1","1Thu 2","1Thu 3","1Thu 4","1Thu 5","1Thu 6","1Fri REG","1Fri 1","1Fri 2","1Fri 3","1Fri 4","1Fri 5","1Fri 6","2Mon REG","2Mon 1","2Mon 2","2Mon 3","2Mon 4","2Mon 5","2Mon 6","2Tue REG","2Tue 1","2Tue 2","2Tue 3","2Tue 4","2Tue 5","2Tue 6","2Wed REG","2Wed 1","2Wed 2","2Wed 3","2Wed 4","2Wed 5","2Wed 6","2Thu REG","2Thu 1","2Thu 2","2Thu 3","2Thu 4","2Thu 5","2Thu 6","2Fri REG","2Fri 1","2Fri 2","2Fri 3","2Fri 4","2Fri 5","2Fri 6"]; var timetableweek_toview = new Date("2017-07-01"); //this is the start of the week or fortnight we will use as 'week1' and 'week2'. Make sure its a normal representative block of time as late as possible in the academic year (avoid holidays, bank holiday funny timetable weeks like enrichment week etc). ///////////other vars /////////////////////////////////////// var currentuser = FrogOS.getUser(); var today = new Date(); var offsetweeks = Math.round((timetableweek_toview-today)/ 604800000); var school_days_in_week = 5; var school_cyclelength = school_weeks * school_days_in_week * school_periods_in_day; var school_weeklength = school_days_in_week * school_periods_in_day; var $mywrapper = this.element.find(".mywrapper"); ////////////////////////////////////////////////////////// var getTimetable = function(anyuuid) { $mywrapper.find("#week1").hide(); $mywrapper.find("#week2").hide(); $mywrapper.find("#loader").show(); $mywrapper.find("table td").html(''); $mywrapper.find("table td").css("background-color",''); Frog.Model .api( 'timetables.getTimetable', { limit: 2, offset: offsetweeks, week_starts_on: 1, user_uuid: currentuser.uuid, target: [anyuuid] } ).done(function(thisResponse) { if (thisResponse.status.code === 0) { var data = thisResponse.data[0]; $.each(data.weeks, function(i,weeks) { $.each(weeks.days, function(p,days) { $.each(days.periods, function(q,periods) { $mywrapper.find("table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3)); var bgcolor = stringToColour(periods.subject+periods.teacher.username); $mywrapper.find("table tr td[id='"+periods.name+"']").css("background-color", bgcolor); $mywrapper.find("table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor)); }); }); }); $mywrapper.find('#searchResult').html('Timetable for '+data.user.displayname); $mywrapper.find("#loader").hide(); $mywrapper.find("#week1").show(); } }); }; var delay = (function(){ var timer = 0; return function(callback, ms){ clearTimeout (timer); timer = setTimeout(callback, ms); }; })(); var stringToColour = function(str) { var hash = 0; for (var i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var colour = '#'; for (var p = 0; p < 3; p++) { var value = (hash >> (p * 8)) & 0xFF; colour += ('00' + value.toString(16)).substr(-2); } return colour; }; var getForegroundColor = function(hexcolor) { var r = parseInt(hexcolor.substr(1,2),16); var g = parseInt(hexcolor.substr(3,2),16); var b = parseInt(hexcolor.substr(5,2),16); var yiq = ((r*299)+(g*587)+(b*114))/1000; return (yiq >= 128) ? 'black' : 'white'; }; $mywrapper.find('#wk1btn').click(function() { $mywrapper.find('#week1').hide(); $mywrapper.find('#week2').show(); }); $mywrapper.find("#wk2btn").click(function() { $mywrapper.find("#week1").show(); $mywrapper.find("#week2").hide(); }); $mywrapper.find("#toggleSearch").click(function(e) { $mywrapper.find(".frogui_components_search").toggle(); e.stopPropagation(); }); $mywrapper.find("#btn-clear").click(function() { $mywrapper.find('.search input').val(''); $mywrapper.find('.dropdown-menu').hide(); $mywrapper.find("ul.typeahead.dropdown-menu").empty(); }); $(this.body).click(function() { $mywrapper.find('.dropdown-menu').hide(); }); //this seems to be needed for IE to do anything right! $mywrapper.find('.search input').on('click', function(e) { e.stopPropagation(); }); $mywrapper.find('.search input').keypress(function(e) { if (e.which === 13) { e.preventDefault(); return false; } }); $mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) { e.preventDefault(); $mywrapper.find(".frogui_components_search").hide(); $mywrapper.find('.dropdown-menu').hide(); $mywrapper.find("ul.typeahead.dropdown-menu").empty(); $mywrapper.find('.search input').val(''); getTimetable($(this).data("value")); }); $mywrapper.find('.search input').keyup(function() { var s = $mywrapper.find('.search input').val(); if(s.length > 1) { delay(function(){ Frog.Model .api( 'textsearch.search', { profile: 'student,staff', find: s, type: 'user', include_associations: true, exclude_sources: ['temporary'] } ).done(function(thisResponse) { if (thisResponse.status.code === 0) { $mywrapper.find("ul.typeahead.dropdown-menu").empty(); var rusers = thisResponse.data.user; if (rusers.length) { $.each(rusers, function(i,users) { $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>'); }); $mywrapper.find('.dropdown-menu').show(); } else { $mywrapper.find('.dropdown-menu').hide(); } } }); }, 800 ); } }); //render the timetable grid var y; var b = 0; for (var i = 0; i < school_periods_in_day; i++) { var trsource = ''; trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>'; for (y = b; y < school_weeklength; y=y+school_periods_in_day) { trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>'; } trsource = trsource + '</tr>'; $mywrapper.find("#week1 table tbody").append(trsource); b++; } if (school_weeks === 2) { var b = 0; for (var i = 0; i < school_periods_in_day; i++) { var trsource = ''; trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>'; for (y = (b+school_weeklength); y < school_cyclelength; y=y+school_periods_in_day) { trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>'; } trsource = trsource + '</tr>'; $mywrapper.find("#week2 table tbody").append(trsource); $mywrapper.find("#wk1btn").show(); b++; } } $mywrapper.find('#searchResult').html('Timetable for '+currentuser.displayname); getTimetable(); </script> Copy and paste into a HTML widget on a page and then you must change the variables in this section ////////////////////////////////////////////////////////////////////////////////////////////////////////// var school_weeks (number) 1 or 2 supported var school_periods_in_day (number) as many as you need var school_lesson_labels[] (array) whatever you like but should be the same number of items in this array as value for school_periods_in_day var school_period_labels[] (array) type exactly what SIMS uses as shorthand for your periods e.g. 'Mon1', 'Mon 1', '1Mon1', '1Mon 1' etc in order var timetableweek_toview (date) pick a Monday as late in the academic year as possible to be the start of the week (or fortnight) that we will use as 'week1' and 'week2'. A date in the future will always result in the most recent timetable being displayed to the user (avoid holidays, bank holiday funny timetable weeks like enrichment week etc). ////////////////////////////////////////////////////////////////////////////////////////////////////// @Chris.Smith one issue remains that I am aware of. The searching for a user works great on all desktop browsers that I have tried it on. However on a touch device (e.g. iPad) the list of matched users appears but tapping on one of them fails to load their timetable. I guess this is the event that fails to fire on iPad... $mywrapper.find('ul.typeahead.dropdown-menu').on('click', 'li', function (e) { e.preventDefault(); $mywrapper.find(".frogui_components_search").hide(); $mywrapper.find('.dropdown-menu').hide(); $mywrapper.find("ul.typeahead.dropdown-menu").empty(); $mywrapper.find('.search input').val(''); getTimetable($(this).data("value")); }); 1 Link to comment Share on other sites More sharing options...
pconkie Posted May 9, 2017 Author Share Posted May 9, 2017 (edited) Actually i figured this one out....found the tap event. changed to... $mywrapper.find('ul.typeahead.dropdown-menu').on('click tap', 'li', function (e) { }); Right fully working code here <style> .frogui_components_search { border: 0 !important; } .table tbody tr { height: 50px; } .table th { text-align: center; } .table { background-color: #FFFFFF; } .table td { line-height: 15px; padding: 4px; } img { margin: 0px auto; display: block; } </style> <div class="mywrapper"> <div class="widget_timetable widget_timetable_user widget_timetable_user_search"> <div class="theme-headerbg clearfix"> <h2 class="widget-title btn btn-text os-ellipsis" id="searchResult"></h2> <button type="button" class="btn btn-text pull-right" id="toggleSearch"><i class="os-icon search-icon"></i></button> </div> <div class="frogui_components_search" data-component="search" style="display: none;"> <form class="search"> <input autocomplete="off" type="text" name="needle" placeholder=""> <ul class="typeahead dropdown-menu" style="top: 30px; left: 0px; display: none;"></ul> <button id="btn-clear" class="search-btn os-icon search-icon search-btn-clear" type="button"></button> </form> </div> </div> <img id="loader" src="../app/public/icon/UI/loading.gif"> <div id="week1" class="table-responsive"> <table class="table table-bordered"> <thead> <tr> <th class="span1">WEEK 1</th> <th class="span2">Monday</th> <th class="span2">Tuesday</th> <th class="span2">Wednesday</th> <th class="span2">Thursday</th> <th class="span2">Friday</th> </tr> </thead> <tbody></tbody> </table><br /> <button type="button" id="wk1btn" class="btn btn-primary" style="display: none;">Click for Week 2</button> </div> <div id="week2" class="table-responsive"> <table class="table table-bordered"> <thead> <tr> <th class="span1">WEEK 2</th> <th class="span2">Monday</th> <th class="span2">Tuesday</th> <th class="span2">Wednesday</th> <th class="span2">Thursday</th> <th class="span2">Friday</th> </tr> </thead> <tbody></tbody> </table><br /> <button type="button" id="wk2btn" class="btn btn-primary">Click for Week 1</button> </div> </div> <script> ///////////these vars should become the preferences in the eventual forg code widget //////// var school_weeks = 2; var school_periods_in_day = 7; //total including any reg/tutor that will appear in your achool_lesson_labels var school_lesson_labels = ["REG","LESSON 1","LESSON 2","LESSON 3","LESSON 4","LESSON 5","LESSON 6"]; var school_period_labels = ["1Mon REG","1Mon 1","1Mon 2","1Mon 3","1Mon 4","1Mon 5","1Mon 6","1Tue REG","1Tue 1","1Tue 2","1Tue 3","1Tue 4","1Tue 5","1Tue 6","1Wed REG","1Wed 1","1Wed 2","1Wed 3","1Wed 4","1Wed 5","1Wed 6","1Thu REG","1Thu 1","1Thu 2","1Thu 3","1Thu 4","1Thu 5","1Thu 6","1Fri REG","1Fri 1","1Fri 2","1Fri 3","1Fri 4","1Fri 5","1Fri 6","2Mon REG","2Mon 1","2Mon 2","2Mon 3","2Mon 4","2Mon 5","2Mon 6","2Tue REG","2Tue 1","2Tue 2","2Tue 3","2Tue 4","2Tue 5","2Tue 6","2Wed REG","2Wed 1","2Wed 2","2Wed 3","2Wed 4","2Wed 5","2Wed 6","2Thu REG","2Thu 1","2Thu 2","2Thu 3","2Thu 4","2Thu 5","2Thu 6","2Fri REG","2Fri 1","2Fri 2","2Fri 3","2Fri 4","2Fri 5","2Fri 6"]; var timetableweek_toview = new Date("2017-07-01"); //this is the start of the week or fortnight we will use as 'week1' and 'week2'. Make sure its a normal representative block of time as late as possible in the academic year (avoid holidays, bank holiday funny timetable weeks like enrichment week etc). ///////////other vars /////////////////////////////////////// var currentuser = FrogOS.getUser(); var today = new Date(); var offsetweeks = Math.round((timetableweek_toview-today)/ 604800000); var school_days_in_week = 5; var school_cyclelength = school_weeks * school_days_in_week * school_periods_in_day; var school_weeklength = school_days_in_week * school_periods_in_day; var $mywrapper = this.element.find(".mywrapper"); ////////////////////////////////////////////////////////// var getTimetable = function(anyuuid) { $mywrapper.find("#week1").hide(); $mywrapper.find("#week2").hide(); $mywrapper.find("#loader").show(); $mywrapper.find("table td").html(''); $mywrapper.find("table td").css("background-color",''); Frog.Model .api( 'timetables.getTimetable', { limit: 2, offset: offsetweeks, week_starts_on: 1, user_uuid: currentuser.uuid, target: [anyuuid] } ).done(function(thisResponse) { if (thisResponse.status.code === 0) { var data = thisResponse.data[0]; $.each(data.weeks, function(i,weeks) { $.each(weeks.days, function(p,days) { $.each(days.periods, function(q,periods) { $mywrapper.find("table tr td[id='"+periods.name+"']").html(periods.subject+'<BR />'+periods.teacher.displayname+'<BR />'+periods.room.slice(-3)); var bgcolor = stringToColour(periods.subject+periods.teacher.username); $mywrapper.find("table tr td[id='"+periods.name+"']").css("background-color", bgcolor); $mywrapper.find("table tr td[id='"+periods.name+"']").css("color", getForegroundColor(bgcolor)); }); }); }); $mywrapper.find('#searchResult').html('Timetable for '+data.user.displayname); $mywrapper.find("#loader").hide(); $mywrapper.find("#week1").show(); } }); }; var delay = (function(){ var timer = 0; return function(callback, ms){ clearTimeout (timer); timer = setTimeout(callback, ms); }; })(); var stringToColour = function(str) { var hash = 0; for (var i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var colour = '#'; for (var p = 0; p < 3; p++) { var value = (hash >> (p * 8)) & 0xFF; colour += ('00' + value.toString(16)).substr(-2); } return colour; }; var getForegroundColor = function(hexcolor) { var r = parseInt(hexcolor.substr(1,2),16); var g = parseInt(hexcolor.substr(3,2),16); var b = parseInt(hexcolor.substr(5,2),16); var yiq = ((r*299)+(g*587)+(b*114))/1000; return (yiq >= 128) ? 'black' : 'white'; }; $mywrapper.find('#wk1btn').click(function() { $mywrapper.find('#week1').hide(); $mywrapper.find('#week2').show(); }); $mywrapper.find("#wk2btn").click(function() { $mywrapper.find("#week1").show(); $mywrapper.find("#week2").hide(); }); $mywrapper.find("#toggleSearch").click(function(e) { $mywrapper.find(".frogui_components_search").toggle(); e.stopPropagation(); }); $mywrapper.find("#btn-clear").click(function() { $mywrapper.find('.search input').val(''); $mywrapper.find('.dropdown-menu').hide(); $mywrapper.find("ul.typeahead.dropdown-menu").empty(); }); $(this.body).click(function() { $mywrapper.find('.dropdown-menu').hide(); }); //this seems to be needed for IE to do anything right! $mywrapper.find('.search input').on('click', function(e) { e.stopPropagation(); }); $mywrapper.find('.search input').keypress(function(e) { if (e.which === 13) { e.preventDefault(); return false; } }); $mywrapper.find('ul.typeahead.dropdown-menu').on('click tap', 'li', function (e) { $mywrapper.find(".frogui_components_search").hide(); $mywrapper.find('.dropdown-menu').hide(); $mywrapper.find("ul.typeahead.dropdown-menu").empty(); $mywrapper.find('.search input').val(''); getTimetable($(this).data("value")); }); $mywrapper.find('.search input').keyup(function() { var s = $mywrapper.find('.search input').val(); if(s.length > 1) { delay(function(){ Frog.Model .api( 'textsearch.search', { profile: 'student,staff', find: s, type: 'user', include_associations: true, exclude_sources: ['temporary'] } ).done(function(thisResponse) { if (thisResponse.status.code === 0) { $mywrapper.find("ul.typeahead.dropdown-menu").empty(); var rusers = thisResponse.data.user; if (rusers.length) { $.each(rusers, function(i,users) { $mywrapper.find("ul.typeahead.dropdown-menu").append('<li data-value="'+users.uuid+'"><a href="#"><span title="'+users.displayname+'">'+users.profile.name+': '+users.displayname+'</span></a></li>'); }); $mywrapper.find('.dropdown-menu').show(); } else { $mywrapper.find('.dropdown-menu').hide(); } } }); }, 800 ); } }); //render the timetable grid var y; var b = 0; for (var i = 0; i < school_periods_in_day; i++) { var trsource = ''; trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>'; for (y = b; y < school_weeklength; y=y+school_periods_in_day) { trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>'; } trsource = trsource + '</tr>'; $mywrapper.find("#week1 table tbody").append(trsource); b++; } if (school_weeks === 2) { var b = 0; for (var i = 0; i < school_periods_in_day; i++) { var trsource = ''; trsource = trsource + '<tr><th>'+school_lesson_labels[b]+'</th>'; for (y = (b+school_weeklength); y < school_cyclelength; y=y+school_periods_in_day) { trsource = trsource + '<td id="' + school_period_labels[y] + '"></td>'; } trsource = trsource + '</tr>'; $mywrapper.find("#week2 table tbody").append(trsource); $mywrapper.find("#wk1btn").show(); b++; } } $mywrapper.find('#searchResult').html('Timetable for '+currentuser.displayname); getTimetable(); </script> Edited May 9, 2017 by pconkie 1 Link to comment Share on other sites More sharing options...
pconkie Posted May 11, 2017 Author Share Posted May 11, 2017 @Chris.Smith @Graham Quince OK - it's now a FrogCode Widget! Deployed on the alpha site. All seems to be working, but as your site doesn't have any timetable data in it the testing that can be done is limited! What do you think? I'd like to get rid of the preference for the lesson labels and either get this from the api or get the user to select the correct format for lesson labels. The api only sends the lesson labels for the lessons where a user has a lesson (if that makes sense) which wouldn't work. Already been asked if it can do room timetables as staff are often looking for a free room - I think the answer is no as the api doesn't seem to support room timetables? Thanks for your help with this. Paul 2 Link to comment Share on other sites More sharing options...
Graham Quince Posted May 11, 2017 Share Posted May 11, 2017 I'm in awe! This is absolutely brilliant, well done. If you're serious about wanting ideas for improvements. I know you mentioned pre-setting the colours? Using the accordion preference - (I'm at home, I'll post tomorrow) you could hide extra configuration stuff for the casual user. And have you thought about being able to define site links so that any timetabled lesson takes you to the department site? Link to comment Share on other sites More sharing options...
Graham Quince Posted May 12, 2017 Share Posted May 12, 2017 Adding this element to a preference type: accordion: 'settings.advanced' allows you to "hide" advanced settings in a drop down area, for example: prefs: { label: { type: 'text', label: 'This is a text field', defaultValue: '', accordion: 'settings.advanced' } }, 2 Link to comment Share on other sites More sharing options...
mobrien Posted June 29, 2017 Share Posted June 29, 2017 @pconkie I've just been having a play with this code and have got it to work!! (after some serious trial and error...) Brilliant stuff, though! In section 5 you describe how you generated the subject colours, based upon their names as they arrive. I've managed to amend the colour schemes by adjusting the 'p * 8' multiplier here: var stringToColour = function(str) { var hash = 0; for (var i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } var colour = '#'; for (var p = 0; p < 3; p++) { var value = (hash >> (p * 8)) & 0xFF; colour += ('00' + value.toString(16)).substr(-2); } return colour; }; My question is, would it be possible to pre-select a colour per subject? For example, we have a colour coded building, and it would be nice to match the subjects with their location colour. I know in you post you mentioned that you tried to avoid this, though. Thanks, Michael. Link to comment Share on other sites More sharing options...
pconkie Posted June 29, 2017 Author Share Posted June 29, 2017 Hi @mobrien setting this up with widget preferences would be ideal - this way we could use the frog colour pickers to set the subject colours. However, this would take some thinking about as we would need to set a colour for every possible subject that could come up and we would have to do this every time the widget was dragged into a page. So I think for now, if this is what you want, perhaps it's best just to write an alternative function for the one you pasted. Something like this might work... var subjectToColour = function(subject) { var colour = ""; switch(subject.toLowerCase()) { case "maths": case "economics": colour = "green"; break; case "english": case "english language": case "english literature": colour = "#FF0000"; break; default: colour = "black";} return colour; }; you can can use certain colour names, but you would be better off googling HTML colour codes as per the English example. You need to keep you subject names identical to what you use in your MIS. All in lower case. Look at how I've grouped some subjects together so that they have the same colour. You will need to copy from "case" to "break" and paste between the { } for additional subjects/colours. Once completed you would need to copy the new function into the code and replace all instances of "stringToColour" for the new "subjectToColour". Good luck! Link to comment Share on other sites More sharing options...
mobrien Posted July 2, 2017 Share Posted July 2, 2017 Hi @pconkie, Thank you for this! It has not quite worked, but nearly there. I've amended it for the two subjects in my own TT (science and physics) and double checked that this is the SIMs name. I also made the 'stringtocolour' changes to 'subjecttocolour', as suggested. I also took out all other subject references to make sure eliminate any other variables and gave it a go. However, the timetable is only returned in the default colour specification (black in this instance). Is there anything that I am missing?: var subjectToColour = function(subject) { var colour = ""; switch(subject.toLowerCase()) { case "science": colour = "#4daa50"; break; case "physics": colour = "#007681"; break; default: colour = "black"; } return colour; }; Thank you for your help with this. Link to comment Share on other sites More sharing options...
pconkie Posted July 2, 2017 Author Share Posted July 2, 2017 @mobrien Most likely explanation is - we are not sending the name of the subject into the function. This is why all lessons return the colour black - nothing can be matched. Do you know how to use the console? You can check what is being passed to the function (what is contained in the variable 'subject') by adding console.log(subject); to line 2 of the function. You can then open the console and have a look at what has been outputted. I have a feeling that it was advantageous when creating dynamic colours to make the string that was sent to the older function as long as possible. It is possible therefore that we are not sending the subject name alone into the function. I'll just check now...... Ok - find this line: var bgcolor = stringToColour(periods.subject+periods.teacher.username); You have presumably already changed it to: var bgcolor = subjectToColour(periods.subject+periods.teacher.username); Just one more small tweak so that it is: var bgcolor = subjectToColour(periods.subject); That should do it. Any further issues will be most likely to do with subject names in the function not matching SIMS. Hope this gets it working for you. Paul Link to comment Share on other sites More sharing options...
mobrien Posted July 3, 2017 Share Posted July 3, 2017 @pconkie You are quite simply: a genius! Brilliant, works a treat. Thank you very much. 2 Link to comment Share on other sites More sharing options...
Pete Foulkes Posted September 4, 2017 Share Posted September 4, 2017 @pconkie I've added this timetable to our platform and it looks amazing, thank you! Just one thing which i'm hoping you can help me with as i'm fairly new to coding, which bit of the code would I need to change to display the class code rather than the teacher name on the timetable? Many thanks, Pete Link to comment Share on other sites More sharing options...
pconkie Posted September 4, 2017 Author Share Posted September 4, 2017 Hi @Pete Foulkes Just one small change.... It's line 116 for me: Change: periods.teacher.displayname To: periods.group.name That should do it! 2 Link to comment Share on other sites More sharing options...
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now