I'm trying to translate an SSRS report. Using this example link. The general report is working perfectly(Inside the blue square in the picture below). But i have an issues with finding have to translate the red squares in the picture below. Is it not possible to translate that part(The text in the red square).
Unfortunately u can't translate parameters prompt and the 'view report' part as well. Those are not the part of the reports so we can't translate them.
Necromancing.
You can do this, even if the user uses another language.
The secret is passing a parameter for the desired language to the ssrs page.
Then, you can create a http-module, that gets the language from the URL/referrer (note: HTTP misspells referrer as referer)
Example code:
https://github.com/ststeiger/SSRS-Localizer/blob/master/libRequestLanguageChanger/RequestLanguageChanger.cs
(this code also sets the P3P-header, as this is necessary for the SSRS-cookies in IE if the page is iframed from a page on a different domain)
This sets SSRS into the correct localization mode if the browser language does not correspond to the user language.
If you want, you can, in addition to that, change ReportViewer, usually located in
C:\Program Files\Microsoft SQL Server\MSRS<whatever>\Reporting Services\ReportServer\Pages\ReportViewer.aspx
and translate the report's paramters's labels using JavaScript.
Note:
if you only change the culture in ReportViewer.aspx in method
protected override void InitializeCulture()
then the datepickers won't work.
Therefore, you need to run it as http-module (and if it loads resources, such as jquery-ui, you need to get the language from the refer[r]er).
Here an example ReportViewer.aspx, where language is passed in url-parameter in_sprache, with parameter labels set in the format Text_DE / Text_FR / Text_IT / Text_EN (/ is not necessarely the best separation character, use a character that you don't need)
<%# Register TagPrefix="RS" Namespace="Microsoft.ReportingServices.WebServer" Assembly="ReportingServicesWebServer" %>
<%# Page Language="C#" AutoEventWireup="true" Inherits="Microsoft.ReportingServices.WebServer.ReportViewerPage" EnableEventValidation="false" %>
<asp:literal runat="server" id="docType"></asp:literal>
<html>
<!-- Here InitializeCulture -->
<head id="headID" runat="server">
<asp:literal runat="server" id="httpEquiv"></asp:literal>
<title><%= GetPageTitle() %></title>
<style type="text/css" media="all">
html
{
scrollbar-arrow-color: #FFA500;
scrollbar-base-color: black;
scrollbar-dark-shadow-color: #aaaaaa;
scrollbar-track-color: black;
scrollbar-face-color: #3d3d3d;
scrollbar-shadow-color: gray;
scrollbar-highlight-color: silver;
scrollbar-3d-light-color: black;
}
::-webkit-scrollbar{width: 13px; height: 13px}
::-webkit-scrollbar:hover{height: 18px}
::-webkit-scrollbar-button:start:decrement,
::-webkit-scrollbar-button:end:increment
{
height: 15px;
width: 13px;
display: block;
background: #101211;
background-repeat: no-repeat;
}
::-webkit-scrollbar-button:horizontal:decrement
{
#background-image: url(Scrolling/black/horizontal-decrement-arrow.png);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAIAAABLMMCEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIhJREFUeNpiFBASZIABXj5WHQPB44deMUH4TEyMviGyU5dYXrnwHshlAWIDU6HsUk15JZ4F025//vQbJKpnJNQ8wYidnRnIuX3jE0Qr88cPDHu3PRMQYldU4X1498uVCx9AohycnF+//Dmy7+Wpo288A2TOnXz7/ftfRmQ3AIG6Nv/Nqx8BAgwAwakucAZmLk0AAAAASUVORK5CYII=');
background-position: 4px 3px;
}
::-webkit-scrollbar-button:horizontal:increment
{
#background-image: url(Scrolling/black/horizontal-increment-arrow.png);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAIAAABLMMCEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAH5JREFUeNpiFBASVNfmv3n1IwMSYALi929/dkw1AcrBRZk5ODm/fvkjJsFR1aYvLcd969pHIJcJInn7xicg6eQhOXetjY6BIAtEVFWDD0g+vPdleu+NKxfeg0R5+Vg9AmSA/I0rH/379x8owigkImRqJXLt0ofPn37DbQMIMAA2Li0My8uNagAAAABJRU5ErkJggg==');
background-position: 3px 3px;
}
::-webkit-scrollbar-button:vertical:decrement
{
#background-image: url(Scrolling/black/vertical-decrement-arrow.png);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAIAAABLMMCEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHFJREFUeNpiFBASZAADPSOhS+feQdhMEEpEjKN5gpGRuTCKaEqeGicXc2q+OhMTI1RUx0DQyUMSyFBW4/UOkgEyWICSCVkqS2bfvX/ns7wST3C0woFdLxiNLeQ+fvj95tUPiFHcPCxAaxnhbkAGAAEGAAxaGY5HzMiTAAAAAElFTkSuQmCC');
background-position: 3px 4px;
}
::-webkit-scrollbar-button:vertical:increment
{
#background-image: url(Scrolling/black/vertical-increment-arrow.png);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAIAAABLMMCEAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAH9JREFUeNpiFBASZMAATJZ2Ytw8LHC+kDC7ogov888fzFMWWXJxs/Dwsto5i4clKK5Z/IDl86ffa5c+yK3QgqgtSjn1799/JiBr67ond299BjL27Xh+5cJ7kLlADJScN+XW929/50y6BdHBzMHJCaSePf527eKH+3c+Q0QBAgwAkHcvV72C85gAAAAASUVORK5CYII=');
background-position: 3px 4px;
}
::-webkit-scrollbar-button:horizontal:decrement:active
{
#background-image: url(Scrolling/black/horizontal-decrement-arrow-active.png);
}
::-webkit-scrollbar-button:horizontal:increment:active
{
#background-image: url(Scrolling/black/horizontal-increment-arrow-active.png);
}
::-webkit-scrollbar-button:vertical:decrement:active
{
#background-image: url(Scrolling/black/vertical-decrement-arrow-active.png);
}
::-webkit-scrollbar-button:vertical:increment:active
{
#background-image: url(Scrolling/black/vertical-increment-arrow-active.png);
}
::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); border-radius: 10px; }
::-webkit-scrollbar-track-piece{background-color: #151716;}
::-webkit-scrollbar-thumb:vertical
{
height: 50px;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, #4d4d4d), color-stop(100%, #333333));
border: 1px solid #0d0d0d;
border-top: 1px solid #666666;
border-left: 1px solid #666666;
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
}
::-webkit-scrollbar-thumb:horizontal
{
width: 50px;
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #4d4d4d), color-stop(100%, #333333));
border: 1px solid #1f1f1f;
border-top: 1px solid #666666;
border-left: 1px solid #666666;
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);
}
::-webkit-scrollbar-corner{background-color: #3D3D3D;}
</style>
</head>
<body style="margin: 0px; overflow: auto">
<form style="width:100%;height:100%" runat="server" ID="ReportViewerForm">
<asp:ScriptManager ID="AjaxScriptManager" AsyncPostBackTimeout="0" runat="server" />
<table cellspacing="0" cellpadding="0" width="100%" height="100%"><tr height="100%"><td width="100%">
<RS:ReportViewerHost ID="ReportViewerControl" PageCountMode="Actual" runat="server" />
</td></tr></table>
</form>
<script language="javascript" type="text/javascript">
// =============================== COR-Code =================================
// Avoid `console` errors in browsers that lack a console.
(function () {
var method;
var noop = function () { };
var methods = [
'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
'timeStamp', 'trace', 'warn'
];
var length = methods.length;
var console = (window.console = window.console || {});
while (length--) {
method = methods[length];
// Only stub undefined methods.
if (!console[method]) {
console[method] = noop;
}
}
}());
// ============================== End-COR-Code ==============================
Sys.WebForms.PageRequestManager.prototype._destroyTree = function(element)
{
var allnodes = element.getElementsByTagName('*'), length = allnodes.length;
var nodes = new Array(length);
for (var k = 0; k < length; k++)
{
nodes[k] = allnodes[k];
} // Next k
for (var j = 0, l = nodes.length; j < l; j++)
{
var node = nodes[j];
if (node.nodeType === 1)
{
if (node.dispose && typeof (node.dispose) === "function")
{
node.dispose();
}
else if (node.control && typeof (node.control.dispose) === "function")
{
node.control.dispose();
}
var behaviors = node._behaviors;
if (behaviors)
{
behaviors = Array.apply(null, behaviors);
for (var k = behaviors.length - 1; k >= 0; k--)
{
behaviors[k].dispose();
}
} // End if (behaviors)
} // End if (node.nodeType === 1)
} // Next j
};
// ============================== COR-Code ==============================
Array.prototype.contains = function (obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
}; // End Function contains
// Read a page's GET URL variables and return them as an associative array.
function getUrlVars(urlHref)
{
var vars = [], hash;
var hashes = urlHref.slice(urlHref.indexOf('?') + 1).split('&');
var i;
for (i = 0; i < hashes.length; i++)
{
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
} // Next i
return vars;
} // End Function getUrlVars
function initLanguage()
{
var language = null;
var StyleSheetSet = null;
var BrowserLanguage = <%= System.Web.HttpContext.Current.Request.UserLanguages != null ? "\"" + System.Convert.ToString(System.Web.HttpContext.Current.Request.UserLanguages[0]) + "\"" : "null" %>;
if(BrowserLanguage == null)
BrowserLanguage = window.navigator.userLanguage || window.navigator.language;
if(BrowserLanguage != null)
BrowserLanguage = BrowserLanguage.substr(0,2).toLowerCase();
var dictParameters = getUrlVars(this.location.href);
if (dictParameters != null && dictParameters.contains("rc:Stylesheet"))
StyleSheetSet = true;
if (dictParameters != null && dictParameters.contains("in_sprache"))
language = dictParameters["in_sprache"];
if(language == null)
language = BrowserLanguage;
if(language == null)
language = "de";
language = language.toLowerCase();
return language;
} // End function initLanguage
function TranslateParameterPrompts(iLanguageIndex)
{
var eles = document.getElementsByTagName("table");
var strParamTableId = "ParametersGridReportViewerControl";
var tblParameters = null;
var ParamLabels = null;
for(var j = 0; j < eles.length; ++j)
{
// console.log(eles[j]);
if(eles[j] != null && eles[j].id != null)
{
if(eles[j].id.slice(0, strParamTableId.length) == strParamTableId) // if startswith str
{
// console.log(eles[j].id);
tblParameters = eles[j];
break;
}
// else console.log(eles[j].id);
} // End if(eles[j] != null && eles[j].id != null)
} // Next j
if(tblParameters != null)
ParamLabels = tblParameters.getElementsByTagName("span");
// var ParamLabels = document.querySelectorAll("table[id^='ParametersGridReportViewerControl'] span");
if(ParamLabels != null)
{
for(var i = 0; i < ParamLabels.length; ++i)
{
var strText = ParamLabels[i].innerHTML;
if (strText != null && strText.indexOf('/') != -1 && strText.indexOf('<input') == -1 )
{
strText = strText.split('/');
if (iLanguageIndex < strText.length)
strText = strText[iLanguageIndex];
else
{
if(strText.length > 0)
strText = strText[0];
}
ParamLabels[i].innerHTML = strText;
} // End if (strText != null && strText.indexOf('/') != -1)
} // Next i
} // End if(ParamLabels != null)
}
function fixReportingServices(container)
{
var language = initLanguage();
switch (language)
{
case "fr":
iLanguageIndex = 1;
break;
case "it":
iLanguageIndex = 2;
break;
case "en":
iLanguageIndex = 3;
break;
default: // "DE"
iLanguageIndex = 0;
} // End Switch
TranslateParameterPrompts(iLanguageIndex);
}
// needed when AsyncEnabled=true.
Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function () { fixReportingServices('rpt-container'); });
</script>
</body>
</html>
(Note: this is for SSRS 2016 - not necessarely backward or forward compatible, but you get the idea)
Related
I have created a user directory web app concept that pulls user images, names, and email addresses within G-Suite. I'm generating a Google Sheet with the data and displaying that data through an HTML table. All the data pulls over into the table cells as intended, including the thumbnails on my end. However, when another admin uses the application, the thumbnails show up as the default "head and shoulders" image. I have all my permissions set so anyone in the domain can use it, so I'm pretty sure it's not an issue with permissions on the application or the sheet. If anyone has any incite as to why the user.thumbnailPhotoUrl is not giving the user's images it would be very much appreciated. It might also be worth noting that I added all the profile pictures myself through the admin panel. My supervisor updated his profile picture and was only able to see his own.
Here is the code I am using:
code.gs
function doGet(){
return HtmlService.createHtmlOutputFromFile("table3");
}
function getDomainUsersList() {
var users = [];
var options = {
domain: "my_domain", // Google Apps domain name
customer: "my_customer",
maxResults: 100,
projection: "basic", // Fetch basic details of users
viewType: "domain_public",
orderBy: "email" // Sort results by users
}
do {
var response = AdminDirectory.Users.list(options);
response.users.forEach(function(user) {
users.push([user.name.fullName, user.primaryEmail, user.thumbnailPhotoUrl]);
});
// For domains with many users, the results are paged
if (response.nextPageToken) {
options.pageToken = response.nextPageToken;
}
} while (response.nextPageToken);
// Insert data in a spreadsheet
var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1nzRcC8ChbY2C0wjTY_hC0txkYMphMofvxyHws86syfM/edit#gid=0");
var sheet = ss.getSheetByName("Users") || ss.insertSheet("Users", 1);
sheet.getRange(1,1,users.length, users[0].length).setValues(users);
var data = sheet.getRange(1,1,sheet.getLastRow()-1,3).getValues();
return data;
}
/**function getData() {
var ss = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1nzRcC8ChbY2C0wjTY_hC0txkYMphMofvxyHws86syfM/edit#gid=0");
var sheet = ss.getSheetByName("Users") || ss.insertSheet("Users", 1);
var data = sheet.getRange(1,1,sheet.getLastRow()-1,3).getValues();
return data;
table3.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
* {
box-sizing: border-box;
}
#myInput {
background-image: url('/css/searchicon.png');
background-position: 10px 10px;
background-repeat: no-repeat;
width: 100%;
font-size: 16px;
padding: 12px 20px 12px 40px;
border: 1px solid #ddd;
margin-bottom: 12px;
}
#myTable {
border-color: blue;
border-collapse: collapse;
width: 100%;
border: 1px solid #ddd;
font-size: 18px;
}
#myTable th, #myTable td {
text-align: left;
padding: 12px;
}
#myTable tr {
border-bottom: 1px solid #ddd;
}
#myTable tr.header, #myTable tr:hover {
background-color: #f1f1f1;
}
</style>
</head>
<body>
<h2>Employee Directory</h2>
<input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.." title="Type in a name">
<div style="overflow-x:auto;">
<table id="myTable">
<thead>
<tr>
<th id="header">Picture</th>
<th id="header" onclick="sortTable(1)">Name</th>
<th id="header" onclick="sortTable(2)">Email</th>
</tr>
</thead>
<tbody id="table-body">
</tbody>
</table>
</div>
<script>
function myFunction() {
var input, filter, table, tr, td, i, txtValue;
input = document.getElementById("myInput");
filter = input.value.toUpperCase();
table = document.getElementById("myTable");
tr = table.getElementsByTagName("tr");
for (i = 0; i < tr.length; i++) {
td = tr[i].getElementsByTagName("td")[1];
if (td) {
txtValue = td.textContent || td.innerText;
if (txtValue.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
</script>
<script>
document.addEventListener("DOMContentLoaded",function(){
google.script.run.withSuccessHandler(generateTable).getDomainUsersList();
});
function generateTable(dataArray) {
dataArray.forEach(function(r){
var tbody = document.getElementById("table-body");
var row = document.createElement("tr");
var col1 = document.createElement("td");
col1.textContent = r[0];
var col2 = document.createElement("td");
col2.textContent = r[1];
var col3 = document.createElement("td");
var image = document.createElement("img");
image.src = r[2];
col3.appendChild(image);
row.appendChild(col3);
row.appendChild(col1);
row.appendChild(col2);
tbody.appendChild(row);
});
}
</script>
<!--Sortable Headers -->
<script>
function sortTable(n) {
var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
table = document.getElementById("myTable");
switching = true;
//Set the sorting direction to ascending:
dir = "asc";
/*Make a loop that will continue until
no switching has been done:*/
while (switching) {
//start by saying: no switching is done:
switching = false;
rows = table.rows;
/*Loop through all table rows (except the
first, which contains table headers):*/
for (i = 1; i < (rows.length - 1); i++) {
//start by saying there should be no switching:
shouldSwitch = false;
/*Get the two elements you want to compare,
one from current row and one from the next:*/
x = rows[i].getElementsByTagName("TD")[n];
y = rows[i + 1].getElementsByTagName("TD")[n];
/*check if the two rows should switch place,
based on the direction, asc or desc:*/
if (dir == "asc") {
if (x.innerHTML.toLowerCase() > y.innerHTML.toLowerCase()) {
//if so, mark as a switch and break the loop:
shouldSwitch= true;
break;
}
} else if (dir == "desc") {
if (x.innerHTML.toLowerCase() < y.innerHTML.toLowerCase()) {
//if so, mark as a switch and break the loop:
shouldSwitch = true;
break;
}
}
}
if (shouldSwitch) {
/*If a switch has been marked, make the switch
and mark that a switch has been done:*/
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
switching = true;
//Each time a switch is done, increase this count by 1:
switchcount ++;
} else {
/*If no switching has been done AND the direction is "asc",
set the direction to "desc" and run the while loop again.*/
if (switchcount == 0 && dir == "asc") {
dir = "desc";
switching = true;
}
}
}
}
</script>
</body>
</html>
Thanks in advance,
Isaac
#IMTheNachoMan reported the issue in Google's issue tracker. You can click on the star next to the issue number to receive updates and to give more priority to his report.
This is an example of how we can log in chrome dev tools with colors:
console.log('%c test1 ', 'background: black; color: green')
I was wondering whether we can log with table and colors and the same time ?
There's no styling capabilities on the console.table function, as per the Console API.
However, I was able to come up with a really hacky solution for styling a console.log statement as if it were a table. This is probably not going to be good enough, but it was pretty fun to make.
There were many limitations, like not being able to set the width and height properties. My workaround was to pad the text with spaces to match the longest property name/value.
(function() {
function getProperties(obj) {
var props = [];
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
props.push(prop);
}
}
return props;
}
function getLongestTextLength(objArray) {
var longest = 0;
for (var obj of objArray) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
var length = Math.max(prop.length, obj[prop].length);
if (length > longest) longest = length;
}
}
}
return longest;
}
console.fancyTable = function(objArray) {
var objProto = objArray[0];
var args = [];
var header = '';
var baseStyles = 'padding: 2px; line-height: 18px;';
var baseBorders = 'border-top: 1px solid black; border-bottom: 1px solid black; border-left: 1px solid black; '
var headerStyles = baseStyles + baseBorders + 'font-weight: bold; background: lightcoral;';
var lastHeaderStyles = baseStyles + 'font-weight: bold; border: 1px solid black; background: lightcoral;';
var rowStyles = baseStyles + baseBorders + 'background: lightblue;'
var lastRowStyles = baseStyles + 'border: 1px solid black; background: lightblue;'
var props = getProperties(objProto);
var longestTextLength = getLongestTextLength(objArray);
for (var i = 0; i < props.length; i++) {
var prop = props[i];
while (prop.length < longestTextLength) {
prop += ' ';
}
header += '%c' + prop + ' ';
if (i === props.length - 1) {
args.push(lastHeaderStyles);
} else {
args.push(headerStyles);
}
}
for (var i = 0; i < objArray.length; i++) {
var obj = objArray[i];
header += '\n';
for (var j = 0; j < props.length; j++) {
var val = obj[props[j]];
while (val.length < longestTextLength) {
val += ' ';
}
header += '%c' + val + ' ';
if (j === props.length - 1) {
args.push(lastRowStyles);
} else {
args.push(rowStyles);
}
}
}
args.unshift(header);
console.log.apply(this, args);
}
})();
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// create some test objects
var john = new Person("John", "Smith");
var jane = new Person("Jane", "Doe");
var emily = new Person("Emily", "Jones");
var peopleToLog = [john, jane, emily];
console.fancyTable(peopleToLog);
I'll make some improvements if I can, and perhaps publish it for the giggles.
I have a DIV with ContentEditable set to TRUE. Inside it there are several spans with ContentEditable set to FALSE.
I trap the BackSpace key so that if the element under cursor is <span> I can delete it.
The problem is that it works alternately with odd spans only.
So, for example, with the html code below, put the cursor at the end of text in DIV, and press backspace all the way till the beginning of div. Observe that it will select/delete first span, then leave the second, then select/delete the third span, then leave the fourth and so on.
This behavior is only on Internet Explorer. It works exactly as expected on Firefox.
How should I make the behavior consistant in Internet Explorer?
Following html code can be used to reproduce the behavior:
var EditableDiv = document.getElementById('EditableDiv');
EditableDiv.onkeydown = function(event) {
var ignoreKey;
var key = event.keyCode || event.charCode;
if (!window.getSelection) return;
var selection = window.getSelection();
var focusNode = selection.focusNode,
anchorNode = selection.anchorNode;
if (key == 8) { //backspace
if (!selection.isCollapsed) {
if (focusNode.nodeName == 'SPAN' || anchorNode.nodeName == 'SPAN') {
anchorNode.parentNode.removeChild(anchorNode);
ignoreKey = true;
}
} else if (anchorNode.previousSibling && anchorNode.previousSibling.nodeName == 'SPAN' && selection.anchorOffset <= 1) {
SelectText(event, anchorNode.previousSibling);
ignoreKey = true;
}
}
if (ignoreKey) {
var evt = event || window.event;
if (evt.stopPropagation) evt.stopPropagation();
evt.preventDefault();
return false;
}
}
function SelectText(event, element) {
var range, selection;
EditableDiv.focus();
if (document.body.createTextRange && element.nodeName == 'SPAN') {
range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
var evt = (event) ? event : window.event;
if (evt.stopPropagation) evt.stopPropagation();
if (evt.cancelBubble != null) evt.cancelBubble = true;
return false;
}
#EditableDiv {
height: 75px;
width: 500px;
font-family: Consolas;
font-size: 10pt;
font-weight: normal;
letter-spacing: 1px;
background-color: white;
overflow-y: scroll;
overflow-x: hidden;
border: 1px solid black;
padding: 5px;
}
#EditableDiv span {
color: brown;
font-family: Verdana;
font-size: 8.5pt;
min-width: 10px;
_width: 10px;
}
#EditableDiv p,
#EditableDiv br {
display: inline;
}
<div id="EditableDiv" contenteditable="true">
(<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field1</span> < 500) <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>OR</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field2</span> > 100 <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>AND</span> (<span contenteditable='false' onclick='SelectText(event, this);'
unselectable='on'>Field3</span> <=200) )
</div>
EDIT
Just FYI. I have asked this question in MSDN Forum as well.
The challenge to this is to get IE11 to backspace from the right directly against the <span>. Then the next backspace will select and highlight it. This seems like such a simple objective, but IE11 just won't cooperate. There should be a quick easy patch, right? And so the bugs begin.The approach I came up with is to walk the tree backwards to the first previous non-empty node, clearing the empty nodes between to appease IE, and then evaluate a few conditions. If the caret should end up at the right side of the <span>, then do it manually (because IE won't) by creating a new range obj with the selection there at the end of the <span>.online demoI added an additional kludge for IE in the case that two spans are dragged against eachother. For example, Field2Field3. When you then backspace from the right onto Field3, then backspace once again to delete it, IE would jump the caret leftward over Field2. Skip right over Field2. grrr. The workaround is to intercept that and insert a space between the pair of spans. I wasn't confident you'd be happy with that. But, you know, it's a workaround. Anyway, that turned-up yet another bug, where IE changes the inserted space into two empty textnodes. more grrr. And so a workaround for the workaround. See the non-isCollapsed code.
CODE SNIPPET
var EditableDiv = document.getElementById('EditableDiv');
EditableDiv.onkeydown = function(event) {
var ignoreKey;
var key = event.keyCode || event.charCode;
if (!window.getSelection) return;
var selection = window.getSelection();
var focusNode = selection.focusNode,
anchorNode = selection.anchorNode;
var anchorOffset = selection.anchorOffset;
if (!anchorNode) return
if (anchorNode.nodeName.toLowerCase() != '#text') {
if (anchorOffset < anchorNode.childNodes.length)
anchorNode = anchorNode.childNodes[anchorOffset]
else {
while (!anchorNode.nextSibling) anchorNode = anchorNode.parentNode // this might step out of EditableDiv to "justincase" comment node
anchorNode = anchorNode.nextSibling
}
anchorOffset = 0
}
function backseek() {
while ((anchorOffset == 0) && (anchorNode != EditableDiv)) {
if (anchorNode.previousSibling) {
if (anchorNode.previousSibling.nodeName.toLowerCase() == '#text') {
if (anchorNode.previousSibling.nodeValue.length == 0)
anchorNode.parentNode.removeChild(anchorNode.previousSibling)
else {
anchorNode = anchorNode.previousSibling
anchorOffset = anchorNode.nodeValue.length
}
} else if ((anchorNode.previousSibling.offsetWidth == 0) && (anchorNode.previousSibling.offsetHeight == 0))
anchorNode.parentNode.removeChild(anchorNode.previousSibling)
else {
anchorNode = anchorNode.previousSibling
while ((anchorNode.lastChild) && (anchorNode.nodeName.toUpperCase() != 'SPAN')) {
if ((anchorNode.lastChild.offsetWidth == 0) && (anchorNode.lastChild.offsetHeight == 0))
anchorNode.removeChild(anchorNode.lastChild)
else if (anchorNode.lastChild.nodeName.toLowerCase() != '#text')
anchorNode = anchorNode.lastChild
else if (anchorNode.lastChild.nodeValue.length == 0)
anchorNode.removeChild(anchorNode.lastChild)
else {
anchorNode = anchorNode.lastChild
anchorOffset = anchorNode.nodeValue.length
//break //don't need to break, textnode has no children
}
}
break
}
} else
while (((anchorNode = anchorNode.parentNode) != EditableDiv) && !anchorNode.previousSibling) {}
}
}
if (key == 8) { //backspace
if (!selection.isCollapsed) {
try {
document.createElement("select").size = -1
} catch (e) { //kludge for IE when 2+ SPANs are back-to-back adjacent
if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
backseek()
if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
var k = document.createTextNode(" ") // doesn't work here between two spans. IE makes TWO EMPTY textnodes instead !
anchorNode.parentNode.insertBefore(k, anchorNode) // this works
anchorNode.parentNode.insertBefore(anchorNode, k) // simulate "insertAfter"
}
}
}
} else {
backseek()
if (anchorNode == EditableDiv)
ignoreKey = true
else if (anchorNode.nodeName.toUpperCase() == 'SPAN') {
SelectText(event, anchorNode)
ignoreKey = true
} else if ((anchorNode.nodeName.toLowerCase() == '#text') && (anchorOffset <= 1)) {
var prev, anchorNodeSave = anchorNode,
anchorOffsetSave = anchorOffset
anchorOffset = 0
backseek()
if (anchorNode.nodeName.toUpperCase() == 'SPAN') prev = anchorNode
anchorNode = anchorNodeSave
anchorOffset = anchorOffsetSave
if (prev) {
if (anchorOffset == 0)
SelectEvent(prev)
else {
var r = document.createRange()
selection.removeAllRanges()
if (anchorNode.nodeValue.length > 1) {
r.setStart(anchorNode, 0)
selection.addRange(r)
anchorNode.deleteData(0, 1)
}
else {
for (var i = 0, p = prev.parentNode; true; i++)
if (p.childNodes[i] == prev) break
r.setStart(p, ++i)
selection.addRange(r)
anchorNode.parentNode.removeChild(anchorNode)
}
}
ignoreKey = true
}
}
}
}
if (ignoreKey) {
var evt = event || window.event;
if (evt.stopPropagation) evt.stopPropagation();
evt.preventDefault();
return false;
}
}
function SelectText(event, element) {
var range, selection;
EditableDiv.focus();
if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNode(element)
selection.removeAllRanges();
selection.addRange(range);
} else {
range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
}
var evt = (event) ? event : window.event;
if (evt.stopPropagation) evt.stopPropagation();
if (evt.cancelBubble != null) evt.cancelBubble = true;
return false;
}
#EditableDiv {
height: 75px;
width: 500px;
font-family: Consolas;
font-size: 10pt;
font-weight: normal;
letter-spacing: 1px;
background-color: white;
overflow-y: scroll;
overflow-x: hidden;
border: 1px solid black;
padding: 5px;
}
#EditableDiv span {
color: brown;
font-family: Verdana;
font-size: 8.5pt;
min-width: 10px;
/*_width: 10px;*/
/* what is this? */
}
#EditableDiv p,
#EditableDiv br {
display: inline;
}
<div id="EditableDiv" contenteditable="true">
(<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field1</span> < 500) <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>OR</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field2</span> > 100 <span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>AND</span> (<span contenteditable='false' onclick='SelectText(event, this);' unselectable='on'>Field3</span> <= 200) )
</div>
I have seen many ways to make the header fixed on a standard table, but usually this involves cloning the table (to match the variable widths). This of course would not work for me as the header is clickable, allowing people to sort the contents of certain rows.
So, is there a way to essentially 'freeze' the header row while still retaining the functionality of the sort?
Here is the basic table code (all css kept in 'stylesheet' for cleanliness & likewise with js)
var stIsIE = /*#cc_on!#*/false;
sorttable = {
init: function() {
// quit if this function has already been called
if (arguments.callee.done) return;
// flag this function so we don't do the same thing twice
arguments.callee.done = true;
// kill the timer
if (_timer) clearInterval(_timer);
if (!document.createElement || !document.getElementsByTagName) return;
sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
forEach(document.getElementsByTagName('table'), function(table) {
if (table.className.search(/\bsortable\b/) != -1) {
sorttable.makeSortable(table);
}
});
},
makeSortable: function(table) {
if (table.getElementsByTagName('thead').length == 0) {
// table doesn't have a tHead. Since it should have, create one and
// put the first table row in it.
the = document.createElement('thead');
the.appendChild(table.rows[0]);
table.insertBefore(the,table.firstChild);
}
// Safari doesn't support table.tHead, sigh
if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
if (table.tHead.rows.length != 1) return; // can't cope with two header rows
// Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
// "total" rows, for example). This is B&R, since what you're supposed
// to do is put them in a tfoot. So, if there are sortbottom rows,
// for backwards compatibility, move them to tfoot (creating it if needed).
sortbottomrows = [];
for (var i=0; i<table.rows.length; i++) {
if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
sortbottomrows[sortbottomrows.length] = table.rows[i];
}
}
if (sortbottomrows) {
if (table.tFoot == null) {
// table doesn't have a tfoot. Create one.
tfo = document.createElement('tfoot');
table.appendChild(tfo);
}
for (var i=0; i<sortbottomrows.length; i++) {
tfo.appendChild(sortbottomrows[i]);
}
delete sortbottomrows;
}
// work through each column and calculate its type
headrow = table.tHead.rows[0].cells;
for (var i=0; i<headrow.length; i++) {
// manually override the type with a sorttable_type attribute
if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
if (mtch) { override = mtch[1]; }
if (mtch && typeof sorttable["sort_"+override] == 'function') {
headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
} else {
headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
}
// make it clickable to sort
headrow[i].sorttable_columnindex = i;
headrow[i].sorttable_tbody = table.tBodies[0];
dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
if (this.className.search(/\bsorttable_sorted\b/) != -1) {
// if we're already sorted by this column, just
// reverse the table, which is quicker
sorttable.reverse(this.sorttable_tbody);
this.className = this.className.replace('sorttable_sorted',
'sorttable_sorted_reverse');
this.removeChild(document.getElementById('sorttable_sortfwdind'));
sortrevind = document.createElement('span');
sortrevind.id = "sorttable_sortrevind";
sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴';
this.appendChild(sortrevind);
return;
}
if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
// if we're already sorted by this column in reverse, just
// re-reverse the table, which is quicker
sorttable.reverse(this.sorttable_tbody);
this.className = this.className.replace('sorttable_sorted_reverse',
'sorttable_sorted');
this.removeChild(document.getElementById('sorttable_sortrevind'));
sortfwdind = document.createElement('span');
sortfwdind.id = "sorttable_sortfwdind";
sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
this.appendChild(sortfwdind);
return;
}
// remove sorttable_sorted classes
theadrow = this.parentNode;
forEach(theadrow.childNodes, function(cell) {
if (cell.nodeType == 1) { // an element
cell.className = cell.className.replace('sorttable_sorted_reverse','');
cell.className = cell.className.replace('sorttable_sorted','');
}
});
sortfwdind = document.getElementById('sorttable_sortfwdind');
if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
sortrevind = document.getElementById('sorttable_sortrevind');
if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
this.className += ' sorttable_sorted';
sortfwdind = document.createElement('span');
sortfwdind.id = "sorttable_sortfwdind";
sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
this.appendChild(sortfwdind);
// build an array to sort. This is a Schwartzian transform thing,
// i.e., we "decorate" each row with the actual sort key,
// sort based on the sort keys, and then put the rows back in order
// which is a lot faster because you only do getInnerText once per row
row_array = [];
col = this.sorttable_columnindex;
rows = this.sorttable_tbody.rows;
for (var j=0; j<rows.length; j++) {
row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
}
/* If you want a stable sort, uncomment the following line */
//sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
/* and comment out this one */
row_array.sort(this.sorttable_sortfunction);
tb = this.sorttable_tbody;
for (var j=0; j<row_array.length; j++) {
tb.appendChild(row_array[j][1]);
}
delete row_array;
});
}
}
},
guessType: function(table, column) {
// guess the type of a column based on its first non-blank row
sortfn = sorttable.sort_alpha;
for (var i=0; i<table.tBodies[0].rows.length; i++) {
text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
if (text != '') {
if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
return sorttable.sort_numeric;
}
// check for a date: dd/mm/yyyy or dd/mm/yy
// can have / or . or - as separator
// can be mm/dd as well
possdate = text.match(sorttable.DATE_RE)
if (possdate) {
// looks like a date
first = parseInt(possdate[1]);
second = parseInt(possdate[2]);
if (first > 12) {
// definitely dd/mm
return sorttable.sort_ddmm;
} else if (second > 12) {
return sorttable.sort_mmdd;
} else {
// looks like a date, but we can't tell which, so assume
// that it's dd/mm (English imperialism!) and keep looking
sortfn = sorttable.sort_ddmm;
}
}
}
}
return sortfn;
},
getInnerText: function(node) {
// gets the text we want to use for sorting for a cell.
// strips leading and trailing whitespace.
// this is *not* a generic getInnerText function; it's special to sorttable.
// for example, you can override the cell text with a customkey attribute.
// it also gets .value for <input> fields.
if (!node) return "";
hasInputs = (typeof node.getElementsByTagName == 'function') &&
node.getElementsByTagName('input').length;
if (node.getAttribute("sorttable_customkey") != null) {
return node.getAttribute("sorttable_customkey");
}
else if (typeof node.textContent != 'undefined' && !hasInputs) {
return node.textContent.replace(/^\s+|\s+$/g, '');
}
else if (typeof node.innerText != 'undefined' && !hasInputs) {
return node.innerText.replace(/^\s+|\s+$/g, '');
}
else if (typeof node.text != 'undefined' && !hasInputs) {
return node.text.replace(/^\s+|\s+$/g, '');
}
else {
switch (node.nodeType) {
case 3:
if (node.nodeName.toLowerCase() == 'input') {
return node.value.replace(/^\s+|\s+$/g, '');
}
case 4:
return node.nodeValue.replace(/^\s+|\s+$/g, '');
break;
case 1:
case 11:
var innerText = '';
for (var i = 0; i < node.childNodes.length; i++) {
innerText += sorttable.getInnerText(node.childNodes[i]);
}
return innerText.replace(/^\s+|\s+$/g, '');
break;
default:
return '';
}
}
},
reverse: function(tbody) {
// reverse the rows in a tbody
newrows = [];
for (var i=0; i<tbody.rows.length; i++) {
newrows[newrows.length] = tbody.rows[i];
}
for (var i=newrows.length-1; i>=0; i--) {
tbody.appendChild(newrows[i]);
}
delete newrows;
},
/* sort functions
each sort function takes two parameters, a and b
you are comparing a[0] and b[0] */
sort_numeric: function(a,b) {
aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
if (isNaN(aa)) aa = 0;
bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
if (isNaN(bb)) bb = 0;
return aa-bb;
},
sort_alpha: function(a,b) {
if (a[0].toLowerCase()==b[0].toLowerCase()) return 0;
if (a[0].toLowerCase()<b[0].toLowerCase()) return -1;
return 1;
},
sort_ddmm: function(a,b) {
mtch = a[0].match(sorttable.DATE_RE);
y = mtch[3]; m = mtch[2]; d = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt1 = y+m+d;
mtch = b[0].match(sorttable.DATE_RE);
y = mtch[3]; m = mtch[2]; d = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt2 = y+m+d;
if (dt1==dt2) return 0;
if (dt1<dt2) return -1;
return 1;
},
sort_mmdd: function(a,b) {
mtch = a[0].match(sorttable.DATE_RE);
y = mtch[3]; d = mtch[2]; m = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt1 = y+m+d;
mtch = b[0].match(sorttable.DATE_RE);
y = mtch[3]; d = mtch[2]; m = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt2 = y+m+d;
if (dt1==dt2) return 0;
if (dt1<dt2) return -1;
return 1;
},
shaker_sort: function(list, comp_func) {
// A stable sort function to allow multi-level sorting of data
// see: http://en.wikipedia.org/wiki/Cocktail_sort
// thanks to Joseph Nahmias
var b = 0;
var t = list.length - 1;
var swap = true;
while(swap) {
swap = false;
for(var i = b; i < t; ++i) {
if ( comp_func(list[i], list[i+1]) > 0 ) {
var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
swap = true;
}
} // for
t--;
if (!swap) break;
for(var i = t; i > b; --i) {
if ( comp_func(list[i], list[i-1]) < 0 ) {
var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
swap = true;
}
} // for
b++;
} // while(swap)
}
}
/* ******************************************************************
Supporting functions: bundled here to avoid depending on a library
****************************************************************** */
// Dean Edwards/Matthias Miller/John Resig
/* for Mozilla/Opera9 */
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", sorttable.init, false);
}
/* for Internet Explorer */
/*#cc_on #*/
/*#if (#_win32)
document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete") {
sorttable.init(); // call the onload handler
}
};
/*#end #*/
/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
sorttable.init(); // call the onload handler
}
}, 10);
}
/* for other browsers */
window.onload = sorttable.init;
// written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini
// http://dean.edwards.name/weblog/2005/10/add-event/
function dean_addEvent(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else {
// assign each event handler a unique ID
if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
// create a hash table of event types for the element
if (!element.events) element.events = {};
// create a hash table of event handlers for each element/event pair
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
// store the existing event handler (if there is one)
if (element["on" + type]) {
handlers[0] = element["on" + type];
}
}
// store the event handler in the hash table
handlers[handler.$$guid] = handler;
// assign a global event handler to do all the work
element["on" + type] = handleEvent;
}
};
// a counter used to create unique IDs
dean_addEvent.guid = 1;
function removeEvent(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
}
};
function handleEvent(event) {
var returnValue = true;
// grab the event object (IE uses a global event object)
event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
// get a reference to the hash table of event handlers
var handlers = this.events[event.type];
// execute each event handler
for (var i in handlers) {
this.$$handleEvent = handlers[i];
if (this.$$handleEvent(event) === false) {
returnValue = false;
}
}
return returnValue;
};
function fixEvent(event) {
// add W3C standard event methods
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
};
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
}
// Dean's forEach: http://dean.edwards.name/base/forEach.js
/*
forEach, version 1.0
Copyright 2006, Dean Edwards
License: http://www.opensource.org/licenses/mit-license.php
*/
// array-like enumeration
if (!Array.forEach) { // mozilla already supports this
Array.forEach = function(array, block, context) {
for (var i = 0; i < array.length; i++) {
block.call(context, array[i], i, array);
}
};
}
// generic enumeration
Function.prototype.forEach = function(object, block, context) {
for (var key in object) {
if (typeof this.prototype[key] == "undefined") {
block.call(context, object[key], key, object);
}
}
};
// character enumeration
String.forEach = function(string, block, context) {
Array.forEach(string.split(""), function(chr, index) {
block.call(context, chr, index, string);
});
};
// globally resolve forEach enumeration
var forEach = function(object, block, context) {
if (object) {
var resolve = Object; // default
if (object instanceof Function) {
// functions have a "length" property
resolve = Function;
} else if (object.forEach instanceof Function) {
// the object implements a custom forEach method so use that
object.forEach(block, context);
return;
} else if (typeof object == "string") {
// the object is a string
resolve = String;
} else if (typeof object.length == "number") {
// the object is array-like
resolve = Array;
}
resolve.forEach(object, block, context);
}
};
div#main { margin-left:1%; margin-right:1%; }
table.sortable th:not(.sorttable_sorted):not(.sorttable_sorted_reverse):not(.sorttable_nosort):after {content: " \25B4\25BE"}
table.sortable tbody tr:nth-child(2n) td {background: #ffcccc;}
table.sortable tbody tr:nth-child(2n+1) td {background: #ccfffff;}
.sm { font-size:small; }
.text { text-align:center; }
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>RuneScape Quest Checklist</title>
<link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8">
<script type="text/javascript" src="sorttable.js"></script>
</head>
<body>
<div id="main">
<table class="sortable" border="0" cellpadding="0" cellspacing="0" width="100%">
<thead>
<tr>
<th class="sorttable_nosort" title="Unsortable" style="width: 55px;"><strong>Done</strong></th>
<th title="Click to sort"><strong>Quest Name</strong></th>
<th title="Click to sort"><strong>Difficulty</strong></th>
<th title="Click to sort"><strong>Length</strong></th>
<th class="sorttable_nosort" title="Unsortable"><strong>Skill Req.</strong></th>
<th class="sorttable_nosort" title="Unsortable"><strong>Quest Req.</strong></th>
<th title="Click to sort"><strong>QP</strong></th>
<th class="sorttable_nosort" title="Unsortable"><strong>Rewards</strong></th>
<th title="Click to sort"><strong>Free/Members</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td class="sm text"><input name="done[1]" value="1" type="checkbox"></td>
<td class="sm text">Quest Name</td>
<td class="sm text"><div style="display: none;">1</div>Novice (to Grandmaster)</td>
<td class="sm text"><div style="display: none;">1</div>Short (to Very Long)</td>
<td class="sm text">Various Skills</td>
<td class="sm text">Various Quests</td>
<td class="sm text">1 (to 3)</td>
<td class="sm">
<ul>
<li>Reward 1</li>
<li>Reward 2</li>
<li>etc...</li>
</ul>
</td>
<td class="sm text">Membership req (or not)</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
try like this example
this link also helpful to you. fixed header of table
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
section {
position: relative;
border: 1px solid #000;
padding-top: 37px;
background: #500;
}
section.positioned {
position: absolute;
top: 100px;
left: 100px;
width: 800px;
box-shadow: 0 0 15px #333;
}
.container {
overflow-y: auto;
height: 200px;
}
table {
border-spacing: 0;
width: 100%;
}
td + td {
border-left: 1px solid #eee;
}
td,
th {
border-bottom: 1px solid #eee;
background: #ddd;
color: #000;
padding: 10px 25px;
}
th {
height: 0;
line-height: 0;
padding-top: 0;
padding-bottom: 0;
color: transparent;
border: none;
white-space: nowrap;
}
th div {
position: absolute;
background: transparent;
color: #fff;
padding: 9px 25px;
top: 0;
margin-left: -25px;
line-height: normal;
border-left: 1px solid #800;
}
th:first-child div {
border: none;
}
<section class="">
<div class="container">
<table>
<thead>
<tr class="header">
<th>
Table attribute name
<div>Table attribute name</div>
</th>
<th>
Value
<div>Value</div>
</th>
<th>
Description
<div>Description</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>align</td>
<td>left, center, right</td>
<td>Not supported in HTML5. Deprecated in HTML 4.01. Specifies the alignment of a table according to surrounding text</td>
</tr>
<tr>
<td>bgcolor</td>
<td>rgb(x,x,x), #xxxxxx, colorname</td>
<td>Not supported in HTML5. Deprecated in HTML 4.01. Specifies the background color for a table</td>
</tr>
<tr>
<td>border</td>
<td>1,""</td>
<td>Specifies whether the table cells should have borders or not</td>
</tr>
<tr>
<td>cellpadding</td>
<td>pixels</td>
<td>Not supported in HTML5. Specifies the space between the cell wall and the cell content</td>
</tr>
<tr>
<td>cellspacing</td>
<td>pixels</td>
<td>Not supported in HTML5. Specifies the space between cells</td>
</tr>
<tr>
<td>frame</td>
<td>void, above, below, hsides, lhs, rhs, vsides, box, border</td>
<td>Not supported in HTML5. Specifies which parts of the outside borders that should be visible</td>
</tr>
<tr>
<td>rules</td>
<td>none, groups, rows, cols, all</td>
<td>Not supported in HTML5. Specifies which parts of the inside borders that should be visible</td>
</tr>
<tr>
<td>summary</td>
<td>text</td>
<td>Not supported in HTML5. Specifies a summary of the content of a table</td>
</tr>
<tr>
<td>width</td>
<td>pixels, %</td>
<td>Not supported in HTML5. Specifies the width of a table</td>
</tr>
</tbody>
</table>
</div>
</section>
I have a nice/working solution with jQuery.
Assume your table's class is "fixed_header" then add following code:
CSS:
.fixed_header{position:relative; border-collapse: collapse;}
JavaScript:
var originalHeader;
var floatingHeader;
$(document).ready(function () {
var tableObj=$('.fixed_header'); //or other CSS selector as `#tableId`
var tableBaseTop = tableObj.offset().top;
originalHeader = $('.fixed_header tr:first-child'); //change CSS selector here also
floatingHeader = originalHeader.clone();
floatingHeader
.css('position', 'absolute')
.css('top', 0);
setFloatingHeaderSize();
tableObj.prepend(floatingHeader);
$(window).scroll(function () {
var windowTop = $(window).scrollTop();
if (windowTop > tableBaseTop && windowTop < tableBaseTop + tableObj.height() - 100)
floatingHeader.css('top', windowTop - tableBaseTop);
else floatingHeader.css('top', 0); //floating-header is just on Original-one
});
$(window).resize(setFloatingHeaderSize);
});
function setFloatingHeaderSize() {
var originalThs = originalHeader.find('th');
var floatingThs = floatingHeader.find('th');
for (var i = 0; i < originalThs.length; i++) {
floatingThs.eq(i)
.css('width', originalThs.eq(i).width())
.css('height', originalThs.eq(i).height());
}
}
Best part of it, you don't need to change anything in you HTML, it'll directly work on any TABLE, just replace class-name(OR any other css-selector to that table) of table in which header should be fixed.
Concept: It creates a clone header(first row) and display above original one. We set the listener-function for window-scroll event AND when table-scrolled to TOP-of-Window then original one goes-up and clone one remains at top, coz we set it's margin-top to scrolled height. See at this line: if (windowTop > tableBaseTop && windowTop < tableBaseTop + tableObj.height() - 100).
Trick For Your Case: For your clickable feature on clone header, I would rather suggest you to apply your event listener on cloned-header, after cloning header(after following line): tableObj.prepend(floatingHeader);. Coz, cloned-header will be one top always, when user will click on any column-head, the event will we actually captured by cloned-header.
If your events are automatically set by any script(means not in your control), then either try putting(running) that script after above(my-js) OR apply click-event-listener on cloned-header's column for original-one, so that if anybody click then original's click event will also fire.
Code-ex:
function simulateClickOnOriginal() {
var originalThs = originalHeader.find('th');
var floatingThs = floatingHeader.find('th');
for (var i = 0; i < floatingThs.length; i++) {
floatingThs.eq(i).click(function(){
originalThs.eq(i).trigger( "click" );
});
}
}
I'm adapting this progress bar:http://www.richardshepherd.com/tv/audio/ to work with my playlist code, but I can't work out why it's not working. I expect it's something ridiculous (I tried adding the (document).ready function, but that broke the rest of my code).
This is what I have:
function loadPlayer() {
var audioPlayer = new Audio();
audioPlayer.controls="controls";
audioPlayer.preload="auto";
audioPlayer.addEventListener('ended',nextSong,false);
audioPlayer.addEventListener('error',errorFallback,true);
document.getElementById("player").appendChild(audioPlayer);
nextSong();
}
function nextSong() {
if(urls[next]!=undefined) {
var audioPlayer = document.getElementsByTagName('audio')[0];
if(audioPlayer!=undefined) {
audioPlayer.src=urls[next];
audioPlayer.load();
audioPlayer.play();
next++;
} else {
loadPlayer();
}
} else {
alert('the end!');
}
}
function errorFallback() {
nextSong();
}
function playPause() {
var audioPlayer = document.getElementsByTagName('audio')[0];
if(audioPlayer!=undefined) {
if (audioPlayer.paused) {
audioPlayer.play();
} else {
audioPlayer.pause();
}
} else {
loadPlayer();
}
}
function stop() {
var audioPlayer = document.getElementsByTagName('audio')[0];
audioPlayer.pause();
audioPlayer.currentTime = 0;
}
function pickSong(num) {
next = num;
nextSong();
}
var urls = new Array();
urls[0] = '01_horses_mouth/mp3/01. Let The Dog See The Rabbit preface.mp3';
urls[1] = '01_horses_mouth/mp3/02. The Other Horse\'s Tale.mp3';
urls[2] = '01_horses_mouth/mp3/03. Caged Tango.mp3';
urls[3] = '01_horses_mouth/mp3/04. Crumbs.mp3';
urls[4] = '01_horses_mouth/mp3/05. Mood Elevator Reprise.mp3';
urls[5] = '01_horses_mouth/mp3/06. Mood Elevator.mp3';
var next = 0;
// Display our progress bar
audioPlayer.addEventListener('timeupdate', function(){
var length = audioPlayer.duration;
var secs = audioPlayer.currentTime;
var progress = (secs / length) * 100;
$('#progress').css({'width' : progress * 2});
var tcMins = parseInt(secs/60);
var tcSecs = parseInt(secs - (tcMins * 60));
if (tcSecs < 10) { tcSecs = '0' + tcSecs; }
$('#timecode').html(tcMins + ':' + tcSecs);
}, false);
I end up getting the default player which works fine, as do my own play/pause and stop buttons, but the progress bar does nothing.
Oh, and this is what I've stuck in my css:
#progressContainer {position: relative; display: block; height: 20px;
background-color: #fff; width: 200px;
-moz-box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
-webkit-box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
box-shadow: 2px 2px 5px rgba(0,0,0,0.4);
margin-top: 5px;}
#progress {
display: block;
height: 20px;
background-color: #99f;
width: 0;
position: absolute;
top: 0;
left: 0;}
and this is the html:
<div id="player" >
<span id="timecode"></span>
<span id="progressContainer">
<span id="timecode"></span>
<span id="progress"></span>
</div>
The page is here: http://lisadearaujo.com/clientaccess/wot-sound/indexiPhone.html
Please note that this is only working with the media query for iPhone portrait orientation, so if you look at it on a desktop, you'll need to squeeze your window up. :-)
I've now gone with a different solution (http://www.adobe.com/devnet/html5/articles/html5-multimedia-pt3.html) which explained how to acheive this a little better for me. I'm a copy/paster so have very little clue about the correct order in which things must go. What I've got now is this:
function loadPlayer() {
var audioPlayer = new Audio();
audioPlayer.controls="controls";
audioPlayer.preload="auto";
audioPlayer.addEventListener('ended',nextSong,false);
audioPlayer.addEventListener('error',errorFallback,true);
audioPlayer.addEventListener('timeupdate',updateProgress, false);
document.getElementById("player").appendChild(audioPlayer);
nextSong();
}
var urls = new Array();
urls[0] = '01_horses_mouth/mp3/01. Let The Dog See The Rabbit preface.mp3';
urls[1] = '01_horses_mouth/mp3/02. The Other Horse\'s Tale.mp3';
urls[2] = '01_horses_mouth/mp3/03. Caged Tango.mp3';
urls[3] = '01_horses_mouth/mp3/04. Crumbs.mp3';
urls[4] = '01_horses_mouth/mp3/05. Mood Elevator Reprise.mp3';
urls[5] = '01_horses_mouth/mp3/06. Mood Elevator.mp3';
var next = 0;
function updateProgress()
{
var audioPlayer = document.getElementsByTagName('audio')[0];
var value = 0;
if (audioPlayer.currentTime > 0) {
value = Math.floor((100 / audioPlayer.duration) * audioPlayer.currentTime);
}
progress.style.width = value + "%";
}
Hurray. It works. I am not entirely sure why, but that's OK for now...