<commit_before>// toggling of navigation menus
var toggleMainNav = document.getElementById('js-main-nav-toggle');
if (toggleMainNav !== null) {
  toggleMainNav.onclick = function() {
    document.body.classList.toggle('has-active-main-nav');
  }
}

var toggleContentNav = document.getElementById('js-content-nav-toggle');
if (toggleContentNav !== null) {
  toggleContentNav.onclick = function() {
    document.body.classList.toggle('has-active-content-nav');
  }
}

function initHotKeys() {

  var bodyEl = document.body;
  var searchInput = document.getElementById('search');

  document.addEventListener('keydown', function (event) {
      if (event.defaultPrevented) {
          return;
      }

      var key = event.key || event.keyCode;

      // ESC to close search window
      if (key === 'Escape' || key === 'Esc' || key === 27) {
        document.body.classList.remove('has-active-search-window');
        console.log('esc');
      }

      // / to close toggle window
      if (key === '/' || key === 'Period' || key === 58) {
        event.preventDefault();
        document.body.classList.toggle('has-active-search-window');
        if (bodyEl.classList.contains('has-active-search-window')) {
          searchInput.focus();
        }
      }

      //when search is active TAB puts search input into focus
      if (bodyEl.classList.contains('has-active-search-window')) {
        if (key === 'Tab' || key === 'Tab' || key === 9) {
          searchInput.focus();
        }
      }
  });
};

// search functionality
function debounce(func, wait) {
  var timeout;

  return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timeout);

    timeout = setTimeout(function () {
      timeout = null;
      func.apply(context, args);
    }, wait);
  };
}

// The strategy is as follows:
// First, assign a value to each word in the document:
//  Words that correspond to search terms (stemmer aware): 40
//  Normal words: 2
//  First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in .
function makeTeaser(body, terms) {
  var TERM_WEIGHT = 40;
  var NORMAL_WORD_WEIGHT = 2;
  var FIRST_WORD_WEIGHT = 8;
  //var TEASER_MAX_WORDS = 17;
  var TEASER_MAX_CHARS = 63;

  var stemmedTerms = terms.map(function (w) {
    return elasticlunr.stemmer(w.toLowerCase());
  });

  var termFound = false;
  var index = 0;
  var weighted = []; // contains elements of ["word", weight, index_in_document]

  // split in sentences, then words
  var sentences = body.toLowerCase().split(". ");

  for (var i in sentences) {
    var words = sentences[i].split(" ");
    var value = FIRST_WORD_WEIGHT;

    for (var j in words) {
      var word = words[j];

      if (word.length > 0) {
        for (var k in stemmedTerms) {
          if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
            value = TERM_WEIGHT;
            termFound = true;
          }
        }
        weighted.push([word, value, index]);
        value = NORMAL_WORD_WEIGHT;
      }

      index += word.length;
      index += 1;  // ' ' or '.' if last word in sentence
    }

    index += 1;  // because we split at a two-char boundary '. '
  }

  if (weighted.length === 0) {
    return body;
  }

  var windowWeights = [];
  var windowSize = 0;

  // loop over words (weighted) till max chars is reached adding a counter to windowSize every time a new word has passed
  var sumChars = 0;
  for (var i = 0; i < weighted.length; i++) {
    sumChars += weighted[i][0].length;
    if (sumChars <= TEASER_MAX_CHARS) {
      windowSize++;
    }
  }

  // We add a window with all the weights first
  var curSum = 0;
  for (var i = 0; i < windowSize; i++) {
    curSum += weighted[i][1];
  }
  windowWeights.push(curSum);

  for (var i = 0; i < weighted.length - windowSize; i++) {
    curSum -= weighted[i][1];
    curSum += weighted[i + windowSize][1];
    windowWeights.push(curSum);
  }

  // If we didn't find the term, just pick the first window
  var maxSumIndex = 0;
  if (termFound) {
    var maxFound = 0;
    // backwards
    for (var i = windowWeights.length - 1; i >= 0; i--) {
      if (windowWeights[i] > maxFound) {
        maxFound = windowWeights[i];
        maxSumIndex = i;
      }
    }
  }

  var teaser = [];
  var startIndex = weighted[maxSumIndex][2];
  for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
    var word = weighted[i];
    if (startIndex < word[2]) {
      // missing text from index to start of `word`
      teaser.push(body.substring(startIndex, word[2]));
      startIndex = word[2];
    }

    startIndex = word[2] + word[0].length;
    teaser.push(body.substring(word[2], startIndex));

  }
  teaser.push("…");
  return teaser.join("");
}

function formatSearchResultTitle(item) {
  var pathArray = item.ref.split( '/' );

  // first directory is in 4th element of split path array
  var firstPartTitle = pathArray[3];

  // last directory is in 2nd to last element of array
  var lastPartTitle = item.doc.title;

  var fullTitle = firstPartTitle + '' + ' / ' + lastPartTitle + '';

  return fullTitle;
}

function formatSearchResultItem(item, terms) {
  console.log(item);
  var li = document.createElement("li");
  var createA = document.createElement("a");
  li.appendChild(createA);
  var teaserTitle = formatSearchResultTitle(item);
  li.classList.add("search-results__item");
  li.classList.add("ph3");
  var hrefA = item.ref;
  createA.setAttribute('href', hrefA);
  createA.setAttribute('class','no-underline block pl6 pv3');
  createA.innerHTML = `<span class="fs45 capitalize">${teaserTitle}</span>`;
  createA.innerHTML += `<span class='pr7 fs5 none float-right'>→</span>`;
  createA.innerHTML += `<div class="fs35 truncate mr10">${makeTeaser(item.doc.body, terms)}</div>`;
  return li;
}

function initSearch() {
  var searchInput = document.getElementById("search");
  var searchForm = document.getElementById("search-form");
  var inputReset = searchForm.nextElementSibling;

  if (!searchInput) {
    return;
  }

  var searchResults = document.querySelector(".search-results");
  var searchResultsHeader = document.querySelector(".search-results__header");
  var searchResultsItems = document.querySelector(".search-results__items");
  var MAX_ITEMS = 200;
  var options = {
    bool: "AND",
    expand: true, // turn on partial word search
    fields: {
      title: {boost: 2},
      body: {boost: 1},
    }
  };
  var currentTerm = "";
  var index = elasticlunr.Index.load(window.searchIndex);
  // toggle search window
  var toggleSearchWindow = document.getElementById('js-search-window-toggle');
  var bodyEl = document.body;
  var searchOverlay = document.getElementById('search-overlay');
  var searchWindow = document.getElementById('search-window');
  var searchActive = bodyEl.classList.contains('has-active-search-window');

  if (toggleSearchWindow !== null) {
    toggleSearchWindow.onclick = function() {
      inputReset.style.display="none";

      //activate search
      bodyEl.classList.toggle('has-active-search-window');
      //put input in focus when activating search
      searchInput.focus();

      searchInput.value = "";
      searchResultsHeader.value = "";
      searchResults.style.display = searchInput.value.trim() === "" ? "none" : "block";
    }
    searchOverlay.addEventListener('click', function(e){
    	if (!searchWindow.contains(e.target)){
        bodyEl.classList.toggle('has-active-search-window');
      }
    })
  }

  searchInput.addEventListener("keyup", debounce(function() {
    inputReset.style.display="block";

    searchResultsHeader.value = "";
    var term = searchInput.value.trim();
    /*
    //removed to fix: a state where there’s a valid search term, but no results

    if (term === currentTerm || !index) {
      return;
    }*/

    searchResults.style.display = term === "" ? "none" : "block";
    searchResults.style.overflowY = term === "" ? "hidden" : "scroll";
    searchResultsItems.innerHTML = "";
    if (term === "") {
      inputReset.style.display="none";
      return;
    }

    var results = index.search(term, options).filter(function (r) {
      return r.doc.body !== "";
    });

    if (results.length === 0) {
      searchResultsHeader.innerText = `No search results for '${term}'.`;
      return;
    }

    currentTerm = term;
    searchResultsHeader.innerText = `${results.length} search results for '${term}':`;
    for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
      if (!results[i].doc.body) {
        continue;
      }
      searchResultsItems.appendChild(formatSearchResultItem(results[i], term.split(" ")));
    }
  }, 150));

  // reset input button
  inputReset.onclick=function(){
    document.getElementById('search-form').reset();
  }
  searchForm.addEventListener("reset", function(event){
    var e = document.createEvent('KeyboardEvent');
    e.initEvent("keyup", false, true);
    searchInput.dispatchEvent(e);
  });

}

if (document.readyState === "complete" ||
    (document.readyState !== "loading" && !document.documentElement.doScroll)
) {
  initToggleSearchWindow();
} else {
  document.addEventListener("DOMContentLoaded", function () {
    initSearch();
    initHotKeys();
  });
}
<commit_msg>Fixes #149.<commit_after>// toggling of navigation menus
var toggleMainNav = document.getElementById('js-main-nav-toggle');
if (toggleMainNav !== null) {
  toggleMainNav.onclick = function() {
    document.body.classList.toggle('has-active-main-nav');
  }
}

var toggleContentNav = document.getElementById('js-content-nav-toggle');
if (toggleContentNav !== null) {
  toggleContentNav.onclick = function() {
    document.body.classList.toggle('has-active-content-nav');
  }
}

function initHotKeys() {

  var bodyEl = document.body;
  var searchInput = document.getElementById('search');

  document.addEventListener('keydown', function (event) {
      if (event.defaultPrevented) {
          return;
      }

      var key = event.key || event.keyCode;

      // ESC to close search window
      if (key === 'Escape' || key === 'Esc' || key === 27) {
        document.body.classList.remove('has-active-search-window');
        console.log('esc');
      }

      // / to close toggle window
      if (key === '/' || key === 'Period' || key === 58) {
        event.preventDefault();
        document.body.classList.toggle('has-active-search-window');
        if (bodyEl.classList.contains('has-active-search-window')) {
          searchInput.focus();
        }
      }

      //when search is active TAB puts search input into focus
      if (bodyEl.classList.contains('has-active-search-window')) {
        if (key === 'Tab' || key === 'Tab' || key === 9) {
          searchInput.focus();
        }
      }
  });
};

// search functionality
function debounce(func, wait) {
  var timeout;

  return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timeout);

    timeout = setTimeout(function () {
      timeout = null;
      func.apply(context, args);
    }, wait);
  };
}

// The strategy is as follows:
// First, assign a value to each word in the document:
//  Words that correspond to search terms (stemmer aware): 40
//  Normal words: 2
//  First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in .
function makeTeaser(body, terms) {
  var TERM_WEIGHT = 40;
  var NORMAL_WORD_WEIGHT = 2;
  var FIRST_WORD_WEIGHT = 8;
  //var TEASER_MAX_WORDS = 17;
  var TEASER_MAX_CHARS = 63;

  var stemmedTerms = terms.map(function (w) {
    return elasticlunr.stemmer(w.toLowerCase());
  });

  var termFound = false;
  var index = 0;
  var weighted = []; // contains elements of ["word", weight, index_in_document]

  // split in sentences, then words
  var sentences = body.toLowerCase().split(". ");

  for (var i in sentences) {
    var words = sentences[i].split(" ");
    var value = FIRST_WORD_WEIGHT;

    for (var j in words) {
      var word = words[j];

      if (word.length > 0) {
        for (var k in stemmedTerms) {
          if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
            value = TERM_WEIGHT;
            termFound = true;
          }
        }
        weighted.push([word, value, index]);
        value = NORMAL_WORD_WEIGHT;
      }

      index += word.length;
      index += 1;  // ' ' or '.' if last word in sentence
    }

    index += 1;  // because we split at a two-char boundary '. '
  }

  if (weighted.length === 0) {
    return body;
  }

  var windowWeights = [];
  var windowSize = 0;

  // loop over words (weighted) till max chars is reached adding a counter to windowSize every time a new word has passed
  var sumChars = 0;
  for (var i = 0; i < weighted.length; i++) {
    sumChars += weighted[i][0].length;
    if (sumChars <= TEASER_MAX_CHARS) {
      windowSize++;
    }
  }

  // We add a window with all the weights first
  var curSum = 0;
  for (var i = 0; i < windowSize; i++) {
    curSum += weighted[i][1];
  }
  windowWeights.push(curSum);

  for (var i = 0; i < weighted.length - windowSize; i++) {
    curSum -= weighted[i][1];
    curSum += weighted[i + windowSize][1];
    windowWeights.push(curSum);
  }

  // If we didn't find the term, just pick the first window
  var maxSumIndex = 0;
  if (termFound) {
    var maxFound = 0;
    // backwards
    for (var i = windowWeights.length - 1; i >= 0; i--) {
      if (windowWeights[i] > maxFound) {
        maxFound = windowWeights[i];
        maxSumIndex = i;
      }
    }
  }

  var teaser = [];
  var startIndex = weighted[maxSumIndex][2];
  for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
    var word = weighted[i];
    if (startIndex < word[2]) {
      // missing text from index to start of `word`
      teaser.push(body.substring(startIndex, word[2]));
      startIndex = word[2];
    }

    startIndex = word[2] + word[0].length;
    teaser.push(body.substring(word[2], startIndex));

  }
  teaser.push("…");
  return teaser.join("");
}

function formatSearchResultTitle(item) {
  var pathArray = item.ref.split( '/' );

  // first directory is in 4th element of split path array
  var firstPartTitle = pathArray[3];

  // last directory is in 2nd to last element of array
  var lastPartTitle = item.doc.title;

  var fullTitle = firstPartTitle + '' + ' / ' + lastPartTitle + '';

  return fullTitle;
}

function formatSearchResultItem(item, terms) {
  console.log(item);
  var li = document.createElement("li");
  var createA = document.createElement("a");
  li.appendChild(createA);
  var teaserTitle = formatSearchResultTitle(item);
  li.classList.add("search-results__item");
  li.classList.add("ph3");
  var hrefA = item.ref;
  createA.setAttribute('href', hrefA);
  createA.setAttribute('class','no-underline block pl6 pv3');
  createA.innerHTML = `<span class="fs45 capitalize">${teaserTitle}</span>`;
  createA.innerHTML += `<span class='pr7 fs5 none float-right'>→</span>`;
  createA.innerHTML += `<div class="fs35 truncate mr10">${makeTeaser(item.doc.body, terms)}</div>`;
  return li;
}

function initSearch() {
  var searchInput = document.getElementById("search");
  var searchForm = document.getElementById("search-form");
  var inputReset = searchForm.nextElementSibling;

  if (!searchInput) {
    return;
  }

  var searchResults = document.querySelector(".search-results");
  var searchResultsHeader = document.querySelector(".search-results__header");
  var searchResultsItems = document.querySelector(".search-results__items");
  var MAX_ITEMS = 200;
  var options = {
    bool: "AND",
    expand: true, // turn on partial word search
    fields: {
      title: {boost: 2},
      body: {boost: 1},
    }
  };
  var currentTerm = "";
  var index = elasticlunr.Index.load(window.searchIndex);
  // toggle search window
  var toggleSearchWindow = document.getElementById('js-search-window-toggle');
  var bodyEl = document.body;
  var searchOverlay = document.getElementById('search-overlay');
  var searchWindow = document.getElementById('search-window');
  var searchActive = bodyEl.classList.contains('has-active-search-window');

  if (toggleSearchWindow !== null) {
    toggleSearchWindow.onclick = function() {
      inputReset.style.display="none";

      //activate search
      bodyEl.classList.toggle('has-active-search-window');
      //put input in focus when activating search
      searchInput.focus();

      searchInput.value = "";
      searchResultsHeader.value = "";
      searchResults.style.display = searchInput.value.trim() === "" ? "none" : "block";
    }
    searchOverlay.addEventListener('click', function(e){
    	if (!searchWindow.contains(e.target)){
        bodyEl.classList.toggle('has-active-search-window');
      }
    })
  }

  // Don't refresh the page on enter
  searchInput.addEventListener('keydown', function(event){
    if (event.keyCode === 13) {
      event.preventDefault();
      return false;
    }
  })

  searchInput.addEventListener("keyup", debounce(function() {
    inputReset.style.display="block";

    searchResultsHeader.value = "";
    var term = searchInput.value.trim();
    /*
    //removed to fix: a state where there’s a valid search term, but no results

    if (term === currentTerm || !index) {
      return;
    }*/

    searchResults.style.display = term === "" ? "none" : "block";
    searchResults.style.overflowY = term === "" ? "hidden" : "scroll";
    searchResultsItems.innerHTML = "";
    if (term === "") {
      inputReset.style.display="none";
      return;
    }

    var results = index.search(term, options).filter(function (r) {
      return r.doc.body !== "";
    });

    if (results.length === 0) {
      searchResultsHeader.innerText = `No search results for '${term}'.`;
      return;
    }

    currentTerm = term;
    searchResultsHeader.innerText = `${results.length} search results for '${term}':`;
    for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
      if (!results[i].doc.body) {
        continue;
      }
      searchResultsItems.appendChild(formatSearchResultItem(results[i], term.split(" ")));
    }
  }, 150));

  // reset input button
  inputReset.onclick=function(){
    document.getElementById('search-form').reset();
  }
  searchForm.addEventListener("reset", function(event){
    var e = document.createEvent('KeyboardEvent');
    e.initEvent("keyup", false, true);
    searchInput.dispatchEvent(e);
  });

}

if (document.readyState === "complete" ||
    (document.readyState !== "loading" && !document.documentElement.doScroll)
) {
  initToggleSearchWindow();
} else {
  document.addEventListener("DOMContentLoaded", function () {
    initSearch();
    initHotKeys();
  });
}
<|endoftext|>


<commit_before>'use strict';

import _ from '../underscore_ext';
import {DATA_TYPE} from './constants';

const {MUTATION_BY_POS, SEGMENTED_CN, GENOMIC} = DATA_TYPE;

const HEADER_LENGTH_LIMIT = 255,
    MULTI_ERROR_LIMIT = 3;

let ERRORS, HEADER_ERRORS;

const getColumns = line => line.replace(/[\r\n]+$/, '').split(/\t/g);

const filterEmpty = arr => arr.filter(elem => !!elem);

const getErrDataType = (dataType) => dataType === MUTATION_BY_POS || dataType === SEGMENTED_CN ? dataType : 'dense';

const getMutationHeaderRegExps = [
	{ regexp: /sample[ _]*(name|id)?/i, name: "'sample'"},
	{ regexp: /chr(om)?/i, name: "'chrom'" },
	{ regexp: /start/i, name: "'start'" },
	{ regexp: /end/i, name: "'end'" },
	{ regexp: /alt(ernate)?/i, name: "'alternate'" },
	{ regexp: /ref(erence)?/i, name: "'reference'" }
];

const getSegmentedHeaderRegExps = [
	{ regexp: /sample[ _]*(name|id)?/i, name: "'sample'"},
	{ regexp: /chr(om)?/i, name: "'chrom'" },
	{ regexp: /start/i, name: "'start'" },
	{ regexp: /end/i, name: "'end'" },
	{ regexp: /value/i, name: "'value'" }
];

const gatherErrorMessages = (state, dataType, errors, line, i) => {
    errors.forEach(error => {
        error.getErrors(state, dataType, line, i);
	});
};

// Return function that error checks a line, keeping results (imperatively)
// in 'state'.
const getErrors = (dataType, state) => {
    var filterByDataType = getErrDataType(dataType),
		dataTypeErrors = ERRORS.filter(err => err.forType.some(dType => dType === filterByDataType)),
		dataTypeHeaderErrors = HEADER_ERRORS.filter(err => err.forType.some(dType => dType === filterByDataType)),
		i = 0;
	state.errors = [];
	state.warnings = [];
	state.snippets = [];

	return line => {
		//iterate through rules and populate state
		var lineCols = getColumns(line); // should this trim()? Or trim() earlier?
		if (i === 0) {
			state.rows = new Set();
			state.headerLen = lineCols.length;
			gatherErrorMessages(state, dataType, dataTypeHeaderErrors, lineCols);
		} else {
			gatherErrorMessages(state, dataType, dataTypeErrors, lineCols, i);
		}

		i += 1;
	};
};

const hasColumn = (header, columnName, regexp) => {
    if (!header.some(h => h.match(regexp))) {
        return columnName;
    }
};

const hasSparseDataColumns = (header, colRegExps) =>
    filterEmpty(colRegExps.map(r => hasColumn(header, r.name, r.regexp)));

ERRORS = [
    {
        //HEADER COLUMN COUNT MISMATCH
        forType: [MUTATION_BY_POS, SEGMENTED_CN, 'dense'], // XXX for all types?
        getErrors: (state, dataType, line, i) => {
			var c = state.columnMismatch || 0;
			if (line.length !== state.headerLen && c < MULTI_ERROR_LIMIT) {
				state.errors.push(`The number of headers doesn't match the number of columns on line ${i + 1}. Please edit the file and reload`);
				state.columnMismatch = c + 1;
			}
        }
    },
    {
        //DUPLICATE ROW KEY
        forType: ['dense'],
        getErrors: (state, dataType, line/*, i*/) => {
			if (!state.duplicateRows && state.rows.has(line[0])) {
				if (dataType === GENOMIC) {
					state.warnings.push(`There are duplicate probe names in your file. An example is ${line[0]}. We will load the first one and rename the duplicates.`);
				} else {
					state.warnings.push(`There are duplicate sample names in your file. An example is ${line[0]}. We will load the first one and ignore all others.`);
				}
				state.duplicateRows = true;
			} else {
				state.rows.add(_.copyStr(line[0]));
			}
        }
    }
];

HEADER_ERRORS = [
    {
        //HEADER LENGTHS
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            const message = (header) => `${header.slice(0, 100)}... is too long. Please limit to 255 characters`;

            var c = 0;
            for(let i = 0; i < line.length; i++) {
                const name = line[i];

                if (name.length > HEADER_LENGTH_LIMIT) {
                    state.errors.push(message(name));
                    c += 1;

                    if (c >= MULTI_ERROR_LIMIT) {
                        break;
                    }
                }
            }
        }
    },
    { // XXX discard, or maybe check for quotes?
        //HEADER CHARACTERS
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (/*state, line*/) => {
            // const message = (header) => `Headers can only have xyz. Please change ${header}`;
        }
    },
    {
        //HEADER EMPTY
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            if (line.some(h => h.trim() === '')) {
				state.errors.push('One or more of your headers is blank. Please edit the file and reload.');
            }
        }
    },
    {
        //DUPLICATE HEADER KEYS
        forType: ['dense'],
        getErrors: (state, dataType, line) => {
			var headers = new Set(),
				dup = _.find(line, s => {
					if (headers.has(s)) {
						return true;
					}
					headers.add(s); // XXX note side-effecting predicate
				});
			if (dup) {
				if (dataType === GENOMIC) {
					state.warnings.push(`There are duplicate samples in your file. An example is ${dup}. We will load the first one and ignore all others.`);
				} else {
					state.warnings.push(`There are duplicate phenotypes in your file. An example is ${dup}. We will load the first one and rename the duplicates.`);
				}
			}
        }
    },
    {
        //SEGMENTED REQUIRED HEADERS
        forType: [SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            const missing = hasSparseDataColumns(line, getSegmentedHeaderRegExps);
			if (missing.length) {
				state.errors.push(`For segmented copy number data we require 5 columns: 'sample', 'chrom', 'start', 'end', and 'value'. You are missing ${missing.join(', ')}. Please edit your file and reload.`);
				state.snippets.push({
					exampleLines: [
						['sample', 'chrom', 'start', 'end', 'value'],
						['TCGA-OR-A5LT-01', 'chr1', '61735', '98588', '0.1842']]
				});

			}
        }
    },
    {
        //MUTATION REQUIRED HEADERS
        forType: [MUTATION_BY_POS],
        getErrors: (state, dataType, line) => {
            const missing = hasSparseDataColumns(line, getMutationHeaderRegExps);
			if (missing.length) {
				state.errors.push(`For mutation data we require 6 columns: 'sample', 'chrom', 'start', 'end', 'reference' and 'alternate'. You are missing ${missing.join(', ')}. Please edit your file and reload.`);
				state.snippets.push({
					exampleLines: [
						['sample', 'chrom', 'start', 'end', 'reference', 'alternate'],
						['TCGA-PK-A5HB-01', 'chr1', '877831', '877831', 'T', 'C']]
				});
			}
        }
    }
];

export default getErrors;
<commit_msg>user data import error checking improvement:  only allow numbers and NAs in genomic data files<commit_after>'use strict';

import _ from '../underscore_ext';
import {DATA_TYPE} from './constants';

const {MUTATION_BY_POS, SEGMENTED_CN, GENOMIC} = DATA_TYPE;

const HEADER_LENGTH_LIMIT = 255,
    MULTI_ERROR_LIMIT = 3;

let ERRORS, HEADER_ERRORS;

const getColumns = line => line.replace(/[\r\n]+$/, '').split(/\t/g);

const filterEmpty = arr => arr.filter(elem => !!elem);

const getErrDataType = (dataType) => dataType === MUTATION_BY_POS || dataType === SEGMENTED_CN ? dataType : 'dense';

const getMutationHeaderRegExps = [
	{ regexp: /sample[ _]*(name|id)?/i, name: "'sample'"},
	{ regexp: /chr(om)?/i, name: "'chrom'" },
	{ regexp: /start/i, name: "'start'" },
	{ regexp: /end/i, name: "'end'" },
	{ regexp: /alt(ernate)?/i, name: "'alternate'" },
	{ regexp: /ref(erence)?/i, name: "'reference'" }
];

const getSegmentedHeaderRegExps = [
	{ regexp: /sample[ _]*(name|id)?/i, name: "'sample'"},
	{ regexp: /chr(om)?/i, name: "'chrom'" },
	{ regexp: /start/i, name: "'start'" },
	{ regexp: /end/i, name: "'end'" },
	{ regexp: /value/i, name: "'value'" }
];

const gatherErrorMessages = (state, dataType, errors, line, i) => {
    errors.forEach(error => {
        error.getErrors(state, dataType, line, i);
	});
};

// Return function that error checks a line, keeping results (imperatively)
// in 'state'.
const getErrors = (dataType, state) => {
    var filterByDataType = getErrDataType(dataType),
		dataTypeErrors = ERRORS.filter(err => err.forType.some(dType => dType === filterByDataType)),
		dataTypeHeaderErrors = HEADER_ERRORS.filter(err => err.forType.some(dType => dType === filterByDataType)),
		i = 0;
	state.errors = [];
	state.warnings = [];
	state.snippets = [];

	return line => {
		//iterate through rules and populate state
		var lineCols = getColumns(line); // should this trim()? Or trim() earlier?
		if (i === 0) {
			state.rows = new Set();
			state.headerLen = lineCols.length;
			gatherErrorMessages(state, dataType, dataTypeHeaderErrors, lineCols);
		} else {
			gatherErrorMessages(state, dataType, dataTypeErrors, lineCols, i);
		}

		i += 1;
	};
};

const hasColumn = (header, columnName, regexp) => {
    if (!header.some(h => h.match(regexp))) {
        return columnName;
    }
};

const hasSparseDataColumns = (header, colRegExps) =>
    filterEmpty(colRegExps.map(r => hasColumn(header, r.name, r.regexp)));

ERRORS = [
    {
        //HEADER COLUMN COUNT MISMATCH
        forType: [MUTATION_BY_POS, SEGMENTED_CN, 'dense'], // XXX for all types?
        getErrors: (state, dataType, line, i) => {
			var c = state.columnMismatch || 0;
			if (line.length !== state.headerLen && c < MULTI_ERROR_LIMIT) {
				state.errors.push(`The number of headers doesn't match the number of columns on line ${i + 1}. \
					Please edit the file and reload.`);
				state.columnMismatch = c + 1;
			}
        }
    },
    {
        //DUPLICATE ROW KEY
		forType: ['dense'],
		getErrors: (state, dataType, line/*, i*/) => {
			if (!state.duplicateRows && state.rows.has(line[0])) {
				if (dataType === GENOMIC) {
					state.warnings.push(`There are duplicate probe names in your file. An example is ${line[0]}. \
						We will load the first one and rename the duplicates.`);
				} else {
					state.warnings.push(`There are duplicate sample names in your file. An example is ${line[0]}. \
						We will load the first one and ignore all others.`);
				}
				state.duplicateRows = true;
			} else {
				state.rows.add(_.copyStr(line[0]));
			}
		}
    },
    {
		//GENOMIC MATRIX SHOULD ONLY HAS NUMBERS and NAs, NO OTHER VALUES SHOULD BE ALLOWED INCLUDING NULL
		forType: ['dense'],
			getErrors: (state, dataType, line, i) => {
			var badNumbers = line.filter((x, i) => i !== 0 && isNaN(x) && x !== 'NA');
			if (badNumbers.length) {
				state.errors.push(`Only NUMBER (e.g. 1, 3.2) or 'NA' is allowed in the genomic data. \
					We detected other values, such as '${badNumbers[0]}', on line ${i + 1}. \
					Please edit the file and reload.`);
			}
        }
    }
];

HEADER_ERRORS = [
    {
        //HEADER LENGTHS
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            const message = (header) => `${header.slice(0, 100)}... is too long. Please limit to 255 characters`;

            var c = 0;
            for(let i = 0; i < line.length; i++) {
                const name = line[i];

                if (name.length > HEADER_LENGTH_LIMIT) {
                    state.errors.push(message(name));
                    c += 1;

                    if (c >= MULTI_ERROR_LIMIT) {
                        break;
                    }
                }
            }
        }
    },
    { // XXX discard, or maybe check for quotes?
        //HEADER CHARACTERS
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (/*state, line*/) => {
            // const message = (header) => `Headers can only have xyz. Please change ${header}`;
        }
    },
    {
        //HEADER EMPTY
        forType: ['dense', MUTATION_BY_POS, SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            if (line.some(h => h.trim() === '')) {
				state.errors.push('One or more of your headers is blank. Please edit the file and reload.');
            }
        }
    },
    {
        //DUPLICATE HEADER KEYS
        forType: ['dense'],
        getErrors: (state, dataType, line) => {
			var headers = new Set(),
				dup = _.find(line, s => {
					if (headers.has(s)) {
						return true;
					}
					headers.add(s); // XXX note side-effecting predicate
				});
			if (dup) {
				if (dataType === GENOMIC) {
					state.warnings.push(`There are duplicate samples in your file. An example is ${dup}. We will load the first one and ignore all others.`);
				} else {
					state.warnings.push(`There are duplicate phenotypes in your file. An example is ${dup}. We will load the first one and rename the duplicates.`);
				}
			}
        }
    },
    {
        //SEGMENTED REQUIRED HEADERS
        forType: [SEGMENTED_CN],
        getErrors: (state, dataType, line) => {
            const missing = hasSparseDataColumns(line, getSegmentedHeaderRegExps);
			if (missing.length) {
				state.errors.push(`For segmented copy number data we require 5 columns: 'sample', 'chrom', 'start', 'end', and 'value'. You are missing ${missing.join(', ')}. Please edit your file and reload.`);
				state.snippets.push({
					exampleLines: [
						['sample', 'chrom', 'start', 'end', 'value'],
						['TCGA-OR-A5LT-01', 'chr1', '61735', '98588', '0.1842']]
				});

			}
        }
    },
    {
        //MUTATION REQUIRED HEADERS
        forType: [MUTATION_BY_POS],
        getErrors: (state, dataType, line) => {
            const missing = hasSparseDataColumns(line, getMutationHeaderRegExps);
			if (missing.length) {
				state.errors.push(`For mutation data we require 6 columns: 'sample', 'chrom', 'start', 'end', 'reference' and 'alternate'. You are missing ${missing.join(', ')}. Please edit your file and reload.`);
				state.snippets.push({
					exampleLines: [
						['sample', 'chrom', 'start', 'end', 'reference', 'alternate'],
						['TCGA-PK-A5HB-01', 'chr1', '877831', '877831', 'T', 'C']]
				});
			}
        }
    }
];

export default getErrors;
<|endoftext|>


<commit_before>"use strict";

/*global mw, ve, OO, jQuery*/

(function() {
    var title = "visualeditor-mwprocessmodel-title",
	dialogueName = "Process Model Dialogue",
	processModelCollection = "process-models",

	makeSearchRequest = function(value, callback) {
	    jQuery.getJSON(
		"/channel/search/process-models",
		{
		    q: value
		},
		callback
	    );
	};

    /*
     Make the dialogue.
     */
    var dialogue = function(surface, config) {
	OO.ui.Dialog.call(this, surface, config);
    };
    OO.inheritClass(dialogue, OO.ui.Dialog);
    dialogue.static.name = dialogueName;
    dialogue.static.titleMessage = title;

    dialogue.prototype.getBodyHeight = function () {
	return 300;
    };

    dialogue.prototype.initialize = function() {
	OO.ui.Dialog.prototype.initialize.call(this);

	var search = new OO.ui.SearchWidget(),
	    doSearch = function(value, callback) {
		makeSearchRequest(value, function(results) {
		    if (value === search.query.value) {
			callback(results);

		    } else {
			// Noop, our search is out of date.
		    }
		});
	    };

	search.query.on("change", function() {
	    doSearch(search.query.value, function(results) {
		search.results.clearItems();
		search.results.addItems(
		    results.map(function(r) {

			return new OO.ui.OptionWidget(
			    r,
			    {
				// Ensure we have a String for the label.
				label: "" + r
			    }
			);
		    })
		);
	    });
	});

	search.on("select", function(data) {
	    alert("Selected " + data);
	});

	this.$body.append(search.$query);
	this.$body.append(search.$results);
    };

    ve.ui.windowFactory.register(dialogue);

    /*
     Make the tool.
     */
    var tool = function(toolGroup, config) {
	ve.ui.Tool.call(this, toolGroup, config);
    };

    OO.inheritClass(tool, ve.ui.Tool);
    tool.static.name = "ProcessModelTool";
    tool.static.title = mw.message(title).text();
    tool.static.dialog = dialogueName;
    tool.prototype.onSelect = function () {
	this.toolbar.getSurface().execute('window', 'open', dialogueName, null);
    };

    ve.ui.toolFactory.register(tool);

}());
<commit_msg>Added a comment.<commit_after>"use strict";

/*global mw, ve, OO, jQuery*/

/*
 Add a 'Process Model' tool to the insert tool group inside the Visual Editor toolbar.

 This will produce a dialogue box, which allows the user to search for an existing Process Model.
 */
(function() {
    var title = "visualeditor-mwprocessmodel-title",
	dialogueName = "Process Model Dialogue",
	processModelCollection = "process-models",

	makeSearchRequest = function(value, callback) {
	    jQuery.getJSON(
		"/channel/search/process-models",
		{
		    q: value
		},
		callback
	    );
	};

    /*
     Make the dialogue.
     */
    var dialogue = function(surface, config) {
	OO.ui.Dialog.call(this, surface, config);
    };
    OO.inheritClass(dialogue, OO.ui.Dialog);
    dialogue.static.name = dialogueName;
    dialogue.static.titleMessage = title;

    dialogue.prototype.getBodyHeight = function () {
	return 300;
    };

    dialogue.prototype.initialize = function() {
	OO.ui.Dialog.prototype.initialize.call(this);

	var search = new OO.ui.SearchWidget(),
	    doSearch = function(value, callback) {
		makeSearchRequest(value, function(results) {
		    if (value === search.query.value) {
			callback(results);

		    } else {
			// Noop, our search is out of date.
		    }
		});
	    };

	search.query.on("change", function() {
	    doSearch(search.query.value, function(results) {
		search.results.clearItems();
		search.results.addItems(
		    results.map(function(r) {

			return new OO.ui.OptionWidget(
			    r,
			    {
				// Ensure we have a String for the label.
				label: "" + r
			    }
			);
		    })
		);
	    });
	});

	search.on("select", function(data) {
	    alert("Selected " + data);
	});

	this.$body.append(search.$query);
	this.$body.append(search.$results);
    };

    ve.ui.windowFactory.register(dialogue);

    /*
     Make the tool.
     */
    var tool = function(toolGroup, config) {
	ve.ui.Tool.call(this, toolGroup, config);
    };

    OO.inheritClass(tool, ve.ui.Tool);
    tool.static.name = "ProcessModelTool";
    tool.static.title = mw.message(title).text();
    tool.static.dialog = dialogueName;
    tool.prototype.onSelect = function () {
	this.toolbar.getSurface().execute('window', 'open', dialogueName, null);
    };

    ve.ui.toolFactory.register(tool);

}());
<|endoftext|>


<commit_before>//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: packaged loading message functionality
//>>label: loading message
//>>group: Navigation

define( [	"jquery",	"./jquery.mobile.core",	"./jquery.mobile.init" ], function( $ ) {
//>>excludeEnd("jqmBuildExclude");

(function( $, window ) {
	// DEPRECATED
	// NOTE global mobile object settings
	$.extend($.mobile, {
		// DEPRECATED Should the text be visble in the loading message?
		loadingMessageTextVisible: undefined,

		// DEPRECATED When the text is visible, what theme does the loading box use?
		loadingMessageTheme: undefined,

		// DEPRECATED default message setting
		loadingMessage: undefined,

		// DEPRECATED
		// Turn on/off page loading message. Theme doubles as an object argument
		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
		showPageLoadingMsg: function( theme, msgText, textonly ) {
			$.mobile.loading( 'show', theme, msgText, textonly );
		},

		// DEPRECATED
		hidePageLoadingMsg: function() {
			$.mobile.loading( 'hide' );
		},

		loading: function() {
			this.loaderWidget.loader.apply(this.loaderWidget, arguments);
		}
	});

	// TODO move loader class down into the widget settings
	var loaderClass = "ui-loader", $html = $( "html" ), $window = $( window );

	$.widget( "mobile.loader", {
		// NOTE if the global config settings are defined they will override these
		//      options
		options: {
			// the theme for the loading message
			theme: "a",

			// whether the text in the loading message is shown
			textVisible: false,

			// custom html for the inner content of the loading message
			html: "",

			// the text to be displayed when the popup is shown
			text: "loading"
		},

		defaultHtml: "
" +
			"" +
			"

" +
			"
",

		// For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top
		fakeFixLoader: function(){
			var activeBtn = $( "." + $.mobile.activeBtnClass ).first();

			this.element
				.css({
					top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 ||
						activeBtn.length && activeBtn.offset().top || 100
				});
		},

		// check position of loader to see if it appears to be "fixed" to center
		// if not, use abs positioning
		checkLoaderPosition: function(){
			var offset = this.element.offset(),
				scrollTop = $window.scrollTop(),
				screenHeight = $.mobile.getScreenHeight();

			if( offset.top < scrollTop || (offset.top - scrollTop) > screenHeight ) {
				this.element.addClass( "ui-loader-fakefix" );
				this.fakeFixLoader();
				$window
					.unbind( "scroll", this.checkLoaderPosition )
					.bind( "scroll", this.fakeFixLoader );
			}
		},

		resetHtml: function() {
			this.element.html( $(this.defaultHtml).html() );
		},

		// Turn on/off page loading message. Theme doubles as an object argument
		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
		// TODO sweet jesus we need to break some of this out
		show: function( theme, msgText, textonly ) {
			this.resetHtml();

			var loadSettings;

			// support for object literal params
			if( $.type(theme) === "object" ){
				loadSettings = $.extend({}, this.options, theme);

				// prefer object property from the param then the old theme setting
				theme = loadSettings.theme || $.mobile.loadingMessageTheme;
			} else {
				loadSettings = this.options;
				theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme;
			}

			$html.addClass( "ui-loading" );

			if ( $.mobile.loadingMessage !== false || loadSettings.html ) {
				// text visibility from argument takes priority
				var textVisible, message, $header;

				// boolean values require a bit more work :P
				// support object properties and old settings
				if( $.mobile.loadingMessageTextVisible !== undefined ) {
					textVisible = $.mobile.loadingMessageTextVisible;
				} else {
					textVisible = loadSettings.textVisible;
				}

				this.element.attr("class", loaderClass +
													" ui-corner-all ui-body-" + theme +
													" ui-loader-" + ( textVisible ? "verbose" : "default" ) +
													( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) );

				// TODO verify that jquery.fn.html is ok to use in both cases here
				//      this might be overly defensive in preventing unknowing xss
				// if the html attribute is defined on the loading settings, use that
				// otherwise use the fallbacks from above
				if( loadSettings.html ) {
					this.element.html( loadSettings.html );
				} else {
					// prefer the param, then the settings object then loading message
					message = msgText || $.mobile.loadingMessage || loadSettings.text;
					this.element.find( "h1" )
						.text( message );
				}

				this.element.appendTo( $.mobile.pageContainer );

				this.checkLoaderPosition();
				$window.bind( "scroll", $.proxy(this.checkLoaderPosition, this));
			}
		},

		hide: function() {
			$html.removeClass( "ui-loading" );

			if( $.mobile.loadingMessage ){
				this.element.removeClass( "ui-loader-fakefix" );
			}

			$( window ).unbind( "scroll", $.proxy(this.fakeFixLoader, this) );
			$( window ).unbind( "scroll", $.proxy(this.checkLoaderPosition, this) );
		}
	});

	$window.bind( 'pagecontainercreate', function() {
		$.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader();
	});
})(jQuery, this);

//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");
<commit_msg>Removed dependency on init which was causing a circular dependency.<commit_after>//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: packaged loading message functionality
//>>label: loading message
//>>group: Navigation

define( [ "jquery",	"./jquery.mobile.core" ], function( $ ) {
//>>excludeEnd("jqmBuildExclude");

(function( $, window ) {
	// DEPRECATED
	// NOTE global mobile object settings
	$.extend($.mobile, {
		// DEPRECATED Should the text be visble in the loading message?
		loadingMessageTextVisible: undefined,

		// DEPRECATED When the text is visible, what theme does the loading box use?
		loadingMessageTheme: undefined,

		// DEPRECATED default message setting
		loadingMessage: undefined,

		// DEPRECATED
		// Turn on/off page loading message. Theme doubles as an object argument
		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
		showPageLoadingMsg: function( theme, msgText, textonly ) {
			$.mobile.loading( 'show', theme, msgText, textonly );
		},

		// DEPRECATED
		hidePageLoadingMsg: function() {
			$.mobile.loading( 'hide' );
		},

		loading: function() {
			this.loaderWidget.loader.apply(this.loaderWidget, arguments);
		}
	});

	// TODO move loader class down into the widget settings
	var loaderClass = "ui-loader", $html = $( "html" ), $window = $( window );

	$.widget( "mobile.loader", {
		// NOTE if the global config settings are defined they will override these
		//      options
		options: {
			// the theme for the loading message
			theme: "a",

			// whether the text in the loading message is shown
			textVisible: false,

			// custom html for the inner content of the loading message
			html: "",

			// the text to be displayed when the popup is shown
			text: "loading"
		},

		defaultHtml: "
" +
			"" +
			"

" +
			"
",

		// For non-fixed supportin browsers. Position at y center (if scrollTop supported), above the activeBtn (if defined), or just 100px from top
		fakeFixLoader: function(){
			var activeBtn = $( "." + $.mobile.activeBtnClass ).first();

			this.element
				.css({
					top: $.support.scrollTop && $window.scrollTop() + $window.height() / 2 ||
						activeBtn.length && activeBtn.offset().top || 100
				});
		},

		// check position of loader to see if it appears to be "fixed" to center
		// if not, use abs positioning
		checkLoaderPosition: function(){
			var offset = this.element.offset(),
				scrollTop = $window.scrollTop(),
				screenHeight = $.mobile.getScreenHeight();

			if( offset.top < scrollTop || (offset.top - scrollTop) > screenHeight ) {
				this.element.addClass( "ui-loader-fakefix" );
				this.fakeFixLoader();
				$window
					.unbind( "scroll", this.checkLoaderPosition )
					.bind( "scroll", this.fakeFixLoader );
			}
		},

		resetHtml: function() {
			this.element.html( $(this.defaultHtml).html() );
		},

		// Turn on/off page loading message. Theme doubles as an object argument
		// with the following shape: { theme: '', text: '', html: '', textVisible: '' }
		// NOTE that the $.mobile.loading* settings and params past the first are deprecated
		// TODO sweet jesus we need to break some of this out
		show: function( theme, msgText, textonly ) {
			this.resetHtml();

			var loadSettings;

			// support for object literal params
			if( $.type(theme) === "object" ){
				loadSettings = $.extend({}, this.options, theme);

				// prefer object property from the param then the old theme setting
				theme = loadSettings.theme || $.mobile.loadingMessageTheme;
			} else {
				loadSettings = this.options;
				theme = theme || $.mobile.loadingMessageTheme || loadSettings.theme;
			}

			$html.addClass( "ui-loading" );

			if ( $.mobile.loadingMessage !== false || loadSettings.html ) {
				// text visibility from argument takes priority
				var textVisible, message, $header;

				// boolean values require a bit more work :P
				// support object properties and old settings
				if( $.mobile.loadingMessageTextVisible !== undefined ) {
					textVisible = $.mobile.loadingMessageTextVisible;
				} else {
					textVisible = loadSettings.textVisible;
				}

				this.element.attr("class", loaderClass +
													" ui-corner-all ui-body-" + theme +
													" ui-loader-" + ( textVisible ? "verbose" : "default" ) +
													( loadSettings.textonly || textonly ? " ui-loader-textonly" : "" ) );

				// TODO verify that jquery.fn.html is ok to use in both cases here
				//      this might be overly defensive in preventing unknowing xss
				// if the html attribute is defined on the loading settings, use that
				// otherwise use the fallbacks from above
				if( loadSettings.html ) {
					this.element.html( loadSettings.html );
				} else {
					// prefer the param, then the settings object then loading message
					message = msgText || $.mobile.loadingMessage || loadSettings.text;
					this.element.find( "h1" )
						.text( message );
				}

				this.element.appendTo( $.mobile.pageContainer );

				this.checkLoaderPosition();
				$window.bind( "scroll", $.proxy(this.checkLoaderPosition, this));
			}
		},

		hide: function() {
			$html.removeClass( "ui-loading" );

			if( $.mobile.loadingMessage ){
				this.element.removeClass( "ui-loader-fakefix" );
			}

			$( window ).unbind( "scroll", $.proxy(this.fakeFixLoader, this) );
			$( window ).unbind( "scroll", $.proxy(this.checkLoaderPosition, this) );
		}
	});

	$window.bind( 'pagecontainercreate', function() {
		$.mobile.loaderWidget = $.mobile.loaderWidget || $( $.mobile.loader.prototype.defaultHtml ).loader();
	});
})(jQuery, this);

//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");
<|endoftext|>


<commit_before>const BASE_URL = "https://meet.jit.si/";
const APP_NAME = "Jitsi";

//A text to be used when adding info to the location field.
const LOCATION_TEXT = APP_NAME + ' Conference';

let generateRoomNameAsDigits = false;

/**
 * The event page we will be updating.
 */
class EventContainer {
    constructor() {
        // Numbers used to access the service, will be listed in the
        // autogenerated description of the event when adding a meeting to it.
        this.numbers = [];
    }

    /**
     * @returns {EventContainer}
     */
    static getInstance() {
        var eventEditPage = document.querySelector('#maincell #coverinner');
        if (eventEditPage)
            return new GEvent(eventEditPage);
        else
            return new MSLiveEvent();
    }

    /**
     * The description of the event.
     * @abstract
     * @returns {Description}
     */
    get description() {}

    /**
     * The button container where we will add the jitsi button.
     * @abstract
     */
    get buttonContainer() {}

    /**
     * The location of the event.
     * @abstract
     * @returns {Location}
     */
    get location() {}

    /**
     * The container element of the event edit page.
     * @returns {*}
     */
    get container(){
        return this.containerElement;
    };

    set container(c){
        this.containerElement = c;
    };

    /**
     * Main entry point of the event modifictaions.
     * @abstract
     */
    update() {}

    /**
     * Checks for the button on current page
     */
    isButtonPresent() {
        return ($('#jitsi_button').length >= 1);
    }

    /**
     * Checks if there is meetingId, if not generate it, otherwise return it.
     * @returns {string} the meeting id to use.
     */
    getMeetingId() {
        let resMeetingId = "";

        var inviteText;
        if (this.location)
            inviteText = this.location.text;
        else
            inviteText = this.description.value;

        var ix = inviteText.indexOf(BASE_URL);
        var url;
        if (ix != -1 && (url = inviteText.substring(ix)) && url.length > 0) {
            resMeetingId = url.substring(BASE_URL.length);

            // there can be ',' after the meeting, normally added when adding
            // physical rooms to the meeting
            var regexp = /([a-zA-Z]+).*/g;
            var match = regexp.exec(resMeetingId);
            if (match && match.length > 1)
                resMeetingId = match[1];
        }
        else {
            if (generateRoomNameAsDigits) {
                resMeetingId = randomDigitString(10);
            }
            else
                resMeetingId = generateRoomWithoutSeparator();
        }

        return resMeetingId;
    }

    /**
     * Adds the jitsi button in buttonContainer.
     */
    addJitsiButton() {
        var container = this.buttonContainer;
        if (!container)
            return;

        var description = this.description;

        if (!description.element)
            return;

        container.addClass('button_container');
        container.append(
            '

');
        // container.find('div.ui-sch').addClass('hangouts_button');
        description.update(this.location);
    }
}

/**
 * Represents the location field.
 */
class Location {
    /**
     * The text in the location field.
     * @abstract
     */
    get text() {}

    /**
     * Adds location info.
     * @abstract
     * @param text
     */
    addLocationText(text){}
}

/**
 * Represents the description of the event.
 */
class Description {
    constructor(event) {
        this.event = event;
    }
    /**
     * Updates the description and location field is not already updated.
     * @param location
     */
    update(location) {
        var isDescriptionUpdated = false;

        // checks whether description was updated.
        if (this.element != undefined) {
            var descriptionContainsURL =
                (this.value.length >= 1 && this.value.indexOf(BASE_URL) !== -1);
            isDescriptionUpdated =
                descriptionContainsURL ||
                // checks whether there is the generated name in the location input
                // if there is a location
                (location != null
                    && location.text.indexOf(LOCATION_TEXT) != -1);
        }

        if(isDescriptionUpdated) {
            // update button url of event has all the data
            this.updateButtonURL();
        } else {
            // update button as event description has no meeting set
            var button = $('#jitsi_button a');
            button.html('Add a ' + APP_NAME + ' Meeting');
            button.attr('href', '#');
            button.on('click', e => {
                e.preventDefault();

                if (!isDescriptionUpdated) {
                    // Build the invitation content
                    this.addDescriptionText(this.getInviteText());
                    this.updateButtonURL();

                    if (location)
                        location.addLocationText(
                            LOCATION_TEXT + ' - '
                                + $('#jitsi_button a').attr('href'));
                }
                this.updateButtonURL();
            });
        }
    }

    /**
     * The description html element.
     * @abstract
     */
    get element() {}

    /**
     * The text value of the description of the event.
     * @abstract
     */
    get value() {}

    /**
     * Adds description text to the existing text.
     * @abstract
     * @param text
     */
    addDescriptionText(text){}

    /**
     * Generates description text used for the invite.
     * @returns {String}
     */
    getInviteText() {
        var inviteText =
            "Click the following link to join the meeting from your computer: "
            + BASE_URL + this.event.meetingId;

        if (this.event.numbers.length > 0) {
            inviteText += "\n\n=====";
            inviteText +="\n\nJust want to dial in on your phone? ";
            inviteText += " \n\nCall one of the following numbers: ";
            this.event.numbers.forEach(function (num) {
                inviteText += "\n" + num;
            });
            inviteText += "\n\nSay your conference name: '"
                + this.event.meetingId
                + "' and you will be connected!";
        }

        return inviteText;
    }

    /**
     * Updates the url for the button.
     */
    updateButtonURL() {
        try {
            $('#jitsi_button').addClass('join');
            var button = $('#jitsi_button a');
            button.html("Join " + this.event.meetingId + " now");
            button.off('click');
            button.attr('href', BASE_URL + this.event.meetingId);
            button.attr('target', '_new');
            button.attr('style', '{color:blue}');
        } catch (e) {
            console.log(e);
        }
    }
}

/**
 * The google calendar specific implementation of the event page.
 */
class GEvent extends EventContainer {
    constructor(eventEditPage) {
        super();

        this.container = eventEditPage;
    }

    /**
     * Updates content (adds the button if is not there).
     * This is the entry point for all page modifications.
     */
    update() {
        if ($('table.ep-dp-dt').is(":visible")) {
            this.meetingId = this.getMeetingId();

            if(!this.isButtonPresent())
                this.addJitsiButton();
        }
    }

    /**
     * The event location.
     * @returns {GLocation}
     */
    get location() {
        if (!this.locationInstance)
            this.locationInstance = new GLocation();
        return this.locationInstance;
    }

    /**
     * The button container holding jitsi button.
     * @returns {*}
     */
    get buttonContainer() {
        var container = $(getNodeID('rtc'));
        if(container.length == 0)
            return null;
        return container;
    }

    /**
     * The event description.
     * @returns {GDescription}
     */
    get description() {
        if (!this.descriptionInstance)
            this.descriptionInstance = new GDescription(this);
        return this.descriptionInstance;
    }

    /**
     * Adds the jitsi button.
     */
    addJitsiButton() {
        super.addJitsiButton();

        var rtcRow = $(getNodeID('rtc-row'));
        if(rtcRow.is(':visible') == false && description.length != 0) {
            rtcRow.show();
            this.buttonContainer.addClass('solo');
        }
    }
}

/**
 * The google calendar specific implementation of the location field in the
 * event page.
 */
class GLocation extends Location {
    constructor() {
        super();
        this.elem = $('[id*=location].ep-dp-input input');
    }

    /**
     * The text from the location input field.
     * @returns {*}
     */
    get text() {
        return this.elem.val();
    }

    /**
     * Adds text to location input.
     * @param text
     */
    addLocationText(text){
        // Set the location if there is content
        var locationNode = this.elem[0];
        if (locationNode) {
            locationNode.dispatchEvent(getKeyboardEvent('keydown'));
            locationNode.value = locationNode.value == '' ?
                text : locationNode.value + ', ' + text;
            locationNode.dispatchEvent(getKeyboardEvent('input'));
            locationNode.dispatchEvent(getKeyboardEvent('keyup'));
            var changeEvt2 = document.createEvent("HTMLEvents");
            changeEvt2.initEvent('change', false, true);
            locationNode.dispatchEvent(changeEvt2);
        }
    }
}

/**
 * The google calendar specific implementation of the description textarea in
 * the event page.
 */
class GDescription extends Description {
    constructor(event) {
        super(event);

        var description = $(getNodeID('descript textarea'))[0];
        var descriptionRow = $(getNodeID('descript-row'));

        if (descriptionRow.find('textarea').length === 0)
            return;

        this.element = description;
    }

    /**
     * The html element.
     * @returns {*}
     */
    get element() {
        return this.el;
    }

    set element(el) {
        this.el = el;
    }

    /**
     * The text value of the description.
     */
    get value() {
        return this.el.value;
    }

    /**
     * Adds text to the description.
     * @param text
     */
    addDescriptionText(text){
        this.el.dispatchEvent(getKeyboardEvent('keydown'));
        this.el.value = this.el.value + text;
        this.el.dispatchEvent(getKeyboardEvent('input'));
        this.el.dispatchEvent(getKeyboardEvent('keyup'));
        var changeEvt1 = document.createEvent("HTMLEvents");
        changeEvt1.initEvent('change', false, true);
        this.el.dispatchEvent(changeEvt1);
    }
}

/**
 * The outlook live calendar specific implementation of the event page.
 */
class MSLiveEvent extends EventContainer {
    constructor() {
        super();

        this.container = document.getElementsByTagName("BODY")[0];
    }

    /**
     * Updates content (adds the button if is not there).
     * This is the entry point for all page modifications.
     */
    update() {
        if ($("div[aria-label='Event compose form']").is(":visible")) {
            this.meetingId = this.getMeetingId();

            if(!this.isButtonPresent())
                this.addJitsiButton();
        }
    }

    /**
     * The event location. Currently not supported.
     * @returns {MSLiveLocation}
     */
    get location() {
        return null;
    }

    /**
     * The button container holding jitsi button.
     * @returns {*}
     */
    get buttonContainer() {
        var container
            = $("span[id='MeetingCompose.LocationInputLabel']").parent();
        if(container.length == 0)
            return null;
        return container;
    }

    /**
     * The event description.
     * @returns {MSLiveDescription}
     */
    get description() {
        if (!this.descriptionInstance)
            this.descriptionInstance = new MSLiveDescription(this);
        return this.descriptionInstance;
    }
}

/**
 * The outlook live calendar specific implementation of the description textarea
 * in the event page.
 */
class MSLiveDescription extends Description {
    constructor(event) {
        super(event);

        var description = $("div[aria-label='Event body'] p:first-child");
        if (description.length == 0)
            return;

        this.element = description;
    }

    /**
     * The html element.
     * @returns {*}
     */
    get element() {
        return this.el[0];
    }

    set element(el) {
        this.el = el;
    }

    /**
     * The text value of the description.
     */
    get value() {
        return this.el.text();
    }

    /**
     * Adds text to the description.
     * @param text
     */
    addDescriptionText(text){
        this.el.text(this.value + text);
    }
}

/**
 * Returns the node id.
 */
function getNodeID(name) {
    var inputNodePrefix = '';
    var labelNode = $("[id*='location-label']");
    if (labelNode.length >= 1) {
        inputNodePrefix = labelNode[0].id.split('.')[0];
    }
    return '#\\' + inputNodePrefix + '\\.' + name;
}

/**
 * Returns an event object that can be used to be simulated
 */
function getKeyboardEvent(event) {
    var keyboardEvent = document.createEvent('KeyboardEvent');
    var initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ?
        'initKeyboardEvent' : 'initKeyEvent';
    keyboardEvent[initMethod](
        event // event type (keydown, keyup, or keypress)
        , true // bubbles
        , true // cancel-able
        , window // viewArg (window)
        , false // ctrlKeyArg
        , false // altKeyArg
        , false // shiftKeyArg
        , false // metaKeyArg
        , 32 // keyCodeArg
        , 0 // charCodeArg
    );

    return keyboardEvent;
}

/**
 * Checks whether it is ok to add the button to current page and add it.
 */
function checkAndUpdateCalendar() {
    var MutationObserver
        = window.MutationObserver || window.WebKitMutationObserver;
    var c = EventContainer.getInstance();
    if (c) {
        new MutationObserver(function(mutations) {
            try {
                mutations.every(function() {
                    c.update();
                });
            } catch(e) {
                console.log(e);
            }
        }).observe(c.container, {
            childList: true, attributes: true, characterData: false });
    }
}

checkAndUpdateCalendar();
<commit_msg>Adds an option for a script to retrieve numbers and other options.<commit_after>const BASE_URL = "https://meet.jit.si/";
const APP_NAME = "Jitsi";
const NUMBER_RETRIEVE_SCRIPT = false;

//A text to be used when adding info to the location field.
const LOCATION_TEXT = APP_NAME + ' Conference';

let generateRoomNameAsDigits = false;

/**
 * The event page we will be updating.
 */
class EventContainer {
    constructor() {
        // Numbers used to access the service, will be listed in the
        // autogenerated description of the event when adding a meeting to it.
        this.numbers = [];
    }

    /**
     * @returns {EventContainer}
     */
    static getInstance() {
        var eventEditPage = document.querySelector('#maincell #coverinner');
        if (eventEditPage)
            return new GEvent(eventEditPage);
        else
            return new MSLiveEvent();
    }

    /**
     * The description of the event.
     * @abstract
     * @returns {Description}
     */
    get description() {}

    /**
     * The button container where we will add the jitsi button.
     * @abstract
     */
    get buttonContainer() {}

    /**
     * The location of the event.
     * @abstract
     * @returns {Location}
     */
    get location() {}

    /**
     * The container element of the event edit page.
     * @returns {*}
     */
    get container(){
        return this.containerElement;
    };

    set container(c){
        this.containerElement = c;
    };

    /**
     * Main entry point of the event modifictaions.
     * @abstract
     */
    update() {}

    /**
     * Checks for the button on current page
     */
    isButtonPresent() {
        return ($('#jitsi_button').length >= 1);
    }

    /**
     * Clears instances.
     */
    reset() {
        this.descriptionInstance = null;
        this.locationInstance = null;
    }

    /**
     * Checks if there is meetingId, if not generate it, otherwise return it.
     * @returns {string} the meeting id to use.
     */
    getMeetingId() {

        if (!this.isButtonPresent()) {
            // there is no button present we will add it, so we will clean
            // the state of the EventContainer, so we can update all values.
            // this clears the states between creating/editing different events
            // we add the button
            this.reset();
        }

        let resMeetingId = "";

        var inviteText;
        if (this.location)
            inviteText = this.location.text;
        else
            inviteText = this.description.value;

        var ix = inviteText.indexOf(BASE_URL);
        var url;
        if (ix != -1 && (url = inviteText.substring(ix)) && url.length > 0) {
            resMeetingId = url.substring(BASE_URL.length);

            // there can be ',' after the meeting, normally added when adding
            // physical rooms to the meeting
            var regexp = /([a-zA-Z]+).*/g;
            var match = regexp.exec(resMeetingId);
            if (match && match.length > 1)
                resMeetingId = match[1];
        }
        else {
            if(NUMBER_RETRIEVE_SCRIPT) {
                // queries a predefined location for settings
                $.getJSON(NUMBER_RETRIEVE_SCRIPT,
                    jsonobj => {
                        this.inviteTextTemplate = jsonobj.inviteTextTemplate;

                        if(!jsonobj.numbersEnabled)
                            return;

                        this.numbers = jsonobj.numbers;
                        this.inviteNumbersTextTemplate
                            = jsonobj.inviteNumbersTextTemplate;
                    });
            }

            if (generateRoomNameAsDigits) {
                resMeetingId = randomDigitString(10);
            }
            else
                resMeetingId = generateRoomWithoutSeparator();
        }

        return resMeetingId;
    }

    /**
     * Adds the jitsi button in buttonContainer.
     */
    addJitsiButton() {
        var container = this.buttonContainer;
        if (!container)
            return;

        var description = this.description;

        if (!description.element)
            return;

        container.addClass('button_container');
        container.append(
            '

');
        // container.find('div.ui-sch').addClass('hangouts_button');
        description.update(this.location);
    }
}

/**
 * Represents the location field.
 */
class Location {
    /**
     * The text in the location field.
     * @abstract
     */
    get text() {}

    /**
     * Adds location info.
     * @abstract
     * @param text
     */
    addLocationText(text){}
}

/**
 * Represents the description of the event.
 */
class Description {
    constructor(event) {
        this.event = event;
    }
    /**
     * Updates the description and location field is not already updated.
     * @param location
     */
    update(location) {
        var isDescriptionUpdated = false;

        // checks whether description was updated.
        if (this.element != undefined) {
            var descriptionContainsURL =
                (this.value.length >= 1 && this.value.indexOf(BASE_URL) !== -1);
            isDescriptionUpdated =
                descriptionContainsURL ||
                // checks whether there is the generated name in the location input
                // if there is a location
                (location != null
                    && location.text.indexOf(LOCATION_TEXT) != -1);
        }

        if(isDescriptionUpdated) {
            // update button url of event has all the data
            this.updateButtonURL();
        } else {
            // update button as event description has no meeting set
            var button = $('#jitsi_button a');
            button.html('Add a ' + APP_NAME + ' Meeting');
            button.attr('href', '#');
            button.on('click', e => {
                e.preventDefault();

                if (!isDescriptionUpdated) {
                    // Build the invitation content
                    this.addDescriptionText(this.getInviteText());
                    this.updateButtonURL();

                    if (location)
                        location.addLocationText(
                            LOCATION_TEXT + ' - '
                                + $('#jitsi_button a').attr('href'));
                }
                this.updateButtonURL();
            });
        }
    }

    /**
     * The description html element.
     * @abstract
     */
    get element() {}

    /**
     * The text value of the description of the event.
     * @abstract
     */
    get value() {}

    /**
     * Adds description text to the existing text.
     * @abstract
     * @param text
     */
    addDescriptionText(text){}

    /**
     * Generates description text used for the invite.
     * @returns {String}
     */
    getInviteText() {
        let inviteText;
        let hasTemplate = false;

        if (this.event.inviteTextTemplate) {
            inviteText = this.event.inviteTextTemplate;
            hasTemplate = true;
        } else {
            inviteText =
                "Click the following link to join the meeting " +
                "from your computer: " + BASE_URL + this.event.meetingId;
        }

        if (this.event.numbers.length > 0) {

            if (this.event.inviteNumbersTextTemplate) {
                inviteText += this.event.inviteNumbersTextTemplate;
                hasTemplate = true;
                for (var i = 0; i < this.event.numbers.length; i++) {
                    inviteText = inviteText.replace(
                        '{' + i + '}', this.event.numbers[i]);
                }
            } else {
                inviteText += "\n\n=====";
                inviteText +="\n\nJust want to dial in on your phone? ";
                inviteText += " \n\nCall one of the following numbers: ";
                this.event.numbers.forEach(function (num) {
                    inviteText += "\n" + num;
                });
                inviteText += "\n\nSay your conference name: '"
                    + this.event.meetingId
                    + "' and you will be connected!";
            }
        }

        if (hasTemplate) {
            inviteText = inviteText.replace(/\{BASE_URL\}/g, BASE_URL);
            inviteText
                = inviteText.replace(/\{MEETING_ID\}/g, this.event.meetingId);
        }

        return inviteText;
    }

    /**
     * Updates the url for the button.
     */
    updateButtonURL() {
        try {
            $('#jitsi_button').addClass('join');
            var button = $('#jitsi_button a');
            button.html("Join " + this.event.meetingId + " now");
            button.off('click');
            button.attr('href', BASE_URL + this.event.meetingId);
            button.attr('target', '_new');
            button.attr('style', '{color:blue}');
        } catch (e) {
            console.log(e);
        }
    }
}

/**
 * The google calendar specific implementation of the event page.
 */
class GEvent extends EventContainer {
    constructor(eventEditPage) {
        super();

        this.container = eventEditPage;
    }

    /**
     * Updates content (adds the button if is not there).
     * This is the entry point for all page modifications.
     */
    update() {
        if ($('table.ep-dp-dt').is(":visible")) {
            this.meetingId = this.getMeetingId();

            if(!this.isButtonPresent())
                this.addJitsiButton();
        }
    }

    /**
     * The event location.
     * @returns {GLocation}
     */
    get location() {
        if (!this.locationInstance)
            this.locationInstance = new GLocation();
        return this.locationInstance;
    }

    /**
     * The button container holding jitsi button.
     * @returns {*}
     */
    get buttonContainer() {
        var container = $(getNodeID('rtc'));
        if(container.length == 0)
            return null;
        return container;
    }

    /**
     * The event description.
     * @returns {GDescription}
     */
    get description() {
        if (!this.descriptionInstance)
            this.descriptionInstance = new GDescription(this);
        return this.descriptionInstance;
    }

    /**
     * Adds the jitsi button.
     */
    addJitsiButton() {
        super.addJitsiButton();

        var rtcRow = $(getNodeID('rtc-row'));
        if(rtcRow.is(':visible') == false && description.length != 0) {
            rtcRow.show();
            this.buttonContainer.addClass('solo');
        }
    }
}

/**
 * The google calendar specific implementation of the location field in the
 * event page.
 */
class GLocation extends Location {
    constructor() {
        super();
        this.elem = $('[id*=location].ep-dp-input input');
    }

    /**
     * The text from the location input field.
     * @returns {*}
     */
    get text() {
        return this.elem.val();
    }

    /**
     * Adds text to location input.
     * @param text
     */
    addLocationText(text){
        // Set the location if there is content
        var locationNode = this.elem[0];
        if (locationNode) {
            locationNode.dispatchEvent(getKeyboardEvent('keydown'));
            locationNode.value = locationNode.value == '' ?
                text : locationNode.value + ', ' + text;
            locationNode.dispatchEvent(getKeyboardEvent('input'));
            locationNode.dispatchEvent(getKeyboardEvent('keyup'));
            var changeEvt2 = document.createEvent("HTMLEvents");
            changeEvt2.initEvent('change', false, true);
            locationNode.dispatchEvent(changeEvt2);
        }
    }
}

/**
 * The google calendar specific implementation of the description textarea in
 * the event page.
 */
class GDescription extends Description {
    constructor(event) {
        super(event);

        var description = $(getNodeID('descript textarea'))[0];
        var descriptionRow = $(getNodeID('descript-row'));

        if (descriptionRow.find('textarea').length === 0)
            return;

        this.element = description;
    }

    /**
     * The html element.
     * @returns {*}
     */
    get element() {
        return this.el;
    }

    set element(el) {
        this.el = el;
    }

    /**
     * The text value of the description.
     */
    get value() {
        return this.el.value;
    }

    /**
     * Adds text to the description.
     * @param text
     */
    addDescriptionText(text){
        this.el.dispatchEvent(getKeyboardEvent('keydown'));
        this.el.value = this.el.value + text;
        this.el.dispatchEvent(getKeyboardEvent('input'));
        this.el.dispatchEvent(getKeyboardEvent('keyup'));
        var changeEvt1 = document.createEvent("HTMLEvents");
        changeEvt1.initEvent('change', false, true);
        this.el.dispatchEvent(changeEvt1);
    }
}

/**
 * The outlook live calendar specific implementation of the event page.
 */
class MSLiveEvent extends EventContainer {
    constructor() {
        super();

        this.container = document.getElementsByTagName("BODY")[0];
    }

    /**
     * Updates content (adds the button if is not there).
     * This is the entry point for all page modifications.
     */
    update() {
        if ($("div[aria-label='Event compose form']").is(":visible")) {
            this.meetingId = this.getMeetingId();

            if(!this.isButtonPresent())
                this.addJitsiButton();
        }
    }

    /**
     * The event location. Currently not supported.
     * @returns {MSLiveLocation}
     */
    get location() {
        return null;
    }

    /**
     * The button container holding jitsi button.
     * @returns {*}
     */
    get buttonContainer() {
        var container
            = $("span[id='MeetingCompose.LocationInputLabel']").parent();
        if(container.length == 0)
            return null;
        return container;
    }

    /**
     * The event description.
     * @returns {MSLiveDescription}
     */
    get description() {
        if (!this.descriptionInstance)
            this.descriptionInstance = new MSLiveDescription(this);
        return this.descriptionInstance;
    }
}

/**
 * The outlook live calendar specific implementation of the description textarea
 * in the event page.
 */
class MSLiveDescription extends Description {
    constructor(event) {
        super(event);

        var description = $("div[aria-label='Event body'] p:first-child");
        if (description.length == 0)
            return;

        this.element = description;
    }

    /**
     * The html element.
     * @returns {*}
     */
    get element() {
        return this.el[0];
    }

    set element(el) {
        this.el = el;
    }

    /**
     * The text value of the description.
     */
    get value() {
        return this.el.text();
    }

    /**
     * Adds text to the description.
     * @param text
     */
    addDescriptionText(text){
        this.el.text(this.value + text);
    }
}

/**
 * Returns the node id.
 */
function getNodeID(name) {
    var inputNodePrefix = '';
    var labelNode = $("[id*='location-label']");
    if (labelNode.length >= 1) {
        inputNodePrefix = labelNode[0].id.split('.')[0];
    }
    return '#\\' + inputNodePrefix + '\\.' + name;
}

/**
 * Returns an event object that can be used to be simulated
 */
function getKeyboardEvent(event) {
    var keyboardEvent = document.createEvent('KeyboardEvent');
    var initMethod = typeof keyboardEvent.initKeyboardEvent !== 'undefined' ?
        'initKeyboardEvent' : 'initKeyEvent';
    keyboardEvent[initMethod](
        event // event type (keydown, keyup, or keypress)
        , true // bubbles
        , true // cancel-able
        , window // viewArg (window)
        , false // ctrlKeyArg
        , false // altKeyArg
        , false // shiftKeyArg
        , false // metaKeyArg
        , 32 // keyCodeArg
        , 0 // charCodeArg
    );

    return keyboardEvent;
}

/**
 * Checks whether it is ok to add the button to current page and add it.
 */
function checkAndUpdateCalendar() {
    var MutationObserver
        = window.MutationObserver || window.WebKitMutationObserver;
    var c = EventContainer.getInstance();
    if (c) {
        new MutationObserver(function(mutations) {
            try {
                mutations.every(function() {
                    c.update();
                });
            } catch(e) {
                console.log(e);
            }
        }).observe(c.container, {
            childList: true, attributes: true, characterData: false });
    }
}

checkAndUpdateCalendar();
<|endoftext|>


<commit_before>angular.module('StarcounterLib', [])
  .directive('uiDatagrid', function () {
    var directiveDefinitionObject = {
      restrict:'A',
      require:'ngModel',
      compile:function compile(tElement, tAttrs, transclude) {

        var defaultOptions = {
          rows:6,
          cols:3,
          outsideClickDeselects:false
        };

        var $container = $('

');

        return function postLink(scope, element, attrs, controller) {
          //console.log('postLink', transclude, element);

          $(element).append($container);

          var options = {};
          var columns = [];
          var colHeaders = [];
          var colToProp = [];
          var propToCol = {};
          var i = 0;
          $(element).find('datacolumn').each(function () {
            var name = $(this).attr('name');
            var title = $(this).attr('title');
            columns.push({data:name});
            colToProp[i] = name;
            propToCol[name] = i;
            colHeaders.push(title);
          });

          if (columns.length > 0) {
            options['columns'] = columns;
          }

          if (colHeaders.length > 0) {
            options['colHeaders'] = colHeaders;
          }

          options = angular.extend({}, defaultOptions, options, scope.$eval(attrs.uiDatagrid));

          $container.handsontable(options);

          $container.on('datachange.handsontable', function (event, changes, source) {
            if (source === 'loadData') {
              return;
            }
            scope.$apply(function () {
              scope.dataChange = !scope.dataChange;
            });
          });

          $container.on('selectionbyprop.handsontable', function (event, r, c, r2, c2) {
            var oldSel = scope.selectionChange;
            var newSel = [r, c, r2, c2];
            if (typeof oldSel === 'undefined' || oldSel[0] != newSel[0] || oldSel[1] != newSel[1] || oldSel[2] != newSel[2] || oldSel[3] != newSel[3]) {
              scope.$apply(function () {
                scope.selectionChange = newSel;
              });
            }
          });

          scope.$watch('dataChange', function (value) {
            //console.log($(element).attr('id'), "triggered dataChange", value);
            $container.handsontable("loadData", scope[attrs.ngModel]);
            scope.$emit('broadcastItems');
          });

          scope.$watch('selectionChange', function (value) {
            //console.log($(element).attr('id'), "triggered selectionChange", value);
            if (value) {
              $container.handsontable("selectCellByProp", value[0], value[1], value[2], value[3]);
            }
          });

          scope.$on('incomingItems', function () {
            $container.handsontable("loadData", scope[attrs.ngModel]);
          });
        }
      }
    };
    return directiveDefinitionObject;
  });<commit_msg>cleanup: remove redundant listener and clarify on loadData usage<commit_after>angular.module('StarcounterLib', [])
  .directive('uiDatagrid', function () {
    var directiveDefinitionObject = {
      restrict:'A',
      require:'ngModel',
      compile:function compile(tElement, tAttrs, transclude) {

        var defaultOptions = {
          rows:6,
          cols:3,
          outsideClickDeselects:false
        };

        var $container = $('

');

        return function postLink(scope, element, attrs, controller) {
          //console.log('postLink', transclude, element);

          $(element).append($container);

          var options = {};
          var columns = [];
          var colHeaders = [];
          var colToProp = [];
          var propToCol = {};
          var i = 0;
          $(element).find('datacolumn').each(function () {
            var name = $(this).attr('name');
            var title = $(this).attr('title');
            columns.push({data:name});
            colToProp[i] = name;
            propToCol[name] = i;
            colHeaders.push(title);
          });

          if (columns.length > 0) {
            options['columns'] = columns;
          }

          if (colHeaders.length > 0) {
            options['colHeaders'] = colHeaders;
          }

          options = angular.extend({}, defaultOptions, options, scope.$eval(attrs.uiDatagrid));

          $container.handsontable(options);

          $container.on('datachange.handsontable', function (event, changes, source) {
            if (source === 'loadData') {
              return;
            }
            scope.$apply(function () {
              scope.dataChange = !scope.dataChange;
            });
          });

          $container.on('selectionbyprop.handsontable', function (event, r, c, r2, c2) {
            var oldSel = scope.selectionChange;
            var newSel = [r, c, r2, c2];
            if (typeof oldSel === 'undefined' || oldSel[0] != newSel[0] || oldSel[1] != newSel[1] || oldSel[2] != newSel[2] || oldSel[3] != newSel[3]) {
              scope.$apply(function () {
                scope.selectionChange = newSel;
              });
            }
          });

          scope.$watch('items', function (value) {
            $container.handsontable("loadData", scope[attrs.ngModel]); //todo: after first iteration it is only used to rerender data
            scope.$emit('broadcastItems');
          });

          scope.$watch('selectionChange', function (value) {
            if (value) {
              $container.handsontable("selectCellByProp", value[0], value[1], value[2], value[3]);
            }
          });
        }
      }
    };
    return directiveDefinitionObject;
  });<|endoftext|>


<commit_before>'use strict'
/* global $ */

/* ------------------------------------ *\
   HELPERS
\* ------------------------------------ */
var block = require('../helpers/block-event')

var $menuIcon = $('.menu-icon')
if ($menuIcon.length > 0) {
  /* ------------------------------------ *\
     MENU ICON SHOWS & HIDES
  \* ------------------------------------ */
  var $window = $(window)
  var mainContentTop = $('.main-content').offset().top
  var isMenuSemiShow = false

  $window.scroll(function () {
    // main content shows
    if ($window.scrollTop() >= mainContentTop) {
      if (!isMenuSemiShow) {
        isMenuSemiShow = true
        $menuIcon.addClass('menu-icon--show menu-icon--semi-show')
        window.setTimeout(function () {
          $menuIcon.removeClass('menu-icon--show')
        }, 500)
      }
    } else { // cover shows
      if (isMenuSemiShow) {
        $menuIcon.removeClass('menu-icon--semi-show')
        isMenuSemiShow = false
      }
    }
  })

  /* ------------------------------------ *\
     MENU SHOWS & HIDES
  \* ------------------------------------ */
  var $siteMenu = $('.site-menu')
  var $siteMenuMask = $('.site-menu-mask')

  // disables scrolling when menu shows up
  $siteMenu.on('touchmove doubleTap', function (evt) {
    block(evt)
  })

  $siteMenuMask.on('touchmove', function (evt) {
    block(evt)
  })
  .on('tap click', function (evt) {
    block(evt)
    if (evt.target === evt.currentTarget) {
      $siteMenuMask.removeClass('site-menu-mask--show')
      $siteMenu.removeClass('site-menu--show')
    }
  })

  $menuIcon.on('tap click swipeRight', function (evt) {
    block(evt)
    $siteMenuMask.addClass('site-menu-mask--show')
    $siteMenu.addClass('site-menu--show')
  })
}
<commit_msg>added swipRight on cover shows menu<commit_after>'use strict'
/* global $ */

/* ------------------------------------ *\
   HELPERS
\* ------------------------------------ */
var block = require('../helpers/block-event')

var $menuIcon = $('.menu-icon')
if ($menuIcon.length > 0) {
  /* ------------------------------------ *\
     MENU ICON SHOWS & HIDES
  \* ------------------------------------ */
  var $window = $(window)
  var mainContentTop = $('.main-content').offset().top
  var isMenuSemiShow = false

  $window.scroll(function () {
    // main content shows
    if ($window.scrollTop() >= mainContentTop) {
      if (!isMenuSemiShow) {
        isMenuSemiShow = true
        $menuIcon.addClass('menu-icon--show menu-icon--semi-show')
        window.setTimeout(function () {
          $menuIcon.removeClass('menu-icon--show')
        }, 500)
      }
    } else { // cover shows
      if (isMenuSemiShow) {
        $menuIcon.removeClass('menu-icon--semi-show')
        isMenuSemiShow = false
      }
    }
  })

  /* ------------------------------------ *\
     MENU SHOWS & HIDES
  \* ------------------------------------ */
  var $siteMenu = $('.site-menu')
  var $siteMenuMask = $('.site-menu-mask')

  // disables scrolling when menu shows up
  $siteMenu.on('touchmove doubleTap', function (evt) {
    block(evt)
  })

  $siteMenuMask.on('touchmove', function (evt) {
    block(evt)
  })
  .on('tap click', function (evt) {
    block(evt)
    if (evt.target === evt.currentTarget) {
      $siteMenuMask.removeClass('site-menu-mask--show')
      $siteMenu.removeClass('site-menu--show')
    }
  })

  var showMenu = function showMenu (evt) {
    block(evt)
    $siteMenuMask.addClass('site-menu-mask--show')
    $siteMenu.addClass('site-menu--show')
  }

  $menuIcon.on('tap click swipeRight', showMenu)

  // swip right on cover
  $('.site-cover').swipeRight(showMenu)
}
<|endoftext|>