Changeset 36217 for main/trunk
- Timestamp:
- 2022-05-26T15:06:05+12:00 (22 months ago)
- Location:
- main/trunk/greenstone3/web/interfaces/default
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js
r36196 r36217 11 11 */ 12 12 function 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;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 21 } 22 22 23 23 /* 24 Tomcat 8 appears to be stricter in requiring unsafe and reserved chars25 in URLs to be escaped with URL encoding26 See section "Character Encoding Chart of27 https://perishablepress.com/stop-using-unsafe-characters-in-urls/28 Reserved chars:29 ; / ? : @ = &30 -----> %3B %2F %3F %3A %40 %3D %2631 [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/BLANK37 ----> %22 %3C %3E %23 %25 %7B %7D %7C %5C %5E ~ %5B %5D %60 and %2038 But the above conflicts with the reserved vs unreserved listings at39 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI40 Possibly more info: https://stackoverflow.com/questions/1547899/which-characters-make-a-url-invalid41 42 And the bottom of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent43 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/encodeURI46 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 at47 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 encoded51 - and makeSafeURLComponent() for JavaScript's encodeURIComponent to additionally make sure all reserved characters in a URL portion are escaped by being URL encoded too52 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 a55 URL query string's parameter values. As such makeSafeURLComponent() should escape both unsafe URL characters and characters that are reserved in URLs since reserved56 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.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 57 */ 58 58 59 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 characters62 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()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 65 */ 66 66 function makeURLComponentSafe(url_part, encode_percentages) { 67 // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript68 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 encoded69 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 74 });75 return url_encoded;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 76 } 77 77 78 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.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 81 */ 82 82 function makeURLSafe(url, encode_percentages) { 83 encode_percentages = encode_percentages || 0; // https://stackoverflow.com/questions/12797118/how-can-i-declare-optional-function-parameters-in-javascript84 85 var url_encoded = url;86 if(encode_percentages) { url_encoded = url_encoded.replace(/\%/g,"%25"); } // encode % first87 //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 93 });94 return url_encoded;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 95 } 96 96 … … 99 99 ***************/ 100 100 function 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 109 110 111 112 });113 } else {114 editbar.data("width", editbar.css("width"));115 editbar.css({116 117 118 119 120 }121 };122 $(window).scroll(move);123 move();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 124 } 125 125 … … 127 127 function floatMenu(enabled) 128 128 { 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 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 167 } 168 168 … … 174 174 175 175 function addTKLabelToImage(labelName, definition, name, comment) { 176 // lists of tkLabels and their corresponding codes, in order177 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 name185 if (labelCode == tkCodes[i]) {186 image.title = "TK " + name + ": " + definition + " Click for more details."; // set tooltip187 if (image.parentElement.parentElement.parentElement.classList[0] != "tocSectionTitle") { // disable onclick event in favourites section188 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 = "×";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 div215 });216 }217 }218 }219 }220 }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 = "×"; 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 221 } 222 222 223 223 function 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.mds228 let tkLabelName = label.attributes.name.value; // Element name=""229 let attributes = label.querySelectorAll("[code=" + lang + "] Attribute"); // gets attributes for selected language230 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 }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 236 } 237 237 238 238 function 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 });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 260 }; 261 261 … … 263 263 264 264 function loadAudio(audio, sectionData) { 265 var speakerObjects = []; 266 var uniqueSpeakers; 267 const inputFile = sectionData; 268 var itemType; 269 270 var accentColour = "#4CA72D"; 271 var regionTransparency = "59"; 272 273 var waveformContainer = document.getElementById("waveform"); 274 275 var wavesurfer = WaveSurfer.create({ // wavesurfer options 276 container: waveformContainer, 277 backend: "MediaElement", 278 backgroundColor: "rgb(54, 73, 78)", 279 waveColor: "white", 280 progressColor: ["white", accentColour], 281 barWidth: 2, 282 barHeight: 1.2, 283 barGap: 2, 284 barRadius: 1, 285 cursorColor: 'black', 286 plugins: [ 287 WaveSurfer.regions.create(), 288 WaveSurfer.timeline.create({ 289 container: "#wave-timeline", 290 primaryColor: "white", 291 secondaryColor: "white", 292 primaryFontColor: "white", 293 secondaryFontColor: "white" 294 }), 295 WaveSurfer.cursor.create({ 296 showTime: true, 297 opacity: 1, 298 customShowTimeStyle: { 299 'background-color': '#000', 300 color: '#fff', 301 padding: '0.2em', 302 'font-size': '12px' 303 }, 304 formatTimeCallback: (num) => { 305 return minutize(num); 306 } 307 }), 308 ], 309 }); 310 311 wavesurfer.load(audio); 312 313 // wavesurfer events 314 315 wavesurfer.on('region-click', function(region, e) { // play region audio on click 316 e.stopPropagation(); 317 regionMouseEvent(region, true); 318 wavesurfer.play(region.start); // plays from start of region 319 // region.play(); // plays region only 320 }); 321 322 wavesurfer.on('region-mouseenter', function(region) { regionMouseEvent(region, true); }); 323 wavesurfer.on('region-mouseleave', function(region) { if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) regionMouseEvent(region, false); }); 324 wavesurfer.on('region-in', function(region) { 325 regionMouseEvent(region, true); 326 if (itemType == "chapter") { 327 document.getElementById("chapter" + region.id.replace("region", "")).scrollIntoView({ 328 behavior: "smooth", 329 block: "center" 330 }); 331 } 332 }); 333 wavesurfer.on('region-out', function(region) { regionMouseEvent(region, false); }); 334 335 var loader = document.createElement("span"); // loading audio element 336 loader.innerHTML = "Loading audio"; 337 loader.id = "waveform-loader"; 338 document.querySelector("#waveform wave").prepend(loader); 339 340 wavesurfer.on('waveform-ready', function() { // retrieve regions once waveforms have loaded 341 if (inputFile.endsWith("csv")) { // diarization if csv 342 itemType = "chapter"; 343 loadCSVFile(inputFile, ["speaker", "start", "end"]); 344 } else if (inputFile.endsWith("json")) { // transcription if json 345 itemType = "word"; 346 loadJSONFile(inputFile); 347 } else { 348 console.log("Filetype of " + inputFile + " not supported.") 349 } 350 loader.remove(); // remove load text 351 }) 352 353 // toolbar elements & event handlers 354 var chapters = document.getElementById("chapters"); 355 var chapterButton = document.getElementById("chapterButton"); 356 var backButton = document.getElementById("backButton"); 357 var playPauseButton = document.getElementById("playPauseButton"); 358 var forwardButton = document.getElementById("forwardButton"); 359 var muteButton = document.getElementById("muteButton"); 360 var zoomSlider = document.getElementById("slider"); 361 chapters.style.height = "0px"; 362 chapterButton.addEventListener("click", function() { toggleChapters(); }); 363 backButton.addEventListener("click", function() { wavesurfer.skipBackward(); }); 364 playPauseButton.addEventListener("click", function() { wavesurfer.playPause() }); 365 forwardButton.addEventListener("click", function() { wavesurfer.skipForward(); }); 366 muteButton.addEventListener("click", function() { wavesurfer.toggleMute(); }); 367 zoomSlider.style["accent-color"] = accentColour; 368 369 // path to toolbar images 370 var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; 371 372 wavesurfer.on("play", function() { playPauseButton.src = interface_bootstrap_images + "pause.svg"; }); 373 wavesurfer.on("pause", function() { playPauseButton.src = interface_bootstrap_images + "play.svg"; }); 374 wavesurfer.on("mute", function(mute) { 375 if (mute) { 376 muteButton.src = interface_bootstrap_images + "mute.svg"; 377 muteButton.style.opacity = 0.6; 378 } 379 else { 380 muteButton.src = interface_bootstrap_images + "unmute.svg"; 381 muteButton.style.opacity = 1; 382 } 383 }); 384 385 zoomSlider.oninput = function () { // slider changes waveform zoom 386 wavesurfer.zoom(Number(this.value) / 4); 387 }; 388 wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point 389 390 var toggleChapters = function() { // show & hide chapter section 391 if (chapters.style.height == "0px") { 392 chapters.style.height = "30vh"; 393 } else { 394 chapters.style.height = "0px"; 395 } 396 } 397 398 function loadCSVFile(filename, manualHeader) { // based around: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript 399 $.ajax({ 400 type: "GET", 401 url: filename, 402 dataType: "text", 403 }).then(function(data) { 404 var dataLines = data.split(/\r\n|\n/); 405 var headers; 406 var startIndex; 407 uniqueSpeakers = []; // used for obtaining unique colours 408 speakerObjects = []; // list of speaker items 409 410 if (manualHeader) { // headers for columns can be provided if not existent in csv 411 headers = manualHeader; 412 startIndex = 0; 413 } else { 414 headers = dataLines[0].split(','); 415 startIndex = 1; 416 } 417 418 for (var i = startIndex; i < dataLines.length; i++) { 419 var data = dataLines[i].split(','); 420 if (data.length == headers.length) { 421 var item = {}; 422 for (var j = 0; j < headers.length; j++) { 423 item[headers[j]] = data[j]; 424 if (j == 0) { 425 if (!uniqueSpeakers.includes(data[j])) { 426 uniqueSpeakers.push(data[j]); 427 } 428 } 429 } 430 speakerObjects.push(item); 431 } 432 } 433 populateChapters(speakerObjects); 434 }); 435 } 436 437 function populateChapters(data) { // populates chapter section and adds regions to waveform 438 // colorbrewer is a web tool for guidance in choosing map colour schemes based on a variety of settings. 439 // this colour scheme is designed for qualitative data 440 colourbrewerset = colorbrewer.Set2[uniqueSpeakers.length]; 441 for (var i = 0; i < data.length; i++) { 442 var chapter = document.createElement("div"); 443 chapter.id = "chapter" + i; 444 chapter.classList.add("chapter"); 445 chapter.innerHTML = data[i].speaker + "<span class='speakerTime' id='" + "chapter" + i + "'>" + minutize(data[i].start) + " - " + minutize(data[i].end) + "</span>"; 446 chapter.addEventListener("click", e => { chapterClicked(e.target.id) }); 447 chapter.addEventListener("mouseover", e => { chapterEnter(e.target.id) }); 448 chapter.addEventListener("mouseleave", e => { chapterLeave(e.target.id) }); 449 chapters.appendChild(chapter); 450 wavesurfer.addRegion({ 451 id: "region" + i, 452 start: data[i].start, 453 end: data[i].end, 454 drag: false, 455 resize: false, 456 color: colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)] + regionTransparency, 457 }); 458 } 459 } 460 461 function loadJSONFile(filename) { 462 $.ajax({ 463 type: "GET", 464 url: filename, 465 dataType: "text", 466 }).then(function(data){ populateWords(JSON.parse(data)) }); 467 } 468 469 function populateWords(data) { // populates word section and adds regions to waveform 470 var transcription = data.transcription; 471 var words = data.words; 472 var wordContainer = document.createElement("div"); 473 wordContainer.id = "word-container"; 474 for (var i = 0; i < words.length; i++) { 475 var word = document.createElement("span"); 476 word.id = "word" + i; 477 word.classList.add("word"); 478 word.innerHTML = transcription.split(" ")[i]; 479 word.addEventListener("click", e => { wordClicked(data, e.target.id) }); 480 word.addEventListener("mouseover", e => { chapterEnter(e.target.id) }); 481 word.addEventListener("mouseleave", e => { chapterLeave(e.target.id) }); 482 wordContainer.appendChild(word); 483 wavesurfer.addRegion({ 484 id: "region" + i, 485 start: words[i].startTime, 486 end: words[i].endTime, 487 drag: false, 488 resize: false, 489 color: "rgba(255, 255, 255, 0.1)", 490 }); 491 } 492 chapters.appendChild(wordContainer); 493 } 494 495 var chapterClicked = function(id) { // plays audio from start of chapter 496 var index = id.replace("chapter", ""); 497 var start = speakerObjects[index].start; 498 var end = speakerObjects[index].end; 499 // wavesurfer.play(start, end); 500 wavesurfer.play(start); 501 } 502 503 function wordClicked(data, id) { // plays audio from start of word 504 var index = id.replace("word", ""); 505 var start = data.words[index].startTime; 506 var end = data.words[index].endTime; 507 // wavesurfer.play(start, end); 508 wavesurfer.play(start); 509 } 510 511 function chapterEnter(id) { 512 regionEnter(wavesurfer.regions.list["region" + id.replace(itemType, "")]); 513 } 514 515 function chapterLeave(id) { 516 regionLeave(wavesurfer.regions.list["region" + id.replace(itemType, "")]); 517 } 518 519 function regionMouseEvent(region, highlight) { // handles region, chapter & word colours 520 var colour; 521 if (highlight) { 522 colour = "rgb(101, 116, 116)"; 523 regionEnter(region); 524 } else { 525 colour = ""; 526 regionLeave(region); 527 } 528 var regionIndex = region.id.replace("region",""); 529 var corrItem = document.getElementById(itemType + regionIndex); 530 corrItem.style.backgroundColor = colour; 531 } 532 533 function regionEnter(region) { 534 region.update({ color: "rgba(255, 255, 255, 0.35)" }); 535 } 536 537 function regionLeave(region) { 538 if (itemType == "chapter") { 539 if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { 540 var index = region.id.replace("region", ""); 541 region.update({ color: colourbrewerset[uniqueSpeakers.indexOf(speakerObjects[index].speaker)] + regionTransparency }); 542 } 543 } else { 544 region.update({ color: "rgba(255, 255, 255, 0.1)" }); 545 } 546 } 547 548 function minutize(num) { // converts seconds to m:ss 549 var seconds = Math.round(num % 60); 550 if (seconds.toString().length == 1) seconds = "0" + seconds; 551 return Math.floor(num / 60) + ":" + seconds; 552 } 553 } 265 var speakerObjects = []; 266 var uniqueSpeakers; 267 const inputFile = sectionData; 268 var itemType; 269 270 var accentColour = "#4CA72D"; 271 // var accentColour = "#F8C537"; 272 var regionTransparency = "59"; 273 274 var waveformContainer = document.getElementById("waveform"); 275 276 var wavesurfer = WaveSurfer.create({ // wavesurfer options 277 container: waveformContainer, 278 backend: "MediaElement", 279 backgroundColor: "rgb(54, 73, 78)", 280 waveColor: "white", 281 progressColor: ["white", accentColour], 282 barWidth: 2, 283 barHeight: 1.2, 284 barGap: 2, 285 barRadius: 1, 286 cursorColor: 'black', 287 plugins: [ 288 WaveSurfer.regions.create(), 289 WaveSurfer.timeline.create({ 290 container: "#wave-timeline", 291 secondaryColor: "white", 292 secondaryFontColor: "white", 293 notchPercentHeight: "0", 294 fontSize: "12" 295 }), 296 WaveSurfer.cursor.create({ 297 showTime: true, 298 opacity: 1, 299 customShowTimeStyle: { 300 'background-color': '#000', 301 color: '#fff', 302 padding: '0.2em', 303 'font-size': '12px' 304 }, 305 formatTimeCallback: (num) => { return minutize(num); } 306 }), 307 ], 308 }); 309 310 wavesurfer.load(audio); 311 312 // wavesurfer events 313 314 wavesurfer.on('region-click', function(region, e) { // play region audio on click 315 e.stopPropagation(); 316 handleRegionColours(region, true); 317 wavesurfer.play(region.start); // plays from start of region 318 // region.play(); // plays region only 319 }); 320 321 wavesurfer.on('region-mouseenter', function(region) { handleRegionColours(region, true); }); 322 wavesurfer.on('region-mouseleave', function(region) { if (!(wavesurfer.getCurrentTime() <= region.end && wavesurfer.getCurrentTime() >= region.start)) handleRegionColours(region, false); }); 323 wavesurfer.on('region-in', function(region) { 324 handleRegionColours(region, true); 325 if (itemType == "chapter") { 326 document.getElementById("chapter" + region.id.replace("region", "")).scrollIntoView({ 327 behavior: "smooth", 328 block: "center" 329 }); 330 } 331 }); 332 wavesurfer.on('region-out', function(region) { handleRegionColours(region, false); }); 333 334 var loader = document.createElement("span"); // loading audio element 335 loader.innerHTML = "Loading audio"; 336 loader.id = "waveform-loader"; 337 document.querySelector("#waveform wave").prepend(loader); 338 339 wavesurfer.on('waveform-ready', function() { // retrieve regions once waveforms have loaded 340 if (inputFile.endsWith("csv")) { // diarization if csv 341 itemType = "chapter"; 342 loadCSVFile(inputFile, ["speaker", "start", "end"]); 343 } else if (inputFile.endsWith("json")) { // transcription if json 344 itemType = "word"; 345 loadJSONFile(inputFile); 346 } else { 347 console.log("Filetype of " + inputFile + " not supported.") 348 } 349 loader.remove(); // remove load text 350 }); 351 352 function downloadURI(loc, name) 353 { 354 var link = document.createElement("a"); 355 link.download = name; 356 link.href = loc; 357 link.click(); 358 } 359 360 // toolbar elements & event handlers 361 var chapters = document.getElementById("chapters"); 362 var chapterButton = document.getElementById("chapterButton"); 363 var backButton = document.getElementById("backButton"); 364 var playPauseButton = document.getElementById("playPauseButton"); 365 var forwardButton = document.getElementById("forwardButton"); 366 var downloadButton = document.getElementById("downloadButton"); 367 var muteButton = document.getElementById("muteButton"); 368 var zoomSlider = document.getElementById("slider"); 369 chapters.style.height = "0px"; 370 chapterButton.addEventListener("click", function() { toggleChapters(); }); 371 backButton.addEventListener("click", function() { wavesurfer.skipBackward(); }); 372 playPauseButton.addEventListener("click", function() { wavesurfer.playPause() }); 373 forwardButton.addEventListener("click", function() { wavesurfer.skipForward(); }); 374 // audio = /greenstone3/library/sites/localsite/collect/tiriana-audio/index/assoc/HASHa6b7.dir/Te_Kakano_CH09_B.mp3 375 downloadButton.addEventListener("click", function() { downloadURI(audio, audio.split(".dir/")[1]) }); 376 muteButton.addEventListener("click", function() { wavesurfer.toggleMute(); }); 377 zoomSlider.style["accent-color"] = accentColour; 378 379 // path to toolbar images 380 var interface_bootstrap_images = "interfaces/" + gs.xsltParams.interface_name + "/images/bootstrap/"; 381 382 wavesurfer.on("play", function() { playPauseButton.src = interface_bootstrap_images + "pause.svg"; }); 383 wavesurfer.on("pause", function() { playPauseButton.src = interface_bootstrap_images + "play.svg"; }); 384 wavesurfer.on("mute", function(mute) { 385 if (mute) { 386 muteButton.src = interface_bootstrap_images + "mute.svg"; 387 muteButton.style.opacity = 0.6; 388 } 389 else { 390 muteButton.src = interface_bootstrap_images + "unmute.svg"; 391 muteButton.style.opacity = 1; 392 } 393 }); 394 395 zoomSlider.oninput = function () { // slider changes waveform zoom 396 wavesurfer.zoom(Number(this.value) / 4); 397 }; 398 wavesurfer.zoom(zoomSlider.value / 4); // set default zoom point 399 400 var toggleChapters = function() { // show & hide chapter section 401 if (chapters.style.height == "0px") { 402 chapters.style.height = "30vh"; 403 } else { 404 chapters.style.height = "0px"; 405 } 406 } 407 408 function loadCSVFile(filename, manualHeader) { // based around: https://stackoverflow.com/questions/7431268/how-to-read-data-from-csv-file-using-javascript 409 $.ajax({ 410 type: "GET", 411 url: filename, 412 dataType: "text", 413 }).then(function(data) { 414 var dataLines = data.split(/\r\n|\n/); 415 var headers; 416 var startIndex; 417 uniqueSpeakers = []; // used for obtaining unique colours 418 speakerObjects = []; // list of speaker items 419 420 if (manualHeader) { // headers for columns can be provided if not existent in csv 421 headers = manualHeader; 422 startIndex = 0; 423 } else { 424 headers = dataLines[0].split(','); 425 startIndex = 1; 426 } 427 428 for (var i = startIndex; i < dataLines.length; i++) { 429 var data = dataLines[i].split(','); 430 if (data.length == headers.length) { 431 var item = {}; 432 for (var j = 0; j < headers.length; j++) { 433 item[headers[j]] = data[j]; 434 if (j == 0) { 435 if (!uniqueSpeakers.includes(data[j])) { 436 uniqueSpeakers.push(data[j]); 437 } 438 } 439 } 440 speakerObjects.push(item); 441 } 442 } 443 populateChapters(speakerObjects); 444 }); 445 } 446 447 function populateChapters(data) { // populates chapter section and adds regions to waveform 448 // colorbrewer is a web tool for guidance in choosing map colour schemes based on a variety of settings. 449 // this colour scheme is designed for qualitative data 450 451 if (uniqueSpeakers.length > 8) colourbrewerset = colorbrewer.Set2[8]; 452 else if (uniqueSpeakers.length < 3) colourbrewerset = colorbrewer.Set2[3]; 453 else colourbrewerset = colorbrewer.Set2[uniqueSpeakers.length]; 454 455 for (var i = 0; i < data.length; i++) { 456 var chapter = document.createElement("div"); 457 var speakerLetter = getLetter(data[i]); 458 chapter.id = "chapter" + i; 459 chapter.classList.add("chapter"); 460 chapter.innerHTML = "Speaker " + speakerLetter + "<span class='speakerTime' id='" + "chapter" + i + "'>" + minutize(data[i].start) + " - " + minutize(data[i].end) + "</span>"; 461 chapter.addEventListener("click", e => { chapterClicked(e.target.id) }); 462 chapter.addEventListener("mouseover", e => { chapterEnter(e.target.id) }); 463 chapter.addEventListener("mouseleave", e => { chapterLeave(e.target.id) }); 464 chapters.appendChild(chapter); 465 // console.log("index: " + uniqueSpeakers.indexOf(data[i].speaker)%8); 466 // console.log("colour: " + colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8]); 467 wavesurfer.addRegion({ 468 id: "region" + i, 469 start: data[i].start, 470 end: data[i].end, 471 drag: false, 472 resize: false, 473 color: colourbrewerset[uniqueSpeakers.indexOf(data[i].speaker)%8] + regionTransparency, 474 }); 475 } 476 } 477 478 function loadJSONFile(filename) { 479 $.ajax({ 480 type: "GET", 481 url: filename, 482 dataType: "text", 483 }).then(function(data){ populateWords(JSON.parse(data)) }); 484 } 485 486 function populateWords(data) { // populates word section and adds regions to waveform 487 var transcription = data.transcription; 488 var words = data.words; 489 var wordContainer = document.createElement("div"); 490 wordContainer.id = "word-container"; 491 for (var i = 0; i < words.length; i++) { 492 var word = document.createElement("span"); 493 word.id = "word" + i; 494 word.classList.add("word"); 495 word.innerHTML = transcription.split(" ")[i]; 496 word.addEventListener("click", e => { wordClicked(data, e.target.id) }); 497 word.addEventListener("mouseover", e => { chapterEnter(e.target.id) }); 498 word.addEventListener("mouseleave", e => { chapterLeave(e.target.id) }); 499 wordContainer.appendChild(word); 500 wavesurfer.addRegion({ 501 id: "region" + i, 502 start: words[i].startTime, 503 end: words[i].endTime, 504 drag: false, 505 resize: false, 506 color: "rgba(255, 255, 255, 0.1)", 507 }); 508 } 509 chapters.appendChild(wordContainer); 510 } 511 512 var chapterClicked = function(id) { // plays audio from start of chapter 513 var index = id.replace("chapter", ""); 514 var start = speakerObjects[index].start; 515 var end = speakerObjects[index].end; 516 // wavesurfer.play(start, end); 517 wavesurfer.play(start); 518 } 519 520 function wordClicked(data, id) { // plays audio from start of word 521 var index = id.replace("word", ""); 522 var start = data.words[index].startTime; 523 var end = data.words[index].endTime; 524 // wavesurfer.play(start, end); 525 wavesurfer.play(start); 526 } 527 528 function chapterEnter(id) { 529 regionEnter(wavesurfer.regions.list["region" + id.replace(itemType, "")]); 530 } 531 532 function chapterLeave(id) { 533 regionLeave(wavesurfer.regions.list["region" + id.replace(itemType, "")]); 534 } 535 536 function handleRegionColours(region, highlight) { // handles region, chapter & word colours 537 var colour; 538 if (highlight) { 539 colour = "rgb(101, 116, 116)"; 540 regionEnter(region); 541 } else { 542 colour = ""; 543 regionLeave(region); 544 } 545 var regionIndex = region.id.replace("region",""); 546 var corrItem = document.getElementById(itemType + regionIndex); 547 corrItem.style.backgroundColor = colour; 548 } 549 550 function regionEnter(region) { 551 region.update({ color: "rgba(255, 255, 255, 0.35)" }); 552 } 553 554 function regionLeave(region) { 555 if (itemType == "chapter") { 556 if (!(wavesurfer.getCurrentTime() + 0.1 < region.end && wavesurfer.getCurrentTime() > region.start)) { 557 var index = region.id.replace("region", ""); 558 region.update({ color: colourbrewerset[uniqueSpeakers.indexOf(speakerObjects[index].speaker)%8] + regionTransparency }); 559 } 560 } else { 561 region.update({ color: "rgba(255, 255, 255, 0.1)" }); 562 } 563 } 564 565 function minutize(num) { // converts seconds to m:ss for chapters & waveform hover 566 var seconds = Math.round(num % 60); 567 if (seconds.toString().length == 1) seconds = "0" + seconds; 568 return Math.floor(num / 60) + ":" + seconds; 569 } 570 571 function getLetter(val) { 572 var speakerNum = parseInt(val.speaker.replace("SPEAKER_","")); 573 return String.fromCharCode(65 + speakerNum); // 'A' == UTF-16 65 574 } 575 } 576 577 function formatAudioDuration(duration) { 578 console.log(duration); 579 var [hrs, mins, secs, ms] = duration.replace(".", ":").split(":"); 580 return hrs + ":" + mins + ":" + secs; 581 } -
main/trunk/greenstone3/web/interfaces/default/style/core.css
r36198 r36217 1386 1386 1387 1387 #audioContainer { 1388 width: 65vw;1389 background-color: rgb(24, 36, 39);1388 width: 100%; 1389 /* background-color: rgb(24, 36, 39); */ 1390 1390 scrollbar-color: white transparent; 1391 1391 } … … 1396 1396 1397 1397 #toolbar { 1398 background-color: rgb(24, 36, 39); 1398 1399 position: relative; 1399 1400 display: flex; … … 1428 1429 1429 1430 #chapters { 1430 width: 100%;1431 width: 50%; 1431 1432 height: 0; 1432 1433 max-height: 30vh; … … 1458 1459 } 1459 1460 1460 #muteButton { 1461 padding-left: 1em; 1461 #downloadButton { 1462 transform: scale(0.8); 1463 padding-right: 0.75em; 1462 1464 } 1463 1465 1464 1466 .wavesurfer-region { 1465 1467 cursor: pointer !important; 1466 /* border: 1px solid white!important; */1468 /* border: 1px solid rgba(255, 255, 255, 0.3) !important; */ 1467 1469 transition: 0.1s ease; 1468 1470 } … … 1481 1483 #zoomIcon { 1482 1484 width: 1.2em !important; 1485 } 1486 1487 #chapterButton { 1483 1488 padding-left: 0.2em; 1489 padding-right: 0.75em; 1484 1490 } 1485 1491 … … 1507 1513 } 1508 1514 1515 wave { 1516 width: 100%; 1517 } 1518 1509 1519 .toolbar-section { 1510 width: 12em;1520 width: 33%; 1511 1521 } 1512 1522 … … 1536 1546 #tapeDetails { 1537 1547 border: 1px solid olive; 1548 min-width: 30%; 1538 1549 } 1539 1550 1540 1551 #tapeDetails td { 1541 1552 padding: 0.2em; 1542 1553 } 1554 1555 #tapeDetails td:nth-child(1) { 1556 padding-right: 6em; 1543 1557 } 1544 1558 … … 1546 1560 background-color: darkseagreen; 1547 1561 } 1548 1549 #fCol {1550 padding-right: 5em;1551 }
Note:
See TracChangeset
for help on using the changeset viewer.