foam-framework
Version:
MVC metaprogramming framework
368 lines (331 loc) • 11.3 kB
JavaScript
/**
* @license
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
CLASS({
package: 'foam.ui',
name: 'TextFieldView',
label: 'Text Field',
extends: 'foam.ui.SimpleView',
requires: [ 'foam.ui.AutocompleteView' ],
documentation: function() { /*
The default $$DOC{ref:'foam.ui.View'} for a string. Supports autocomplete
when an autocompleter is installed in $$DOC{ref:'.autocompleter'}.
*/},
models: [
{
name: 'OnEnterValue',
properties: [
{
name: 'element',
},
{
name: 'listeners',
factory: function() {
return [];
}
}
],
methods: [
function get() { return this.element.value; },
function set(value) {
if ( this.get() !== value ) this.element.value = value;
},
function addListener(listener) {
if ( ! listener ) return;
if ( this.listeners.length === 0 )
this.element.addEventListener('keydown', this.onKeyDown);
this.listeners.push(listener);
},
function removeListener(listener) {
var index = this.listeners.indexOf(listener);
if ( index >= 0 ) this.listeners.splice(i, 1);
},
function fireListeners(e) {
for (var i = 0; i < this.listeners.length; i++) {
this.listeners[i](e);
}
}
],
listeners: [
{
name: 'onKeyDown',
code: function(e) {
if ( e.keyCode === 13 ) {
this.fireListeners(e);
}
}
}
]
}
],
properties: [
{
model_: 'StringProperty',
name: 'name',
defaultValue: 'field',
documentation: function() { /* The name of the field. */}
},
{
model_: 'IntProperty',
name: 'displayWidth',
defaultValue: 30,
documentation: function() { /* The width to fix the HTML text box. */}
},
{
model_: 'IntProperty',
name: 'displayHeight',
defaultValue: 1,
documentation: function() { /* The height to fix the HTML text box. */}
},
{
model_: 'StringProperty',
name: 'type',
defaultValue: 'text',
documentation: function() { /* The type of field to create. */}
},
{
model_: 'StringProperty',
name: 'placeholder',
defaultValue: undefined,
documentation: function() { /* Placeholder to use when empty. */}
},
{
model_: 'BooleanProperty',
name: 'onKeyMode',
help: 'If true, value is updated on each keystroke.',
documentation: function() { /* If true, value is updated on each keystroke. */},
getter: function() {
return this.updateMode === this.EACH_KEYSTROKE;
},
setter: function(nu) {
this.updateMode = nu ? this.EACH_KEYSTROKE : this.DONE_EDITING;
}
},
{
model_: 'foam.core.types.StringEnumProperty',
name: 'updateMode',
help: 'Controls when the real .data is updated: on every keystroke, ' +
'when the user presses enter or blurs the box, or on enter only.',
defaultValue: 'DONE_EDITING',
choices: [
['DONE_EDITING', 'Done editing'],
['EACH_KEYSTROKE', 'Every keystroke'],
['ENTER_ONLY', 'Enter only']
]
},
{
model_: 'BooleanProperty',
name: 'escapeHTML',
defaultValue: true,
// TODO: make the default 'true' for security reasons
help: 'If true, HTML content is escaped in display mode.',
documentation: function() { /* If true, HTML content is escaped in display mode. */}
},
{
model_: 'StringProperty',
name: 'mode',
defaultValue: 'read-write',
view: { factory_: 'foam.ui.ChoiceView', choices: ['read-only', 'read-write', 'final'] },
documentation: function() { /* Can be 'read-only', 'read-write' or 'final'. */}
},
{
model_: 'BooleanProperty',
name: 'required',
documentation: 'If value is required.'
},
{
model_: 'StringProperty',
name: 'pattern',
documentation: 'Regex pattern for value.'
},
{
name: 'domValue',
hidden: true
},
{
name: 'data',
documentation: function() { /* The object to bind to the user's entered text. */}
},
{
model_: 'StringProperty',
name: 'readWriteTagName',
defaultValueFn: function() {
return this.displayHeight === 1 ? 'input' : 'textarea';
},
hidden: true
},
{
model_: 'BooleanProperty',
name: 'autocomplete',
defaultValue: true,
documentation: function() { /* Set to true to enable autocomplete. */}
},
{
name: 'autocompleter',
documentation: function() { /* The autocompleter model to use. */}
},
{
name: 'autocompleteView',
documentation: function() { /* The autocomplete view created. */}
}
],
constants: {
/** Escape topic published when user presses 'escape' key to abort edits. **/
// TODO: Model as a 'Topic'
ESCAPE: ['escape'],
// These are the constants used by the updateMode. The text of these is
// duplicated in the choices array of the updateMode property.
// TODO(braden): That duplication sucks, we need a better way to handle
// enums.
DONE_EDITING: 'DONE_EDITING',
EACH_KEYSTROKE: 'EACH_KEYSTROKE',
ENTER_ONLY: 'ENTER_ONLY'
},
methods: {
toHTML: function() {
/* Selects read-only versus read-write DOM output */
return this.mode === 'read-write' ?
this.toReadWriteHTML() :
this.toReadOnlyHTML() ;
},
toReadWriteHTML: function() {
/* Supplies the correct element for read-write mode */
var str = '<' + this.readWriteTagName + ' id="' + this.id + '"';
str += ' type="' + this.type + '" ' + this.cssClassAttr();
this.on('click', this.onClick, this.id);
str += this.readWriteTagName === 'input' ?
' size="' + this.displayWidth + '"' :
' rows="' + this.displayHeight + '" cols="' + this.displayWidth + '"';
if ( this.required ) str += ' required';
if ( this.pattern ) str += ' pattern="' + this.pattern + '"';
str += this.extraAttributes();
str += ' name="' + this.name + '">';
str += '</' + this.readWriteTagName + '>';
return str;
},
extraAttributes: function() { return ''; },
toReadOnlyHTML: function() {
/* Supplies the correct element for read-only mode */
var self = this;
this.setClass('placeholder', function() { return self.data === ''; }, this.id);
// Changing to a textarea doesn't work well because you can't override displayHeight
// in templates
return /* this.displayHeight === 1 ? */ '<' + this.tagName + ' id="' + this.id + '"' + this.cssClassAttr() + ' name="' + this.name + '"></' + this.tagName + '>' /*:
'<textarea readonly id="' + this.id + '"' + this.cssClassAttr() + ' name="' + this.name + '" rows="' + this.displayHeight + '" cols="' + this.displayWidth + '"></textarea>'*/ ;
},
setupAutocomplete: function() {
/* Initializes autocomplete, if $$DOC{ref:'.autocomplete'} and
$$DOC{ref:'.autocompleter'} are set. */
if ( ! this.autocomplete || ! this.autocompleter ) return;
var view = this.autocompleteView = this.AutocompleteView.create({
autocompleter: this.autocompleter,
target: this
});
this.bindAutocompleteEvents(view);
},
onAutocomplete: function(data) {
this.data = data;
},
bindAutocompleteEvents: function(view) {
this.$.addEventListener('blur', function() {
// Notify the autocomplete view of a blur, it can decide what to do from there.
view.publish('blur');
});
this.$.addEventListener('input', (function() {
view.autocomplete(this.textToValue(this.$.value));
}).bind(this));
this.$.addEventListener('focus', (function() {
view.autocomplete(this.textToValue(this.$.value));
}).bind(this));
},
initHTML: function() {
if ( ! this.$ ) return;
this.SUPER();
if ( this.mode === 'read-write' ) {
if ( this.placeholder ) this.$.placeholder = this.placeholder;
if ( this.updateMode === this.EACH_KEYSTROKE ) {
this.domValue = DomValue.create(this.$, 'input');
} else if ( this.updateMode === this.DONE_EDITING ) {
this.domValue = DomValue.create(this.$, 'change');
} else {
this.domValue = this.OnEnterValue.create({ element: this.$ });
}
// In KeyMode we disable feedback to avoid updating the field
// while the user is still typing. Then we update the view
// once they leave(blur) the field.
Events.relate(
this.data$,
this.domValue,
this.valueToText.bind(this),
this.textToValue.bind(this),
this.updateMode === this.EACH_KEYSTROKE);
if ( this.updateMode === this.EACH_KEYSTROKE )
this.$.addEventListener('blur', this.onBlur);
this.$.addEventListener('keydown', this.onKeyDown);
this.$.addEventListener('keypress', this.onKeyPress);
this.setupAutocomplete();
} else {
this.domValue = DomValue.create(
this.$,
'undefined',
this.escapeHTML ? 'textContent' : 'innerHTML');
Events.map(
this.data$,
this.domValue,
this.valueToText.bind(this))
}
},
textToValue: function(text) { /* Passthrough */ return text; },
valueToText: function(value) { /* Filters for read-only mode */
if ( this.mode === 'read-only' )
return (value === '') ? this.placeholder : value;
return value;
},
destroy: function( isParentDestroyed ) { /* Unlinks key handler. */
this.SUPER(isParentDestroyed);
Events.unlink(this.domValue, this.data$);
}
},
listeners: [
{
name: 'onKeyDown',
code: function(e) {
if ( e.keyCode == 27 /* ESCAPE KEY */ ) {
this.domValue.set(this.data);
this.publish(this.ESCAPE);
} else {
this.publish(['keydown'], e);
}
}
},
{
name: 'onKeyPress',
documentation: 'Prevent shortcut keys from firing on <input> element',
code: function(e) { e.stopPropagation(); }
},
{
name: 'onBlur',
code: function(e) {
if ( this.domValue.get() !== this.data )
this.domValue.set(this.data);
}
},
{
name: 'onClick',
code: function(e) { this.$ && this.$.focus(); }
},
]
});