/********************************************************** * 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" ); }