/* * Git HTML Archive Viewer * * Benedikt Bieringer * https://nuserv.uni-muenster.de:8443/bbieringer/statusarchiveviewer */ class ArchiveCommit { constructor(message, time, sha) { this.message = message this.time = time this.sha = sha } } function replace_urls(text, url_before, url_after) { for(const key of ["src=\""]) { segments = text.split(key) result_text = segments[0] for(const segment of segments.slice(1)) { var index = segment.indexOf('"') var first = segment.substring(0, index) var second = segment.substring(index) var new_url = url_before + encodeURIComponent(first) + url_after result_text += key + new_url + second } text = result_text } return text } function hide_iframes(text) { return text.replaceAll("", "/div>") } function date_to_iso(value) { if ((value + "").includes("-")) { return value } var date = new Date(parseInt(value)*1000) return date.toISOString().substring(0,16) } function date_to_unix(value) { return parseInt(new Date(value + ":00+00:00").getTime() / 1000) } class ArchiveView { constructor(mindate, maindiv, token, url, project_id, branch, page_path, index) { this.token = token this.url = url this.project_id = project_id this.branch = branch this.next_commit = "" this.current_commit = "" this.last_from = "" this.last_to = "" this.maindiv = maindiv var outer_span = document.createElement("span") this.maindiv.appendChild(outer_span) // --- this.selecttype = document.createElement("select") outer_span.appendChild(this.selecttype) var option_date = document.createElement("option") option_date.value = "date" option_date.innerHTML = "Date" this.selecttype.appendChild(option_date) var option_range = document.createElement("option") option_range.value = "range" option_range.innerHTML = "Range" this.selecttype.appendChild(option_range) this.selecttype.addEventListener("input",(() => this.update_hash())) // --- outer_span.append(" (" + mindate + " to today): ") this.dateselector = document.createElement("input") this.dateselector.type = "date" // Alternative, doesn't allow selecting today in first two hours of day: // var maxdate = new Date().toISOString().split("T")[0] var maxdate = new Date().toLocaleDateString('en-ca') this.dateselector.value = "" this.dateselector.max = maxdate this.dateselector.addEventListener("input",(() => this.update_hash())) outer_span.appendChild(this.dateselector) // --- this.rangespan = document.createElement("span") outer_span.appendChild(this.rangespan) this.range_from = document.createElement("input") this.range_from.type = "datetime-local" this.range_from.value = "" this.range_from.min = mindate + "T00:00" this.range_from.max = maxdate + "T23:59" this.range_from.addEventListener("input",(() => this.update_hash())) this.rangespan.appendChild(this.range_from) this.rangespan.insertAdjacentHTML('beforeend', " – ") this.range_to = document.createElement("input") this.range_to.type = "datetime-local" this.range_to.disabled = true this.range_to.value = "" this.range_to.max = maxdate + "T23:59" this.range_to.addEventListener("input",(() => this.update_hash())) this.rangespan.appendChild(this.range_to) // --- outer_span.append(" | Version: ") this.commitsSelect = document.createElement("select") this.commitsSelect.className = "archive commits" this.commitsSelect.addEventListener("input",(() => this.on_commit_selected())) outer_span.appendChild(this.commitsSelect) outer_span.insertAdjacentHTML('beforeend', "Note: Large time ranges can slow the version selection down.") // --- this.content = document.createElement("iframe") this.content.className = "archive content" this.content.height = "100%" this.maindiv.appendChild(this.content) this.page_path = page_path this.index = index this.defaultcontent = new Blob([` Welcome to the archive viewer! Select a date and version to start. `], {type: "text/html"}) this.content.src = URL.createObjectURL(this.defaultcontent) window.addEventListener('hashchange', (event) => this.on_hash_changed(event)) // Updates this.on_hash_changed(null) this.update_hash() // Update visibilities this.load_pages() // Update version list } update_hash() { var old_hash = window.location.hash var new_hash = "" if (this.selecttype.value == "date") { new_hash = "#date=" + this.dateselector.value + "&commit=" + this.next_commit } else { new_hash = "#from=" + this.range_from.value + "&to=" + this.range_to.value + "&commit=" + this.next_commit } if (new_hash == old_hash) { return false } window.location.hash = new_hash return true } on_hash_changed(event) { var remaining_hash = window.location.hash.substring(1).split("&") var date_from = null var length = null var date_to = null var date = null while (remaining_hash.length > 0) { var current_part = remaining_hash.pop() var [key, value] = current_part.split("=") if (key == "from") { date_from = "" if (value) { date_from = date_to_iso(value) } } else if (key == "length") { length = value } else if (key == "to") { date_to = "" if (value) { date_to = date_to_iso(value) } } else if (key == "date"){ date = "" if (value) { date = date_to_iso(value) } } else if (key == "commit"){ this.next_commit = value } } if (date) { this.dateselector.value = date this.selecttype.value = "date" // Convert date to range so one only needs to work with ranges if (this.dateselector.value) { var date_from_tmp = this.dateselector.value date_from = date_from_tmp + "T00:00" var date_to_obj = new Date(date_from_tmp) date_to_obj.setDate(date_to_obj.getDate() + 1) // Works also across months, years date_to = date_to_obj.toJSON().slice(0, 10) + "T00:00" } } if (date_from) { this.range_from.value = date_from this.range_to.min = date_from this.range_to.disabled = (date_from == "") if (!date) { this.selecttype.value = "range" } } if (date_to) { this.range_to.value = date_to } if (length) { this.range_to.value = date_to_iso(date_to_unix(date_from)+parseInt(length)) } if (this.update_hash()) { return // Triggered another hash change } // Visibilities var is_date = this.selecttype.value == "date" this.dateselector.hidden = !is_date this.rangespan.hidden = is_date if (!is_date) { this.dateselector.value = "" } if (!date_from) { return } if (!date_to) { return } if (this.commitsSelect.length && date_from == this.last_from && date_to == this.last_to) { this.commitsSelect.value = this.next_commit this.on_commit_selected() return // List didn't change } this.last_from = date_from this.last_to = date_to if (this.next_commit) { this.load_pages(false) return } this.load_pages(true) } add_commits(commits, open_first) { var to_open = open_first commits.map(commit => { var elem = document.createElement("option") elem.innerHTML = commit.message elem.value = commit.sha this.commitsSelect.appendChild(elem) if (to_open) { this.commitsSelect.value = elem.value this.on_commit_selected() to_open = false } }) } on_commit_selected() { if (!this.commitsSelect.value) { this.content.contentWindow.location.replace(URL.createObjectURL(this.defaultcontent)) this.next_commit = "" this.update_hash() return } this.next_commit = this.commitsSelect.value this.update_hash() if (this.next_commit == this.current_commit) { return // Nothing changed } this.current_commit = this.next_commit var url_before = 'https://' + this.url + '/api/v4/projects/' + this.project_id + '/repository/files/' + encodeURIComponent(this.page_path + "/") var url_after = '/raw?ref=' + this.next_commit + '&private_token=' + this.token var url = url_before + encodeURIComponent(this.index) + url_after fetch(url) .then(response => response.text()) .then(text => { var cleaned_text = hide_iframes(replace_urls(text, url_before, url_after)) const blobContent = new Blob([cleaned_text], {type: "text/html"}) this.content.contentWindow.location.replace(URL.createObjectURL(blobContent)) // Don't affect browser history by content change (as opposed to this.content.src = ...) }) } load_pages(open_first=false) { var date_from = this.range_from.value var date_to = this.range_to.value this.load_pages_recursive(1, date_from, date_to, open_first) } load_pages_recursive(page, date_from, date_to, open_first) { if (page == 1) { // Clean commits list this.commitsSelect.innerHTML = "" this.commitsSelect.appendChild(document.createElement("option")) } if (date_from == "" || date_to == "") { return } var commits_url = 'https://' + this.url + '/api/v4/projects/' + this.project_id + '/repository/commits?ref_name=' + this.branch + '&private_token=' + this.token + "&since="+date_from+"&until="+date_to+"&per_page=100&page=" + page; var length_promise = fetch(commits_url) .then(response => response.json()) .then( // Translate response to commits json => json.map( gitlab_commit => new ArchiveCommit( gitlab_commit.message, gitlab_commit.created_at, gitlab_commit.id ) ) ).then((commits) => { // Skip if new date is selected if (date_from != this.range_from.value || date_to != this.range_to.value) { return 0; } this.add_commits(commits, open_first) return commits.length } ).then((length) => { if (length > 0) { this.load_pages_recursive(page+1, date_from, date_to, false) } else { // Loading done, update commit this.commitsSelect.value = this.next_commit this.on_commit_selected() } }) } }