archiveviewer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*
  2. * Git HTML Archive Viewer
  3. *
  4. * Benedikt Bieringer <benedikt.b@wwu.de>
  5. * https://nuserv.uni-muenster.de:8443/bbieringer/statusarchiveviewer
  6. */
  7. class ArchiveCommit {
  8. constructor(message, time, sha) {
  9. this.message = message
  10. this.time = time
  11. this.sha = sha
  12. }
  13. }
  14. function replace_urls(text, url_before, url_after) {
  15. for(const key of ["src=\""]) {
  16. segments = text.split(key)
  17. result_text = segments[0]
  18. for(const segment of segments.slice(1)) {
  19. var index = segment.indexOf('"')
  20. var first = segment.substring(0, index)
  21. var second = segment.substring(index)
  22. var new_url = url_before + encodeURIComponent(first) + url_after
  23. result_text += key + new_url + second
  24. }
  25. text = result_text
  26. }
  27. return text
  28. }
  29. function hide_iframes(text) {
  30. return text.replaceAll("<iframe", "<div hidden ").replaceAll("/iframe>", "/div>")
  31. }
  32. function date_to_iso(value) {
  33. console.log(value)
  34. if ((value + "").includes("-")) {
  35. return value
  36. }
  37. var date = new Date(parseInt(value)*1000)
  38. return date.toISOString().substring(0,16)
  39. }
  40. function date_to_unix(value) {
  41. return parseInt(new Date(value + ":00+00:00").getTime() / 1000)
  42. }
  43. class ArchiveView {
  44. constructor(mindate, maindiv, token, url, project_id, branch, page_path, index) {
  45. this.token = token
  46. this.url = url
  47. this.project_id = project_id
  48. this.branch = branch
  49. this.maindiv = maindiv
  50. var outer_span = document.createElement("span")
  51. this.maindiv.appendChild(outer_span)
  52. // ---
  53. this.selecttype = document.createElement("select")
  54. outer_span.appendChild(this.selecttype)
  55. var option_date = document.createElement("option")
  56. option_date.value = "date"
  57. option_date.innerHTML = "Date"
  58. this.selecttype.appendChild(option_date)
  59. var option_range = document.createElement("option")
  60. option_range.value = "range"
  61. option_range.innerHTML = "Range"
  62. this.selecttype.appendChild(option_range)
  63. this.selecttype.addEventListener("input",(() => this.on_selecttype_selected()))
  64. // ---
  65. outer_span.append(" (" + mindate + " to today): ")
  66. this.dateselector = document.createElement("input")
  67. this.dateselector.type = "date"
  68. // Alternative, doesn't allow selecting today in first two hours of day:
  69. // var maxdate = new Date().toISOString().split("T")[0]
  70. var maxdate = new Date().toLocaleDateString('en-ca')
  71. this.dateselector.value = ""
  72. this.dateselector.max = maxdate
  73. this.dateselector.addEventListener("input",((event) => this.on_date_selected(event)))
  74. outer_span.appendChild(this.dateselector)
  75. // ---
  76. this.rangespan = document.createElement("span")
  77. outer_span.appendChild(this.rangespan)
  78. this.range_from = document.createElement("input")
  79. this.range_from.type = "datetime-local"
  80. this.range_from.value = ""
  81. this.range_from.min = mindate + "T00:00"
  82. this.range_from.max = maxdate + "T23:59"
  83. this.range_from.addEventListener("input",(() => this.on_range_from_selected()))
  84. this.rangespan.appendChild(this.range_from)
  85. this.rangespan.insertAdjacentHTML('beforeend', " &ndash; ")
  86. this.range_to = document.createElement("input")
  87. this.range_to.type = "datetime-local"
  88. this.range_to.disabled = true
  89. this.range_to.value = ""
  90. this.range_to.max = maxdate + "T23:59"
  91. this.range_to.addEventListener("input",(() => this.on_range_to_selected()))
  92. this.rangespan.appendChild(this.range_to)
  93. // ---
  94. outer_span.append(" | Version: ")
  95. this.commitsSelect = document.createElement("select")
  96. this.commitsSelect.className = "archive commits"
  97. this.commitsSelect.addEventListener("input",(() => this.on_commit_selected()))
  98. outer_span.appendChild(this.commitsSelect)
  99. outer_span.insertAdjacentHTML('beforeend', "<br/><i>Note: Large time ranges can slow the version selection down.</i>")
  100. // ---
  101. this.content = document.createElement("iframe")
  102. this.content.className = "archive content"
  103. this.content.height = "100%"
  104. this.maindiv.appendChild(this.content)
  105. this.page_path = page_path
  106. this.index = index
  107. const blobContent = new Blob([`
  108. <!doctype html><html><head>
  109. <style>
  110. body {
  111. min-height: 100vh;
  112. display: flex;
  113. align-items: center;
  114. justify-content: space-around;
  115. }
  116. h1,h2 {
  117. display: flex;
  118. align-items: center;
  119. justify-content: center;
  120. }
  121. </style>
  122. </head>
  123. <body style="background:#aaa; color:#fff;">
  124. <div>
  125. <h1>Welcome to the archive viewer!</h1>
  126. <h2>Select a date and version to start.</h2>
  127. </div>
  128. </body>
  129. </html>
  130. `], {type: "text/html"})
  131. this.content.src = URL.createObjectURL(blobContent)
  132. window.addEventListener('hashchange', (event) => this.on_hash_changed(event))
  133. // Updates
  134. this.on_hash_changed(null)
  135. this.on_selecttype_selected() // Update visibilities
  136. this.load_pages() // Update version list
  137. }
  138. on_hash_changed(event) {
  139. var remaining_hash = window.location.hash.substring(1).split("&")
  140. var date_from = null
  141. var length = null
  142. var to = null
  143. var provided_full_range = false;
  144. while (remaining_hash.length > 0) {
  145. var current_part = remaining_hash.pop()
  146. var [key, value] = current_part.split("=")
  147. if (key == "from") {
  148. date_from = value
  149. }
  150. else if (key == "length") {
  151. length = value
  152. }
  153. else if (key == "to") {
  154. to = value
  155. }
  156. }
  157. if (date_from != null) {
  158. this.selecttype.value = "range"
  159. var from_iso = date_to_iso(date_from)
  160. console.log(date_from, from_iso)
  161. this.range_from.value = from_iso
  162. this.on_range_from_selected()
  163. if (length != null) {
  164. // Only works when given UNIX timestamps
  165. console.log(from_iso)
  166. console.log(from_iso,date_to_iso(date_to_unix(from_iso)))
  167. this.range_to.value = date_to_iso(date_to_unix(from_iso)+parseInt(length))
  168. provided_full_range = true
  169. }
  170. }
  171. if (to != null) {
  172. if (date_from != nullptr) {
  173. provided_full_range = true
  174. }
  175. this.selecttype.value = "range"
  176. this.on_selecttype_selected() // Update visibilities
  177. this.range_to.value = date_to_iso(to)
  178. }
  179. if (provided_full_range) {
  180. this.load_pages(true)
  181. }
  182. }
  183. add_commits(commits, open_first) {
  184. var to_open = open_first
  185. commits.map(commit => {
  186. var elem = document.createElement("option")
  187. elem.innerHTML = commit.message
  188. elem.value = commit.sha
  189. this.commitsSelect.appendChild(elem)
  190. if (to_open) {
  191. this.commitsSelect.value = elem.value
  192. this.on_commit_selected()
  193. to_open = false
  194. }
  195. })
  196. }
  197. on_selecttype_selected() {
  198. this.dateselector.value = ""
  199. var is_date = this.selecttype.value == "date"
  200. this.dateselector.hidden = !is_date
  201. this.rangespan.hidden = is_date
  202. }
  203. on_date_selected(event) {
  204. if (!event.target.value)
  205. return
  206. var date_from = event.target.value
  207. var date_to_obj = new Date(date_from)
  208. date_to_obj.setDate(date_to_obj.getDate() + 1)
  209. // Works also across months, years
  210. var date_to = date_to_obj.toJSON().slice(0, 10);
  211. this.range_from.value = date_from + "T00:00"
  212. this.on_range_from_selected()
  213. this.range_to.value = date_to + "T00:00"
  214. this.on_range_to_selected()
  215. }
  216. on_range_from_selected() {
  217. var val = this.range_from.value
  218. this.range_to.min = val
  219. this.range_to.disabled = (val == "")
  220. this.load_pages()
  221. }
  222. on_range_to_selected() {
  223. this.load_pages()
  224. }
  225. on_commit_selected() {
  226. var sha = this.commitsSelect.value
  227. var url_before = 'https://' + this.url + '/api/v4/projects/' + this.project_id + '/repository/files/' + encodeURIComponent(this.page_path + "/")
  228. var url_after = '/raw?ref=' + sha + '&private_token=' + this.token
  229. var url = url_before + encodeURIComponent(this.index) + url_after
  230. fetch(url)
  231. .then(response => response.text())
  232. .then(text => {
  233. var cleaned_text = hide_iframes(replace_urls(text, url_before, url_after))
  234. const blobContent = new Blob([cleaned_text], {type: "text/html"})
  235. this.content.src = URL.createObjectURL(blobContent)
  236. })
  237. }
  238. load_pages(open_first=false) {
  239. var date_from = this.range_from.value
  240. var date_to = this.range_to.value
  241. this.load_pages_recursive(1, date_from, date_to, open_first)
  242. }
  243. load_pages_recursive(page, date_from, date_to, open_first) {
  244. if (page == 1) {
  245. // Clean commits list
  246. this.commitsSelect.innerHTML = ""
  247. this.commitsSelect.appendChild(document.createElement("option"))
  248. }
  249. if (date_from == "" || date_to == "") {
  250. return
  251. }
  252. 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;
  253. var length_promise = fetch(commits_url)
  254. .then(response => response.json())
  255. .then(
  256. // Translate response to commits
  257. json => json.map(
  258. gitlab_commit => new ArchiveCommit(
  259. gitlab_commit.message,
  260. gitlab_commit.created_at,
  261. gitlab_commit.id
  262. )
  263. )
  264. ).then((commits) => {
  265. // Skip if new date is selected
  266. if (date_from != this.range_from.value
  267. || date_to != this.range_to.value) {
  268. return 0;
  269. }
  270. this.add_commits(commits, open_first)
  271. return commits.length
  272. }
  273. ).then((length) => {
  274. if (length > 0) {
  275. this.load_pages_recursive(page+1, date_from, date_to, false)
  276. }
  277. })
  278. }
  279. }