Changeset 36196


Ignore:
Timestamp:
2022-05-19T13:56:44+12:00 (2 years ago)
Author:
davidb
Message:

audio player improvements

Location:
main/trunk/greenstone3/web/interfaces/default
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • main/trunk/greenstone3/web/interfaces/default/js/utility_scripts.js

    r36163 r36196  
    1111*/
    1212function 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;
    2121}
    2222
    2323/*
    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.
     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.
    5757*/
    5858
    5959/* 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()
     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()
    6565*/
    6666function 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;
     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;
    7676}
    7777
    7878/*
    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.
    8181*/
    8282function 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;
     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;
    9595}
    9696
     
    9999***************/
    100100function 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();
     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();
    124124}
    125125
     
    127127function floatMenu(enabled)
    128128{
    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);
     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);
    167167}
    168168
     
    174174
    175175function 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    }
     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  }
    221221}
    222222
    223223function 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    }
     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  }
    236236}
    237237
    238238function 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  });
    260260};
    261261
     
    263263
    264264function 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 }
     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}
  • main/trunk/greenstone3/web/interfaces/default/style/core.css

    r36166 r36196  
    13691369.flex-leftalign {
    13701370    display: flex;
    1371     flex-wrap: wrap;
    1372     flex-direction: row;
     1371    flex-flow: row wrap;
    13731372    justify-content: left;
    1374     align-items: left;
     1373}
     1374
     1375.flex-rightalign {
     1376    display: flex;
     1377    flex-flow: row wrap;
     1378    justify-content: right;
     1379}
     1380
     1381.flex-centeralign {
     1382    display: flex;
     1383    flex-flow: row wrap;
     1384    justify-content: center;
    13751385}
    13761386
     
    13781388    width: 35vw;
    13791389    background-color: rgb(24, 36, 39);
     1390    scrollbar-color: white transparent;
    13801391}
    13811392
     
    13871398    position: relative;
    13881399    display: flex;
     1400    flex-direction: row;
    13891401    flex-wrap: wrap;
    1390     flex-direction: row;
    1391     justify-content: center;
     1402    justify-content: space-between;
    13921403    align-items: center;
    13931404    color: white;
    13941405    width: 100%;
    1395     height: 2em;
     1406    height: fit-content;
     1407}
     1408
     1409#toolbar button {
     1410    cursor: pointer;
     1411    height: 100%;
    13961412}
    13971413
     
    14141430    width: 100%;
    14151431    height: 0;
    1416     max-height: 60vh;
     1432    max-height: 30vh;
    14171433    font-size: 14px;
    14181434    background-color: rgb(54, 73, 78);
    14191435    color: white;
    14201436    overflow-y: scroll;
    1421     transition: height 0.7s ease;
     1437    transition: height 0.4s ease;
    14221438}
    14231439
     
    14421458}
    14431459
    1444 #chapterButton {
    1445     padding-left: 0.5em;
    1446 }
    1447 
    14481460#muteButton {
    1449     position: absolute;
    1450     right: 0.5em;
    1451 }
    1452 
    1453 #toolbar button {
    1454     cursor: pointer;
    1455     height: 100%;
     1461    padding-left: 1em;
    14561462}
    14571463
     
    14671473
    14681474#slider {
    1469     accent-color: #F8C537;
    1470     background-image: url("icons/zoom-in.svg");
    1471     background-size: contain;
    1472     background-position: center center;
    1473     background-repeat: no-repeat;
    1474     width: 60%;
     1475    width: 10em;
    14751476    margin-left: 0.5em;
    14761477    margin-right: 0.5em;
     1478    cursor: pointer;
    14771479}
    14781480
    14791481#zoomIcon {
    14801482    width: 1.2em !important;
     1483    padding-left: 0.2em;
    14811484}
    14821485
     
    14851488}
    14861489
    1487 #zoom-section {
    1488     position: absolute;
    1489     left: 0.5em;
     1490#waveform-loader {
     1491    color: white;
     1492    padding: 0.2em;
     1493}
     1494
     1495#waveform-loader:after {
     1496    display: inline-block;
     1497    animation: dotty steps(1, end) 1s infinite;
     1498    content: '';
     1499}
     1500
     1501@keyframes dotty {
     1502    0%   { content: ''; }
     1503    25%  { content: '.'; }
     1504    50%  { content: '..'; }
     1505    75%  { content: '...'; }
     1506    100% { content: ''; }
     1507}
     1508
     1509.toolbar-section {
     1510    width: 12em;
    14901511}
    14911512
Note: See TracChangeset for help on using the changeset viewer.