<commit_before>(function() {
	if ( navigator.platform === "iPad" ) {
		var scale = 1.2;
		document.write('<meta name="viewport" content="width=device-width; initial-scale='+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0;" />');

	} else if ( navigator.platform === "iPhone" ) {
		var scale = 1.0;
		document.write('<meta name="viewport" content="width=device-width; initial-scale='+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0;" />');

	} else if ( navigator.userAgent.indexOf("Android") != -1 ) {
		var scale = 1.2;
		document.write('<meta name="viewport" content="width=device-width; initial-scale-'+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0; target-densitydpi="device-dpi"; />');
	} else {
		return;
	}
})();

var clickMore = false;

// The last headingCard in the page is a hidden marker. If it can't be hidden
// by Javascript, it will display a "load more content" link that can be
// clicked to load additional content into the page, based on the state
// variable, which opening the page will change. If clicked, provide a
// "true" argument. Otherwise provide "false" to the function.
function addMoreContent(was_clicked) {
   clickMore = was_clicked;

   // Heading card named for the "tombstone" needs to get replaced
   var tombstone = document.getElementById('tombstone');

   // Replace tombstone with page contents
   var state = tombstone.querySelector('#state').innerHTML;

   // Add content into the page
   $.get(
      "/?" + state,
       function (data) {
          $(".container").append(data);
       }
   );

   tombstone.id = "old_tombstone";
}

// Make all loading buttons show a "Loading..." state
function loadingButton() {
   // And add a listener to set the loading text for this and others
   document.getElementById('loadButton').addEventListener('click', function() {
      document.getElementById('loadButton').style.display = "none";
      document.getElementById('inProgress').style.display = "block";
   }, false);
}

// Once page has loaded, add new event listeners. When new cards are added to
// the page, run this code to regenerate tombstones and scrollstones that
// manage the loading of new content
$(function() {
   if ( document.getElementById('loadButton') != null ) {
      loadingButton();
   }

   // Detect when we loaded more content, and scroll to it if it was done via
   // a button click at the bottom of the page. Need a timeout value so that
   // we scrollIntoView() only when all divs are finally rendered
   document.getElementById('container').addEventListener( 'DOMNodeInserted', function () {
      // If the bottom-of-page button was actually clicked, do a page-shift
      // so that it's clear the new content is finished loading.
      setTimeout(function () {
         var old_tombstone = document.getElementById('old_tombstone');
         if( clickMore == true ) {
            clickMore = false;
            // Go to an element next to the previous tombstone
            last_element = old_tombstone.previousElementSibling;
            // If last element was expanded, shrink it
            shortener = last_element.querySelector('.showShort');
            if ( shortener ) {
               if ( shortener.style.display == "none" ) {
                  cardToggle(last_element.id);
               }
            }
            last_element.scrollIntoView();
         }
         // Any old tombstones should be removed
         $("#old_tombstone").remove();
         $("#old_scrollstone").remove();
         // Any new divs should get topic links that are clickable
         activateTopicLinks();
         loadingButton();
      }, 200);
   }, false );

   // If we scrolled past the scrollstone itself, start adding new content
   window.addEventListener( 'scroll', function() {
      var scrollstone = document.getElementById('scrollstone');
      // If scrolled past the scrollstone, start loading data
      if ( document.body.scrollTop >= scrollstone.offsetTop ) {
         scrollstone.id = "old_scrollstone";
         addMoreContent(false);

         document.getElementById('loadButton').style.display = "none";
         document.getElementById('inProgress').style.display = "block";
      }
   });

   // Make new topic links clickable and populate the search bar
   activateTopicLinks();

   // Process input into the search form
   $('#searchForm').submit(function() {
      $('#searchEntry').blur();   // Make iOS keyboard disappear after submitting
      var query = $('#searchEntry').val().trim();
      // Remove or escape any search processing characters here like commas
      $(".container").empty();

      // Create a now-searching div
      var loadingText = ''
      if ( query == '' ) {
         loadingText = document.createTextNode('Reshuffling...');
      } else {
         loadingText = document.createTextNode('Searching...');
      }
      var loading = document.createElement('div');
      loading.className = "card heading";
      loading.id = "search_waiting";
      var loadingContents = document.createElement('p');
      loadingContents.id = "loadButton";
      loadingContents.appendChild(loadingText);
      loading.appendChild(loadingContents);
      $(".container").append(loading);

      // Allow searches using special characters like #
      query = escape(query);

      // Load more data
      $("#searchForm").action = "/?xs" + query;
      $.get(
         "/?xs" + query,
          function (data) {
             // hack hack hack (length check)
             if ( data.length <= 200 ) {
                var failedText = document.createTextNode('No results.');
                loadingContents.removeChild(loadingText);
                loadingContents.appendChild(failedText);
             } else {
                $(".container").empty();
                $(".container").append(data);
             }
          }
      );
   });
});

function activateTopicLinks() {
   var linkList = document.getElementsByClassName('topicLink');
   for (var i = 0; i < linkList.length; i++ ) {
      linkList[i].addEventListener('click', function() {
         $('#searchEntry').val(this.text);
         $('#searchForm').submit();
      }, false);
   }
}

function revealToggle(id) {
   var card = document.getElementById(id);
   var largeImgs = card.querySelectorAll('.imgExpand')
   var expandLink = card.querySelector('.showShort')
   var expandDiv = card.querySelector('.divExpand')
   var todo = expandLink.style.display;

   if ( todo !== "none" ) {
      expandLink.style.display = "none";
      expandDiv.style.display = "block";
      for ( var i = 0; i < largeImgs.length; i++ ) {
         largeImgs[i].style.display = "block";
      }
   }
   else {
      expandLink.style.display = "inline";
      expandDiv.style.display = "none";
      for ( var i = 0; i < largeImgs.length; i++ ) {
         largeImgs[i].style.display = "none";
      }
   }
}

function createPost(id, mode) {
   var card = document.getElementById(id);
   var newPostCard = document.createElement('div');
   newPostCard.className = 'newPostCard';
   var reply = document.createElement('textarea');
   reply.className = "newThread";
   reply.required = true;

   // If reply already clicked, get rid of previous text boxes
   var nextElement = card.nextElementSibling;
   if ( nextElement.classList.contains("newPostCard") ) {
      card.nextSibling.remove();
   }
   if ( nextElement.classList.contains(mode) ) {
      return;   // Undraw, and call it good
   }

   reply.name = mode;
   newPostCard.classList.add(mode);
   if ( mode === "quote" ) {
      // Get body of the earlier message, and format it without nested
      // quotes. Only basic paragraphs (avoid pre/code/friends) TODO
      quotetext = card.childNodes[3].getElementsByClassName('postBody');
      // TODO: munge all paragraphs, and put it in a [QUOTE] tag.
      reply.defaultValue = quotetext[0].textContent;

   } else {
      reply.placeholder = "Add Your Reply!";
   }

   // Append edit menu afterwards
   var replyFooter = document.createElement('div');
   // TODO: not newthread class!
   replyFooter.className = "card rolldown newthread";
   var attachmentButton = document.createElement('input');
   attachmentButton.className = "threadFileInput";
   attachmentButton.type = "file";
   attachmentButton.name = "attachment";
   var submitButton = document.createElement('input');
   submitButton.className = "threadSubmit";
   submitButton.type = "submit";
   submitButton.name = "submit";
   replyFooter.appendChild(attachmentButton);
   replyFooter.appendChild(submitButton);

   newPostCard.appendChild(reply);
   newPostCard.appendChild(replyFooter);

   card.insertAdjacentElement('afterEnd', newPostCard);
}
<commit_msg>fixed textarea sizing for generated reply textareas<commit_after>(function() {
	if ( navigator.platform === "iPad" ) {
		var scale = 1.2;
		document.write('<meta name="viewport" content="width=device-width; initial-scale='+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0;" />');

	} else if ( navigator.platform === "iPhone" ) {
		var scale = 1.0;
		document.write('<meta name="viewport" content="width=device-width; initial-scale='+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0;" />');

	} else if ( navigator.userAgent.indexOf("Android") != -1 ) {
		var scale = 1.2;
		document.write('<meta name="viewport" content="width=device-width; initial-scale-'+scale+'; minimum-scale='+scale+'; maximum-scale='+scale+'; user-scalable=0; target-densitydpi="device-dpi"; />');
	} else {
		return;
	}
})();

var clickMore = false;

// The last headingCard in the page is a hidden marker. If it can't be hidden
// by Javascript, it will display a "load more content" link that can be
// clicked to load additional content into the page, based on the state
// variable, which opening the page will change. If clicked, provide a
// "true" argument. Otherwise provide "false" to the function.
function addMoreContent(was_clicked) {
   clickMore = was_clicked;

   // Heading card named for the "tombstone" needs to get replaced
   var tombstone = document.getElementById('tombstone');

   // Replace tombstone with page contents
   var state = tombstone.querySelector('#state').innerHTML;

   // Add content into the page
   $.get(
      "/?" + state,
       function (data) {
          $(".container").append(data);
       }
   );

   tombstone.id = "old_tombstone";
}

// Make all loading buttons show a "Loading..." state
function loadingButton() {
   // And add a listener to set the loading text for this and others
   document.getElementById('loadButton').addEventListener('click', function() {
      document.getElementById('loadButton').style.display = "none";
      document.getElementById('inProgress').style.display = "block";
   }, false);
}

// Once page has loaded, add new event listeners. When new cards are added to
// the page, run this code to regenerate tombstones and scrollstones that
// manage the loading of new content
$(function() {
   if ( document.getElementById('loadButton') != null ) {
      loadingButton();
   }

   // Detect when we loaded more content, and scroll to it if it was done via
   // a button click at the bottom of the page. Need a timeout value so that
   // we scrollIntoView() only when all divs are finally rendered
   document.getElementById('container').addEventListener( 'DOMNodeInserted', function () {
      // If the bottom-of-page button was actually clicked, do a page-shift
      // so that it's clear the new content is finished loading.
      setTimeout(function () {
         var old_tombstone = document.getElementById('old_tombstone');
         if( clickMore == true ) {
            clickMore = false;
            // Go to an element next to the previous tombstone
            last_element = old_tombstone.previousElementSibling;
            // If last element was expanded, shrink it
            shortener = last_element.querySelector('.showShort');
            if ( shortener ) {
               if ( shortener.style.display == "none" ) {
                  cardToggle(last_element.id);
               }
            }
            last_element.scrollIntoView();
         }
         // Any old tombstones should be removed
         $("#old_tombstone").remove();
         $("#old_scrollstone").remove();
         // Any new divs should get topic links that are clickable
         activateTopicLinks();
         loadingButton();
      }, 200);
   }, false );

   // If we scrolled past the scrollstone itself, start adding new content
   window.addEventListener( 'scroll', function() {
      var scrollstone = document.getElementById('scrollstone');
      // If scrolled past the scrollstone, start loading data
      if ( document.body.scrollTop >= scrollstone.offsetTop ) {
         scrollstone.id = "old_scrollstone";
         addMoreContent(false);

         document.getElementById('loadButton').style.display = "none";
         document.getElementById('inProgress').style.display = "block";
      }
   });

   // Make new topic links clickable and populate the search bar
   activateTopicLinks();

   // Process input into the search form
   $('#searchForm').submit(function() {
      $('#searchEntry').blur();   // Make iOS keyboard disappear after submitting
      var query = $('#searchEntry').val().trim();
      // Remove or escape any search processing characters here like commas
      $(".container").empty();

      // Create a now-searching div
      var loadingText = ''
      if ( query == '' ) {
         loadingText = document.createTextNode('Reshuffling...');
      } else {
         loadingText = document.createTextNode('Searching...');
      }
      var loading = document.createElement('div');
      loading.className = "card heading";
      loading.id = "search_waiting";
      var loadingContents = document.createElement('p');
      loadingContents.id = "loadButton";
      loadingContents.appendChild(loadingText);
      loading.appendChild(loadingContents);
      $(".container").append(loading);

      // Allow searches using special characters like #
      query = escape(query);

      // Load more data
      $("#searchForm").action = "/?xs" + query;
      $.get(
         "/?xs" + query,
          function (data) {
             // hack hack hack (length check)
             if ( data.length <= 200 ) {
                var failedText = document.createTextNode('No results.');
                loadingContents.removeChild(loadingText);
                loadingContents.appendChild(failedText);
             } else {
                $(".container").empty();
                $(".container").append(data);
             }
          }
      );
   });
});

function activateTopicLinks() {
   var linkList = document.getElementsByClassName('topicLink');
   for (var i = 0; i < linkList.length; i++ ) {
      linkList[i].addEventListener('click', function() {
         $('#searchEntry').val(this.text);
         $('#searchForm').submit();
      }, false);
   }
}

function revealToggle(id) {
   var card = document.getElementById(id);
   var largeImgs = card.querySelectorAll('.imgExpand')
   var expandLink = card.querySelector('.showShort')
   var expandDiv = card.querySelector('.divExpand')
   var todo = expandLink.style.display;

   if ( todo !== "none" ) {
      expandLink.style.display = "none";
      expandDiv.style.display = "block";
      for ( var i = 0; i < largeImgs.length; i++ ) {
         largeImgs[i].style.display = "block";
      }
   }
   else {
      expandLink.style.display = "inline";
      expandDiv.style.display = "none";
      for ( var i = 0; i < largeImgs.length; i++ ) {
         largeImgs[i].style.display = "none";
      }
   }
}

function createPost(id, mode) {
   var card = document.getElementById(id);
   var newPostCard = document.createElement('div');
   newPostCard.className = 'card newPost';
   var reply = document.createElement('textarea');
   reply.className = "newThread";
   reply.required = true;

   // If reply already clicked, get rid of previous text boxes
   var nextElement = card.nextElementSibling;
   if ( nextElement.classList.contains("newPost") ) {
      card.nextSibling.remove();
   }
   if ( nextElement.classList.contains(mode) ) {
      return;   // Undraw, and call it good
   }

   reply.name = mode;
   newPostCard.classList.add(mode);
   if ( mode === "quote" ) {
      // Get body of the earlier message, and format it without nested
      // quotes. Only basic paragraphs (avoid pre/code/friends) TODO
      quotetext = card.childNodes[3].getElementsByClassName('postBody');
      // Munge all paragraphs, and put it in a [QUOTE] tag.
      reply.defaultValue = "[QUOTE]" + quotetext[0].textContent + "[/QUOTE]";

   } else {
      reply.placeholder = "Add Your Reply!";
   }

   // Append edit menu afterwards
   var replyFooter = document.createElement('div');
   // TODO: not newthread class!
   replyFooter.className = "rolldown newthread";
   var attachmentButton = document.createElement('input');
   attachmentButton.className = "threadFileInput";
   attachmentButton.type = "file";
   attachmentButton.name = "attachment";
   var submitButton = document.createElement('input');
   submitButton.className = "threadSubmit";
   submitButton.type = "submit";
   submitButton.name = "submit";
   replyFooter.appendChild(attachmentButton);
   replyFooter.appendChild(submitButton);

   newPostCard.appendChild(reply);
   newPostCard.appendChild(replyFooter);

   card.insertAdjacentElement('afterEnd', newPostCard);
}
<|endoftext|>


<commit_before>/* global document beforeEach afterEach describe it window */
const proxyquireStrict = require('proxyquire').noCallThru();
const mockDOMSliderStub = {};

jest.mock('dom-slider', () => mockDOMSliderStub);

describe('Toggle init', () => {
  const { toggle } = require('../components/toggle');

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll
      .getCall(0).args[0]).toBe('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const mockDOMSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': mockDOMSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('button');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-controls', 'triggerId');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-u-BareButton',
      'sprk-u-TextCrop--none',
      'sprk-b-Link',
      'sprk-b-Link--has-icon',
      'sprk-b-Link--simple',
    );

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--simple',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.setAttribute('id', 'contentId');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');
    content.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add(
      'sprk-b-TypeBodyTwo',
      'sprk-c-Accordion__content',
    );
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should add aria-expanded attribute set to false if not found on'
    + ' trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).toBe('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should toggle accordion item open', () => {
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion open', () => {
    triggerAccordion
      .setAttribute('data-sprk-toggle-type', 'masthead-accordion');
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-MastheadAccordion__item--open')).toBe(true);
    expect(iconAccordionUseElement
      .getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(
      iconAccordion,
      iconAccordionUseElement,
      'chevron-up-circle-two-color',
      'chevron-down-circle-two-color',
    );
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-down-circle-two-color');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    trigger.dispatchEvent(new window.Event('click'));
    expect(trigger.style.pointerEvents).toBe('none');
  });

  it('should enable clicks after slide toggle animation runs', (cb) => {
    bindToggleUIEvents(container);
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).toBe('auto');
      cb();
    }, 100);
  });

  it('should still toggle content if icon is not present', () => {
    handleToggleClick(contentAccordion, null, null, triggerAccordion);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should not add Accordion__item class if toggle is not an'
    + 'accordion', () => {
    handleToggleClick(content, null, null, trigger);
    expect(container.classList.contains('sprk-c-Accordion__item--open')).toBe(false);
  });
});
<commit_msg>Add test toggle id<commit_after>/* global document beforeEach afterEach describe it window */
const proxyquireStrict = require('proxyquire').noCallThru();
const mockDOMSliderStub = {};

jest.mock('dom-slider', () => mockDOMSliderStub);

describe('Toggle init', () => {
  const { toggle } = require('../components/toggle');

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll
      .getCall(0).args[0]).toBe('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const mockDOMSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': mockDOMSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('button');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.setAttribute('aria-controls', 'toggle-1');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-u-BareButton',
      'sprk-u-TextCrop--none',
      'sprk-b-Link',
      'sprk-b-Link--has-icon',
      'sprk-b-Link--simple',
    );

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--simple',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.setAttribute('id', 'toggle-1');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');
    content.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add(
      'sprk-b-TypeBodyTwo',
      'sprk-c-Accordion__content',
    );
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => new Promise((resolve) => {
      resolve();
    });

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should add aria-expanded attribute set to false if not found on'
    + ' trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).toBe('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).toBe(true);
  });

  it('should toggle accordion item open', () => {
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion open', () => {
    triggerAccordion
      .setAttribute('data-sprk-toggle-type', 'masthead-accordion');
    handleToggleClick(
      contentAccordion,
      iconAccordion,
      iconAccordionUseElement,
      triggerAccordion,
    );
    expect(containerAccordion.classList
      .contains('sprk-c-MastheadAccordion__item--open')).toBe(true);
    expect(iconAccordionUseElement
      .getAttribute('xlink:href')).toBe('#chevron-up-circle-two-color');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(
      iconAccordion,
      iconAccordionUseElement,
      'chevron-up-circle-two-color',
      'chevron-down-circle-two-color',
    );
    expect(iconAccordionUseElement.getAttribute('xlink:href')).toBe('#chevron-down-circle-two-color');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).toBe('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    trigger.dispatchEvent(new window.Event('click'));
    expect(trigger.style.pointerEvents).toBe('none');
  });

  it('should enable clicks after slide toggle animation runs', (cb) => {
    bindToggleUIEvents(container);
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).toBe('auto');
      cb();
    }, 100);
  });

  it('should still toggle content if icon is not present', () => {
    handleToggleClick(contentAccordion, null, null, triggerAccordion);
    expect(containerAccordion.classList
      .contains('sprk-c-Accordion__item--open')).toBe(true);
  });

  it('should not add Accordion__item class if toggle is not an'
    + 'accordion', () => {
    handleToggleClick(content, null, null, trigger);
    expect(container.classList.contains('sprk-c-Accordion__item--open')).toBe(false);
  });
});
<|endoftext|>


<commit_before>import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import axios from 'axios';
import React, { Component } from 'react';
import Button from 'react-bootstrap/Button';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import { handleChange, OptionalBoolean, OptionalNumberField, RequiredBoolean, stateProperty, StringList } from './forms';
import { errorAlert, sourceQueryStringParams } from './uiutil';

function policyTypeName(s) {
    if (!s.host && !s.userName) {
        return "Global Policy"
    }

    if (!s.userName) {
        return "Per-Host Policy";
    }

    if (!s.path) {
        return "Per-User Policy";
    }

    return "Directory Policy";
}

export class PolicyEditor extends Component {
    constructor() {
        super();
        this.state = {
            items: [],
            isLoading: false,
            error: null,
        };

        this.fetchPolicy = this.fetchPolicy.bind(this);
        this.handleChange = handleChange.bind(this);
        this.saveChanges = this.saveChanges.bind(this);
        this.isGlobal = this.isGlobal.bind(this);
        this.deletePolicy = this.deletePolicy.bind(this);
        this.snapshotURL = this.snapshotURL.bind(this);
    }

    componentDidMount() {
        axios.get('/api/v1/repo/algorithms').then(result => {
            this.setState({
                algorithms: result.data,
            });

            this.fetchPolicy(this.props);
        });
    }

    componentWillReceiveProps(props) {
        this.fetchPolicy(props);
    }

    fetchPolicy(props) {
        axios.get(this.snapshotURL(props)).then(result => {
            this.setState({
                isLoading: false,
                policy: result.data,
            });
        }).catch(error => {
            if (error.response && error.response.data.code !== "NOT_FOUND") {
                this.setState({
                    error: error,
                    isLoading: false
                })
            } else {
                this.setState({
                    policy: {},
                    isNew: true,
                    isLoading: false
                })
            }
        });
    }

    saveChanges(e) {
        e.preventDefault()

        function removeEmpty(l) {
            if (!l) {
                return l;
            }

            let result = [];
            for (let i = 0; i < l.length; i++) {
                const s = l[i];
                if (s === "") {
                    continue;
                }

                result.push(s);
            }

            return result;
        }

        // clean up policy before saving
        let policy = JSON.parse(JSON.stringify(this.state.policy));
        if (policy.files) {
            if (policy.files.ignore) {
                policy.files.ignore = removeEmpty(policy.files.ignore)
            }
            if (policy.files.ignoreDotFiles) {
                policy.files.ignoreDotFiles = removeEmpty(policy.files.ignoreDotFiles)
            }
        }

        if (policy.compression) {
            if (policy.compression.onlyCompress) {
                policy.compression.onlyCompress = removeEmpty(policy.compression.onlyCompress)
            }
            if (policy.compression.neverCompress) {
                policy.compression.neverCompress = removeEmpty(policy.compression.neverCompress)
            }
        }

        axios.put(this.snapshotURL(this.props), policy).then(result => {
            this.props.close();
        }).catch(error => {
            errorAlert(error, 'Error saving policy');
        });
    }

    deletePolicy() {
        if (window.confirm('Are you sure you want to delete this policy?')) {
            axios.delete(this.snapshotURL(this.props)).then(result => {
                this.props.close();
            }).catch(error => {
                errorAlert(error, 'Error deleting policy');
            });
        }
    }

    snapshotURL(props) {
        return '/api/v1/policy?' + sourceQueryStringParams(props);
    }

    isGlobal() {
        return !this.props.host && !this.props.userName && !this.props.path;
    }

    render() {
        const { isLoading, error } = this.state;
        if (error) {
            return
{error.message}
;
        }

        if (isLoading) {
            return
Loading ...
;
        }

        return

            {!this.props.embedded &&
<Button size="sm" variant="outline-secondary" onClick={this.props.close} ><FontAwesomeIcon icon={faChevronLeft} /> Return </Button>
              {policyTypeName(this.props)}
}
            <Form onSubmit={this.saveChanges}>
            {!this.props.embedded && <Row>
                <Col xs="2">
                    <Form.Group>
                        <Form.Label className="required">Target User</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.userName || "<any user>"} />
                    </Form.Group>
                </Col>
                <Col xs="2">
                    <Form.Group>
                        <Form.Label className="required">Target Host</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.host || "<any host>"} />
                    </Form.Group>
                </Col>
                <Col xs="8">
                    <Form.Group>
                        <Form.Label className="required">Target Directory Path</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.path || "<any path>"} />
                    </Form.Group>
                </Col>
            </Row>}
            <Tabs defaultActiveKey="retention">
                <Tab eventKey="retention" title="Retention">

Controls how many latest snapshots to keep per source directory

                        <Form.Row>
                            {OptionalNumberField(this, "Latest", "policy.retention.keepLatest", { placeholder: "# of latest snapshots" })}
                            {OptionalNumberField(this, "Hourly", "policy.retention.keepHourly", { placeholder: "# of hourly snapshots" })}
                            {OptionalNumberField(this, "Daily", "policy.retention.keepDaily", { placeholder: "# of daily snapshots" })}
                        </Form.Row>
                        <Form.Row>
                            {OptionalNumberField(this, "Weekly", "policy.retention.keepWeekly", { placeholder: "# of weekly snapshots" })}
                            {OptionalNumberField(this, "Monthly", "policy.retention.keepMonthly", { placeholder: "# of monthly snapshots" })}
                            {OptionalNumberField(this, "Annual", "policy.retention.keepAnnual", { placeholder: "# of annual snapshots" })}
                        </Form.Row>

                </Tab>
                <Tab eventKey="files" title="Files">

Controls which files should be included and excluded when snapshotting. Use .gitignore syntax.

                        <Form.Row>
                            {StringList(this, "Ignore Rules", "policy.files.ignore", "List of file name patterns to ignore.")}
                            {StringList(this, "Ignore Rule Files", "policy.files.ignoreDotFiles", "List of additional files containing ignore rules. Each file configures ignore rules for the directory and its subdirectories.")}
                        </Form.Row>
                        <Form.Row>
                            {RequiredBoolean(this, "Ignore Parent Rules", "policy.files.noParentIgnore")}
                            {RequiredBoolean(this, "Ignore Parent Rule Files", "policy.files.noParentDotFiles")}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Ignore Well-Known Cache Directories", "policy.files.ignoreCacheDirs", "inherit from parent")}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Scan only one filesystem", "policy.files.oneFileSystem", "inherit from parent")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="errors" title="Errors">

Controls how errors detected while snapshotting are handled.

                        <Form.Row>
                            {OptionalBoolean(this, "Ignore Directory Errors", "policy.errorHandling.ignoreDirectoryErrors", "inherit from parent")}
                            {OptionalBoolean(this, "Ignore File Errors", "policy.errorHandling.ignoreFileErrors", "inherit from parent")}
                            {OptionalBoolean(this, "Ignore Unknown Types", "policy.errorHandling.ignoreUnknownTypes", "inherit from parent")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="compression" title="Compression">

Controls which files are compressed.

                        <Form.Row>
                            <Form.Group as={Col}>
                                <Form.Label className="required">Compression</Form.Label>
                                <Form.Control as="select"
                                    name="policy.compression.compressorName"
                                    onChange={this.handleChange}
                                    value={stateProperty(this, "policy.compression.compressorName")}>
                                    <option value="">(none)</option>
                                    {this.state.algorithms && this.state.algorithms.compression.map(x => <option key={x} value={x}>{x}</option>)}
                                </Form.Control>
                            </Form.Group>
                            {OptionalNumberField(this, "Min File Size", "policy.compression.minSize", { placeholder: "minimum file size to compress" })}
                            {OptionalNumberField(this, "Max File Size", "policy.compression.maxSize", { placeholder: "maximum file size to compress" })}
                        </Form.Row>
                        <Form.Row>
                            {StringList(this, "Only Compress Extensions", "policy.compression.onlyCompress", "Only compress files with the above file extensions (one extension per line)")}
                            {StringList(this, "Never Compress Extensions", "policy.compression.neverCompress", "Never compress the above file extensions (one extension per line)")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="scheduling" title="Scheduling">

Controls when snapshots are automatically created.

                        <Form.Row>
                            {OptionalNumberField(this, "Snapshot Interval", "policy.scheduling.intervalSeconds", { placeholder: "seconds" })}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Only create snapshots manually (disables scheduled snapshots)", "policy.scheduling.manual")}
                        </Form.Row>

                </Tab>
            </Tabs>
            <Form.Row>
                {RequiredBoolean(this, "Disable Parent Policy Evaluation (prevents any parent policies from affecting this directory and subdirectories)", "policy.noParent")}
            </Form.Row>

            <Button size="sm" variant="success" type="submit" onClick={this.saveChanges}>Save Policy</Button>
            {!this.state.isNew && <>
            <Button size="sm" variant="danger" disabled={this.isGlobal()} onClick={this.deletePolicy}>Delete Policy</Button>
            </>}

            {!this.props.embedded && <>

JSON representation

{JSON.stringify(this.state.policy, null, 4)}

            </>}
            </Form>

;
    }
}
<commit_msg>ui: show a spinner when saving/deleting policy (#1018)<commit_after>import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import axios from 'axios';
import React, { Component } from 'react';
import Button from 'react-bootstrap/Button';
import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';
import Spinner from 'react-bootstrap/Spinner';
import Tab from 'react-bootstrap/Tab';
import Tabs from 'react-bootstrap/Tabs';
import { handleChange, OptionalBoolean, OptionalNumberField, RequiredBoolean, stateProperty, StringList } from './forms';
import { errorAlert, sourceQueryStringParams } from './uiutil';

function policyTypeName(s) {
    if (!s.host && !s.userName) {
        return "Global Policy"
    }

    if (!s.userName) {
        return "Per-Host Policy";
    }

    if (!s.path) {
        return "Per-User Policy";
    }

    return "Directory Policy";
}

export class PolicyEditor extends Component {
    constructor() {
        super();
        this.state = {
            items: [],
            isLoading: false,
            error: null,
        };

        this.fetchPolicy = this.fetchPolicy.bind(this);
        this.handleChange = handleChange.bind(this);
        this.saveChanges = this.saveChanges.bind(this);
        this.isGlobal = this.isGlobal.bind(this);
        this.deletePolicy = this.deletePolicy.bind(this);
        this.snapshotURL = this.snapshotURL.bind(this);
    }

    componentDidMount() {
        axios.get('/api/v1/repo/algorithms').then(result => {
            this.setState({
                algorithms: result.data,
            });

            this.fetchPolicy(this.props);
        });
    }

    componentWillReceiveProps(props) {
        this.fetchPolicy(props);
    }

    fetchPolicy(props) {
        axios.get(this.snapshotURL(props)).then(result => {
            this.setState({
                isLoading: false,
                policy: result.data,
            });
        }).catch(error => {
            if (error.response && error.response.data.code !== "NOT_FOUND") {
                this.setState({
                    error: error,
                    isLoading: false
                })
            } else {
                this.setState({
                    policy: {},
                    isNew: true,
                    isLoading: false
                })
            }
        });
    }

    saveChanges(e) {
        e.preventDefault()

        function removeEmpty(l) {
            if (!l) {
                return l;
            }

            let result = [];
            for (let i = 0; i < l.length; i++) {
                const s = l[i];
                if (s === "") {
                    continue;
                }

                result.push(s);
            }

            return result;
        }

        // clean up policy before saving
        let policy = JSON.parse(JSON.stringify(this.state.policy));
        if (policy.files) {
            if (policy.files.ignore) {
                policy.files.ignore = removeEmpty(policy.files.ignore)
            }
            if (policy.files.ignoreDotFiles) {
                policy.files.ignoreDotFiles = removeEmpty(policy.files.ignoreDotFiles)
            }
        }

        if (policy.compression) {
            if (policy.compression.onlyCompress) {
                policy.compression.onlyCompress = removeEmpty(policy.compression.onlyCompress)
            }
            if (policy.compression.neverCompress) {
                policy.compression.neverCompress = removeEmpty(policy.compression.neverCompress)
            }
        }

        this.setState({saving: true});
        axios.put(this.snapshotURL(this.props), policy).then(result => {
            this.props.close();
        }).catch(error => {
            this.setState({saving: false});
            errorAlert(error, 'Error saving policy');
        });
    }

    deletePolicy() {
        if (window.confirm('Are you sure you want to delete this policy?')) {
            this.setState({saving: true});

            axios.delete(this.snapshotURL(this.props)).then(result => {
                this.props.close();
            }).catch(error => {
                this.setState({saving: false});
                errorAlert(error, 'Error deleting policy');
            });
        }
    }

    snapshotURL(props) {
        return '/api/v1/policy?' + sourceQueryStringParams(props);
    }

    isGlobal() {
        return !this.props.host && !this.props.userName && !this.props.path;
    }

    render() {
        const { isLoading, error } = this.state;
        if (error) {
            return
{error.message}
;
        }

        if (isLoading) {
            return
Loading ...
;
        }

        return

            {!this.props.embedded &&
<Button size="sm" variant="outline-secondary" onClick={this.props.close} ><FontAwesomeIcon icon={faChevronLeft} /> Return </Button>
              {policyTypeName(this.props)}
}
            <Form onSubmit={this.saveChanges}>
            {!this.props.embedded && <Row>
                <Col xs="2">
                    <Form.Group>
                        <Form.Label className="required">Target User</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.userName || "<any user>"} />
                    </Form.Group>
                </Col>
                <Col xs="2">
                    <Form.Group>
                        <Form.Label className="required">Target Host</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.host || "<any host>"} />
                    </Form.Group>
                </Col>
                <Col xs="8">
                    <Form.Group>
                        <Form.Label className="required">Target Directory Path</Form.Label>
                        <Form.Control size="sm" type="text" readOnly value={this.props.path || "<any path>"} />
                    </Form.Group>
                </Col>
            </Row>}
            <Tabs defaultActiveKey="retention">
                <Tab eventKey="retention" title="Retention">

Controls how many latest snapshots to keep per source directory

                        <Form.Row>
                            {OptionalNumberField(this, "Latest", "policy.retention.keepLatest", { placeholder: "# of latest snapshots" })}
                            {OptionalNumberField(this, "Hourly", "policy.retention.keepHourly", { placeholder: "# of hourly snapshots" })}
                            {OptionalNumberField(this, "Daily", "policy.retention.keepDaily", { placeholder: "# of daily snapshots" })}
                        </Form.Row>
                        <Form.Row>
                            {OptionalNumberField(this, "Weekly", "policy.retention.keepWeekly", { placeholder: "# of weekly snapshots" })}
                            {OptionalNumberField(this, "Monthly", "policy.retention.keepMonthly", { placeholder: "# of monthly snapshots" })}
                            {OptionalNumberField(this, "Annual", "policy.retention.keepAnnual", { placeholder: "# of annual snapshots" })}
                        </Form.Row>

                </Tab>
                <Tab eventKey="files" title="Files">

Controls which files should be included and excluded when snapshotting. Use .gitignore syntax.

                        <Form.Row>
                            {StringList(this, "Ignore Rules", "policy.files.ignore", "List of file name patterns to ignore.")}
                            {StringList(this, "Ignore Rule Files", "policy.files.ignoreDotFiles", "List of additional files containing ignore rules. Each file configures ignore rules for the directory and its subdirectories.")}
                        </Form.Row>
                        <Form.Row>
                            {RequiredBoolean(this, "Ignore Parent Rules", "policy.files.noParentIgnore")}
                            {RequiredBoolean(this, "Ignore Parent Rule Files", "policy.files.noParentDotFiles")}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Ignore Well-Known Cache Directories", "policy.files.ignoreCacheDirs", "inherit from parent")}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Scan only one filesystem", "policy.files.oneFileSystem", "inherit from parent")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="errors" title="Errors">

Controls how errors detected while snapshotting are handled.

                        <Form.Row>
                            {OptionalBoolean(this, "Ignore Directory Errors", "policy.errorHandling.ignoreDirectoryErrors", "inherit from parent")}
                            {OptionalBoolean(this, "Ignore File Errors", "policy.errorHandling.ignoreFileErrors", "inherit from parent")}
                            {OptionalBoolean(this, "Ignore Unknown Types", "policy.errorHandling.ignoreUnknownTypes", "inherit from parent")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="compression" title="Compression">

Controls which files are compressed.

                        <Form.Row>
                            <Form.Group as={Col}>
                                <Form.Label className="required">Compression</Form.Label>
                                <Form.Control as="select"
                                    name="policy.compression.compressorName"
                                    onChange={this.handleChange}
                                    value={stateProperty(this, "policy.compression.compressorName")}>
                                    <option value="">(none)</option>
                                    {this.state.algorithms && this.state.algorithms.compression.map(x => <option key={x} value={x}>{x}</option>)}
                                </Form.Control>
                            </Form.Group>
                            {OptionalNumberField(this, "Min File Size", "policy.compression.minSize", { placeholder: "minimum file size to compress" })}
                            {OptionalNumberField(this, "Max File Size", "policy.compression.maxSize", { placeholder: "maximum file size to compress" })}
                        </Form.Row>
                        <Form.Row>
                            {StringList(this, "Only Compress Extensions", "policy.compression.onlyCompress", "Only compress files with the above file extensions (one extension per line)")}
                            {StringList(this, "Never Compress Extensions", "policy.compression.neverCompress", "Never compress the above file extensions (one extension per line)")}
                        </Form.Row>

                </Tab>
                <Tab eventKey="scheduling" title="Scheduling">

Controls when snapshots are automatically created.

                        <Form.Row>
                            {OptionalNumberField(this, "Snapshot Interval", "policy.scheduling.intervalSeconds", { placeholder: "seconds" })}
                        </Form.Row>
                        <Form.Row>
                            {OptionalBoolean(this, "Only create snapshots manually (disables scheduled snapshots)", "policy.scheduling.manual")}
                        </Form.Row>

                </Tab>
            </Tabs>
            <Form.Row>
                {RequiredBoolean(this, "Disable Parent Policy Evaluation (prevents any parent policies from affecting this directory and subdirectories)", "policy.noParent")}
            </Form.Row>

            <Button size="sm" variant="success" type="submit" onClick={this.saveChanges} disabled={this.state.saving}>Save Policy</Button>
            {!this.state.isNew && <>
            <Button size="sm" variant="danger" disabled={this.isGlobal() || this.state.saving} onClick={this.deletePolicy}>Delete Policy</Button>
            </>}
            {this.state.saving && <>

                <Spinner animation="border" variant="primary" size="sm" />
                </>}

            {!this.props.embedded && <>

JSON representation

{JSON.stringify(this.state.policy, null, 4)}

            </>}
            </Form>

;
    }
}
<|endoftext|>


<commit_before>// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt

$.extend(cur_frm.cscript, {
	onload: function() {
		if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) {
			cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series;
		}
	},
	refresh: function(){
		toggle_mandatory(cur_frm)
	},
	enable_checkout: function(){
		toggle_mandatory(cur_frm)
	}
});

function toggle_mandatory (cur_frm){
	cur_frm.toggle_reqd("payment_gateway_account", false);
	if(cur_frm.doc.enabled && cur_frm.doc.enable_checkout) {
		cur_frm.toggle_reqd("payment_gateway_account", true);
	}
}
<commit_msg>Refresh quotation_series field to show series<commit_after>// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt

$.extend(cur_frm.cscript, {
	onload: function() {
		if(cur_frm.doc.__onload && cur_frm.doc.__onload.quotation_series) {
			cur_frm.fields_dict.quotation_series.df.options = cur_frm.doc.__onload.quotation_series;
			cur_frm.refresh_field("quotation_series");
		}
	},
	refresh: function(){
		toggle_mandatory(cur_frm)
	},
	enable_checkout: function(){
		toggle_mandatory(cur_frm)
	}
});

function toggle_mandatory (cur_frm){
	cur_frm.toggle_reqd("payment_gateway_account", false);
	if(cur_frm.doc.enabled && cur_frm.doc.enable_checkout) {
		cur_frm.toggle_reqd("payment_gateway_account", true);
	}
}
<|endoftext|>


<commit_before>{
	"label": "Label",
	"alias": "Alias",
	"hideFieldsetControls": "Hide Fieldset Controls?",
	"hideFieldsetControlsDescription": "Hides the fieldset add/remove/sort controls.",
	"toggleAdvanced": "Toggle Advanced Options",
	"toggleAdvancedDescription": "Show advanced options.",
	"hidePropertyLabels": "Hide Property Labels?",
	"hidePropertyLabelsDescription": "Hides the property labels.",
	"maxFieldsets": "Max Fieldsets",
	"maxFieldsetsDescription": "How many Fieldsets are allowed? Entering '1' will disable the controls. Default is unlimited.",
	"enableMultipleFieldsets": "Enable Multiple Fieldsets?",
	"enableMultipleFieldsetsDescription": "Allows multiple types of fieldsets within this archetype.",
	"hideFieldsetToolbar": "Hide Fieldset Toolbar?",
	"hideFieldsetToolbarDescription": "Hides the fieldset toolbar that appears when more than one fieldset model appears.",
	"customWrapperClass": "Custom Wrapper Class",
	"customWrapperClassDescription": "(Optional) Enter a custom CSS class that will be applied to the wrapper div.",
	"cssFile": "CSS File",
	"cssFileDescription": "(Optional) Enter a path for a custom CSS file to be included on the page.",
	"jsFile": "JS File",
	"jsFileDescription": "(Optional) Enter a path for a custom JS file to be included on the page.",
	"toggleDeveloperMode": "Toggle Developer Mode",
	"toggleDeveloperModeDescription": "Turn on for developer options.",
	"configModel": "Config Model",
	"configModelDescription": "Be careful editing the text below, it controls the schema for this archetype.",
	"helpText": "Help Text",
	"defaultValue": "Default Value",
	"dataType": "DataType",
	"required": "Required",
	"properties": "Properties",
	"labelTemplate": "Label Template",
	"select": "Select",
	"icon": "Icon"
}
<commit_msg>Translation to danish<commit_after>{
	"label": "Label",
	"alias": "Alias",
	"hideFieldsetControls": "Skjul fieldsets?",
	"hideFieldsetControlsDescription": "Skjuler kontrollerne tilføj/fjern/sortér.",
	"toggleAdvanced": "Slå avancerede muligheder til/fra",
	"toggleAdvancedDescription": "Vis avancerede muligheder.",
	"hidePropertyLabels": "Skjul labels på egenskaber?",
	"hidePropertyLabelsDescription": "Skjuler labels på egenskaber.",
	"maxFieldsets": "Maks antal fieldsets",
	"maxFieldsetsDescription": "Hvor mange fieldsets er tilladt? Hvis der skrives '1' vil deaktivere kontrollerne. Som standard er det ubegrænset.",
	"enableMultipleFieldsets": "Aktivér flere Fieldsets?",
	"enableMultipleFieldsetsDescription": "Tillader flere typer af fieldsets i denne archetype.",
	"hideFieldsetToolbar": "Skjul Fieldset værktøjslinie?",
	"hideFieldsetToolbarDescription": "Skjuler den fieldset værktøjslinie der fremkommer når der findes mere end ét fieldset.",
	"customWrapperClass": "Egen Wrapper Class",
	"customWrapperClassDescription": "(Mulighed) Angiv din egen CSS class som tilføjes på wrapper div'en.",
	"cssFile": "CSS fil",
	"cssFileDescription": "(Mulighed) Tilføj en sti til din egen CSS fil der skal inkluderes på siden.",
	"jsFile": "JavaScript fil",
	"jsFileDescription": "(Mulighed) Tilføj en sti til din egen JavaScript fil der skal inkluderes på siden.",
	"toggleDeveloperMode": "Slå udvikler muligheder til/fra.",
	"toggleDeveloperModeDescription": "Aktivér på udvikler muligheder.",
	"configModel": "Konfigurationsmodel",
	"configModelDescription": "Vær forsigt med at redigere teksten for neden, den kontrollerer skemaet for denne archetype.",
	"helpText": "Hjælpe tekst",
	"defaultValue": "Standard værdi",
	"dataType": "Datatype",
	"required": "Påkrævet",
	"properties": "Egenskaber",
	"labelTemplate": "Label skabelon",
	"select": "Vælg",
	"icon": "Ikon"
}
<|endoftext|>


<commit_before>class Canvas {
    /**
     * Over all constructor of the canvas. Sets up the event listeners and records the state
     * of the overall canvas. Adapted from a guide by Simon Sarris.
     * @param canvas The canvas element to be passed in.
     */
    constructor(canvas) {
        this.canvas = canvas;                              // Canvas passed in.
        this.padding = 100;                                // White padding outside of background image.
        this.numberOfFrames = 5000;
        this.scrollBarSize = 636;

        /* Gets the first frame's height and width and sets up canvas to it. */
        var img = new Image();
        img.src = frame_path(0);
        canvas.width = img.naturalWidth + this.padding;
        canvas.height = img.naturalHeight + this.padding;
        this.width = canvas.width;                         // Width of the canvas.
        this.height = canvas.height;                       // Height of the canvas.
        this.ctx = canvas.getContext('2d');                // Canvas to be modified.

        document.getElementById("fixed-panel").style.width = this.width + 20;
        document.getElementById("frame-controller").style.width = this.width + 7;

        this.selectionWidth = 2;                           // Border fillSize of selected boxes.
        this.selectionCorner = 5;                          // Corner fillSize of selected boxes.
        this.interval = 30;                                // Frequency to be redrawn.

        /* Fixes mouse co-ordinate problems when there's a border or padding. See getMouse for more
         details.  */
        if (document.defaultView && document.defaultView.getComputedStyle) {
            this.stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['paddingLeft'], 10) || 0;
            this.stylePaddingTop = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['paddingTop'], 10) || 0;
            this.styleBorderLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['borderLeftWidth'], 10) || 0;
            this.styleBorderTop = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['borderTopWidth'], 10) || 0;
        }

        /* Used to recalculate mouse coordinates in the event of fixed-position bars throughout
         the page. */
        var html = document.body.parentNode;
        this.htmlTop = html.offsetTop;
        this.htmlLeft = html.offsetLeft;

        /* Maintains the state of the canvas. */
        this.valid = false;             // When false, the canvas will be redrawn.
        this.things = [];               // Collection of things in the video
        this.boxes = [];                // List of boxes in the current frame
        this.dragging = false;          // Keeps track of when we are dragging.
        this.enlargeDirection = null;   // Keeps the direction being dragged when enlarging.
        this.previousSelection = null;  // Previous selection used for key frames.
        this.selection = null;          // Keeps the selected box that was clicked on.
        this.move = false;              // Keeps track of when we are moving a box.
        this.play = false;                                 // Playing frames.

        /* Used for keepign track where  the mouse was clicked to create a smooth drag. */
        this.dragoffx = 0;
        this.dragoffy = 0;

        /* Keeps track of the current frame that is being annotated.
         * Getters and setters will update the boxes for the frame if this is changed */
        this._frame = 0;

        this.setUpListeners(canvas, html);
    }

    setUpListeners(canvas, html) {
        /* Since we want to modify the state of the canvas, we use closure in order
        * to access its variables. */
        var myState = this;

        /* Fixes a problem where double clicking causes text to get selected on the canvas. */
        canvas.addEventListener('selectstart', function (e) {
            e.preventDefault();
            return false;
        }, false);

        /* Events for mouse down.*/
        canvas.addEventListener('mousedown', function (e) {
            var mouse = myState.getMouse(e);
            var mx = mouse.x;
            var my = mouse.y;
            var boxes = myState.boxes;
            var l = boxes.length;

            /* Checks if within border. If not, checks if it is within the box. If it's in the
             border, enlargingDirection is set to the border that was clicked. If it's in the
             box or its border, we set the selection to that box and record where it was
             clicked. */
            for (var i = l - 1; i >= 0; i--) {
                var border = boxes[i].withinBorder(mx, my);
                if (boxes[i].contains(mx, my)) {
                    var mySel = boxes[i];

                    /* Used for smooth transition in mouseMove. */
                    myState.dragoffx = mx - mySel.x;
                    myState.dragoffy = my - mySel.y;
                    myState.dragging = true;
                    myState.previousSelection = myState.selection;
                    myState.selection = mySel;
                    if (border) {
                        myState.enlargeDirection = border;
                    } else {
                        document.body.style.cursor = 'move';
                        myState.move = true;
                    }
                    myState.valid = false;
                    return;
                }
            }

            /* Haven't found any box. This deselects any previously selected boxes. */
            if (myState.selection) {
                myState.selection = null;
                myState.valid = false;
            }

            /* Creates a new thing & box. Continues to enlarge it as the user
               continues to drag. */
            var newThing = new Thing();
            var newBox = new Box(newThing, myState.frame, mouse.x, mouse.y, 1, 1, true);
            myState.things.push(newThing);
            myState.boxes.push(newBox);
            myState.valid = false;
            myState.selection = newBox;
            myState.enlargeDirection = Box.border.BOTTOMRIGHT;

            var thingDom = document.createElement("li");
            thingDom.className = "list-group-item col-xs-6";
            thingDom.id = this.fill;
            thingDom.style = "color: azure; background-color: " + this.fill + "; width: 57px";
            thingDom.innerText = "Car";
            thingDom.addEventListener("click", function() {
                myState.selection = thingDom.id;
            });

            document.getElementById("shape-list").appendChild(thingDom);

        }, true);

        /* Event for moving the mouse. Primarily used for enlarging a box and moving it. */
        canvas.addEventListener('mousemove', function (e) {
            var mouse = myState.getMouse(e);
            var mx = mouse.x;
            var my = mouse.y;
            var boxes = myState.boxes;
            var border = false;
            // var box = null;

            /* Checks if mouse is hovering over a border. */
            for (var i = boxes.length - 1; i >= 0; i--) {
                // box = boxes[i];
                border = boxes[i].withinBorder(mx, my);
                if (border && !myState.move) {
                    boxes[i].doubleArrow(mx, my);
                    break;
                }
            }

            if (!myState.enlargeDirection && !myState.dragging && !border) {
                document.body.style.cursor = 'default';
            } else if (myState.enlargeDirection === Box.border.RIGHT) {
                myState.selection.moveRight(mouse.x);
            } else if (myState.enlargeDirection === Box.border.LEFT) {
                myState.selection.moveLeft(mouse.x);
            } else if (myState.enlargeDirection === Box.border.TOP) {
                myState.selection.moveUp(mouse.y);
            } else if (myState.enlargeDirection === Box.border.BOTTOM) {
                myState.selection.moveDown(mouse.y);
            } else if (myState.enlargeDirection === Box.border.BOTTOMRIGHT) {
                myState.selection.moveDown(mouse.y);
                myState.selection.moveRight(mouse.x);
            } else if (myState.enlargeDirection === Box.border.BOTTOMLEFT) {
                myState.selection.moveDown(mouse.y);
                myState.selection.moveLeft(mouse.x);
            } else if (myState.enlargeDirection === Box.border.TOPLEFT) {
                myState.selection.moveLeft(mouse.x);
                myState.selection.moveUp(mouse.y);
            } else if (myState.enlargeDirection === Box.border.TOPRIGHT) {
                myState.selection.moveUp(mouse.y);
                myState.selection.moveRight(mouse.x);
            } else if (myState.dragging) {
                myState.selection.x = mouse.x - myState.dragoffx;
                myState.selection.y = mouse.y - myState.dragoffy;
            }
            myState.valid = false;
        }, true);

        /* Adds controls for moving boxes with arrow keys as well as deleting them with the 'd'.
            In the event no box is selected, we use the arrow keys to transition frames.*/
        html.addEventListener("keydown", function (event) {
            if (myState.selection != null) { /* Handles box. */
                switch (event.keyCode) {
                    case 38:  /* Arrow down. */
                        myState.selection.y -= 1;
                        break;
                    case 40:  /* Arrow up. */
                        myState.selection.y += 1;
                        break;
                    case 37:  /* Arrow left. */
                        myState.selection.x -= 1;
                        break;
                    case 39:  /* Arrow right. */
                        myState.selection.x += 1;
                        break;
                    case 68:  /* Deletes with 'd'. */
                        myState.removeBox(myState.selection);
                        myState.selection = null;
                        break;
                }
                myState.valid = false;
            } else if (event.keyCode === 37) { /* Moves left a frame. */
                if (myState.frame > 0) {
                    var frame = myState.frame--;
                    canvas.style.backgroundImage = frame_url(frame);
                }
            } else if (event.keyCode === 39) { /* Moves right a frame. */
                var frame = myState.frame++;
                canvas.style.backgroundImage = frame_url(frame);
            }
            myState.valid = false;

        }, true);

        /* Modifies state to prevent dragging. If the box is too small, delete it. */
        canvas.addEventListener('mouseup', function (e) {
            if (document.body.style.cursor === 'move') {
                document.body.style.cursor = 'default';
            }
            myState.move = false;
            myState.dragging = false;
            myState.enlargeDirection = false;
            for (var i = 0; i < myState.boxes.length; i++) {
                if (myState.boxes[i].w === 1 || myState.boxes[i].h === 1) {
                    myState.removeBox(myState.boxes[i]);
                    myState.selection = null;
                    myState.valid = false;
                }
            }
        }, true);

        /* Redraws canvas every interval seconds */
        setInterval(function () {
            myState.draw();
            if (myState.play) {
                myState.frame += 1;
            }
        }, myState.interval);
    }

    get frame() {return this._frame;}
    set frame(value) {
        value = parseInt(value);
        frame_preload(value);
        var frameNumberBox = document.getElementById("frame-number");
        document.getElementById("scroll-bar").value = value;
        frameNumberBox.setAttribute("value", value);
        this._frame = value;
        this.boxes = this.getBoxes(value);
        this.valid = false;
        document.getElementById("frame").style.backgroundImage = frame_url(this._frame);
        if (this.selection != null && this.selection.thing != null) {
            for (var box of this.boxes) {
                if (box.thing.id == this.selection.thing.id) {
                    this.selection = box;
                    break;
                }
            }
        }
    }

    /* Retrieves a list of box objects that belong in the current frame */
    getBoxes(frame) {
        var boxes = [];
        for (var thing of this.things) {
            var box = thing.getKeyframe(frame);
            if (box != null) {
                boxes.push(box);
            }
        }
        return boxes;
    }

    /**
     * Removes box from list of boxes and redraws it. Deletes the Thing if it has no more boxes
     * @param box The box to be removed.
     */
    removeBox(box) {
        var index = this.boxes.indexOf(box);
        this.boxes.splice(index, 1);
        if (box.thing.deleteKeyframe(box)) {
            let index = this.things.indexOf(box.thing);

            var element = document.getElementById(this.things.splice(index, 1)[0].id);
            element.parentNode.removeChild(element);
        }
        this.valid = false;
    }

    // Copied from http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
    shadeColor(color, percent) {
        var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
        return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
    }

    /**
     * Draws the state of the canvas if it's invalid and updates key frames.
     */
    draw() {
        if (!this.valid) {
            var ctx = this.ctx;
            ctx.clearRect(0, 0, this.width, this.height);

            for (var i = 0; i < this.boxes.length; i++) {
                this.boxes[i].draw(ctx);
            }

            /* Draws border for selected box. */

            if (this.selection != null) {
                for (var box of this.boxes) {
                    if (box.thing === this.selection.thing) {
                        ctx.strokeStyle = this.shadeColor(this.selection.thing.fill, -20);
                        ctx.lineWidth = this.selectionWidth;
                        var mySel = this.selection;
                        ctx.strokeRect(mySel.x, mySel.y, mySel.w, mySel.h);

                        /* Handles corners. */
                        var fillSize = this.selectionCorner;
                        ctx.fillStyle = "#000000";
                        ctx.fillRect(mySel.x - 2, mySel.y - 2, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 3 + mySel.w, mySel.y - 2, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 2, mySel.y - 2 + mySel.h, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 3 + mySel.w, mySel.y - 2 + mySel.h, fillSize, fillSize);
                    }
                }

            }

            /* Updates key frames */

            if (this.selection != this.previousSelection || (this.selection && this.selection.thing.keyFramesChanged)) {
                var myNode = document.getElementById("key-frames"); //Faster than setting innerHtml to null.
                while (myNode.firstChild) {
                    myNode.removeChild(myNode.firstChild);
                }
                for (var box of this.selection.thing.keyframes) {
                    var dot = document.createElement("div");
                    dot.className = "dots";
                    dot.style = `left: ${box.frame / this.numberOfFrames * this.scrollBarSize}px;`;
                    document.getElementById("key-frames").appendChild(dot);
                }
                this.selection.thing.keyFramesChanged = false;

            }
            this.valid = true;
        }
    }

    /**
     * Creates a modified x and y coordinate for a mouse event to allow for smooth transitioning.
     * This takes into consideration fixed borders and other objects within the page.
     * @param e
     * @returns {{x: (number|*), y: (number|*)}}
     */
    getMouse(e) {
        var element = this.canvas, offsetX = 0, offsetY = 0, mx, my;

        /* Compute the total offset. */
        if (element.offsetParent !== undefined) {
            do {
                offsetX += element.offsetLeft;
                offsetY += element.offsetTop;
            } while ((element = element.offsetParent));
        }

        /* Add padding and border style widths to offset. Also add the <html> offsets in case
         there's a fixed position bar. */
        offsetX += this.stylePaddingLeft + this.styleBorderLeft + this.htmlLeft;
        offsetY += this.stylePaddingTop + this.styleBorderTop + this.htmlTop;

        mx = e.pageX - offsetX;
        my = e.pageY - offsetY;

        return {x: mx, y: my};
    }
}
<commit_msg>Fixed coloring glitch for boxes<commit_after>class Canvas {
    /**
     * Over all constructor of the canvas. Sets up the event listeners and records the state
     * of the overall canvas. Adapted from a guide by Simon Sarris.
     * @param canvas The canvas element to be passed in.
     */
    constructor(canvas) {
        this.canvas = canvas;                              // Canvas passed in.
        this.padding = 100;                                // White padding outside of background image.
        this.numberOfFrames = 5000;
        this.scrollBarSize = 636;

        /* Gets the first frame's height and width and sets up canvas to it. */
        var img = new Image();
        img.src = frame_path(0);
        canvas.width = img.naturalWidth + this.padding;
        canvas.height = img.naturalHeight + this.padding;
        this.width = canvas.width;                         // Width of the canvas.
        this.height = canvas.height;                       // Height of the canvas.
        this.ctx = canvas.getContext('2d');                // Canvas to be modified.

        document.getElementById("fixed-panel").style.width = this.width + 20;
        document.getElementById("frame-controller").style.width = this.width + 7;

        this.selectionWidth = 2;                           // Border fillSize of selected boxes.
        this.selectionCorner = 5;                          // Corner fillSize of selected boxes.
        this.interval = 30;                                // Frequency to be redrawn.

        /* Fixes mouse co-ordinate problems when there's a border or padding. See getMouse for more
         details.  */
        if (document.defaultView && document.defaultView.getComputedStyle) {
            this.stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['paddingLeft'], 10) || 0;
            this.stylePaddingTop = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['paddingTop'], 10) || 0;
            this.styleBorderLeft = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['borderLeftWidth'], 10) || 0;
            this.styleBorderTop = parseInt(document.defaultView.getComputedStyle(canvas, null)
                    ['borderTopWidth'], 10) || 0;
        }

        /* Used to recalculate mouse coordinates in the event of fixed-position bars throughout
         the page. */
        var html = document.body.parentNode;
        this.htmlTop = html.offsetTop;
        this.htmlLeft = html.offsetLeft;

        /* Maintains the state of the canvas. */
        this.valid = false;             // When false, the canvas will be redrawn.
        this.things = [];               // Collection of things in the video
        this.boxes = [];                // List of boxes in the current frame
        this.dragging = false;          // Keeps track of when we are dragging.
        this.enlargeDirection = null;   // Keeps the direction being dragged when enlarging.
        this.previousSelection = null;  // Previous selection used for key frames.
        this.selection = null;          // Keeps the selected box that was clicked on.
        this.move = false;              // Keeps track of when we are moving a box.
        this.play = false;                                 // Playing frames.

        /* Used for keepign track where  the mouse was clicked to create a smooth drag. */
        this.dragoffx = 0;
        this.dragoffy = 0;

        /* Keeps track of the current frame that is being annotated.
         * Getters and setters will update the boxes for the frame if this is changed */
        this._frame = 0;

        this.setUpListeners(canvas, html);
    }

    setUpListeners(canvas, html) {
        /* Since we want to modify the state of the canvas, we use closure in order
        * to access its variables. */
        var myState = this;

        /* Fixes a problem where double clicking causes text to get selected on the canvas. */
        canvas.addEventListener('selectstart', function (e) {
            e.preventDefault();
            return false;
        }, false);

        /* Events for mouse down.*/
        canvas.addEventListener('mousedown', function (e) {
            var mouse = myState.getMouse(e);
            var mx = mouse.x;
            var my = mouse.y;
            var boxes = myState.boxes;
            var l = boxes.length;

            /* Checks if within border. If not, checks if it is within the box. If it's in the
             border, enlargingDirection is set to the border that was clicked. If it's in the
             box or its border, we set the selection to that box and record where it was
             clicked. */
            for (var i = l - 1; i >= 0; i--) {
                var border = boxes[i].withinBorder(mx, my);
                if (boxes[i].contains(mx, my)) {
                    var mySel = boxes[i];

                    /* Used for smooth transition in mouseMove. */
                    myState.dragoffx = mx - mySel.x;
                    myState.dragoffy = my - mySel.y;
                    myState.dragging = true;
                    myState.previousSelection = myState.selection;
                    myState.selection = mySel;
                    if (border) {
                        myState.enlargeDirection = border;
                    } else {
                        document.body.style.cursor = 'move';
                        myState.move = true;
                    }
                    myState.valid = false;
                    return;
                }
            }

            /* Haven't found any box. This deselects any previously selected boxes. */
            if (myState.selection) {
                myState.selection = null;
                myState.valid = false;
            }

            /* Creates a new thing & box. Continues to enlarge it as the user
               continues to drag. */
            var newThing = new Thing();
            var newBox = new Box(newThing, myState.frame, mouse.x, mouse.y, 1, 1, true);
            myState.things.push(newThing);
            myState.boxes.push(newBox);
            myState.valid = false;
            myState.selection = newBox;
            myState.enlargeDirection = Box.border.BOTTOMRIGHT;

            var thingDom = document.createElement("li");
            thingDom.className = "list-group-item col-xs-6";
            thingDom.id = newThing.fill;
            thingDom.style = "color: azure; background-color: " + newThing.fill + "; width: 57px";
            thingDom.innerText = "Car";
            thingDom.addEventListener("click", function() {
                myState.selection = newThing.id;
            });

            document.getElementById("shape-list").appendChild(thingDom);

        }, true);

        /* Event for moving the mouse. Primarily used for enlarging a box and moving it. */
        canvas.addEventListener('mousemove', function (e) {
            var mouse = myState.getMouse(e);
            var mx = mouse.x;
            var my = mouse.y;
            var boxes = myState.boxes;
            var border = false;
            // var box = null;

            /* Checks if mouse is hovering over a border. */
            for (var i = boxes.length - 1; i >= 0; i--) {
                // box = boxes[i];
                border = boxes[i].withinBorder(mx, my);
                if (border && !myState.move) {
                    boxes[i].doubleArrow(mx, my);
                    break;
                }
            }

            if (!myState.enlargeDirection && !myState.dragging && !border) {
                document.body.style.cursor = 'default';
            } else if (myState.enlargeDirection === Box.border.RIGHT) {
                myState.selection.moveRight(mouse.x);
            } else if (myState.enlargeDirection === Box.border.LEFT) {
                myState.selection.moveLeft(mouse.x);
            } else if (myState.enlargeDirection === Box.border.TOP) {
                myState.selection.moveUp(mouse.y);
            } else if (myState.enlargeDirection === Box.border.BOTTOM) {
                myState.selection.moveDown(mouse.y);
            } else if (myState.enlargeDirection === Box.border.BOTTOMRIGHT) {
                myState.selection.moveDown(mouse.y);
                myState.selection.moveRight(mouse.x);
            } else if (myState.enlargeDirection === Box.border.BOTTOMLEFT) {
                myState.selection.moveDown(mouse.y);
                myState.selection.moveLeft(mouse.x);
            } else if (myState.enlargeDirection === Box.border.TOPLEFT) {
                myState.selection.moveLeft(mouse.x);
                myState.selection.moveUp(mouse.y);
            } else if (myState.enlargeDirection === Box.border.TOPRIGHT) {
                myState.selection.moveUp(mouse.y);
                myState.selection.moveRight(mouse.x);
            } else if (myState.dragging) {
                myState.selection.x = mouse.x - myState.dragoffx;
                myState.selection.y = mouse.y - myState.dragoffy;
            }
            myState.valid = false;
        }, true);

        /* Adds controls for moving boxes with arrow keys as well as deleting them with the 'd'.
            In the event no box is selected, we use the arrow keys to transition frames.*/
        html.addEventListener("keydown", function (event) {
            if (myState.selection != null) { /* Handles box. */
                switch (event.keyCode) {
                    case 38:  /* Arrow down. */
                        myState.selection.y -= 1;
                        break;
                    case 40:  /* Arrow up. */
                        myState.selection.y += 1;
                        break;
                    case 37:  /* Arrow left. */
                        myState.selection.x -= 1;
                        break;
                    case 39:  /* Arrow right. */
                        myState.selection.x += 1;
                        break;
                    case 68:  /* Deletes with 'd'. */
                        myState.removeBox(myState.selection);
                        myState.selection = null;
                        break;
                }
                myState.valid = false;
            } else if (event.keyCode === 37) { /* Moves left a frame. */
                if (myState.frame > 0) {
                    var frame = myState.frame--;
                    canvas.style.backgroundImage = frame_url(frame);
                }
            } else if (event.keyCode === 39) { /* Moves right a frame. */
                var frame = myState.frame++;
                canvas.style.backgroundImage = frame_url(frame);
            }
            myState.valid = false;

        }, true);

        /* Modifies state to prevent dragging. If the box is too small, delete it. */
        canvas.addEventListener('mouseup', function (e) {
            if (document.body.style.cursor === 'move') {
                document.body.style.cursor = 'default';
            }
            myState.move = false;
            myState.dragging = false;
            myState.enlargeDirection = false;
            for (var i = 0; i < myState.boxes.length; i++) {
                if (myState.boxes[i].w === 1 || myState.boxes[i].h === 1) {
                    myState.removeBox(myState.boxes[i]);
                    myState.selection = null;
                    myState.valid = false;
                }
            }
        }, true);

        /* Redraws canvas every interval seconds */
        setInterval(function () {
            myState.draw();
            if (myState.play) {
                myState.frame += 1;
            }
        }, myState.interval);
    }

    get frame() {return this._frame;}
    set frame(value) {
        value = parseInt(value);
        frame_preload(value);
        var frameNumberBox = document.getElementById("frame-number");
        document.getElementById("scroll-bar").value = value;
        frameNumberBox.setAttribute("value", value);
        this._frame = value;
        this.boxes = this.getBoxes(value);
        this.valid = false;
        document.getElementById("frame").style.backgroundImage = frame_url(this._frame);
        if (this.selection != null && this.selection.thing != null) {
            for (var box of this.boxes) {
                if (box.thing.id == this.selection.thing.id) {
                    this.selection = box;
                    break;
                }
            }
        }
    }

    /* Retrieves a list of box objects that belong in the current frame */
    getBoxes(frame) {
        var boxes = [];
        for (var thing of this.things) {
            var box = thing.getKeyframe(frame);
            if (box != null) {
                boxes.push(box);
            }
        }
        return boxes;
    }

    /**
     * Removes box from list of boxes and redraws it. Deletes the Thing if it has no more boxes
     * @param box The box to be removed.
     */
    removeBox(box) {
        var index = this.boxes.indexOf(box);
        this.boxes.splice(index, 1);
        if (box.thing.deleteKeyframe(box)) {
            let index = this.things.indexOf(box.thing);

            var element = document.getElementById(this.things.splice(index, 1)[0].id);
            element.parentNode.removeChild(element);
        }
        this.valid = false;
    }

    // Copied from http://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
    shadeColor(color, percent) {
        var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
        return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
    }

    /**
     * Draws the state of the canvas if it's invalid and updates key frames.
     */
    draw() {
        if (!this.valid) {
            var ctx = this.ctx;
            ctx.clearRect(0, 0, this.width, this.height);

            for (var i = 0; i < this.boxes.length; i++) {
                this.boxes[i].draw(ctx);
            }

            /* Draws border for selected box. */

            if (this.selection != null) {
                for (var box of this.boxes) {
                    if (box.thing === this.selection.thing) {
                        ctx.strokeStyle = this.shadeColor(this.selection.thing.fill, -20);
                        ctx.lineWidth = this.selectionWidth;
                        var mySel = this.selection;
                        ctx.strokeRect(mySel.x, mySel.y, mySel.w, mySel.h);

                        /* Handles corners. */
                        var fillSize = this.selectionCorner;
                        ctx.fillStyle = "#000000";
                        ctx.fillRect(mySel.x - 2, mySel.y - 2, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 3 + mySel.w, mySel.y - 2, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 2, mySel.y - 2 + mySel.h, fillSize, fillSize);
                        ctx.fillRect(mySel.x - 3 + mySel.w, mySel.y - 2 + mySel.h, fillSize, fillSize);
                    }
                }

            }

            /* Updates key frames */

            if (this.selection != this.previousSelection || (this.selection && this.selection.thing.keyFramesChanged)) {
                var myNode = document.getElementById("key-frames"); //Faster than setting innerHtml to null.
                while (myNode.firstChild) {
                    myNode.removeChild(myNode.firstChild);
                }
                for (var box of this.selection.thing.keyframes) {
                    var dot = document.createElement("div");
                    dot.className = "dots";
                    dot.style = `left: ${box.frame / this.numberOfFrames * this.scrollBarSize}px;`;
                    document.getElementById("key-frames").appendChild(dot);
                }
                this.selection.thing.keyFramesChanged = false;

            }
            this.valid = true;
        }
    }

    /**
     * Creates a modified x and y coordinate for a mouse event to allow for smooth transitioning.
     * This takes into consideration fixed borders and other objects within the page.
     * @param e
     * @returns {{x: (number|*), y: (number|*)}}
     */
    getMouse(e) {
        var element = this.canvas, offsetX = 0, offsetY = 0, mx, my;

        /* Compute the total offset. */
        if (element.offsetParent !== undefined) {
            do {
                offsetX += element.offsetLeft;
                offsetY += element.offsetTop;
            } while ((element = element.offsetParent));
        }

        /* Add padding and border style widths to offset. Also add the <html> offsets in case
         there's a fixed position bar. */
        offsetX += this.stylePaddingLeft + this.styleBorderLeft + this.htmlLeft;
        offsetY += this.stylePaddingTop + this.styleBorderTop + this.htmlTop;

        mx = e.pageX - offsetX;
        my = e.pageY - offsetY;

        return {x: mx, y: my};
    }
}
<|endoftext|>


<commit_before>/** @jsx React.DOM */

var MyDescription = React.createClass({
  render: function() {
    var tag="Make data pop!";
    var desc = "This series of events focuses on human factors in data science.";
    return (

                {tag}

                {desc}

                 Schedule

            Every Monday evening, 7-9pm.

            Check the NYC Python Calendar for meeting locations.

    );
  }
});

var MySchedule = React.createClass({
  getInitialState: function() {
    return {data1: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data1: data.items});
      }.bind(this)
    });
  },

  render: function() {
    var n = this.state.data1.length;

    var scheduleNodes = this.state.data1.map(function(item, i) {
      if (i == n-1){
        return (

                  {moment(item.date).fromNow()} - "{item.topic}"

        );
      }
    });

    return (

            {scheduleNodes}

    );
  }
});

React.renderComponent(
  <MyDescription />,
  document.getElementById('description')
);

React.renderComponent(
  <MySchedule url="schedule.json"  />,
  document.getElementById('schedule')
);

<commit_msg>wordin<commit_after>/** @jsx React.DOM */

var MyDescription = React.createClass({
  render: function() {
    var tag="Make data pop!";
    var desc = "This series of events focuses on human factors in data science.";
    return (

                {tag}

                {desc}

                 Schedule

            Check the NYC Python Calendar for meeting locations.

    );
  }
});

var MySchedule = React.createClass({
  getInitialState: function() {
    return {data1: []};
  },
  componentDidMount: function() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      success: function(data) {
        this.setState({data1: data.items});
      }.bind(this)
    });
  },

  render: function() {
    var n = this.state.data1.length;

    var scheduleNodes = this.state.data1.map(function(item, i) {
      if (i == n-1){
        return (

                  {moment(item.date).fromNow()} - "{item.topic}"

        );
      }
    });

    return (

            {scheduleNodes}

    );
  }
});

React.renderComponent(
  <MyDescription />,
  document.getElementById('description')
);

React.renderComponent(
  <MySchedule url="schedule.json"  />,
  document.getElementById('schedule')
);

<|endoftext|>