<commit_before>var phaxMachine = {
	faxLogs: {
		render: function() {
			$.getJSON("/logs.json", {}, function(response, status, jqXHR) {
				if (response.success) {
					phaxMachine.createAlert("success", response.message)
					phaxMachine.faxLogs.populateFaxLogsTable(response.data)
					phaxMachine.faxLogs.makeRecipientsClickable()
				}
				else {
					phaxMachine.createAlert("danger", response.message)
				}

				phaxMachine.faxLogs.hideLoadingRow()
			})
		},

		hideLoadingRow: function() {
			$("#loading-row").hide()
		},

		populateFaxLogsTable: function(faxesData) {
			var logAttributes = [
				"id", "num_pages", "cost", "direction", "status", "type",
				"requested_at", "recipients"
			]

			faxesData.forEach(function(faxData) {
				faxData = phaxMachine.faxLogs.processFaxData(faxData)

				var tableRow = $("

")

				var faxFileCell = $('	')
				tableRow.append(faxFileCell)

				logAttributes.forEach(function(logAttribute) {
					var cell = $("	")
					cell.data("fax-attribute", logAttribute)
					cell.append(faxData[logAttribute])
					tableRow.append(cell)
				})
				tableRow.data("recipients", faxData.recipientsData)
				$("#fax-log-table").find("tbody").append(tableRow)
			})
		},

		makeRecipientsClickable: function() {
			var recipientCells = $("#fax-log-table").find("td").filter(function() {
				return $(this).data("fax-attribute") === "recipients"
			})
			recipientCells.each(function() {
				var recipientCountStr = $(this).text()
				if (recipientCountStr === '-') {
					return
				}

				recipientCount = parseInt(recipientCountStr)
				recipientsDataLink = $("" + recipientCount + "")
				recipientsDataLink.on("click", phaxMachine.faxLogs.displayRecipientsDataModal)
				$(this).html(recipientsDataLink)
			})
		},

		displayRecipientsDataModal: function() {
			var tableRow = $(this).closest("tr")
			var recipientsData = tableRow.data("recipients")
			var modal = $("#recipientsDataModal")
			var recipientAttributes = [
				"bitrate", "completed_at", "number", "resolution", "status"
			]
			var recipientsTable = $("#recipientsDataTable")

			recipientsTable.find("tbody").find("tr").remove()

			recipientsData.forEach(function(recipientData) {
				var recipientRow = $("

")
				var recipientData = phaxMachine.faxLogs.processRecipientData(recipientData)

				recipientAttributes.forEach(function(recipientAttribute) {
					var cell = $("" + recipientData[recipientAttribute] + "	")
					recipientRow.append(cell)
				})

				recipientsTable.find("tbody").append(recipientRow)
			})
			modal.modal("show")
		},

		processRecipientData: function(recipientData) {
			return {
				"bitrate": recipientData.bitrate,
				"completed_at": phaxMachine.faxLogs.formatTimestamp(recipientData.completed_at),
				"number": recipientData.number,
				"resolution": recipientData.resolution,
				"status": phaxMachine.faxLogs.formatStatus(recipientData.status)
			}
		},

		processFaxData: function(faxData) {
			processedFaxData = {
				"id": faxData.id,
				"num_pages": faxData.num_pages,
				"cost": phaxMachine.faxLogs.formatMoney(faxData.cost),
				"direction": phaxMachine.faxLogs.capitalize(faxData.direction),
				"status": phaxMachine.faxLogs.formatStatus(faxData.status),
				"type": phaxMachine.faxLogs.formatType(faxData.is_test),
				"requested_at": phaxMachine.faxLogs.formatTimestamp(faxData.requested_at)
			}

			if (faxData.direction === 'sent') {
				processedFaxData.recipients = faxData.recipients.length
				processedFaxData.recipientsData = faxData.recipients
			}
			else {
				processedFaxData.recipients = '-'
			}

			return processedFaxData
		},

		formatMoney: function(valueInCents) {
			var valueString = String(valueInCents)
			while (valueString.length < 3)
				valueString = "0" + valueString
			var valueChars = valueString.split("")
			valueChars.splice(-2, 0, ".")
			return "$" + valueChars.join("")
		},

		capitalize: function(string) {
			chars = string.split('')
			chars[0] = chars[0].toUpperCase()
			return chars.join('')
		},

		formatStatus: function(statusStr) {
			var statusMap = {
				building:       ['Building',        'info'   ],
				pendingbatch:   ['Batching',        'info'   ],
				queued:         ['Queued',          'info'   ],
				inprogress:     ['In Progress',     'info'   ],
				ringing:        ['Ringing',         'info'   ],
				callactive:     ['Call Active',     'info'   ],
				success:        ['Success',         'success'],
				partialsuccess: ['Partial Success', 'warning'],
				willretry:      ['Will Retry',      'warning'],
				failure:        ['Failure',         'danger' ]
			}

			var [statusText, statusClass] = statusMap[statusStr]

			return "" + statusText + ""
		},

		formatType: function(isTest) {
			if (isTest) {
				return 'Test'
			}
			else {
				return 'Live'
			}
		},

		formatTimestamp: function(timeInSeconds) {
			// Value must be converted to milliseconds
			return String(new Date(timeInSeconds * 1000))
		}
	},

	sendFax: {
		render: function() {
			$("#sendFaxForm").on("submit", function(e) {
				$.ajax('/send', {
					type: 'POST',
					data: new FormData(this),
					cache: false,
					contentType: false,
					processData: false,
					xhr: function() {
						var jqXhr = $.ajaxSettings.xhr()

						if (jqXhr.upload) {
							jqXhr.upload.addEventListener('progress', function(e) {
								if (e.lengthComputable) {
									var percentStr = String(Math.floor(100 * (e.loaded / e.total)))
									$("#faxFileProgress").show().text(
										"Uploading file: " + percentStr + "%"
									)
								}
							},
							false)
						}

						return jqXhr
					}
				}).done(function(response, _textStatus, _jqXhr) {
					responseJson = JSON.parse(response)
					if (responseJson.success) {
						var message = "Fax #" + responseJson.faxId + " queued for sending."
						phaxMachine.createAlert("success", message)
					}
					else {
						phaxMachine.createAlert("danger", responseJson.message)
					}
				})

				e.preventDefault()
			})
		}
	},

	createAlert: function(type, message) {
		var alert = $("

")
		var closeAlertButton = $(
			'<button type="button" class="close" data-dismiss="alert" aria-label="Close">'
			+ '×'
			+ '</button>'
		)
		alert.append(closeAlertButton)
		alert.append(message)
		$("#alerts").append(alert)
	}
}

$(document).ready(function() {
	pageScript = phaxMachine[$("main").attr("id")]
	if (pageScript != null) {
		pageScript.render()
	}
})

<commit_msg>Get faxId for downloadFaxFile button<commit_after>var phaxMachine = {
	faxLogs: {
		render: function() {
			$.getJSON("/logs.json", {}, function(response, status, jqXHR) {
				if (response.success) {
					phaxMachine.createAlert("success", response.message)
					phaxMachine.faxLogs.populateFaxLogsTable(response.data)
					phaxMachine.faxLogs.makeRecipientsClickable()
				}
				else {
					phaxMachine.createAlert("danger", response.message)
				}

				phaxMachine.faxLogs.hideLoadingRow()
			})
		},

		hideLoadingRow: function() {
			$("#loading-row").hide()
		},

		populateFaxLogsTable: function(faxesData) {
			var logAttributes = [
				"id", "num_pages", "cost", "direction", "status", "type",
				"requested_at", "recipients"
			]

			faxesData.forEach(function(faxData) {
				faxData = phaxMachine.faxLogs.processFaxData(faxData)

				var tableRow = $("

")

				var faxFileCell = $('	')
				faxFileCell.on('click', phaxMachine.faxLogs.downloadFaxFile)
				tableRow.append(faxFileCell)

				logAttributes.forEach(function(logAttribute) {
					var cell = $("	")
					cell.data("fax-attribute", logAttribute)
					cell.append(faxData[logAttribute])
					tableRow.append(cell)
				})
				tableRow.data("recipients", faxData.recipientsData)
				$("#fax-log-table").find("tbody").append(tableRow)
			})
		},

		makeRecipientsClickable: function() {
			var recipientCells = $("#fax-log-table").find("td").filter(function() {
				return $(this).data("fax-attribute") === "recipients"
			})
			recipientCells.each(function() {
				var recipientCountStr = $(this).text()
				if (recipientCountStr === '-') {
					return
				}

				recipientCount = parseInt(recipientCountStr)
				recipientsDataLink = $("" + recipientCount + "")
				recipientsDataLink.on("click", phaxMachine.faxLogs.displayRecipientsDataModal)
				$(this).html(recipientsDataLink)
			})
		},

		downloadFaxFile: function() {
			var faxId = $($(this).closest('tr').find('td').filter(function() { return $(this).data('fax-attribute') == 'id' })[0]).text()
			console.log(faxId)
		},

		displayRecipientsDataModal: function() {
			var tableRow = $(this).closest("tr")
			var recipientsData = tableRow.data("recipients")
			var modal = $("#recipientsDataModal")
			var recipientAttributes = [
				"bitrate", "completed_at", "number", "resolution", "status"
			]
			var recipientsTable = $("#recipientsDataTable")

			recipientsTable.find("tbody").find("tr").remove()

			recipientsData.forEach(function(recipientData) {
				var recipientRow = $("

")
				var recipientData = phaxMachine.faxLogs.processRecipientData(recipientData)

				recipientAttributes.forEach(function(recipientAttribute) {
					var cell = $("" + recipientData[recipientAttribute] + "	")
					recipientRow.append(cell)
				})

				recipientsTable.find("tbody").append(recipientRow)
			})
			modal.modal("show")
		},

		processRecipientData: function(recipientData) {
			return {
				"bitrate": recipientData.bitrate,
				"completed_at": phaxMachine.faxLogs.formatTimestamp(recipientData.completed_at),
				"number": recipientData.number,
				"resolution": recipientData.resolution,
				"status": phaxMachine.faxLogs.formatStatus(recipientData.status)
			}
		},

		processFaxData: function(faxData) {
			processedFaxData = {
				"id": faxData.id,
				"num_pages": faxData.num_pages,
				"cost": phaxMachine.faxLogs.formatMoney(faxData.cost),
				"direction": phaxMachine.faxLogs.capitalize(faxData.direction),
				"status": phaxMachine.faxLogs.formatStatus(faxData.status),
				"type": phaxMachine.faxLogs.formatType(faxData.is_test),
				"requested_at": phaxMachine.faxLogs.formatTimestamp(faxData.requested_at)
			}

			if (faxData.direction === 'sent') {
				processedFaxData.recipients = faxData.recipients.length
				processedFaxData.recipientsData = faxData.recipients
			}
			else {
				processedFaxData.recipients = '-'
			}

			return processedFaxData
		},

		formatMoney: function(valueInCents) {
			var valueString = String(valueInCents)
			while (valueString.length < 3)
				valueString = "0" + valueString
			var valueChars = valueString.split("")
			valueChars.splice(-2, 0, ".")
			return "$" + valueChars.join("")
		},

		capitalize: function(string) {
			chars = string.split('')
			chars[0] = chars[0].toUpperCase()
			return chars.join('')
		},

		formatStatus: function(statusStr) {
			var statusMap = {
				building:       ['Building',        'info'   ],
				pendingbatch:   ['Batching',        'info'   ],
				queued:         ['Queued',          'info'   ],
				inprogress:     ['In Progress',     'info'   ],
				ringing:        ['Ringing',         'info'   ],
				callactive:     ['Call Active',     'info'   ],
				success:        ['Success',         'success'],
				partialsuccess: ['Partial Success', 'warning'],
				willretry:      ['Will Retry',      'warning'],
				failure:        ['Failure',         'danger' ]
			}

			var [statusText, statusClass] = statusMap[statusStr]

			return "" + statusText + ""
		},

		formatType: function(isTest) {
			if (isTest) {
				return 'Test'
			}
			else {
				return 'Live'
			}
		},

		formatTimestamp: function(timeInSeconds) {
			// Value must be converted to milliseconds
			return String(new Date(timeInSeconds * 1000))
		}
	},

	sendFax: {
		render: function() {
			$("#sendFaxForm").on("submit", function(e) {
				$.ajax('/send', {
					type: 'POST',
					data: new FormData(this),
					cache: false,
					contentType: false,
					processData: false,
					xhr: function() {
						var jqXhr = $.ajaxSettings.xhr()

						if (jqXhr.upload) {
							jqXhr.upload.addEventListener('progress', function(e) {
								if (e.lengthComputable) {
									var percentStr = String(Math.floor(100 * (e.loaded / e.total)))
									$("#faxFileProgress").show().text(
										"Uploading file: " + percentStr + "%"
									)
								}
							},
							false)
						}

						return jqXhr
					}
				}).done(function(response, _textStatus, _jqXhr) {
					responseJson = JSON.parse(response)
					if (responseJson.success) {
						var message = "Fax #" + responseJson.faxId + " queued for sending."
						phaxMachine.createAlert("success", message)
					}
					else {
						phaxMachine.createAlert("danger", responseJson.message)
					}
				})

				e.preventDefault()
			})
		}
	},

	createAlert: function(type, message) {
		var alert = $("

")
		var closeAlertButton = $(
			'<button type="button" class="close" data-dismiss="alert" aria-label="Close">'
			+ '×'
			+ '</button>'
		)
		alert.append(closeAlertButton)
		alert.append(message)
		$("#alerts").append(alert)
	}
}

$(document).ready(function() {
	pageScript = phaxMachine[$("main").attr("id")]
	if (pageScript != null) {
		pageScript.render()
	}
})

<|endoftext|>


<commit_before>'use strict';

//Start by defining the main module and adding the module dependencies
angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies);

// Setting HTML5 Location Mode
angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider',
	function($locationProvider) {
		//$locationProvider.hashPrefix('!');
		$locationProvider.html5Mode(true);
	}
])
.config(function($sceDelegateProvider) {
	  $sceDelegateProvider.resourceUrlWhitelist([
		   // Allow same origin resource loads.
		   'self',
		   // Allow loading from our assets domain.  Notice the difference between * and **.
		   'http://scontent*.cdninstagram.com/**',
		   'https://scontent*.cdninstagram.com/**',
		   'http://d1cda9fzfodqce.cloudfront.net/**',
		   'https://d1cda9fzfodqce.cloudfront.net/**'
 ]);
});

//Then define the init function for starting up the application
angular.element(document).ready(function() {
	//Fixing facebook bug with redirect
	//if (window.location.hash === '#_=_') window.location.hash = '#!';

	//Then init the app
	angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]);
});
<commit_msg>allow ng-ing URIs from s3<commit_after>'use strict';

//Start by defining the main module and adding the module dependencies
angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies);

// Setting HTML5 Location Mode
angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider',
	function($locationProvider) {
		//$locationProvider.hashPrefix('!');
		$locationProvider.html5Mode(true);
	}
])
.config(function($sceDelegateProvider) {
	  $sceDelegateProvider.resourceUrlWhitelist([
		   // Allow same origin resource loads.
		   'self',
		   // Allow loading from our assets domain.  Notice the difference between * and **.
		   'http://scontent*.cdninstagram.com/**',
		   'https://scontent*.cdninstagram.com/**',
		   'http://d1cda9fzfodqce.cloudfront.net/**',
		   'https://d1cda9fzfodqce.cloudfront.net/**',
		   'http://s3.amazonaws.com/journeh.dew/**',
		   'https://s3.amazonaws.com/journeh.dew/**',
		   'http://s3.amazonaws.com/journeh/**',
		   'https://s3.amazonaws.com/journeh/**'
 ]);
});

//Then define the init function for starting up the application
angular.element(document).ready(function() {
	//Fixing facebook bug with redirect
	//if (window.location.hash === '#_=_') window.location.hash = '#!';

	//Then init the app
	angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]);
});
<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

/**
 * @author Michael Kauzmann
 */
define( require => {
  'use strict';

  // modules
  const FocusHighlightFromNode = require( 'SCENERY/accessibility/FocusHighlightFromNode' );
  const FocusHighlightPath = require( 'SCENERY/accessibility/FocusHighlightPath' );
  const GrabReleaseCueNode = require( 'SCENERY_PHET/accessibility/nodes/GrabReleaseCueNode' );
  const KeyboardUtil = require( 'SCENERY/accessibility/KeyboardUtil' );
  const Node = require( 'SCENERY/nodes/Node' );
  const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  const SceneryPhetA11yStrings = require( 'SCENERY_PHET/SceneryPhetA11yStrings' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Tandem = require( 'TANDEM/Tandem' );
  const utteranceQueue = require( 'SCENERY_PHET/accessibility/utteranceQueue' );

  // a11y strings
  const grabPatternString = SceneryPhetA11yStrings.grabPattern.value;
  const defaultThingToGrabString = SceneryPhetA11yStrings.defaultThingToGrab.value;
  const releasedString = SceneryPhetA11yStrings.released.value;

  /**
   * NOTE: if passing inthis class assumes
   * @param {Node} parentButton - Node passed in that will be mutated with a11y options to have the grab functionality in the PDOM
   * @param  {Object} options
   */
  class A11yGrabDragNode extends Node {
    constructor( parentButton, options ) {
      super();

      options = _.extend( {
        cursor: 'pointer',

        // A string that is filled in to the appropriate button label
        thingToGrab: defaultThingToGrabString,

        // {function} - called when the node is "grabbed" (when the grab button fires)
        onGrab: _.noop(),

        // {function} - if you override this, make sure to handle the alert in the default onRelease
        onRelease: A11yGrabDragNode.onRelease,

        // TODO: these are really just a11y options for "parentButton"
        // {Object} - Node options passed to the actually <button> created for the PDOM, filled in below
        grabButtonOptions: {},

        // TODO: these are really just a11y options for "this" https://github.com/phetsims/scenery-phet/issues/421
        // {Object} - Options for the child Node that will be the draggable component in the PDOM. This gets a11y
        // related draggable listeners and such.
        a11yDraggableNodeOptions: {},

        // TODO: why support this? shouldn't we just attach this to the focusHighlight passed in https://github.com/phetsims/scenery-phet/issues/421
        // // {null|Node} -  additional cueing node who's visibility can be toggled.
        supplementaryCueNode: null,

        // {Object} - to pass in options to the cue
        grabCueOptions: {},

        // {number} - the number of times a user has to successfully grab the object before hiding the cue.
        grabsToCue: 1,

        // not passed to this node, but instead to the grabButton
        tandem: Tandem.required,

        // a11y - this node will act as a container for more accessible content, its children will implement
        // most of the keyboard navigation
        tagName: 'div'
      }, options );

      assert && assert( typeof options.onGrab === 'function' );
      assert && assert( typeof options.onRelease === 'function' );

      if ( parentButton.focusHighlight ) {
        assert && assert( parentButton.focusHighlight instanceof phet.scenery.FocusHighlightPath,
          'if provided, focusHighlight must be a Path' );
      }

      assert && assert( typeof options.grabsToCue === 'number' );

      assert && assert( typeof options.a11yDraggableNodeOptions === 'object' );
      assert && assert( typeof options.grabCueOptions === 'object' );

      assert && assert( options.grabCueOptions.visible === undefined, 'Should not set visibility of the cue node' );
      if ( options.supplementaryCueNode !== null ) {
        assert && assert( options.supplementaryCueNode instanceof Node );
        assert && assert( !options.supplementaryCueNode.parent, 'A11yGrabDragNode adds supplementaryCueNode to focusHighlight' );
        assert && assert( options.supplementaryCueNode.visible === true, 'supplementaryCueNode should be visible' );
      }

      // TODO: do we need this and options?
      options.a11yDraggableNodeOptions = _.extend( {

        // a11y
        tagName: 'div',
        ariaRole: 'application',
        focusable: true
      }, options.a11yDraggableNodeOptions );

      // TODO - maybe we don't need this to be an option https://github.com/phetsims/scenery-phet/issues/421
      // TODO - provide a way to not do this if we don't want to (though do it by default).
      // TODO: Likely if we don't have grabButtonOPtions, at the very least we will want to assert that options
      // TODO: aren't trying to set these
      options.grabButtonOptions = _.extend( {

        // a11y
        containerTagName: 'div',
        tagName: 'button',
        focusHighlightLayerable: true
      }, options.grabButtonOptions );

      assert && assert( !options.grabButtonOptions.innerContent, 'A11yGrabDragNode sets its own innerContent, see thingToGrab' );

      // @private
      this.numberOfGrabs = 0; // {number}
      this.supplementaryCueNode = options.supplementaryCueNode; // {Node|null}

      options.grabButtonOptions.innerContent = StringUtils.fillIn( grabPatternString, {
        thingToGrab: options.thingToGrab
      } );

      // Add options to draggable node to make it look like a button
      parentButton.mutate( options.grabButtonOptions );

      // @private
      this.grabCueNode = new GrabReleaseCueNode( options.grabCueOptions );

      this.mutate( options.a11yDraggableNodeOptions );

      // by default should be hidden until "grabbed" (grab button is pressed)
      this.accessibleVisible = false;

      // Update the passed in node's focusHighlight to make it "dashed"
      let parentButtonFocusHighlight = parentButton.focusHighlight;
      if ( !parentButtonFocusHighlight ) {
        parentButtonFocusHighlight = new FocusHighlightFromNode( parentButton );
      }
      parentButton.focusHighlight = parentButtonFocusHighlight;

      // Make the grab button's focusHighlight in the spitting image of the parentButton's
      const childDraggableFocusHighlight = new FocusHighlightPath( parentButtonFocusHighlight.shape, {
        visible: false
      } );
      childDraggableFocusHighlight.makeDashed();
      this.focusHighlight = childDraggableFocusHighlight;

      // if ever we update the parentButton's focusHighlight, then update the grab button's too to keep in syn.
      let onHighlightChange = () => {
        childDraggableFocusHighlight.setShape( parentButtonFocusHighlight.shape );
      };
      parentButton.focusHighlight.highlightChangedEmitter.addListener( onHighlightChange );

      // TODO: Likely we will need to monitor the parent for changes, and update accordingly, though for now it works in
      // TODO: friction, see https://github.com/phetsims/scenery-phet/issues/421
      this.grabCueNode.prependMatrix( parentButton.getMatrix() );
      parentButton.focusHighlight.addChild( this.grabCueNode );
      if ( this.supplementaryCueNode ) {
        this.supplementaryCueNode.prependMatrix( parentButton.getMatrix() );
        parentButton.focusHighlight.addChild( this.supplementaryCueNode );
      }

      // some keypresses can fire the parentButton's click (the grab button) from the same press that fires the event below, so guard against that.
      let guardKeyPressFromDraggable = false;

      // when the "Grab {{thing}}" button is pressed, focus the draggable node and set to dragged state
      const grabButtonListener = {
        click: () => {

          // if the draggable was just released, don't pick it up again until the next click event so we don't "loop"
          // and pick it up immediately again.
          if ( !guardKeyPressFromDraggable ) {

            this.numberOfGrabs++;

            options.onGrab();
            this.accessibleVisible = true;

            // TODO: so hacky!!!! https://github.com/phetsims/scenery-phet/issues/421
            if ( this.focusHighlightLayerable &&
                 !parentButtonFocusHighlight.parent.hasChild( childDraggableFocusHighlight ) ) {
              assert && assert( parentButtonFocusHighlight.parent, 'how can we have focusHighlightLayerable with a ' +
                                                                   'node that is not in the scene graph?' );
              parentButtonFocusHighlight.parent.addChild( childDraggableFocusHighlight );
            }
            this.focus();
          }

          // "grab" the draggable on the next click event
          guardKeyPressFromDraggable = false;
        },

        blur: () => {
          if ( this.numberOfGrabs >= options.grabsToCue ) {

            this.grabCueNode.visible = false;
            if ( this.supplementaryCueNode ) {
              this.supplementaryCueNode.visible = false;
            }
          }
        }
      };
      parentButton.addAccessibleInputListener( grabButtonListener );

      // Release the balloon after an accessible interaction, resetting  model Properties, returning focus
      // to the "grab" button, and hiding the draggable balloon.
      const a11yReleaseWrappedNode = () => {

        // set a guard that will make sure that the click doesn't inappropriately bubble up to the parent listener
        // (likely that is the parentButton)
        // NOTE: we need this for spacebar also when "this" node is added as a child of the `parentButton`
        guardKeyPressFromDraggable = true;

        // focus the grab button again
        parentButton.focus();

        // the draggable node should no longer be discoverable in the parallel DOM
        this.accessibleVisible = false;

        // reset the key state of the drag handler by interrupting the drag
        this.interruptInput();

        // callback when node is "released"
        options.onRelease();
      };

      this.addAccessibleInputListener( {

        // Release the balloon on 'enter' key, tracking that we have released the balloon with this key so that
        // we don't immediately catch the 'click' event while the enter key is down on the button
        keydown: ( event ) => {
          if ( event.keyCode === KeyboardUtil.KEY_ENTER ) {
            a11yReleaseWrappedNode();
          }
        },
        keyup: ( event ) => {

          // Release  on keyup of spacebar so that we don't pick up the balloon again when we release the spacebar
          // and trigger a click event - escape could be added to either keyup or keydown listeners
          if ( event.keyCode === KeyboardUtil.KEY_SPACE || event.keyCode === KeyboardUtil.KEY_ESCAPE ) {
            a11yReleaseWrappedNode();
          }
        },

        // arrow function for this
        blur: () => {
          // No need to interrupt the KeyboardDragHandler, accessibilityInputListeners are already interrupted on blur

          // the draggable node should no longer be focusable
          this.accessibleVisible = false;

        }
      } );

      // TODO: Handle what is best here, I think we may want to move button logic from the draggableNode an to "this" (A11yGrabDragNode) https://github.com/phetsims/scenery-phet/issues/421
      // pull the this out of the parentButton's children so that they are on the same level of the PDOM.
      // parentButton.accessibleOrder = [ parentButton, this ];

      this.disposeA11yGrabDragNode = () => {

        parentButton.removeAccessibleInputListener( grabButtonListener );
        parentButton.focusHighlight.highlightChangedEmitter.removeListener( onHighlightChange );

        if ( this.focusHighlightLayerable ) {
          assert && assert( parentButtonFocusHighlight.parent, 'how can we have focusHighlightLayerable with a ' +
                                                               'node that is not in the scene graph?' );
          parentButtonFocusHighlight.parent.removeChild( childDraggableFocusHighlight );
        }

        parentButton.focusHighlight.removeChild( this.grabCueNode );
        this.supplementaryCueNode && parentButton.focusHighlight.removeChild( this.supplementaryCueNode );
      };

      this.mutate( options );
    }

    /**
     * @public
     */
    static onRelease() {
      utteranceQueue.addToBack( releasedString );
    }

    /**
     * @override
     * @public
     */
    dispose() {
      this.disposeA11yGrabDragNode();
      super.dispose();
    }

    /**
     * Reset to initial state
     * @public
     */
    reset() {
      this.numberOfGrabs = 0;

      this.grabCueNode.visible = true;
      if ( this.supplementaryCueNode ) {
        this.supplementaryCueNode.visible = true;
      }
    }
  }

  return sceneryPhet.register( 'A11yGrabDragNode', A11yGrabDragNode );

} );
<commit_msg>added dragCueNode to common code, removed a11yDraggableNodeOptions, added magnifierNode dragCueNode, reorganizing and renaming, https://github.com/phetsims/friction/issues/142 https://github.com/phetsims/scenery-phet/issues/421<commit_after>// Copyright 2018, University of Colorado Boulder

/**
 * @author Michael Kauzmann
 */
define( require => {
  'use strict';

  // modules
  const FocusHighlightFromNode = require( 'SCENERY/accessibility/FocusHighlightFromNode' );
  const FocusHighlightPath = require( 'SCENERY/accessibility/FocusHighlightPath' );
  const GrabReleaseCueNode = require( 'SCENERY_PHET/accessibility/nodes/GrabReleaseCueNode' );
  const KeyboardUtil = require( 'SCENERY/accessibility/KeyboardUtil' );
  const Node = require( 'SCENERY/nodes/Node' );
  const sceneryPhet = require( 'SCENERY_PHET/sceneryPhet' );
  const SceneryPhetA11yStrings = require( 'SCENERY_PHET/SceneryPhetA11yStrings' );
  const StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  const Tandem = require( 'TANDEM/Tandem' );
  const utteranceQueue = require( 'SCENERY_PHET/accessibility/utteranceQueue' );

  // a11y strings
  const grabPatternString = SceneryPhetA11yStrings.grabPattern.value;
  const defaultThingToGrabString = SceneryPhetA11yStrings.defaultThingToGrab.value;
  const releasedString = SceneryPhetA11yStrings.released.value;

  /**
   * NOTE: if passing inthis class assumes
   * @param {Node} parentButton - Node passed in that will be mutated with a11y options to have the grab functionality in the PDOM
   * @param  {Object} options
   */
  class A11yGrabDragNode extends Node {
    constructor( parentButton, options ) {

      options = _.extend( {
        cursor: 'pointer',

        // A string that is filled in to the appropriate button label
        thingToGrab: defaultThingToGrabString,

        // {function} - called when the node is "grabbed" (when the grab button fires)
        onGrab: _.noop(),

        // {function} - if you override this, make sure to handle the alert in the default onRelease
        onRelease: A11yGrabDragNode.onRelease,

        // TODO: these are really just a11y options for "parentButton"
        // {Object} - Node options passed to the actually <button> created for the PDOM, filled in below
        grabButtonOptions: {},

        // // {null|Node} - Optional node to cue the drag interaction once successfully updated.
        dragCueNode: null,

        // {function} - returns {boolean}, whether or not there has been a successful drag interaction,
        //              thus determining whether or not to show the dragCueNode.
        successfulDrag: _.noop,

        // {Object} - to pass in options to the cue
        grabCueOptions: {},

        // {number} - the number of times a user has to successfully grab the object before hiding the cue.
        grabsToCue: 1,

        // not passed to this node, but instead to the grabButton
        tandem: Tandem.required,

        // general a11y options
        // a11y - this node will act as a container for more accessible content, its children will implement
        // most of the keyboard navigation
        tagName: 'div',
        ariaRole: 'application',
        focusable: true
      }, options );

      if ( parentButton.focusHighlight ) {
        assert && assert( parentButton.focusHighlight instanceof phet.scenery.FocusHighlightPath,
          'if provided, focusHighlight must be a Path' );
      }
      assert && assert( typeof options.onGrab === 'function' );
      assert && assert( typeof options.onRelease === 'function' );
      assert && assert( typeof options.grabsToCue === 'number' );
      assert && assert( typeof options.grabCueOptions === 'object' );
      assert && assert( options.grabCueOptions.visible === undefined, 'Should not set visibility of the cue node' );
      if ( options.dragCueNode !== null ) {
        assert && assert( options.dragCueNode instanceof Node );
        assert && assert( !options.dragCueNode.parent, 'A11yGrabDragNode adds dragCueNode to focusHighlight' );
        assert && assert( options.dragCueNode.visible === true, 'dragCueNode should be visible to begin with' );
      }

      // TODO - maybe we don't need this to be an option https://github.com/phetsims/scenery-phet/issues/421
      // TODO - provide a way to not do this if we don't want to (though do it by default).
      // TODO: Likely if we don't have grabButtonOPtions, at the very least we will want to assert that options
      // TODO: aren't trying to set these
      options.grabButtonOptions = _.extend( {

        // a11y
        containerTagName: 'div',
        tagName: 'button',
        focusHighlightLayerable: true // TODO: is this really true? I don't think so
      }, options.grabButtonOptions );

      assert && assert( !options.grabButtonOptions.innerContent, 'A11yGrabDragNode sets its own innerContent, see thingToGrab' );

      options.grabButtonOptions.innerContent = StringUtils.fillIn( grabPatternString, {
        thingToGrab: options.thingToGrab
      } );

      // Add options to draggable node to make it look like a button
      parentButton.mutate( options.grabButtonOptions );

      super( options );

      // @private
      this.numberOfGrabs = 0; // {number}
      this.dragCueNode = options.dragCueNode; // {Node|null}
      this.grabCueNode = new GrabReleaseCueNode( options.grabCueOptions );

      // by default should be hidden until "grabbed" (grab button is pressed)
      this.accessibleVisible = false;

      // Update the passed in node's focusHighlight to make it "dashed"
      let parentButtonFocusHighlight = parentButton.focusHighlight;
      if ( !parentButtonFocusHighlight ) {
        parentButtonFocusHighlight = new FocusHighlightFromNode( parentButton );
      }
      parentButton.focusHighlight = parentButtonFocusHighlight;

      // Make the grab button's focusHighlight in the spitting image of the parentButton's
      const childDraggableFocusHighlight = new FocusHighlightPath( parentButtonFocusHighlight.shape, {
        visible: false
      } );
      childDraggableFocusHighlight.makeDashed();
      this.focusHighlight = childDraggableFocusHighlight;

      // if ever we update the parentButton's focusHighlight, then update the grab button's too to keep in syn.
      let onHighlightChange = () => {
        childDraggableFocusHighlight.setShape( parentButtonFocusHighlight.shape );
      };
      parentButton.focusHighlight.highlightChangedEmitter.addListener( onHighlightChange );

      // TODO: Likely we will need to monitor the parent for changes, and update accordingly, though for now it works in
      // TODO: friction, see https://github.com/phetsims/scenery-phet/issues/421
      this.grabCueNode.prependMatrix( parentButton.getMatrix() );
      parentButton.focusHighlight.addChild( this.grabCueNode );
      if ( this.dragCueNode ) {
        this.dragCueNode.prependMatrix( parentButton.getMatrix() );
        childDraggableFocusHighlight.addChild( this.dragCueNode );
      }

      // some keypresses can fire the parentButton's click (the grab button) from the same press that fires the event below, so guard against that.
      let guardKeyPressFromDraggable = false;

      // when the "Grab {{thing}}" button is pressed, focus the draggable node and set to dragged state
      const grabButtonListener = {
        click: () => {

          // if the draggable was just released, don't pick it up again until the next click event so we don't "loop"
          // and pick it up immediately again.
          if ( !guardKeyPressFromDraggable ) {

            this.numberOfGrabs++;

            options.onGrab();
            this.accessibleVisible = true;

            // TODO: so hacky!!!! https://github.com/phetsims/scenery-phet/issues/421
            if ( this.focusHighlightLayerable &&
                 !parentButtonFocusHighlight.parent.hasChild( childDraggableFocusHighlight ) ) {
              assert && assert( parentButtonFocusHighlight.parent, 'how can we have focusHighlightLayerable with a ' +
                                                                   'node that is not in the scene graph?' );
              parentButtonFocusHighlight.parent.addChild( childDraggableFocusHighlight );
            }
            this.focus();
          }

          // "grab" the draggable on the next click event
          guardKeyPressFromDraggable = false;
        },

        blur: () => {
          if ( this.numberOfGrabs >= options.grabsToCue ) {
            this.grabCueNode.visible = false;
          }
        }
      };
      parentButton.addAccessibleInputListener( grabButtonListener );

      // Release the balloon after an accessible interaction, resetting  model Properties, returning focus
      // to the "grab" button, and hiding the draggable balloon.
      const a11yReleaseWrappedNode = () => {

        // set a guard that will make sure that the click doesn't inappropriately bubble up to the parent listener
        // (likely that is the parentButton)
        // NOTE: we need this for spacebar also when "this" node is added as a child of the `parentButton`
        guardKeyPressFromDraggable = true;

        // focus the grab button again
        parentButton.focus();

        // the draggable node should no longer be discoverable in the parallel DOM
        this.accessibleVisible = false;

        // reset the key state of the drag handler by interrupting the drag
        this.interruptInput();

        // callback when node is "released"
        options.onRelease();
      };

      this.addAccessibleInputListener( {

        // Release the balloon on 'enter' key, tracking that we have released the balloon with this key so that
        // we don't immediately catch the 'click' event while the enter key is down on the button
        keydown: ( event ) => {
          if ( event.keyCode === KeyboardUtil.KEY_ENTER ) {
            a11yReleaseWrappedNode();
          }
        },
        keyup: ( event ) => {

          // Release  on keyup of spacebar so that we don't pick up the balloon again when we release the spacebar
          // and trigger a click event - escape could be added to either keyup or keydown listeners
          if ( event.keyCode === KeyboardUtil.KEY_SPACE || event.keyCode === KeyboardUtil.KEY_ESCAPE ) {
            a11yReleaseWrappedNode();
          }

          // if successfully dragged, then make the cue node invisible
          if ( this.dragCueNode && options.successfulDrag() ) {
            this.dragCueNode.visible = false;
          }
        },

        // arrow function for this
        blur: () => {
          // No need to interrupt the KeyboardDragHandler, accessibilityInputListeners are already interrupted on blur

          // the draggable node should no longer be focusable
          this.accessibleVisible = false;

        },
        focus: () => {

          // if successfully dragged, then make the cue node invisible
          if ( this.dragCueNode && options.successfulDrag() ) {
            this.dragCueNode.visible = false;
          }
        }
      } );

      // TODO: Handle what is best here, I think we may want to move button logic from the draggableNode an to "this" (A11yGrabDragNode) https://github.com/phetsims/scenery-phet/issues/421
      // pull the this out of the parentButton's children so that they are on the same level of the PDOM.
      // parentButton.accessibleOrder = [ parentButton, this ];

      this.disposeA11yGrabDragNode = () => {

        parentButton.removeAccessibleInputListener( grabButtonListener );
        parentButton.focusHighlight.highlightChangedEmitter.removeListener( onHighlightChange );

        if ( this.focusHighlightLayerable ) {
          assert && assert( parentButtonFocusHighlight.parent, 'how can we have focusHighlightLayerable with a ' +
                                                               'node that is not in the scene graph?' );
          parentButtonFocusHighlight.parent.removeChild( childDraggableFocusHighlight );
        }

        parentButton.focusHighlight.removeChild( this.grabCueNode );
        this.dragCueNode && childDraggableFocusHighlight.focusHighlight.removeChild( this.dragCueNode );
      };
    }

    /**
     * @public
     */
    static onRelease() {
      utteranceQueue.addToBack( releasedString );
    }

    /**
     * @override
     * @public
     */
    dispose() {
      this.disposeA11yGrabDragNode();
      super.dispose();
    }

    /**
     * Reset to initial state
     * @public
     */
    reset() {
      this.numberOfGrabs = 0;

      this.grabCueNode.visible = true;
      if ( this.dragCueNode ) {
        this.dragCueNode.visible = true;
      }
    }
  }

  return sceneryPhet.register( 'A11yGrabDragNode', A11yGrabDragNode );

} );
<|endoftext|>


<commit_before>import React, { PureComponent } from "react"; // eslint-disable-line

// Numeric text fields DEFAULT value is not
// the same thing as MAX_SCORE_DEFAULT.
const DEFAULT_NUMERIC_INPUT_VALUE = 0;
const DEFAULT_NUMERIC_MIN_VALUE = 0;

class NumericTextField extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      showOnlyNeedReview: true,
      showFeedbackPanel: false,
      value: 0,
    };
    this.updateValue = this.updateValue.bind(this);
    this.setValue = this.setValue.bind(this);
  }

  componentDidMount() {
    const {value} = this.props;
    this.setState({value: value});
  }

  updateValue(event) {
    this.setState({value: event.target.value});
  }

  setValue(event) {
    const {min, onChange} = this.props;
    const numericValue = parseInt(this.state.value, 10);
    const lastValue = this.props.value;

    const defaultValue = typeof this.props.default === "undefined"
      ? DEFAULT_NUMERIC_INPUT_VALUE
      : this.props.default;

    const minValue = typeof min === "undefined"
      ? DEFAULT_NUMERIC_MIN_VALUE
      : min;

    let newValue = isNaN(numericValue)
      ? defaultValue
      : numericValue;

    newValue = newValue >= minValue
      ? newValue
      : defaultValue;

    this.setState({value: newValue});
    if ((newValue !== lastValue) && onChange) {
      onChange(newValue);
    }
  }

  render() {
    const { className, disabled } = this.props;
    const { value } = this.state;
    return (
      <input
        className={className}
        value={value}
        onChange={this.updateValue}
        onBlur={this.setValue}
        disabled={disabled}
      />
    );
  }
}

export default NumericTextField;
<commit_msg>fix showing the autoscore value to the teacher<commit_after>import React, { PureComponent } from "react"; // eslint-disable-line

// Numeric text fields DEFAULT value is not
// the same thing as MAX_SCORE_DEFAULT.
const DEFAULT_NUMERIC_INPUT_VALUE = 0;
const DEFAULT_NUMERIC_MIN_VALUE = 0;

class NumericTextField extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      showOnlyNeedReview: true,
      showFeedbackPanel: false,
      value: 0,
    };
    this.updateValue = this.updateValue.bind(this);
    this.setValue = this.setValue.bind(this);
  }

  componentDidMount() {
    const {value} = this.props;
    this.setState({value: value});
  }

  updateValue(event) {
    this.setState({value: event.target.value});
  }

  setValue(event) {
    const {min, onChange} = this.props;
    const numericValue = parseInt(this.state.value, 10);
    const lastValue = this.props.value;

    const defaultValue = typeof this.props.default === "undefined"
      ? DEFAULT_NUMERIC_INPUT_VALUE
      : this.props.default;

    const minValue = typeof min === "undefined"
      ? DEFAULT_NUMERIC_MIN_VALUE
      : min;

    let newValue = isNaN(numericValue)
      ? defaultValue
      : numericValue;

    newValue = newValue >= minValue
      ? newValue
      : defaultValue;

    this.setState({value: newValue});
    if ((newValue !== lastValue) && onChange) {
      onChange(newValue);
    }
  }

  render() {
    const { className, disabled } = this.props;
    // If the parent component changes the props.value after the component is mounted
    // this change isn't reflected in the UI.
    // I think this use case only happens when the parent component also disables the
    // NumericTextField, so the code below fixes it by ignoring the state.value.
    //
    // However, this doesnt seem like the right way to solve the problem. This component
    // is trying to be an input and output at the same time. And in most cases the parent
    // is actually managing the state. So it might be better to stop using state.value
    // all together.
    //
    // It might not be useful, but this post talks about something of a similiar scenario:
    // https://stackoverflow.com/a/49868300/3195497
    // However the main use case of mixing state and props seems to be when computing an
    // expensive state value. That isn't the case for us.
    const value = disabled ? this.props.value : this.state.value;
    return (
      <input
        className={className}
        value={value}
        onChange={this.updateValue}
        onBlur={this.setValue}
        disabled={disabled}
      />
    );
  }
}

export default NumericTextField;
<|endoftext|>


<commit_before>// Copyright 2016, University of Colorado Boulder

/**
 * Node representing the beaker heater view.
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // modules
  var BeakerHeater = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/BeakerHeater' );
  var BeakerView = require( 'ENERGY_FORMS_AND_CHANGES/common/view/BeakerView' );
  var EFACBaseNode = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/view/EFACBaseNode' );
  var EFACModelImageNode = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/view/EFACModelImageNode' );
  var EnergyChunkLayer = require( 'ENERGY_FORMS_AND_CHANGES/common/view/EnergyChunkLayer' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var ThermometerNode = require( 'SCENERY_PHET/ThermometerNode' );

  var DEBUG = false;

  /**
   * @param {BeakerHeater} beakerHeater
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @param {ModelViewTransform2} modelViewTransform
   * @constructor
   */
  function BeakerHeaterNode( beakerHeater, energyChunksVisibleProperty, modelViewTransform ) {

    EFACBaseNode.call( this, beakerHeater, modelViewTransform );

    var energizedCoil = new EFACModelImageNode( BeakerHeater.HEATER_ELEMENT_ON_IMAGE, modelViewTransform );

    // Add the images that are used to depict this element along with the
    // layer that will contain the energy chunks.
    this.addChild( new EFACModelImageNode( BeakerHeater.WIRE_STRAIGHT_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.WIRE_CURVE_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.ELEMENT_BASE_BACK_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.HEATER_ELEMENT_OFF_IMAGE, modelViewTransform ) );
    this.addChild( energizedCoil );
    this.addChild( new EnergyChunkLayer( beakerHeater.energyChunkList, beakerHeater.positionProperty, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.ELEMENT_BASE_FRONT_IMAGE, modelViewTransform ) );

    var thermometerNode = new ThermometerNode( 0, 1, beakerHeater.heatProportionProperty, {} );
    thermometerNode.translate( 150, -120 );
    this.addChild( thermometerNode );

    // Add the beaker.  A compensating MVT is needed because the beaker
    // node is being added as a child of this node, but wants to set its
    // own offset in model space.
    var scale = modelViewTransform.matrix.scaleVector;
    var offset = modelViewTransform.modelToViewDelta( beakerHeater.positionProperty.value ).negated();
    var beakerMvt = ModelViewTransform2.createOffsetXYScaleMapping( offset, scale.x, -scale.y );

    this.beakerHeater = beakerHeater;

    this.beakerView = new BeakerView( beakerHeater.beaker, energyChunksVisibleProperty, beakerMvt );

    this.addChild( this.beakerView.backNode );
    this.addChild( new EnergyChunkLayer( beakerHeater.radiatedEnergyChunkList, beakerHeater.beaker.positionProperty,
      modelViewTransform ) );
    this.addChild( this.beakerView.frontNode );

    if ( DEBUG ) {
      var Rectangle = require( 'SCENERY/nodes/Rectangle' );
      var sb = beakerMvt.modelToViewBounds( beakerHeater.beaker.getSliceBounds() );
      var bb = beakerMvt.modelToViewBounds( beakerHeater.beaker.getBounds() );
      var rect = beakerMvt.modelToViewShape( beakerHeater.beaker.getRect() );
      this.addChild( new Rectangle( sb.x, sb.y, sb.width, sb.height, { stroke: 'blue' } ) ); // slice bounds
      this.addChild( new Rectangle( bb.x, bb.y, bb.width, bb.height, { stroke: 'red', lineWidth: 4 } ) ); // beaker bounds
      this.addChild( new Rectangle( rect.x, rect.y, rect.width, rect.height, { stroke: 'lime' } ) ); // from getRect()

      // var beakerXY = modelViewTransform.modelToViewPosition(beakerHeater.beaker.position);
      var beakerXY = beakerMvt.modelToViewPosition( beakerHeater.beaker.position );
      var Circle = require( 'SCENERY/nodes/Circle' );
      this.addChild( new Circle( 10, {
        centerX: beakerXY.x,
        centerY: beakerXY.y,
        fill: 'lime'
      } ) );
    }
    // Update the transparency of the hot element to make the dark element
    // appear to heat up.
    beakerHeater.heatProportionProperty.link( function( litProportion ) {
      energizedCoil.opacity = litProportion;
    } );

  }

  energyFormsAndChanges.register( 'BeakerHeaterNode', BeakerHeaterNode );

  return inherit( EFACBaseNode, BeakerHeaterNode );
} );

<commit_msg>code cleanup<commit_after>// Copyright 2016, University of Colorado Boulder

/**
 * a Scenery Node representing the beaker heater in the view
 *
 * @author John Blanco
 * @author Andrew Adare
 */
define( function( require ) {
  'use strict';

  // modules
  var BeakerHeater = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/model/BeakerHeater' );
  var BeakerView = require( 'ENERGY_FORMS_AND_CHANGES/common/view/BeakerView' );
  var EFACBaseNode = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/view/EFACBaseNode' );
  var EFACModelImageNode = require( 'ENERGY_FORMS_AND_CHANGES/energy-systems/view/EFACModelImageNode' );
  var EnergyChunkLayer = require( 'ENERGY_FORMS_AND_CHANGES/common/view/EnergyChunkLayer' );
  var energyFormsAndChanges = require( 'ENERGY_FORMS_AND_CHANGES/energyFormsAndChanges' );
  var inherit = require( 'PHET_CORE/inherit' );
  var ModelViewTransform2 = require( 'PHETCOMMON/view/ModelViewTransform2' );
  var ThermometerNode = require( 'SCENERY_PHET/ThermometerNode' );

  // TODO: Make this a query parameter
  var DEBUG = false;

  /**
   * @param {BeakerHeater} beakerHeater
   * @param {Property.<boolean>} energyChunksVisibleProperty
   * @param {ModelViewTransform2} modelViewTransform
   * @constructor
   */
  function BeakerHeaterNode( beakerHeater, energyChunksVisibleProperty, modelViewTransform ) {

    EFACBaseNode.call( this, beakerHeater, modelViewTransform );

    var energizedCoil = new EFACModelImageNode( BeakerHeater.HEATER_ELEMENT_ON_IMAGE, modelViewTransform );

    // add the images that are used to depict this element along with the layer that will contain the energy chunks
    this.addChild( new EFACModelImageNode( BeakerHeater.WIRE_STRAIGHT_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.WIRE_CURVE_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.ELEMENT_BASE_BACK_IMAGE, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.HEATER_ELEMENT_OFF_IMAGE, modelViewTransform ) );
    this.addChild( energizedCoil );
    this.addChild( new EnergyChunkLayer( beakerHeater.energyChunkList, beakerHeater.positionProperty, modelViewTransform ) );
    this.addChild( new EFACModelImageNode( BeakerHeater.ELEMENT_BASE_FRONT_IMAGE, modelViewTransform ) );

    // add the thermometer that will indicate the beaker water temperature
    // TODO: Why aren't we using one that looks more like those used on the first screen?
    var thermometerNode = new ThermometerNode( 0, 1, beakerHeater.heatProportionProperty, {
      left: energizedCoil.right + 20,
      bottom: energizedCoil.top
    } );
    this.addChild( thermometerNode );

    // Add the beaker.  A compensating MVT is needed because the beaker node is being added as a child of this node,
    // but wants to set its own offset in model space.
    var scale = modelViewTransform.matrix.scaleVector;
    var offset = modelViewTransform.modelToViewDelta( beakerHeater.positionProperty.value ).negated();
    var beakerMvt = ModelViewTransform2.createOffsetXYScaleMapping( offset, scale.x, -scale.y );

    // @public (read-only) {BeakerView}
    this.beakerView = new BeakerView( beakerHeater.beaker, energyChunksVisibleProperty, beakerMvt );

    this.addChild( this.beakerView.backNode );
    this.addChild( new EnergyChunkLayer(
      beakerHeater.radiatedEnergyChunkList,
      beakerHeater.beaker.positionProperty,
      modelViewTransform
    ) );
    this.addChild( this.beakerView.frontNode );

    if ( DEBUG ) {
      var Rectangle = require( 'SCENERY/nodes/Rectangle' );
      var sb = beakerMvt.modelToViewBounds( beakerHeater.beaker.getSliceBounds() );
      var bb = beakerMvt.modelToViewBounds( beakerHeater.beaker.getBounds() );
      var rect = beakerMvt.modelToViewShape( beakerHeater.beaker.getRect() );
      this.addChild( new Rectangle( sb.x, sb.y, sb.width, sb.height, { stroke: 'blue' } ) ); // slice bounds
      this.addChild( new Rectangle( bb.x, bb.y, bb.width, bb.height, { stroke: 'red', lineWidth: 4 } ) ); // beaker bounds
      this.addChild( new Rectangle( rect.x, rect.y, rect.width, rect.height, { stroke: 'lime' } ) ); // from getRect()

      // var beakerXY = modelViewTransform.modelToViewPosition(beakerHeater.beaker.position);
      var beakerXY = beakerMvt.modelToViewPosition( beakerHeater.beaker.position );
      var Circle = require( 'SCENERY/nodes/Circle' );
      this.addChild( new Circle( 10, {
        centerX: beakerXY.x,
        centerY: beakerXY.y,
        fill: 'lime'
      } ) );
    }

    // update the transparency of the hot element to make the dark element appear to heat up
    beakerHeater.heatProportionProperty.link( function( litProportion ) {
      energizedCoil.opacity = litProportion;
    } );
  }

  energyFormsAndChanges.register( 'BeakerHeaterNode', BeakerHeaterNode );

  return inherit( EFACBaseNode, BeakerHeaterNode );
} );
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */

CLASS({
  package: 'foam.apps.builder',
  name: 'AppConfigSheetView',
  extendsModel: 'foam.ui.SimpleView',

  imports: [
    'popup',
  ],

  properties: [
    'data',
    'popup',
    {
      model_: 'ViewFactoryProperty',
      name: 'innerView',
      defaultValue: 'foam.ui.md.DetailView'
    },
    {
      name: '$headings',
      getter: function() {
        if ( ! this.$ ) return [];
        var mainHeading = this.$.querySelector('#' + this.id + '-heading');
        var swipeHeadings = this.$.querySelectorAll('app-config ul.swipeAltHeader');
        var headings = [mainHeading];
        for ( var i = 0; i < swipeHeadings.length; ++i ) {
          headings.push(swipeHeadings[i]);
        }
        return headings;
      },
    },
    {
      name: '$choices',
      getter: function() {
        if ( ! this.$ ) return [];
        var choiceList = this.$.querySelectorAll('app-config ul.swipeAltHeader .choice');
        var choices = [];
        for ( var i = 0; i < choiceList.length; ++i ) {
          choices.push(choiceList[i]);
        }
        return choices;
      },
    },
  ],

  methods: [
    function initHTML() {
      this.SUPER();
      // TODO(markdittmer): This is really only needed when we're in a popup or
      // other view that supports 'open' and 'expanded' (with CSS class:
      // .expanded). We should have a clearer way of indicating and detecting
      // these conditions.
      var duration = (this.popup ?
          this.popup.TRANSITION_DURATION : '.3') || '.3';
      this.$headings.forEach(function($heading) {
        $heading.style.transition =
            'background-color cubic-bezier(0.4, 0.0, 1, 1) ' + duration + 's' +
            ', color cubic-bezier(0.4, 0.0, 1, 1) ' + duration + 's';
      });
      this.$choices.forEach(function($choice) {
        $choice.style.transition =
            'border cubic-bezier(0.4, 0.0, 1, 1) ' + duration + 's';
      });
    },
  ],

  actions: [
    {
      name: 'close',
      ligature: 'close',
      iconUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAAa0lEQVR4AeWSsREDIQwEtwlRhO3vP0JFPLgeHJDdnEfBh2y8F2hHnM5FJ1AayRtLshiE6F8WHUsw9kT0m8BDMFlMotZ10rzuaHtS63qo6s8HWkaLFXpo5ErXyKWukS25dRM5sXz+Pt+Ls/kBnolC6l7shJkAAAAASUVORK5CYII=',
      isAvailable: function() { return this.popup; },
      code: function() {
        this.popup && this.popup.close();
      },
    },
  ],

  templates: [
    function toHTML() {/*
      <app-config id="%%id" %%cssClassAttr()>

            <%# (this.data ? this.data.appName : 'App') + ' Configuration' %>
            $$close

        <%= this.innerView({
              data$: this.data$,
            }, this.Y) %>
      </app-config>
      <% this.setClass('expanded', function() {
           return ( ! this.popup ) ||
               this.popup.state === 'expanding' ||
               this.popup.state === 'expanded';
         }.bind(this), this.id);
         this.on('click', function() {
           this.popup && this.popup.expandOrCollapse();
         }.bind(this), this.id + '-heading'); %>
    */},
    function CSS() {/*
      app-config {
        flex-grow: 1;
        display: flex;
        flex-direction: column;
        z-index: 2;
        background-color: #fff;
        box-shadow: 2px 0px 7px rgba(0, 0, 0, 0.48);
      }
      app-config .heading-content {
        flex-grow: 1;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      app-config .md-heading {
        display: flex;
        background-color: transparent;
      }
      app-config.expanded .md-heading {
        color: #fff;
        background-color: #3e50b4;
      }

      app-config .swipeAltHeader {
        padding-left: 3px !important;
        height: 36px;
      }
      app-config .swipeAltHeader li {
        font-size: 14px;
        line-height: 46px;
        padding-bottom: 14px;
      }
      app-config .swipeAltHeader .selected {
        border-bottom: 2px solid rgba(0,0,0,.87);
        font-weight: 500;
      }
      app-config.expanded .swipeAltHeader .selected {
        border-bottom: 2px solid #ff3f80;
      }
      app-config ul.swipeAltHeader {
        background-color: transparent;
        box-shadow: 0 1px 1px rgba(0,0,0,.25);
        height: 47px;
        margin: 0;
        overflow: hidden;
        padding: 0 0 0 56px;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: -moz-none;
        -o-user-select: none;
        user-select: none;
      }
      app-config.expanded ul.swipeAltHeader {
        color: #fff;
        background-color: #3e50b4;
      }
      app-config .foamChoiceListView.horizontal .choice {
        text-transform: uppercase;
        padding: 14px 16px;
        margin: 0;
      }
    */},
  ],
});
<commit_msg>App Builder: App config sheet view: Move transition CSS from element.style.transition to rules in
    */},
    function CSS() {/*
      app-config {
        flex-grow: 1;
        display: flex;
        flex-direction: column;
        z-index: 2;
        background-color: #fff;
        box-shadow: 2px 0px 7px rgba(0, 0, 0, 0.48);
      }
      app-config .heading-content {
        flex-grow: 1;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      app-config .md-heading {
        display: flex;
        background-color: transparent;
      }
      app-config.expanded .md-heading {
        color: #fff;
        background-color: #3e50b4;
      }

      app-config .swipeAltHeader {
        padding-left: 3px !important;
        height: 36px;
      }
      app-config .swipeAltHeader li {
        font-size: 14px;
        line-height: 46px;
        padding-bottom: 14px;
      }
      app-config .swipeAltHeader .selected {
        border-bottom: 2px solid rgba(0,0,0,.87);
        font-weight: 500;
      }
      app-config.expanded .swipeAltHeader .selected {
        border-bottom: 2px solid #ff3f80;
      }
      app-config .swipeAltHeader {
        background-color: transparent;
        box-shadow: 0 1px 1px rgba(0,0,0,.25);
        height: 47px;
        margin: 0;
        overflow: hidden;
        padding: 0 0 0 56px;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: -moz-none;
        -o-user-select: none;
        user-select: none;
      }
      app-config.expanded .swipeAltHeader {
        color: #fff;
        background-color: #3e50b4;
      }
      app-config .foamChoiceListView.horizontal .choice {
        text-transform: uppercase;
        padding: 14px 16px;
        margin: 0;
      }
    */},
  ],
});
<|endoftext|>


<commit_before>import Component from 'flarum/component';
import ItemList from 'flarum/utils/item-list';
import PostStream from 'flarum/utils/post-stream';
import DiscussionList from 'flarum/components/discussion-list';
import DiscussionHero from 'flarum/components/discussion-hero';
import StreamContent from 'flarum/components/stream-content';
import StreamScrubber from 'flarum/components/stream-scrubber';
import ComposerReply from 'flarum/components/composer-reply';
import ActionButton from 'flarum/components/action-button';
import LoadingIndicator from 'flarum/components/loading-indicator';
import DropdownSplit from 'flarum/components/dropdown-split';
import Separator from 'flarum/components/separator';
import listItems from 'flarum/helpers/list-items';

export default class DiscussionPage extends Component {
  /**

   */
  constructor(props) {
    super(props);

    this.discussion = m.prop();

    // Set up the stream. The stream is an object that represents the posts in
    // a discussion, as they're displayed on the screen (i.e. missing posts
    // are condensed into "load more" gaps).
    this.stream = m.prop();

    // Get the discussion. We may already have a copy of it in our store, so
    // we'll start off with that. If we do have a copy of the discussion, and
    // its posts relationship has been loaded (i.e. we've viewed this
    // discussion before), then we can proceed with displaying it immediately.
    // If not, we'll make an API request first.
    this.currentNear = m.route.param('near');
    var params = this.params();
    params.include = params.include.join(',');
    app.store.find('discussions', m.route.param('id'), params).then(this.setupDiscussion.bind(this));

    if (app.cache.discussionList) {
      if (!(app.current instanceof DiscussionPage)) {
        app.cache.discussionList.subtrees.map(subtree => subtree.invalidate());
      }
      app.pane.enable();
      app.pane.hide();
      m.redraw.strategy('diff'); // otherwise pane redraws and mouseenter even is triggered so it doesn't hide
    }

    app.history.push('discussion');
    app.current = this;
    app.composer.minimize();
  }

  params() {
    return {
      near: this.currentNear,
      include: ['posts', 'posts.user']
    };
  }

  /*

   */
  setupDiscussion(discussion) {
    this.discussion(discussion);

    var includedPosts = [];
    discussion.payload.included && discussion.payload.included.forEach(record => {
      if (record.type === 'posts' && (record.contentType !== 'comment' || record.contentHtml)) {
        includedPosts.push(record.id);
      }
    });

    // Set up the post stream for this discussion, and add all of the posts we
    // have loaded so far.
    this.stream(new PostStream(discussion));
    this.stream().addPosts(discussion.posts().filter(value => value && includedPosts.indexOf(value.id()) !== -1));
    this.streamContent = new StreamContent({
      stream: this.stream(),
      className: 'discussion-posts posts',
      positionChanged: this.positionChanged.bind(this)
    });

    // Hold up there skippy! If the slug in the URL doesn't match up, we'll
    // redirect so we have the correct one.
    // Waiting on https://github.com/lhorie/mithril.js/issues/539
    // if (m.route.param('id') === discussion.id() && m.route.param('slug') !== discussion.slug()) {
    //   var params = m.route.param();
    //   params.slug = discussion.slug();
    //   params.near = params.near || '';
    //   m.route(app.route('discussion.near', params), null, true);
    //   return;
    // }

    this.streamContent.goToNumber(this.currentNear, true);
  }

  onload(element, isInitialized, context) {
    if (isInitialized) { return; }

    context.retain = true;

    $('body').addClass('discussion-page');
    context.onunload = function() {
      $('body').removeClass('discussion-page');
    }
  }

  /**

   */
  onunload(e) {
    // If we have routed to the same discussion as we were viewing previously,
    // cancel the unloading of this controller and instead prompt the post
    // stream to jump to the new 'near' param.
    var discussion = this.discussion();
    if (discussion) {
      var discussionRoute = app.route('discussion', { id: discussion.id(), slug: discussion.slug() });
      if (m.route().substr(0, discussionRoute.length) === discussionRoute) {
        e.preventDefault();
        if (m.route.param('near') != this.currentNear) {
          this.streamContent.goToNumber(m.route.param('near'));
        }
        return;
      }
    }

    app.pane.disable();
  }

  /**

   */
  view() {
    var discussion = this.discussion();

    return m('div', {config: this.onload.bind(this)}, [
      app.cache.discussionList ? m('div.index-area.paned', {config: this.configIndex.bind(this)}, app.cache.discussionList.view()) : '',
      m('div.discussion-area', discussion ? [
        DiscussionHero.component({discussion}),
        m('div.container', [
          m('nav.discussion-nav', [
            m('ul', listItems(this.sidebarItems().toArray()))
          ]),
          this.streamContent.view()
        ])
      ] : LoadingIndicator.component({className: 'loading-indicator-block'}))
    ]);
  }

  /**

   */
  configIndex(element, isInitialized, context) {
    if (isInitialized) { return; }

    context.retain = true;

    var $index = $(element);

    // When viewing a discussion (for which the discussions route is the
    // parent,) the discussion list is still rendered but it becomes a
    // pane hidden on the side of the screen. When the mouse enters and
    // leaves the discussions pane, we want to show and hide the pane
    // respectively. We also create a 10px 'hot edge' on the left of the
    // screen to activate the pane.
    var pane = app.pane;
    $index.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));

    var hotEdge = function(e) {
      if (e.pageX < 10) { pane.show(); }
    };
    $(document).on('mousemove', hotEdge);
    context.onunload = function() {
      $(document).off('mousemove', hotEdge);
    };

    var $discussion = $index.find('.discussion-summary.active');
    if ($discussion.length) {
      var indexTop = $index.offset().top;
      var discussionTop = $discussion.offset().top;
      if (discussionTop < indexTop || discussionTop + $discussion.outerHeight() > indexTop + $index.outerHeight()) {
        $index.scrollTop($index.scrollTop() - indexTop + discussionTop);
      }
    }
  }

  /**

   */
  sidebarItems() {
    var items = new ItemList();

    items.add('controls',
      DropdownSplit.component({
        items: this.discussion().controls(this).toArray(),
        icon: 'reply',
        buttonClass: 'btn btn-primary',
        wrapperClass: 'primary-control'
      })
    );

    items.add('scrubber',
      StreamScrubber.component({
        streamContent: this.streamContent,
        wrapperClass: 'title-control'
      })
    );

    return items;
  }

  /**

   */
  positionChanged(startNumber, endNumber) {
    var discussion = this.discussion();

    var url = app.route('discussion.near', {
      id: discussion.id(),
      slug: discussion.slug(),
      near: this.currentNear = startNumber
    });

    // https://github.com/lhorie/mithril.js/issues/559
    m.route(url, true);
    window.history.replaceState(null, document.title, (m.route.mode === 'hash' ? '#' : '')+url);

    app.history.push('discussion');

    if (app.session.user() && endNumber > discussion.readNumber()) {
      discussion.save({readNumber: endNumber});
      m.redraw();
    }
  }
}
<commit_msg>Don't be so picky about the slug<commit_after>import Component from 'flarum/component';
import ItemList from 'flarum/utils/item-list';
import PostStream from 'flarum/utils/post-stream';
import DiscussionList from 'flarum/components/discussion-list';
import DiscussionHero from 'flarum/components/discussion-hero';
import StreamContent from 'flarum/components/stream-content';
import StreamScrubber from 'flarum/components/stream-scrubber';
import ComposerReply from 'flarum/components/composer-reply';
import ActionButton from 'flarum/components/action-button';
import LoadingIndicator from 'flarum/components/loading-indicator';
import DropdownSplit from 'flarum/components/dropdown-split';
import Separator from 'flarum/components/separator';
import listItems from 'flarum/helpers/list-items';

export default class DiscussionPage extends Component {
  /**

   */
  constructor(props) {
    super(props);

    this.discussion = m.prop();

    // Set up the stream. The stream is an object that represents the posts in
    // a discussion, as they're displayed on the screen (i.e. missing posts
    // are condensed into "load more" gaps).
    this.stream = m.prop();

    // Get the discussion. We may already have a copy of it in our store, so
    // we'll start off with that. If we do have a copy of the discussion, and
    // its posts relationship has been loaded (i.e. we've viewed this
    // discussion before), then we can proceed with displaying it immediately.
    // If not, we'll make an API request first.
    this.currentNear = m.route.param('near');
    var params = this.params();
    params.include = params.include.join(',');
    app.store.find('discussions', m.route.param('id'), params).then(this.setupDiscussion.bind(this));

    if (app.cache.discussionList) {
      if (!(app.current instanceof DiscussionPage)) {
        app.cache.discussionList.subtrees.map(subtree => subtree.invalidate());
      }
      app.pane.enable();
      app.pane.hide();
      m.redraw.strategy('diff'); // otherwise pane redraws and mouseenter even is triggered so it doesn't hide
    }

    app.history.push('discussion');
    app.current = this;
    app.composer.minimize();
  }

  params() {
    return {
      near: this.currentNear,
      include: ['posts', 'posts.user']
    };
  }

  /*

   */
  setupDiscussion(discussion) {
    this.discussion(discussion);

    var includedPosts = [];
    discussion.payload.included && discussion.payload.included.forEach(record => {
      if (record.type === 'posts' && (record.contentType !== 'comment' || record.contentHtml)) {
        includedPosts.push(record.id);
      }
    });

    // Set up the post stream for this discussion, and add all of the posts we
    // have loaded so far.
    this.stream(new PostStream(discussion));
    this.stream().addPosts(discussion.posts().filter(value => value && includedPosts.indexOf(value.id()) !== -1));
    this.streamContent = new StreamContent({
      stream: this.stream(),
      className: 'discussion-posts posts',
      positionChanged: this.positionChanged.bind(this)
    });

    // Hold up there skippy! If the slug in the URL doesn't match up, we'll
    // redirect so we have the correct one.
    // Waiting on https://github.com/lhorie/mithril.js/issues/539
    // if (m.route.param('id') === discussion.id() && m.route.param('slug') !== discussion.slug()) {
    //   var params = m.route.param();
    //   params.slug = discussion.slug();
    //   params.near = params.near || '';
    //   m.route(app.route('discussion.near', params), null, true);
    //   return;
    // }

    this.streamContent.goToNumber(this.currentNear, true);
  }

  onload(element, isInitialized, context) {
    if (isInitialized) { return; }

    context.retain = true;

    $('body').addClass('discussion-page');
    context.onunload = function() {
      $('body').removeClass('discussion-page');
    }
  }

  /**

   */
  onunload(e) {
    // If we have routed to the same discussion as we were viewing previously,
    // cancel the unloading of this controller and instead prompt the post
    // stream to jump to the new 'near' param.
    var discussion = this.discussion();
    if (discussion) {
      if (m.route.param('id') == discussion.id()) {
        e.preventDefault();
        if (m.route.param('near') != this.currentNear) {
          this.streamContent.goToNumber(m.route.param('near'));
        }
        this.currentNear = null;
        return;
      }
    }

    app.pane.disable();
  }

  /**

   */
  view() {
    var discussion = this.discussion();

    return m('div', {config: this.onload.bind(this)}, [
      app.cache.discussionList ? m('div.index-area.paned', {config: this.configIndex.bind(this)}, app.cache.discussionList.view()) : '',
      m('div.discussion-area', discussion ? [
        DiscussionHero.component({discussion}),
        m('div.container', [
          m('nav.discussion-nav', [
            m('ul', listItems(this.sidebarItems().toArray()))
          ]),
          this.streamContent.view()
        ])
      ] : LoadingIndicator.component({className: 'loading-indicator-block'}))
    ]);
  }

  /**

   */
  configIndex(element, isInitialized, context) {
    if (isInitialized) { return; }

    context.retain = true;

    var $index = $(element);

    // When viewing a discussion (for which the discussions route is the
    // parent,) the discussion list is still rendered but it becomes a
    // pane hidden on the side of the screen. When the mouse enters and
    // leaves the discussions pane, we want to show and hide the pane
    // respectively. We also create a 10px 'hot edge' on the left of the
    // screen to activate the pane.
    var pane = app.pane;
    $index.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));

    var hotEdge = function(e) {
      if (e.pageX < 10) { pane.show(); }
    };
    $(document).on('mousemove', hotEdge);
    context.onunload = function() {
      $(document).off('mousemove', hotEdge);
    };

    var $discussion = $index.find('.discussion-summary.active');
    if ($discussion.length) {
      var indexTop = $index.offset().top;
      var discussionTop = $discussion.offset().top;
      if (discussionTop < indexTop || discussionTop + $discussion.outerHeight() > indexTop + $index.outerHeight()) {
        $index.scrollTop($index.scrollTop() - indexTop + discussionTop);
      }
    }
  }

  /**

   */
  sidebarItems() {
    var items = new ItemList();

    items.add('controls',
      DropdownSplit.component({
        items: this.discussion().controls(this).toArray(),
        icon: 'reply',
        buttonClass: 'btn btn-primary',
        wrapperClass: 'primary-control'
      })
    );

    items.add('scrubber',
      StreamScrubber.component({
        streamContent: this.streamContent,
        wrapperClass: 'title-control'
      })
    );

    return items;
  }

  /**

   */
  positionChanged(startNumber, endNumber) {
    var discussion = this.discussion();

    var url = app.route('discussion.near', {
      id: discussion.id(),
      slug: discussion.slug(),
      near: this.currentNear = startNumber
    });

    // https://github.com/lhorie/mithril.js/issues/559
    m.route(url, true);
    window.history.replaceState(null, document.title, (m.route.mode === 'hash' ? '#' : '')+url);

    app.history.push('discussion');

    if (app.session.user() && endNumber > discussion.readNumber()) {
      discussion.save({readNumber: endNumber});
      m.redraw();
    }
  }
}
<|endoftext|>


<commit_before>// harmonizer version 0.4.0
//
// Copyright (c) 2016, Alex Nichol.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(function() {

  var exports;
  if ('undefined' !== typeof self) {
    if (!self.harmonizer) {
      self.harmonizer = {};
    }
    exports = self.harmonizer;
  } else if ('undefined' !== typeof window) {
    if (!window.harmonizer) {
      window.harmonizer = {};
    }
    exports = window.harmonizer;
  } else if ('undefined' !== typeof module) {
    exports = module.exports;
  }

  var POLYFILL_FPS = 60;

  var supportsAnimationFrame = ('requestAnimationFrame' in window &&
    'undefined' !== typeof performance && 'now' in performance);

  function getCurrentTime() {
    if (supportsAnimationFrame) {
      return performance.now();
    } else {
      return new Date().getTime();
    }
  }

  function requestAnimationFrameOrPolyfill(func) {
    if (supportsAnimationFrame) {
      return window.requestAnimationFrame(func);
    } else {
      return setTimeout(function() {
        func(getCurrentTime());
      }, 1000/POLYFILL_FPS);
    }
  }

  function cancelAnimationFrameOrPolyfill(id) {
    if (supportsAnimationFrame) {
      window.cancelAnimationFrame(id);
    } else {
      clearTimeout(id);
    }
  }

  var ANIMATION_STOPPED = 0;
  var ANIMATION_RUNNING = 1;
  var ANIMATION_PAUSED = 2;

  function Harmonizer(context) {
    EventEmitter.call(this);

    this._context = context || exports.defaultContext;

    this._parent = null;
    this._children = [];

    this._animationState = ANIMATION_STOPPED;
    this._animationStartTime = 0;
    this._animationSkipTime = 0;
  }

  Harmonizer.prototype = Object.create(EventEmitter.prototype);
  Harmonizer.prototype.constructor = Harmonizer;

  Harmonizer.prototype.start = function() {
    switch (this._animationState) {
    case ANIMATION_RUNNING:
      return;
    case ANIMATION_STOPPED:
    case ANIMATION_PAUSED:
      this._animationState = ANIMATION_RUNNING;
      this._animationStartTime = getCurrentTime();
      this._context._addAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.stop = function() {
    switch (this._animationState) {
    case ANIMATION_STOPPED:
      break;
    case ANIMATION_PAUSED:
      this._animationSkipTime = 0;
      this._animationState = ANIMATION_STOPPED;
      break;
    case ANIMATION_RUNNING:
      this._animationSkipTime = 0;
      this._animationState = ANIMATION_STOPPED;
      this._context._removeAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.pause = function() {
    switch (this._animationState) {
    case ANIMATION_PAUSED:
    case ANIMATION_STOPPED:
      break;
    case ANIMATION_RUNNING:
      this._animationSkipTime += getCurrentTime() - this._animationStartTime;
      this._animationState = ANIMATION_PAUSED;
      this._context._removeAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.requestPaint = function() {
    var root = this._rootHarmonizer();
    if (this._context._inAnimationFrame()) {
      this._context._addPaintHarmonizer(root);
    } else {
      root._paint();
    }
  };

  Harmonizer.prototype.appendChild = function(child) {
    assert(this._children.indexOf(child) < 0);
    assert(child._context === this._context);
    this._children.push(child);
    child._parent = this;
  };

  Harmonizer.prototype.removeChild = function(child) {
    var idx = this._children.indexOf(child);
    assert(idx >= 0);
    this._children.splice(idx, 1);
    child._parent = null;
  };

  Harmonizer.prototype.getParent = function() {
    return this._parent;
  };

  Harmonizer.prototype.spawnChild = function() {
    var res = new Harmonizer(this._context);
    this.appendChild(res);
    return res;
  };

  Harmonizer.prototype.makeSingleShot = function(handler) {
    if ('undefined' === typeof handler) {
      this.on('animationFrame', function() {
        this.stop();
        this.requestPaint();
      }.bind(this));
    } else {
      this.on('animationFrame', function() {
        this.stop();
        handler();
      }.bind(this));
    }
  };

  Harmonizer.prototype._handleFrame = function(time) {
    assert(this._animationState === ANIMATION_RUNNING);
    this.emit('animationFrame', time-this._animationStartTime+this._animationSkipTime);
  };

  Harmonizer.prototype._paint = function() {
    this.emit('paint');
  };

  Harmonizer.prototype._rootHarmonizer = function() {
    var root = this;
    while (root._parent !== null) {
      root = root._parent;
    }
    return root;
  };

  exports.Harmonizer = Harmonizer;

  function Context() {
    this._paintHarmonizers = [];
    this._animatingHarmonizers = [];
    this._animationFrameRequest = null;
    this._boundCallback = this._animationFrame.bind(this);
    this._isHandlingAnimationFrame = false;
  }

  Context.prototype._addAnimatingHarmonizer = function(h) {
    this._animatingHarmonizers.push(h);
    if (this._animationFrameRequest === null) {
      this._animationFrameRequest = requestAnimationFrameOrPolyfill(this._boundCallback);
    }
  };

  Context.prototype._removeAnimatingHarmonizer = function(h) {
    var idx = this._animatingHarmonizers.indexOf(h);
    assert(idx >= 0);
    this._animatingHarmonizers.splice(idx, 1);
    if (this._animatingHarmonizers.length === 0) {
      cancelAnimationFrameOrPolyfill(this._animationFrameRequest);
      this._animationFrameRequest = null;
    }
  };

  Context.prototype._inAnimationFrame = function() {
    return this._isHandlingAnimationFrame;
  };

  Context.prototype._addPaintHarmonizer = function(h) {
    var idx = this._paintHarmonizers.indexOf(h);
    if (idx < 0) {
      this._paintHarmonizers.push(h);
    }
  };

  Context.prototype._animationFrame = function(time) {
    this._isHandlingAnimationFrame = true;
    this._animationFrameRequest = requestAnimationFrameOrPolyfill(this._boundCallback);

    var destinations = this._animatingHarmonizers.slice();
    for (var i = 0, len = destinations.length; i < len; ++i) {
      var destination = destinations[i];
      if (this._animatingHarmonizers.indexOf(destination) < 0) {
        continue;
      }
      destination._handleFrame(time);
    }

    var paintHarmonizers = this._paintHarmonizers.slice();
    for (var i = 0, len = paintHarmonizers.length; i < len; ++i) {
      paintHarmonizers[i]._paint();
    }
    this._paintHarmonizers = [];

    this._isHandlingAnimationFrame = false;
  };

  exports.defaultContext = new Context();
  exports.Context = Context;

  function assert(flag) {
    if (!flag) {
      throw new Error('Assertion failure.');
    }
  }

})();
<commit_msg>use harmonizer v0.4.3 in demo<commit_after>// harmonizer version 0.4.3
//
// Copyright (c) 2016, Alex Nichol.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(function() {

  var exports;
  if ('undefined' !== typeof self) {
    if (!self.harmonizer) {
      self.harmonizer = {};
    }
    exports = self.harmonizer;
  } else if ('undefined' !== typeof window) {
    if (!window.harmonizer) {
      window.harmonizer = {};
    }
    exports = window.harmonizer;
  } else if ('undefined' !== typeof module) {
    exports = module.exports;
  }

  var POLYFILL_FPS = 60;

  var supportsAnimationFrame = ('requestAnimationFrame' in window &&
    'undefined' !== typeof performance && 'now' in performance);

  function getCurrentTime() {
    if (supportsAnimationFrame) {
      return performance.now();
    } else {
      return new Date().getTime();
    }
  }

  function requestAnimationFrameOrPolyfill(func) {
    if (supportsAnimationFrame) {
      return window.requestAnimationFrame(func);
    } else {
      return setTimeout(function() {
        func(getCurrentTime());
      }, 1000/POLYFILL_FPS);
    }
  }

  function cancelAnimationFrameOrPolyfill(id) {
    if (supportsAnimationFrame) {
      window.cancelAnimationFrame(id);
    } else {
      clearTimeout(id);
    }
  }

  var ANIMATION_STOPPED = 0;
  var ANIMATION_RUNNING = 1;
  var ANIMATION_PAUSED = 2;

  function Harmonizer(context) {
    EventEmitter.call(this);

    this._context = context || exports.defaultContext;

    this._parent = null;
    this._children = [];

    this._animationState = ANIMATION_STOPPED;
    this._animationStartTime = 0;
    this._animationSkipTime = 0;
  }

  Harmonizer.prototype = Object.create(EventEmitter.prototype);
  Harmonizer.prototype.constructor = Harmonizer;

  Harmonizer.prototype.getContext = function() {
    return this._context;
  };

  Harmonizer.prototype.isAnimating = function() {
    return this._animationState === ANIMATION_RUNNING;
  };

  Harmonizer.prototype.isPaused = function() {
    return this._animationState === ANIMATION_PAUSED;
  };

  Harmonizer.prototype.start = function() {
    switch (this._animationState) {
    case ANIMATION_RUNNING:
      return;
    case ANIMATION_STOPPED:
    case ANIMATION_PAUSED:
      this._animationState = ANIMATION_RUNNING;
      this._animationStartTime = getCurrentTime();
      this._context._addAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.stop = function() {
    switch (this._animationState) {
    case ANIMATION_STOPPED:
      break;
    case ANIMATION_PAUSED:
      this._animationSkipTime = 0;
      this._animationState = ANIMATION_STOPPED;
      break;
    case ANIMATION_RUNNING:
      this._animationSkipTime = 0;
      this._animationState = ANIMATION_STOPPED;
      this._context._removeAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.pause = function() {
    switch (this._animationState) {
    case ANIMATION_PAUSED:
    case ANIMATION_STOPPED:
      break;
    case ANIMATION_RUNNING:
      this._animationSkipTime += getCurrentTime() - this._animationStartTime;
      this._animationState = ANIMATION_PAUSED;
      this._context._removeAnimatingHarmonizer(this);
      break;
    }
  };

  Harmonizer.prototype.requestPaint = function() {
    var root = this._rootHarmonizer();
    if (this._context._inAnimationFrame()) {
      this._context._addPaintHarmonizer(root);
    } else {
      root._paint();
    }
  };

  Harmonizer.prototype.appendChild = function(child) {
    assert(this._children.indexOf(child) < 0);
    assert(child._context === this._context);
    this._children.push(child);
    child._parent = this;
  };

  Harmonizer.prototype.removeChild = function(child) {
    var idx = this._children.indexOf(child);
    assert(idx >= 0);
    this._children.splice(idx, 1);
    child._parent = null;
  };

  Harmonizer.prototype.getParent = function() {
    return this._parent;
  };

  Harmonizer.prototype.spawnChild = function() {
    var res = new Harmonizer(this._context);
    this.appendChild(res);
    return res;
  };

  Harmonizer.prototype.makeSingleShot = function(handler) {
    if ('undefined' === typeof handler) {
      this.on('animationFrame', function() {
        this.stop();
        this.requestPaint();
      }.bind(this));
    } else {
      this.on('animationFrame', function() {
        this.stop();
        handler();
      }.bind(this));
    }
  };

  Harmonizer.prototype._handleFrame = function(time) {
    assert(this._animationState === ANIMATION_RUNNING);

    if (time < this._animationStartTime) {
      // This may occur in an edge case when the animation is started in a
      // requestAnimationFrame() handler that was triggered right before the
      // context's animation frame handler, since the same timestamp is passed
      // to all requestAnimationFrame handlers even though Performance.now()
      // might a bit ahead.
      this._animationStartTime = time;
    }

    var elapsed = time - this._animationStartTime + this._animationSkipTime;
    this.emit('animationFrame', elapsed);
  };

  Harmonizer.prototype._paint = function() {
    this.emit('paint');
  };

  Harmonizer.prototype._rootHarmonizer = function() {
    var root = this;
    while (root._parent !== null) {
      root = root._parent;
    }
    return root;
  };

  exports.Harmonizer = Harmonizer;

  function Context() {
    this._paintHarmonizers = [];
    this._animatingHarmonizers = [];
    this._animationFrameRequest = null;
    this._boundCallback = this._animationFrame.bind(this);
    this._isHandlingAnimationFrame = false;
  }

  Context.prototype._addAnimatingHarmonizer = function(h) {
    this._animatingHarmonizers.push(h);
    if (this._animationFrameRequest === null) {
      this._animationFrameRequest = requestAnimationFrameOrPolyfill(this._boundCallback);
    }
  };

  Context.prototype._removeAnimatingHarmonizer = function(h) {
    var idx = this._animatingHarmonizers.indexOf(h);
    assert(idx >= 0);
    this._animatingHarmonizers.splice(idx, 1);
    if (this._animatingHarmonizers.length === 0) {
      cancelAnimationFrameOrPolyfill(this._animationFrameRequest);
      this._animationFrameRequest = null;
    }
  };

  Context.prototype._inAnimationFrame = function() {
    return this._isHandlingAnimationFrame;
  };

  Context.prototype._addPaintHarmonizer = function(h) {
    var idx = this._paintHarmonizers.indexOf(h);
    if (idx < 0) {
      this._paintHarmonizers.push(h);
    }
  };

  Context.prototype._animationFrame = function(time) {
    this._isHandlingAnimationFrame = true;
    this._animationFrameRequest = requestAnimationFrameOrPolyfill(this._boundCallback);

    var destinations = this._animatingHarmonizers.slice();
    for (var i = 0, len = destinations.length; i < len; ++i) {
      var destination = destinations[i];
      if (this._animatingHarmonizers.indexOf(destination) < 0) {
        continue;
      }
      destination._handleFrame(time);
    }

    var paintHarmonizers = this._paintHarmonizers.slice();
    for (var i = 0, len = paintHarmonizers.length; i < len; ++i) {
      paintHarmonizers[i]._paint();
    }
    this._paintHarmonizers = [];

    this._isHandlingAnimationFrame = false;
  };

  exports.defaultContext = new Context();
  exports.Context = Context;

  function assert(flag) {
    if (!flag) {
      throw new Error('Assertion failure.');
    }
  }

})();
<|endoftext|>