/********************************************************** * ABAPGIT JS 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 enumerateTocAllRepos */ /* exported enumerateJumpAllFiles */ /********************************************************** * 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), fToBind = this, fNOP = function() {}, 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; } }); } /********************************************************** * 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 pre-created form or create a hidden form // and submit with sapevent function submitSapeventForm(params, action, method) { var stub_form_id = "form_" + action; var form = document.getElementById(stub_form_id); if (form === null) { form = document.createElement("form"); form.setAttribute("method", method || "post"); form.setAttribute("action", "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); } if (form.id !== stub_form_id) { 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 initilization 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 = []; } /********************************************************** * TAG PAGE Logic **********************************************************/ function onTagTypeChange(oSelectObject){ var sValue = oSelectObject.value; submitSapeventForm({ type: sValue }, "change_tag_type", "post"); } /********************************************************** * 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() { this.setHooks(); this.pageId = "RepoOverViewHelperState"; // constant is OK for this case this.isDetailsDisplayed = false; this.detailCssClass = findStyleSheetByName(".ro-detail"); } RepoOverViewHelper.prototype.toggleRepoListDetail = function(forceDisplay) { if (this.detailCssClass) { this.isDetailsDisplayed = forceDisplay || !this.isDetailsDisplayed; this.detailCssClass.style.display = this.isDetailsDisplayed ? "" : "none"; } }; RepoOverViewHelper.prototype.setHooks = function() { window.onbeforeunload = this.onPageUnload.bind(this); window.onload = this.onPageLoad.bind(this); }; RepoOverViewHelper.prototype.onPageUnload = function() { if (!window.sessionStorage) return; var data = { isDetailsDisplayed: this.isDetailsDisplayed }; window.sessionStorage.setItem(this.pageId, JSON.stringify(data)); }; RepoOverViewHelper.prototype.onPageLoad = function() { var data = window.sessionStorage && JSON.parse(window.sessionStorage.getItem(this.pageId)); if (data && data.isDetailsDisplayed) this.toggleRepoListDetail(true); debugOutput("RepoOverViewHelper.onPageLoad: " + ((data) ? "from Storage" : "initial state")); }; /********************************************************** * STAGE PAGE Logic **********************************************************/ // Stage helper constructor function StageHelper(params) { this.pageSeed = params.seed; this.formAction = params.formAction; this.choiseCount = 0; this.lastFilterValue = ""; // DOM nodes this.dom = { stageTab: document.getElementById(params.ids.stageTab), commitBtn: document.getElementById(params.ids.commitBtn), commitAllBtn: document.getElementById(params.ids.commitAllBtn), objectSearch: document.getElementById(params.ids.objectSearch), fileCounter: document.getElementById(params.ids.fileCounter) }; // 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(); } // Hook global click listener on table, load/unload actions StageHelper.prototype.setHooks = function() { this.dom.stageTab.onclick = this.onTableClick.bind(this); this.dom.commitBtn.onclick = this.submit.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); }; // 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); } debugOutput("StageHelper.onPageLoad: " + ((data) ? "from Storage" : "initial state")); }; // 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(); }; // 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 ) { this.applyFilterValue(e.target.value); submitSapeventForm({ filterValue: e.target.value }, "stage_filter", "post"); } }; StageHelper.prototype.applyFilterValue = function(sFilterValue) { this.lastFilterValue = sFilterValue; this.iterateStageTab(true, this.applyFilterToRow, sFilterValue); }; // 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) { var elem = row.cells[this.colIndex[attr]]; if (elem.firstChild && elem.firstChild.tagName === "A") elem = elem.firstChild; return { elem: elem, plainText: elem.innerText, // without tags 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]; target.newHtml = (filter) ? target.plainText.replace(filter, ""+filter+"") : 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; } }; // 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.choiseCount += 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; } }; // Update menu items visibility StageHelper.prototype.updateMenu = function () { this.dom.commitBtn.style.display = (this.choiseCount > 0) ? "" : "none"; this.dom.commitAllBtn.style.display = (this.choiseCount > 0) ? "none" : ""; this.dom.fileCounter.innerHTML = this.choiseCount.toString(); }; // Submit stage state to the server StageHelper.prototype.submit = function () { submitSapeventForm(this.collectData(), this.formAction); }; // 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; }; // Table iteration helper StageHelper.prototype.iterateStageTab = function (changeMode, cb /*, ...*/) { var restArgs = Array.prototype.slice.call(arguments, 2); var table = this.dom.stageTab; 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); cb.apply(this, args); // callback } } if (changeMode) { this.dom.stageTab.style.display = ""; window.scrollTo(0, scrollOffset); } }; /********************************************************** * Check list wrapper **********************************************************/ function CheckListWrapper(id, cbAction) { this.id = document.getElementById(id); this.cbAction = cbAction; 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 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.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); }; // 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"); } }; // 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"; } /********************************************************** * Other functions **********************************************************/ // News announcement function toggleDisplay(divId) { var div = document.getElementById(divId); if (div) div.style.display = (div.style.display) ? "" : "none"; } 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 link hints */ function LinkHints(linkHintHotKey){ this.linkHintHotKey = linkHintHotKey; this.areHintsDisplayed = false; this.pendingPath = ""; // already typed code prefix this.hintsMap = this.deployHintContainers(); this.activatedDropdown = null; } LinkHints.prototype.getHintStartValue = function(targetsCount){ // if we have 321 tooltips we start from 100 var maxHintStringLength = targetsCount.toString().length; return Math.pow(10, maxHintStringLength - 1); }; LinkHints.prototype.deployHintContainers = function() { var hintTargets = document.querySelectorAll("a, input[type='checkbox']"); var codeCounter = this.getHintStartValue(hintTargets.length); var hintsMap = { first: codeCounter }; // // 123 // for (var i = 0, N = hintTargets.length; i < N; i++) { 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.container.classList.add("link-hint-input"); } else { hint.container.classList.add("link-hint-a"); } 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") { // does not work if inside the input, so appending 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; } var activeElementType = (document.activeElement && document.activeElement.nodeName) || ""; // link hints are disabled for input and textareas for obvious reasons. // Maybe we must add other types here in the future if (event.key === this.linkHintHotKey && activeElementType !== "INPUT" && activeElementType !== "TEXTAREA") { // on user hide hints, close an opened dropdown too if (this.areHintsDisplayed && this.activatedDropdown) this.closeActivatedDropdown(); 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's activate it this.displayHints(false); this.hintActivate(hint); } else { // we are not there yet, but let's 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 { hint.parent.click(); if (this.activatedDropdown) this.closeActivatedDropdown(); } }; 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]; // We replace the actions with callback functions to unify // the hotkey execution this.oKeyMap[sKey] = function(oEvent) { // We have either a js function on this if (this[action]) { this[action].call(this); return; } // Or a global function if (window[action]) { window[action].call(this); } // Or a SAP event var sUiSapEvent = this.getSapEvent(action); if (sUiSapEvent) { submitSapeventForm({}, sUiSapEvent, "post"); 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.getSapEvent = function(sSapEvent) { var fnNormalizeSapEventHref = function(sSapEvent, oSapEvent) { if (new RegExp(sSapEvent + "$" ).test(oSapEvent.href) || (new RegExp(sSapEvent + "\\?" ).test(oSapEvent.href))) { return oSapEvent.href.replace("sapevent:",""); } }; var aSapEvents = document.querySelectorAll('a[href^="sapevent:' + sSapEvent + '"]'); var aFilteredAndNormalizedSapEvents = [].map.call(aSapEvents, function(oSapEvent){ return fnNormalizeSapEventHref(sSapEvent, oSapEvent); }).filter(function(elem){ // remove false positives return (elem && !elem.includes("sapevent:")); }); return (aFilteredAndNormalizedSapEvents && aFilteredAndNormalizedSapEvents[0]); }; Hotkeys.prototype.onkeydown = function(oEvent){ if (oEvent.defaultPrevented) { return; } var activeElementType = ((document.activeElement && document.activeElement.nodeName) || ""); if (activeElementType === "INPUT" || activeElementType === "TEXTAREA") { return; } var sKey = oEvent.key || String.fromCharCode(oEvent.keyCode), fnHotkey = this.oKeyMap[sKey]; if (fnHotkey) { fnHotkey.call(this, oEvent); } }; 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"); spanId.className = "key-id"; spanId.innerText = key; var spanDescr = document.createElement("span"); 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 / 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" }; 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 registerStagePatch(){ var elStage = document.querySelector("#" + this.ID.STAGE); elStage.addEventListener("click", this.stagePatch.bind(this)); // for hotkeys window.stagePatch = function(){ this.stagePatch(); }.bind(this); }; Patch.prototype.stagePatch = function() { // 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}, this.ACTION.PATCH_STAGE, "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(); } /********************************************************** * Page branch overview * * Hovering a commit node in the branch overview will show * a popup with the commit details. Single click on a node * will fix the popup, so that users can select text. The * fixation is removed when any node is hovered or the popup * is closed via 'X'. * **********************************************************/ function BranchOverview() { this.bFixed = false; this.elCurrentCommit = { style : { display: "none" } }; } BranchOverview.prototype.toggleCommit = function(sSha1, bFixPopup) { // If the popup is fixed, we just remove the fixation. // The popup will then be hidden by the next call of hideCommit if (this.bFixed) { this.bFixed = false; return; } // We hide the previous shown commit popup this.elCurrentCommit.style.display = "none"; // Display the new commit popup if sha1 is supplied if (sSha1){ this.elCurrentCommit = document.getElementById(sSha1); this.elCurrentCommit.style.display = ""; // and fix the popup so that the next hideCommit won't hide it. this.bFixed = bFixPopup; } }; // called by onClick of commit nodes in branch overview BranchOverview.prototype.onCommitClick = function(commit){ this.toggleCommit(commit.sha1, true); }; // Called by commit:mouseover BranchOverview.prototype.showCommit = function(event){ this.toggleCommit(event.data.sha1); }; // Called by commit:mouseout BranchOverview.prototype.hideCommit = function (){ this.toggleCommit(); }; // Initialize Top Horizontal Scroller on GitGraph function setGitGraphScroller(){ // eslint-disable-line no-unused-vars // Get gitGraph Element Canvas Width var gitGraphEl = document.getElementById("gitGraph"); var gitGraphWidth = gitGraphEl.offsetWidth; // Initialize gitGraph-HTopScroller Element width as gitGraph var HTopScrollerEl = document.querySelector(".gitGraph-HTopScroller"); HTopScrollerEl.style.width = gitGraphWidth + "px"; } // Setup Top Horizontal Scroller on GitGraph event function GitGraphScroller() { // eslint-disable-line no-unused-vars var gitGraphWrapperEl = document.querySelector(".gitGraph-Wrapper"); var gitGraphscrollWrapperEl = document.querySelector(".gitGraph-scrollWrapper"); gitGraphWrapperEl.scrollLeft = gitGraphscrollWrapperEl.scrollLeft; } /********************************************************** * Ctrl + P - command palette **********************************************************/ // 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); } 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"); div.className = "cmd-palette"; div.style.display = "none"; var input = document.createElement("input"); input.placeholder = this.hotkeyDescription; var ul = document.createElement("ul"); 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; 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); } }; /* COMMAND ENUMERATORS */ function enumerateTocAllRepos() { var root = document.getElementById("toc-all-repos"); if (!root || root.nodeName !== "UL") return null; var items = []; for (var i = 0; i < root.children.length; i++) { if (root.children[i].nodeName === "LI") items.push(root.children[i]); } items = items.map(function(listItem) { var anchor = listItem.children[0]; return { action: anchor.href.replace("sapevent:", ""), // a iconClass: anchor.childNodes[0].className, // i with icon title: anchor.childNodes[1].textContent // text with repo name }; }); return items; } function enumerateToolbarActions() { 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 processUL(item.children[1], item.children[0].innerText); } else if (item.firstElementChild && item.firstElementChild.nodeName === "A") { var anchor = item.firstElementChild; if (anchor.href && anchor.href !== "#") items.push([anchor, prefix]); } } } var toolbarRoot = document.getElementById("toolbar-main"); if (toolbarRoot && toolbarRoot.nodeName === "UL") processUL(toolbarRoot); toolbarRoot = document.getElementById("toolbar-repo"); if (toolbarRoot && toolbarRoot.nodeName === "UL") processUL(toolbarRoot); // Add more toolbars ? if (items.length === 0) return; items = items.map(function(item) { var anchor = item[0]; var prefix = item[1]; return { action: anchor.href.replace("sapevent:", ""), title: (prefix ? prefix + ": " : "") + anchor.innerText }; }); 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 };}); }