landmark-serve
Version:
Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose
316 lines (248 loc) • 8.95 kB
JavaScript
/*!
* Module dependencies.
*/
var _ = require('underscore'),
marked = require('marked'),
Path = require('./path'),
fspath = require('path'),
jade = require('jade'),
fs = require('fs'),
landmark = require('../'),
utils = require('landmark-utils'),
compiledTemplates = {};
/**
* Field Constructor
* =================
*
* Extended by fieldType Classes, should not be used directly.
*
* @api public
*/
function Field(list, path, options) {
// Set field properties and options
this.list = list;
this._path = new Path(path);
this.path = path;
this.type = this.constructor.name;
this.options = utils.options(this.defaults, options);
this.label = options.label || utils.keyToLabel(this.path);
this.typeDescription = options.typeDescription || this.typeDescription || this.type;
// Add the field to the schema
this.list.automap(this);
this.addToSchema();
// Warn on required fields that aren't initial
if (this.options.required &&
this.options.initial === undefined &&
this.options.default === undefined &&
!this.options.value &&
!this.list.get('nocreate') &&
this.path != this.list.mappings.name
) {
console.error('\nError: Invalid Configuration\n\n' +
'Field (' + list.key + '.' + path + ') is required but not initial, and has no default or generated value.\n' +
'Please provide a default, remove the required setting, or set initial: false to override this error.\n');
process.exit(1);
}
// Set up templates
this.templateDir = fspath.normalize(options.templateDir || (__dirname + '../../templates/fields/' + this.type));
var defaultTemplates = {
"form": this.templateDir + '/' + 'form.jade',
"initial": this.templateDir + '/' + 'initial.jade'
};
this.templates = utils.options(defaultTemplates, this.options.templates);
// Add pre-save handler to the list if this field watches others
if (this.options.watch) {
this.list.schema.pre('save', this.getPreSaveWatcher());
}
// Convert notes from markdown to html
var note = null;
Object.defineProperty(this, 'note', { get: function() {
return (note === null) ? (note = (this.options.note) ? marked(this.options.note) : '') : note;
} });
}
Field.prototype.getPreSaveWatcher = function(next) {
var field = this,
applyValue;
if (this.options.watch === true) {
// watch == true means always apply the value method
applyValue = function() { return true; };
} else {
// if watch is a string, convert it to a list of paths to watch
if (_.isString(this.options.watch)) {
this.options.watch = this.options.watch.split(' ');
}
if (_.isFunction(this.options.watch)) {
applyValue = this.options.watch;
} else if (_.isArray(this.options.watch)) {
applyValue = function(item) {
var pass = false;
field.options.watch.forEach(function(path) {
if (!item.isModified(path)) pass = true;
});
return pass;
};
} else if (_.isObject(this.options.watch)) {
applyValue = function(item) {
var pass = false;
_.each(field.options.watch, function(value, path) {
if (item.isModified(path) && item.get('path') == value) pass = true;
});
return pass;
};
}
}
if (!applyValue) {
console.error('\nError: Invalid Configuration\n\n' +
'Invalid watch value (' + this.options.watch + ') provided for ' + list.key + '.' + this.path + ' (' + this.type + ')');
process.exit(1);
}
if (!_.isFunction(this.options.value)) {
console.error('\nError: Invalid Configuration\n\n' +
'Watch set with no value method provided for ' + list.key + '.' + this.path + ' (' + this.type + ')');
process.exit(1);
}
return function(next) {
if (!applyValue(this)) {
return next();
}
this.set(field.path, field.options.value.call(this));
next();
};
};
exports = module.exports = Field;
/** Getter properties for the Field prototype */
Object.defineProperty(Field.prototype, 'width', { get: function() { return this.options.width || 'full'; } }); // !! field width is, for certain types, overridden by css
Object.defineProperty(Field.prototype, 'initial', { get: function() { return this.options.initial || false; } });
Object.defineProperty(Field.prototype, 'required', { get: function() { return this.options.required || false; } });
Object.defineProperty(Field.prototype, 'col', { get: function() { return this.options.col || false; } });
Object.defineProperty(Field.prototype, 'noedit', { get: function() { return this.options.noedit || false; } });
Object.defineProperty(Field.prototype, 'nocol', { get: function() { return this.options.nocol || false; } });
Object.defineProperty(Field.prototype, 'nosort', { get: function() { return this.options.nosort || false; } });
Object.defineProperty(Field.prototype, 'nofilter', { get: function() { return this.options.nofilter || false; } });
Object.defineProperty(Field.prototype, 'collapse', { get: function() { return this.options.collapse || false; } });
Object.defineProperty(Field.prototype, 'hidden', { get: function() { return this.options.hidden || false; } });
Object.defineProperty(Field.prototype, 'dependsOn', { get: function() { return this.options.dependsOn || false; } });
/**
* Default method to register the field on the List's Mongoose Schema.
* Overridden by some fieldType Classes
*
* @api public
*/
Field.prototype.addToSchema = function() {
var ops = (this._nativeType) ? _.defaults({ type: this._nativeType }, this.options) : this.options;
this.list.schema.path(this.path, ops);
this.bindUnderscoreMethods();
};
Field.prototype.bindUnderscoreMethods = function(methods) {
var field = this;
// automatically bind underscore methods specified by the _underscoreMethods property
// always include the 'update' method
(this._underscoreMethods || []).concat({ fn: 'updateItem', as: 'update' }, (methods || [])).forEach(function(method) {
if ('string' == typeof method) {
method = { fn: method, as: method };
}
if ('function' != typeof field[method.fn]) {
throw new Error('Invalid underscore method (' + method.fn + ') applied to ' + field.list.key + '.' + field.path + ' (' + field.type + ')');
}
field.underscoreMethod(method.as, function() {
var args = [this].concat(Array.prototype.slice.call(arguments));
return field[method.fn].apply(field, args);
});
});
};
/**
* Adds a method to the underscoreMethods collection on the field's list,
* with a path prefix to match this field's path and bound to the document
*
* @api public
*/
Field.prototype.underscoreMethod = function(path, fn) {
this.list.underscoreMethod(this.path + '.' + path, function() {
return fn.apply(this, arguments);
});
};
/**
* Default method to format the field value for display
* Overridden by some fieldType Classes
*
* @api public
*/
Field.prototype.format = function(item) {
return item.get(this.path);
};
/**
* Default method to detect whether the field has been modified in an item
* Overridden by some fieldType Classes
*
* @api public
*/
Field.prototype.isModified = function(item) {
return item.isModified(this.path);
};
/**
* Validates that a value for this field has been provided in a data object
* Overridden by some fieldType Classes
*
* @api public
*/
Field.prototype.validateInput = function(data, required, item) {
if (!required) return true;
if (!(this.path in data) && item && item.get(this.path)) return true;
if ('string' == typeof data[this.path]) {
return (data[this.path].trim()) ? true : false;
} else {
return (data[this.path]) ? true : false;
}
};
/**
* Updates the value for this field in the item from a data object
* Overridden by some fieldType Classes
*
* @api public
*/
Field.prototype.updateItem = function(item, data) {
if (this.path in data && data[this.path] != item.get(this.path)) {
item.set(this.path, data[this.path]);
}
};
/**
* Compiles a field template and caches it
*
* @api public
*/
Field.prototype.compile = function(type, callback) {
var templatePath = this.templates[type];
if (!compiledTemplates[templatePath]) {
fs.readFile(templatePath, 'utf8', function(err, file) {
if (!err){
compiledTemplates[templatePath] = jade.compile(file, {
filename: templatePath,
pretty: landmark.get('env') != "production"
});
}
if (callback) return callback();
});
} else if (callback) {
return callback();
}
};
/**
* Compiles a field template and caches it
*
* @api public
*/
Field.prototype.render = function(type, item, locals) {
var templatePath = this.templates[type];
// Compile the template synchronously if it hasn't already been compiled
if (!compiledTemplates[templatePath]) {
var file = fs.readFileSync(templatePath, 'utf8');
compiledTemplates[templatePath] = jade.compile(file, {
filename: templatePath,
pretty: (landmark.get('env') != "production")
});
}
return compiledTemplates[templatePath](_.extend(locals || {}, {
field: this,
item: item
}));
};