{"action":"created","comment":{"url":"https://api.github.com/repos/phenotips/phenotips/pulls/comments/46729358","id":46729358,"diff_hunk":"@@ -1,276 +1,437 @@\n-define([\n-    \"pedigree/model/helpers\"\n-  ], function(\n-    Helpers\n-  ){\n-\n-  var PedigreeFuzzyDatePickerDropdown = Class.create({\n-    initialize : function(options) {\n-      this.span     = new Element('span');\n-      this.options  = options;\n-      this.callback = null;\n+define([], function(){\n+\n+  /**\n+  * AbstractFuzzyDateInput is an object representing a text input for the @class FuzzyDatePicker\n+  *\n+  * @class AbstractFuzzyDateInput\n+  * @constructor\n+  * @param {element} A text input element. The behaviour of the element will be modified.\n+  * @param {alwayEnabled} Booolean indicating whether this input should be enabled at all times\n+  */\n+  var AbstractFuzzyDateInput = Class.create({\n+    initialize : function(element, alwaysEnabled){\n+      this.element = element;\n+      this.alwaysEnabled = alwaysEnabled;\n+\n+      //The isFuzzy boolean specifies whether the value contained in the iput is specific or fuzzy(a range, decade etc.). Blank and invalid values are not fuzzy.\n+      this.isFuzzy = false;\n+      this.isValid = true;\n+\n+      if (this.alwaysEnabled) {\n+        this.enable();\n+      } else {\n+        this.disable();\n+      }\n+      this.element.onkeypress = this.onKeyPress.bind(this);\n+      this.element.observe(\"focus\", this.onFocus.bind(this));\n+      this.element.observe(\"blur\", this.onBlur.bind(this));\n+      this.element.observe(\"keydown\", this.onKeyDown.bind(this));\n+    },\n+    getValue : function() {\n+      return this.element.value;\n+    },\n+    isEmpty : function() {\n+      return this.getValue() === \"\";\n+    },\n+    setValue : function (value) {\n+      value && this.enable();\n+      var newVal = this.formatValue(value);\n+      this.element.setValue(newVal);\n+    },\n+    getElement : function() {\n+      return this.element;\n+    },\n+    onDateChange : function() {\n+      this.refreshAvailableValues();\n+      this.validate();\n+      if (!this.isValid && (this.getValue() !== \"\")) {\n+        this.element.addClassName(\"invalid\");\n+      } else {\n+        this.element.removeClassName(\"invalid\");\n+      }\n+    },\n+    enable: function() {\n+      this.refreshAvailableValues();\n+      this.element.enable();\n     },\n+    disable: function() {\n+      this.element.setValue(\"\");\n+      this.element.removeClassName(\"invalid\");\n+      this.isValid = true;\n+      this.element.disable();\n+    },\n+    onFocus : function(e) {\n+      this.element.removeClassName(\"invalid\");\n+    },\n+    onBlur : function(e) {\n+      this.onValueSelected(e);\n+    },\n+    onValueSelected : function(e) {\n+      this.element.fire(\"datepicker:date:changed\");\n+    },\n+    onKeyDown : function(e) {\n+      var charCode = e.key || e.which;\n+      if (charCode == 9) {\n+        this.onValueSelected(e);\n+      }\n+    },\n+    onKeyPress : function(e, regexp, maxLength, padZeros) {\n+      var charCode = e.charCode || e.keyCode;\n+      var character = String.fromCharCode(charCode);\n+      //Call select handler on tab press.\n+      if(charCode == 9){\n+        this.onValueSelected();\n+        return true;\n+      }\n+      //Allow non-character keys always\n+      if (character === \"\") {return true;}\n+      //Block non-digits\n+      if (!regexp.test(character)) {return false;}\n \n-    populate : function(values) {\n-      var selectedIndex = this.dropdown ? (this.dropdown.selectedIndex || this._tmpSelectedIndex) : 0;\n+      var currentVal = this.getValue();\n+      var caretPos = this.getCaretPosition();\n \n-      // using raw HTML for performance reasons: generating many years takes a noticeable time using\n-      // more proper methods (e.g. new Element()...)\n-      // (Note: using span around select because IE9 does not allow setting innerHTML of <select>-s)\n-      var optionsHTML = '<select name=\"' + this.options.name +\n-                             '\" class=\"' + (this.options.cssClass || this.options.name || '') +\n-                             '\" placeholder=\"' + (this.options.hint || this.options.name || '') +\n-                             '\" title=\"'       + (this.options.hint || this.options.name || '') + '\">';\n+      currentVal = currentVal.replace(this.getSelectedText(), \"\");\n+      if (currentVal.length == maxLength){return false;}\n \n-      optionsHTML += '<option value=\"\" class=\"empty\"></option>';\n-      var _this = this;\n-      values.each(function (item) {\n-         optionsHTML += '<option value=\"' + item.value + '\"';\n-         if (item.cssClass) {\n-             optionsHTML += ' class=\"' + item.cssClass + '\"';\n-         }\n-         optionsHTML += '>' + (item.text || item.value || '') + '</option>';\n-      });\n-      optionsHTML += \"</select>\";\n-      this.span.innerHTML = optionsHTML;\n-      this.dropdown = this.span.firstChild;\n-      this.callback && this.onSelect(this.callback);\n-      if (this.dropdown.selectedIndex <= 0 && selectedIndex >= 0 && selectedIndex < this.dropdown.options.length) {\n-        this.dropdown.selectedIndex = selectedIndex;\n+      if (padZeros && character != \"0\"){\n+        currentVal = (currentVal || \"0\") + character; //zero is prepended to character to allow for \"2\" as valid input\n+      } else {\n+        currentVal = currentVal.substr(0, caretPos) + character + currentVal.substr(caretPos, currentVal.length);\n       }\n-    },\n \n-    enable : function () {\n-      if (!this.options.alwaysEnabled) {\n-          this.dropdown.enable();\n-          if (this.dropdown.selectedIndex <= 0 && this._tmpSelectedIndex < this.dropdown.options.length) {\n-            this.dropdown.selectedIndex = this._tmpSelectedIndex;\n-            return (this._tmpSelectedIndex > 0);\n-          }\n-      }\n-      return false;\n+      return this.availableValues.map(function(a){return a.indexOf(currentVal) == 0}).reduce(function(a,b){return a || b;});\n     },\n-\n-    disable : function () {\n-      if (!this.options.alwaysEnabled) {\n-          this.dropdown.disable();\n-          this._tmpSelectedIndex = this.dropdown.selectedIndex;\n-          this.dropdown.selectedIndex = 0;\n+    //Returns the currently selected text.\n+    getSelectedText : function() {\n+      var text = \"\";\n+      if (typeof window.getSelection != \"undefined\") {\n+        text = window.getSelection().toString();\n+      } else if (typeof document.selection != \"undefined\" && document.selection.type == \"Text\") {\n+        text = document.selection.createRange().text;\n       }\n+      return text;\n     },\n-\n-    getElement : function() {\n-      return this.span;\n+    getCaretPosition: function() {\n+      if (this.element.createTextRange) {\n+        var range = document.selection.createRange().duplicate()\n+        range.moveEnd('character', this.element.value.length)\n+        if (range.text == '') return this.element.value.length\n+        return this.element.value.lastIndexOf(range.text)\n+      } else return this.element.selectionStart;\n     },\n-\n-    onSelect : function(callback) {\n-      var _this = this;\n-      this.callback = callback;\n-      var events = ['change'];\n-      browser.isGecko && events.push('keyup');\n-      events.each(function(eventName) {\n-        _this.dropdown.observe(eventName, function() {\n-          callback();\n-          if (this.inputFormat == \"YMD\") {\n-            _this._tmpSelectedIndex = _this.dropdown.selectedIndex;\n-          }\n-        });\n-      });\n+    validate: function() {\n+      this.isValid = this.availableValues.indexOf(this.getValue()) > -1 || this.getValue() === \"\";\n+      return this.isValid;\n     },\n+    formatValue : function(value) {return value;},\n+    refreshAvailableValues : function() {}\n+  });\n \n-    onFocus : function(callback) {\n-      var _this = this;\n-      this.dropdown.observe('focus', function() {\n-        callback();\n-        if (_this.dropdown.selectedIndex == -1 && _this._tmpSelectedIndex < _this.dropdown.options.size()) {\n-          _this.dropdown.selectedIndex = _this._tmpSelectedIndex;\n-        }\n-      });\n+  /**\n+  * FuzzyYearInput is the text input year component of the @class FuzzyDatePicker .\n+  * Note that the year input will always be enabled.\n+  *\n+  * @class FuzzyYearInput\n+  * @Extends AbstractFuzzyDateInput\n+  * @constructor\n+  * @param {element} A text input element. The behaviour of the element will be modified.\n+  * @param {decadesEnabled} Booolean indicating whether deacades are supported.\n+  */\n+  var FuzzyYearInput = Class.create(AbstractFuzzyDateInput,{\n+    initialize : function($super, element, decadesEnabled) {\n+      $super(element, true);\n+      this.decadesEnabled = decadesEnabled || false;\n     },\n-    onBlur : function(callback) {\n-      this.dropdown.observe('blur', callback);\n+    onKeyPress : function($super, e) {\n+      if (this.decadesEnabled) {\n+        return $super(e, /[0-9]|s/, 5, false);\n+      } else {\n+        return $super(e, /[0-9]/, 4, false)\n+      }\n     },\n-\n-    getSelectedValue : function () {\n-       return (this.dropdown.selectedIndex >= 0) ? this.dropdown.options[this.dropdown.selectedIndex].value : '';\n+    refreshAvailableValues : function() {\n+      var availableValues = [];\n+      for (var i = (new Date()).getYear() + 1900; i >= 1800; i--){\n+        availableValues.push(i + \"\");\n+        if (this.decadesEnabled && i % 10 == 0){\n+          availableValues.push(i + \"s\");\n+        }\n+      }\n+      this.availableValues = availableValues;\n+      return availableValues;\n     },\n-\n-    getSelectedOption : function () {\n-       return (this.dropdown.selectedIndex >= 0) ? this.dropdown.options[this.dropdown.selectedIndex].innerHTML : '';\n+    validate : function($super) {\n+      $super();\n+      if ( !this.isValid || (this.decadesEnabled && /\\d\\d\\d\\ds/.test(this.getValue())) ) {\n+        this.isFuzzy = true;\n+      } else {\n+        this.isFuzzy = false;\n+      }\n+      return this.isValid;\n     }\n   });\n \n-  var PedigreeFuzzyDatePicker = Class.create({\n-    initialize : function (input, inputFormat) {\n-      this.inputFormat = inputFormat ? inputFormat : \"YMD\";\n-\n-      if (!input) {return};\n-      this.__input = input;\n-      this.__input.hide();\n-\n-      this.container = new Element('div', {'class' : 'fuzzy-date-picker'});\n-      this.__input.insert({after : this.container});\n-\n-      if (this.inputFormat == \"DMY\" || this.inputFormat == \"MY\") {\n-          var hideDay = (this.inputFormat == \"MY\")\n-          this.container.insert(this.createDayDropdown(hideDay));\n-          this.container.insert(this.createMonthDropdown());\n-          this.container.insert(this.createYearDropdown());\n+  /**\n+  * FuzzyMonthInput is the text input month component of the @class FuzzyDatePicker\n+  *\n+  * @class FuzzyMonthInput\n+  * @Extends AbstractFuzzyDateInput\n+  * @constructor\n+  * @param {element} A text input element. The behaviour of the element will be modified.\n+  * @param {alwaysEnabled}  Booolean indicating whether the month value should always be enabled. If false\n+  *                         the value will be enabled iff the year input is valid and not fuzzy.\n+  * @param {yearInput} The @class FuzzyYearInput to which this month input is associated with.\n+  */\n+  var FuzzyMonthInput = Class.create(AbstractFuzzyDateInput, {\n+    initialize : function($super, element, alwaysEnabled, yearInput) {\n+      this.yearInput = yearInput;\n+      $super(element, alwaysEnabled);\n+    },\n+    onDateChange : function($super) {\n+      $super();\n+      if (this.yearInput.isFuzzy) {\n+        this.disable();\n+      } else if (this.alwaysEnabled) {\n+        this.enable();\n+      } else if (this.yearInput.isEmpty()) {\n+        this.disable();\n+      } else if (this.yearInput.isValid) {\n+        this.enable();\n       } else {\n-          this.container.insert(this.createYearDropdown());\n-          this.container.insert(this.createMonthDropdown());\n-          this.container.insert(this.createDayDropdown());\n+        this.disable();\n       }\n-\n-      // TODO: yearSelector's (and month's & day's) .onSelect() does not seem to fire\n-      //       upon programmatic update if a substitute is found can remove these hackish events\n-      this.container.observe(\"datepicker:date:changed\", this.onProgrammaticUpdate.bind(this));\n     },\n-\n-    onProgrammaticUpdate : function() {\n-        this.yearSelected(true);\n-        this.monthSelected(true);\n-        this.updateDate(true);\n+    onValueSelected: function ($super) {\n+      var value = this.getValue();\n+      if (value.length == 1) {\n+        this.element.setValue(\"0\" + value);\n+      }\n+      $super();\n+    },\n+    onKeyPress : function($super, e) {\n+      return $super(e, /[0-9]/, 2, true);\n     },\n \n-    createYearDropdown : function() {\n-      //var timer = new Helpers.Timer();\n-      this.yearSelector = new PedigreeFuzzyDatePickerDropdown({name: \"year\", alwaysEnabled: (this.inputFormat != \"YMD\")});\n-\n-      var today = new Date();\n-      var crtYear = today.getYear() + 1900;\n-      var startYear = 1850;\n-\n-      var values = [];\n-      for (var y = crtYear; y >= startYear; --y) {\n-        values.push({\"value\" : y});\n-        if (y % 10 == 0) {\n-            values.push({\"value\" : (y + \"s\"), \"cssClass\" : \"decade\", \"text\" : (y + 's')});\n-        }\n+    refreshAvailableValues : function() {\n+      var availableValues = [];\n+      for (var i = 1; i <= 12; i++) {\n+        availableValues.push((\"00\" + i).slice(-2));\n       }\n-      values.push({\"value\": \"1800s\", \"cssClass\" : \"decade\"});\n-      values.push({\"value\": \"1700s\", \"cssClass\" : \"decade\"});\n-      values.push({\"value\": \"1600s\", \"cssClass\" : \"decade\"});\n-\n-      this.yearSelector.populate(values);\n-      this.yearSelector.onSelect(this.yearSelected.bind(this));\n-\n-      //console.log( \"=== Generate year dropdown time: \" + timer.report() + \"ms ==========\" );\n-      return this.yearSelector.getElement();\n+      this.availableValues = availableValues;\n+      return availableValues;\n     },\n-\n-    yearSelected : function(doNotNotifyOnChange) {\n-      if (this.yearSelector.getSelectedValue() > 0) {\n-        this.monthSelector.enable();\n-        this.monthSelected(doNotNotifyOnChange);\n-      } else {\n-        this.monthSelector.disable();\n-        this.daySelector.disable();\n+    formatValue : function(value) {\n+      if (typeof value == 'number') {\n+        value = value.toString();\n+      }\n+      if (value.length == 1) {\n+        value = \"0\" + value;\n       }\n-      this.updateDate(doNotNotifyOnChange);\n+      return value;\n+    }\n+  });\n+\n+  /**\n+  * FuzzyDayInput is the text input day component of the @class FuzzyDatePicker\n+  *\n+  * @class FuzzyDayInput\n+  * @Extends AbstractFuzzyDateInput\n+  * @constructor\n+  * @param {element} A text input element. The behaviour of the element will be modified.\n+  * @param {alwaysEnabled}  Booolean indicating whether the day value should always be enabled. If false\n+  *                         the value will be enabled iff the year input is valid and not fuzzy.\n+  * @param {yearInput} The @class FuzzyYearInput to which this day input is associated with.\n+  * @param {monthInput} The @class FuzzyMonthInput to which this day input is associated with.\n+  */\n+  var FuzzyDayInput = Class.create(AbstractFuzzyDateInput, {\n+    initialize : function($super, element, alwaysEnabled, yearInput, monthInput) {\n+      this.yearInput = yearInput;\n+      this.monthInput = monthInput;\n+      $super(element, alwaysEnabled);\n     },\n \n-    createMonthDropdown : function() {\n-      this.monthSelector = new PedigreeFuzzyDatePickerDropdown({name: \"month\", alwaysEnabled: (this.inputFormat != \"YMD\")});\n-      this.monthSelector.populate(this.getZeroPaddedValueRange(1,12));\n-      this.monthSelector.disable();\n-      this.monthSelector.onSelect(this.monthSelected.bind(this));\n-      return this.monthSelector.getElement();\n+    onKeyPress : function($super, e) {\n+      return $super(e, /[0-9]/, 2, true);\n     },\n \n-    monthSelected : function(doNotNotifyOnChange) {\n-      if (this.monthSelector.getSelectedValue() > 0) {\n-        this.daySelector.populate(this.getAvailableDays());\n-        this.daySelector.enable();\n+    onDateChange : function($super) {\n+      $super();\n+      if (this.yearInput.isFuzzy || this.monthInput.isFuzzy) {\n+        this.disable();\n+      } else if (this.alwaysEnabled) {\n+        this.enable();\n+      } else if (this.yearInput.isEmpty() || this.monthInput.isEmpty()) {\n+        this.disable();\n+      } else if (this.yearInput.isValid && this.monthInput.isValid) {\n+        this.enable();\n       } else {\n-        if (this.inputFormat == \"DMY\") {\n-            // in \"DMY\" mode let user pick any day if no month is selected\n-            this.daySelector.populate(this.getZeroPaddedValueRange(1,31));\n-        } else {\n-            this.daySelector.disable();\n-        }\n+        this.disable();\n       }\n-      this.updateDate(doNotNotifyOnChange);\n     },\n-\n-    createDayDropdown : function(hide) {\n-      this.daySelector = new PedigreeFuzzyDatePickerDropdown({name: \"day\", alwaysEnabled: (this.inputFormat != \"YMD\")});\n-      this.daySelector.populate(this.getZeroPaddedValueRange(1,31));\n-      this.daySelector.disable();\n-      this.daySelector.onSelect(this.updateDate.bind(this));\n-      if (hide) {\n-          this.daySelector.getElement().hide();\n+    onValueSelected: function ($super) {\n+      var value = this.getValue();\n+      if (value.length == 1) {\n+        this.element.setValue(\"0\" + value);\n       }\n-      return this.daySelector.getElement();\n+      $super();\n     },\n-\n-    getAvailableDays : function () {\n-      var year = this.yearSelector.getSelectedValue() * 1;\n-      var month = this.monthSelector.getSelectedValue() * 1;\n+    refreshAvailableValues : function() {\n+      var year = this.yearInput.getValue() || 1;\n+      var month = this.monthInput.getValue() || 1;\n       var lastDayOfMonth = 0;\n-      if ([1,3,5,7,8,10,12].indexOf(month) >= 0) {\n+      if ([\"01\",\"03\",\"05\",\"07\",\"08\",\"10\",\"12\"].indexOf(month) >= 0) {\n         lastDayOfMonth = 31;\n-      } else if ([4,6,9,11].indexOf(month) >= 0) {\n+      } else if ([\"04\",\"06\",\"09\",\"11\"].indexOf(month) >= 0) {\n         lastDayOfMonth = 30\n-      } else if (month == 2) {\n+      } else if (month === \"02\") {\n         if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) {\n           lastDayOfMonth = 29;\n         } else {\n           lastDayOfMonth = 28;\n         }\n       }\n-      return this.getZeroPaddedValueRange(1, lastDayOfMonth);\n+      var availableValues = [];\n+      for (var i = 1; i<= lastDayOfMonth; i++) {\n+        availableValues.push((\"00\" + i).slice(-2));\n+      }\n+      this.availableValues = availableValues;\n+      return availableValues;\n+     },\n+     formatValue : function(value) {\n+      if (typeof value == 'number') {\n+        value = value.toString();\n+      }\n+      if (value.length == 1) {\n+        value = \"0\" + value;\n+      }\n+      return value;\n+    }\n+  });\n+\n+  /**\n+  * FuzzyDatePicker A javascript component for turning a text input into a fuzzy date picker.\n+  *\n+  * @class FuzzyDatePicker\n+  * @constructor\n+  * @param {input} A text input element. Date values will be read and written to this element.\n+  * @param {options}  An optional map of options:\n+  *         inputFormat: any permutation or combination of \"YMD\". Defaults to YMD,\n+  *         alwaysEnabled: Set to true to disable dynamic input enabling. Defaults to false,\n+  *         decadesEnabled: Toggle support for decades. Defaults to false\n+  *\n+  * @Usage\n+  *         var element = new Element('text', {'name':'fuzzy-date-picker'});\n+  *         var options = {inputFormat : \"MY\", decadesEnabled : true};\n+  *         var fuzzyDatePicker = new FuzzyDatePicker(element, options);\n+  *\n+  * @Events\n+  *         datepicker:date:changed -> Fired. Occures when the user changes the date. Causes an xwiki:date:changed event to be fired as well.\n+  *         datepicker:date:updated -> Listened. The fuzzy date picker will update its displayed value from its text element on this event.\n+  */\n+  var FuzzyDatePicker = Class.create({\n+    initialize: function(input, options) {\n+      this.options = options || {};\n+      this.inputFormat = options.inputFormat || \"YMD\";\n+      this.alwaysEnabled = options.alwaysEnabled || this.inputFormat != \"YMD\";\n+      this.decadesEnabled = options.decadesEnabled || false;\n+\n+      if (!input) {return};\n+      this._input = input;\n+      this._input.hide();\n+\n+      this.initStrings();\n+\n+      this.container = new Element('div', {'class' : 'fuzzy-date-picker'});\n+      this.container.observe(\"datepicker:date:changed\", this.onDateChange.bind(this))\n+      this.container.observe(\"datepicker:date:updated\", this.updateDateFromInput.bind(this));\n+      this._input.wrap(this.container);\n+      this.createInputs();\n+      this.insertInputs();\n     },\n+    createInputs : function() {\n+      var yearElement = new Element('input', {'type': 'text', 'class' : 'text', 'name': 'year', 'placeholder': this.strings.yearPlaceholder});\n+      var monthElement = new Element('input', {'type': 'text', 'class' : 'text', 'name': 'month', 'placeholder': this.strings.monthPlaceholder});\n+      var dayElement = new Element('input', {'type': 'text', 'class' : 'text', 'name': 'day', 'placeholder': this.strings.dayPlaceholder});\n+      this.inputs = {};\n+      this.inputs.y = new FuzzyYearInput(yearElement, this.decadesEnabled);\n+      this.inputs.m = new FuzzyMonthInput(monthElement, this.alwaysEnabled, this.inputs.y);\n+      this.inputs.d = new FuzzyDayInput(dayElement, this.alwaysEnabled, this.inputs.y, this.inputs.m);\n \n-    getZeroPaddedValueRange : function (start, end) {\n-      var values = [];\n-      if (start <= end) {\n-          for (var v = start; v <= end; ++v) {\n-              values.push({'value': v, 'text' : (\"0\" + v).slice(-2)});\n-          }\n-      } else {\n-          for (var v = end; v <= start; --v) {\n-              values.push({'value': v, 'text' : (\"0\" + v).slice(-2)});\n-          }\n-      }\n-      return values;\n     },\n+    insertInputs : function(){\n+      this.inputFormat.toLocaleLowerCase().split(\"\").forEach(function(identifier){\n+        this.container.insert(this.inputs[identifier].getElement());\n+      }.bind(this));\n+    },\n+    updateDateFromInput : function() {\n+      var dateObject = JSON.parse(this._input.value);\n+      if (dateObject[\"year\"] != null) {\n+        this.inputs.y.setValue(dateObject[\"year\"]);\n+      } else if (dateObject[\"decade\"] != null) {\n+        this.inputs.y.setValue(dateObject[\"decade\"]);\n+      }\n \n+      dateObject[\"month\"] != null && this.inputs.m.setValue(dateObject[\"month\"]);\n+      dateObject[\"day\"] != null && this.inputs.d.setValue(dateObject[\"day\"]);\n+      this.container.fire(\"datepicker:date:changed\");\n+    },\n+    onDateChange : function(e) {\n+      var validDate = true;\n+      for (input in this.inputs) {\n+        if (this.inputs.hasOwnProperty(input)){\n+          this.inputs[input].onDateChange();\n+          validDate = validDate && this.inputs[input].isValid;\n+        }\n+      }\n+      if (validDate) {\n+        this.updateDate(false);\n+      }\n+    },\n     updateDate : function (doNotFireEventOnChange) {\n         var dateObject = {};\n \n-        var y = this.yearSelector.getSelectedValue();\n+        var y = this.inputs.y.getValue();\n         if (y.match(/\\d\\d\\d\\ds$/)) {\n-            dateObject[\"decade\"] = y;\n+          dateObject[\"decade\"] = y;\n         } else {\n-            if (y != \"\") {\n-                dateObject[\"year\"] = y;\n-            }\n+          if (y != \"\") {\n+            dateObject[\"year\"] = y;\n+          }\n         }\n \n         if (y > 0 || this.inputFormat == \"DMY\") {\n-            var m = this.monthSelector.getSelectedValue();\n-            if (m > 0) {\n-                dateObject[\"month\"] = this.monthSelector.getSelectedOption();\n-            }\n+          var m = this.inputs.m.getValue();\n+          if (m > 0) {\n+            dateObject[\"month\"] = m;\n+          }\n \n-            if (m > 0 || this.inputFormat == \"DMY\") {\n-                var d = this.daySelector.getSelectedValue();\n-                if (d > 0) {\n-                    dateObject[\"day\"] = this.daySelector.getSelectedOption();\n-                }\n+          if (m > 0 || this.inputFormat == \"DMY\") {\n+            var d = this.inputs.d.getValue();\n+            if (d > 0) {\n+              dateObject[\"day\"] = d;\n             }\n+          }\n         }\n \n         var newValue = JSON.stringify(dateObject);\n-        if (newValue != this.__input.value) {\n-            this.__input.value = JSON.stringify(dateObject);\n-            if (!doNotFireEventOnChange) {\n-                this.__input.fire(\"xwiki:date:changed\");\n-            }\n+        if (newValue != this._input.value) {\n+          this._input.value = JSON.stringify(dateObject);\n+          if (!doNotFireEventOnChange) {\n+            this._input.fire(\"xwiki:date:changed\");\n+          }\n         }\n+    },\n+    initStrings: function() {\n+      var strings = {\n+        dayPlaceholder: 'DD',\n+        monthPlaceholder: 'MM'\n+      };\n+      strings.yearPlaceholder = this.decadesEnabled ? 'YYYYs' : 'YYYY';","path":"components/pedigree/resources/src/main/resources/pedigree/view/datepicker.js","position":639,"original_position":639,"commit_id":"c148273129105cbfa82eae231daf1f5677e8995c","original_commit_id":"c148273129105cbfa82eae231daf1f5677e8995c","user":{"login":"allasm","id":4990078,"avatar_url":"https://avatars.githubusercontent.com/u/4990078?v=3","gravatar_id":"","url":"https://api.github.com/users/allasm","html_url":"https://github.com/allasm","followers_url":"https://api.github.com/users/allasm/followers","following_url":"https://api.github.com/users/allasm/following{/other_user}","gists_url":"https://api.github.com/users/allasm/gists{/gist_id}","starred_url":"https://api.github.com/users/allasm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/allasm/subscriptions","organizations_url":"https://api.github.com/users/allasm/orgs","repos_url":"https://api.github.com/users/allasm/repos","events_url":"https://api.github.com/users/allasm/events{/privacy}","received_events_url":"https://api.github.com/users/allasm/received_events","type":"User","site_admin":false},"body":"should work as long as you initialize strings after you initialize this.decadesEnabled","created_at":"2015-12-04T20:37:20Z","updated_at":"2015-12-04T20:37:20Z","html_url":"https://github.com/phenotips/phenotips/pull/1944#discussion_r46729358","pull_request_url":"https://api.github.com/repos/phenotips/phenotips/pulls/1944","_links":{"self":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/comments/46729358"},"html":{"href":"https://github.com/phenotips/phenotips/pull/1944#discussion_r46729358"},"pull_request":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/1944"}}},"pull_request":{"url":"https://api.github.com/repos/phenotips/phenotips/pulls/1944","id":52703547,"html_url":"https://github.com/phenotips/phenotips/pull/1944","diff_url":"https://github.com/phenotips/phenotips/pull/1944.diff","patch_url":"https://github.com/phenotips/phenotips/pull/1944.patch","issue_url":"https://api.github.com/repos/phenotips/phenotips/issues/1944","number":1944,"state":"open","locked":false,"title":"Pt 1447","user":{"login":"mjshepherd","id":9931605,"avatar_url":"https://avatars.githubusercontent.com/u/9931605?v=3","gravatar_id":"","url":"https://api.github.com/users/mjshepherd","html_url":"https://github.com/mjshepherd","followers_url":"https://api.github.com/users/mjshepherd/followers","following_url":"https://api.github.com/users/mjshepherd/following{/other_user}","gists_url":"https://api.github.com/users/mjshepherd/gists{/gist_id}","starred_url":"https://api.github.com/users/mjshepherd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mjshepherd/subscriptions","organizations_url":"https://api.github.com/users/mjshepherd/orgs","repos_url":"https://api.github.com/users/mjshepherd/repos","events_url":"https://api.github.com/users/mjshepherd/events{/privacy}","received_events_url":"https://api.github.com/users/mjshepherd/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-12-04T18:24:44Z","updated_at":"2015-12-04T20:37:20Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8091d37e2cada9e5cf1c72e84fad4d602fa01091","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/phenotips/phenotips/pulls/1944/commits","review_comments_url":"https://api.github.com/repos/phenotips/phenotips/pulls/1944/comments","review_comment_url":"https://api.github.com/repos/phenotips/phenotips/pulls/comments{/number}","comments_url":"https://api.github.com/repos/phenotips/phenotips/issues/1944/comments","statuses_url":"https://api.github.com/repos/phenotips/phenotips/statuses/c148273129105cbfa82eae231daf1f5677e8995c","head":{"label":"mjshepherd:PT-1447","ref":"PT-1447","sha":"c148273129105cbfa82eae231daf1f5677e8995c","user":{"login":"mjshepherd","id":9931605,"avatar_url":"https://avatars.githubusercontent.com/u/9931605?v=3","gravatar_id":"","url":"https://api.github.com/users/mjshepherd","html_url":"https://github.com/mjshepherd","followers_url":"https://api.github.com/users/mjshepherd/followers","following_url":"https://api.github.com/users/mjshepherd/following{/other_user}","gists_url":"https://api.github.com/users/mjshepherd/gists{/gist_id}","starred_url":"https://api.github.com/users/mjshepherd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mjshepherd/subscriptions","organizations_url":"https://api.github.com/users/mjshepherd/orgs","repos_url":"https://api.github.com/users/mjshepherd/repos","events_url":"https://api.github.com/users/mjshepherd/events{/privacy}","received_events_url":"https://api.github.com/users/mjshepherd/received_events","type":"User","site_admin":false},"repo":{"id":35223758,"name":"phenotips","full_name":"mjshepherd/phenotips","owner":{"login":"mjshepherd","id":9931605,"avatar_url":"https://avatars.githubusercontent.com/u/9931605?v=3","gravatar_id":"","url":"https://api.github.com/users/mjshepherd","html_url":"https://github.com/mjshepherd","followers_url":"https://api.github.com/users/mjshepherd/followers","following_url":"https://api.github.com/users/mjshepherd/following{/other_user}","gists_url":"https://api.github.com/users/mjshepherd/gists{/gist_id}","starred_url":"https://api.github.com/users/mjshepherd/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mjshepherd/subscriptions","organizations_url":"https://api.github.com/users/mjshepherd/orgs","repos_url":"https://api.github.com/users/mjshepherd/repos","events_url":"https://api.github.com/users/mjshepherd/events{/privacy}","received_events_url":"https://api.github.com/users/mjshepherd/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mjshepherd/phenotips","description":"PhenoTips provides an easy-to-use Web interface and standardized database back-end for collecting clinical findings observed in patients with possible genetic disorders.","fork":true,"url":"https://api.github.com/repos/mjshepherd/phenotips","forks_url":"https://api.github.com/repos/mjshepherd/phenotips/forks","keys_url":"https://api.github.com/repos/mjshepherd/phenotips/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mjshepherd/phenotips/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mjshepherd/phenotips/teams","hooks_url":"https://api.github.com/repos/mjshepherd/phenotips/hooks","issue_events_url":"https://api.github.com/repos/mjshepherd/phenotips/issues/events{/number}","events_url":"https://api.github.com/repos/mjshepherd/phenotips/events","assignees_url":"https://api.github.com/repos/mjshepherd/phenotips/assignees{/user}","branches_url":"https://api.github.com/repos/mjshepherd/phenotips/branches{/branch}","tags_url":"https://api.github.com/repos/mjshepherd/phenotips/tags","blobs_url":"https://api.github.com/repos/mjshepherd/phenotips/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mjshepherd/phenotips/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mjshepherd/phenotips/git/refs{/sha}","trees_url":"https://api.github.com/repos/mjshepherd/phenotips/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mjshepherd/phenotips/statuses/{sha}","languages_url":"https://api.github.com/repos/mjshepherd/phenotips/languages","stargazers_url":"https://api.github.com/repos/mjshepherd/phenotips/stargazers","contributors_url":"https://api.github.com/repos/mjshepherd/phenotips/contributors","subscribers_url":"https://api.github.com/repos/mjshepherd/phenotips/subscribers","subscription_url":"https://api.github.com/repos/mjshepherd/phenotips/subscription","commits_url":"https://api.github.com/repos/mjshepherd/phenotips/commits{/sha}","git_commits_url":"https://api.github.com/repos/mjshepherd/phenotips/git/commits{/sha}","comments_url":"https://api.github.com/repos/mjshepherd/phenotips/comments{/number}","issue_comment_url":"https://api.github.com/repos/mjshepherd/phenotips/issues/comments{/number}","contents_url":"https://api.github.com/repos/mjshepherd/phenotips/contents/{+path}","compare_url":"https://api.github.com/repos/mjshepherd/phenotips/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mjshepherd/phenotips/merges","archive_url":"https://api.github.com/repos/mjshepherd/phenotips/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mjshepherd/phenotips/downloads","issues_url":"https://api.github.com/repos/mjshepherd/phenotips/issues{/number}","pulls_url":"https://api.github.com/repos/mjshepherd/phenotips/pulls{/number}","milestones_url":"https://api.github.com/repos/mjshepherd/phenotips/milestones{/number}","notifications_url":"https://api.github.com/repos/mjshepherd/phenotips/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mjshepherd/phenotips/labels{/name}","releases_url":"https://api.github.com/repos/mjshepherd/phenotips/releases{/id}","created_at":"2015-05-07T14:10:06Z","updated_at":"2015-05-07T14:10:11Z","pushed_at":"2015-12-04T19:31:54Z","git_url":"git://github.com/mjshepherd/phenotips.git","ssh_url":"git@github.com:mjshepherd/phenotips.git","clone_url":"https://github.com/mjshepherd/phenotips.git","svn_url":"https://github.com/mjshepherd/phenotips","homepage":"http://phenotips.org/","size":628485,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"phenotips:master","ref":"master","sha":"c5264633fb2a1133049ae917faa19347f83ad7d7","user":{"login":"phenotips","id":4872353,"avatar_url":"https://avatars.githubusercontent.com/u/4872353?v=3","gravatar_id":"","url":"https://api.github.com/users/phenotips","html_url":"https://github.com/phenotips","followers_url":"https://api.github.com/users/phenotips/followers","following_url":"https://api.github.com/users/phenotips/following{/other_user}","gists_url":"https://api.github.com/users/phenotips/gists{/gist_id}","starred_url":"https://api.github.com/users/phenotips/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/phenotips/subscriptions","organizations_url":"https://api.github.com/users/phenotips/orgs","repos_url":"https://api.github.com/users/phenotips/repos","events_url":"https://api.github.com/users/phenotips/events{/privacy}","received_events_url":"https://api.github.com/users/phenotips/received_events","type":"Organization","site_admin":false},"repo":{"id":2685841,"name":"phenotips","full_name":"phenotips/phenotips","owner":{"login":"phenotips","id":4872353,"avatar_url":"https://avatars.githubusercontent.com/u/4872353?v=3","gravatar_id":"","url":"https://api.github.com/users/phenotips","html_url":"https://github.com/phenotips","followers_url":"https://api.github.com/users/phenotips/followers","following_url":"https://api.github.com/users/phenotips/following{/other_user}","gists_url":"https://api.github.com/users/phenotips/gists{/gist_id}","starred_url":"https://api.github.com/users/phenotips/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/phenotips/subscriptions","organizations_url":"https://api.github.com/users/phenotips/orgs","repos_url":"https://api.github.com/users/phenotips/repos","events_url":"https://api.github.com/users/phenotips/events{/privacy}","received_events_url":"https://api.github.com/users/phenotips/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/phenotips/phenotips","description":"PhenoTips provides an easy-to-use Web interface and standardized database back-end for collecting clinical findings observed in patients with possible genetic disorders.","fork":false,"url":"https://api.github.com/repos/phenotips/phenotips","forks_url":"https://api.github.com/repos/phenotips/phenotips/forks","keys_url":"https://api.github.com/repos/phenotips/phenotips/keys{/key_id}","collaborators_url":"https://api.github.com/repos/phenotips/phenotips/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/phenotips/phenotips/teams","hooks_url":"https://api.github.com/repos/phenotips/phenotips/hooks","issue_events_url":"https://api.github.com/repos/phenotips/phenotips/issues/events{/number}","events_url":"https://api.github.com/repos/phenotips/phenotips/events","assignees_url":"https://api.github.com/repos/phenotips/phenotips/assignees{/user}","branches_url":"https://api.github.com/repos/phenotips/phenotips/branches{/branch}","tags_url":"https://api.github.com/repos/phenotips/phenotips/tags","blobs_url":"https://api.github.com/repos/phenotips/phenotips/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/phenotips/phenotips/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/phenotips/phenotips/git/refs{/sha}","trees_url":"https://api.github.com/repos/phenotips/phenotips/git/trees{/sha}","statuses_url":"https://api.github.com/repos/phenotips/phenotips/statuses/{sha}","languages_url":"https://api.github.com/repos/phenotips/phenotips/languages","stargazers_url":"https://api.github.com/repos/phenotips/phenotips/stargazers","contributors_url":"https://api.github.com/repos/phenotips/phenotips/contributors","subscribers_url":"https://api.github.com/repos/phenotips/phenotips/subscribers","subscription_url":"https://api.github.com/repos/phenotips/phenotips/subscription","commits_url":"https://api.github.com/repos/phenotips/phenotips/commits{/sha}","git_commits_url":"https://api.github.com/repos/phenotips/phenotips/git/commits{/sha}","comments_url":"https://api.github.com/repos/phenotips/phenotips/comments{/number}","issue_comment_url":"https://api.github.com/repos/phenotips/phenotips/issues/comments{/number}","contents_url":"https://api.github.com/repos/phenotips/phenotips/contents/{+path}","compare_url":"https://api.github.com/repos/phenotips/phenotips/compare/{base}...{head}","merges_url":"https://api.github.com/repos/phenotips/phenotips/merges","archive_url":"https://api.github.com/repos/phenotips/phenotips/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/phenotips/phenotips/downloads","issues_url":"https://api.github.com/repos/phenotips/phenotips/issues{/number}","pulls_url":"https://api.github.com/repos/phenotips/phenotips/pulls{/number}","milestones_url":"https://api.github.com/repos/phenotips/phenotips/milestones{/number}","notifications_url":"https://api.github.com/repos/phenotips/phenotips/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/phenotips/phenotips/labels{/name}","releases_url":"https://api.github.com/repos/phenotips/phenotips/releases{/id}","created_at":"2011-11-01T05:55:21Z","updated_at":"2015-12-03T09:51:58Z","pushed_at":"2015-12-04T19:42:57Z","git_url":"git://github.com/phenotips/phenotips.git","ssh_url":"git@github.com:phenotips/phenotips.git","clone_url":"https://github.com/phenotips/phenotips.git","svn_url":"https://github.com/phenotips/phenotips","homepage":"http://phenotips.org/","size":808268,"stargazers_count":24,"watchers_count":24,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":15,"mirror_url":null,"open_issues_count":294,"forks":15,"open_issues":294,"watchers":24,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/1944"},"html":{"href":"https://github.com/phenotips/phenotips/pull/1944"},"issue":{"href":"https://api.github.com/repos/phenotips/phenotips/issues/1944"},"comments":{"href":"https://api.github.com/repos/phenotips/phenotips/issues/1944/comments"},"review_comments":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/1944/comments"},"review_comment":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/phenotips/phenotips/pulls/1944/commits"},"statuses":{"href":"https://api.github.com/repos/phenotips/phenotips/statuses/c148273129105cbfa82eae231daf1f5677e8995c"}}}}