0byt3m1n1
Path:
C:
/
Users
/
Administrator
/
AppData
/
Roaming
/
RStudio
/
Cache
/
Cache_Data
/
[
Home
]
File: f_000051
/*jshint browser:true, strict:false, curly:false, indent:3*/ /* * dtviewer.js * * Copyright (C) 2022 by Posit Software, PBC * * Unless you have received this program directly from Posit Software pursuant * to the terms of a commercial license agreement with Posit Software, then * this program is licensed to you under the terms of version 3 of the * GNU Affero General Public License. This program is distributed WITHOUT * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. * */ (function () { // the data table itself var table; // the column definitions from the server var cols; // the column currently being resized var resizingColIdx = null; // the column widths (on initial draw) var origColWidths = []; // the initial width of the column that we are resizing var initResizingWidth = null; // the starting table width before resize var origTableWidth = null; // initial resize X position var initResizeX = null; // the amount that resizing bounds have exceeded var resizingBoundsExceeded = 0; // dismiss the active filter popup, if any var dismissActivePopup = null; // cached search/filter values--it's expensive to pull these from a new API // instance every time we render a cell, so we cache them var cachedSearch = ""; var cachedFilterValues = []; // the height of the table at the last time we adjusted it to fit its window var lastHeight = 0; // the table header height of the table at the last time we adjusted it to fit var lastHeaderHeight = 0; // the following is a dirty hack to get tab switching to not destroy the // vertical scroll position in Firefox, and to stop other sources from // scrolling the data when we don't want them to. var lastScrollPos = 0; let pauseScrollEvent = true; let addedScrollHandler = false; // "poll" until scrollBody has an element associated with it, and it becomes // possible to add listeners. Animation frame is a good choice because it // only runs after the window redraws itself. const addScrollHandler = () => { let scrollBody = $(".dataTables_scrollBody"); if (scrollBody && scrollBody.length) { scrollBody.on("scroll", evt => { // this ONLY allows "user-generated" scroll events to actually set // lastScrollPos. Otherwise, any other programmatic attempt to scroll // the body will be ignored, and the last user-set scroll position will // be forcefully restored. If we wanted to actually set the scrollTop, // we would have to remember to first set pauseScrollEvent = false, or // set both lastScrollPos and scrollTop if (!pauseScrollEvent) { lastScrollPos = evt.target.scrollTop; } else { evt.target.scrollTop = lastScrollPos } }); const done = debounce(() => { pauseScrollEvent = true; }, 200); // the following event handlers are here to enable standard channels of // user input -- it doesn't matter that they are at the window level, // since they can represent ANY valid user-based action. // allow the user to scroll with the mouse wheel window.addEventListener("wheel", () => { pauseScrollEvent = false; done(); }); // allow scrolling with the keyboard arrows window.addEventListener("keydown", () => { pauseScrollEvent = false; done(); }); window.addEventListener("touchstart", () => { pauseScrollEvent = false; window.addEventListener("touchend", () => { pauseScrollEvent = true; }, { once: true }); }); // the mousedown and mouseup events detect when the user is dragging the scrollbar window.addEventListener("mousedown", () => { pauseScrollEvent = false; window.addEventListener("mouseup", () => { pauseScrollEvent = true; }, { once: true }); }); addedScrollHandler = true; } if (!addedScrollHandler) { requestAnimationFrame(addScrollHandler); } }; addScrollHandler(); // End dirty hack // display nulls as NAs var displayNullsAsNAs = false; // status text (replaces "Showing x of y...") var statusTextOverride = null; // enables ordering in the table var ordering = true; // show row numbers in index column var rowNumbers = true; // list of calls to defer after table is init (e.g. showing headers) var postInitActions = {}; // callback to trigger column options var onColumnOpen; // callback to dismiss column options var onColumnDismiss; // reference to the column being opened var columnsPopup = null; // Active column properties var activeColumnInfo = {}; // manually adjusted widths of each column var manualWidths = []; // offset from which to start rendering columns var columnOffset = 0; // maximum number of columns to draw // the default is maintained separately for view element considerations var defaultMaxColumns = 50; var maxColumns = defaultMaxColumns; // boolean for whether bootstrapping is occurring, used to // rate limit certain events var bootstrapping = false; // helper for creating a tag with properties + content // (created as a string) var createTag = function (tag, content, attributes) { // if content is an object and attributes is undefined, // treat this as request to create tag with attributes // but no content if (typeof content === "object" && typeof attributes === "undefined") { attributes = content; content = ""; } // ensure attributes is an object attributes = attributes || {}; // compute inner attributes var parts = []; for (var key in attributes) { // extract value var value = attributes[key]; // join arrays of values if (Object.prototype.toString.call(value) === "[object Array]") value = value.join(" "); // skip non-string values if (typeof value !== "string") continue; // push attribute parts.push(key + '="' + value.replace(/"/g, """) + '"'); } // build the final html var opener = "<" + tag + " " + parts.join(" ") + ">"; var closer = "</" + tag + ">"; return opener + (content || "") + closer; }; var isHeaderWidthMismatched = function () { // find the elements to measure (they may not exist) var rs = document.getElementById("rsGridData"); if (!rs || !rs.firstChild.clientWidth || !rs.firstChild.clientWidth > 0) return false; var sh = document.getElementsByClassName("dataTables_scrollHeadInner"); if (sh.length === 0 || !sh[0].firstChild || !sh[0].firstChild.firstChild) return false; // match the widths return rs.firstChild.clientWidth !== sh[0].firstChild.firstChild.clientWidth; }; // update search/filter value cache var updateCachedSearchFilter = function () { if (table) { cachedSearch = table.search(); cachedFilterValues = []; for (var idx = 0; idx < table.columns()[0].length; idx++) { cachedFilterValues.push(table.columns(idx).search()[0]); } } }; // throttle to avoid redrawing the table too frequently (when filtering, // resizing, etc.) var debounce = function (func, wait) { var timeout; return function () { var context = this, args = arguments; var later = function () { timeout = null; func.apply(context, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // show an error--not recoverable; user must click 'retry' to reload var showError = function (msg) { document.getElementById("errorWrapper").style.display = "block"; document.getElementById("errorMask").style.display = "block"; document.getElementById("error").textContent = msg; var rsGridData = document.getElementById("rsGridData"); if (rsGridData) rsGridData.style.display = "none"; }; // simple HTML escaping (avoid XSS in data) var escapeHtml = function (html) { if (!html) return ""; // handle special cells if (typeof html === "number") return html.toString(); // in other types, replace special characters var replacements = { "<": "<", ">": ">", "&": "&", '"': """, " ": " ", }; return html.replace(/[&<> ]/g, function (ch) { return replacements[ch]; }); }; var highlightSearchMatch = function (data, search, pos) { return ( escapeHtml(data.substring(0, pos)) + '<span class="searchMatch">' + escapeHtml(data.substring(pos, pos + search.length)) + "</span>" + escapeHtml(data.substring(pos + search.length, data.length)) ); }; // render cell contents--if no search is active, just renders the data // literally; when search is active, highlights the portion of the text that // matches the search var renderCellContents = function (data, type, row, meta, clazz) { // usually data is a string; 0 is a special value signifying NA if (data === 0 || (displayNullsAsNAs && data === null)) { return '<span class="naCell">NA</span>'; } if (clazz === "dataCell") { // a little ugly: R deparses data cell values as list(col = val, col = val...). when rendering // a data cell which appears to have this format, count the assignment tokens to get a variable // count and show that as a summary. if (data.substring(0, 5) === "list(" && data.indexOf("=") > 0) { var varCount = data.split("=").length - 1; data = "" + varCount + " variable"; if (varCount > 1) data += "s"; } } else if (cachedSearch.length > 0) { // if row matches because of a global search, highlight that var idx = data.toLowerCase().indexOf(cachedSearch.toLowerCase()); if (idx >= 0) { return highlightSearchMatch(data, cachedSearch, idx); } } // if row matches because of a column search (characterwise), highlight that if (meta.col < cachedFilterValues.length) { var colSearch = cachedFilterValues[meta.col]; if (colSearch.indexOf("character|") === 0) { colSearch = decodeURIComponent(parseSearchString(colSearch)); var colIdx = data.toLowerCase().indexOf(colSearch.toLowerCase()); if (colIdx >= 0) { return highlightSearchMatch(data, colSearch, colIdx); } } } var escaped = escapeHtml(data); // special additional rendering for cells which themselves contain data frames or lists: // these include an icon that can be clicked to view contents if (clazz === "dataCell" || clazz === "listCell") { escaped = "<i>" + escaped + "</i> " + '<a class="viewerLink" href="javascript:window.' + (clazz === "dataCell" ? "dataViewerCallback" : "listViewerCallback") + "(" + row[0] + ", " + (meta.col + columnOffset) + ')">' + '<img src="' + (clazz === "dataCell" ? "data-viewer.png" : "object-viewer.png") + '" class="viewerImage" /></a>'; } return escaped; }; var renderCellClass = function (data, type, row, meta, clazz) { // render cell contents var contents = renderCellContents(data, type, row, meta, clazz); // compute classes for tag var classes = [clazz]; // treat data with more than 10 characters as 'long' if (contents.length >= 10) classes.push("largeCell"); // compute title (if any) var title; if (typeof data === "string") title = data; // produce tag return createTag("div", contents, { class: classes, title: title, }); }; // render a number cell var renderNumberCell = function (data, type, row, meta) { return renderCellClass(data, type, row, meta, "numberCell"); }; // render a text cell var renderTextCell = function (data, type, row, meta) { return renderCellClass(data, type, row, meta, "textCell"); }; // render a data cell var renderDataCell = function (data, type, row, meta) { return renderCellClass(data, type, row, meta, "dataCell"); }; // render a list cell var renderListCell = function (data, type, row, meta) { return renderCellClass(data, type, row, meta, "listCell"); }; var syncWidth = function () { // shrink container to width of first row; reschedule size if first row // hasn't been drawn yet var rsGridData = document.getElementById("rsGridData"); if (!rsGridData || !rsGridData.firstChild || rsGridData.firstChild.clientWidth === 0) { return false; } rsGridData.style.width = rsGridData.firstChild.clientWidth + "px"; return true; }; // applies a new size to the table--called on init, on tab activate (from // RStudio), and when the window size changes var sizeDataTable = function (force) { // don't apply a zero height if (window.innerHeight < 1) { return; } var thead = document.getElementById("data_cols"); var theadHeight = thead ? thead.clientHeight : 0; // ignore if height hasn't changed if (lastHeight === window.innerHeight && lastHeaderHeight === theadHeight && !force) { return; } lastHeight = window.innerHeight; lastHeaderHeight = theadHeight; // adjust scroll body height accordingly var scrollBody = $(".dataTables_scrollBody"); if (scrollBody && scrollBody.length > 0) { // apply the new height scrollBody.css("height", window.innerHeight - ($("thead").height() + 25)); } // apply new size if (table) { table.settings().scroller().measure(false); table.draw(); } }; var debouncedDataTableSize = debounce(sizeDataTable, 75); // run a function after window size stops changing var runAfterSizing = function (func) { var height = window.innerHeight; var interval = window.setInterval(function () { if (height === window.innerHeight) { window.clearInterval(interval); func(); } else { height = window.innerHeight; } }, 25); }; var loadingTimer = 0; var preDrawCallback = function () { // when the loading indicator is shown by the scroller, apply a style to // transition it smoothly into view after a few ms window.clearTimeout(loadingTimer); loadingTimer = window.setTimeout(function () { var indicator = $(".DTS_Loading"); if (indicator && !indicator.hasClass("showLoading")) { indicator.addClass("showLoading"); } }, 100); // synchronize table with content syncWidth(); // prior to drawing, update cached search/filter values updateCachedSearchFilter(); }; var postDrawCallback = function () { // cols might not be initialized at this point if (!cols) { return; } var indicator = $(".DTS_Loading"); if (indicator) { indicator.removeClass("showLoading"); } // re-apply manual column sizes to the cells in the first row var bcols = $(".dataTables_scrollBody #data_cols th"); var hcols = $(".dataTables_scrollHead #data_cols th"); var delta = 0; for (var i = 0; i < cols.length; i++) { if (typeof manualWidths[i] === "undefined") continue; var col = bcols.eq(i); delta += manualWidths[i] - col.width(); col.width(manualWidths[i]); hcols.eq(i).width(manualWidths[i]); } // adjust table if some column sizes differed from their natural size if (delta !== 0) { $("#rsGridData").width($("#rsGridData").width() + delta); } // Check to see whether the header widths are out of sync after drawing -- // unfortunately this is a possibility since DataTables doesn't know the // width of the table until after the draw is complete. If the widths don't // match, we resize the main table body to match its content, then do an // in-place redraw of the scrolling-related elements (header, etc.), using an // internal API (without which a redraw would also page in data from the // server, etc). if (isHeaderWidthMismatched()) { syncWidth(); $.fn.dataTableExt.internal._fnScrollDraw($("#rsGridData").DataTable().settings()[0]); } window.clearTimeout(loadingTimer); }; // returns the effective search value for a column (strips the type prefix) var parseSearchString = function (val) { var pipe = val.indexOf("|"); if (pipe > 0) { return val.substr(pipe + 1); } return val; }; var parseSearchVal = function (idx) { return parseSearchString(table.columns(idx).search()[0]); }; var createNumericFilterUI = function (idx, col, onDismiss) { var ele = document.createElement("div"); invokeFilterPopup( ele, function (popup) { popup.className += " numericFilterPopup"; var min = col.col_breaks[0].toString(); var max = col.col_breaks[col.col_breaks.length - 1].toString(); var val = parseSearchVal(idx); if (val.indexOf("_") > 0) { var range = val.split("_"); min = range[0]; max = range[1]; } else if (!isNaN(parseFloat(val))) { min = parseFloat(val); max = parseFloat(val); } var filterFromRange = function (start, end) { if (Math.abs(start - end) === 0) return "" + start; return start + " - " + end; }; // create textbox to show range selected by histogram var numVal = document.createElement("input"); numVal.type = "text"; numVal.className = "numValueBox"; numVal.style.textAlign = "center"; numVal.value = filterFromRange(min, max); // update view to show expression var updateView = debounce(function (val) { var searchText = ""; // discard invalid characters val = val.replace(/[^-0-9 .]/, ""); // just one number? var digit = val.match(/^\s*-?\d+\.?\d*\s*$/); if (digit !== null && digit.length > 0) { searchText = digit[0]; } else { var matches = val.match(/^\s*(-?\d+\.?\d*)\s*-\s*(-?\d+\.?\d*)\s*/); if (matches !== null && matches.length > 2) { // we found a properly formatted query; check to make sure it actually reduces the data // set before applying if ( Math.abs(parseFloat(matches[1]) - min) !== 0 || Math.abs(parseFloat(matches[2]) - max) !== 0 ) { searchText = matches[1] + "_" + matches[2]; } } } if (searchText.length > 0) { // we found a query! apply it. searchText = "numeric|" + searchText; } table.columns(idx).search(searchText).draw(); }, 200); numVal.addEventListener("change", function () { updateView(numVal.value); }); numVal.addEventListener("click", function (evt) { // prevent clicks into the value box from invoking light dismiss evt.stopPropagation(); }); numVal.addEventListener("keydown", function (evt) { // dismiss when user finishes typing in the value box if (!dismissActivePopup) return; else if (evt.keyCode === 27) dismissActivePopup(false); else if (evt.keyCode === 13) dismissActivePopup(true); }); var updateText = function (start, end) { numVal.value = filterFromRange(start, end); updateView(numVal.value); }; var histBrush = document.createElement("div"); histBrush.className = "numHist"; // default to selecting everything var binStart = 0; var binEnd = col.col_breaks.length - 2; // find the bins that best fit the current min/max values for (var i = 0; i < col.col_breaks.length; i++) { if (Math.abs(col.col_breaks[i] - min) < Math.abs(col.col_breaks[binStart] - min)) { binStart = i; } if (i === 0) continue; if (Math.abs(col.col_breaks[i] - max) < Math.abs(col.col_breaks[binEnd] - max)) { binEnd = i - 1; } } // select just one bin in the single bin case if (binEnd < binStart) { binStart = binEnd; } // create histogram hist( histBrush, // element to host histogram col.col_breaks, // array of endpoints for bins col.col_counts, // count of data points in each bin binStart, // index of first selected bin binEnd, // index of last selected bin function (start, end) { updateText(start, end); } ); popup.appendChild(histBrush); popup.appendChild(numVal); }, onDismiss, false ); ele.textContent = "[...]"; return ele; }; // shared among factor and text filter UI var createTextFilterBox = function (ele, idx, col, onDismiss) { var input = document.createElement("input"); input.type = "text"; input.className = "textFilterBox"; // apply the search filter value if this column is filtered as character var searchvals = table.columns(idx).search()[0].split("|"); if (searchvals.length > 1 && searchvals[0] === "character") input.value = searchvals[1]; var updateView = debounce(function () { table .columns(idx) .search("character|" + encodeURIComponent(input.value)) .draw(); }, 200); input.addEventListener("keyup", function (evt) { updateView(); }); input.addEventListener("keydown", function (evt) { if (evt.keyCode === 27) { onDismiss(); } }); ele.addEventListener("click", function (evt) { input.focus(); evt.preventDefault(); evt.stopPropagation(); }); ele.appendChild(input); return input; }; var createFactorFilterUI = function (idx, col, onDismiss) { var ele = document.createElement("div"); var input = createTextFilterBox(ele, idx, col, onDismiss); input.addEventListener("keyup", function (evt) { // when the user starts typing in the text box, hide the drop list if (dismissActivePopup) { dismissActivePopup(false); } }); input.addEventListener("blur", function (evt) { if (!dismissActivePopup) onDismiss(); }); input.addEventListener("focus", function (evt) { if (dismissActivePopup) dismissActivePopup(false); }); var setValHandler = function (factor, text) { return function (evt) { var searchText = "factor|" + factor.toString(); table.columns(idx).search(searchText).draw(); input.value = text; }; }; invokeFilterPopup( ele, function (popup) { var list = document.createElement("div"); list.className = "choiceList"; var current = 0; var searchvals = table.columns(idx).search()[0].split("|"); if (searchvals.length > 1 && searchvals[0] === "factor") current = parseInt(searchvals[1]); for (var i = 0; i < col.col_vals.length; i++) { var opt = document.createElement("div"); opt.textContent = col.col_vals[i]; opt.className = "choiceListItem"; opt.addEventListener("click", setValHandler(i + 1, col.col_vals[i])); list.appendChild(opt); } popup.appendChild(list); }, onDismiss, false ); return ele; }; var createTextFilterUI = function (idx, col, onDismiss) { var ele = document.createElement("div"); var input = createTextFilterBox(ele, idx, col, onDismiss); input.addEventListener("blur", function (evt) { onDismiss(); }); input.addEventListener("focus", function (evt) { if (dismissActivePopup) dismissActivePopup(true); }); return ele; }; var createBooleanFilterUI = function (idx, col, onDismiss) { var ele = document.createElement("div"); var display = document.createElement("span"); display.innerHTML = " "; ele.appendChild(display); var setBoolValHandler = function (text) { return function (evt) { var searchText = "boolean|" + text; table.columns(idx).search(searchText).draw(); display.textContent = text; }; }; invokeFilterPopup( ele, function (popup) { var list = document.createElement("div"); list.className = "choiceList"; var values = ["TRUE", "FALSE"]; for (var logical in values) { var opt = document.createElement("div"); opt.textContent = values[logical]; opt.className = "choiceListItem"; opt.addEventListener("click", setBoolValHandler(values[logical])); list.appendChild(opt); } popup.appendChild(list); }, onDismiss, false ); return ele; }; var invokeFilterPopup = function (ele, buildPopup, onDismiss, dismissOnClick) { var popup = null; var dismissPopup = function (actionComplete) { if (popup) { document.body.removeChild(popup); document.body.removeEventListener("click", checkLightDismiss); document.body.removeEventListener("keydown", checkEscDismiss); dismissActivePopup = null; popup = null; if (actionComplete) onDismiss(); return true; } return false; }; var checkLightDismiss = function (evt) { if (popup && (!dismissOnClick || !popup.contains(evt.target))) { dismissPopup(true); } }; var checkEscDismiss = function (evt) { if (popup && evt.keyCode === 27) { dismissPopup(true); } }; ele.addEventListener("click", function (evt) { // dismiss any other popup if (dismissActivePopup && dismissActivePopup != dismissPopup) { dismissActivePopup(true); } if (popup) { dismissPopup(true); } else { popup = createFilterPopup(); var popupInfo = buildPopup(popup); document.body.appendChild(popup); // compute position var top = $(ele).offset().top + (!popupInfo ? 20 : popupInfo.top); var left = $(ele).offset().left + (!popupInfo ? -4 : popupInfo.left); if (popupInfo && popupInfo.width) { $(popup).width(popupInfo.width(ele)); } // ensure we're not outside the body if (popup.offsetWidth + left > document.body.offsetWidth) { left = document.body.offsetWidth - popup.offsetWidth; } popup.style.top = top + "px"; popup.style.left = left + "px"; document.body.addEventListener("click", checkLightDismiss); document.body.addEventListener("keydown", checkEscDismiss); dismissActivePopup = dismissPopup; } evt.preventDefault(); evt.stopPropagation(); }); }; var createFilterUI = function (idx, col) { // the index coming into this function is for absolute data purposes // since this is a visual-centric function we operate based on the visible index var visualIndex = idx - columnOffset; // don't filter rownames column if (visualIndex < 1) { return; } var host = document.createElement("div"); var val = null, ui = null; host.className = "colFilter unfiltered"; var setUnfiltered = function () { if (ui !== null) { if (ui.parentNode === host) host.replaceChild(val, ui); ui = null; } host.className = "colFilter unfiltered"; clear.style.display = "none"; }; var onDismiss = function () { if (table.columns(visualIndex).search()[0].length === 0) { setUnfiltered(); } }; var clear = document.createElement("img"); clear.src = "datatables/images/clear_filter.png"; clear.className = "clearFilter"; clear.style.display = "none"; clear.addEventListener("click", function (evt) { if (dismissActivePopup) dismissActivePopup(true); table.columns(visualIndex).search("").draw(); setUnfiltered(); evt.preventDefault(); evt.stopPropagation(); }); host.appendChild(clear); val = document.createElement("div"); val.textContent = "All"; val.addEventListener("click", function (evt) { if (col.col_search_type === "numeric") { ui = createNumericFilterUI(visualIndex, col, onDismiss); } else if (col.col_search_type === "factor") { ui = createFactorFilterUI(visualIndex, col, onDismiss); } else if (col.col_search_type === "character") { ui = createTextFilterUI(visualIndex, col, onDismiss); } else if (col.col_search_type === "boolean") { ui = createBooleanFilterUI(visualIndex, col, onDismiss); } if (ui) { ui.className += " filterValue"; host.replaceChild(ui, val); host.className = "colFilter filtered"; clear.style.display = "block"; var click = document.createEvent("MouseEvents"); click.initEvent("click", true, false); ui.dispatchEvent(click); evt.preventDefault(); evt.stopPropagation(); } }); host.appendChild(val); return host; }; var createColumnTypesUI = function (th, idx, col) { var host = document.createElement("div"); host.className = "columnTypeWrapper"; var checkLightDismiss = function (evt) { if (columnsPopup && onColumnDismiss && !columnsPopup.contains(evt.target)) onColumnDismiss(); }; var checkEscDismiss = function (evt) { if (evt.keyCode === 27) { if (onColumnDismiss) onColumnDismiss(); } }; var val = document.createElement("div"); val.textContent = "(" + (col.col_type_assigned ? col.col_type_assigned : col.col_type_r) + ")"; val.className = "columnTypeHeader"; th.className = th.className + " columnClickable"; th.addEventListener("click", function (evt) { if (columnsPopup == null || columnsPopup != th) { columnsPopup = th; activeColumnInfo = { left: $(host).offset().left - 5, top: $(host).parent().height() + 11, width: $(th).outerWidth() - 1, index: idx, name: col.col_name, }; if (onColumnOpen) onColumnOpen(); evt.preventDefault(); evt.stopPropagation(); } else { columnsPopup = null; if (onColumnDismiss) onColumnDismiss(); } }); document.body.addEventListener("click", checkLightDismiss); document.body.addEventListener("keydown", checkEscDismiss); host.appendChild(val); return host; }; var createFilterPopup = function () { var filterUI = document.createElement("div"); filterUI.className = "filterPopup"; return filterUI; }; var createHeader = function (idx, col) { var th = document.createElement("th"); // wrapper for cell contents var interior = document.createElement("div"); interior.className = "headerCell"; // add the title var title = document.createElement("div"); title.textContent = col.col_name; interior.appendChild(title); if (col.col_type === "rownames") { th.title = "row names"; } else { th.title = "column " + (idx - columnOffset) + ": " + col.col_type; } if (col.col_type === "numeric") { th.title += " with range " + col.col_breaks[0] + " - " + col.col_breaks[col.col_breaks.length - 1]; } else if (col.col_type === "factor") { th.title += " with " + col.col_vals.length + " levels"; } if (idx === 0) { th.className = "first-child sorting"; } // add the column label, if it has one if (col.col_label && col.col_label.length > 0) { var label = document.createElement("div"); label.className = "colLabel"; label.textContent = col.col_label; label.title = col.col_label; interior.appendChild(label); } // add a grabber for resizing var resizer = document.createElement("div"); resizer.className = "resizer"; resizer.setAttribute("data-col", idx); interior.appendChild(resizer); th.appendChild(interior); return th; }; var parseLocationUrl = function () { var parsedLocation = {}; parsedLocation.env = parsedLocation.obj = parsedLocation.cacheKey = parsedLocation.id = ""; parsedLocation.maxCols = defaultMaxColumns; var query = window.location.search.substring(1); var queryVars = query.split("&"); for (var i = 0; i < queryVars.length; i++) { var queryVar = queryVars[i].split("="); if (queryVar[0] == "env") { parsedLocation.env = queryVar[1]; } else if (queryVar[0] == "obj") { parsedLocation.obj = queryVar[1]; } else if (queryVar[0] == "cache_key") { parsedLocation.cacheKey = queryVar[1]; } else if (queryVar[0] == "data_source") { parsedLocation.dataSource = queryVar[1]; } else if (queryVar[0] == "id") { parsedLocation.id = queryVar[1]; } else if (queryVar[0] == "max_cols") { parsedLocation.maxCols = parseInt(queryVar[1], 10); } } return parsedLocation; }; var initDataTableLoad = function (result) { table = $("#rsGridData").DataTable(); // datatables has a bug wherein it sometimes thinks an LTR browser is RTL if // the LTR browser is at >100% zoom; this causes layout problems, so force // into LTR mode as we don't support RTL here. $.fn.dataTableSettings[0].oBrowser.bScrollbarLeft = false; // listen for size changes debouncedDataTableSize(); window.addEventListener("resize", function () { debouncedDataTableSize(); }); // trigger post-init actions for (var actionName in postInitActions) { if (postInitActions[actionName]) { postInitActions[actionName](); } } bootstrapping = false; }; var initDataTable = function (resCols, data) { if (resCols.error) { showError(cols.error); return; } // an issue was discovered late in the RStudio v1.2 release cycle whereby // attempts to render data tables containing large numbers could fail, due to // an issue wherein our JSON serializer would incorrectly serialize large // numbers. to avoid churning the JSON serializer so close to release, we // instead transmit these columns as strings and then convert back to numeric // here. for (var i = 0; i < resCols.length; i++) { var entry = resCols[i]; if (entry.hasOwnProperty("col_breaks")) { var col_breaks = entry["col_breaks"]; for (var j = 0; j < col_breaks.length; j++) { if (typeof col_breaks[j] === "string") col_breaks[j] = parseFloat(col_breaks[j]); } } } // save reference to column data cols = resCols; // due to the jquery magic done in dataTables with rewriting variables and // the amount of window parameters we're already using this is a sane fit // for setting constants from dtviewer to dataTables window.dataTableMaxColumns = Math.max(0, cols.length - 1); window.dataTableColumnOffset = columnOffset; // look up the query parameters var parsedLocation = parseLocationUrl(); var env = parsedLocation.env, obj = parsedLocation.obj, cacheKey = parsedLocation.cacheKey; maxColumns = defaultMaxColumns = parsedLocation.maxCols; // keep track of column types for later render var typeIndices = { numeric: [], "data.frame": [], list: [], text: [], }; // add each column, offset this and only add as many as current maxColumns var thead = document.getElementById("data_cols"); for (j = 0; j < cols.length && j <= maxColumns + columnOffset; j++) { // create table header thead.appendChild(createHeader(j <= 0 ? j : j + columnOffset, cols[j])); var colType = cols[j].col_type; if (colType === "numeric" || colType === "data.frame" || colType === "list") { typeIndices[colType].push(j); } else { typeIndices["text"].push(j); } // start at 0 for the dummy column but then one time increment by initialIndex if (j <= 0) { j = columnOffset; } } addResizeHandlers(thead); addGlobalResizeHandlers(); var scrollHeight = window.innerHeight - (thead.clientHeight + 2); var dataTableAjax = null; var dataTableData = null; var dataTableColumnDefs = null; var dataTableColumns = null; if (!data) { dataTableAjax = { url: "../grid_data", type: "POST", data: function (d) { d.env = env; d.obj = obj; d.cache_key = cacheKey; d.show = "data"; d.column_offset = columnOffset; d.max_columns = maxColumns; }, error: function (jqXHR) { if (jqXHR.responseText[0] !== "{") showError(jqXHR.responseText); else { var result = $.parseJSON(jqXHR.responseText); if (result.error) { showError(result.error); } else { showError("The data could not be displayed."); } } }, }; dataTableColumnDefs = [ { targets: typeIndices["numeric"], render: renderNumberCell, }, { targets: typeIndices["text"], render: renderTextCell, }, { targets: typeIndices["list"], render: renderListCell, }, { targets: typeIndices["data.frame"], render: renderDataCell, }, { targets: "_all", width: "4em", }, { targets: 0, sClass: "first-child sorting", width: "4em", orderable: true, }, ]; } else { // Create an empty array of data to be use as a map in the callback dataTableData = []; if (data.length > 0) { for (i = 0; i < data[0].length; i++) { dataTableData.push(i); } } dataTableColumns = resCols.length > 0 ? cols.map(function (e, idx) { var className = rowNumbers && idx === 0 ? "first-child sorting" : null; if (e.col_disabled) { className = "disabledColumn"; } return { sClass: className, visible: !rowNumbers && idx === 0 ? false : true, data: function (row, type, set, meta) { return meta.col === 0 ? meta.row : data ? data[meta.col - 1][meta.row] : null; }, width: "4em", render: e.col_type === "numeric" ? renderNumberCell : renderTextCell, }; }) : [{}]; } // activate the data table $("#rsGridData").dataTable({ processing: true, serverSide: dataTableData ? false : true, autoWidth: false, pagingType: "full_numbers", pageLength: 25, scrollY: scrollHeight + "px", scrollX: true, scroller: { rowHeight: 23, // sync w/ CSS (scroller auto row height is busted) loadingIndicator: true, // show loading indicator when loading }, preDrawCallback: preDrawCallback, drawCallback: postDrawCallback, dom: "tiS", deferRender: true, columnDefs: dataTableColumnDefs, ajax: dataTableAjax, data: dataTableData, columns: dataTableColumns, fnInfoCallback: !statusTextOverride ? null : function (oSettings, iStart, iEnd, iMax, iTotal, sPre) { return statusTextOverride; }, ordering: ordering, }); initDataTableLoad(); // update the GWT column widget window.columnFrameCallback(columnOffset, maxColumns); }; var debouncedSearch = debounce(function (text) { if (text != table.search()) { table.search(text).draw(); } }, 100); var loadDataFromUrl = function (callback) { // call the server to get data shape $.ajax({ url: "../grid_data", data: "show=cols&" + window.location.search.substring(1), type: "POST", }) .done(function (result) { callback(result); }) .fail(function (jqXHR) { if (jqXHR.responseText[0] !== "{") showError(jqXHR.responseText); else { var result = $.parseJSON(jqXHR.responseText); if (result.error) { showError(result.error); } else { showError("The object could not be displayed."); } } }); }; var addGlobalResizeHandlers = function () { // if we've already done the setup, don't add the handlers again if (!!document.resizeHandlersInit) return; document.resizeHandlersInit = true; var applyDelta = function (delta) { if (!resizingColIdx) return; var colWidth = initResizingWidth + delta; // don't allow resizing beneath minimum size. prefer // the original column width, but for large columns allow // resizing to minimum of 100 pixels var minColWidth = origColWidths[resizingColIdx] || 50; if (minColWidth > 100) minColWidth = 100; if (delta < 0 && colWidth < minColWidth) { resizingBoundsExceeded += delta; return; } // if positive delta, consume exceeded bounds before returning to resize // mode if (delta > 0 && resizingBoundsExceeded < 0) { resizingBoundsExceeded += delta; if (resizingBoundsExceeded < 0) { // didn't consume all bounds return; } else { // consumed all bounds; resize remaining portion of motion delta = resizingBoundsExceeded; colWidth = initResizingWidth + delta; } } // resize the column in the given direction $(".dataTables_scrollHeadInner table").width(origTableWidth + delta); var grid = $("#rsGridData"); if (grid.hasClass("autoSize")) { // if the data table is still in auto size mode, we need to switch it // to fixed layout // observe and manually apply column widths in preparation for // transition to a fixed layout var head = $(".dataTables_scrollHead #data_cols th"); var body = $(".dataTables_scrollBody #data_cols th"); for (var i = 0; i < Math.min(head.length, body.length); i++) { var thHead = head.eq(i), thBody = body.eq(i); thHead.width(thHead.width()); thBody.width(thBody.width()); manualWidths[i] = thBody.width(); } // switch table out of auto size mode and into manual size mode grid.addClass("manualSize"); grid.removeClass("autoSize"); } // adjust header width and width of first column $("#data_cols th:nth-child(" + resizingColIdx + ")").width(colWidth); grid.width(origTableWidth + delta); // record manual width for re-apply on redraw manualWidths[resizingColIdx - (rowNumbers ? 1 : 0)] = colWidth; }; var endResize = function () { // end the resize operation $("#rsGridData td:nth-child(" + resizingColIdx + ")").css("border-right-color", ""); resizingColIdx = null; }; $("body").on("mousemove", function (evt) { if (resizingColIdx !== null) { // if we have an active resize column, resize it by the amount given var original = evt.originalEvent; applyDelta(original.clientX - initResizeX); evt.preventDefault(); } }); $("body").on("click", function (evt) { if (resizingColIdx !== null) { // ignore clicks while resizing evt.stopPropagation(); } }); $("body").on("mouseup", function (evt) { if (resizingColIdx !== null) { // end resizing if active endResize(); evt.stopPropagation(); } }); $("body").on("mouseleave", function (evt) { if (resizingColIdx !== null) { // the mouse left; treat this as a cancel (since leaving means we // won't get a corresponding mouseup) applyDelta(0); endResize(); } }); }; var addResizeHandlers = function (ele) { $(ele).on("mousedown", function (evt) { var original = evt.originalEvent; if (original.target.className === "resizer") { // when the mouse is clicked on the resizer, enter resize mode; figure // out which column we're targeting and set up the initial sizes resizingColIdx = parseInt(original.target.getAttribute("data-col")); // account for row names column if (rowNumbers) resizingColIdx++; // disable propagation of clicks from the resizer to the outer cell $(original.target).on("click", function (evt) { return false; }); initResizeX = original.clientX; initResizingWidth = $("#data_cols th:nth-child(" + resizingColIdx + ")").width(); origTableWidth = $("#rsGridData").width(); resizingBoundsExceeded = 0; if (typeof origColWidths[resizingColIdx] === "undefined") origColWidths[resizingColIdx] = initResizingWidth; $("#rsGridData td:nth-child(" + resizingColIdx + ")").css( "border-right-color", "#A0A0FF" ); evt.preventDefault(); } }); }; // bootstrapping: // 1. clean up state (we re-bootstrap whenever table structure changes) // 2. make the request to get the shape of the data object to be viewed // (we want this to start as soon as possible so the shape can be prepared // on the server while we wait for the geometry to finish initializing on // the client) // 3. wait for the document to be ready // 4. wait for the window size to stop changing (RStudio animates tab opening) // 5. initialize the data table var bootstrap = function (data) { boostrapping = true; // dismiss any active popups if (dismissActivePopup) dismissActivePopup(true); // clean state table = null; cols = null; resizingColIdx = null; origColWidths = []; initResizingWidth = null; origTableWidth = null; initResizeX = null; resizingBoundsExceeded = 0; dismissActivePopup = null; cachedSearch = ""; cachedFilterValues = []; lastHeight = 0; lastHeaderHeight = 0; lastScrollPos = 0; // when datatables is initialized on an element, it adds a bunch of goo // around the element to handle scrolling, etc.--we need to pull the whole // thing from the DOM so we get a clean re-init oldEle = document.getElementById("rsGridData_wrapper"); if (oldEle) { oldEle.parentNode.removeChild(oldEle); oldEle = null; } // make a new one, but don't hook it up yet (the document may not exist at // this point) var newEle = document.createElement("table"); newEle.id = "rsGridData"; newEle.className = "dataTable autoSize"; newEle.setAttribute("cellspacing", "0"); newEle.innerHTML = "<thead>" + " <tr id='data_cols'>" + " </tr>" + "</thead>"; addResizeHandlers(newEle); if (!data) { loadDataFromUrl(function (result) { $(document).ready(function () { document.body.appendChild(newEle); runAfterSizing(function () { if (result) { initDataTable(result); } }); }); }); } else { $(document).ready(function () { document.body.appendChild(newEle); runAfterSizing(function () { // Assign line numbers: if (data.data) { data.data = data.data.map(function (e, idx) { var eWithNumber = e; eWithNumber[""] = idx + 1; return eWithNumber; }); } else { data.data = [[]]; } if (data.columns) { initDataTable(data.columns, data.data); } }); }); } }; var setHeaderUIVisible = function (visible, initialize, hide) { var thead = document.getElementById("data_cols"); // it's possible the dable is getting redrawn right now; if it is, defer // this request. if (thead === null || table === null || cols === null) { postInitActions["setHeaderUIVisible"] = visible ? function () { setHeaderUIVisible(true, initialize); } : null; return false; } if (!visible) { hide(thead); // close any popup if (dismissActivePopup) dismissActivePopup(true); } for (var i = 0; i < thead.children.length; i++) { var colIdx = i + (rowNumbers ? 0 : 1) + columnOffset; var col = cols[colIdx]; var th = thead.children[i]; if (visible) { var headerElement = initialize(th, col, colIdx); if (headerElement) { th.appendChild(headerElement); } } else if (th.children.length > 1) { th.removeChild(th.lastChild); } } sizeDataTable(true); return true; }; // Exports ------------------------------------------------------------------- // called from RStudio to toggle the filter UI window.setFilterUIVisible = function (visible) { var setFilterUIVisiblePerColumn = function (th, col, i) { if ( col.col_search_type === "numeric" || col.col_search_type === "character" || col.col_search_type === "factor" || col.col_search_type === "boolean" ) { return createFilterUI(i, col); } return null; }; var hideFilterUI = function () { // clear all the filter data table.columns().search(""); }; return setHeaderUIVisible(visible, setFilterUIVisiblePerColumn, hideFilterUI); }; // called from RStudio to toggle the filter UI window.setColumnDefinitionsUIVisible = function (visible, onColOpen, onColDismiss) { var setColumnDefinitionsUIVisiblePerColumn = function (th, col, i) { return createColumnTypesUI(th, i, col); }; var hideColumnTypeUI = function (thead) { $(".columnTypeWrapper").remove(); }; onColumnOpen = onColOpen ? onColOpen : onColumnOpen; onColumnDismiss = onColDismiss ? onColDismiss : onColumnDismiss; return setHeaderUIVisible(visible, setColumnDefinitionsUIVisiblePerColumn, hideColumnTypeUI); }; // called from RStudio when the underlying object changes window.refreshData = function () { bootstrap(); }; // called from RStudio to apply a column-wide search. window.applySearch = function (text) { debouncedSearch(text); }; window.onActivate = function () { // resize the table once animation finishes debouncedDataTableSize(false); }; window.setData = function (data) { bootstrap(data); }; window.setOption = function (option, value) { switch (option) { case "nullsAsNAs": displayNullsAsNAs = value === "true" ? true : false; break; case "status": statusTextOverride = value; break; case "ordering": ordering = value === "true" ? true : false; break; case "rowNumbers": rowNumbers = value === "true" ? true : false; break; case "dataViewerCallback": window.dataViewerCallback = value; break; case "listViewerCallback": window.listViewerCallback = value; break; case "columnFrameCallback": window.columnFrameCallback = value; break; } }; // default viewer for data cells window.dataViewerCallback = function (row, col) { alert("No viewer for data at " + col + ", " + row + "."); }; // default viewer for list cells window.listViewerCallback = function (row, col) { alert("No viewer for list at " + col + ", " + row + "."); }; // callback for updating the GWT column widget window.columnFrameCallback = function () {}; window.getActiveColumn = function () { return activeColumnInfo; }; window.nextColumnPage = function () { if (bootstrapping) { return; } var newOffset = Math.max( 0, Math.min(cols.length - 1 - maxColumns, columnOffset + maxColumns) ); if (columnOffset != newOffset) { columnOffset = newOffset; bootstrap(); } }; window.prevColumnPage = function () { if (bootstrapping) { return; } var newOffset = Math.max( 0, Math.min(cols.length - 1 - maxColumns, columnOffset - maxColumns) ); if (columnOffset != newOffset) { columnOffset = newOffset; bootstrap(); } }; window.firstColumnPage = function () { if (bootstrapping) { return; } if (columnOffset != 0) { columnOffset = 0; bootstrap(); } }; window.lastColumnPage = function () { if (bootstrapping) { return; } if (columnOffset != cols.length - 1 - maxColumns) { columnOffset = cols.length - 1 - maxColumns; bootstrap(); } }; window.setOffsetAndMaxColumns = function (newOffset, newMax) { if (bootstrapping) { return; } if (newOffset >= cols.length) { return; } if (newOffset > 0) { columnOffset = newOffset; } if (newMax > 0) { newMax = Math.min(cols.length - 1 - newOffset, newMax); maxColumns = newMax; } bootstrap(); }; // return whether to show the column frame UI elements window.isLimitedColumnFrame = function () { return cols.length > defaultMaxColumns; }; var parsedLocation = parseLocationUrl(); var dataMode = parsedLocation && parsedLocation.dataSource ? parsedLocation.dataSource : "server"; // start the first request if (dataMode === "server") { bootstrap(); } })();