/**********************************************************
* abapGit JavaScript Function Library
**********************************************************/
/**********************************************************
Global variables used from outside
**********************************************************/
/* exported setInitialFocus */
/* exported setInitialFocusWithQuerySelector */
/* exported submitFormById */
/* exported errorStub */
/* exported confirmInitialized */
/* exported perfOut */
/* exported perfLog */
/* exported perfClear */
/* exported enableArrowListNavigation */
/* exported activateLinkHints */
/* exported setKeyBindings */
/* exported preparePatch */
/* exported registerStagePatch */
/* exported toggleRepoListDetail */
/* exported onTagTypeChange */
/* exported getIndocStyleSheet */
/* exported addMarginBottom */
/* exported enumerateJumpAllFiles */
/* exported createRepoCatalogEnumerator */
/* exported enumerateUiActions */
/* exported onDiffCollapse */
/* exported restoreScrollPosition */
/* exported toggleBrowserControlWarning */
/* exported displayBrowserControlFooter */
/**********************************************************
* Polyfills
**********************************************************/
// Bind polyfill (for IE7), taken from https://developer.mozilla.org/
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
throw new TypeError("Function.prototype.bind - subject is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function() { };
var fBound = function() {
return fToBind.apply(
this instanceof fNOP ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
);
};
if (this.prototype) {
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
// String includes polyfill, taken from https://developer.mozilla.org
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
"use strict";
if (typeof start !== "number") {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
};
}
// String startsWith polyfill, taken from https://developer.mozilla.org
if (!String.prototype.startsWith) {
Object.defineProperty(String.prototype, "startsWith", {
value: function(search, pos) {
pos = !pos || pos < 0 ? 0 : +pos;
return this.substring(pos, pos + search.length) === search;
}
});
}
// forEach polyfill, taken from https://developer.mozilla.org
// used for querySelectorAll results
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
/**********************************************************
* Common functions
**********************************************************/
// Output text to the debug div
function debugOutput(text, dstID) {
var stdout = document.getElementById(dstID || "debug-output");
var wrapped = "
" + text + "
";
stdout.innerHTML = stdout.innerHTML + wrapped;
}
// Use a supplied form, a pre-created form or create a hidden form
// and submit with sapevent
function submitSapeventForm(params, action, method, form) {
function getSapeventPrefix() {
// Depending on the used browser control and its version, different URL schemes
// are used which we distinguish here
if (document.querySelector('a[href*="file:///SAPEVENT:"]')) {
// Prefix for old (SAPGUI <= 8.00 PL3) chromium based browser control
return "file:///";
} else if (document.querySelector('a[href^="sap-cust"]')) {
// Prefix for new (SAPGUI >= 8.00 PL3 Hotfix 1) chromium based browser control
return "sap-cust://sap-place-holder/";
} else {
return ""; // No prefix for old IE control
}
}
var stub_form_id = "form_" + action;
form = form
|| document.getElementById(stub_form_id)
|| document.createElement("form");
form.setAttribute("method", method || "post");
if (/sapevent/i.test(action)) {
form.setAttribute("action", action);
} else {
form.setAttribute("action", getSapeventPrefix() + "SAPEVENT:" + action);
}
for (var key in params) {
var hiddenField = document.createElement("input");
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("name", key);
hiddenField.setAttribute("value", params[key]);
form.appendChild(hiddenField);
}
var formExistsInDOM = form.id && Boolean(document.querySelector("#" + form.id));
if (form.id !== stub_form_id && !formExistsInDOM) {
document.body.appendChild(form);
}
form.submit();
}
// Set focus to a control
function setInitialFocus(id) {
document.getElementById(id).focus();
}
// Set focus to a element with query selector
function setInitialFocusWithQuerySelector(sSelector, bFocusParent) {
var oSelected = document.querySelector(sSelector);
if (oSelected) {
if (bFocusParent) {
oSelected.parentElement.focus();
} else {
oSelected.focus();
}
}
}
// Submit an existing form
function submitFormById(id) {
document.getElementById(id).submit();
}
// JS error stub
function errorStub(event) {
var element = event.target || event.srcElement;
var targetName = element.id || element.name || "???";
alert("JS Error, please log an issue (@" + targetName + ")");
}
// Confirm JS initialization
function confirmInitialized() {
var errorBanner = document.getElementById("js-error-banner");
if (errorBanner) {
errorBanner.style.display = "none";
}
debugOutput("js: OK"); // Final final confirmation :)
}
/**********************************************************
* Performance utils (for debugging)
**********************************************************/
var gPerf = [];
function perfOut(prefix) {
var totals = {};
for (var i = gPerf.length - 1; i >= 0; i--) {
if (!totals[gPerf[i].name]) totals[gPerf[i].name] = { count: 0, time: 0 };
totals[gPerf[i].name].time += gPerf[i].time;
totals[gPerf[i].name].count += 1;
}
var keys = Object.keys(totals);
for (var j = keys.length - 1; j >= 0; j--) {
console.log(prefix
+ " " + keys[j] + ": "
+ totals[keys[j]].time.toFixed(3) + "ms"
+ " (" + totals[keys[j]].count.toFixed() + ")");
}
}
function perfLog(name, startTime) {
gPerf.push({ name: name, time: window.performance.now() - startTime });
}
function perfClear() {
gPerf = [];
}
/**********************************************************
* Repo Overview Logic
**********************************************************/
function findStyleSheetByName(name) {
for (var s = 0; s < document.styleSheets.length; s++) {
var styleSheet = document.styleSheets[s];
var classes = styleSheet.cssRules || styleSheet.rules;
for (var i = 0; i < classes.length; i++) {
if (classes[i].selectorText === name) return classes[i];
}
}
}
function getIndocStyleSheet() {
for (var s = 0; s < document.styleSheets.length; s++) {
if (!document.styleSheets[s].href) return document.styleSheets[s]; // One with empty href
}
// None found ? create one
var style = document.createElement("style");
document.head.appendChild(style);
return style.sheet;
}
function RepoOverViewHelper(opts) {
if (opts && opts.focusFilterKey) {
this.focusFilterKey = opts.focusFilterKey;
}
this.setHooks();
this.pageId = "RepoOverViewHelperState"; // constant is OK for this case
this.isDetailsDisplayed = false;
this.isOnlyFavoritesDisplayed = false;
this.detailCssClass = findStyleSheetByName(".repo-overview .ro-detail");
var icon = document.getElementById("icon-filter-detail");
this.toggleFilterIcon(icon, this.isDetailsDisplayed);
this.registerRowSelection();
this.registerKeyboardShortcuts();
}
RepoOverViewHelper.prototype.setHooks = function() {
window.onload = this.onPageLoad.bind(this);
};
RepoOverViewHelper.prototype.onPageLoad = function() {
var data = window.localStorage && JSON.parse(window.localStorage.getItem(this.pageId));
if (data) {
if (data.isDetailsDisplayed) {
this.toggleItemsDetail(true);
}
if (data.selectedRepoKey) {
this.selectRowByRepoKey(data.selectedRepoKey);
} else {
this.selectRowByIndex(0);
}
}
};
RepoOverViewHelper.prototype.registerKeyboardShortcuts = function() {
var self = this;
document.addEventListener("keypress", function(event) {
if (document.activeElement.id === "filter") {
return;
}
if (self.focusFilterKey && event.key === self.focusFilterKey && !CommandPalette.isVisible()) {
var filterInput = document.getElementById("filter");
if (filterInput) filterInput.focus();
event.preventDefault();
return;
}
var keycode = event.keyCode;
var rows = Array.prototype.slice.call(self.getVisibleRows());
var selected = document.querySelector(".repo-overview tr.selected");
var indexOfSelected = rows.indexOf(selected);
var lastRow = rows.length - 1;
if (keycode == 13 && document.activeElement.tagName.toLowerCase() != "input") {
// "enter" to open, unless command field has focus
self.openSelectedRepo();
} else if ((keycode == 52 || keycode == 56) && indexOfSelected > 0) {
// "4,8" for previous, digits are the numlock keys
// NB: numpad must be activated, keypress does not detect arrows
// if we need arrows it will be keydown. But then mind the keycodes, they may change !
// e.g. 100 is 'd' with keypress (and conflicts with diff hotkey), and also it is arrow-left keydown
self.selectRowByIndex(indexOfSelected - 1);
} else if ((keycode == 54 || keycode == 50) && indexOfSelected < lastRow) {
// "6,2" for next
self.selectRowByIndex(indexOfSelected + 1);
}
});
};
RepoOverViewHelper.prototype.openSelectedRepo = function() {
this.selectedRepoKey = document.querySelector(".repo-overview tr.selected").dataset.key;
this.saveLocalStorage();
document.querySelector(".repo-overview tr.selected td.ro-go a").click();
};
RepoOverViewHelper.prototype.selectRowByIndex = function(index) {
var rows = this.getVisibleRows();
if (rows.length >= index) {
var selectedRow = rows[index];
if (selectedRow.classList.contains("selected")) {
return;
}
this.deselectAllRows();
rows[index].classList.add("selected");
this.selectedRepoKey = selectedRow.dataset.key;
this.updateActionLinks(selectedRow);
this.saveLocalStorage();
}
};
RepoOverViewHelper.prototype.selectRowByRepoKey = function(key) {
var attributeQuery = "[data-key='" + key + "']";
var row = document.querySelector(".repo-overview tbody tr" + attributeQuery);
// navigation to already selected repo
if (row.dataset.key === key && row.classList.contains("selected")) {
return;
}
this.deselectAllRows();
row.classList.add("selected");
this.selectedRepoKey = key;
this.updateActionLinks(row);
this.saveLocalStorage();
};
RepoOverViewHelper.prototype.updateActionLinks = function(selectedRow) {
// now we have a repo selected, determine which action buttons are relevant
var selectedRepoKey = selectedRow.dataset.key;
var selectedRepoIsOffline = selectedRow.dataset.offline === "X";
var actionLinks = document.querySelectorAll("a.action_link");
actionLinks.forEach(function(link) {
// adjust repo key in urls
link.href = link.href.replace(/\?key=(#|\d+)/, "?key=" + selectedRepoKey);
// toggle button visibility
if (link.classList.contains("action_offline_repo")) {
if (selectedRepoIsOffline) {
link.parentElement.classList.add("enabled");
} else {
link.parentElement.classList.remove("enabled");
}
}
else if (link.classList.contains("action_online_repo")) {
if (!selectedRepoIsOffline) {
link.parentElement.classList.add("enabled");
} else {
link.parentElement.classList.remove("enabled");
}
}
else {
// if the action is for both repository types, it will only have the .action_link class
// it still needs to be toggled as we want to hide everything if no repo is selected
link.parentElement.classList.add("enabled");
}
});
};
RepoOverViewHelper.prototype.deselectAllRows = function() {
document.querySelectorAll(".repo-overview tbody tr").forEach(function(x) {
x.classList.remove("selected");
});
};
RepoOverViewHelper.prototype.getVisibleRows = function() {
return document.querySelectorAll(".repo-overview tbody tr:not(.nodisplay)");
};
RepoOverViewHelper.prototype.registerRowSelection = function() {
var self = this;
document.querySelectorAll(".repo-overview tr td:not(.ro-go)").forEach(function(repoListRowCell) {
repoListRowCell.addEventListener("click", function() {
self.selectRowByRepoKey(this.parentElement.dataset.key);
});
});
document.querySelectorAll(".repo-overview tr td.ro-go").forEach(function(openRepoIcon) {
openRepoIcon.addEventListener("click", function() {
var selectedRow = this.parentElement;
self.selectRowByRepoKey(selectedRow.dataset.key);
self.openSelectedRepo();
});
});
};
RepoOverViewHelper.prototype.toggleRepoListDetail = function(forceDisplay) {
if (this.detailCssClass) {
this.toggleItemsDetail(forceDisplay);
this.saveLocalStorage();
}
};
RepoOverViewHelper.prototype.toggleItemsDetail = function(forceDisplay) {
if (this.detailCssClass) {
this.isDetailsDisplayed = forceDisplay || !this.isDetailsDisplayed;
// change layout to wide if details are displayed
if (this.isDetailsDisplayed) {
document.body.classList.remove("centered");
document.body.classList.add("full_width");
} else {
document.body.classList.add("centered");
document.body.classList.remove("full_width");
}
this.detailCssClass.style.display = this.isDetailsDisplayed ? "" : "none";
var icon = document.getElementById("icon-filter-detail");
this.toggleFilterIcon(icon, this.isDetailsDisplayed);
}
};
RepoOverViewHelper.prototype.toggleFilterIcon = function(icon, isEnabled) {
if (isEnabled) {
icon.classList.remove("grey");
icon.classList.add("blue");
} else {
icon.classList.remove("blue");
icon.classList.add("grey");
}
};
RepoOverViewHelper.prototype.saveLocalStorage = function() {
if (!window.localStorage) return;
var data = {
isDetailsDisplayed : this.isDetailsDisplayed,
isOnlyFavoritesDisplayed: this.isOnlyFavoritesDisplayed,
selectedRepoKey : this.selectedRepoKey,
};
window.localStorage.setItem(this.pageId, JSON.stringify(data));
};
/**********************************************************
* Staging Logic
**********************************************************/
// Stage helper constructor
function StageHelper(params) {
this.pageSeed = params.seed;
this.formAction = params.formAction;
this.patchAction = params.patchAction;
this.user = params.user;
this.ids = params.ids;
this.selectedCount = 0;
this.filteredCount = 0;
this.lastFilterValue = "";
this.focusFilterKey = params.focusFilterKey;
// DOM nodes
this.dom = {
stageTab : document.getElementById(params.ids.stageTab),
commitAllBtn : document.getElementById(params.ids.commitAllBtn),
commitSelectedBtn: document.getElementById(params.ids.commitSelectedBtn),
commitFilteredBtn: document.getElementById(params.ids.commitFilteredBtn),
patchBtn : document.getElementById(params.ids.patchBtn),
objectSearch : document.getElementById(params.ids.objectSearch),
selectedCounter : null,
filteredCounter : null,
};
this.findCounters();
// Table columns (autodetection)
this.colIndex = this.detectColumns();
this.filterTargets = ["name", "user", "transport"];
// Constants
this.HIGHLIGHT_STYLE = "highlight";
this.STATUS = {
add : "A",
remove : "R",
ignore : "I",
reset : "?",
isValid: function(status) { return "ARI?".indexOf(status) == -1 }
};
this.TEMPLATES = {
cmdReset : "reset",
cmdLocal : "add",
cmdRemote: "ignoreremove"
};
this.setHooks();
if (this.user) this.injectFilterMe();
Hotkeys.addHotkeyToHelpSheet("^Enter", "Commit");
}
StageHelper.prototype.findCounters = function() {
this.dom.selectedCounter = this.dom.commitSelectedBtn.querySelector("span.counter");
this.dom.filteredCounter = this.dom.commitFilteredBtn.querySelector("span.counter");
};
StageHelper.prototype.injectFilterMe = function() {
var tabFirstHead = this.dom.stageTab.tHead.rows[0];
if (!tabFirstHead || tabFirstHead.className !== "local") {
return; // for the case only "remove part" is displayed
}
var changedByHead = tabFirstHead.cells[this.colIndex.user];
changedByHead.innerText = changedByHead.innerText + " (";
var a = document.createElement("A");
a.appendChild(document.createTextNode("me"));
a.onclick = this.onFilterMe.bind(this);
a.href = "#";
changedByHead.appendChild(a);
changedByHead.appendChild(document.createTextNode(")"));
};
StageHelper.prototype.onFilterMe = function() {
this.dom.objectSearch.value = this.user;
this.onFilter({ type: "keypress", which: 13, target: this.dom.objectSearch });
};
// Hook global click listener on table, load/unload actions
StageHelper.prototype.setHooks = function() {
window.onkeypress = this.onCtrlEnter.bind(this);
this.dom.stageTab.onclick = this.onTableClick.bind(this);
this.dom.commitSelectedBtn.onclick = this.submit.bind(this);
this.dom.commitFilteredBtn.onclick = this.submitVisible.bind(this);
this.dom.patchBtn.onclick = this.submitPatch.bind(this);
this.dom.objectSearch.oninput = this.onFilter.bind(this);
this.dom.objectSearch.onkeypress = this.onFilter.bind(this);
window.onbeforeunload = this.onPageUnload.bind(this);
window.onload = this.onPageLoad.bind(this);
var self = this;
document.addEventListener("keypress", function(event) {
if (document.activeElement.id !== self.ids.objectSearch
&& self.focusFilterKey && event.key === self.focusFilterKey
&& !CommandPalette.isVisible()) {
self.dom.objectSearch.focus();
event.preventDefault();
}
});
};
// Detect column index
StageHelper.prototype.detectColumns = function() {
var dataRow = this.dom.stageTab.tBodies[0].rows[0];
var colIndex = {};
for (var i = dataRow.cells.length - 1; i >= 0; i--) {
if (dataRow.cells[i].className) colIndex[dataRow.cells[i].className] = i;
}
return colIndex;
};
// Store table state on leaving the page
StageHelper.prototype.onPageUnload = function() {
if (!window.sessionStorage) return;
var data = this.collectData();
window.sessionStorage.setItem(this.pageSeed, JSON.stringify(data));
};
// Re-store table state on entering the page
StageHelper.prototype.onPageLoad = function() {
var data = window.sessionStorage && JSON.parse(window.sessionStorage.getItem(this.pageSeed));
this.iterateStageTab(true, function(row) {
var status = data && data[row.cells[this.colIndex["name"]].innerText];
this.updateRow(row, status || this.STATUS.reset);
});
this.updateMenu();
if (this.dom.objectSearch.value) {
this.applyFilterValue(this.dom.objectSearch.value);
}
};
// Table event handler, change status
StageHelper.prototype.onTableClick = function(event) {
var target = event.target || event.srcElement;
if (!target) return;
var td;
if (target.tagName === "A") {
td = target.parentNode;
} else if (target.tagName === "TD") {
td = target;
if (td.children.length === 1 && td.children[0].tagName === "A") {
target = td.children[0];
} else return;
} else return;
if (["TD", "TH"].indexOf(td.tagName) == -1 || td.className != "cmd") return;
var status = this.STATUS[target.innerText]; // Convert anchor text to status
var targetRow = td.parentNode;
if (td.tagName === "TD") {
this.updateRow(targetRow, status);
} else { // TH
this.iterateStageTab(true, function(row) {
if (row.style.display !== "none" // Not filtered out
&& row.className === targetRow.className // Same context as header
) {
this.updateRow(row, status);
}
});
}
this.updateMenu();
};
StageHelper.prototype.onCtrlEnter = function(e) {
if (e.ctrlKey && (e.which === 10 || e.key === "Enter")) {
var clickMap = {
"default" : this.dom.commitAllBtn,
"selected": this.dom.commitSelectedBtn,
"filtered": this.dom.commitFilteredBtn
};
clickMap[this.calculateActiveCommitCommand()].click();
}
};
// Search object
StageHelper.prototype.onFilter = function(e) {
if ( // Enter hit or clear, IE SUCKS !
e.type === "input" && !e.target.value && this.lastFilterValue
|| e.type === "keypress" && (e.which === 13 || e.key === "Enter") && !e.ctrlKey) {
this.applyFilterValue(e.target.value);
submitSapeventForm({ filterValue: e.target.value }, "stage_filter", "post");
}
};
StageHelper.prototype.applyFilterValue = function(sFilterValue) {
this.lastFilterValue = sFilterValue;
this.filteredCount = this.iterateStageTab(true, this.applyFilterToRow, sFilterValue);
this.updateMenu();
};
// Apply filter to a single stage line - hide or show
StageHelper.prototype.applyFilterToRow = function(row, filter) {
// Collect data cells
var targets = this.filterTargets.map(function(attr) {
// Get the innermost tag with the text we want to filter
// text | : elem = td-tag
// text | : elem = a-tag
var elem = row.cells[this.colIndex[attr]];
var elemA = elem.getElementsByTagName("A")[0];
if (elemA) elem = elemA;
return {
elem : elem,
plainText: elem.innerText.replace(/ /g, "\u00a0"), // without tags, with encoded spaces
curHtml : elem.innerHTML
};
}, this);
var isVisible = false;
// Apply filter to cells, mark filtered text
for (var i = targets.length - 1; i >= 0; i--) {
var target = targets[i];
// Ignore case of filter
var regFilter = new RegExp("(" + filter + ")", "gi");
target.newHtml = (filter)
? target.plainText.replace(regFilter, "$1")
: target.plainText;
target.isChanged = target.newHtml !== target.curHtml;
isVisible = isVisible || !filter || target.newHtml !== target.plainText;
}
// Update DOM
row.style.display = isVisible ? "" : "none";
for (var j = targets.length - 1; j >= 0; j--) {
if (targets[j].isChanged) targets[j].elem.innerHTML = targets[j].newHtml;
}
return isVisible ? 1 : 0;
};
// Get how status should affect object counter
StageHelper.prototype.getStatusImpact = function(status) {
if (typeof status !== "string"
|| status.length !== 1
|| this.STATUS.isValid(status)) {
alert("Unknown status");
} else {
return (status !== this.STATUS.reset) ? 1: 0;
}
};
// Update table line
StageHelper.prototype.updateRow = function(row, newStatus) {
var oldStatus = row.cells[this.colIndex["status"]].innerText;
if (oldStatus !== newStatus) {
this.updateRowStatus(row, newStatus);
this.updateRowCommand(row, newStatus);
} else if (!row.cells[this.colIndex["cmd"]].children.length) {
this.updateRowCommand(row, newStatus); // For initial run
}
this.selectedCount += this.getStatusImpact(newStatus) - this.getStatusImpact(oldStatus);
};
// Update Status cell (render set of commands)
StageHelper.prototype.updateRowStatus = function(row, status) {
row.cells[this.colIndex["status"]].innerText = status;
if (status === this.STATUS.reset) {
row.cells[this.colIndex["status"]].classList.remove(this.HIGHLIGHT_STYLE);
} else {
row.cells[this.colIndex["status"]].classList.add(this.HIGHLIGHT_STYLE);
}
};
// Update Command cell (render set of commands)
StageHelper.prototype.updateRowCommand = function(row, status) {
var cell = row.cells[this.colIndex["cmd"]];
if (status === this.STATUS.reset) {
cell.innerHTML = (row.className == "local")
? this.TEMPLATES.cmdLocal
: this.TEMPLATES.cmdRemote;
} else {
cell.innerHTML = this.TEMPLATES.cmdReset;
}
};
StageHelper.prototype.calculateActiveCommitCommand = function() {
var active;
if (this.selectedCount > 0) {
active = "selected";
} else if (this.lastFilterValue) {
active = "filtered";
} else {
active = "default";
}
return active;
};
// Update menu items visibility
StageHelper.prototype.updateMenu = function() {
var display = this.calculateActiveCommitCommand();
if (display === "selected") this.dom.selectedCounter.innerText = this.selectedCount.toString();
if (display === "filtered") this.dom.filteredCounter.innerText = this.filteredCount.toString();
this.dom.commitAllBtn.style.display = display === "default" ? "" : "none";
this.dom.commitSelectedBtn.style.display = display === "selected" ? "" : "none";
this.dom.commitFilteredBtn.style.display = display === "filtered" ? "" : "none";
};
// Submit stage state to the server
StageHelper.prototype.submit = function() {
submitSapeventForm(this.collectData(), this.formAction);
};
StageHelper.prototype.submitVisible = function() {
this.markVisiblesAsAdded();
submitSapeventForm(this.collectData(), this.formAction);
};
StageHelper.prototype.submitPatch = function() {
submitSapeventForm(this.collectData(), this.patchAction);
};
// Extract data from the table
StageHelper.prototype.collectData = function() {
var data = {};
this.iterateStageTab(false, function(row) {
data[row.cells[this.colIndex["name"]].innerText] = row.cells[this.colIndex["status"]].innerText;
});
return data;
};
StageHelper.prototype.markVisiblesAsAdded = function() {
this.iterateStageTab(false, function(row) {
// TODO refacotr, unify updateRow logic
if (row.style.display === "" && row.className === "local") { // visible
this.updateRow(row, this.STATUS.add);
} else {
this.updateRow(row, this.STATUS.reset);
}
});
};
// Table iteration helper
StageHelper.prototype.iterateStageTab = function(changeMode, cb /*, ...*/) {
var restArgs = Array.prototype.slice.call(arguments, 2);
var table = this.dom.stageTab;
var retTotal = 0;
if (changeMode) {
var scrollOffset = window.pageYOffset;
this.dom.stageTab.style.display = "none";
}
for (var b = 0, bN = table.tBodies.length; b < bN; b++) {
var tbody = table.tBodies[b];
for (var r = 0, rN = tbody.rows.length; r < rN; r++) {
var args = [tbody.rows[r]].concat(restArgs);
var retVal = cb.apply(this, args); // callback
if (typeof retVal === "number") retTotal += retVal;
}
}
if (changeMode) {
this.dom.stageTab.style.display = "";
window.scrollTo(0, scrollOffset);
}
return retTotal;
};
/**********************************************************
* Check List Wrapper
**********************************************************/
function CheckListWrapper(id, cbAction, cbActionOnlyMyChanges) {
this.id = document.getElementById(id);
this.cbAction = cbAction;
this.cbActionOnlyMyChanges = cbActionOnlyMyChanges;
this.id.onclick = this.onClick.bind(this);
}
CheckListWrapper.prototype.onClick = function(e) { // eslint-disable-line no-unused-vars
// Get nodes
var target = event.target || event.srcElement;
if (!target) return;
if (target.tagName !== "A") { target = target.parentNode } // icon clicked
if (target.tagName !== "A") return;
if (target.parentNode.tagName !== "LI") return;
var nodeA = target;
var nodeLi = target.parentNode;
var nodeIcon = target.children[0];
if (!nodeIcon.classList.contains("icon")) return;
// Node updates
var option = nodeA.innerText;
var oldState = nodeLi.getAttribute("data-check");
if (oldState === null) return; // no data-check attribute - non-checkbox
var newState = oldState === "X" ? false : true;
if (newState) {
nodeIcon.classList.remove("grey");
nodeIcon.classList.add("blue");
nodeLi.setAttribute("data-check", "X");
} else {
nodeIcon.classList.remove("blue");
nodeIcon.classList.add("grey");
nodeLi.setAttribute("data-check", "");
}
// Action callback, special handling for "Only My Changes"
if (option === "Only my changes") {
this.cbActionOnlyMyChanges(nodeLi.getAttribute("data-aux"), newState);
// hide "Changed By" menu
} else {
this.cbAction(nodeLi.getAttribute("data-aux"), option, newState);
}
};
/**********************************************************
* Diff Page Logic
**********************************************************/
// Diff helper constructor
function DiffHelper(params) {
this.pageSeed = params.seed;
this.counter = 0;
this.stageAction = params.stageAction;
// DOM nodes
this.dom = {
diffList : document.getElementById(params.ids.diffList),
stageButton: document.getElementById(params.ids.stageButton)
};
this.repoKey = this.dom.diffList.getAttribute("data-repo-key");
if (!this.repoKey) return; // Unexpected
this.dom.jump = document.getElementById(params.ids.jump);
this.dom.jump.onclick = this.onJump.bind(this);
// Checklist wrapper
if (document.getElementById(params.ids.filterMenu)) {
this.checkList = new CheckListWrapper(params.ids.filterMenu, this.onFilter.bind(this), this.onFilterOnlyMyChanges.bind(this));
this.dom.filterButton = document.getElementById(params.ids.filterMenu).parentNode;
}
// Hijack stage command
if (this.dom.stageButton) {
this.dom.stageButton.href = "#";
this.dom.stageButton.onclick = this.onStage.bind(this);
}
}
// Action on jump click
DiffHelper.prototype.onJump = function(e) {
var text = ((e.target && e.target.text) || e);
if (!text) return;
var elFile = document.querySelector("[data-file*='" + text + "']");
if (!elFile) return;
setTimeout(function() {
elFile.scrollIntoView();
}, 100);
};
// Action on filter click
DiffHelper.prototype.onFilter = function(attr, target, state) {
this.applyFilter(attr, target, state);
this.highlightButton(state);
};
DiffHelper.prototype.onFilterOnlyMyChanges = function(username, state) {
this.applyOnlyMyChangesFilter(username, state);
this.counter = 0;
if (state) {
this.dom.filterButton.classList.add("bgorange");
} else {
this.dom.filterButton.classList.remove("bgorange");
}
// apply logic on Changed By list items
var changedByListItems = Array.prototype.slice.call(document.querySelectorAll("[data-aux*=changed-by]"));
changedByListItems
.map(function(item) {
var nodeIcon = item.children[0].children[0];
if (state === true) {
if (item.innerText === username) { // current user
item.style.display = "";
item.setAttribute("data-check", "X");
if (nodeIcon) {
nodeIcon.classList.remove("grey");
nodeIcon.classList.add("blue");
}
} else { // other users
item.style.display = "none";
item.setAttribute("data-check", "");
}
} else {
item.style.display = "";
item.setAttribute("data-check", "X");
if (nodeIcon) {
nodeIcon.classList.remove("grey");
nodeIcon.classList.add("blue");
}
}
});
};
DiffHelper.prototype.applyOnlyMyChangesFilter = function(username, state) {
var jumpListItems = Array.prototype.slice.call(document.querySelectorAll("[id*=li_jump]"));
this.iterateDiffList(function(div) {
if (state === true) { // switching on "Only my changes" filter
if (div.getAttribute("data-changed-by") === username) {
div.style.display = state ? "" : "none";
} else {
div.style.display = state ? "none" : "";
}
} else { // disabling
div.style.display = "";
}
// hide the file in the jump list
var dataFile = div.getAttribute("data-file");
jumpListItems
.filter(function(item) { return dataFile.includes(item.text) })
.map(function(item) { item.style.display = div.style.display });
});
};
// Hide/show diff based on params
DiffHelper.prototype.applyFilter = function(attr, target, state) {
var jumpListItems = Array.prototype.slice.call(document.querySelectorAll("[id*=li_jump]"));
this.iterateDiffList(function(div) {
if (div.getAttribute("data-" + attr) === target) {
div.style.display = state ? "" : "none";
// hide the file in the jump list
var dataFile = div.getAttribute("data-file");
jumpListItems
.filter(function(item) { return dataFile.includes(item.text) })
.map(function(item) { item.style.display = div.style.display });
}
});
};
// Action on stage -> save visible diffs as state for stage page
DiffHelper.prototype.onStage = function(e) { // eslint-disable-line no-unused-vars
if (window.sessionStorage) {
var data = this.buildStageCache();
window.sessionStorage.setItem(this.pageSeed, JSON.stringify(data));
}
var getParams = { key: this.repoKey, seed: this.pageSeed };
submitSapeventForm(getParams, this.stageAction, "get");
};
// Collect visible diffs
DiffHelper.prototype.buildStageCache = function() {
var list = {};
this.iterateDiffList(function(div) {
var filename = div.getAttribute("data-file");
if (!div.style.display && filename) { // No display override - visible !!
list[filename] = "A"; // Add
}
});
return list;
};
// Table iterator
DiffHelper.prototype.iterateDiffList = function(cb /*, ...*/) {
var restArgs = Array.prototype.slice.call(arguments, 1);
var diffList = this.dom.diffList;
for (var i = 0, iN = diffList.children.length; i < iN; i++) {
var div = diffList.children[i];
if (div.className !== "diff") continue;
var args = [div].concat(restArgs);
cb.apply(this, args);// callback
}
};
// Highlight filter button if filter is activate
DiffHelper.prototype.highlightButton = function(state) {
this.counter += state ? -1 : 1;
if (this.counter > 0) {
this.dom.filterButton.classList.add("bgorange");
} else {
this.dom.filterButton.classList.remove("bgorange");
}
};
// Collapse or expand diffs
function onDiffCollapse(event) {
var source = event.target || event.srcElement;
var nextDiffContent = source.parentElement.nextElementSibling;
var hide;
if (source.classList.contains("icon-chevron-down")) {
source.classList.remove("icon-chevron-down");
source.classList.add("icon-chevron-right");
hide = true;
} else {
source.classList.remove("icon-chevron-right");
source.classList.add("icon-chevron-down");
hide = false;
}
hide ? nextDiffContent.classList.add("nodisplay"): nextDiffContent.classList.remove("nodisplay");
}
// Add bottom margin, so that we can scroll to the top of the last file
function addMarginBottom() {
document.getElementsByTagName("body")[0].style.marginBottom = screen.height + "px";
}
/**********************************************************
* Diff Page Column Selection
**********************************************************/
function DiffColumnSelection() {
this.selectedColumnIdx = -1;
this.lineNumColumnIdx = -1;
//https://stackoverflow.com/questions/2749244/javascript-setinterval-and-this-solution
document.addEventListener("mousedown", this.mousedownEventListener.bind(this));
document.addEventListener("copy", this.copyEventListener.bind(this));
}
DiffColumnSelection.prototype.mousedownEventListener = function(e) {
// Select text in a column of an HTML table and copy to clipboard (in DIFF view)
// (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
// Process mousedown event for all TD elements -> apply CSS class at TABLE level.
// (https://stackoverflow.com/questions/40956717/how-to-addeventlistener-to-multiple-elements-in-a-single-line)
var unifiedLineNumColumnIdx = 0;
var unifiedCodeColumnIdx = 3;
var splitLineNumLeftColumnIdx = 0;
var splitCodeLeftColumnIdx = 2;
var splitLineNumRightColumnIdx = 3;
var splitCodeRightColumnIdx = 5;
if (e.button !== 0) return; // function is only valid for left button, not right button
var td = e.target;
while (td != undefined && td.tagName != "TD" && td.tagName != "TBODY") td = td.parentElement;
if (td == undefined) return;
var table = td.parentElement.parentElement;
var patchColumnCount = 0;
if (td.parentElement.cells[0].classList.contains("patch")) {
patchColumnCount = 1;
}
if (td.classList.contains("diff_left")) {
table.classList.remove("diff_select_right");
table.classList.add("diff_select_left");
if (window.getSelection() && this.selectedColumnIdx != splitCodeLeftColumnIdx + patchColumnCount) {
// De-select to avoid effect of dragging selection in case the right column was first selected
if (document.body.createTextRange) { // All IE but Edge
// document.getSelection().removeAllRanges() may trigger error
// so use this code which is equivalent but does not fail
// (https://stackoverflow.com/questions/22914075/javascript-error-800a025e-using-range-selector)
range = document.body.createTextRange();
range.collapse();
range.select();
} else {
document.getSelection().removeAllRanges();
}
}
this.selectedColumnIdx = splitCodeLeftColumnIdx + patchColumnCount;
this.lineNumColumnIdx = splitLineNumLeftColumnIdx + patchColumnCount;
} else if (td.classList.contains("diff_right")) {
table.classList.remove("diff_select_left");
table.classList.add("diff_select_right");
if (window.getSelection() && this.selectedColumnIdx != splitCodeRightColumnIdx + patchColumnCount) {
if (document.body.createTextRange) { // All IE but Edge
// document.getSelection().removeAllRanges() may trigger error
// so use this code which is equivalent but does not fail
// (https://stackoverflow.com/questions/22914075/javascript-error-800a025e-using-range-selector)
var range = document.body.createTextRange();
range.collapse();
range.select();
} else {
document.getSelection().removeAllRanges();
}
}
this.selectedColumnIdx = splitCodeRightColumnIdx + patchColumnCount;
this.lineNumColumnIdx = splitLineNumRightColumnIdx + patchColumnCount;
} else if (td.classList.contains("diff_unified")) {
this.selectedColumnIdx = unifiedCodeColumnIdx;
this.lineNumColumnIdx = unifiedLineNumColumnIdx;
} else {
this.selectedColumnIdx = -1;
this.lineNumColumnIdx = -1;
}
};
DiffColumnSelection.prototype.copyEventListener = function(e) {
// Select text in a column of an HTML table and copy to clipboard (in DIFF view)
// (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
var td = e.target;
while (td != undefined && td.tagName != "TD" && td.tagName != "TBODY") td = td.parentElement;
if (td != undefined) {
// Use window.clipboardData instead of e.clipboardData
// (https://stackoverflow.com/questions/23470958/ie-10-copy-paste-issue)
var clipboardData = (e.clipboardData == undefined ? window.clipboardData : e.clipboardData);
var text = this.getSelectedText();
clipboardData.setData("text", text);
e.preventDefault();
}
};
DiffColumnSelection.prototype.getSelectedText = function() {
// Select text in a column of an HTML table and copy to clipboard (in DIFF view)
// (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var doc = range.cloneContents();
var nodes = doc.querySelectorAll("tr");
var text = "";
if (nodes.length === 0) {
text = doc.textContent;
} else {
var newline = "";
var realThis = this;
var copySide = "";
[].forEach.call(nodes, function(tr, i) {
var cellIdx = (i == 0 ? 0 : realThis.selectedColumnIdx);
if (tr.cells.length > cellIdx) {
var tdSelected = tr.cells[cellIdx];
// decide which side to copy based on first line of selection
if (i == 0) {
copySide = (tdSelected.classList.contains("new") ? "new" : "old" );
}
// copy is interesting only for one side of code, do not copy lines which exist on other side
if (i == 0 || copySide == "new" && !tdSelected.classList.contains("old") || copySide == "old" && !tdSelected.classList.contains("new")) {
text += newline + tdSelected.textContent;
// special processing for TD tag which sometimes contains newline
// (expl: /src/ui/zabapgit_js_common.w3mi.data.js) so do not add newline again in that case.
var lastChar = tdSelected.textContent[tdSelected.textContent.length - 1];
if (lastChar == "\n") newline = "";
else newline = "\n";
}
}
});
}
return text;
};
/**********************************************************
* Display Helper
**********************************************************/
// Toggle display of changelog (news) and message popups
function toggleDisplay(divId) {
var div = document.getElementById(divId);
if (div) div.style.display = (div.style.display) ? "" : "none";
}
/**********************************************************
* Keyboard Navigation
**********************************************************/
function KeyNavigation() { }
KeyNavigation.prototype.onkeydown = function(event) {
if (event.defaultPrevented) return;
// navigate with arrows through list items and support pressing links with enter and space
var isHandled = false;
if (event.key === "Enter" || event.key === "") {
isHandled = this.onEnterOrSpace();
} else if (/Down$/.test(event.key)) {
isHandled = this.onArrowDown();
} else if (/Up$/.test(event.key)) {
isHandled = this.onArrowUp();
} else if (event.key === "Backspace") {
isHandled = this.onBackspace();
}
if (isHandled) event.preventDefault();
};
KeyNavigation.prototype.onEnterOrSpace = function() {
if (document.activeElement.nodeName !== "A") return;
var anchor = document.activeElement;
if (anchor.href.replace(/#$/, "") === document.location.href.replace(/#$/, "")
&& !anchor.onclick
&& anchor.parentElement
&& anchor.parentElement.nodeName === "LI") {
anchor.parentElement.classList.toggle("force-nav-hover");
} else {
anchor.click();
}
return true;
};
KeyNavigation.prototype.focusListItem = function(li) {
var anchor = li.firstElementChild;
if (!anchor || anchor.nodeName !== "A") return false;
anchor.focus();
return true;
};
KeyNavigation.prototype.closeDropdown = function(dropdownLi) {
dropdownLi.classList.remove("force-nav-hover");
if (dropdownLi.firstElementChild.nodeName === "A") dropdownLi.firstElementChild.focus();
return true;
};
KeyNavigation.prototype.onBackspace = function() {
var activeElement = document.activeElement;
// Detect opened subsequent dropdown
if (activeElement.nodeName === "A"
&& activeElement.parentElement
&& activeElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.classList.contains("force-nav-hover")) {
return this.closeDropdown(activeElement.parentElement);
}
// Detect opened parent dropdown
if (activeElement.nodeName === "A"
&& activeElement.parentElement
&& activeElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.parentElement
&& activeElement.parentElement.parentElement.nodeName === "UL"
&& activeElement.parentElement.parentElement.parentElement
&& activeElement.parentElement.parentElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.parentElement.parentElement.classList.contains("force-nav-hover")) {
return this.closeDropdown(activeElement.parentElement.parentElement.parentElement);
}
};
KeyNavigation.prototype.onArrowDown = function() {
var activeElement = document.activeElement;
// Start of dropdown list: LI > selected A :: UL > LI > A
if (activeElement.nodeName === "A"
&& activeElement.parentElement
&& activeElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.classList.contains("force-nav-hover") // opened dropdown
&& activeElement.nextElementSibling
&& activeElement.nextElementSibling.nodeName === "UL"
&& activeElement.nextElementSibling.firstElementChild
&& activeElement.nextElementSibling.firstElementChild.nodeName === "LI") {
return this.focusListItem(activeElement.nextElementSibling.firstElementChild);
}
// Next item of dropdown list: ( LI > selected A ) :: LI > A
if (activeElement.nodeName === "A"
&& activeElement.parentElement
&& activeElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.nextElementSibling
&& activeElement.parentElement.nextElementSibling.nodeName === "LI") {
return this.focusListItem(activeElement.parentElement.nextElementSibling);
}
};
KeyNavigation.prototype.onArrowUp = function() {
var activeElement = document.activeElement;
// Prev item of dropdown list: ( LI > selected A ) <:: LI > A
if (activeElement.nodeName === "A"
&& activeElement.parentElement
&& activeElement.parentElement.nodeName === "LI"
&& activeElement.parentElement.previousElementSibling
&& activeElement.parentElement.previousElementSibling.nodeName === "LI") {
return this.focusListItem(activeElement.parentElement.previousElementSibling);
}
};
KeyNavigation.prototype.getHandler = function() {
return this.onkeydown.bind(this);
};
// this functions enables the navigation with arrows through list items (li)
// e.g. in dropdown menus
function enableArrowListNavigation() {
document.addEventListener("keydown", new KeyNavigation().getHandler());
}
/**********************************************************
* Link Hints (Vimium-like)
**********************************************************/
function LinkHints(linkHintHotKey) {
this.linkHintHotKey = linkHintHotKey;
this.areHintsDisplayed = false;
this.pendingPath = ""; // already typed code prefix
this.hintsMap = this.deployHintContainers();
this.activatedDropdown = null;
this.yankModeActive = false;
}
LinkHints.prototype.getHintStartValue = function(targetsCount) {
// e.g. if we have 89 tooltips we start from 10
// if we have 90 tooltips we start from 100
// if we have 900 tooltips we start from 1000
var
baseLength = Math.pow(10, targetsCount.toString().length - 1),
maxHintStringLength = (targetsCount + baseLength).toString().length;
return Math.pow(10, maxHintStringLength - 1);
};
LinkHints.prototype.deployHintContainers = function() {
var hintTargets = document.querySelectorAll("a, input, textarea, i");
var codeCounter = this.getHintStartValue(hintTargets.length);
var hintsMap = { first: codeCounter };
//
// 123
//
for (var i = 0, N = hintTargets.length; i < N; i++) {
// skip hidden fields
if (hintTargets[i].type === "HIDDEN") {
continue;
}
var hint = {};
hint.container = document.createElement("span");
hint.pendingSpan = document.createElement("span");
hint.remainingSpan = document.createElement("span");
hint.parent = hintTargets[i];
hint.code = codeCounter.toString();
hint.container.appendChild(hint.pendingSpan);
hint.container.appendChild(hint.remainingSpan);
hint.pendingSpan.classList.add("pending");
hint.container.classList.add("link-hint");
if (hint.parent.nodeName === "INPUT" || hint.parent.nodeName === "TEXTAREA") {
hint.container.classList.add("link-hint-input");
} else if (hint.parent.nodeName === "A") {
hint.container.classList.add("link-hint-a");
} else if (hint.parent.nodeName === "I" && hint.parent.classList.contains("cursor-pointer")) {
hint.container.classList.add("link-hint-i");
} else {
continue;
}
hint.container.classList.add("nodisplay"); // hide by default
hint.container.dataset.code = codeCounter.toString(); // not really needed, more for debug
if (hintTargets[i].nodeName === "INPUT" || hintTargets[i].nodeName === "TEXTAREA") {
// does not work if inside the input node
if (hintTargets[i].type === "checkbox" || hintTargets[i].type === "radio") {
if (hintTargets[i].nextElementSibling && hintTargets[i].nextElementSibling.nodeName === "LABEL") {
// insert at end of label
hintTargets[i].nextElementSibling.appendChild(hint.container);
} else {
// inserting right after
hintTargets[i].insertAdjacentElement("afterend", hint.container);
}
} else {
// inserting right after
hintTargets[i].insertAdjacentElement("afterend", hint.container);
}
} else {
hintTargets[i].appendChild(hint.container);
}
hintsMap[codeCounter++] = hint;
}
hintsMap.last = codeCounter - 1;
return hintsMap;
};
LinkHints.prototype.getHandler = function() {
return this.handleKey.bind(this);
};
LinkHints.prototype.handleKey = function(event) {
if (event.defaultPrevented) {
return;
}
if (event.key === "y") {
this.yankModeActive = !this.yankModeActive;
}
if (event.key === this.linkHintHotKey && Hotkeys.isHotkeyCallPossible()) {
// on user hide hints, close an opened dropdown too
if (this.areHintsDisplayed && this.activatedDropdown) this.closeActivatedDropdown();
if (this.areHintsDisplayed) this.yankModeActive = false;
this.pendingPath = "";
this.displayHints(!this.areHintsDisplayed);
} else if (this.areHintsDisplayed) {
// the user tries to reach a hint
this.pendingPath += event.key;
var hint = this.hintsMap[this.pendingPath];
if (hint) { // we are there, we have a fully specified tooltip. Let us activate or yank it
this.displayHints(false);
event.preventDefault();
if (this.yankModeActive) {
submitSapeventForm({ clipboard: hint.parent.firstChild.textContent }, "yank_to_clipboard");
this.yankModeActive = false;
} else {
this.hintActivate(hint);
}
} else {
// we are not there yet, but let us filter the link so that only
// the partially matched are shown
var visibleHints = this.filterHints();
if (!visibleHints) {
this.displayHints(false);
if (this.activatedDropdown) this.closeActivatedDropdown();
}
}
}
};
LinkHints.prototype.closeActivatedDropdown = function() {
if (!this.activatedDropdown) return;
this.activatedDropdown.classList.remove("force-nav-hover");
this.activatedDropdown = null;
};
LinkHints.prototype.displayHints = function(isActivate) {
this.areHintsDisplayed = isActivate;
for (var i = this.hintsMap.first; i <= this.hintsMap.last; i++) {
var hint = this.hintsMap[i];
if (isActivate) {
hint.container.classList.remove("nodisplay");
hint.pendingSpan.innerText = "";
hint.remainingSpan.innerText = hint.code;
} else {
hint.container.classList.add("nodisplay");
}
}
};
LinkHints.prototype.hintActivate = function(hint) {
if (hint.parent.nodeName === "A"
// hint.parent.href doesn`t have a # at the end while accessing dropdowns the first time.
// Seems like a idiosyncrasy of SAPGUI`s IE. So let`s ignore the last character.
&& (hint.parent.href.substr(0, hint.parent.href.length - 1) === document.location.href)// href is #
&& !hint.parent.onclick // no handler
&& hint.parent.parentElement && hint.parent.parentElement.nodeName === "LI") {
// probably it is a dropdown ...
this.activatedDropdown = hint.parent.parentElement;
this.activatedDropdown.classList.toggle("force-nav-hover");
hint.parent.focus();
} else if (hint.parent.type === "checkbox") {
this.toggleCheckbox(hint);
} else if (hint.parent.type === "radio") {
this.toggleRadioButton(hint);
} else if (hint.parent.type === "submit") {
hint.parent.click();
} else if (hint.parent.nodeName === "INPUT" || hint.parent.nodeName === "TEXTAREA") {
hint.parent.focus();
} else {
hint.parent.click();
if (this.activatedDropdown) this.closeActivatedDropdown();
}
};
LinkHints.prototype.toggleCheckbox = function(hint) {
var checked = hint.parent.checked;
this.triggerClickHandler(hint.parent.parentElement);
if (checked === hint.parent.checked) {
// fallback if no handler is registered
hint.parent.checked = !hint.parent.checked;
}
};
LinkHints.prototype.toggleRadioButton = function(hint) {
this.triggerClickHandler(hint.parent);
};
LinkHints.prototype.triggerClickHandler = function(el) {
// ensures that onclick handler is executed
// https://stackoverflow.com/questions/41981509/trigger-an-event-when-a-checkbox-is-changed-programmatically-via-javascript
var event = document.createEvent("HTMLEvents");
event.initEvent("click", false, true);
el.dispatchEvent(event);
};
LinkHints.prototype.filterHints = function() {
var visibleHints = 0;
for (var i = this.hintsMap.first; i <= this.hintsMap.last; i++) {
var hint = this.hintsMap[i];
if (i.toString().startsWith(this.pendingPath)) {
hint.pendingSpan.innerText = this.pendingPath;
hint.remainingSpan.innerText = hint.code.substring(this.pendingPath.length);
// hint.container.classList.remove("nodisplay"); // for backspace
visibleHints++;
} else {
hint.container.classList.add("nodisplay");
}
}
return visibleHints;
};
function activateLinkHints(linkHintHotKey) {
if (!linkHintHotKey) return;
var oLinkHint = new LinkHints(linkHintHotKey);
document.addEventListener("keypress", oLinkHint.getHandler());
}
/**********************************************************
* Hotkeys
**********************************************************/
function Hotkeys(oKeyMap) {
this.oKeyMap = oKeyMap || {};
// these are the hotkeys provided by the backend
Object.keys(this.oKeyMap).forEach(function(sKey) {
var action = this.oKeyMap[sKey];
// add a tooltip/title with the hotkey, currently only sapevents are supported
this.getAllSapEventsForSapEventName(action).forEach(function(elAnchor) {
elAnchor.title = elAnchor.title + " [" + sKey + "]";
});
// We replace the actions with callback functions to unify
// the hotkey execution
this.oKeyMap[sKey] = function(oEvent) {
// gHelper is only valid for diff page
var diffHelper = (window.gHelper || {});
// We have either a js function on this
if (this[action]) {
this[action].call(this);
return;
}
// Or a method of the helper object for the diff page
if (diffHelper[action]) {
diffHelper[action].call(diffHelper);
return;
}
// Or a global function
if (window[action] && typeof (window[action]) === "function") {
window[action].call(this);
return;
}
// Or a SAP event link
var sUiSapEventHref = this.getSapEventHref(action);
if (sUiSapEventHref) {
submitSapeventForm({}, sUiSapEventHref, "post");
oEvent.preventDefault();
return;
}
// Or a SAP event input
var sUiSapEventInputAction = this.getSapEventInputAction(action);
if (sUiSapEventInputAction) {
submitSapeventForm({}, sUiSapEventInputAction, "post");
oEvent.preventDefault();
return;
}
// Or a SAP event main form
var elForm = this.getSapEventForm(action);
if (elForm) {
elForm.submit();
oEvent.preventDefault();
return;
}
};
}.bind(this));
}
Hotkeys.prototype.showHotkeys = function() {
var elHotkeys = document.querySelector("#hotkeys");
if (elHotkeys) {
elHotkeys.style.display = (elHotkeys.style.display) ? "" : "none";
}
};
Hotkeys.prototype.getAllSapEventsForSapEventName = function (sSapEvent) {
if (/^#+$/.test(sSapEvent)){
// sSapEvent contains only #. Nothing sensible can be done here
return [];
}
var includesSapEvent = function(text){
return (text.includes("sapevent") || text.includes("SAPEVENT"));
};
return [].slice
.call(document.querySelectorAll("a[href*="+ sSapEvent +"], input[formaction*="+ sSapEvent+"]"))
.filter(function (elem) {
return (elem.nodeName === "A" && includesSapEvent(elem.href)
|| (elem.nodeName === "INPUT" && includesSapEvent(elem.formAction)));
});
};
Hotkeys.prototype.getSapEventHref = function(sSapEvent) {
return this.getAllSapEventsForSapEventName(sSapEvent)
.filter(function(el) {
// only anchors
return (!!el.href);
})
.map(function(oSapEvent) {
return oSapEvent.href;
})
.filter(this.eliminateSapEventFalsePositives(sSapEvent))
.pop();
};
Hotkeys.prototype.getSapEventInputAction = function(sSapEvent) {
return this.getAllSapEventsForSapEventName(sSapEvent)
.filter(function(el) {
// input forms
return (el.type === "submit");
})
.map(function(oSapEvent) {
return oSapEvent.formAction;
})
.filter(this.eliminateSapEventFalsePositives(sSapEvent))
.pop();
};
Hotkeys.prototype.getSapEventForm = function(sSapEvent) {
return this.getAllSapEventsForSapEventName(sSapEvent)
.filter(function(el) {
// forms
var parentForm = el.parentNode.parentNode.parentNode;
return (el.type === "submit" && parentForm.nodeName === "FORM");
})
.map(function(oSapEvent) {
return oSapEvent.parentNode.parentNode.parentNode;
})
.pop();
};
Hotkeys.prototype.eliminateSapEventFalsePositives = function(sapEvent) {
return function(sapEventAttr) {
return sapEventAttr.match(new RegExp("\\b" + sapEvent + "\\b"));
};
};
Hotkeys.prototype.onkeydown = function(oEvent) {
if (oEvent.defaultPrevented) {
return;
}
if (!Hotkeys.isHotkeyCallPossible()) {
return;
}
var
sKey = oEvent.key || String.fromCharCode(oEvent.keyCode),
fnHotkey = this.oKeyMap[sKey];
if (fnHotkey) {
fnHotkey.call(this, oEvent);
}
};
Hotkeys.isHotkeyCallPossible = function() {
var activeElementType = ((document.activeElement && document.activeElement.nodeName) || "");
var activeElementReadOnly = ((document.activeElement && document.activeElement.readOnly) || false);
return (activeElementReadOnly || (activeElementType !== "INPUT" && activeElementType !== "TEXTAREA"));
};
Hotkeys.addHotkeyToHelpSheet = function(key, description) {
var hotkeysUl = document.querySelector("#hotkeys ul.hotkeys");
if (!hotkeysUl) return;
var li = document.createElement("li");
var spanId = document.createElement("span");
var spanDescr = document.createElement("span");
spanId.className = "key-id";
spanId.innerText = key;
spanDescr.className = "key-descr";
spanDescr.innerText = description;
li.appendChild(spanId);
li.appendChild(spanDescr);
hotkeysUl.appendChild(li);
};
function setKeyBindings(oKeyMap) {
var oHotkeys = new Hotkeys(oKeyMap);
document.addEventListener("keypress", oHotkeys.onkeydown.bind(oHotkeys));
setTimeout(function() {
var div = document.getElementById("hotkeys-hint");
if (div) div.style.opacity = 0.2;
}, 4900);
setTimeout(function() { toggleDisplay("hotkeys-hint") }, 5000);
}
/**********************************************************
* Patch Logic (git add -p)
**********************************************************/
/*
We have three type of cascading checkboxes.
Which means that by clicking a file or section checkbox all corresponding line checkboxes are checked.
The id of the checkbox indicates its semantics and its membership.
*/
/*
1) file links
example id of file link
patch_file_zcl_abapgit_user_exit.clas.abap
\________/ \_____________________________/
| |
| |____ file name
|
|
|
constant prefix
*/
function PatchFile(sId) {
var oRegex = new RegExp("(" + this.ID + ")_(.*$)");
var oMatch = sId.match(oRegex);
this.id = sId;
this.prefix = oMatch[1];
this.file_name = oMatch[2];
}
PatchFile.prototype.ID = "patch_file";
/*
2) section links within a file
example id of section link
patch_section_zcl_abapgit_user_exit.clas.abap_1
\___________/ \_____________________________/ ^
| | |
| file name |
| |
| ------ section
|
constant prefix
*/
function PatchSection(sId) {
var oRegex = new RegExp("(" + this.ID + ")_(.*)_(\\d+$)");
var oMatch = sId.match(oRegex);
this.id = sId;
this.prefix = oMatch[1];
this.file_name = oMatch[2];
this.section = oMatch[3];
}
PatchSection.prototype.ID = "patch_section";
/*
3) line links within a section
example id of line link
patch_line_zcl_abapgit_user_exit.clas.abap_1_25
\________/ \_____________________________/ ^ ^
^ ^ | |
| | | ------- line number
| file name |
| section
|
|
constant prefix
*/
function PatchLine() { }
PatchLine.prototype.ID = "patch_line";
function Patch() { }
Patch.prototype.ID = {
STAGE: "stage"
};
Patch.prototype.ACTION = {
PATCH_STAGE : "patch_stage",
REFRESH_LOCAL: "refresh_local",
REFRESH_ALL : "refresh_all"
};
Patch.prototype.escape = function(sFileName) {
return sFileName
.replace(/\./g, "\\.")
.replace(/#/g, "\\#");
};
Patch.prototype.preparePatch = function() {
this.registerClickHandlerForFiles();
this.registerClickHandlerForSections();
this.registerClickHandlerForLines();
};
Patch.prototype.buildSelectorInputStartsWithId = function(sId) {
return "input[id^='" + sId + "']";
};
Patch.prototype.registerClickHandlerForFiles = function() {
this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchFile.prototype.ID), this.onClickFileCheckbox);
};
Patch.prototype.registerClickHandlerForSections = function() {
this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchSection.prototype.ID), this.onClickSectionCheckbox);
};
Patch.prototype.registerClickHandlerForLines = function() {
this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchLine.prototype.ID), this.onClickLineCheckbox);
};
Patch.prototype.registerClickHandlerForSelectorParent = function(sSelector, fnCallback) {
var elAll = document.querySelectorAll(sSelector);
[].forEach.call(elAll, function(elem) {
elem.parentElement.addEventListener("click", fnCallback.bind(this));
}.bind(this));
};
Patch.prototype.getAllLineCheckboxesForFile = function(oFile) {
return this.getAllLineCheckboxesForId(oFile.id, PatchFile.prototype.ID);
};
Patch.prototype.getAllSectionCheckboxesForFile = function(oFile) {
return this.getAllSectionCheckboxesForId(oFile.id, PatchFile.prototype.ID);
};
Patch.prototype.getAllLineCheckboxesForSection = function(oSection) {
return this.getAllLineCheckboxesForId(oSection.id, PatchSection.prototype.ID);
};
Patch.prototype.getAllLineCheckboxesForId = function(sId, sIdPrefix) {
return this.getAllCheckboxesForId(sId, sIdPrefix, PatchLine.prototype.ID);
};
Patch.prototype.getAllSectionCheckboxesForId = function(sId, sIdPrefix) {
return this.getAllCheckboxesForId(sId, sIdPrefix, PatchSection.prototype.ID);
};
Patch.prototype.getAllCheckboxesForId = function(sId, sIdPrefix, sNewIdPrefix) {
var oRegex = new RegExp("^" + sIdPrefix);
sId = sId.replace(oRegex, sNewIdPrefix);
return document.querySelectorAll(this.buildSelectorInputStartsWithId(this.escape(sId)));
};
Patch.prototype.getToggledCheckbox = function(oEvent) {
var elCheckbox = null;
// We have either an input element or any element with input child
// in the latter case we have to toggle the checkbox manually
if (oEvent.srcElement.nodeName === "INPUT") {
elCheckbox = oEvent.srcElement;
} else {
elCheckbox = this.toggleCheckbox(oEvent.srcElement.querySelector("INPUT"));
}
return elCheckbox;
};
Patch.prototype.toggleCheckbox = function(elCheckbox) {
elCheckbox.checked = !elCheckbox.checked;
return elCheckbox;
};
Patch.prototype.onClickFileCheckbox = function(oEvent) {
var elCheckbox = this.getToggledCheckbox(oEvent);
var oFile = new PatchFile(elCheckbox.id);
var elAllLineCheckboxesOfFile = this.getAllLineCheckboxesForFile(oFile);
var elAllSectionCheckboxesOfFile = this.getAllSectionCheckboxesForFile(oFile);
[].forEach.call(elAllLineCheckboxesOfFile, function(elem) {
elem.checked = elCheckbox.checked;
}.bind(this));
[].forEach.call(elAllSectionCheckboxesOfFile, function(elem) {
elem.checked = elCheckbox.checked;
}.bind(this));
};
Patch.prototype.onClickSectionCheckbox = function(oEvent) {
var elSrcElement = this.getToggledCheckbox(oEvent);
var oSection = new PatchSection(elSrcElement.id);
this.clickAllLineCheckboxesInSection(oSection, elSrcElement.checked);
};
Patch.prototype.onClickLineCheckbox = function(oEvent) {
this.getToggledCheckbox(oEvent);
};
Patch.prototype.clickAllLineCheckboxesInSection = function(oSection, bChecked) {
var elAllLineCheckboxesOfSection = this.getAllLineCheckboxesForSection(oSection);
[].forEach.call(elAllLineCheckboxesOfSection, function(elem) {
elem.checked = bChecked;
}.bind(this));
};
Patch.prototype.registerStagePatch = function() {
var elStage = document.querySelector("#" + this.ID.STAGE);
var REFRESH_PREFIX = "refresh";
elStage.addEventListener("click", this.submitPatch.bind(this, this.ACTION.PATCH_STAGE));
var aRefresh = document.querySelectorAll("[id*=" + REFRESH_PREFIX + "]");
[].forEach.call(aRefresh, function(el) {
el.addEventListener("click", memorizeScrollPosition(this.submitPatch.bind(this, el.id)).bind(this));
}.bind(this));
// for hotkeys
window.stagePatch = function() {
this.submitPatch(this.ACTION.PATCH_STAGE);
}.bind(this);
window.refreshLocal = memorizeScrollPosition(function() {
this.submitPatch(this.ACTION.REFRESH_LOCAL);
}.bind(this));
window.refreshAll = memorizeScrollPosition(function() {
this.submitPatch(this.ACTION.REFRESH_ALL);
}.bind(this));
};
Patch.prototype.submitPatch = function(action) {
// Collect add and remove info and submit to backend
var aAddPatch = this.collectElementsForCheckboxId(PatchLine.prototype.ID, true);
var aRemovePatch = this.collectElementsForCheckboxId(PatchLine.prototype.ID, false);
submitSapeventForm({ add: aAddPatch, remove: aRemovePatch }, action, "post");
};
Patch.prototype.collectElementsForCheckboxId = function(sId, bChecked) {
var sSelector = this.buildSelectorInputStartsWithId(sId);
return [].slice.call(document.querySelectorAll(sSelector))
.filter(function(elem) {
return (elem.checked === bChecked);
}).map(function(elem) {
return elem.id;
});
};
function preparePatch() {
var oPatch = new Patch();
oPatch.preparePatch();
}
function registerStagePatch() {
var oPatch = new Patch();
oPatch.registerStagePatch();
}
/**********************************************************
* Command Palette (Ctrl + P)
**********************************************************/
// fuzzy match helper
// return non empty marked string in case it fits the filter
// abc + b = abc
function fuzzyMatchAndMark(str, filter) {
var markedStr = "";
var filterLower = filter.toLowerCase();
var strLower = str.toLowerCase();
var cur = 0;
for (var i = 0; i < filter.length; i++) {
while (filterLower[i] !== strLower[cur] && cur < str.length) {
markedStr += str[cur++];
}
if (cur === str.length) break;
markedStr += "" + str[cur++] + "";
}
var matched = i === filter.length;
if (matched && cur < str.length) markedStr += str.substring(cur);
return matched ? markedStr: null;
}
function CommandPalette(commandEnumerator, opts) {
if (typeof commandEnumerator !== "function") throw Error("commandEnumerator must be a function");
if (typeof opts !== "object") throw Error("opts must be an object");
if (typeof opts.toggleKey !== "string" || !opts.toggleKey) throw Error("toggleKey must be a string");
this.commands = commandEnumerator();
if (!this.commands) return;
// this.commands = [{
// action: "sap_event_action_code_with_params"
// iconClass: "icon icon_x ..."
// title: "my command X"
// }, ...];
if (opts.toggleKey[0] === "^") {
this.toggleKeyCtrl = true;
this.toggleKey = opts.toggleKey.substring(1);
if (!this.toggleKey) throw Error("Incorrect toggleKey");
} else {
this.toggleKeyCtrl = false;
this.toggleKey = opts.toggleKey;
}
this.hotkeyDescription = opts.hotkeyDescription;
this.elements = {
palette: null,
ul : null,
input : null
};
this.selectIndex = -1; // not selected
this.filter = "";
this.renderAndBindElements();
this.hookEvents();
Hotkeys.addHotkeyToHelpSheet(opts.toggleKey, opts.hotkeyDescription);
if (!CommandPalette.instances) {
CommandPalette.instances = [];
}
CommandPalette.instances.push(this);
}
CommandPalette.prototype.hookEvents = function() {
document.addEventListener("keydown", this.handleToggleKey.bind(this));
this.elements.input.addEventListener("keyup", this.handleInputKey.bind(this));
this.elements.ul.addEventListener("click", this.handleUlClick.bind(this));
};
CommandPalette.prototype.renderCommandItem = function(cmd) {
var li = document.createElement("li");
if (cmd.iconClass) {
var icon = document.createElement("i");
icon.className = cmd.iconClass;
li.appendChild(icon);
}
var titleSpan = document.createElement("span");
li.appendChild(titleSpan);
cmd.element = li;
cmd.titleSpan = titleSpan;
return li;
};
CommandPalette.prototype.renderAndBindElements = function() {
var div = document.createElement("div");
var input = document.createElement("input");
var ul = document.createElement("ul");
div.className = "cmd-palette";
div.style.display = "none";
input.placeholder = this.hotkeyDescription;
for (var i = 0; i < this.commands.length; i++) ul.appendChild(this.renderCommandItem(this.commands[i]));
div.appendChild(input);
div.appendChild(ul);
this.elements.palette = div;
this.elements.input = input;
this.elements.ul = ul;
document.body.appendChild(div);
};
CommandPalette.prototype.handleToggleKey = function(event) {
if (event.key !== this.toggleKey) return;
if (this.toggleKeyCtrl && !event.ctrlKey) return;
this.toggleDisplay();
event.preventDefault();
};
CommandPalette.prototype.handleInputKey = function(event) {
if (event.key === "ArrowUp" || event.key === "Up") {
this.selectPrev();
} else if (event.key === "ArrowDown" || event.key === "Down") {
this.selectNext();
} else if (event.key === "Enter") {
this.exec(this.getSelected());
} else if (event.key === "Backspace" && !this.filter) {
this.toggleDisplay(false);
} else if (this.filter !== this.elements.input.value) {
this.filter = this.elements.input.value;
this.applyFilter();
this.selectFirst();
}
event.preventDefault();
};
CommandPalette.prototype.applyFilter = function() {
for (var i = 0; i < this.commands.length; i++) {
var cmd = this.commands[i];
if (!this.filter) {
cmd.element.style.display = "";
cmd.titleSpan.innerText = cmd.title;
} else {
var matchedTitle = fuzzyMatchAndMark(cmd.title, this.filter);
if (matchedTitle) {
cmd.titleSpan.innerHTML = matchedTitle;
cmd.element.style.display = "";
} else {
cmd.element.style.display = "none";
}
}
}
};
CommandPalette.prototype.applySelectIndex = function(newIndex) {
if (newIndex !== this.selectIndex) {
if (this.selectIndex >= 0) this.commands[this.selectIndex].element.classList.remove("selected");
var newCmd = this.commands[newIndex];
newCmd.element.classList.add("selected");
this.selectIndex = newIndex;
this.adjustScrollPosition(newCmd.element);
}
};
CommandPalette.prototype.selectFirst = function() {
for (var i = 0; i < this.commands.length; i++) {
if (this.commands[i].element.style.display === "none") continue; // skip hidden
this.applySelectIndex(i);
break;
}
};
CommandPalette.prototype.selectNext = function() {
for (var i = this.selectIndex + 1; i < this.commands.length; i++) {
if (this.commands[i].element.style.display === "none") continue; // skip hidden
this.applySelectIndex(i);
break;
}
};
CommandPalette.prototype.selectPrev = function() {
for (var i = this.selectIndex - 1; i >= 0; i--) {
if (this.commands[i].element.style.display === "none") continue; // skip hidden
this.applySelectIndex(i);
break;
}
};
CommandPalette.prototype.getSelected = function() {
return this.commands[this.selectIndex];
};
CommandPalette.prototype.adjustScrollPosition = function(itemElement) {
var bItem = itemElement.getBoundingClientRect();
var bContainer = this.elements.ul.getBoundingClientRect();
bItem.top = Math.round(bItem.top);
bItem.bottom = Math.round(bItem.bottom);
bItem.height = Math.round(bItem.height);
bItem.mid = Math.round(bItem.top + bItem.height / 2);
bContainer.top = Math.round(bContainer.top);
bContainer.bottom = Math.round(bContainer.bottom);
if (bItem.mid > bContainer.bottom - 2) {
this.elements.ul.scrollTop += bItem.bottom - bContainer.bottom;
} else if (bItem.mid < bContainer.top + 2) {
this.elements.ul.scrollTop += bItem.top - bContainer.top;
}
};
CommandPalette.prototype.toggleDisplay = function(forceState) {
var isDisplayed = (this.elements.palette.style.display !== "none");
var tobeDisplayed = (forceState !== undefined) ? forceState : !isDisplayed;
if (tobeDisplayed) {
// auto close other command palettes
CommandPalette.instances.forEach(function(instance) {
instance.elements.palette.style.display = "none";
});
}
this.elements.palette.style.display = tobeDisplayed ? "" : "none";
if (tobeDisplayed) {
this.elements.input.value = "";
this.elements.input.focus();
this.applyFilter();
this.selectFirst();
}
};
CommandPalette.prototype.getCommandByElement = function(element) {
for (var i = 0; i < this.commands.length; i++) {
if (this.commands[i].element === element) return this.commands[i];
}
};
CommandPalette.prototype.handleUlClick = function(event) {
var element = event.target || event.srcElement;
if (!element) return;
if (element.nodeName === "SPAN") element = element.parentNode;
if (element.nodeName === "I") element = element.parentNode;
if (element.nodeName !== "LI") return;
this.exec(this.getCommandByElement(element));
};
CommandPalette.prototype.exec = function(cmd) {
if (!cmd) return;
this.toggleDisplay(false);
if (typeof cmd.action === "function") {
cmd.action();
} else {
submitSapeventForm(null, cmd.action);
}
};
// Is any command palette visible?
CommandPalette.isVisible = function() {
return CommandPalette.instances.reduce(function(result, instance) { return result || instance.elements.palette.style.display !== "none" }, false);
};
/**********************************************************
* Command Enumerators
**********************************************************/
function createRepoCatalogEnumerator(catalog, action) {
// expecting [{ key, isOffline, displayName }]
return function() {
return catalog.map(function(i) {
return {
action : action + "?key=" + i.key,
iconClass: i.isOffline ? "icon icon-plug darkgrey" : "icon icon-cloud-upload-alt blue",
title : i.displayName
};
});
};
}
function enumerateUiActions() {
var items = [];
function processUL(ulNode, prefix) {
for (var i = 0; i < ulNode.children.length; i++) {
var item = ulNode.children[i];
if (item.nodeName !== "LI") continue; // unexpected node
if (item.children.length >= 2 && item.children[1].nodeName === "UL") {
// submenu detected
var menutext = item.children[0].innerText;
// special treatment for menus without text
if (!menutext) {
menutext = item.children[0].getAttribute("title");
}
processUL(item.children[1], menutext);
} else if (item.firstElementChild && item.firstElementChild.nodeName === "A") {
var anchor = item.firstElementChild;
if (anchor.href && anchor.href !== "#") items.push([anchor, prefix]);
}
}
}
// toolbars
[].slice.call(document.querySelectorAll("[id*=toolbar]"))
.filter(function(toolbar) {
return (toolbar && toolbar.nodeName === "UL");
}).forEach(function(toolbar) {
processUL(toolbar);
});
items = items.map(function(item) {
var action = "";
var anchor = item[0];
if (anchor.href.includes("#")) {
action = function() {
anchor.click();
};
} else {
action = anchor.href.replace("sapevent:", "");
}
var prefix = item[1];
return {
action: action,
title : (prefix ? prefix + ": " : "") + anchor.innerText.trim()
};
});
// forms
[].slice.call(document.querySelectorAll("input[type='submit']"))
.forEach(function(input) {
items.push({
action: function() {
if (input.form.action.includes(input.formAction) || input.classList.contains("main")) {
input.form.submit();
} else {
submitSapeventForm({}, input.formAction, "post", input.form);
}
},
title: input.value + " " + input.title.replace(/\[.*\]/, "")
});
});
// radio buttons
[].slice.call(document.querySelectorAll("input[type='radio']"))
.forEach(function(input) {
items.push({
action: function() {
input.click();
},
title: document.querySelector("label[for='" + input.id + "']").textContent
});
});
// others:
// - links inside forms
// - label links
// - command links
// - other header links
[].slice.call(document.querySelectorAll("form a, a.command, #header ul:not([id*='toolbar']) a"))
.filter(function(anchor) {
return !!anchor.title || !!anchor.text;
}).forEach(function(anchor) {
items.push({
action: function() {
anchor.click();
},
title: (function() {
var result = anchor.title + anchor.text;
if (anchor.href.includes("label")) {
result = "Label: " + result;
}
return result;
})()
});
});
return items;
}
function enumerateJumpAllFiles() {
var root = document.getElementById("jump");
if (!root || root.nodeName !== "UL") return null;
return Array
.prototype.slice.call(root.children)
.filter(function(elem) { return elem.nodeName === "LI" })
.map(function(listItem) {
var title = listItem.children[0].childNodes[0].textContent;
return {
action: root.onclick.bind(null, title),
title : title
};
});
}
/**********************************************************
* Save Scroll Position
**********************************************************/
function saveScrollPosition() {
// Not supported by Java GUI
try { if (!window.sessionStorage) { return } }
catch (err) { return }
window.sessionStorage.setItem("scrollTop", document.querySelector("html").scrollTop);
}
function restoreScrollPosition() {
// Not supported by Java GUI
try { if (!window.sessionStorage) { return } }
catch (err) { return }
var scrollTop = window.sessionStorage.getItem("scrollTop");
if (scrollTop) {
document.querySelector("html").scrollTop = scrollTop;
}
window.sessionStorage.setItem("scrollTop", 0);
}
function memorizeScrollPosition(fn) {
return function() {
saveScrollPosition();
return fn.call(this, fn.args);
}.bind(this);
}
/**********************************************************
* Sticky Header
**********************************************************/
/* https://www.w3schools.com/howto/howto_js_navbar_sticky.asp */
/* Note: We have to use JS since IE does not support CSS position:sticky */
// When the user scrolls the page, execute toggleSticky
window.onscroll = function() { toggleSticky() };
// Add the sticky class to the navbar when you reach its scroll position.
// Remove "sticky" when you leave the scroll position
function toggleSticky() {
var body = document.getElementsByTagName("body")[0];
var header = document.getElementById("header");
var sticky = header.offsetTop;
var stickyClass = "sticky";
if (body.classList.contains("full_width")) {
stickyClass = "sticky_full_width";
}
if (window.pageYOffset >= sticky) {
header.classList.add(stickyClass);
} else {
header.classList.remove(stickyClass);
}
}
/**********************************************************
* Browser Control
**********************************************************/
// Toggle display of warning message when using Edge (based on Chromium) browser control
// Todo: Remove once https://github.com/abapGit/abapGit/issues/4841 is fixed
function toggleBrowserControlWarning(){
if (!navigator.userAgent.includes("Edg")){
var elBrowserControlWarning = document.getElementById("browser-control-warning");
if (elBrowserControlWarning) {
elBrowserControlWarning.style.display = "none";
}
}
}
// Output type of HTML control in the abapGit footer
function displayBrowserControlFooter() {
var out = document.getElementById("browser-control-footer");
out.innerHTML = " - " + ( navigator.userAgent.includes("Edg") ? "Edge" : "IE" );
}