source: main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js@ 36163

Last change on this file since 36163 was 36163, checked in by davidb, 2 years ago

Tidy up in TK Labels; Introducing enriched audio playback using wavesurfer and its ability to have audio regions

File size: 21.4 KB
Line 
1/** JavaScript file of utility functions.
2 * At present contains functions for sanitising of URLs,
3 * since tomcat 8+, being more compliant with URL/URI standards, is more strict about URLs.
4 */
5
6/*
7 Given a string consisting of a single character, returns the %hex (%XX)
8 https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
9 https://stackoverflow.com/questions/40100096/what-is-equivalent-php-chr-and-ord-functions-in-javascript
10 https://www.w3resource.com/javascript-exercises/javascript-string-exercise-27.php
11*/
12function urlEncodeChar(single_char_string) {
13 /*var hex = Number(single_char_string.charCodeAt(0)).toString(16);
14 var str = "" + hex;
15 str = "%" + str.toUpperCase();
16 return str;
17 */
18
19 var hex = "%" + Number(single_char_string.charCodeAt(0)).toString(16).toUpperCase();
20 return hex;
21}
22
23/*
24 Tomcat 8 appears to be stricter in requiring unsafe and reserved chars
25 in URLs to be escaped with URL encoding
26 See section "Character Encoding Chart of
27 https://perishablepress.com/stop-using-unsafe-characters-in-urls/
28 Reserved chars:
29 ; / ? : @ = &
30 -----> %3B %2F %3F %3A %40 %3D %26
31 [Now also reserved, but no special meaning yet in URLs (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)
32 and not required to be enforced yet, so we're aren't at present dealing with these:
33 ! ' ( ) *
34 ]
35 Unsafe chars:
36 " < > # % { } | \ ^ ~ [ ] ` and SPACE/BLANK
37 ----> %22 %3C %3E %23 %25 %7B %7D %7C %5C %5E ~ %5B %5D %60 and %20
38 But the above conflicts with the reserved vs unreserved listings at
39 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
40 Possibly more info: https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid
41
42 And the bottom of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
43 lists additional characters that have been reserved since and which need encoding when in a URL component.
44
45 Javascript already provides functions encodeURI() and encodeURIComponent(), see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
46 However, the set of chars they deal with only partially overlap with the set of chars that need encoding as per the RFC3986 for URIs and RFC1738 for URLs discussed at
47 https://perishablepress.com/stop-using-unsafe-characters-in-urls/
48 We want to handle all the characters listed as unsafe and reserved at https://perishablepress.com/stop-using-unsafe-characters-in-urls/
49 so we define and use our own conceptually equivalent methods for both existing JavaScript methods:
50 - makeSafeURL() for Javascript's encodeURI() to make sure all unsafe characters in URLs are escaped by being URL encoded
51 - and makeSafeURLComponent() for JavaScript's encodeURIComponent to additionally make sure all reserved characters in a URL portion are escaped by being URL encoded too
52
53 Function makeSafeURL() is passed a string that represents a URL and therefore only deals with characters that are unsafe in a URL and which therefore require escaping.
54 Function makeSafeURLComponent() deals with portions of a URL that when decoded need not represent a URL at all, for example data like inline templates passed in as a
55 URL query string's parameter values. As such makeSafeURLComponent() should escape both unsafe URL characters and characters that are reserved in URLs since reserved
56 characters in the query string part (as query param values representing data) may take on a different meaning from their reserved meaning in a URL context.
57*/
58
59/* URL encodes both
60 - UNSAFE characters to make URL safe, by calling makeSafeURL()
61 - and RESERVED characters (characters that have reserved meanings within a URL) to make URL valid, since the url component parameter could use reserved characters
62 in a non-URL sense. For example, the inline template (ilt) parameter value of a URL could use '=' and '&' signs where these would have XSLT rather than URL meanings.
63
64 See end of https://www.w3schools.com/jsref/jsref_replace.asp to use a callback passing each captured element of a regex in str.replace()
65*/
66function makeURLComponentSafe(url_part, encode_percentages) {
67 // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
68 encode_percentages = encode_percentages || 1; // this method forces the URL-encoding of any % in url_part, e.g. do this for inline-templates that haven't ever been encoded
69
70 var url_encoded = makeURLSafe(url_part, encode_percentages);
71 //return url_encoded.replace(/;/g, "%3B").replace(/\//g, "%2F").replace(/\?/g, "%3F").replace(/\:/g, "%3A").replace(/\@/g, "%40").replace(/=/g, "%3D").replace(/\&/g,"%26");
72 url_encoded = url_encoded.replace(/[\;\/\?\:\@\=\&]/g, function(s) {
73 return urlEncodeChar(s);
74 });
75 return url_encoded;
76}
77
78/*
79 URL encode UNSAFE characters to make URL passed in safe.
80 Set encode_percentages to 1 (true) if you don't want % signs encoded: you'd do so if the url is already partly URL encoded.
81*/
82function makeURLSafe(url, encode_percentages) {
83 encode_percentages = encode_percentages || 0; // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript
84
85 var url_encoded = url;
86 if(encode_percentages) { url_encoded = url_encoded.replace(/\%/g,"%25"); } // encode % first
87 //url_encoded = url_encoded.replace(/ /g, "%20").replace(/\"/g,"%22").replace(/\</g,"%3C").replace(/\>/g,"%3E").replace(/\#/g,"%23").replace(/\{/g,"%7B").replace(/\}/g,"%7D");
88 //url_encoded = url_encoded.replace(/\|/g,"%7C").replace(/\\/g,"%5C").replace(/\^/g,"%5E").replace(/\[/g,"%5B").replace(/\]/g,"%5D").replace(/\`/g,"%60");
89 // Should we handle ~, but then what is its URL encoded value? Because https://meyerweb.com/eric/tools/dencoder/ URLencodes ~ to ~.
90 //return url_encoded;
91 url_encoded = url_encoded.replace(/[\ \"\<\>\#\{\}\|\\^\~\[\]\`]/g, function(s) {
92 return urlEncodeChar(s);
93 });
94 return url_encoded;
95}
96
97/***************
98* MENU SCRIPTS *
99***************/
100function moveScroller() {
101 var move = function() {
102 var editbar = $("#editBar");
103 var st = $(window).scrollTop();
104 var fa = $("#float-anchor").offset().top;
105 if(st > fa) {
106
107 editbar.css({
108 position: "fixed",
109 top: "0px",
110 width: editbar.data("width"),
111 //width: "30%"
112 });
113 } else {
114 editbar.data("width", editbar.css("width"));
115 editbar.css({
116 position: "relative",
117 top: "",
118 width: ""
119 });
120 }
121 };
122 $(window).scroll(move);
123 move();
124}
125
126
127function floatMenu(enabled)
128{
129 var menu = $(".tableOfContentsContainer");
130 if(enabled)
131 {
132 menu.data("position", menu.css("position"));
133 menu.data("width", menu.css("width"));
134 menu.data("right", menu.css("right"));
135 menu.data("top", menu.css("top"));
136 menu.data("max-height", menu.css("max-height"));
137 menu.data("overflow", menu.css("overflow"));
138 menu.data("z-index", menu.css("z-index"));
139
140 menu.css("position", "fixed");
141 menu.css("width", "300px");
142 menu.css("right", "0px");
143 menu.css("top", "100px");
144 menu.css("max-height", "600px");
145 menu.css("overflow", "auto");
146 menu.css("z-index", "200");
147
148 $("#unfloatTOCButton").show();
149 }
150 else
151 {
152 menu.css("position", menu.data("position"));
153 menu.css("width", menu.data("width"));
154 menu.css("right", menu.data("right"));
155 menu.css("top", menu.data("top"));
156 menu.css("max-height", menu.data("max-height"));
157 menu.css("overflow", menu.data("overflow"));
158 menu.css("z-index", menu.data("z-index"));
159
160 $("#unfloatTOCButton").hide();
161 $("#floatTOCToggle").prop("checked", false);
162 }
163
164 var url = gs.xsltParams.library_name + "?a=d&ftoc=" + (enabled ? "1" : "0") + "&c=" + gs.cgiParams.c;
165
166 $.ajax(url);
167}
168
169// TK Label Scripts
170
171var tkMetadataSetStatus = "needs-to-be-loaded";
172var tkMetadataElements = null;
173
174
175function addTKLabelToImage(labelName, definition, name, comment) {
176 // lists of tkLabels and their corresponding codes, in order
177 let tkLabels = ["Attribution","Clan","Family","MultipleCommunities","CommunityVoice","Creative","Verified","NonVerified","Seasonal","WomenGeneral","MenGeneral",
178 "MenRestricted","WomenRestricted","CulturallySensitive","SecretSacred","OpenToCommercialization","NonCommercial","CommunityUseOnly","Outreach","OpenToCollaboration"];
179 let tkCodes = ["tk_a","tk_cl","tk_f","tk_mc","tk_cv","tk_cr","tk_v","tk_nv","tk_s","tk_wg","tk_mg","tk_mr","tk_wr","tk_cs","tk_ss","tk_oc","tk_nc","tk_co","tk_o","tk_cb"];
180 for (let i = 0; i < tkLabels.length; i++) {
181 if (labelName == tkLabels[i]) {
182 let labeldiv = document.querySelectorAll(".tklabels img");
183 for (image of labeldiv) {
184 let labelCode = image.src.substr(image.src.lastIndexOf("/") + 1).replace(".png", ""); // get tk label code from image file name
185 if (labelCode == tkCodes[i]) {
186 image.title = "TK " + name + ": " + definition + " Click for more details."; // set tooltip
187 if (image.parentElement.parentElement.parentElement.classList[0] != "tocSectionTitle") { // disable onclick event in favourites section
188 image.addEventListener("click", function(e) {
189 let currPopup = document.getElementsByClassName("tkPopup")[0];
190 if (currPopup == undefined || (currPopup != undefined && currPopup.id != labelCode)) {
191 let popup = document.createElement("div");
192 popup.classList.add("tkPopup");
193 popup.id = labelCode;
194 let popupText = document.createElement("span");
195 let heading = "<h1>Traditional Knowledge Label:<br><h2>" + name + "</h2></h1>";
196 let moreInformation = "<br> For more information about TK Labels, ";
197 let link = document.createElement("a");
198 link.innerHTML = "click here.";
199 link.href = "https://localcontexts.org/labels/traditional-knowledge-labels/";
200 link.target = "_blank";
201 popupText.innerHTML = heading + comment + moreInformation;
202 popupText.appendChild(link);
203 let closeButton = document.createElement("span");
204 closeButton.innerHTML = "&#215;";
205 closeButton.id = "tkCloseButton";
206 closeButton.title = "Click to close window."
207 closeButton.addEventListener("click", function(e) {
208 closeButton.parentElement.remove();
209 });
210 popup.appendChild(closeButton);
211 popup.appendChild(popupText);
212 e.target.parentElement.appendChild(popup);
213 }
214 if (currPopup) currPopup.remove(); // remove existing popup div
215 });
216 }
217 }
218 }
219 }
220 }
221}
222
223function addTKLabelsToImages(lang) {
224 if (tkMetadataElements == null) {
225 console.error("ajax call not yet loaded tk label metadata set");
226 } else {
227 for (label of tkMetadataElements) { // for each tklabel element in tk.mds
228 let tkLabelName = label.attributes.name.value; // Element name=""
229 let attributes = label.querySelectorAll("[code=" + lang + "] Attribute"); // gets attributes for selected language
230 let tkName = attributes[0].textContent; // name="label"
231 let tkDefinition = attributes[1].textContent; // name="definition"
232 let tkComment = attributes[2].textContent; // name="comment"
233 addTKLabelToImage(tkLabelName, tkDefinition, tkName, tkComment);
234 }
235 }
236}
237
238function loadTKMetadataSet(lang) {
239 tkMetadataSetStatus = "loading";
240 $.ajax({
241 url: gs.variables["tkMetadataURL"],
242 success: function(xml) {
243 tkMetadataSetStatus = "loaded";
244 let parser = new DOMParser();
245 let tkmds = parser.parseFromString(xml, "text/xml");
246 tkMetadataElements = tkmds.querySelectorAll("Element");
247 if (document.readyState === "complete") {
248 addTKLabelsToImages(lang);
249 } else {
250 window.onload = function() {
251 addTKLabelsToImages(lang);
252 }
253 }
254 },
255 error: function() {
256 tkMetadataSetStatus = "no-metadata-set-for-this-collection";
257 console.log("No TK Label Metadata-set found for this collection");
258 }
259 });
260};
261
262// Audio Scripts for Enriched Playback
263
264function loadAudio(audio, sectionData) {
265 var speakerObjects = [];
266 var uniqueSpeakers;
267 var prevColour;
268 // const inputFile = "output.csv";
269 const inputFile = sectionData;
270 // audio = "audio/akl_mi_pk_0002.wav";
271 var itemType;
272
273 var wavesurfer = WaveSurfer.create({
274 container: document.querySelector('#waveform'),
275 backend: 'MediaElement',
276 backgroundColor: 'rgb(54, 73, 78)',
277 waveColor: 'white',
278 progressColor: '#F8C537',
279 barWidth: 2,
280 barHeight: 1,
281 barGap: 2,
282 barRadius: 1,
283 cursorColor: 'black',
284 plugins: [
285 WaveSurfer.regions.create(),
286 WaveSurfer.timeline.create({
287 container: "#wave-timeline",
288 primaryColor: "white",
289 secondaryColor: "white",
290 primaryFontColor: "white",
291 secondaryFontColor: "white"
292 }),
293 ],
294 });
295
296 // wavesurfer.load('https://wavesurfer-js.org/example/elan/transcripts/001z.mp3');
297 wavesurfer.load(audio);
298
299 wavesurfer.on('region-click', function(region, e) {
300 e.stopPropagation();
301 regionMouseEvent(region, true);
302 wavesurfer.play(region.start);
303 // region.play();
304 });
305
306 wavesurfer.on('region-mouseenter', function(region) { regionMouseEvent(region, true); });
307 wavesurfer.on('region-mouseleave', function(region) {
308 if (wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start) {
309 // console.log("");
310 } else {
311 regionMouseEvent(region, false);
312 }
313 });
314 wavesurfer.on('region-in', function(region) { regionMouseEvent(region, true); });
315 wavesurfer.on('region-out', function(region) { regionMouseEvent(region, false); });
316
317 function regionMouseEvent(region, highlight) {
318 var colour;
319 if (highlight) {
320 colour = "rgb(101, 116, 116)";
321 regionEnter(region);
322 } else {
323 colour = "";
324 regionLeave(region);
325 }
326 var regionIndex = region.id.replace("region","");
327 var corrItem = corrItem = document.getElementById(itemType + regionIndex);
328 corrItem.style.backgroundColor = colour;
329 }
330
331 var chapterButton = document.getElementById("chapterButton");
332 var backButton = document.getElementById("backButton");
333 var playPauseButton = document.getElementById("playPauseButton");
334 var forwardButton = document.getElementById("forwardButton");
335 var muteButton = document.getElementById("muteButton");
336 chapterButton.addEventListener("click", function() { toggleChapters(); });
337 backButton.addEventListener("click", function() { wavesurfer.skipBackward(); });
338 playPauseButton.addEventListener("click", function() { wavesurfer.playPause() });
339 forwardButton.addEventListener("click", function() { wavesurfer.skipForward(); });
340 muteButton.addEventListener("click", function() { wavesurfer.toggleMute(); });
341
342 var chapters = document.getElementById("chapters");
343 chapters.style.height = "0px";
344
345 if (inputFile.endsWith("csv")) {
346 itemType = "chapter";
347 loadCSVFile(inputFile, ["speaker", "start", "end"]);
348 } else if (inputFile.endsWith("json")) {
349 itemType = "word";
350 loadJSONFile(inputFile);
351 } else {
352 console.log("Filetype of " + inputFile + " not supported.")
353 }
354
355 var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/";
356
357 wavesurfer.on("play", function() { playPauseButton.src = interface_bootstrap_images + "pause.svg"; });
358 wavesurfer.on("pause", function() { playPauseButton.src = interface_bootstrap_images + "play.svg"; });
359 wavesurfer.on("mute", function(mute) {
360 if (mute) muteButton.src = interface_bootstrap_images + "mute.svg";
361 else muteButton.src = interface_bootstrap_images + "unmute.svg";
362 });
363
364 document.querySelector('#slider').oninput = function () {
365 wavesurfer.zoom(Number(this.value));
366 };
367
368 var toggleChapters = function() {
369 if (chapters.style.height == "0px") {
370 chapters.style.height = "500px";
371 } else {
372 chapters.style.height = "0px";
373 }
374 }
375
376 function loadCSVFile(filename, manualHeader) { // based around: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript
377 $.ajax({
378 type: "GET",
379 url: filename,
380 dataType: "text",
381 }).then(function(data) {
382
383 var dataLines = data.split(/\r\n|\n/);
384 var headers;
385 var startIndex;
386 uniqueSpeakers = [];
387 speakerObjects = [];
388
389 if (manualHeader) {
390 headers = manualHeader;
391 startIndex = 0;
392 } else {
393 headers = dataLines[0].split(',');
394 startIndex = 1;
395 }
396
397 for (var i = startIndex; i < dataLines.length; i++) {
398 var data = dataLines[i].split(',');
399 if (data.length == headers.length) {
400 var item = {};
401 for (var j = 0; j < headers.length; j++) {
402 item[headers[j]] = data[j];
403 if (j == 0) {
404 if (!uniqueSpeakers.includes(data[j])) {
405 uniqueSpeakers.push(data[j]);
406 }
407 }
408 }
409 speakerObjects.push(item);
410 }
411 }
412 populateChapters(speakerObjects);
413 });
414 }
415
416 function populateChapters(data) {
417 // TODO: colorbrewer info
418 colourbrewerset = colorbrewer.Set2[uniqueSpeakers.length];
419 for (var i = 0; i < data.length; i++) {
420 var chapter = document.createElement("div");
421 chapter.id = "chapter" + i;
422 chapter.classList.add("chapter");
423 chapter.innerHTML = data[i].speaker + "<span class='speakerTime' id='" + "chapter" + i + "'>" + data[i].start + " - " + data[i].end + "s</span>";
424 chapter.addEventListener("click", e => { chapterClicked(e.target.id) });
425 chapter.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
426 chapter.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
427 chapters.appendChild(chapter);
428 wavesurfer.addRegion({
429 id: "region" + i,
430 start: data[i].start,
431 end: data[i].end,
432 drag: false,
433 resize: false,
434 // color: "rgba(255, 255, 255, 0.2)",
435 color: colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)] + "66",
436 });
437 }
438 }
439
440 function loadJSONFile(filename) {
441 $.ajax({
442 type: "GET",
443 url: filename,
444 dataType: "text",
445 }).then(function(data){populateWords(JSON.parse(data))});
446 }
447
448 function populateWords(data) {
449 var transcription = data.transcription;
450 var words = data.words;
451 var wordContainer = document.createElement("div");
452 wordContainer.id = "word-container";
453 for (var i = 0; i < words.length; i++) {
454 var word = document.createElement("span");
455 word.id = "word" + i;
456 word.classList.add("word");
457 word.innerHTML = transcription.split(" ")[i];
458 word.addEventListener("click", e => { wordClicked(data, e.target.id) });
459 word.addEventListener("mouseover", e => { chapterEnter(e.target.id) });
460 word.addEventListener("mouseleave", e => { chapterLeave(e.target.id) });
461 wordContainer.appendChild(word);
462 wavesurfer.addRegion({
463 id: "region" + i,
464 start: words[i].startTime,
465 end: words[i].endTime,
466 drag: false,
467 resize: false,
468 color: "rgba(255, 255, 255, 0.1)",
469 });
470 }
471 chapters.appendChild(wordContainer);
472 }
473
474 var chapterClicked = function(id) {
475 var index = id.replace("chapter", "");
476 var start = speakerObjects[index].start;
477 var end = speakerObjects[index].end;
478 // wavesurfer.play(start, end);
479 wavesurfer.play(start);
480 }
481
482 function wordClicked(data, id) {
483 var index = id.replace("word", "");
484 var start = data.words[index].startTime;
485 var end = data.words[index].endTime;
486 // wavesurfer.play(start, end);
487 wavesurfer.play(start);
488 }
489
490 function chapterEnter(id) {
491 regionEnter(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
492 }
493
494 function chapterLeave(id) {
495 regionLeave(wavesurfer.regions.list["region" + id.replace(itemType, "")]);
496 }
497
498 function regionEnter(region) {
499 setPrevColour(region);
500 region.update({ color: "rgba(255, 255, 255, 0.35)" });
501 }
502
503 function regionLeave(region) {
504 if (itemType == "chapter") {
505 region.update({ color: prevColour });
506 // chapters.innerHTML = "";
507 // wavesurfer.clearRegions();
508 // populateChapters(speakerObjects);
509
510 } else {
511 region.update({ color: "rgba(255, 255, 255, 0.1)" });
512 }
513 }
514
515 function setPrevColour(region) {
516 var regionIndex = region.id.replace("region","");
517 var corrItem = corrItem = document.getElementById(itemType + regionIndex);
518 prevColour = colourbrewerset[uniqueSpeakers.indexOf(corrItem.firstChild.data)] + "66";
519 }
520}
Note: See TracBrowser for help on using the repository browser.