<commit_before>define([
  "dojo/_base/declare"
, "dojo/when"
, "dojo/topic"
, "dojo/on"
, "dojo/dom-construct"
, "dojo/dom-style"
, "dojo/dom-attr"
, "dojo/dom-class"
, "dojo/dom-geometry"
, "dojo/_base/lang"

, "dijit/_WidgetBase"
, "dijit/_OnDijitClickMixin"
, "dijit/_Container"
, "dijit/layout/_ContentPaneResizeMixin"
, "dijit/_TemplatedMixin"
, "dijit/_WidgetsInTemplateMixin"
, "dijit/form/_FormValueMixin"

, "dgrid/OnDemandList"
, "dgrid/Selection"
, "dgrid/Keyboard"
, "dgrid/extensions/DijitRegistry"
, "put-selector/put"

, "hotplate/hotClientDojo/widgets/_OverlayMixin"
, "hotplate/hotClientDojo/stores/stores"

], function(
  declare
, when
, topic
, on
, domConstruct
, domStyle
, domAttr
, domClass
, domGeometry
, lang

, _WidgetBase
, _OnDijitClickMixin
, _Container
, _ContentPaneResizeMixin
, _TemplatedMixin
, _WidgetsInTemplateMixin
, _FormValueMixin

, OnDemandList
, Selection
, Keyboard
, DijitRegistry
, put

, _OverlayMixin
, stores

){

  return declare( [ _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, _Container, _ContentPaneResizeMixin, _OnDijitClickMixin, _FormValueMixin, _OverlayMixin ] , {

    templateString: '
\n' +
                    '
\n'+
                    '

Pick

\n' +
                    '

\n' +
                    '
\n'+
                    '

\n'+
                    '    <input type="hidden" ${!nameAttrSetting} data-dojo-attach-point="valueNode" value="${value}" aria-hidden="true" />\n'+
                    '
\n'+
                    '\n'+
                    '',

    widgetsInTemplate: true,

    // Store
    //store: null,

    ownClass: null,

    // Store name. Useful for topics etc.
    storeName: '',
    storeParameters: null,

    initialSort: [],
    initialFilter: null,

    // Refresh immediately on refreshData topic
    immediateRefresh: false,

    // renderRow which will be used by the created dgrid.
    // It needs to be defined
    renderRow: function( o ){ return "renderRow not defined"; } ,

    // The list widget
    listWidget: null,

    // Whether the listWidget is visible or not
    expanded: false,

    // When automatically moving based on the current window,
    // the minimum distance to the bottom
    distanceFromBottom: 20,

    _loadingValue: false,

    // Proxy method for listwidget.row()
    row: function( p ){
      if( this.listWidget ){
        return this.listWidget.row( p );
      }
    },

    resize: function(){
      this._moveIfNecessary();
      this.inherited(arguments);
    },

    destroy: function(){
      this.listWidget.destroy();
      this.inherited(arguments);
    },

    // STOLEN FROM _FormWidget.js
    postMixInProperties: function(){
      // Setup name=foo string to be referenced from the template (but only if a name has been specified)
      // Unfortunately we can't use _setNameAttr to set the name due to IE limitations, see #8484, #8660.
      // Regarding escaping, see heading "Attribute values" in
      // http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
      this.nameAttrSetting = this.name ? ('name="' + this.name.replace(/"/g, """) + '"') : '';
      this.inherited(arguments);

    },

    // Override automatic assigning type --> focusNode, it causes exception on IE.
    // Instead, type must be specified as ${type} in the template, as part of the original DOM
    _setTypeAttr: null,

    // Sets valueNode in the DOM
    _setValueAttr: function(value){

      this.inherited(arguments);
      var self = this;

      self._set( 'value', value );

      // Set the valueNode (the hidden field in this widget)
      domAttr.set(this.valueNode, "value", value);
      // domAttr.set(this.valueNode, "value", this.get("value"));

      if( value ){

        // If the row is displayed in the dgrid List, then take it from there...
        var row = self.row( value );
        if( row && row.data ){
          if( self.labelNode ){
            self.labelNode.innerHTML = '';
            put( self.labelNode, self.renderRow( row.data ) );
          }

        // Otherwise, if the raw is not in the dgrid list, load it up
        } else {

          when( self.store.get( value ) ).then(

            function( data ){
              self.labelNode.innerHTML = '';
              put( self.labelNode, self.renderRow( data ) );
            },
            function( err ) {
              self.labelNode.innerHTML = "ID: " + value;
            }

          );

          //self.labelNode.innerHTML = "ID: " + value;
        }
      }

    },

    startup: function(){
      this.inherited(arguments);
    },

    postCreate: function(){

      this.inherited(arguments);

      var self = this;

      // Make up the store internal variable
      this.store = stores( this.storeName, this.storeParameters );

      // Set this widget's own class, if defined in prototype
      if( self.ownClass ){
        domClass.add( self.domNode, self.ownClass );
      }

      // Overlayed, non-clickable by default
      //self.set( 'overlayStatus', { overlayed: true, clickable: false } );

      // Make up the constructor for the select list. It's basically a dgrid, with the
      // renderRow set as the parent's, and single selection mode
      var ListConstructor = declare( [ OnDemandList, Selection, Keyboard, DijitRegistry ], {
        renderRow: self.renderRow,
        selectionMode: 'single',
        //minRowsPerPage: 0, // Download the _lot_, or selected item might not be in memory

        // Adding and deleting rows will trigger the "position calculation"
        // function to make sure that the select list is not ourside of the
        // visible area
        newRow: function(){
          var r = this.inherited(arguments);
          self._moveIfNecessary();
          return r;
        },
        removeRow: function(){
          var r = this.inherited(arguments);
          self._moveIfNecessary();
          return r;
        },

        postCreate: function(){
          this.inherited(arguments);
        },

      } );

      // Make up the collection with the initial sorting/filters
      var collection = self.store;
      if( self.initialSort.length) collection = collection.sort( self.initialSort );
      if( self.initialFilter ) collection = collection.filter( self.initialFilter );

      // Make the dgrid widget.
      self.listWidget = new ListConstructor( { collection: collection });

      // Place the grid, invisible, in the right node, start it up, and
      // make it invisible
      domConstruct.place( self.listWidget.domNode, self.listWidgetNode );
      self.listWidget.startup();
      domStyle.set( self.listWidget.domNode, { display: 'none' } );

      self.listWidget.on("dgrid-select", function( e ) {
        var rowId = e.rows[0].id;
        self.set( 'value', rowId );
      });

      // If there is a change and store is queryEngine-less and has sorting/filtering, will need to refresh
      self.store.on( 'add,update,delete', function( event ){

        if( ! self.store.queryEngine && self.listWidget._renderedCollection.queryLog.length ) {

          // Zap partialResults, so that Observable doesn't actually work
          self.listWidget._renderedCollection._partialResults = null;

          // Refresh the data
          self.listWidget.refresh( { keepScrollPosition: true } );
        }
      });

      // If the element currently selected is changed, need to re-render
      // the labelNode to reflect the changes.
      // Ditto for deletions.
      self.store.on( 'update', function( event ){
        if( event.target[ self.store.idProperty ] == self.value ){
          self.labelNode.innerHTML = '';
          put( self.labelNode, self.renderRow( event.target ) );
        }
      });
      self.store.on( 'delete', function( event ){
        if( event.target[ self.store.idProperty ] == self.value ){
          self.labelNode.innerHTML = 'Item deleted';
        }
      });

      self.own(

        // Reload ON can ne triggered by a click, or by a refreshData
        on( self.domNode, 'overlayClick', function( e ){
          self.set( 'overlayStatus', { overlayed: false, clickable: true } ); // CLICKME OFF
          self.set( 'overlayStatus', { overlayed: true, clickable: false } ); // LOADING ON
          self.listWidget.refresh();
        }),
        topic.subscribe('refreshData', function( ){
          if( self.immediateRefresh ){
            self.set( 'overlayStatus', { overlayed: true, clickable: false } ); // LOADING ON
            self.listWidget.refresh();
          } else {
            self.set( 'overlayStatus', { overlayed: true, clickable: true } ); // CLICKME ON
            collapseList();
          }
        }),

        // Reload OFF can be triggered by a refresh-complete or by a dgrid error
        on( self.listWidget.domNode, 'dgrid-refresh-complete', function( e ){
           self.set( 'overlayStatus', { overlayed: false, clickable: false } ); // LOADING OFF
           self.set( 'value', self.value );
        }),
        on( self.listWidget.domNode, 'dgrid-error', function( e ){
           self.set( 'overlayStatus', { overlayed: false, clickable: false } ); // LOADING OFF
           self.set( 'overlayStatus', { overlayed: true, clickable: true } ); // CLICKME ON
          topic.publish( 'globalAlert', 'Error: ' + e.error.message, 5000 );
          collapseList();
        }),

        // Escape pressed at body level: close the select box, select
        // value that selected in the first place when it was open
        //on( document.body, 'keydown', function( e ) {
        on( self.listWidget.domNode, 'keydown', function( e ) {
          if( e.keyCode == 27 && self.expanded){
            e.stopPropagation();
            domStyle.set( self.listWidget.domNode, { display: 'none' } );
            self.expanded = false;
            self.listWidget.clearSelection();
            self.listWidget.select( self.valueWhenOpen );
          }
        }),

        // Open or close when pressing enter
        on( self.domNode, 'keydown', function( e ){
          if( e.keyCode == 13 ){
            if( ! self.expanded  ){
              expandList();
            } else {
              collapseList( self.row( e ) );
            }
          }
        }),

        // Open or close when rlicking on it with mouse
        on( self.domNode, 'click', function( e ){
         // self.listWidget.refresh();
          if( ! self.expanded ){
            expandList();
          } else {
            collapseList( self.row( e ) );
         }
        }),

        // Focus out: hide the select box
        // FIXME: Haven't quite managed this one yet
        on( document.body, 'click', function( e ) {
          if( self.expanded){
          //  domStyle.set( self.listWidget.domNode, { display: 'none' } );
          //  self.expanded = false;
          }
        })

      );

      // Function to expand the widget to include the select list.
      // It will do all of the required operations: make it visible in the CSS,
      // give the focus to the right element, mark it as "expanded", and move it
      // if necessary to do so (e.g. its bottom is out of the visible screen)
      function expandList(){
        self.valueWhenOpen = self.value;
        domStyle.set( self.listWidget.domNode, { display: 'block' } );
        if( typeof( self.value ) !== 'undefined' && self.value !== null ){
          var r = self.row( self.value );
          self.listWidget.focus( r );
        }
        self.expanded = true;
        self._moveIfNecessary();
      }

      // Function to collapse the widget so that the select list is no longe there.
      // It will just make it invisible, but it will also set the widget value to
      // the row element. This is to be called once a pick is successful, either
      // with an Enter or with a mouse click.
      function collapseList( row ){

        domStyle.set( self.listWidget.domNode, { display: 'none' } );
        self.expanded = false;
        if( row ){
          self.set( 'value', row.id);
        }
      }

    },

    // Te dgrid might grow to the point that its bottom is outside of the
    // actual visible area. This would be bad (the bottom elements would be
    // unreachable). So, it calculates things up and makes sure that the dgrid
    // is moved up, also considering the required self.distanceFromBottom
    _moveIfNecessary: function(){
      var self = this;

      //self.listWidget.resize();
      domStyle.set( self.listWidget.domNode, { top: '' } );
      var bodyBox = domGeometry.getContentBox( document.body );
      var listBox = domGeometry.position( self.listWidget.domNode, true );
      var delta = bodyBox.h - ( listBox.y + listBox.h ) - self.distanceFromBottom;
      if( delta < 0 ){
        domStyle.set( self.listWidget.domNode, { top: (delta)+'px' } );
      } else {
        domStyle.set( self.listWidget.domNode, { top: '' } );
      }

    },

  });

});

<commit_msg>Added self.own to avoid hanging handlers<commit_after>define([
  "dojo/_base/declare"
, "dojo/when"
, "dojo/topic"
, "dojo/on"
, "dojo/dom-construct"
, "dojo/dom-style"
, "dojo/dom-attr"
, "dojo/dom-class"
, "dojo/dom-geometry"
, "dojo/_base/lang"

, "dijit/_WidgetBase"
, "dijit/_OnDijitClickMixin"
, "dijit/_Container"
, "dijit/layout/_ContentPaneResizeMixin"
, "dijit/_TemplatedMixin"
, "dijit/_WidgetsInTemplateMixin"
, "dijit/form/_FormValueMixin"

, "dgrid/OnDemandList"
, "dgrid/Selection"
, "dgrid/Keyboard"
, "dgrid/extensions/DijitRegistry"
, "put-selector/put"

, "hotplate/hotClientDojo/widgets/_OverlayMixin"
, "hotplate/hotClientDojo/stores/stores"

], function(
  declare
, when
, topic
, on
, domConstruct
, domStyle
, domAttr
, domClass
, domGeometry
, lang

, _WidgetBase
, _OnDijitClickMixin
, _Container
, _ContentPaneResizeMixin
, _TemplatedMixin
, _WidgetsInTemplateMixin
, _FormValueMixin

, OnDemandList
, Selection
, Keyboard
, DijitRegistry
, put

, _OverlayMixin
, stores

){

  return declare( [ _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, _Container, _ContentPaneResizeMixin, _OnDijitClickMixin, _FormValueMixin, _OverlayMixin ] , {

    templateString: '
\n' +
                    '
\n'+
                    '

Pick

\n' +
                    '

\n' +
                    '
\n'+
                    '

\n'+
                    '    <input type="hidden" ${!nameAttrSetting} data-dojo-attach-point="valueNode" value="${value}" aria-hidden="true" />\n'+
                    '
\n'+
                    '\n'+
                    '',

    widgetsInTemplate: true,

    // Store
    //store: null,

    ownClass: null,

    // Store name. Useful for topics etc.
    storeName: '',
    storeParameters: null,

    initialSort: [],
    initialFilter: null,

    // Refresh immediately on refreshData topic
    immediateRefresh: false,

    // renderRow which will be used by the created dgrid.
    // It needs to be defined
    renderRow: function( o ){ return "renderRow not defined"; } ,

    // The list widget
    listWidget: null,

    // Whether the listWidget is visible or not
    expanded: false,

    // When automatically moving based on the current window,
    // the minimum distance to the bottom
    distanceFromBottom: 20,

    _loadingValue: false,

    // Proxy method for listwidget.row()
    row: function( p ){
      if( this.listWidget ){
        return this.listWidget.row( p );
      }
    },

    resize: function(){
      this._moveIfNecessary();
      this.inherited(arguments);
    },

    destroy: function(){
      this.listWidget.destroy();
      this.inherited(arguments);
    },

    // STOLEN FROM _FormWidget.js
    postMixInProperties: function(){
      // Setup name=foo string to be referenced from the template (but only if a name has been specified)
      // Unfortunately we can't use _setNameAttr to set the name due to IE limitations, see #8484, #8660.
      // Regarding escaping, see heading "Attribute values" in
      // http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.3.2
      this.nameAttrSetting = this.name ? ('name="' + this.name.replace(/"/g, """) + '"') : '';
      this.inherited(arguments);
    },

    // Override automatic assigning type --> focusNode, it causes exception on IE.
    // Instead, type must be specified as ${type} in the template, as part of the original DOM
    _setTypeAttr: null,

    // Sets valueNode in the DOM
    _setValueAttr: function(value){

      this.inherited(arguments);
      var self = this;

      self._set( 'value', value );

      // Set the valueNode (the hidden field in this widget)
      domAttr.set(this.valueNode, "value", value);
      // domAttr.set(this.valueNode, "value", this.get("value"));

      if( value ){

        // If the row is displayed in the dgrid List, then take it from there...
        var row = self.row( value );
        if( row && row.data ){
          if( self.labelNode ){
            self.labelNode.innerHTML = '';
            put( self.labelNode, self.renderRow( row.data ) );
          }

        // Otherwise, if the raw is not in the dgrid list, load it up
        } else {

          when( self.store.get( value ) ).then(

            function( data ){
              self.labelNode.innerHTML = '';
              put( self.labelNode, self.renderRow( data ) );
            },
            function( err ) {
              self.labelNode.innerHTML = "ID: " + value;
            }

          );

          //self.labelNode.innerHTML = "ID: " + value;
        }
      }

    },

    startup: function(){
      this.inherited(arguments);
    },

    postCreate: function(){

      this.inherited(arguments);

      var self = this;

      // Make up the store internal variable
      this.store = stores( this.storeName, this.storeParameters );

      // Set this widget's own class, if defined in prototype
      if( self.ownClass ){
        domClass.add( self.domNode, self.ownClass );
      }

      // Overlayed, non-clickable by default
      //self.set( 'overlayStatus', { overlayed: true, clickable: false } );

      // Make up the constructor for the select list. It's basically a dgrid, with the
      // renderRow set as the parent's, and single selection mode
      var ListConstructor = declare( [ OnDemandList, Selection, Keyboard, DijitRegistry ], {
        renderRow: self.renderRow,
        selectionMode: 'single',
        //minRowsPerPage: 0, // Download the _lot_, or selected item might not be in memory

        // Adding and deleting rows will trigger the "position calculation"
        // function to make sure that the select list is not ourside of the
        // visible area
        newRow: function(){
          var r = this.inherited(arguments);
          self._moveIfNecessary();
          return r;
        },
        removeRow: function(){
          var r = this.inherited(arguments);
          self._moveIfNecessary();
          return r;
        },

        postCreate: function(){
          this.inherited(arguments);
        },

      } );

      // Make up the collection with the initial sorting/filters
      var collection = self.store;
      if( self.initialSort.length) collection = collection.sort( self.initialSort );
      if( self.initialFilter ) collection = collection.filter( self.initialFilter );

      // Make the dgrid widget.
      self.listWidget = new ListConstructor( { collection: collection });

      // Place the grid, invisible, in the right node, start it up, and
      // make it invisible
      domConstruct.place( self.listWidget.domNode, self.listWidgetNode );
      self.listWidget.startup();
      domStyle.set( self.listWidget.domNode, { display: 'none' } );

      self.own(

        self.listWidget.on("dgrid-select", function( e ) {
          var rowId = e.rows[0].id;
          self.set( 'value', rowId );
        }),

        // Reload ON can ne triggered by a click, or by a refreshData
        on( self.domNode, 'overlayClick', function( e ){
          self.set( 'overlayStatus', { overlayed: false, clickable: true } ); // CLICKME OFF
          self.set( 'overlayStatus', { overlayed: true, clickable: false } ); // LOADING ON
          self.listWidget.refresh();
        }),
        topic.subscribe('refreshData', function( ){
          if( self.immediateRefresh ){
            self.set( 'overlayStatus', { overlayed: true, clickable: false } ); // LOADING ON
            self.listWidget.refresh();
          } else {
            self.set( 'overlayStatus', { overlayed: true, clickable: true } ); // CLICKME ON
            collapseList();
          }
        }),

        // Reload OFF can be triggered by a refresh-complete or by a dgrid error
        on( self.listWidget.domNode, 'dgrid-refresh-complete', function( e ){
           self.set( 'overlayStatus', { overlayed: false, clickable: false } ); // LOADING OFF
           self.set( 'value', self.value );
        }),
        on( self.listWidget.domNode, 'dgrid-error', function( e ){
           self.set( 'overlayStatus', { overlayed: false, clickable: false } ); // LOADING OFF
           self.set( 'overlayStatus', { overlayed: true, clickable: true } ); // CLICKME ON
          topic.publish( 'globalAlert', 'Error: ' + e.error.message, 5000 );
          collapseList();
        }),

        // Escape pressed at body level: close the select box, select
        // value that selected in the first place when it was open
        //on( document.body, 'keydown', function( e ) {
        on( self.listWidget.domNode, 'keydown', function( e ) {
          if( e.keyCode == 27 && self.expanded){
            e.stopPropagation();
            domStyle.set( self.listWidget.domNode, { display: 'none' } );
            self.expanded = false;
            self.listWidget.clearSelection();
            self.listWidget.select( self.valueWhenOpen );
          }
        }),

        // Open or close when pressing enter
        on( self.domNode, 'keydown', function( e ){
          if( e.keyCode == 13 ){
            if( ! self.expanded  ){
              expandList();
            } else {
              collapseList( self.row( e ) );
            }
          }
        }),

        // Open or close when rlicking on it with mouse
        on( self.domNode, 'click', function( e ){
         // self.listWidget.refresh();
          if( ! self.expanded ){
            expandList();
          } else {
            collapseList( self.row( e ) );
         }
        }),

        // Focus out: hide the select box
        // FIXME: Haven't quite managed this one yet
        on( document.body, 'click', function( e ) {
          if( self.expanded){
          //  domStyle.set( self.listWidget.domNode, { display: 'none' } );
          //  self.expanded = false;
          }
        }),

        // If there is a change and store is queryEngine-less and has sorting/filtering, will need to refresh
        self.store.on( 'add,update,delete', function( event ){

          if( ! self.store.queryEngine && self.listWidget._renderedCollection.queryLog.length ) {

            // Zap partialResults, so that Observable doesn't actually work
            self.listWidget._renderedCollection._partialResults = null;

            // Refresh the data
            self.listWidget.refresh( { keepScrollPosition: true } );
          }
        }),

        // If the element currently selected is changed, need to re-render
        // the labelNode to reflect the changes.
        // Ditto for deletions.
        self.store.on( 'update', function( event ){
          if( event.target[ self.store.idProperty ] == self.value ){
            self.labelNode.innerHTML = '';
            put( self.labelNode, self.renderRow( event.target ) );
          }
        }),
        self.store.on( 'delete', function( event ){
          if( event.target[ self.store.idProperty ] == self.value ){
            self.labelNode.innerHTML = 'Item deleted';
          }
        })

      );

      // Function to expand the widget to include the select list.
      // It will do all of the required operations: make it visible in the CSS,
      // give the focus to the right element, mark it as "expanded", and move it
      // if necessary to do so (e.g. its bottom is out of the visible screen)
      function expandList(){
        self.valueWhenOpen = self.value;
        domStyle.set( self.listWidget.domNode, { display: 'block' } );
        if( typeof( self.value ) !== 'undefined' && self.value !== null ){
          var r = self.row( self.value );
          self.listWidget.focus( r );
        }
        self.expanded = true;
        self._moveIfNecessary();
      }

      // Function to collapse the widget so that the select list is no longe there.
      // It will just make it invisible, but it will also set the widget value to
      // the row element. This is to be called once a pick is successful, either
      // with an Enter or with a mouse click.
      function collapseList( row ){

        domStyle.set( self.listWidget.domNode, { display: 'none' } );
        self.expanded = false;
        if( row ){
          self.set( 'value', row.id);
        }
      }

    },

    // Te dgrid might grow to the point that its bottom is outside of the
    // actual visible area. This would be bad (the bottom elements would be
    // unreachable). So, it calculates things up and makes sure that the dgrid
    // is moved up, also considering the required self.distanceFromBottom
    _moveIfNecessary: function(){
      var self = this;

      //self.listWidget.resize();
      domStyle.set( self.listWidget.domNode, { top: '' } );
      var bodyBox = domGeometry.getContentBox( document.body );
      var listBox = domGeometry.position( self.listWidget.domNode, true );
      var delta = bodyBox.h - ( listBox.y + listBox.h ) - self.distanceFromBottom;
      if( delta < 0 ){
        domStyle.set( self.listWidget.domNode, { top: (delta)+'px' } );
      } else {
        domStyle.set( self.listWidget.domNode, { top: '' } );
      }

    },

  });

});

<|endoftext|>


<commit_before>module.controller('TagsPanel', [
	'$scope', 'pageService', function ($scope, page) {
		var DEFAULT_MESSAGE = {
			type: 'blue',
			countdown: 3000
		};

		//

		var createMessage = function (message) {
			if (typeof(message) == 'string') {
				page.messages.add(angular.extend({}, DEFAULT_MESSAGE, {
					body: message
				}));
			}
			else {
				page.messages.add(angular.extend({}, DEFAULT_MESSAGE, message));
			}
		};

		//

		$scope.tags = [];
		$scope.target = {
			container: null,
			segment: null
		};

		$scope.refreshPanel = function () {
			switch (page.$route.current.controller) {
				case 'VolumeView':
					$scope.prepareTags($scope.volume.tags);
					$scope.target.container = $scope.volume.top.id;
					$scope.target.segment = ',';
					$scope.enabled = true;

					$scope.enabled = $scope.tags.length > 0 || $scope.auth.isLoggedIn();
					break;

				case 'PartyView':
					$scope.prepareTags($scope.party.tags);

					$scope.enabled = $scope.tags.length > 0;
					break;
			}
		};

		$scope.prepareTags = function (tags) {
			var temp = [];

			angular.forEach(tags, function (tag) {
				temp.push(tag);
			});

			$scope.tags = temp;

			$scope.sortTags();
		};

		$scope.sortTags = function () {
			$scope.tags = $scope.tags.sort(function (a, b) {
				return (a.weight > b.weight) ? -1 : (a.weight < b.weight) ? 1 : 0;
			});
		};

		$scope.retrieveTags = function () {
			switch (page.$route.current.controller) {
				case 'VolumeView':
					page.models.Volume.$cache.removeAll();

					page.models.Volume.get({
						id: $scope.volume.id,
						tags: ''
					}, function (data) {
						$scope.volume.tags = data.tags;
						$scope.refreshPanel();
					}, function (res) {
						page.messages.addError({
							body: page.constants.message('tags.update.error'),
							report: res,
						})
					});

					break;
			}
		};

		//

		$scope.vote = function (tag, vote) {
			var tagModel = new page.models.Tag({id: tag.id});

			tagModel.$save({
				id: tag.id,
				vote: vote == -1 ? 'false' : vote == 1 ? "true" : "",
				container: $scope.target.container,
				segment: $scope.target.segment
			}, function (newTag) {
				if (newTag.weight == 0 && !newTag.vote) {
					$scope.tags.splice($scope.tags.indexOf(tag), 1);
				}
				else {
					$scope.tags.splice($scope.tags.indexOf(tag), 1, newTag);
				}

				switch (vote) {
					case -1:
						createMessage(page.constants.message('tags.vote.down.success', tag.id));
						break;

					case 0:
						createMessage(page.constants.message('tags.vote.null.success', tag.id));
						break;

					case 1:
						createMessage(page.constants.message('tags.vote.up.success', tag.id));
						break;
				}

				hideTooltips();
			}, function (res) {
				page.messages.addError({
					body: page.constants.message('tags.vote.error', tag.id),
					report: res,
				});

				hideTooltips();
			});
		};

		$scope.voteNew = function (form) {
			if (form.$invalid || !form.newNameVal) {
				return;
			}

			emptyAuto();

			var tagModel = new page.models.Tag({id: form.newNameVal});
			var data = {
				id: form.newNameVal,
				vote: "true",
				container: $scope.target.container,
				segment: $scope.target.segment
			};

			form.newNameVal = '';

			tagModel.$save(data, function (newTag, status, headers, config) {
				createMessage(page.constants.message('tags.new.success', data.id));
				emptyAuto();

				$scope.retrieveTags();
				hideTooltips();
			}, function (res) {
				page.messages.addError({
					body: page.constants.message('tags.new.error', data.id),
					report: res,
				});

				emptyAuto();
				hideTooltips();
			});
		};

		//

		$scope.newNameChange = function (form) {
			if (form.newName.$dirty && form.newName.$valid) {
				updateAuto(form);
			}

			if (form.newName.$pristine || form.newName.$valid) {
				return disableNewNameError();
			}

			return enableNewNameError();
		};

		var keypress = function (event, form) {
			if (event.which == 40) {
				// down
				if (angular.isUndefined($scope.autoSelect) || $scope.autoSelect == $scope.autoList.length - 1) {
					$scope.autoSelect = 0;
				}
				else {
					$scope.autoSelect = $scope.autoSelect + 1;
				}

				return;
			} else if (event.which == 38) {
				// up
				if (angular.isUndefined($scope.autoSelect) || $scope.autoSelect == 0) {
					$scope.autoSelect = $scope.autoList.length - 1;
				}
				else {
					$scope.autoSelect = $scope.autoSelect - 1;
				}

				return;
			} else if (event.which == 13) {
				// enter
				if (angular.isDefined($scope.autoSelect)) {
					$scope.fillAuto(form, $scope.autoList[$scope.autoSelect]);
				}

			} else if (event.which == 27) {
				// escape
				emptyAuto();
			}
		};

		$scope.newNameFocus = function (form) {
			emptyAuto();

			$('#newName').on('keydown', function (event) {
				$scope.$apply(function () {
					keypress(event, form);
				});
			});

			$scope.newNameChange(form);
		};

		$scope.newNameBlur = function (form) {
			$('#newName').off('keypress');

			emptyAutoAfter(250);

			return disableNewNameError();
		};

		$scope.autoList = [];
		$scope.autoSelect = undefined;

		var updateAuto = function (form) {
			page.$http
				.get('/api/tag', {
					params: {
						query: form.newNameVal
					}
				}).success(function (data) {
					emptyAuto();

					if (form.newNameVal) {
						$scope.autoList = data;
					}
				}).error(function (errors, status) {
					page.messages.addError({
						body: page.constants.message('tags.auto.error'),
						errors: errors,
						status: status
					});

					emptyAuto();
				});
		};

		var emptyAuto = function () {
			$scope.autoList = [];
			$scope.autoSelect = undefined;
		};

		var emptyAutoAfter = function (after) {
			page.$timeout(function () {
				emptyAuto();
			}, angular.isNumber(after) ? after : 1);
		};

		$scope.fillAuto = function (form, autoTag) {
			form.newNameVal = autoTag;
			emptyAuto();
			$scope.voteNew(form);
		};

		//

		var enableNewNameError = function () {
			emptyAuto();

			if ($scope.tagNewFormMessage) {
				page.messages.enable($scope.tagNewFormMessage);
			} else {
				var message = {
					enabled: true,
					type: 'red',
					body: '
' +
						'
Tag Name
' +
						'
Must be between 3 and 32 characters.
' +
						'
Only letters, spaces, and dashes (-) allowed.
' +
						'
'
				};

				$scope.tagNewFormMessage = page.messages.add(message);
			}
		};

		var disableNewNameError = function () {
			if ($scope.tagNewFormMessage) {
				page.messages.disable($scope.tagNewFormMessage);
			}
		};

		//

		var tips = [];

		var bindTooltips = function () {
			var unsetTips = {
				'.panel_tags_list .vote.available.up': page.constants.message('tags.vote.up'),
				'.panel_tags_list .vote.available.null': page.constants.message('tags.vote.null'),
				'.panel_tags_list .vote.available.down': page.constants.message('tags.vote.down')
			};

			angular.forEach(unsetTips, function (message, target) {
				tips.push(page.tooltips.add({
					live: true,
					$target: target,
					message: message
				}));
			});
		};

		bindTooltips();

		var hideTooltips = function () {
			angular.forEach(tips, function (tip) {
				page.tooltips.hide(tip);
			});
		};

		$scope.$on('$destroy', function () {
			angular.forEach(tips, function (tip) {
				page.tooltips.remove(tip);
			});

			tips = [];
		});
	}
]);
<commit_msg>change default tag message color<commit_after>module.controller('TagsPanel', [
	'$scope', 'pageService', function ($scope, page) {
		var DEFAULT_MESSAGE = {
			type: 'green',
			countdown: 3000,
		};

		//

		var createMessage = function (message) {
			if (typeof(message) == 'string') {
				page.messages.add(angular.extend({}, DEFAULT_MESSAGE, {
					body: message,
				}));
			}
			else {
				page.messages.add(angular.extend({}, DEFAULT_MESSAGE, message));
			}
		};

		//

		$scope.tags = [];
		$scope.target = {
			container: null,
			segment: null
		};

		$scope.refreshPanel = function () {
			switch (page.$route.current.controller) {
				case 'VolumeView':
					$scope.prepareTags($scope.volume.tags);
					$scope.target.container = $scope.volume.top.id;
					$scope.target.segment = ',';
					$scope.enabled = true;

					$scope.enabled = $scope.tags.length > 0 || $scope.auth.isLoggedIn();
					break;

				case 'PartyView':
					$scope.prepareTags($scope.party.tags);

					$scope.enabled = $scope.tags.length > 0;
					break;
			}
		};

		$scope.prepareTags = function (tags) {
			var temp = [];

			angular.forEach(tags, function (tag) {
				temp.push(tag);
			});

			$scope.tags = temp;

			$scope.sortTags();
		};

		$scope.sortTags = function () {
			$scope.tags = $scope.tags.sort(function (a, b) {
				return (a.weight > b.weight) ? -1 : (a.weight < b.weight) ? 1 : 0;
			});
		};

		$scope.retrieveTags = function () {
			switch (page.$route.current.controller) {
				case 'VolumeView':
					page.models.Volume.$cache.removeAll();

					page.models.Volume.get({
						id: $scope.volume.id,
						tags: ''
					}, function (data) {
						$scope.volume.tags = data.tags;
						$scope.refreshPanel();
					}, function (res) {
						page.messages.addError({
							body: page.constants.message('tags.update.error'),
							report: res,
						})
					});

					break;
			}
		};

		//

		$scope.vote = function (tag, vote) {
			var tagModel = new page.models.Tag({id: tag.id});

			tagModel.$save({
				id: tag.id,
				vote: vote == -1 ? 'false' : vote == 1 ? "true" : "",
				container: $scope.target.container,
				segment: $scope.target.segment
			}, function (newTag) {
				if (newTag.weight == 0 && !newTag.vote) {
					$scope.tags.splice($scope.tags.indexOf(tag), 1);
				}
				else {
					$scope.tags.splice($scope.tags.indexOf(tag), 1, newTag);
				}

				switch (vote) {
					case -1:
						createMessage(page.constants.message('tags.vote.down.success', tag.id));
						break;

					case 0:
						createMessage(page.constants.message('tags.vote.null.success', tag.id));
						break;

					case 1:
						createMessage(page.constants.message('tags.vote.up.success', tag.id));
						break;
				}

				hideTooltips();
			}, function (res) {
				page.messages.addError({
					body: page.constants.message('tags.vote.error', tag.id),
					report: res,
				});

				hideTooltips();
			});
		};

		$scope.voteNew = function (form) {
			if (form.$invalid || !form.newNameVal) {
				return;
			}

			emptyAuto();

			var tagModel = new page.models.Tag({id: form.newNameVal});
			var data = {
				id: form.newNameVal,
				vote: "true",
				container: $scope.target.container,
				segment: $scope.target.segment
			};

			form.newNameVal = '';

			tagModel.$save(data, function (newTag, status, headers, config) {
				createMessage(page.constants.message('tags.new.success', data.id));
				emptyAuto();

				$scope.retrieveTags();
				hideTooltips();
			}, function (res) {
				page.messages.addError({
					body: page.constants.message('tags.new.error', data.id),
					report: res,
				});

				emptyAuto();
				hideTooltips();
			});
		};

		//

		$scope.newNameChange = function (form) {
			if (form.newName.$dirty && form.newName.$valid) {
				updateAuto(form);
			}

			if (form.newName.$pristine || form.newName.$valid) {
				return disableNewNameError();
			}

			return enableNewNameError();
		};

		var keypress = function (event, form) {
			if (event.which == 40) {
				// down
				if (angular.isUndefined($scope.autoSelect) || $scope.autoSelect == $scope.autoList.length - 1) {
					$scope.autoSelect = 0;
				}
				else {
					$scope.autoSelect = $scope.autoSelect + 1;
				}

				return;
			} else if (event.which == 38) {
				// up
				if (angular.isUndefined($scope.autoSelect) || $scope.autoSelect == 0) {
					$scope.autoSelect = $scope.autoList.length - 1;
				}
				else {
					$scope.autoSelect = $scope.autoSelect - 1;
				}

				return;
			} else if (event.which == 13) {
				// enter
				if (angular.isDefined($scope.autoSelect)) {
					$scope.fillAuto(form, $scope.autoList[$scope.autoSelect]);
				}

			} else if (event.which == 27) {
				// escape
				emptyAuto();
			}
		};

		$scope.newNameFocus = function (form) {
			emptyAuto();

			$('#newName').on('keydown', function (event) {
				$scope.$apply(function () {
					keypress(event, form);
				});
			});

			$scope.newNameChange(form);
		};

		$scope.newNameBlur = function (form) {
			$('#newName').off('keypress');

			emptyAutoAfter(250);

			return disableNewNameError();
		};

		$scope.autoList = [];
		$scope.autoSelect = undefined;

		var updateAuto = function (form) {
			page.$http
				.get('/api/tag', {
					params: {
						query: form.newNameVal
					}
				}).success(function (data) {
					emptyAuto();

					if (form.newNameVal) {
						$scope.autoList = data;
					}
				}).error(function (errors, status) {
					page.messages.addError({
						body: page.constants.message('tags.auto.error'),
						errors: errors,
						status: status
					});

					emptyAuto();
				});
		};

		var emptyAuto = function () {
			$scope.autoList = [];
			$scope.autoSelect = undefined;
		};

		var emptyAutoAfter = function (after) {
			page.$timeout(function () {
				emptyAuto();
			}, angular.isNumber(after) ? after : 1);
		};

		$scope.fillAuto = function (form, autoTag) {
			form.newNameVal = autoTag;
			emptyAuto();
			$scope.voteNew(form);
		};

		//

		var enableNewNameError = function () {
			emptyAuto();

			if ($scope.tagNewFormMessage) {
				page.messages.enable($scope.tagNewFormMessage);
			} else {
				var message = {
					enabled: true,
					type: 'red',
					body: '
' +
						'
Tag Name
' +
						'
Must be between 3 and 32 characters.
' +
						'
Only letters, spaces, and dashes (-) allowed.
' +
						'
'
				};

				$scope.tagNewFormMessage = page.messages.add(message);
			}
		};

		var disableNewNameError = function () {
			if ($scope.tagNewFormMessage) {
				page.messages.disable($scope.tagNewFormMessage);
			}
		};

		//

		var tips = [];

		var bindTooltips = function () {
			var unsetTips = {
				'.panel_tags_list .vote.available.up': page.constants.message('tags.vote.up'),
				'.panel_tags_list .vote.available.null': page.constants.message('tags.vote.null'),
				'.panel_tags_list .vote.available.down': page.constants.message('tags.vote.down')
			};

			angular.forEach(unsetTips, function (message, target) {
				tips.push(page.tooltips.add({
					live: true,
					$target: target,
					message: message
				}));
			});
		};

		bindTooltips();

		var hideTooltips = function () {
			angular.forEach(tips, function (tip) {
				page.tooltips.hide(tip);
			});
		};

		$scope.$on('$destroy', function () {
			angular.forEach(tips, function (tip) {
				page.tooltips.remove(tip);
			});

			tips = [];
		});
	}
]);
<|endoftext|>


<commit_before>import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import ReduxThunk from 'redux-thunk';

// Routes
import Routes from './routes/';

// Reducers
import RootReducers from './reducers/';

// configure Redux extension
const componseEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_CHROME__ || compose;

// default store
// const storeWithMiddlewares = applyMiddleware(
//   ReduxThunk
// )(createStore);
// export const store = storeWithMiddlewares(RootReducers);
export const store = createStore(RootReducers, {}, componseEnhancers(
  applyMiddleware(ReduxThunk)
));

// for debuggin
// window.store = store;

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Provider store={store}>
      { Routes }
    </Provider>
    , document.getElementById('app')
  );
});<commit_msg>Validate if user has already sigin<commit_after>import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import ReduxThunk from 'redux-thunk';
// Actions
import { authUser, setUserData } from './actions/auth-actions';
// Routes
import Routes from './routes/';
// Reducers
import RootReducers from './reducers/';
// APIs
import AuthAPI from './utils/api/auth-api';
// utils
import { verifyToken, getSession } from './utils/api/header-config';
// configure Redux extension
const componseEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// default store
// const storeWithMiddlewares = applyMiddleware(
//   ReduxThunk
// )(createStore);
// export const store = storeWithMiddlewares(RootReducers);
export const store = createStore(RootReducers, {}, componseEnhancers(
  applyMiddleware(ReduxThunk.withExtraArgument({ AuthAPI }))
));

// validate session
const session = getSession();
if (session && verifyToken()) {
  store.dispatch(authUser());
  store.dispatch(setUserData(session));
}

// for debuggin
// window.store = store;

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Provider store={store}>
      { Routes }
    </Provider>
    , document.getElementById('app')
  );
});<|endoftext|>


<commit_before>/*global document */

import { Class } from '../../core/Core.js';
import '../../foundation/ComputedProps.js';  // For Function#property, #nocache
import '../../foundation/EventTarget.js';  // For Function#on
import '../../foundation/ObservableProps.js';  // For Function#observes
import UA from '../../ua/UA.js';
import Element from '../../dom/Element.js';
import { lookupKey } from '../../dom/DOMEvent.js';
import ScrollView from '../containers/ScrollView.js';
import AbstractControlView from './AbstractControlView.js';

const nativePlaceholder = 'placeholder' in document.createElement( 'input' );
const isFirefox = !!UA.firefox;

/**
    Class: O.TextView

    Extends: O.AbstractControlView

    A text input control. The `value` property is two-way bindable, representing
    the input text.
*/
const TextView = Class({

    Extends: AbstractControlView,

    init (/* ...mixins */) {
        TextView.parent.init.apply( this, arguments );
        this._settingFromInput = false;
    },

    /**
        Property: O.TextView#isMultiline
        Type: Boolean
        Default: false

        If set to true, the text field will accept line breaks.

        This property *must not* be changed after the view has been rendered.
    */
    isMultiline: false,

    /**
        Property: O.TextView#isExpanding
        Type: Boolean
        Default: false

        If <#isMultiline> is set to true, setting <#isExpanding> to true will
        make it automatically expand vertically to fit its contents, rather than
        show a scrollbar.
    */
    isExpanding: false,

    /**
        Property: O.TextView#isValid
        Type: Boolean
        Default: true

        If false, an `invalid' class will be added to the view's class name.
    */
    isValid: true,

    /**
        Property: O.TextView#isHighlighted
        Type: Boolean
        Default: false

        If true, a `highlight` class will be added to the view's class name.
        This is a styling hook for highlighting the view, e.g. if it fails
        validation.
    */
    isHighlighted: false,

    /**
        Property: O.TextView#inputType
        Type: String
        Default: "text"

        The type property for the <input> DOM node (e.g. "password", "tel" etc.)

        This property *must not* be changed after the view has been rendered.
    */
    inputType: 'text',

    /**
        Property: O.TextView#placeholder
        Type: String
        Default: ''

        Placeholder text to be displayed in the text input when it is empty.
    */
    placeholder: '',

    /**
        Property: O.TextView#value
        Type: String
        Default: ''

        The value currently input in the text field.
    */
    value: '',

    /**
        Property: O.TextView#inputAttributes
        Type: Object

        Extra attributes to add to the text view. Examples include:

        - maxLength: Number
        - autocomplete: 'on' or 'off'
        - autocapitalize: 'on' or 'off'
        - autocorrect: 'on' or 'off'
        - pattern: String (regexp)
    */
    inputAttributes: {
        autocomplete: 'off',
    },

    /**
        Property: O.TextView#selection
        Type: Object

        When used as a getter, this will return an object with two properties:

        start - {Number} The number of characters offset from the beginning of
                the text that the selection starts.
        end   - {Number} The number of characters offset from the beginning of
                the text that the selection ends.

        Note, if there is no selection, the start and end values will be the
        same, and give the position of the cursor.

        When used as a setter, you can give it an object as described above to
        set the selection, or if you just want to give it a cursor position, you
        can pass a number instead.

        Note, this property is *not observable* and cannot be used to monitor
        changes in selection/cursor position.

    */
    selection: function ( selection ) {
        const control = this._domControl;
        const isNumber = ( typeof selection === 'number' );
        let start = selection ? isNumber ?
                    selection : selection.start : 0;
        let end = selection ? isNumber ?
                    selection : selection.end || start : start;
        if ( selection !== undefined ) {
            // Ensure any value changes have been drawn.
            this.redraw();
            // Firefox will throw an error if the control is not actually in the
            // document when trying to set the selection. There might be other
            // situations where it does so as well, so just using a try/catch to
            // guard against all.
            try {
                control.setSelectionRange( start, end );
            } catch ( error ) {}
        } else {
            // Firefox sometimes throws an error if you try to read the
            // selection. Again, probably if the control is not actually in the
            // document.
            try {
                start = control.selectionStart;
                end = control.selectionEnd;
            } catch ( error ) {}
        }
        return selection || {
            start,
            end,
        };
    }.property().nocache(),

    /**
        Property: O.TextView#blurOnKeys
        Type: Object
        Default: { esc: true }

        For each truthy value in the object, if the user is focussed in the
        text view and hits the key, the focus will be removed.
    */
    blurOnKeys: { esc: true },

    // --- Render ---

    /**
        Property: O.TextView#allowTextSelection
        Type: Boolean
        Default: true

        Overrides default in <O.View#allowTextSelection>.
    */
    allowTextSelection: true,

    /**
        Property: O.TextView#type
        Type: String

        Will be added to the view's class name.
    */
    type: '',

    layerTag: 'span',

    /**
        Property: O.TextView#className
        Type: String

        Overrides default in <O.View#className>. Will have the class `v-Text`,
        and any classes given in the <#type> property, along with the following
        other class names dependent on state:

        is-highlight - The <#isHighlighted> property is true.
        is-focussed  - The <#isFocussed> property is true.
        is-invalid   - The <#isValid> property is false.
        is-disabled  - The <#isDisabled> property is true.
    */
    className: function () {
        const type = this.get( 'type' );
        return 'v-Text' +
            ( this.get( 'isExpanding' ) ? ' v-Text--expanding' : '' ) +
            ( this.get( 'isHighlighted' ) ? ' is-highlighted' : '' ) +
            ( this.get( 'isFocussed' ) ? ' is-focussed' : '' ) +
            ( this.get( 'isValid' ) ? '' : ' is-invalid' ) +
            ( this.get( 'isDisabled' ) ? ' is-disabled' : '' ) +
            ( type ? ' ' + type : '' );
    }.property( 'type', 'isHighlighted',
        'isFocussed', 'isValid', 'isDisabled' ),

    layerStyles: function () {
        return Object.assign({
            position: this.get( 'positioning' ),
            display: this.get( 'isMultiline' ) ? 'block' : 'inline-block',
            cursor: 'text',
            userSelect: 'text',
        }, this.get( 'layout' ) );
    }.property( 'layout', 'positioning' ),

    /**
        Method: O.TextView#draw

        Overridden to draw view. See <O.View#draw>.
    */
    draw ( layer, Element, el ) {
        const value = this.get( 'value' );
        const placeholder = this.get( 'placeholder' );
        const isMultiline = this.get( 'isMultiline' );
        const control = this._domControl = el(
                isMultiline ? 'textarea' : 'input', {
                    id: this.get( 'id' ) + '-input',
                    className: 'v-Text-input',
                    rows: isMultiline ? '1' : undefined,
                    name: this.get( 'name' ),
                    type: this.get( 'inputType' ),
                    disabled: this.get( 'isDisabled' ),
                    tabIndex: this.get( 'tabIndex' ),
                    value,
                });

        this.redrawInputAttributes();

        if ( placeholder ) {
            if ( nativePlaceholder ) {
                control.placeholder = placeholder;
            } else if ( !value ) {
                this._placeholderShowing = true;
                Element.addClass( control, 'v-Text-input--placeholder' );
                control.value = placeholder;
            }
        }

        layer.title = this.get( 'tooltip' );

        return [
            control,
        ];
    },

    // --- Keep render in sync with state ---

    /**
        Method: O.TextView#textNeedsRedraw

        Calls <O.View#propertyNeedsRedraw> for extra properties requiring
        redraw.
    */
    textNeedsRedraw: function ( self, property, oldValue ) {
        const isValue = ( property === 'value' );
        if ( !isValue || !this._settingFromInput ) {
            this.propertyNeedsRedraw( self, property, oldValue );
        }
        if ( isValue && this.get( 'isExpanding' ) ) {
            this.propertyNeedsRedraw( self, 'textHeight', oldValue );
        }
    }.observes( 'isExpanding', 'value', 'placeholder', 'inputAttributes' ),

    /**
        Method: O.TextView#redrawValue

        Updates the content of the `<textarea>` or `<input>` to match the
        <#value> property.
    */
    redrawValue () {
        const value = this.get( 'value' );
        this._domControl.value = value;
        // Ensure placeholder is updated.
        if ( !nativePlaceholder && !this.get( 'isFocussed' ) ) {
            this._setPlaceholder();
        }
    },

    /**
        Method: O.TextView#redrawPlaceholder

        Updates the placeholder text in the DOM when the <#placeholder> property
        changes.
    */
    redrawPlaceholder () {
        const placeholder = this.get( 'placeholder' );
        const control = this._domControl;
        if ( nativePlaceholder ) {
            control.placeholder = placeholder;
        } else if ( this._placeholderShowing ) {
            control.value = placeholder;
        }
    },

    /**
        Method: O.TextView#redrawInputAttributes

        Updates any other properties of the `<input>` element.
    */
    redrawInputAttributes () {
        const inputAttributes = this.get( 'inputAttributes' );
        const control = this._domControl;
        for ( const property in inputAttributes ) {
            control.set( property, inputAttributes[ property ] );
        }
    },

    redrawTextHeight () {
        // Firefox gets pathologically slow when resizing really large text
        // areas, so automatically turn this off in such a case.
        // 2^13 chars is an arbitrary cut off point that seems to be reasonable
        // in practice
        if ( isFirefox && this.get( 'value' ).length > 8192 ) {
            this.set( 'isExpanding', false );
            return;
        }
        const control = this._domControl;
        const style = control.style;
        const scrollView = this.getParent( ScrollView );
        // Set to auto to collapse it back to one line, otherwise it would
        // never shrink if you delete text.
        style.height = 'auto';
        const scrollHeight = control.scrollHeight;
        // Presto returns 0 immediately after appending to doc.
        if ( scrollHeight ) {
            style.height = scrollHeight + 'px';
        }
        // Collapsing the height will mess with the scroll, so make sure we
        // reset the scroll position back to what it was.
        if ( scrollView ) {
            scrollView.redrawScroll();
        }
    },

    redrawIsExpanding () {
        if ( this.get( 'isExpanding' ) ) {
            this.redrawTextHeight();
        } else {
            this._domControl.style.height = 'auto';
            // Scroll to cursor
            if ( this.get( 'isFocussed' ) ) {
                this.blur().focus();
            }
        }
    },

    redrawLabel () {},

    // --- Activate ---

    /**
        Method: O.TextView#activate

        Overridden to focus the text view. See <O.AbstractControlView#activate>.
    */
    activate () {
        this.focus();
    },

    // --- Scrolling and focus ---

    savedSelection: null,

    /**
        Method: O.TextView#didEnterDocument

        Overridden to restore scroll position and selection. See
        <O.View#didEnterDocument>.
    */
    didEnterDocument () {
        TextView.parent.didEnterDocument.call( this );
        if ( this.get( 'isMultiline' ) ) {
            if ( this.get( 'isExpanding' ) ) {
                this.redrawTextHeight();
            }
            // Restore scroll positions:
            const control = this._domControl;
            const left = this.get( 'scrollLeft' );
            const top = this.get( 'scrollTop' );
            if ( left ) { control.scrollLeft = left; }
            if ( top ) { control.scrollTop = top; }
            control.addEventListener( 'scroll', this, false );
        }
        const selection = this.get( 'savedSelection' );
        if ( selection ) {
            this.set( 'selection', selection ).focus();
            this.set( 'savedSelection', null );
        }
        return this;
    },

    /**
        Method: O.TextView#willLeaveDocument

        Overridden to save scroll position and selection. See
        <O.View#willLeaveDocument>.
    */
    willLeaveDocument () {
        // If focussed, save cursor position
        if ( this.get( 'isFocussed' ) ) {
            this.set( 'savedSelection', this.get( 'selection' ) );
            this.blur();
        }
        // Stop listening for scrolls:
        if ( this.get( 'isMultiline' ) ) {
            this._domControl.removeEventListener( 'scroll', this, false );
        }
        return TextView.parent.willLeaveDocument.call( this );
    },

    /**
        Method (private): O.TextView#_syncBackScrolls

        Sets the <O.View#scrollLeft> and <O.View#scrollTop> properties whenever
        the user scrolls the textarea.

        Parameters:
            event - {Event} The scroll event.
    */
    _syncBackScrolls: function ( event ) {
        const control = this._domControl;
        const left = control.scrollLeft;
        const top = control.scrollTop;

        this.beginPropertyChanges()
            .set( 'scrollLeft', left )
            .set( 'scrollTop', top )
        .endPropertyChanges();

        event.stopPropagation();
    }.on( 'scroll' ),

    // --- Keep state in sync with render ---

    /**
        Method: O.TextView#syncBackValue

        Updates the <#value> property when the user interacts with the textarea.

        Parameters:
            event - {Event} The input event.
    */
    syncBackValue: function () {
        this._settingFromInput = true;
        if ( nativePlaceholder || !this._placeholderShowing ) {
            this.set( 'value', this._domControl.value );
        }
        this._settingFromInput = false;
    }.on( 'input' ),

    /**
        Method (private): O.TextView#_setPlaceholder

        Sets/removes the placholder text for browsers that don't support this
        natively.
    */
    _setPlaceholder: nativePlaceholder ? null :
    function ( _, __, ___, isFocussed ) {
        const control = this._domControl;
        if ( isFocussed ) {
            if ( this._placeholderShowing ) {
                this._placeholderShowing = false;
                Element.removeClass( control, 'v-Text-input--placeholder' );
                control.value = '';
            }
        } else {
            const placeholder = this.get( 'placeholder' );
            if ( placeholder && !this.get( 'value' ) ) {
                this._placeholderShowing = true;
                Element.addClass( control, 'v-Text-input--placeholder' );
                control.value = placeholder;
            }
        }
    }.observes( 'isFocussed' ),

    /**
        Method (private): O.TextView#_onClick

        Focus and set selection to the end.

        Parameters:
            event - {Event} The click event.
    */
    _onClick: function ( event ) {
        if ( event.target === this.get( 'layer' ) ) {
            this.set( 'selection', this.get( 'value' ).length )
                .focus();
        }
    }.on( 'click' ),

    /**
        Method (private): O.TextView#_onKeypress

        Stop IE automatically focussing the nearest button when the user hits
        enter in single line text inputs.

        Parameters:
            event - {Event} The keypress event.
    */
    _onKeypress: function ( event ) {
        // If key == enter, IE will automatically focus the nearest button
        // (presumably as though it were submitting the form). Stop this
        // unless we're actually in a form.
        if ( !this.get( 'isMultiline' ) &&
                lookupKey( event, true ) === 'enter' &&
                !Element.nearest( this.get( 'layer' ), 'FORM' ) ) {
            event.preventDefault();
        }
    }.on( 'keypress' ),

    /**
        Method (private): O.TextView#_blurOnKey

        Blur the text area when the user hits certain keys, provided by the
        <#blurOnKeys> property.

        Parameters:
            event - {Event} The keyup event.
    */
    _blurOnKey: function ( event ) {
        const key = lookupKey( event, true );
        if ( this.get( 'blurOnKeys' )[ key ] ) {
            this.blur();
        }
    }.on( 'keyup' ),
});

export default TextView;
<commit_msg>Fix crash in Firefox when undefined TextView#value<commit_after>/*global document */

import { Class } from '../../core/Core.js';
import '../../foundation/ComputedProps.js';  // For Function#property, #nocache
import '../../foundation/EventTarget.js';  // For Function#on
import '../../foundation/ObservableProps.js';  // For Function#observes
import UA from '../../ua/UA.js';
import Element from '../../dom/Element.js';
import { lookupKey } from '../../dom/DOMEvent.js';
import ScrollView from '../containers/ScrollView.js';
import AbstractControlView from './AbstractControlView.js';

const nativePlaceholder = 'placeholder' in document.createElement( 'input' );
const isFirefox = !!UA.firefox;

/**
    Class: O.TextView

    Extends: O.AbstractControlView

    A text input control. The `value` property is two-way bindable, representing
    the input text.
*/
const TextView = Class({

    Extends: AbstractControlView,

    init (/* ...mixins */) {
        TextView.parent.init.apply( this, arguments );
        this._settingFromInput = false;
    },

    /**
        Property: O.TextView#isMultiline
        Type: Boolean
        Default: false

        If set to true, the text field will accept line breaks.

        This property *must not* be changed after the view has been rendered.
    */
    isMultiline: false,

    /**
        Property: O.TextView#isExpanding
        Type: Boolean
        Default: false

        If <#isMultiline> is set to true, setting <#isExpanding> to true will
        make it automatically expand vertically to fit its contents, rather than
        show a scrollbar.
    */
    isExpanding: false,

    /**
        Property: O.TextView#isValid
        Type: Boolean
        Default: true

        If false, an `invalid' class will be added to the view's class name.
    */
    isValid: true,

    /**
        Property: O.TextView#isHighlighted
        Type: Boolean
        Default: false

        If true, a `highlight` class will be added to the view's class name.
        This is a styling hook for highlighting the view, e.g. if it fails
        validation.
    */
    isHighlighted: false,

    /**
        Property: O.TextView#inputType
        Type: String
        Default: "text"

        The type property for the <input> DOM node (e.g. "password", "tel" etc.)

        This property *must not* be changed after the view has been rendered.
    */
    inputType: 'text',

    /**
        Property: O.TextView#placeholder
        Type: String
        Default: ''

        Placeholder text to be displayed in the text input when it is empty.
    */
    placeholder: '',

    /**
        Property: O.TextView#value
        Type: String
        Default: ''

        The value currently input in the text field.
    */
    value: '',

    /**
        Property: O.TextView#inputAttributes
        Type: Object

        Extra attributes to add to the text view. Examples include:

        - maxLength: Number
        - autocomplete: 'on' or 'off'
        - autocapitalize: 'on' or 'off'
        - autocorrect: 'on' or 'off'
        - pattern: String (regexp)
    */
    inputAttributes: {
        autocomplete: 'off',
    },

    /**
        Property: O.TextView#selection
        Type: Object

        When used as a getter, this will return an object with two properties:

        start - {Number} The number of characters offset from the beginning of
                the text that the selection starts.
        end   - {Number} The number of characters offset from the beginning of
                the text that the selection ends.

        Note, if there is no selection, the start and end values will be the
        same, and give the position of the cursor.

        When used as a setter, you can give it an object as described above to
        set the selection, or if you just want to give it a cursor position, you
        can pass a number instead.

        Note, this property is *not observable* and cannot be used to monitor
        changes in selection/cursor position.

    */
    selection: function ( selection ) {
        const control = this._domControl;
        const isNumber = ( typeof selection === 'number' );
        let start = selection ? isNumber ?
                    selection : selection.start : 0;
        let end = selection ? isNumber ?
                    selection : selection.end || start : start;
        if ( selection !== undefined ) {
            // Ensure any value changes have been drawn.
            this.redraw();
            // Firefox will throw an error if the control is not actually in the
            // document when trying to set the selection. There might be other
            // situations where it does so as well, so just using a try/catch to
            // guard against all.
            try {
                control.setSelectionRange( start, end );
            } catch ( error ) {}
        } else {
            // Firefox sometimes throws an error if you try to read the
            // selection. Again, probably if the control is not actually in the
            // document.
            try {
                start = control.selectionStart;
                end = control.selectionEnd;
            } catch ( error ) {}
        }
        return selection || {
            start,
            end,
        };
    }.property().nocache(),

    /**
        Property: O.TextView#blurOnKeys
        Type: Object
        Default: { esc: true }

        For each truthy value in the object, if the user is focussed in the
        text view and hits the key, the focus will be removed.
    */
    blurOnKeys: { esc: true },

    // --- Render ---

    /**
        Property: O.TextView#allowTextSelection
        Type: Boolean
        Default: true

        Overrides default in <O.View#allowTextSelection>.
    */
    allowTextSelection: true,

    /**
        Property: O.TextView#type
        Type: String

        Will be added to the view's class name.
    */
    type: '',

    layerTag: 'span',

    /**
        Property: O.TextView#className
        Type: String

        Overrides default in <O.View#className>. Will have the class `v-Text`,
        and any classes given in the <#type> property, along with the following
        other class names dependent on state:

        is-highlight - The <#isHighlighted> property is true.
        is-focussed  - The <#isFocussed> property is true.
        is-invalid   - The <#isValid> property is false.
        is-disabled  - The <#isDisabled> property is true.
    */
    className: function () {
        const type = this.get( 'type' );
        return 'v-Text' +
            ( this.get( 'isExpanding' ) ? ' v-Text--expanding' : '' ) +
            ( this.get( 'isHighlighted' ) ? ' is-highlighted' : '' ) +
            ( this.get( 'isFocussed' ) ? ' is-focussed' : '' ) +
            ( this.get( 'isValid' ) ? '' : ' is-invalid' ) +
            ( this.get( 'isDisabled' ) ? ' is-disabled' : '' ) +
            ( type ? ' ' + type : '' );
    }.property( 'type', 'isHighlighted',
        'isFocussed', 'isValid', 'isDisabled' ),

    layerStyles: function () {
        return Object.assign({
            position: this.get( 'positioning' ),
            display: this.get( 'isMultiline' ) ? 'block' : 'inline-block',
            cursor: 'text',
            userSelect: 'text',
        }, this.get( 'layout' ) );
    }.property( 'layout', 'positioning' ),

    /**
        Method: O.TextView#draw

        Overridden to draw view. See <O.View#draw>.
    */
    draw ( layer, Element, el ) {
        const value = this.get( 'value' );
        const placeholder = this.get( 'placeholder' );
        const isMultiline = this.get( 'isMultiline' );
        const control = this._domControl = el(
                isMultiline ? 'textarea' : 'input', {
                    id: this.get( 'id' ) + '-input',
                    className: 'v-Text-input',
                    rows: isMultiline ? '1' : undefined,
                    name: this.get( 'name' ),
                    type: this.get( 'inputType' ),
                    disabled: this.get( 'isDisabled' ),
                    tabIndex: this.get( 'tabIndex' ),
                    value,
                });

        this.redrawInputAttributes();

        if ( placeholder ) {
            if ( nativePlaceholder ) {
                control.placeholder = placeholder;
            } else if ( !value ) {
                this._placeholderShowing = true;
                Element.addClass( control, 'v-Text-input--placeholder' );
                control.value = placeholder;
            }
        }

        layer.title = this.get( 'tooltip' );

        return [
            control,
        ];
    },

    // --- Keep render in sync with state ---

    /**
        Method: O.TextView#textNeedsRedraw

        Calls <O.View#propertyNeedsRedraw> for extra properties requiring
        redraw.
    */
    textNeedsRedraw: function ( self, property, oldValue ) {
        const isValue = ( property === 'value' );
        if ( !isValue || !this._settingFromInput ) {
            this.propertyNeedsRedraw( self, property, oldValue );
        }
        if ( isValue && this.get( 'isExpanding' ) ) {
            this.propertyNeedsRedraw( self, 'textHeight', oldValue );
        }
    }.observes( 'isExpanding', 'value', 'placeholder', 'inputAttributes' ),

    /**
        Method: O.TextView#redrawValue

        Updates the content of the `<textarea>` or `<input>` to match the
        <#value> property.
    */
    redrawValue () {
        const value = this.get( 'value' );
        this._domControl.value = value;
        // Ensure placeholder is updated.
        if ( !nativePlaceholder && !this.get( 'isFocussed' ) ) {
            this._setPlaceholder();
        }
    },

    /**
        Method: O.TextView#redrawPlaceholder

        Updates the placeholder text in the DOM when the <#placeholder> property
        changes.
    */
    redrawPlaceholder () {
        const placeholder = this.get( 'placeholder' );
        const control = this._domControl;
        if ( nativePlaceholder ) {
            control.placeholder = placeholder;
        } else if ( this._placeholderShowing ) {
            control.value = placeholder;
        }
    },

    /**
        Method: O.TextView#redrawInputAttributes

        Updates any other properties of the `<input>` element.
    */
    redrawInputAttributes () {
        const inputAttributes = this.get( 'inputAttributes' );
        const control = this._domControl;
        for ( const property in inputAttributes ) {
            control.set( property, inputAttributes[ property ] );
        }
    },

    redrawTextHeight () {
        // Firefox gets pathologically slow when resizing really large text
        // areas, so automatically turn this off in such a case.
        // 2^13 chars is an arbitrary cut off point that seems to be reasonable
        // in practice
        if ( isFirefox && ( this.get( 'value' ) || '' ).length > 8192 ) {
            this.set( 'isExpanding', false );
            return;
        }
        const control = this._domControl;
        const style = control.style;
        const scrollView = this.getParent( ScrollView );
        // Set to auto to collapse it back to one line, otherwise it would
        // never shrink if you delete text.
        style.height = 'auto';
        const scrollHeight = control.scrollHeight;
        // Presto returns 0 immediately after appending to doc.
        if ( scrollHeight ) {
            style.height = scrollHeight + 'px';
        }
        // Collapsing the height will mess with the scroll, so make sure we
        // reset the scroll position back to what it was.
        if ( scrollView ) {
            scrollView.redrawScroll();
        }
    },

    redrawIsExpanding () {
        if ( this.get( 'isExpanding' ) ) {
            this.redrawTextHeight();
        } else {
            this._domControl.style.height = 'auto';
            // Scroll to cursor
            if ( this.get( 'isFocussed' ) ) {
                this.blur().focus();
            }
        }
    },

    redrawLabel () {},

    // --- Activate ---

    /**
        Method: O.TextView#activate

        Overridden to focus the text view. See <O.AbstractControlView#activate>.
    */
    activate () {
        this.focus();
    },

    // --- Scrolling and focus ---

    savedSelection: null,

    /**
        Method: O.TextView#didEnterDocument

        Overridden to restore scroll position and selection. See
        <O.View#didEnterDocument>.
    */
    didEnterDocument () {
        TextView.parent.didEnterDocument.call( this );
        if ( this.get( 'isMultiline' ) ) {
            if ( this.get( 'isExpanding' ) ) {
                this.redrawTextHeight();
            }
            // Restore scroll positions:
            const control = this._domControl;
            const left = this.get( 'scrollLeft' );
            const top = this.get( 'scrollTop' );
            if ( left ) { control.scrollLeft = left; }
            if ( top ) { control.scrollTop = top; }
            control.addEventListener( 'scroll', this, false );
        }
        const selection = this.get( 'savedSelection' );
        if ( selection ) {
            this.set( 'selection', selection ).focus();
            this.set( 'savedSelection', null );
        }
        return this;
    },

    /**
        Method: O.TextView#willLeaveDocument

        Overridden to save scroll position and selection. See
        <O.View#willLeaveDocument>.
    */
    willLeaveDocument () {
        // If focussed, save cursor position
        if ( this.get( 'isFocussed' ) ) {
            this.set( 'savedSelection', this.get( 'selection' ) );
            this.blur();
        }
        // Stop listening for scrolls:
        if ( this.get( 'isMultiline' ) ) {
            this._domControl.removeEventListener( 'scroll', this, false );
        }
        return TextView.parent.willLeaveDocument.call( this );
    },

    /**
        Method (private): O.TextView#_syncBackScrolls

        Sets the <O.View#scrollLeft> and <O.View#scrollTop> properties whenever
        the user scrolls the textarea.

        Parameters:
            event - {Event} The scroll event.
    */
    _syncBackScrolls: function ( event ) {
        const control = this._domControl;
        const left = control.scrollLeft;
        const top = control.scrollTop;

        this.beginPropertyChanges()
            .set( 'scrollLeft', left )
            .set( 'scrollTop', top )
        .endPropertyChanges();

        event.stopPropagation();
    }.on( 'scroll' ),

    // --- Keep state in sync with render ---

    /**
        Method: O.TextView#syncBackValue

        Updates the <#value> property when the user interacts with the textarea.

        Parameters:
            event - {Event} The input event.
    */
    syncBackValue: function () {
        this._settingFromInput = true;
        if ( nativePlaceholder || !this._placeholderShowing ) {
            this.set( 'value', this._domControl.value );
        }
        this._settingFromInput = false;
    }.on( 'input' ),

    /**
        Method (private): O.TextView#_setPlaceholder

        Sets/removes the placholder text for browsers that don't support this
        natively.
    */
    _setPlaceholder: nativePlaceholder ? null :
    function ( _, __, ___, isFocussed ) {
        const control = this._domControl;
        if ( isFocussed ) {
            if ( this._placeholderShowing ) {
                this._placeholderShowing = false;
                Element.removeClass( control, 'v-Text-input--placeholder' );
                control.value = '';
            }
        } else {
            const placeholder = this.get( 'placeholder' );
            if ( placeholder && !this.get( 'value' ) ) {
                this._placeholderShowing = true;
                Element.addClass( control, 'v-Text-input--placeholder' );
                control.value = placeholder;
            }
        }
    }.observes( 'isFocussed' ),

    /**
        Method (private): O.TextView#_onClick

        Focus and set selection to the end.

        Parameters:
            event - {Event} The click event.
    */
    _onClick: function ( event ) {
        if ( event.target === this.get( 'layer' ) ) {
            this.set( 'selection', this.get( 'value' ).length )
                .focus();
        }
    }.on( 'click' ),

    /**
        Method (private): O.TextView#_onKeypress

        Stop IE automatically focussing the nearest button when the user hits
        enter in single line text inputs.

        Parameters:
            event - {Event} The keypress event.
    */
    _onKeypress: function ( event ) {
        // If key == enter, IE will automatically focus the nearest button
        // (presumably as though it were submitting the form). Stop this
        // unless we're actually in a form.
        if ( !this.get( 'isMultiline' ) &&
                lookupKey( event, true ) === 'enter' &&
                !Element.nearest( this.get( 'layer' ), 'FORM' ) ) {
            event.preventDefault();
        }
    }.on( 'keypress' ),

    /**
        Method (private): O.TextView#_blurOnKey

        Blur the text area when the user hits certain keys, provided by the
        <#blurOnKeys> property.

        Parameters:
            event - {Event} The keyup event.
    */
    _blurOnKey: function ( event ) {
        const key = lookupKey( event, true );
        if ( this.get( 'blurOnKeys' )[ key ] ) {
            this.blur();
        }
    }.on( 'keyup' ),
});

export default TextView;
<|endoftext|>