mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-09 15:08:04 -05:00
273 lines
9.6 KiB
JavaScript
273 lines
9.6 KiB
JavaScript
/*jshint browser:true, jquery:true, strict:false, curly:false, indent:2*/
|
|
|
|
(function() {
|
|
var animateMs = 400;
|
|
|
|
// Given a DOM node and a column (count of characters), walk recursively
|
|
// through the node's siblings counting characters until the given number
|
|
// of characters have been found.
|
|
//
|
|
// If the given count is bigger than the number of characters contained by
|
|
// the node and its siblings, returns a null node and the number of
|
|
// characters found.
|
|
function findTextColPoint(node, col) {
|
|
var cols = 0;
|
|
if (node.nodeType === 3) {
|
|
var nchar = node.nodeValue.replace(/\n/g, "").length;
|
|
if (nchar >= col) {
|
|
return { element: node, offset: col };
|
|
} else {
|
|
cols += nchar;
|
|
}
|
|
} else if (node.nodeType === 1 && node.firstChild) {
|
|
var ret = findTextColPoint(node.firstChild, col);
|
|
if (ret.element !== null) {
|
|
return ret;
|
|
} else {
|
|
cols += ret.offset;
|
|
}
|
|
}
|
|
if (node.nextSibling)
|
|
return findTextColPoint(node.nextSibling, col - cols);
|
|
else
|
|
return { element: null, offset: cols };
|
|
}
|
|
|
|
// Returns an object indicating the element containing the given line and
|
|
// column of text, and the offset into that element where the text was found.
|
|
//
|
|
// If the given line and column are not found, returns a null element and
|
|
// the number of lines found.
|
|
function findTextPoint(el, line, col) {
|
|
var newlines = 0;
|
|
for (var childId = 0; childId < el.childNodes.length; childId++) {
|
|
var child = el.childNodes[childId];
|
|
// If this is a text node, count the number of newlines it contains.
|
|
if (child.nodeType === 3) { // TEXT_NODE
|
|
var newlinere = /\n/g;
|
|
var match;
|
|
while ((match = newlinere.exec(child.nodeValue)) !== null) {
|
|
newlines++;
|
|
// Found the desired line, now find the column.
|
|
if (newlines === line) {
|
|
return findTextColPoint(child, match.index + col + 1);
|
|
}
|
|
}
|
|
}
|
|
// If this is not a text node, descend recursively to see how many
|
|
// lines it contains.
|
|
else if (child.nodeType === 1) { // ELEMENT_NODE
|
|
var ret = findTextPoint(child, line - newlines, col);
|
|
if (ret.element !== null)
|
|
return ret;
|
|
else
|
|
newlines += ret.offset;
|
|
}
|
|
}
|
|
return { element: null, offset: newlines };
|
|
}
|
|
|
|
// Draw a highlight effect for the given source ref. srcref is assumed to be
|
|
// an integer array of length 6, following the standard R format for source
|
|
// refs.
|
|
function highlightSrcref (srcref, srcfile) {
|
|
// Check to see if the browser supports text ranges (IE8 doesn't)
|
|
if (!document.createRange)
|
|
return;
|
|
|
|
// Check to see if we already have a marker for this source ref
|
|
var el = document.getElementById("srcref_" + srcref);
|
|
if (!el) {
|
|
// We don't have a marker, create one
|
|
el = document.createElement("span");
|
|
el.id = "srcref_" + srcref;
|
|
var ref = srcref;
|
|
var code = document.getElementById(srcfile.replace(/\./g, "_") + "_code");
|
|
// if there is no code file (might be a shiny file), quit early
|
|
if (!code) return;
|
|
var start = findTextPoint(code, ref[0], ref[4]);
|
|
var end = findTextPoint(code, ref[2], ref[5]);
|
|
|
|
// If the insertion point can't be found, bail out now
|
|
if (start.element === null || end.element === null)
|
|
return;
|
|
|
|
var range = document.createRange();
|
|
// If the text points are inside different <SPAN>s, we may not be able to
|
|
// surround them without breaking apart the elements to keep the DOM tree
|
|
// intact. Just move the selection points to encompass the contents of
|
|
// the SPANs.
|
|
if (start.element.parentNode.nodeName === "SPAN" &&
|
|
start.element !== end.element) {
|
|
range.setStartBefore(start.element.parentNode);
|
|
} else {
|
|
range.setStart(start.element, start.offset);
|
|
}
|
|
if (end.element.parentNode.nodeName === "SPAN" &&
|
|
start.element !== end.element) {
|
|
range.setEndAfter(end.element.parentNode);
|
|
} else {
|
|
range.setEnd(end.element, end.offset);
|
|
}
|
|
range.surroundContents(el);
|
|
}
|
|
// End any previous highlight before starting this one
|
|
jQuery(el)
|
|
.stop(true, true)
|
|
.effect("highlight", null, 1600);
|
|
}
|
|
|
|
// If this is the main Shiny window, wire up our custom message handler.
|
|
if (window.Shiny) {
|
|
Shiny.addCustomMessageHandler('showcase-src', function(message) {
|
|
if (message.srcref && message.srcfile) {
|
|
highlightSrcref(message.srcref, message.srcfile);
|
|
}
|
|
});
|
|
}
|
|
|
|
var isCodeAbove = false;
|
|
var setCodePosition = function(above, animate) {
|
|
var animateCodeMs = animate ? animateMs : 1;
|
|
|
|
// set the source and targets for the tab move
|
|
var newHostElement = above ?
|
|
document.getElementById("showcase-sxs-code") :
|
|
document.getElementById("showcase-code-inline");
|
|
var currentHostElement = above ?
|
|
document.getElementById("showcase-code-inline") :
|
|
document.getElementById("showcase-sxs-code");
|
|
|
|
var metadataElement = document.getElementById("showcase-app-metadata");
|
|
if (metadataElement === null) {
|
|
// if there's no app metadata, show and hide the entire well container
|
|
// when the code changes position
|
|
var wellElement = $("#showcase-well");
|
|
if (above) {
|
|
wellElement.fadeOut(animateCodeMs);
|
|
} else {
|
|
wellElement.fadeIn(animateCodeMs);
|
|
}
|
|
}
|
|
|
|
// hide the new element before doing anything to it
|
|
$(newHostElement).hide();
|
|
$(currentHostElement).fadeOut(animateCodeMs, function() {
|
|
var tabs = document.getElementById("showcase-code-tabs");
|
|
currentHostElement.removeChild(tabs);
|
|
newHostElement.appendChild(tabs);
|
|
|
|
// remove or set the height of the code
|
|
if (above) {
|
|
setCodeHeightFromDocHeight();
|
|
} else {
|
|
document.getElementById("showcase-code-content").removeAttribute("style");
|
|
}
|
|
|
|
$(newHostElement).fadeIn(animateCodeMs);
|
|
if (!above) {
|
|
// remove the applied width and zoom on the app container, and
|
|
// scroll smoothly down to the code's new home
|
|
document.getElementById("showcase-app-container").removeAttribute("style");
|
|
if (animate)
|
|
$(document.body).animate({ scrollTop: $(newHostElement).offset().top });
|
|
}
|
|
// if there's a readme, move it either alongside the code or beneath
|
|
// the app
|
|
var readme = document.getElementById("readme-md");
|
|
if (readme !== null) {
|
|
readme.parentElement.removeChild(readme);
|
|
if (above) {
|
|
currentHostElement.appendChild(readme);
|
|
$(currentHostElement).fadeIn(animateCodeMs);
|
|
}
|
|
else
|
|
document.getElementById("showcase-app-metadata").appendChild(readme);
|
|
}
|
|
|
|
// change the text on the toggle button to reflect the new state
|
|
document.getElementById("showcase-code-position-toggle").innerHTML = above ?
|
|
'<i class="fa fa-level-down"></i> show below' :
|
|
'<i class="fa fa-level-up"></i> show with app';
|
|
});
|
|
if (above) {
|
|
$(document.body).animate({ scrollTop: 0 }, animateCodeMs);
|
|
}
|
|
isCodeAbove = above;
|
|
setAppCodeSxsWidths(above && animate);
|
|
$(window).trigger("resize");
|
|
};
|
|
|
|
var setAppCodeSxsWidths = function(animate) {
|
|
var appTargetWidth = 960;
|
|
var appWidth = appTargetWidth;
|
|
var zoom = 1.0;
|
|
var totalWidth = document.getElementById("showcase-app-code").offsetWidth;
|
|
if (totalWidth / 2 > appTargetWidth) {
|
|
// If the app can use only half the available space and still meet its
|
|
// target, take half the available space.
|
|
appWidth = totalWidth / 2;
|
|
} else if (totalWidth * 0.66 > appTargetWidth) {
|
|
// If the app can meet its target by taking up more space (up to 66%
|
|
// of its container), take up more space.
|
|
appWidth = 960;
|
|
} else {
|
|
// The space is too narrow for the app and code to live side-by-side
|
|
// in a friendly way. Keep the app at 2/3 of the space but scale it.
|
|
appWidth = totalWidth * 0.66;
|
|
zoom = appWidth/appTargetWidth;
|
|
}
|
|
var app = document.getElementById("showcase-app-container");
|
|
$(app).animate({
|
|
width: appWidth + "px",
|
|
zoom: (zoom*100) + "%"
|
|
}, animate ? animateMs : 0);
|
|
};
|
|
|
|
var toggleCodePosition = function() {
|
|
setCodePosition(!isCodeAbove, true);
|
|
};
|
|
|
|
// if the browser is sized to wider than 1350px, show the code next to the
|
|
// app by default
|
|
var setInitialCodePosition = function() {
|
|
if (document.body.offsetWidth > 1350) {
|
|
setCodePosition(true, false);
|
|
}
|
|
};
|
|
|
|
// make the code scrollable to about the height of the browser, less space
|
|
// for the tabs
|
|
var setCodeHeightFromDocHeight = function() {
|
|
document.getElementById("showcase-code-content").style.height =
|
|
$(window).height() + "px";
|
|
};
|
|
|
|
// if there's a block of markdown content, render it to HTML
|
|
var renderMarkdown = function() {
|
|
var mdContent = document.getElementById("showcase-markdown-content");
|
|
if (mdContent !== null) {
|
|
// IE8 puts the content of <script> tags into innerHTML but
|
|
// not innerText
|
|
var content = mdContent.innerText || mdContent.innerHTML;
|
|
document.getElementById("readme-md").innerHTML =
|
|
(new Showdown.converter()).makeHtml(content)
|
|
}
|
|
}
|
|
|
|
$(window).resize(function() {
|
|
if (isCodeAbove) {
|
|
setAppCodeSxsWidths(false);
|
|
setCodeHeightFromDocHeight();
|
|
}
|
|
});
|
|
|
|
window.toggleCodePosition = toggleCodePosition;
|
|
|
|
$(window).on("load", setInitialCodePosition);
|
|
$(window).on("load", renderMarkdown);
|
|
|
|
if (window.hljs)
|
|
hljs.initHighlightingOnLoad();
|
|
})();
|