/** * Show a confirm dialog * @param question the question displayed */ function confirmDelete(question) { return confirm(question); } /** * Format a date to mm/dd/yyyy hh:mm:ss format * @param date the date to format (timestamp) */ function formatDate(date) { var d = new Date(date * 1000); // note: .getMonth() returns the month from 0-11 return [ ('00' + (d.getMonth()+1)).slice(-2), ('00' + d.getDate()).slice(-2), d.getFullYear() ].join('/') + ' ' + [ ('00' + d.getHours()).slice(-2), ('00' + d.getMinutes()).slice(-2), ('00' + d.getSeconds()).slice(-2) ].join(':'); } /** * Format a date to mmm dd /yyyy hh:mm:ss format * @param date the date to format (timestamp) */ months = Array('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'); function formatDateLong(date) { var d = new Date(date * 1000); return [ months[d.getMonth()], ('00' + d.getDate()).slice(-2), d.getFullYear() ].join(' ') + ' ' + [ ('00' + d.getHours()).slice(-2), ('00' + d.getMinutes()).slice(-2), ('00' + d.getSeconds()).slice(-2) ].join(':'); } function nameMonth(monthNum) { months = Array('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'); } /** * Find out what kind of resource an URI is * @param uri the URI to check. If it start with a protocol (ebut not file://) return 'Live', else 'Recorded' */ function TypeofResource(uri) { var protocol = /([a-zA-Z]+):\/\//.exec(uri); if(protocol === null || (protocol[1] && protocol[1] === 'file')) { return 'Recorded'; }else{ return 'Live'; } } /** * convert a short limit name to a long one using the table above * @param name the short name of a limit */ function shortToLongLimit(name) { var i; for(i = 0; i < ltypes.length; i++) { if(name == ltypes[i][0]) { return ltypes[i][1]; } } return name; } /** * forse the server to save to the config file * @param callback function to call after the command is send */ function forceJSONSave(callback) { // build the object to send to the server var data = { 'authorize': { 'username': settings.credentials.username, 'password': (settings.credentials.authstring != "" ? MD5(MD5(settings.credentials.password) + settings.credentials.authstring) : "" ) }, 'save': 1 }; // make the XHR call $.ajax( { 'url': settings.server, 'data': { "command": JSON.stringify(data) }, 'dataType': 'jsonp', 'timeout': 10000, 'error': function(){}, 'success': function(){} }); } /** * retrieves data from the server ;) * note: does not authenticate first. Assumes user is logged in. * @param callback the function to call when the data has been retrieved. This callback has 1 parameter, the data retrieved. */ function getData(callback) { var data = { 'authorize': { 'username': settings.credentials.username, 'password': (settings.credentials.authstring != "" ? MD5(MD5(settings.credentials.password) + settings.credentials.authstring) : "" ) }, 'capabilities': {}, 'conversion': {'query': settings.settings.conversion.query} }; if (!settings.settings.conversion.encoders) { data.conversion.encoders = {}; } //console.log('sending data:',data); $.ajax( { 'url': settings.server, 'data': { "command": JSON.stringify(data) }, 'dataType': 'jsonp', 'timeout': 300000, 'error': function(){}, 'success': function(d) { //console.log('receiving data:',d); var ret = $.extend(true, { "streams": {}, "capabilities": {}, "statistics": {}, "conversion": {} }, d); //IE breaks if the console isn't opened, so keep commented when committing //console.log('[651] RECV', ret); if(callback) { callback(ret); } } }); } /** * retrieved the status and number of viewers from all streams * @param callback function that is called when the data is collected. Has one parameter, the data retrieved */ function getStreamsData(callback) { getData(function(data) { var streams = {}; // streamID: [status, numViewers]; var cnt = 0; for(var stream in data.streams) { streams[stream] = [data.streams[stream].online, 0, data.streams[stream].error]; cnt++; } if(cnt === 0) { return; // if there are no streams, don't collect data and just return } for(stream in data.statistics) { if(data.statistics[stream].curr) { for(var viewer in data.statistics[stream].curr) { streams[stream][1]++; } } } callback(streams); }); } /** * parses an url and returns the parts of it. * @return object containing the parts of the URL: protocol, host and port. */ function parseURL(url) { var pattern = /(https?)\:\/\/([^:\/]+)\:(\d+)?/i; var retobj = {protocol: '', host: '', port: ''}; var results = url.match(pattern); if(results != null) { retobj.protocol = results[1]; retobj.host = results[2]; retobj.port = results[3]; } return retobj; } /** * go figure. * @return true if there is a HTTP connector... and false if there isn't. */ function isThereAHTTPConnector() { var i, len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); for(i = 0; i < len; i++) { if((settings.settings.config.protocols[i].connector == 'HTTP.exe') || (settings.settings.config.protocols[i].connector == 'HTTP')) { return true; } } return false; } /** * retrieve port of the http connector * @return the port number */ function getHTTPControllerPort() { var i, len = (settings.settings.config.protocols ? settings.settings.config.protocols.length : 0); for(i = 0; i < len; i++) { if((settings.settings.config.protocols[i].connector == 'HTTP.exe') || (settings.settings.config.protocols[i].connector == 'HTTP')) { if ((settings.settings.config.protocols[i].port == 0) || (settings.settings.config.protocols[i].port == '') || (!settings.settings.config.protocols[i].port)){ return 8080; } else { return settings.settings.config.protocols[i].port; } } } return 0; } /** * retrieves the stream status (online and total number of streams) and viewer info (total number of viewers). * @param callback function that is called when data is retrieved. Has one parameter, the retrieved data. */ function getStatData(callback) { getData(function(data) { var svr, viewer, ret, numstr = 0, numvwr = 0, numtotstr = 0; for(svr in data.statistics) { if(data.statistics[svr].curr) { for(viewer in data.statistics[svr].curr) { numvwr++; } } } for(svr in data.streams) { numtotstr++; if(data.streams[svr].online && data.streams[svr].online != 0 ) { numstr++; } } ret = {streams: [numstr, numtotstr], viewers: numvwr}; callback(ret); }); } /** * Connect to the server and retrieve the data * @param callback the function to call when connected. Has one parameter, an optional error string. */ function loadSettings(callback) { // display 'loading, please wait' while retrieving data $('body').append( $('<div>').attr('id', 'shield').text('Loading, please wait...') ); var errorstr = '', data = $.extend(settings.settings, { 'authorize': { 'username': settings.credentials.username, 'password': (settings.credentials.authstring != "" ? MD5(MD5(settings.credentials.password) + settings.credentials.authstring) : "" ) } }); delete data.log; // don't send the logs back to the server delete data.statistics; // same goes for the stats //IE breaks if the console isn't opened, so keep commented when committing //console.log('[763] SEND', data); $.ajax( { 'url': settings.server, 'data': { "command": JSON.stringify(data) }, 'dataType': 'jsonp', 'timeout': 5000, 'error': function(jqXHR,textStatus,errorThrown) { showTab('disconnect'); $('#shield').remove(); // remove loading display alert('O dear! An error occurred while attempting to communicatie with the MistServer.\n\n'+textStatus+'\n'+errorThrown); }, 'success': function(d) { $('#shield').remove(); // remove loading display //IE breaks if the console isn't opened, so keep commented when committing //console.log('[785] RECV', d); if(d && d['authorize'] && d['authorize']['challenge']) { if (settings.credentials.authstring != d['authorize']['challenge']) { settings.credentials.authstring = d['authorize']['challenge']; loadSettings(callback); return; }else{ errorstr = 'wrong credentials'; } }else{ settings.settings = $.extend(true, { "config": { "host": "", "limits": [], "name": "", "protocols": [], "status": "", "version": "" }, "streams": {}, "capabilities": {}, "log": {}, "statistics": {}, "conversion": { "status" : "" } }, d) if (settings.settings.LTS == 1) { $('.LTSonly').show(); } else { $('.LTSonly').hide(); } } if(callback) { callback(errorstr); } } }); } /** * Sets the page's header text (loging in, connected, disconnected), title and pretty colors (!) * @param state the state of the header. Possible are 'logingin', 'disconnected' or 'connected'. */ function setHeaderState(state) { var text, cname, title; switch(state) { case 'logingin': text = 'connecting...'; cname = 'loggingin'; title = 'connecting to ' + settings.server; break; case 'disconnected': text = 'disconnected'; cname = 'disconnected'; title = 'disconnected'; break; case 'connected': text = 'connected'; cname = 'connected'; title = 'connected to ' + settings.server; break; } document.title = 'Mistserver Manager - ' + title; $('#header-connection').attr('class', cname); $('#header-connection').text(text); $('#header-host').text(settings.server.replace('HTTP://', '')); } /** * Formats the status property to a string (with colors!) * @param status, the status property of a stream */ function formatStatus(status,text) { if(status == undefined) { return "<span>Unknown, checking...</span>"; } if(text == undefined) { switch(status) { case -1: return "<span>Unknown, checking...</span>"; break; case 0: return "<span class='red'>Unavailable</span>"; break; case 1: return "<span class='green'>Active</span>"; break; case 2: return "<span class='orange'>Inactive</span>"; break; default: return "<span>"+status+"</span>"; break; } } else { switch(status) { case -1: return "<span>Unknown, checking...</span>"; break; case 0: return "<span class='red'>"+text+"</span>"; break; case 1: return "<span class='green'>"+text+"</span>"; break; case 2: return "<span class='orange'>"+text+"</span>"; break; default: return "<span>"+text+"</span>"; break; } } } /** * Build a HTML limit row * @param l the limit data * @return a (jQuery'd) HTML Element */ function BuildLimitRow(l) { l['type'] = l['type'] || 'soft'; l['name'] = l['name'] || 'kbps_max'; l['val'] = l['val'] || 0; var i, j, lv, db, output = $("<tr>").addClass("limits_row"), selects = [$("<select class=\"limits_type\">"), $("<select class=\"limits_name\">")], options = [ [ ['soft', 'Softlimit', 'Allow this limit to be passed. (Usefull for alerts)'], ['hard', 'Hardlimit','Do not allow this limit to be passed.'] ], [ ['kbps_max', 'Current bandwidth', 'In bytes/s. Refuses new connections after current bandwidth limit is reached.'], ['users', 'Concurrent users','Maximum concurrent users.'], ['geo', 'Geolimited', 'Either a blacklist or whitelist containing country codes.'], ['host', 'Hostlimited', 'Either a blacklist or whitelist containing hosts seperated by spaces.'] ], ['type', 'name'] ]; /* Limits that are currently not in use but may return later: ['kb_total', 'Total bandwidth','Total bandwidth in bytes.'], ['duration', 'Duration', 'Maximum duration a user may be connected in seconds.'], ['str_kbps_min', 'Minimum bitrate','Minimum bitrate in bytes/s.'], ['str_kbps_max', 'Maximum bitrate','Maximum bitrate in bytes/s.'] */ for(i = 0; i < 2; i++) { for(j = 0; j < options[i].length; j++) { selects[i].append( $('<option>').val(options[i][j][0]).text(options[i][j][1]).data('desc',options[i][j][2]) ); } selects[i].val(l[options[2][i]]); output.append($('<td>').html(selects[i])); } lv = $('<td>').addClass('limit_input_container'); BuildLimitRowInput(lv,l); appliesto = $('<td>'); var appliesselect = $('<select>').attr('class', 'new-limit-appliesto').append( $('<option>').attr('value', 'server').text('Server') ); for(var strm in settings.settings.streams) { appliesselect.append( $('<option>').attr('value', strm).text(settings.settings.streams[strm].name) ); } if (l.appliesto) { appliesselect.val(l.appliesto); } appliesto.append(appliesselect); db = $('<td>').html( $('<button>').addClass('limit_delete_button').text('Delete').click(function(){$(this).parent().parent().remove();}) ); output.append(lv).append(appliesto).append(db); return output; } /** * Build a HTML limit row input field * @param target where the input field should be inserted * @param l the limit data */ function BuildLimitRowInput(target,l) { if (!l.val) { l.val=0; } //console.log(l); switch (l.name) { case 'geo': var countrylist = [['AF','Afghanistan'],['AX','Åland Islands'],['AL','Albania'],['DZ','Algeria'],['AS','American Samoa'],['AD','Andorra'],['AO','Angola'],['AI','Anguilla'],['AQ','Antarctica'],['AG','Antigua and Barbuda'],['AR','Argentina'],['AM','Armenia'],['AW','Aruba'],['AU','Australia'],['AT','Austria'],['AZ','Azerbaijan'],['BS','Bahamas'],['BH','Bahrain'],['BD','Bangladesh'],['BB','Barbados'],['BY','Belarus'],['BE','Belgium'],['BZ','Belize'],['BJ','Benin'],['BM','Bermuda'],['BT','Bhutan'],['BO','Bolivia, Plurinational State of'],['BQ','Bonaire, Sint Eustatius and Saba'],['BA','Bosnia and Herzegovina'],['BW','Botswana'],['BV','Bouvet Island'],['BR','Brazil'],['IO','British Indian Ocean Territory'],['BN','Brunei Darussalam'],['BG','Bulgaria'],['BF','Burkina Faso'],['BI','Burundi'],['KH','Cambodia'],['CM','Cameroon'],['CA','Canada'],['CV','Cape Verde'],['KY','Cayman Islands'],['CF','Central African Republic'],['TD','Chad'],['CL','Chile'],['CN','China'],['CX','Christmas Island'],['CC','Cocos (Keeling) Islands'],['CO','Colombia'],['KM','Comoros'],['CG','Congo'],['CD','Congo, the Democratic Republic of the'],['CK','Cook Islands'],['CR','Costa Rica'],['CI','Côte d\'Ivoire'],['HR','Croatia'],['CU','Cuba'],['CW','Curaçao'],['CY','Cyprus'],['CZ','Czech Republic'],['DK','Denmark'],['DJ','Djibouti'],['DM','Dominica'],['DO','Dominican Republic'],['EC','Ecuador'],['EG','Egypt'],['SV','El Salvador'],['GQ','Equatorial Guinea'],['ER','Eritrea'],['EE','Estonia'],['ET','Ethiopia'],['FK','Falkland Islands (Malvinas)'],['FO','Faroe Islands'],['FJ','Fiji'],['FI','Finland'],['FR','France'],['GF','French Guiana'],['PF','French Polynesia'],['TF','French Southern Territories'],['GA','Gabon'],['GM','Gambia'],['GE','Georgia'],['DE','Germany'],['GH','Ghana'],['GI','Gibraltar'],['GR','Greece'],['GL','Greenland'],['GD','Grenada'],['GP','Guadeloupe'],['GU','Guam'],['GT','Guatemala'],['GG','Guernsey'],['GN','Guinea'],['GW','Guinea-Bissau'],['GY','Guyana'],['HT','Haiti'],['HM','Heard Island and McDonald Islands'],['VA','Holy See (Vatican City State)'],['HN','Honduras'],['HK','Hong Kong'],['HU','Hungary'],['IS','Iceland'],['IN','India'],['ID','Indonesia'],['IR','Iran, Islamic Republic of'],['IQ','Iraq'],['IE','Ireland'],['IM','Isle of Man'],['IL','Israel'],['IT','Italy'],['JM','Jamaica'],['JP','Japan'],['JE','Jersey'],['JO','Jordan'],['KZ','Kazakhstan'],['KE','Kenya'],['KI','Kiribati'],['KP','Korea, Democratic People\'s Republic of'],['KR','Korea, Republic of'],['KW','Kuwait'],['KG','Kyrgyzstan'],['LA','Lao People\'s Democratic Republic'],['LV','Latvia'],['LB','Lebanon'],['LS','Lesotho'],['LR','Liberia'],['LY','Libya'],['LI','Liechtenstein'],['LT','Lithuania'],['LU','Luxembourg'],['MO','Macao'],['MK','Macedonia, the former Yugoslav Republic of'],['MG','Madagascar'],['MW','Malawi'],['MY','Malaysia'],['MV','Maldives'],['ML','Mali'],['MT','Malta'],['MH','Marshall Islands'],['MQ','Martinique'],['MR','Mauritania'],['MU','Mauritius'],['YT','Mayotte'],['MX','Mexico'],['FM','Micronesia, Federated States of'],['MD','Moldova, Republic of'],['MC','Monaco'],['MN','Mongolia'],['ME','Montenegro'],['MS','Montserrat'],['MA','Morocco'],['MZ','Mozambique'],['MM','Myanmar'],['NA','Namibia'],['NR','Nauru'],['NP','Nepal'],['NL','Netherlands'],['NC','New Caledonia'],['NZ','New Zealand'],['NI','Nicaragua'],['NE','Niger'],['NG','Nigeria'],['NU','Niue'],['NF','Norfolk Island'],['MP','Northern Mariana Islands'],['NO','Norway'],['OM','Oman'],['PK','Pakistan'],['PW','Palau'],['PS','Palestine, State of'],['PA','Panama'],['PG','Papua New Guinea'],['PY','Paraguay'],['PE','Peru'],['PH','Philippines'],['PN','Pitcairn'],['PL','Poland'],['PT','Portugal'],['PR','Puerto Rico'],['QA','Qatar'],['RE','Réunion'],['RO','Romania'],['RU','Russian Federation'],['RW','Rwanda'],['BL','Saint Barthélemy'],['SH','Saint Helena, Ascension and Tristan da Cunha'],['KN','Saint Kitts and Nevis'],['LC','Saint Lucia'],['MF','Saint Martin (French part)'],['PM','Saint Pierre and Miquelon'],['VC','Saint Vincent and the Grenadines'],['WS','Samoa'],['SM','San Marino'],['ST','Sao Tome and Principe'],['SA','Saudi Arabia'],['SN','Senegal'],['RS','Serbia'],['SC','Seychelles'],['SL','Sierra Leone'],['SG','Singapore'],['SX','Sint Maarten (Dutch part)'],['SK','Slovakia'],['SI','Slovenia'],['SB','Solomon Islands'],['SO','Somalia'],['ZA','South Africa'],['GS','South Georgia and the South Sandwich Islands'],['SS','South Sudan'],['ES','Spain'],['LK','Sri Lanka'],['SD','Sudan'],['SR','Suriname'],['SJ','Svalbard and Jan Mayen'],['SZ','Swaziland'],['SE','Sweden'],['CH','Switzerland'],['SY','Syrian Arab Republic'],['TW','Taiwan, Province of China'],['TJ','Tajikistan'],['TZ','Tanzania, United Republic of'],['TH','Thailand'],['TL','Timor-Leste'],['TG','Togo'],['TK','Tokelau'],['TO','Tonga'],['TT','Trinidad and Tobago'],['TN','Tunisia'],['TR','Turkey'],['TM','Turkmenistan'],['TC','Turks and Caicos Islands'],['TV','Tuvalu'],['UG','Uganda'],['UA','Ukraine'],['AE','United Arab Emirates'],['GB','United Kingdom'],['US','United States'],['UM','United States Minor Outlying Islands'],['UY','Uruguay'],['UZ','Uzbekistan'],['VU','Vanuatu'],['VE','Venezuela, Bolivarian Republic of'],['VN','Viet Nam'],['VG','Virgin Islands, British'],['VI','Virgin Islands, U.S.'],['WF','Wallis and Futuna'],['EH','Western Sahara'],['YE','Yemen'],['ZM','Zambia'],['ZW','Zimbabwe']]; var entrylist = l.val.toString(); //build the template country selectbox var geoselect = $('<select>').addClass('limit_listentry'); geoselect.append( $('<option>').val('').text('[Select a country]') ); for (i in countrylist) { geoselect.append( $('<option>').val(countrylist[i][0]).html(countrylist[i][1]) ); } //build the blacklist or whitelist selectbox var selectbox = $('<select>').addClass('limit_listtype'); selectbox.append($('<option>').val('-').text('Blacklist')); selectbox.append($('<option>').val('+').text('Whitelist')); if (entrylist.charAt(0) == '+') { selectbox.val('+'); } else { selectbox.val('-'); } var inputfields = $('<table>').addClass('limit_inputfields').append( $('<tbody>').append( $('<tr>').append( $('<td>').append( selectbox ) ) ) ); entrylist = entrylist.substring(1).split(' '); var firstfieldused = false; //insert selectboxes with currently set geolimits for (i in entrylist) { if (entrylist[i] == "") { continue; } //make a new country selectbox based on the template var newgeoselect = geoselect.clone(); newgeoselect.val(entrylist[i]); if (firstfieldused) { inputfields.children('tbody').append( $('<tr>').append( $('<td>') ).append( $('<td>').html(newgeoselect) ) ); } else { inputfields.children('tbody').children('tr').append( $('<td>').html(newgeoselect) ); firstfieldused = true; } } if (!firstfieldused) { inputfields.children('tbody').children('tr').append( $('<td>').html(geoselect.clone()) ); } //add a button to insert country selectboxes var addbutton = $('<button>').text('Add country selectbox').click(function(){ $(this).parent('td').parent('tr').before( $('<tr>').append( $('<td>') ).append( $('<td>').html(geoselect.clone()) ) ) }); inputfields.children('tbody').append( $('<tr>').append( $('<td>') ).append( $('<td>').html(addbutton) ) ); target.html(inputfields); break; case 'host': case 'time': var entrylist = l.val.toString(); //build the blacklist or whitelist selectbox var selectbox = $('<select>').addClass('limit_listtype'); selectbox.append($('<option>').val('-').text('Blacklist')); selectbox.append($('<option>').val('+').text('Whitelist')); if (entrylist.charAt(0) == '+') { selectbox.val('+'); } else { selectbox.val('-'); } target.html( $('<span>').addClass('limit_inputfields').append( selectbox ) ); entrylist = entrylist.substring(1); //add inputfield target.children('.limit_inputfields').append( $('<input>').attr('type','text').addClass('limit_listentry').val(entrylist) ); break; default: target.html($("<input type=\"text\" class=\"limits_val\">").val(l["val"])); break; } } /** * At the logs page, refreshes data for the logs */ function getLogsdata(){ getData(function(data){ settings.settings.log = data.log; $('#page table').remove(); $('#page').append(buildLogsTable()); }); } /** * At the logs page, builds a table to display the logs */ function buildLogsTable(){ $table = $('<table>'); $table.html("<thead><th>Date<span class='theadinfo'>(MM/DD/YYYY)</span></th><th>Type</th><th>Message</th></thead>"); $tbody = $('<tbody>'); if(!settings.settings.log) { return; // no logs, so just bail } var i, cur, $tr, logs = settings.settings.log, len = logs.length; if(len >= 2 && settings.settings.log[0][0] < settings.settings.log[len - 1][0]) { logs.reverse(); } $tbody.html(''); for(i = 0; i < len; i++) { cur = settings.settings.log[i]; $tr = $('<tr>').append( $('<td>').text(formatDate(cur[0])) ).append( $('<td>').text(cur[1]) ).append( $('<td>').text(cur[2]) ); $tbody.append($tr); } $table.append($tbody); return $table; } /** * At the conversion page, does a directory query to obtain input files @params: - query: the directory query */ function conversionDirQuery(query) { if (!settings.settings.conversion.query) { settings.settings.conversion.query = {}; } settings.settings.conversion.query.path = query; $('#conv-edit-dir').val(query); $('#conversiondirquery-status').text(' Searching for input files..'); getData(function(data){ var c = data.conversion.query; var j = 0; var dir = $('#conv-edit-dir').val(); if (dir.substr(dir.length -1) != '/') { dir += '/'; } delete settings.settings.conversion.query; $('#conv-edit-input').html(''); for (var i in c) { var ext = i.split('.'); ext = ext[ext.length-1]; if ((c[i] == null) && (ext != 'dtsc')) { $('#conv-edit-input').append( $('<option>').val(dir+i).text(i) ); j++; } } if (j == 0) { $('#conv-edit-input').append( $('<option>').val('').text('- No suitable files found -') ) } conversionSelectInput($('#conv-edit-input').val()); $('#conversiondirquery-status').text(''); }); } function conversionSelectInput(filename) { var filename = filename.split('.'); filename.pop(); filename = filename.join('.'); filename += '.dtsc'; $('#conv-edit-output').val(filename); } /** * Tooltip creator - creates a tooltip near the cursor @params: - position: the object returned by the hover or click event - appendto: the jquery element to which the tooltip should be appended - contents: the content of the tooltip */ function showTooltip(position,appendto,contents,debug) { if (position == null) { position = { pageX: 0, pageY:0 } } if (appendto == null) { appendto = $('body'); } if (contents == null) { contents = 'Empty'; } var css = {}; if (position.pageX > window.innerWidth / 2) { css.right = window.innerWidth - position.pageX + 10; } else { css.left = position.pageX - appendto.offset().left + 10; } if (position.pageY > window.innerHeight / 2) { css.bottom = window.innerHeight - position.pageY + 10; } else { css.top = position.pageY - appendto.offset().top + 10; } $("<div>").attr('id','tooltip').html( contents ).css(css).appendTo(appendto).fadeIn("fast"); if (debug) { console.log('Tooltip.. position:',position,'appendto:',appendto,'contents:',contents,'css:',css); } } function removeTooltip() { $('#tooltip').remove(); }