First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
6
sites/all/modules/civicrm/packages/backbone-forms/.gitmodules
vendored
Normal file
6
sites/all/modules/civicrm/packages/backbone-forms/.gitmodules
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
[submodule "lib/backbone.bootstrap-modal"]
|
||||
path = lib/backbone.bootstrap-modal
|
||||
url = git@github.com:powmedia/backbone.bootstrap-modal.git
|
||||
[submodule "node_modules/buildify"]
|
||||
path = node_modules/buildify
|
||||
url = git@github.com:powmedia/buildify.git
|
19
sites/all/modules/civicrm/packages/backbone-forms/LICENSE
Normal file
19
sites/all/modules/civicrm/packages/backbone-forms/LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2012 Charles Davison
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
951
sites/all/modules/civicrm/packages/backbone-forms/README.md
Normal file
951
sites/all/modules/civicrm/packages/backbone-forms/README.md
Normal file
|
@ -0,0 +1,951 @@
|
|||
#backbone-forms
|
||||
|
||||
A flexible, customisable form framework for Backbone.JS applications.
|
||||
|
||||
- Simple schema definition to auto-generate forms
|
||||
- Validation
|
||||
- Nested forms
|
||||
- Advanced and custom editors (e.g. NestedModel, List, Date, DateTime)
|
||||
- Custom HTML templates
|
||||
|
||||
|
||||
###Example
|
||||
|
||||
var User = Backbone.Model.extend({
|
||||
schema: {
|
||||
title: { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] },
|
||||
name: 'Text',
|
||||
email: { validators: ['required', 'email'] },
|
||||
birthday: 'Date',
|
||||
password: 'Password',
|
||||
address: { type: 'NestedModel', model: Address },
|
||||
notes: { type: 'List', listType: 'Text' }
|
||||
}
|
||||
});
|
||||
|
||||
var user = new User();
|
||||
|
||||
var form = new Backbone.Form({
|
||||
model: user
|
||||
}).render();
|
||||
|
||||
$('body').append(form.el);
|
||||
|
||||
|
||||
###Live editable demos
|
||||
- [User form](http://jsfiddle.net/evilcelery/VkUFu/)
|
||||
- [Form with Bootstrap templates and an Object list](http://jsfiddle.net/evilcelery/4XZMb/)
|
||||
- [Update form elements based on user input](http://jsfiddle.net/evilcelery/c5QHr/)
|
||||
|
||||
|
||||
|
||||
<a name="top"/>
|
||||
##Guide
|
||||
|
||||
###Table of Contents:
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Backbone.Form](#form)
|
||||
- [Schema Definition](#schema-definition)
|
||||
- [Text](#editor-text)
|
||||
- [Checkboxes](#editor-checkboxes)
|
||||
- [Select](#editor-select)
|
||||
- [Radio](#editor-radio)
|
||||
- [Object](#editor-object)
|
||||
- [NestedModel](#editor-nestedmodel)
|
||||
- [Date](#editor-date)
|
||||
- [DateTime](#editor-datetime)
|
||||
- [List](#editor-list)
|
||||
- [Validation](#validation)
|
||||
- [Customising templates](#customising-templates)
|
||||
- [More](#more)
|
||||
- [Editors without forms](#editors-without-forms)
|
||||
- [Using nested fields](#nested-fields)
|
||||
- [Custom editors](#custom-editors)
|
||||
- [Help](#help)
|
||||
- [Changelog](#changelog)
|
||||
|
||||
|
||||
|
||||
<a name="installation"/>
|
||||
##Installation
|
||||
|
||||
Dependencies:
|
||||
- [Backbone 0.9.2](http://documentcloud.github.com/backbone/)
|
||||
|
||||
|
||||
Include backbone-forms.js and backbone-forms.css:
|
||||
|
||||
<script src="backbone-forms/distribution/backbone-forms.min.js"></script>
|
||||
<link href="backbone-forms/distribution/templates/default.css" rel="stylesheet" />
|
||||
|
||||
Optionally, you can include the extra editors, for example the List editor:
|
||||
|
||||
<script src="backbone-forms/distribution/editors/list.min.js"></script>
|
||||
|
||||
To use a custom template pack, e.g. Bootstrap, include the relevants file after backbone-forms.js. You can remove `templates/default.css` and replace it with `templates/bootstrap.css`.
|
||||
|
||||
<script src="backbone-forms/distribution/templates/bootstrap.js"></script>
|
||||
<link href="backbone-forms/distribution/templates/bootstrap.css" rel="stylesheet" />
|
||||
|
||||
If you use Backbone with node.js, you can just `require('backbone-forms');` in your index file.
|
||||
|
||||
Note there is also a distribution file for RequireJS / AMD.
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
|
||||
<a name="usage"/>
|
||||
##Usage
|
||||
|
||||
Define a `schema` attribute on your Backbone models. The schema keys should match the attributes that get set on the model. `type` defaults to `Text`. When you don't need to specify any options you can use the shorthand by passing the editor name as a string.
|
||||
See [schema definition](#schema-definition) for more information.
|
||||
|
||||
var User = Backbone.Model.extend({
|
||||
schema: {
|
||||
title: { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] },
|
||||
name: 'Text',
|
||||
email: { validators: ['required', 'email'] },
|
||||
birthday: 'Date',
|
||||
password: 'Password',
|
||||
address: { type: 'NestedModel', model: Address },
|
||||
notes: { type: 'List', listType: 'Text' }
|
||||
}
|
||||
});
|
||||
|
||||
var user = new User();
|
||||
|
||||
var form = new Backbone.Form({
|
||||
model: user
|
||||
}).render();
|
||||
|
||||
$('body').append(form.el);
|
||||
|
||||
|
||||
Once the user is done with the form, call `form.commit()` to apply the updated values to the model. If there are validation errors they will be returned.
|
||||
See [validation](#validation) for more information.
|
||||
|
||||
var errors = form.commit();
|
||||
|
||||
To update a field after the form has been rendered, use `form.setValue`:
|
||||
|
||||
model.on('change:name', function(model, name) {
|
||||
form.setValue({ name: name });
|
||||
});
|
||||
|
||||
|
||||
###Usage without models
|
||||
|
||||
You can create a form without tying it to a model. For example, to create a form for a simple object of data:
|
||||
|
||||
var form = new Backbone.Form({
|
||||
//Data to populate the form with
|
||||
data: {
|
||||
id: 123,
|
||||
name: 'Rod Kimble',
|
||||
password: 'cool beans'
|
||||
},
|
||||
|
||||
//Schema
|
||||
schema: {
|
||||
id: 'Number',
|
||||
name: 'Text',
|
||||
password: 'Password'
|
||||
}
|
||||
}).render();
|
||||
|
||||
Then instead of `form.commit()`, do:
|
||||
|
||||
var data = form.getValue(); //Returns object with new form values
|
||||
|
||||
|
||||
###Initial data
|
||||
If a form has a model attached to it, the initial values are taken from the model's defaults. Otherwise, you may pass default values using the `schema.data`.
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
<a name="form"/>
|
||||
##Backbone.Form
|
||||
|
||||
###Options
|
||||
|
||||
- **`model`**
|
||||
|
||||
The model to tie the form to. Calling `form.commit()` will update the model with new values.
|
||||
|
||||
- **`data`**
|
||||
|
||||
If not using the `model` option, pass a native object through the `data` option. Then use `form.getValue()` to get the new values.
|
||||
|
||||
- **`schema`**
|
||||
|
||||
The schema to use to create the form. Pass it in if you don't want to store the schema on the model, or to override the model schema.
|
||||
|
||||
- **`fieldsets`**
|
||||
|
||||
An array of fieldsets descriptions. A fieldset is either a list of field names, or an object with `legend` and `fields` attributes. The `legend` will be inserted at the top of the fieldset inside a `<legend>` tag; the list of fields will be treated as `fields` is below. `fieldsets` takes priority over `fields`.
|
||||
|
||||
- **`fields`**
|
||||
|
||||
An array of field names (keys). Only the fields defined here will be added to the form. You can also use this to re-order the fields.
|
||||
|
||||
- **`idPrefix`**
|
||||
|
||||
A string that will be prefixed to the form DOM element IDs. Useful if you will have multiple forms on the same page. E.g. `idPrefix: 'user-'` will result in IDs like 'user-name', 'user-email', etc.
|
||||
|
||||
If not defined, the model's CID will be used as a prefix to avoid conflicts when there are multiple instances of the form on the page. To override this behaviour, pass a null value to `idPrefix`.
|
||||
|
||||
- **`template`**
|
||||
|
||||
The template name to use for generating the form. E.g.:
|
||||
|
||||
Backbone.Form.setTemplates({
|
||||
customForm: '<form class="custom-form">{{fieldsets}}</form>'
|
||||
});
|
||||
|
||||
var form = new Backbone.Form({
|
||||
model: user,
|
||||
template: 'customForm'
|
||||
});
|
||||
|
||||
|
||||
###Events
|
||||
|
||||
`Backbone.Form` fires the following events:
|
||||
|
||||
- **`change`**
|
||||
|
||||
This event is triggered whenever something happens that affects the result of `form.getValue()`.
|
||||
|
||||
- **`focus`**
|
||||
|
||||
This event is triggered whenever this form gains focus, i.e. when the input of an editor within this form becomes the `document.activeElement`.
|
||||
|
||||
- **`blur`**
|
||||
|
||||
This event is triggered whenever this form loses focus, i.e. when the input of an editor within this form stops being the `document.activeElement`.
|
||||
|
||||
- **`<key>:<event>`**
|
||||
|
||||
Events fired by editors within this form will bubble up and be fired as `<key>:<event>`.
|
||||
|
||||
form.on('title:change', function(form, titleEditor) {
|
||||
console.log('Title changed to "' + titleEditor.getValue() + '".');
|
||||
});
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
<a name="schema-definition"/>
|
||||
##Schema definition
|
||||
|
||||
The schema defined on your model can be the schema object itself, or a function that returns a schema object. This can be useful if you're referencing variables that haven't been initialized yet.
|
||||
|
||||
The following default editors are included:
|
||||
|
||||
- [Text](#editor-text)
|
||||
- Number
|
||||
- Password
|
||||
- TextArea
|
||||
- Checkbox
|
||||
- [Checkboxes](#editor-checkboxes)
|
||||
- Hidden
|
||||
- [Select](#editor-select)
|
||||
- [Radio](#editor-radio)
|
||||
- [Object](#editor-object)
|
||||
- [NestedModel](#editor-nestedmodel)
|
||||
- [Date](#editor-date)
|
||||
- [DateTime](#editor-datetime)
|
||||
- [List](#editor-list) An editable list of items (included in a separate file: `distribution/editors/list.min.js`)
|
||||
|
||||
|
||||
The old jQuery editors are still included but may be moved to another repository:
|
||||
- jqueryui.List
|
||||
- jqueryui.Date (uses the jQuery UI popup datepicker)
|
||||
- jqueryui.DateTime
|
||||
|
||||
|
||||
|
||||
###Main attributes
|
||||
|
||||
For each field definition in the schema you can use the following optional attributes:
|
||||
|
||||
- **`type`**
|
||||
|
||||
The editor to use in the field. Can be a string for any editor that has been added to `Backbone.Form.editors`, such as the built-in editors (e.g. `{ type: 'TextArea' }`), or can be a constructor function for a custom editor (e.g. : `{ type: MyEditor }`).
|
||||
|
||||
If not defined, defaults to 'Text'.
|
||||
|
||||
- **`title`**
|
||||
|
||||
Defines the text that appears in a form field's `<label>`. If not defined, defaults to a formatted version of the camelCased field key. E.g. `firstName` becomes `First Name`. This behaviour can be changed by assigning your own function to `Backbone.Form.helpers.keyToTitle`.
|
||||
|
||||
- **`validators`**
|
||||
|
||||
A list of validators. See [Validation](#validation) for more information
|
||||
|
||||
- **`help`**
|
||||
|
||||
Help text to add next to the editor.
|
||||
|
||||
- **`editorClass`**
|
||||
|
||||
String of CSS class name(s) to add to the editor
|
||||
|
||||
- **`editorAttrs`**
|
||||
|
||||
A map of attributes to add to the editor, e.g. `{ maxlength: 30, title: 'Tooltip help' }`
|
||||
|
||||
- **`fieldClass`**
|
||||
|
||||
String of CSS class name(s) to add to the field
|
||||
|
||||
- **`fieldAttrs`**
|
||||
|
||||
A map of attributes to add to the field, e.g. `{ style: 'background: red', title: 'Tooltip help' }`
|
||||
|
||||
- **`template`**
|
||||
|
||||
Name of the template to use for this field. See [Customising templates](#customising-templates) for more information.
|
||||
|
||||
###Main events
|
||||
|
||||
Every editor fires the following events:
|
||||
|
||||
- **`change`**
|
||||
|
||||
This event is triggered whenever something happens that affects the result of `editor.getValue()`.
|
||||
|
||||
- **`focus`**
|
||||
|
||||
This event is triggered whenever this editor gains focus, i.e. when an input within this editor becomes the `document.activeElement`.
|
||||
|
||||
- **`blur`**
|
||||
|
||||
This event is triggered whenever this editor loses focus, i.e. when an input within this editor stops being the `document.activeElement`.
|
||||
|
||||
Besides these three, editors can implement custom events, which are described below.
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
<a name="editor-text"/>
|
||||
##Text
|
||||
|
||||
Creates a normal text input.
|
||||
|
||||
- **`dataType`**
|
||||
|
||||
Changes the `type="text"` attribute. Used for HTML5 form inputs such as `url`, `tel`, `email`. When viewing on a mobile device e.g. iOS, this will change the type of keyboard that is opened. For example, `tel` opens a numeric keypad.
|
||||
|
||||
|
||||
<a name="editor-select"/>
|
||||
##Select
|
||||
|
||||
Creates and populates a `<select>` element.
|
||||
|
||||
- **`options`**
|
||||
|
||||
Options to populate the `<select>`.
|
||||
|
||||
Can be either:
|
||||
- String of HTML `<option>`s
|
||||
- Array of strings/numbers
|
||||
- Array of objects in the form `{ val: 123, label: 'Text' }`
|
||||
- A Backbone collection
|
||||
- A function that calls back with one of the above
|
||||
|
||||
**Backbone collection notes**
|
||||
|
||||
If using a Backbone collection as the `options` attribute, models in the collection must implement a `toString()` method. This populates the label of the `<option>`. The ID of the model populates the `value` attribute.
|
||||
|
||||
If there are no models in the collection, it will be `fetch()`ed.
|
||||
|
||||
|
||||
####Methods
|
||||
|
||||
- **`setOptions()`**
|
||||
|
||||
Update the options in the select. Accepts any of the types that can be set in the schema `options`
|
||||
|
||||
|
||||
|
||||
####Examples
|
||||
|
||||
var schema = {
|
||||
country: { type: 'Select', options: new CountryCollection() }
|
||||
};
|
||||
|
||||
var schema = {
|
||||
users: { type: 'Select', options: function(callback) {
|
||||
users = db.getUsers();
|
||||
|
||||
callback(users);
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
<a name="editor-radio"/>
|
||||
##Radio
|
||||
|
||||
Creates and populates a list of radio inputs. Behaves the same way and has the same options as a `Select`.
|
||||
|
||||
|
||||
<a name="editor-checkboxes"/>
|
||||
##Checkboxes
|
||||
|
||||
Creates and populates a list of checkbox inputs. Behaves the same way and has the same options as a `Select`. To set defaults for this editor, use an array of values.
|
||||
|
||||
|
||||
<a name="editor-object"/>
|
||||
##Object
|
||||
|
||||
The Object editor creates an embedded child form representing a Javascript object.
|
||||
|
||||
###Attributes
|
||||
|
||||
- **`subSchema`**
|
||||
|
||||
A schema object which defines the field schema for each attribute in the object
|
||||
|
||||
###Events
|
||||
|
||||
- **`<key>:<event>`**
|
||||
|
||||
Events fired by editors within this Object editor will bubble up and be fired as `<key>:<event>`.
|
||||
|
||||
####Examples
|
||||
|
||||
var schema = {
|
||||
address: { type: 'Object', subSchema: {
|
||||
street: {},
|
||||
zip: { type: 'Number' },
|
||||
country: { 'Select', options: countries }
|
||||
}}
|
||||
};
|
||||
|
||||
addressEditor.on('zip:change', function(addressEditor, zipEditor) {
|
||||
console.log('Zip changed to "' + zipEditor.getValue() + '".');
|
||||
});
|
||||
|
||||
|
||||
<a name="editor-nestedmodel"/>
|
||||
##NestedModel
|
||||
|
||||
Used to embed models within models. Similar to the Object editor, but adds validation of the child form (if it is defined on the model), and keeps your schema cleaner.
|
||||
|
||||
###Attributes
|
||||
|
||||
- **`model`**
|
||||
|
||||
A reference to the constructor function for your nested model. The referenced model must have it's own `schema` attribute
|
||||
|
||||
###Events
|
||||
|
||||
- **`<key>:<event>`**
|
||||
|
||||
Events fired by editors within this NestedModel editor will bubble up and be fired as `<key>:<event>`.
|
||||
|
||||
####Examples
|
||||
|
||||
var schema = {
|
||||
address: { type: 'NestedModel', model: Address }
|
||||
};
|
||||
|
||||
addressEditor.on('zip:change', function(addressEditor, zipEditor) {
|
||||
console.log('Zip changed to "' + zipEditor.getValue() + '".');
|
||||
});
|
||||
|
||||
|
||||
|
||||
<a name="editor-date"/>
|
||||
##Date
|
||||
|
||||
Creates `<select>`s for date, month and year.
|
||||
|
||||
- **`yearStart`**
|
||||
|
||||
First year in the list. Default: 100 years ago
|
||||
|
||||
- **`yearEnd`**
|
||||
|
||||
Last year in the list. Default: current year
|
||||
|
||||
|
||||
####Extra options
|
||||
You can customise the way this editor behaves, throughout your app:
|
||||
|
||||
var editors = Backbone.Form.editors;
|
||||
|
||||
editors.Date.showMonthNames = false; //Defaults to true
|
||||
editors.Date.monthNames = ['Jan', 'Feb', ...] //Defaults to full month names in English
|
||||
|
||||
|
||||
<a name="editor-datetime"/>
|
||||
##DateTime
|
||||
|
||||
Creates a Date editor and adds `<select>`s for time (hours and minutes).
|
||||
|
||||
- **`minsInterval`**
|
||||
|
||||
Optional. Controls the numbers in the minutes dropdown. Defaults to 15, so it is populated with 0, 15, 30, and 45 minutes.
|
||||
|
||||
|
||||
<a name="editor-list"/>
|
||||
##List
|
||||
|
||||
Creates a list of items that can be added, removed and edited. Used to manage arrays of data.
|
||||
|
||||
This is a special editor which is in **a separate file and must be included**:
|
||||
|
||||
<script src="backbone-forms/distribution/editors/list.min.js" />
|
||||
|
||||
**If using the `Object` or `NestedModel` listType**, you will need to include a modal adapter on the page. [Backbone.BootstrapModal](http://github.com/powmedia/backbone.bootstrap-modal) is provided for this purpose. It must be included on the page:
|
||||
|
||||
<script src="backbone-forms/distribution/adapters/backbone.bootstrap-modal.min.js" />
|
||||
|
||||
*This list replaces the old jQueryUI list, but may need some upgrade work. The old jQueryUI List editor is still included in a separate file.*
|
||||
|
||||
###Attributes
|
||||
|
||||
- **`itemType`**
|
||||
|
||||
Defines the editor that will be used for each item in the list. Similar in use to the main 'type' schema attribute. Defaults to 'Text'.
|
||||
|
||||
- **`confirmDelete`**
|
||||
|
||||
Optional. Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
|
||||
|
||||
- **`itemToString`**
|
||||
|
||||
A function that returns a string representing how the object should be displayed in a list item.
|
||||
|
||||
Optional, but recommended when using listType 'Object'. When listType is 'NestedModel', the model's `toString()` method will be used, unless a specific `itemToString()` function is defined on the schema.
|
||||
|
||||
- **`listTemplate`**
|
||||
|
||||
Name of the template to hold the list. Edit if you want to customize the 'Add' button, for instance. Optional, defaults to 'list'.
|
||||
|
||||
|
||||
- **`listItemTemplate`**
|
||||
|
||||
Name of the template to hold the list item (including the remove item button).
|
||||
|
||||
Optional, defaults to 'listItem'
|
||||
|
||||
|
||||
###Events
|
||||
|
||||
- **`add`**
|
||||
|
||||
This event is triggered when a new item is added to the list.
|
||||
|
||||
- **`remove`**
|
||||
|
||||
This event is triggered when an existing item is removed from the list.
|
||||
|
||||
- **`item:<event>`**
|
||||
|
||||
Events fired by any item's editor will bubble up and be fired as `item:<event>`.
|
||||
|
||||
####Examples
|
||||
|
||||
function userToName(user) {
|
||||
return user.firstName + ' ' + user.lastName;
|
||||
}
|
||||
|
||||
var schema = {
|
||||
users: { type: 'List', itemType: 'Object', itemToString: userToName }
|
||||
};
|
||||
|
||||
listEditor.on('add', function(listEditor, itemEditor) {
|
||||
console.log('User with first name "' + itemEditor.getValue().firstName + '" added.');
|
||||
});
|
||||
|
||||
listEditor.on('item:focus', function(listEditor, itemEditor) {
|
||||
console.log('User "' + userToName(itemEditor.getValue()) + '" has been given focus.');
|
||||
});
|
||||
|
||||
listEditor.on('item:lastName:change', function(listEditor, itemEditor, lastNameEditor) {
|
||||
console.log('Last name for user "' + itemEditor.getValue().firstName + '" changed to "' + lastNameEditor.getValue() +'".');
|
||||
});
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
|
||||
<a name="validation"/>
|
||||
##Validation
|
||||
|
||||
There are 2 levels of validation: schema validators and the regular built-in Backbone model validation. Backbone Forms will run both when either `form.commit()` or `form.validate()` are called.
|
||||
|
||||
|
||||
###Schema validation
|
||||
|
||||
Validators can be defined in several ways:
|
||||
|
||||
- **As a string** - Shorthand for adding a built-in validator. You can add custom validators to this list by adding them to `Backbone.Form.validators`. See the source for more information.
|
||||
- **As an object** - For adding a built-in validator with options, e.g. overriding the default error message.
|
||||
- **As a function** - Runs a custom validation function. Each validator the following arguments: `value` and `formValues`
|
||||
- **As a regular expression** - Runs the built-in `regexp` validator with a custom regular expresssion.
|
||||
|
||||
###Built-in validators
|
||||
|
||||
- **required**: Checks the field has been filled in
|
||||
- **email**: Checks it is a valid email address
|
||||
- **url**: Checks it is a valid URL
|
||||
- **match**: Checks that the field matches another. The other field name must be set in the `field` option.
|
||||
- **regexp**: Runs a regular expression. Requires the `regexp` option, which takes a compiled regular expression.
|
||||
|
||||
####Examples
|
||||
|
||||
var schema = {
|
||||
//Built-in validator
|
||||
name: { validators: ['required'] },
|
||||
|
||||
//Multiple built-in validators
|
||||
email: { validators: ['required', 'email'] },
|
||||
|
||||
//Built-in editors with options:
|
||||
password: { validators: [
|
||||
{ type: 'match', field: 'passwordConfirm', message: 'Passwords must match!' }
|
||||
] },
|
||||
|
||||
//Regular expression
|
||||
foo: { validators: [/foo/] },
|
||||
|
||||
//Custom function
|
||||
username: { validators: [
|
||||
function checkUsername(value, formValues) {
|
||||
var err = {
|
||||
type: 'username',
|
||||
message: 'Usernames must be at least 3 characters long'
|
||||
};
|
||||
|
||||
if (value.length < 3) return err;
|
||||
}
|
||||
] }
|
||||
}
|
||||
|
||||
|
||||
###Handling errors
|
||||
|
||||
Error messages will be added to the field's help text area, and a customisable `bbf-error` class will be added to the field element so it can be styled with CSS.
|
||||
|
||||
Validation runs when `form.commit()` or `form.validate()` are called. If validation fails, an error object is returned with the `type` (validator that failed) and customisable `message`:
|
||||
|
||||
//Example returned errors from form validation:
|
||||
{
|
||||
name: { type: 'required', message: 'Required' }, //Error on the name field
|
||||
email: { type: 'email', message: 'Invalid email address' }, //Error on the email field
|
||||
_others: ['Custom model.validate() error'] //Error from model.validate()
|
||||
}
|
||||
|
||||
|
||||
###Customising error messages
|
||||
|
||||
After including the Backbone Forms file, you can override the default error messages.
|
||||
|
||||
{{mustache}} tags are supported; they will be replaced with the options passed into the validator configuration object. `{{value}}` is a special tag which is passed the current field value.
|
||||
|
||||
Backbone.Form.validators.errMessages.required = 'Please enter a value for this field.';
|
||||
|
||||
Backbone.Form.validators.errMessages.match = 'This value must match the value of {{field}}';
|
||||
|
||||
Backbone.Form.validators.errMessages.email = '{{value}} is an invalid email address.';
|
||||
|
||||
You can also override the error message on a field by field basis by passing the `message` option in the validator config.
|
||||
|
||||
|
||||
###Model validation
|
||||
|
||||
If your models have a `validate()` method the errors will be added to the error object. To make the most of the validation system, the method should return an error object, keyed by the field object. If an unrecognised field is added, or just a string is returned, it will be added to the `_others` array of errors:
|
||||
|
||||
var User = Backbone.Model.extend({
|
||||
validate: function(attrs) {
|
||||
var errs = {};
|
||||
|
||||
if (usernameTaken(attrs.username)) errs.username = 'The username is taken'
|
||||
|
||||
if !_.isEmpty(errs) return errs;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
###Schema validators
|
||||
Forms provide a `validate` method, which returns a dictionary of errors, or `null`. Validation is determined using the `validators` attribute on the schema (see above).
|
||||
|
||||
If you model provides a `validate` method, then this will be called when you call `Form.validate`. Forms are also validated when you call `commit`. See the Backbone documentation for more details on model validation.
|
||||
|
||||
####Example
|
||||
|
||||
//Schema definition:
|
||||
var schema = {
|
||||
name: { validators: ['required']
|
||||
}
|
||||
|
||||
var errors = form.commit();
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
|
||||
<a name="customising-templates"/>
|
||||
##Customising templates
|
||||
|
||||
Backbone-Forms comes with a few options for rendering HTML. To use another template pack, such as for [Bootstrap](http://twitter.github.com/bootstrap/), just include the .js file from the `templates` folder, after including `backbone-forms.js`.
|
||||
|
||||
You can use your own custom templates by passing your templates (in Mustache syntax) and class names into `Backbone.Form.setTemplates()`. See the included templates files for examples.
|
||||
|
||||
You can include different field templates and then use them on a field-by-field basis by passing the `template` option in the field schema.
|
||||
|
||||
####Example
|
||||
|
||||
var templates = {
|
||||
//field is the default template used
|
||||
field: '\
|
||||
<div>\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div>{{editor}}</div> <div>{{help}}</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
//Specify an alternate field template
|
||||
altField: '<div class="altField">{{editor}}</div>'
|
||||
};
|
||||
|
||||
//Set the templates
|
||||
Backbone.Form.setTemplates(templates, classNames);
|
||||
|
||||
var schema = {
|
||||
age: { type: 'Number' }, //Uses the default 'field' template
|
||||
name: { template: 'altField' } //Uses the 'altField' template
|
||||
};
|
||||
|
||||
|
||||
####Using custom variables in templates
|
||||
You can pass your own template data into templates by overriding the Form.Field.renderingContext method with your own locals.
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
|
||||
<a name="changing-template-compiler"/>
|
||||
###Changing template compiler
|
||||
|
||||
You can use your own custom template compiler, like [Handlebars](http://handlebarsjs.com/) by passing a reference to the function into `Backbone.Form.setTemplateCompiler()`.
|
||||
|
||||
####Example
|
||||
|
||||
Backbone.Form.setTemplateCompiler(Handlebars.compile);
|
||||
|
||||
[Back to top](#top)
|
||||
|
||||
|
||||
|
||||
<a name="more"/>
|
||||
##More
|
||||
|
||||
<a name="editors-without-forms"/>
|
||||
###Editors without forms
|
||||
|
||||
You can add editors by themselves, without being part of a form. For example:
|
||||
|
||||
var select = new Backbone.Form.editors.Select({
|
||||
model: user,
|
||||
key: 'country',
|
||||
options: getCountries()
|
||||
}).render();
|
||||
|
||||
//When done, apply selection to model:
|
||||
select.commit();
|
||||
|
||||
|
||||
<a name="nested-fields"/>
|
||||
###Using nested fields
|
||||
|
||||
If you are using a schema with nested attributes (using the `Object` type), you may want to include only some of the nested fields in a form. This can be accomplished by using 'path' syntax as in the example below.
|
||||
|
||||
However, due to Backbone's lack of support for nested model attributes, getting and setting values will not work out of the box. For this to work as expected you must adapt your model's get() and set() methods to handle the path names, or simply use [DeepModel](http://github.com/powmedia/backbone-deep-model) which will handle paths for you automatically.
|
||||
|
||||
var Model = Backbone.DeepModel.extend({
|
||||
schema: {
|
||||
title: 'Text',
|
||||
author: { type: 'Object', subSchema: {
|
||||
id: 'Number',
|
||||
name: { type: 'Object', subSchema: {
|
||||
first: 'Text',
|
||||
last: 'Text'
|
||||
}}
|
||||
}}
|
||||
}
|
||||
});
|
||||
|
||||
var form = new Backbone.Form({
|
||||
model: new Model,
|
||||
fields: ['title', 'author.id', 'author.name.last']
|
||||
}).render();
|
||||
|
||||
The following shorthand is also valid:
|
||||
|
||||
var Model = Backbone.DeepModel.extend({
|
||||
schema: {
|
||||
title: 'Text',
|
||||
'author.id': 'Number',
|
||||
'author.name.first': 'Text'
|
||||
}
|
||||
});
|
||||
|
||||
var form = new Backbone.Form({
|
||||
model: new Model
|
||||
})
|
||||
|
||||
|
||||
<a name="custom-editors"/>
|
||||
###Custom editors
|
||||
|
||||
Writing a custom editor is simple. They must extend from Backbone.Form.editors.Base.
|
||||
|
||||
var CustomEditor = Backbone.Form.editors.Base.extend({
|
||||
|
||||
tagName: 'input',
|
||||
|
||||
events: {
|
||||
'change': function() {
|
||||
// The 'change' event should be triggered whenever something happens
|
||||
// that affects the result of `this.getValue()`.
|
||||
this.trigger('change', this);
|
||||
},
|
||||
'focus': function() {
|
||||
// The 'focus' event should be triggered whenever an input within
|
||||
// this editor becomes the `document.activeElement`.
|
||||
this.trigger('focus', this);
|
||||
// This call automatically sets `this.hasFocus` to `true`.
|
||||
},
|
||||
'blur': function() {
|
||||
// The 'blur' event should be triggered whenever an input within
|
||||
// this editor stops being the `document.activeElement`.
|
||||
this.trigger('blur', this);
|
||||
// This call automatically sets `this.hasFocus` to `false`.
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
// Call parent constructor
|
||||
Backbone.Form.editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
// Custom setup code.
|
||||
if (this.schema.customParam) this.doSomething();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.setValue(this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.$el.val();
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.$el.val(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
// This method call should result in an input within this edior
|
||||
// becoming the `document.activeElement`.
|
||||
// This, in turn, should result in this editor's `focus` event
|
||||
// being triggered, setting `this.hasFocus` to `true`.
|
||||
// See above for more detail.
|
||||
this.$el.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
this.$el.blur();
|
||||
}
|
||||
});
|
||||
|
||||
**Notes:**
|
||||
|
||||
- The editor must implement `getValue()`, `setValue()`, `focus()` and `blur()` methods.
|
||||
- The editor must fire `change`, `focus` and `blur` events.
|
||||
- The original value is available through `this.value`.
|
||||
- The field schema can be accessed via `this.schema`. This allows you to pass in custom parameters.
|
||||
|
||||
|
||||
|
||||
<a name="help"/>
|
||||
##Help & discussion
|
||||
|
||||
- [Google Groups](http://groups.google.com/group/backbone-forms)
|
||||
|
||||
|
||||
<a name="changelog"/>
|
||||
##Changelog
|
||||
|
||||
###master
|
||||
- Don't show <label> if schema title===false (philfreo)
|
||||
- Fix change event on radio editor (DominicBoettger)
|
||||
- Fix model errors not being return by validate() (mutewinter)
|
||||
- Setting value with setValue only from form.schema (okhomenko)
|
||||
- Some smaller optimisation and fixes according to jsHint (MarcelloDiSimone)
|
||||
- Add Form.setValue(key, val) option for arguments (lennym)
|
||||
- Support ordering years in descending order in Date field (lennym)
|
||||
- Allow the Number field type to accept decimal values (philfreo)
|
||||
- Added 'backbone-forms' as a dependency to the AMD wrapper for templates. (seanparmelee)
|
||||
- Allow use of required validator with checkbox fields (lennym)
|
||||
- Make Form.Field template rendering context overrideable (drd)
|
||||
- Fix a mismatched button element in the bootstrap.js template file. (there4)
|
||||
- Fix AMD editors that must have backbone forms (philfreo)
|
||||
- Skip undefined properties when setting form value from model.toJSON() (jgarbers)
|
||||
- Add listItemTemplate option to list editors (philfreo)
|
||||
- Fix NestedModel values being overridden by defaults (#99)
|
||||
- Add Select.setOptions() method to change options on demand
|
||||
- AMD improvements (see issue #77)
|
||||
- Add 'change', 'focus' and 'blur' events (DouweM)
|
||||
- Fix: #72 Hitting 'Enter' being focused on any text field in examples deletes nested "notes"
|
||||
- Pressing enter in a list now adds a new item to the bottom of the list (Juice10)
|
||||
- Customization of List Template & Tweaked default templates (philfreo)
|
||||
- Fix not rendering of hidden fields (#75) (DouweM)
|
||||
- DateTime editor:
|
||||
- Convert strings to dates
|
||||
- Remove built-in Date editor before removing self
|
||||
- Email validator should accept "+" sign (#70)
|
||||
|
||||
|
||||
###0.10.0
|
||||
- update build scripts & package.json for jam packaging (dstendardi)
|
||||
- Fields with options, such as Select, should handle 0 as value (torbjorntorbjorn)
|
||||
- Update backbone.bootstrap-modal adapter
|
||||
- Refactor rendering.
|
||||
- <legend> tags are now defined in the template.
|
||||
- Where a template is used, (e.g. advanced editors, field etc.), the entirety of the HTML is now defined in the template to make custom templating easier.
|
||||
- All templates must now have a main 'parent' element.
|
||||
- Create new List, Date and DateTime editors that don't rely on jQuery UI.
|
||||
- You will still need to use jQuery UI editors for the calendar.
|
||||
- For list items of type `Object` and `NestedModel` you must include a modal adapter, such as the included Bootstrap Modal one. Should create one for jQuery UI.
|
||||
- Improve the way dependencies are defined and module is exported for browser & CommonJS
|
||||
- Add underscore dependency to AMD version
|
||||
- Use [buildify](http://github.com/powmedia/buildify) for building distribution files.
|
||||
- Rename jQuery UI editors to jqueryui.List, jqueryui.Date, jqueryui.DateTime. These may be moved to a separate repository soon.
|
||||
- Fix #65 Number editor Firefox NaN bug
|
||||
- Fix bug with hidden fields (jeffutter)
|
||||
- Fix AMD distribution bug (ikr)
|
||||
|
||||
####Required changes when upgrading:
|
||||
- List editor:
|
||||
- Change 'listType' to 'itemType' in schema definition.
|
||||
- Make sure you have a modal adapter included if using Object and NestedModel itemTypes. See the List editor section.
|
||||
|
||||
###0.9.0
|
||||
- Added ability to use a custom template compiler (geowa4)
|
||||
- Added distribution files (development and minified production versions)
|
||||
- Added AMD-compatible version (development and minified production versions)
|
File diff suppressed because it is too large
Load diff
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/backbone-forms.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/backbone-forms.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
65
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.css
vendored
Normal file
65
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.css
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
.bbf-jui-date input, .bbf-jui-datetime input {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime input {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime select {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bbf-jui-list {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-jui-list ul {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-jui-list li {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
margin-top: -1px;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.bbf-jui-list .bbf-list-sortable li {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions button {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-add {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-editor {
|
||||
width: 98%;
|
||||
}
|
505
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.js
vendored
Normal file
505
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.js
vendored
Normal file
|
@ -0,0 +1,505 @@
|
|||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
|
||||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
Base = Form.editors.Base,
|
||||
createTemplate = Form.helpers.createTemplate,
|
||||
triggerCancellableEvent = Form.helpers.triggerCancellableEvent,
|
||||
exports = {};
|
||||
|
||||
/**
|
||||
* Additional editors that depend on jQuery UI
|
||||
*/
|
||||
|
||||
//DATE
|
||||
exports['jqueryui.Date'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-date',
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
//Cast to Date
|
||||
if (this.value && !_.isDate(this.value)) {
|
||||
this.value = new Date(this.value);
|
||||
}
|
||||
|
||||
//Set default date
|
||||
if (!this.value) {
|
||||
var date = new Date();
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
this.value = date;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
$el.html('<input>');
|
||||
|
||||
var input = $('input', $el);
|
||||
|
||||
input.datepicker({
|
||||
dateFormat: 'dd/mm/yy',
|
||||
showButtonPanel: true
|
||||
});
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Make sure setValue of this object is called, not of any objects extending it (e.g. DateTime)
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected date
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
$('input', this.el).datepicker('setDate', value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('show');
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('hide');
|
||||
},
|
||||
|
||||
_observeDatepickerEvents: function() {
|
||||
var self = this;
|
||||
this.$('input').datepicker('option', 'onSelect', function() {
|
||||
self.trigger('change', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'onClose', function() {
|
||||
if (!self.hasFocus) return;
|
||||
self.trigger('blur', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'beforeShow', function() {
|
||||
if (self.hasFocus) return {};
|
||||
self.trigger('focus', self);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//DATETIME
|
||||
exports['jqueryui.DateTime'] = exports['jqueryui.Date'].extend({
|
||||
|
||||
className: 'bbf-jui-datetime',
|
||||
|
||||
template: createTemplate('<select>{{hours}}</select> : <select>{{mins}}</select>'),
|
||||
|
||||
render: function() {
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
//Render the date element first
|
||||
exports['jqueryui.Date'].prototype.render.call(this);
|
||||
|
||||
//Setup hour options
|
||||
var hours = _.range(0, 24),
|
||||
hoursOptions = [];
|
||||
|
||||
_.each(hours, function(hour) {
|
||||
hoursOptions.push('<option value="'+hour+'">' + pad(hour) + '</option>');
|
||||
});
|
||||
|
||||
//Setup minute options
|
||||
var minsInterval = this.schema.minsInterval || 15,
|
||||
mins = _.range(0, 60, minsInterval),
|
||||
minsOptions = [];
|
||||
|
||||
_.each(mins, function(min) {
|
||||
minsOptions.push('<option value="'+min+'">' + pad(min) + '</option>');
|
||||
});
|
||||
|
||||
//Render time selects
|
||||
this.$el.append(this.template({
|
||||
hours: hoursOptions.join(),
|
||||
mins: minsOptions.join()
|
||||
}));
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Store references to selects
|
||||
this.$hours = $('select:eq(0)', this.el);
|
||||
this.$mins = $('select:eq(1)', this.el);
|
||||
|
||||
//Set time
|
||||
this.setValue(this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected datetime
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
date.setHours(this.$hours.val());
|
||||
date.setMinutes(this.$mins.val());
|
||||
date.setMilliseconds(0);
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(date) {
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, date);
|
||||
|
||||
this.$hours.val(date.getHours());
|
||||
this.$mins.val(date.getMinutes());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//LIST
|
||||
exports['jqueryui.List'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-list',
|
||||
|
||||
//Note: The extra div around the <ul> is used to limit the drag area
|
||||
template: createTemplate('\
|
||||
<ul></ul>\
|
||||
<div><button class="bbf-list-add">Add</div>\
|
||||
'),
|
||||
|
||||
itemTemplate: createTemplate('\
|
||||
<li rel="{{id}}">\
|
||||
<span class="bbf-list-text">{{text}}</span>\
|
||||
<div class="bbf-list-actions">\
|
||||
<button class="bbf-list-edit">Edit</button>\
|
||||
<button class="bbf-list-del">Delete</button>\
|
||||
</div>\
|
||||
</li>\
|
||||
'),
|
||||
|
||||
editorTemplate: createTemplate('\
|
||||
<div class="bbf-field">\
|
||||
<div class="bbf-list-editor"></div>\
|
||||
</div>\
|
||||
'),
|
||||
|
||||
events: {
|
||||
'click .bbf-list-add': 'addNewItem',
|
||||
'click .bbf-list-edit': 'editItem',
|
||||
'click .bbf-list-del': 'deleteItem'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
if (!this.schema) throw "Missing required option 'schema'";
|
||||
|
||||
this.schema.listType = this.schema.listType || 'Text';
|
||||
|
||||
if (this.schema.listType === 'NestedModel' && !this.schema.model)
|
||||
throw "Missing required option 'schema.model'";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
//Main element
|
||||
$el.html(this.template());
|
||||
|
||||
//Create list
|
||||
var self = this,
|
||||
data = this.value || [],
|
||||
schema = this.schema,
|
||||
itemToString = this.itemToString,
|
||||
itemTemplate = this.itemTemplate,
|
||||
listEl = $('ul', $el);
|
||||
|
||||
_.each(data, function(itemData) {
|
||||
var text = itemToString.call(self, itemData);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(itemTemplate({
|
||||
id: itemData.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Attach data
|
||||
$.data(li[0], 'data', itemData);
|
||||
|
||||
listEl.append(li);
|
||||
});
|
||||
|
||||
//Make sortable
|
||||
if (schema.sortable !== false) {
|
||||
listEl.sortable({
|
||||
axis: 'y',
|
||||
cursor: 'move',
|
||||
containment: 'parent'
|
||||
});
|
||||
|
||||
$el.addClass('bbf-list-sortable');
|
||||
}
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-add', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-plus' }
|
||||
});
|
||||
$('button.bbf-list-edit', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats an item for display in the list
|
||||
* For example objects, dates etc. can have a custom
|
||||
* itemToString method which says how it should be formatted.
|
||||
*/
|
||||
itemToString: function(data) {
|
||||
if (!data) return data;
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(data);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (this.schema.listType === 'NestedModel') {
|
||||
var model = new (this.schema.model)(data);
|
||||
|
||||
return model.toString();
|
||||
}
|
||||
|
||||
//Last resort, just return the data as is
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list if it is completed in the editor
|
||||
*/
|
||||
addNewItem: function(event) {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
this.openEditor(null, function(value, editor) {
|
||||
//Fire 'addItem' cancellable event
|
||||
triggerCancellableEvent(self, 'addItem', [value, editor], function() {
|
||||
var text = self.itemToString(value);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(self.itemTemplate({
|
||||
id: value.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', value);
|
||||
|
||||
$('ul', self.el).append(li);
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-edit', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
self.trigger('add', self, value);
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit an existing item in the list
|
||||
*/
|
||||
editItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
originalValue = $.data(li[0], 'data');
|
||||
|
||||
this.openEditor(originalValue, function(newValue, editor) {
|
||||
//Fire 'editItem' cancellable event
|
||||
triggerCancellableEvent(self, 'editItem', [newValue, editor], function() {
|
||||
//Update display
|
||||
$('.bbf-list-text', li).html(self.itemToString(newValue));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', newValue);
|
||||
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
data = $.data(li[0], 'data');
|
||||
|
||||
var confirmDelete = (this.schema.confirmDelete) ? this.schema.confirmDelete : false,
|
||||
confirmMsg = this.schema.confirmDeleteMsg || 'Are you sure?';
|
||||
|
||||
function remove() {
|
||||
triggerCancellableEvent(self, 'removeItem', [data], function() {
|
||||
li.remove();
|
||||
|
||||
self.trigger('remove', self, data);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.schema.confirmDelete) {
|
||||
if (confirm(confirmMsg)) remove();
|
||||
} else {
|
||||
remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the sub editor dialog
|
||||
* @param {Mixed} Data (if editing existing list item, null otherwise)
|
||||
* @param {Function} Save callback. receives: value
|
||||
*/
|
||||
openEditor: function(data, callback) {
|
||||
var self = this,
|
||||
schema = this.schema,
|
||||
listType = schema.listType || 'Text';
|
||||
|
||||
var editor = Form.helpers.createEditor(listType, {
|
||||
key: '',
|
||||
schema: schema,
|
||||
value: data
|
||||
}).render();
|
||||
|
||||
var container = this.editorContainer = $(this.editorTemplate());
|
||||
$('.bbf-list-editor', container).html(editor.el);
|
||||
|
||||
var saveAndClose = function() {
|
||||
var errs = editor.validate();
|
||||
if (errs) return;
|
||||
|
||||
callback(editor.getValue(), editor);
|
||||
container.dialog('close');
|
||||
};
|
||||
|
||||
var handleEnterPressed = function(event) {
|
||||
if (event.keyCode !== 13) return;
|
||||
|
||||
saveAndClose();
|
||||
};
|
||||
|
||||
$(container).dialog({
|
||||
resizable: false,
|
||||
modal: true,
|
||||
width: 500,
|
||||
title: data ? 'Edit item' : 'New item',
|
||||
buttons: {
|
||||
'OK': saveAndClose,
|
||||
'Cancel': function() {
|
||||
container.dialog('close');
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
self.editorContainer = null;
|
||||
|
||||
$(document).unbind('keydown', handleEnterPressed);
|
||||
|
||||
editor.remove();
|
||||
container.remove();
|
||||
|
||||
self.trigger('item:close', self, editor);
|
||||
self.trigger('item:blur', self, editor);
|
||||
self.trigger('blur', self);
|
||||
}
|
||||
});
|
||||
|
||||
this.trigger('item:open', this, editor);
|
||||
this.trigger('item:focus', this, editor);
|
||||
this.trigger('focus', this);
|
||||
|
||||
//Save and close dialog on Enter keypress
|
||||
$(document).bind('keydown', handleEnterPressed);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var data = [];
|
||||
|
||||
$('li', this.el).each(function(index, li) {
|
||||
data.push($.data(li, 'data'));
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
var item = this.$('li .bbf-list-edit').first();
|
||||
if (item.length > 0) {
|
||||
item.click();
|
||||
}
|
||||
else {
|
||||
this.addNewItem();
|
||||
}
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.editorContainer) this.editorContainer.dialog('close');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//Exports
|
||||
_.extend(Form.editors, exports);
|
||||
|
||||
})();
|
||||
|
||||
|
||||
});
|
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,584 @@
|
|||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
|
||||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
editors = Form.editors;
|
||||
|
||||
/**
|
||||
* LIST
|
||||
*
|
||||
* An array editor. Creates a list of other editor items.
|
||||
*
|
||||
* Special options:
|
||||
* @param {String} [options.schema.itemType] The editor type for each item in the list. Default: 'Text'
|
||||
* @param {String} [options.schema.confirmDelete] Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
|
||||
*/
|
||||
editors.List = editors.Base.extend({
|
||||
|
||||
events: {
|
||||
'click [data-action="add"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.addItem(null, true);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
if (!schema) throw "Missing required option 'schema'";
|
||||
|
||||
//List schema defaults
|
||||
this.schema = _.extend({
|
||||
listTemplate: 'list',
|
||||
listItemTemplate: 'listItem'
|
||||
}, schema);
|
||||
|
||||
//Determine the editor to use
|
||||
this.Editor = (function() {
|
||||
var type = schema.itemType;
|
||||
|
||||
//Default to Text
|
||||
if (!type) return editors.Text;
|
||||
|
||||
//Use List-specific version if available
|
||||
if (editors.List[type]) return editors.List[type];
|
||||
|
||||
//Or whichever was passed
|
||||
return editors[type];
|
||||
})();
|
||||
|
||||
this.items = [];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this,
|
||||
value = this.value || [];
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listTemplate]({
|
||||
items: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
//Store a reference to the list (item container)
|
||||
this.$list = $el.find('.bbf-tmp').parent().empty();
|
||||
|
||||
//Add existing items
|
||||
if (value.length) {
|
||||
_.each(value, function(itemValue) {
|
||||
self.addItem(itemValue);
|
||||
});
|
||||
}
|
||||
|
||||
//If no existing items create an empty one, unless the editor specifies otherwise
|
||||
else {
|
||||
if (!this.Editor.isAsync) this.addItem();
|
||||
}
|
||||
|
||||
this.setElement($el);
|
||||
this.$el.attr('id', this.id);
|
||||
this.$el.attr('name', this.key);
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list
|
||||
* @param {Mixed} [value] Value for the new item editor
|
||||
* @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
|
||||
*/
|
||||
addItem: function(value, userInitiated) {
|
||||
var self = this;
|
||||
|
||||
//Create the item
|
||||
var item = new editors.List.Item({
|
||||
list: this,
|
||||
schema: this.schema,
|
||||
value: value,
|
||||
Editor: this.Editor,
|
||||
key: this.key
|
||||
}).render();
|
||||
|
||||
var _addItem = function() {
|
||||
self.items.push(item);
|
||||
self.$list.append(item.el);
|
||||
|
||||
item.editor.on('all', function(event) {
|
||||
if (event === 'change') return;
|
||||
|
||||
// args = ["key:change", itemEditor, fieldEditor]
|
||||
var args = _.toArray(arguments);
|
||||
args[0] = 'item:' + event;
|
||||
args.splice(1, 0, self);
|
||||
// args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]
|
||||
|
||||
editors.List.prototype.trigger.apply(this, args);
|
||||
}, self);
|
||||
|
||||
item.editor.on('change', function() {
|
||||
if (!item.addEventTriggered) {
|
||||
item.addEventTriggered = true;
|
||||
this.trigger('add', this, item.editor);
|
||||
}
|
||||
this.trigger('item:change', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}, self);
|
||||
|
||||
item.editor.on('focus', function() {
|
||||
if (this.hasFocus) return;
|
||||
this.trigger('focus', this);
|
||||
}, self);
|
||||
item.editor.on('blur', function() {
|
||||
if (!this.hasFocus) return;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
|
||||
self.trigger('blur', self);
|
||||
}, 0);
|
||||
}, self);
|
||||
|
||||
if (userInitiated || value) {
|
||||
item.addEventTriggered = true;
|
||||
}
|
||||
|
||||
if (userInitiated) {
|
||||
self.trigger('add', self, item.editor);
|
||||
self.trigger('change', self);
|
||||
}
|
||||
};
|
||||
|
||||
//Check if we need to wait for the item to complete before adding to the list
|
||||
if (this.Editor.isAsync) {
|
||||
item.editor.on('readyToAdd', _addItem, this);
|
||||
}
|
||||
|
||||
//Most editors can be added automatically
|
||||
else {
|
||||
_addItem();
|
||||
}
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an item from the list
|
||||
* @param {List.Item} item
|
||||
*/
|
||||
removeItem: function(item) {
|
||||
//Confirm delete
|
||||
var confirmMsg = this.schema.confirmDelete;
|
||||
if (confirmMsg && !confirm(confirmMsg)) return;
|
||||
|
||||
var index = _.indexOf(this.items, item);
|
||||
|
||||
this.items[index].remove();
|
||||
this.items.splice(index, 1);
|
||||
|
||||
if (item.addEventTriggered) {
|
||||
this.trigger('remove', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}
|
||||
|
||||
if (!this.items.length && !this.Editor.isAsync) this.addItem();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var values = _.map(this.items, function(item) {
|
||||
return item.getValue();
|
||||
});
|
||||
|
||||
//Filter empty items
|
||||
return _.without(values, undefined, '');
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
if (this.items[0]) this.items[0].editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
var focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });
|
||||
|
||||
if (focusedItem) focusedItem.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Override default remove function in order to remove item views
|
||||
*/
|
||||
remove: function() {
|
||||
_.invoke(this.items, 'remove');
|
||||
|
||||
editors.Base.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Run validation
|
||||
*
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
validate: function() {
|
||||
if (!this.validators) return null;
|
||||
|
||||
//Collect errors
|
||||
var errors = _.map(this.items, function(item) {
|
||||
return item.validate();
|
||||
});
|
||||
|
||||
//Check if any item has errors
|
||||
var hasErrors = _.compact(errors).length ? true : false;
|
||||
if (!hasErrors) return null;
|
||||
|
||||
//If so create a shared error
|
||||
var fieldError = {
|
||||
type: 'list',
|
||||
message: 'Some of the items in the list failed validation',
|
||||
errors: errors
|
||||
};
|
||||
|
||||
return fieldError;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A single item in the list
|
||||
*
|
||||
* @param {editors.List} options.list The List editor instance this item belongs to
|
||||
* @param {Function} options.Editor Editor constructor function
|
||||
* @param {String} options.key Model key
|
||||
* @param {Mixed} options.value Value
|
||||
* @param {Object} options.schema Field schema
|
||||
*/
|
||||
editors.List.Item = Backbone.View.extend({
|
||||
events: {
|
||||
'click [data-action="remove"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.list.removeItem(this);
|
||||
},
|
||||
'keydown input[type=text]': function(event) {
|
||||
if(event.keyCode !== 13) return;
|
||||
event.preventDefault();
|
||||
this.list.addItem();
|
||||
this.list.$list.find("> li:last input").focus();
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.list = options.list;
|
||||
this.schema = options.schema || this.list.schema;
|
||||
this.value = options.value;
|
||||
this.Editor = options.Editor || editors.Text;
|
||||
this.key = options.key;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
//Create editor
|
||||
this.editor = new this.Editor({
|
||||
key: this.key,
|
||||
schema: this.schema,
|
||||
value: this.value,
|
||||
list: this.list,
|
||||
item: this
|
||||
}).render();
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listItemTemplate]({
|
||||
editor: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
$el.find('.bbf-tmp').replaceWith(this.editor.el);
|
||||
|
||||
//Replace the entire element so there isn't a wrapper tag
|
||||
this.setElement($el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
this.editor.blur();
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.editor.remove();
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
var value = this.getValue(),
|
||||
formValues = this.list.form ? this.list.form.getValue() : {},
|
||||
validators = this.schema.validators,
|
||||
getValidator = Form.helpers.getValidator;
|
||||
|
||||
if (!validators) return null;
|
||||
|
||||
//Run through validators until an error is found
|
||||
var error = null;
|
||||
_.every(validators, function(validator) {
|
||||
error = getValidator(validator)(value, formValues);
|
||||
|
||||
return error ? false : true;
|
||||
});
|
||||
|
||||
//Show/hide error
|
||||
if (error){
|
||||
this.setError(error);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
//Return error to be aggregated by list
|
||||
return error ? error : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a validation error
|
||||
*/
|
||||
setError: function(err) {
|
||||
this.$el.addClass(Form.classNames.error);
|
||||
this.$el.attr('title', err.message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide validation errors
|
||||
*/
|
||||
clearError: function() {
|
||||
this.$el.removeClass(Form.classNames.error);
|
||||
this.$el.attr('title', null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Modal object editor for use with the List editor.
|
||||
* To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
|
||||
*/
|
||||
editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
|
||||
events: {
|
||||
'click': 'openEditor'
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
|
||||
* @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
|
||||
* @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
|
||||
* @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
|
||||
*/
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//Dependencies
|
||||
if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
|
||||
|
||||
//Get nested schema if Object
|
||||
if (schema.itemType === 'Object') {
|
||||
if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
|
||||
|
||||
this.nestedSchema = schema.subSchema;
|
||||
}
|
||||
|
||||
//Get nested schema if NestedModel
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
if (!schema.model) throw 'Missing required option "schema.model"';
|
||||
|
||||
this.nestedSchema = schema.model.prototype.schema;
|
||||
if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the list item representation
|
||||
*/
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
//New items in the list are only rendered when the editor has been OK'd
|
||||
if (_.isEmpty(this.value)) {
|
||||
this.openEditor();
|
||||
}
|
||||
|
||||
//But items with values are added automatically
|
||||
else {
|
||||
this.renderSummary();
|
||||
|
||||
setTimeout(function() {
|
||||
self.trigger('readyToAdd');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the list item representation
|
||||
*/
|
||||
renderSummary: function() {
|
||||
var template = Form.templates['list.Modal'];
|
||||
|
||||
this.$el.html(template({
|
||||
summary: this.getStringValue()
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Function which returns a generic string representation of an object
|
||||
*
|
||||
* @param {Object} value
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
itemToString: function(value) {
|
||||
value = value || {};
|
||||
|
||||
//Pretty print the object keys and values
|
||||
var parts = [];
|
||||
_.each(this.nestedSchema, function(schema, key) {
|
||||
var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
|
||||
val = value[key];
|
||||
|
||||
if (_.isUndefined(val) || _.isNull(val)) val = '';
|
||||
|
||||
parts.push(desc + ': ' + val);
|
||||
});
|
||||
|
||||
return parts.join('<br />');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the string representation of the object value
|
||||
*/
|
||||
getStringValue: function() {
|
||||
var schema = this.schema,
|
||||
value = this.getValue();
|
||||
|
||||
if (_.isEmpty(value)) return '[Empty]';
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(value);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
return new (schema.model)(value).toString();
|
||||
}
|
||||
|
||||
//Otherwise use the generic method or custom overridden method
|
||||
return this.itemToString(value);
|
||||
},
|
||||
|
||||
openEditor: function() {
|
||||
var self = this;
|
||||
|
||||
var form = new Form({
|
||||
schema: this.nestedSchema,
|
||||
data: this.value
|
||||
});
|
||||
|
||||
var modal = this.modal = new Backbone.BootstrapModal({
|
||||
content: form,
|
||||
animate: true
|
||||
}).open();
|
||||
|
||||
this.trigger('open', this);
|
||||
this.trigger('focus', this);
|
||||
|
||||
modal.on('cancel', function() {
|
||||
this.modal = null;
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
}, this);
|
||||
|
||||
modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user clicks 'OK'.
|
||||
* Runs validation and tells the list when ready to add the item
|
||||
*/
|
||||
onModalSubmitted: function(form, modal) {
|
||||
var isNew = !this.value;
|
||||
|
||||
//Stop if there are validation errors
|
||||
var error = form.validate();
|
||||
if (error) return modal.preventClose();
|
||||
this.modal = null;
|
||||
|
||||
//If OK, render the list item
|
||||
this.value = form.getValue();
|
||||
|
||||
this.renderSummary();
|
||||
|
||||
if (isNew) this.trigger('readyToAdd');
|
||||
|
||||
this.trigger('change', this);
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.value;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.openEditor();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.modal) {
|
||||
this.modal.trigger('cancel');
|
||||
this.modal.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
//STATICS
|
||||
|
||||
//The modal adapter that creates and manages the modal dialog.
|
||||
//Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
|
||||
//Can be replaced with another adapter that implements the same interface.
|
||||
ModalAdapter: Backbone.BootstrapModal,
|
||||
|
||||
//Make the wait list for the 'ready' event before adding the item to the list
|
||||
isAsync: true
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
});
|
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/list.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/editors/list.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
43
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/templates/bootstrap.css
vendored
Normal file
43
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/templates/bootstrap.css
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list .bbf-add {
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.bbf-list .bbf-del {
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
92
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/templates/bootstrap.js
vendored
Normal file
92
sites/all/modules/civicrm/packages/backbone-forms/distribution.amd/templates/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
|
||||
//TWITTER BOOTSTRAP TEMPLATES
|
||||
//Requires Bootstrap 2.x
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="form-horizontal">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
{{fields}}\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<div class="control-group field-{{key}}">\
|
||||
<label class="control-label" for="{{id}}">{{title}}</label>\
|
||||
<div class="controls">\
|
||||
{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<div class="field-{{key}}">\
|
||||
<div title="{{title}}" class="input-xlarge">{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul class="unstyled clearfix">{{items}}</ul>\
|
||||
<button class="btn bbf-add" data-action="add">Add</button>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li class="clearfix">\
|
||||
<div class="pull-left">{{editor}}</div>\
|
||||
<button type="button" class="btn bbf-del" data-action="remove">×</button>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<p>{{date}}</p>\
|
||||
<p>\
|
||||
<select data-type="hour" style="width: 4em">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min" style="width: 4em">{{mins}}</select>\
|
||||
</p>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'error' //Set on the field tag when validation fails
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
/* Form */
|
||||
.bbf-form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
/* Field */
|
||||
.bbf-field {
|
||||
margin: 1em 0;
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bbf-field label {
|
||||
float: left;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-editor {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
}
|
||||
|
||||
.bbf-field input, .bbf-field textarea, .bbf-field select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-help {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-error {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bbf-field.bbf-error .bbf-editor {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
|
||||
/* Radio */
|
||||
.bbf-radio {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-radio input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-radio label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* Checkboxes */
|
||||
.bbf-checkboxes {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-checkboxes input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-checkboxes label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-editor-container {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-remove {
|
||||
float: right;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-actions {
|
||||
text-align: center;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime .bbf-date-container {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
|
||||
//DEFAULT TEMPLATES
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="bbf-form">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
<ul>{{fields}}</ul>\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<li class="bbf-field field-{{key}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<li class="bbf-field bbf-nested-field field-{{key}}" title="{{title}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul>{{items}}</ul>\
|
||||
<div class="bbf-actions"><button type="button" data-action="add">Add</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li>\
|
||||
<button type="button" data-action="remove" class="bbf-remove">×</button>\
|
||||
<div class="bbf-editor-container">{{editor}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<div class="bbf-date-container">{{date}}</div>\
|
||||
<select data-type="hour">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min">{{mins}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'bbf-error'
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* Bootstrap Modal wrapper for use with Backbone.
|
||||
*
|
||||
* Takes care of instantiation, manages multiple modals,
|
||||
* adds several options and removes the element from the DOM when closed
|
||||
*
|
||||
* @author Charles Davison <charlie@powmedia.co.uk>
|
||||
*
|
||||
* Events:
|
||||
* shown: Fired when the modal has finished animating in
|
||||
* hidden: Fired when the modal has finished animating out
|
||||
* cancel: The user dismissed the modal
|
||||
* ok: The user clicked OK
|
||||
*/
|
||||
(function($, _, Backbone) {
|
||||
|
||||
//Set custom template settings
|
||||
var _interpolateBackup = _.templateSettings;
|
||||
_.templateSettings = {
|
||||
interpolate: /\{\{(.+?)\}\}/g,
|
||||
evaluate: /<%([\s\S]+?)%>/g
|
||||
}
|
||||
|
||||
var template = _.template('\
|
||||
<% if (title) { %>\
|
||||
<div class="modal-header">\
|
||||
<% if (allowCancel) { %>\
|
||||
<a class="close">×</a>\
|
||||
<% } %>\
|
||||
<h3>{{title}}</h3>\
|
||||
</div>\
|
||||
<% } %>\
|
||||
<div class="modal-body">{{content}}</div>\
|
||||
<div class="modal-footer">\
|
||||
<% if (allowCancel) { %>\
|
||||
<% if (cancelText) { %>\
|
||||
<a href="#" class="btn cancel">{{cancelText}}</a>\
|
||||
<% } %>\
|
||||
<% } %>\
|
||||
<a href="#" class="btn ok btn-primary">{{okText}}</a>\
|
||||
</div>\
|
||||
');
|
||||
|
||||
//Reset to users' template settings
|
||||
_.templateSettings = _interpolateBackup;
|
||||
|
||||
|
||||
var Modal = Backbone.View.extend({
|
||||
|
||||
className: 'modal',
|
||||
|
||||
events: {
|
||||
'click .close': function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.trigger('cancel');
|
||||
},
|
||||
'click .cancel': function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.trigger('cancel');
|
||||
},
|
||||
'click .ok': function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.trigger('ok');
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an instance of a Bootstrap Modal
|
||||
*
|
||||
* @see http://twitter.github.com/bootstrap/javascript.html#modals
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {String|View} [options.content] Modal content. Default: none
|
||||
* @param {String} [options.title] Title. Default: none
|
||||
* @param {String} [options.okText] Text for the OK button. Default: 'OK'
|
||||
* @param {String} [options.cancelText] Text for the cancel button. Default: 'Cancel'. If passed a falsey value, the button will be removed
|
||||
* @param {Boolean} [options.allowCancel Whether the modal can be closed, other than by pressing OK. Default: true
|
||||
* @param {Boolean} [options.escape] Whether the 'esc' key can dismiss the modal. Default: true, but false if options.cancellable is true
|
||||
* @param {Boolean} [options.animate] Whether to animate in/out. Default: false
|
||||
* @param {Function} [options.template] Compiled underscore template to override the default one
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.options = _.extend({
|
||||
title: null,
|
||||
okText: 'OK',
|
||||
cancelText: 'Cancel',
|
||||
allowCancel: true,
|
||||
escape: true,
|
||||
animate: false,
|
||||
template: template
|
||||
}, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the DOM element
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
render: function() {
|
||||
var $el = this.$el,
|
||||
options = this.options,
|
||||
content = options.content;
|
||||
|
||||
//Create the modal container
|
||||
$el.html(options.template(options));
|
||||
|
||||
var $content = this.$content = $el.find('.modal-body')
|
||||
|
||||
//Insert the main content if it's a view
|
||||
if (content.$el) {
|
||||
$el.find('.modal-body').html(content.render().$el);
|
||||
}
|
||||
|
||||
if (options.animate) $el.addClass('fade');
|
||||
|
||||
this.isRendered = true;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders and shows the modal
|
||||
*
|
||||
* @param {Function} [cb] Optional callback that runs only when OK is pressed.
|
||||
*/
|
||||
open: function(cb) {
|
||||
if (!this.isRendered) this.render();
|
||||
|
||||
var self = this,
|
||||
$el = this.$el;
|
||||
|
||||
//Create it
|
||||
$el.modal({
|
||||
keyboard: this.options.allowCancel,
|
||||
backdrop: this.options.allowCancel ? true : 'static'
|
||||
});
|
||||
|
||||
//Focus OK button
|
||||
$el.one('shown', function() {
|
||||
$el.find('.btn.ok').focus();
|
||||
|
||||
self.trigger('shown');
|
||||
});
|
||||
|
||||
//Adjust the modal and backdrop z-index; for dealing with multiple modals
|
||||
var numModals = Modal.count,
|
||||
$backdrop = $('.modal-backdrop:eq('+numModals+')'),
|
||||
backdropIndex = $backdrop.css('z-index'),
|
||||
elIndex = $backdrop.css('z-index');
|
||||
|
||||
$backdrop.css('z-index', backdropIndex + numModals);
|
||||
this.$el.css('z-index', elIndex + numModals);
|
||||
|
||||
if (this.options.allowCancel) {
|
||||
$backdrop.one('click', function() {
|
||||
self.trigger('cancel');
|
||||
});
|
||||
|
||||
$(document).one('keyup.dismiss.modal', function (e) {
|
||||
e.which == 27 && self.trigger('cancel');
|
||||
});
|
||||
}
|
||||
|
||||
this.on('cancel', function() {
|
||||
self.close();
|
||||
});
|
||||
|
||||
Modal.count++;
|
||||
|
||||
//Run callback on OK if provided
|
||||
if (cb) {
|
||||
self.on('ok', cb);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes the modal
|
||||
*/
|
||||
close: function() {
|
||||
var self = this,
|
||||
$el = this.$el;
|
||||
|
||||
//Check if the modal should stay open
|
||||
if (this._preventClose) {
|
||||
this._preventClose = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$el.modal('hide');
|
||||
|
||||
$el.one('hidden', function() {
|
||||
self.remove();
|
||||
|
||||
self.trigger('hidden');
|
||||
});
|
||||
|
||||
Modal.count--;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop the modal from closing.
|
||||
* Can be called from within a 'close' or 'ok' event listener.
|
||||
*/
|
||||
preventClose: function() {
|
||||
this._preventClose = true;
|
||||
}
|
||||
}, {
|
||||
//STATICS
|
||||
|
||||
//The number of modals on display
|
||||
count: 0
|
||||
});
|
||||
|
||||
|
||||
//EXPORTS
|
||||
//CommonJS
|
||||
if (typeof require == 'function' && typeof module !== 'undefined' && exports) {
|
||||
module.exports = Modal;
|
||||
}
|
||||
|
||||
//AMD / RequireJS
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
return define(function() {
|
||||
Backbone.BootstrapModal = Modal;
|
||||
})
|
||||
}
|
||||
|
||||
//Regular; add to Backbone.Bootstrap.Modal
|
||||
else {
|
||||
Backbone.BootstrapModal = Modal;
|
||||
}
|
||||
|
||||
})(jQuery, _, Backbone);
|
|
@ -0,0 +1 @@
|
|||
(function(e,t,n){var r=t.templateSettings;t.templateSettings={interpolate:/\{\{(.+?)\}\}/g,evaluate:/<%([\s\S]+?)%>/g};var i=t.template(' <% if (title) { %> <div class="modal-header"> <% if (allowCancel) { %> <a class="close">×</a> <% } %> <h3>{{title}}</h3> </div> <% } %> <div class="modal-body">{{content}}</div> <div class="modal-footer"> <% if (allowCancel) { %> <% if (cancelText) { %> <a href="#" class="btn cancel">{{cancelText}}</a> <% } %> <% } %> <a href="#" class="btn ok btn-primary">{{okText}}</a> </div> ');t.templateSettings=r;var s=n.View.extend({className:"modal",events:{"click .close":function(e){e.preventDefault(),this.trigger("cancel")},"click .cancel":function(e){e.preventDefault(),this.trigger("cancel")},"click .ok":function(e){e.preventDefault(),this.trigger("ok"),this.close()}},initialize:function(e){this.options=t.extend({title:null,okText:"OK",cancelText:"Cancel",allowCancel:!0,escape:!0,animate:!1,template:i},e)},render:function(){var e=this.$el,t=this.options,n=t.content;e.html(t.template(t));var r=this.$content=e.find(".modal-body");return n.$el&&e.find(".modal-body").html(n.render().$el),t.animate&&e.addClass("fade"),this.isRendered=!0,this},open:function(t){this.isRendered||this.render();var n=this,r=this.$el;r.modal({keyboard:this.options.allowCancel,backdrop:this.options.allowCancel?!0:"static"}),r.one("shown",function(){r.find(".btn.ok").focus(),n.trigger("shown")});var i=s.count,o=e(".modal-backdrop:eq("+i+")"),u=o.css("z-index"),a=o.css("z-index");return o.css("z-index",u+i),this.$el.css("z-index",a+i),this.options.allowCancel&&(o.one("click",function(){n.trigger("cancel")}),e(document).one("keyup.dismiss.modal",function(e){e.which==27&&n.trigger("cancel")})),this.on("cancel",function(){n.close()}),s.count++,t&&n.on("ok",t),this},close:function(){var e=this,t=this.$el;if(this._preventClose){this._preventClose=!1;return}t.modal("hide"),t.one("hidden",function(){e.remove(),e.trigger("hidden")}),s.count--},preventClose:function(){this._preventClose=!0}},{count:0});typeof require=="function"&&typeof module!="undefined"&&exports&&(module.exports=s);if(typeof define=="function"&&define.amd)return define(function(){n.BootstrapModal=s});n.BootstrapModal=s})(jQuery,_,Backbone)
|
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/backbone-forms.amd.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/backbone-forms.amd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/backbone-forms.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/backbone-forms.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
65
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.css
vendored
Normal file
65
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.css
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
.bbf-jui-date input, .bbf-jui-datetime input {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime input {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime select {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bbf-jui-list {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-jui-list ul {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-jui-list li {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
margin-top: -1px;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.bbf-jui-list .bbf-list-sortable li {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions button {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-add {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-editor {
|
||||
width: 98%;
|
||||
}
|
500
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.js
vendored
Normal file
500
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.js
vendored
Normal file
|
@ -0,0 +1,500 @@
|
|||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
Base = Form.editors.Base,
|
||||
createTemplate = Form.helpers.createTemplate,
|
||||
triggerCancellableEvent = Form.helpers.triggerCancellableEvent,
|
||||
exports = {};
|
||||
|
||||
/**
|
||||
* Additional editors that depend on jQuery UI
|
||||
*/
|
||||
|
||||
//DATE
|
||||
exports['jqueryui.Date'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-date',
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
//Cast to Date
|
||||
if (this.value && !_.isDate(this.value)) {
|
||||
this.value = new Date(this.value);
|
||||
}
|
||||
|
||||
//Set default date
|
||||
if (!this.value) {
|
||||
var date = new Date();
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
this.value = date;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
$el.html('<input>');
|
||||
|
||||
var input = $('input', $el);
|
||||
|
||||
input.datepicker({
|
||||
dateFormat: 'dd/mm/yy',
|
||||
showButtonPanel: true
|
||||
});
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Make sure setValue of this object is called, not of any objects extending it (e.g. DateTime)
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected date
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
$('input', this.el).datepicker('setDate', value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('show');
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('hide');
|
||||
},
|
||||
|
||||
_observeDatepickerEvents: function() {
|
||||
var self = this;
|
||||
this.$('input').datepicker('option', 'onSelect', function() {
|
||||
self.trigger('change', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'onClose', function() {
|
||||
if (!self.hasFocus) return;
|
||||
self.trigger('blur', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'beforeShow', function() {
|
||||
if (self.hasFocus) return {};
|
||||
self.trigger('focus', self);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//DATETIME
|
||||
exports['jqueryui.DateTime'] = exports['jqueryui.Date'].extend({
|
||||
|
||||
className: 'bbf-jui-datetime',
|
||||
|
||||
template: createTemplate('<select>{{hours}}</select> : <select>{{mins}}</select>'),
|
||||
|
||||
render: function() {
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
//Render the date element first
|
||||
exports['jqueryui.Date'].prototype.render.call(this);
|
||||
|
||||
//Setup hour options
|
||||
var hours = _.range(0, 24),
|
||||
hoursOptions = [];
|
||||
|
||||
_.each(hours, function(hour) {
|
||||
hoursOptions.push('<option value="'+hour+'">' + pad(hour) + '</option>');
|
||||
});
|
||||
|
||||
//Setup minute options
|
||||
var minsInterval = this.schema.minsInterval || 15,
|
||||
mins = _.range(0, 60, minsInterval),
|
||||
minsOptions = [];
|
||||
|
||||
_.each(mins, function(min) {
|
||||
minsOptions.push('<option value="'+min+'">' + pad(min) + '</option>');
|
||||
});
|
||||
|
||||
//Render time selects
|
||||
this.$el.append(this.template({
|
||||
hours: hoursOptions.join(),
|
||||
mins: minsOptions.join()
|
||||
}));
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Store references to selects
|
||||
this.$hours = $('select:eq(0)', this.el);
|
||||
this.$mins = $('select:eq(1)', this.el);
|
||||
|
||||
//Set time
|
||||
this.setValue(this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected datetime
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
date.setHours(this.$hours.val());
|
||||
date.setMinutes(this.$mins.val());
|
||||
date.setMilliseconds(0);
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(date) {
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, date);
|
||||
|
||||
this.$hours.val(date.getHours());
|
||||
this.$mins.val(date.getMinutes());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//LIST
|
||||
exports['jqueryui.List'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-list',
|
||||
|
||||
//Note: The extra div around the <ul> is used to limit the drag area
|
||||
template: createTemplate('\
|
||||
<ul></ul>\
|
||||
<div><button class="bbf-list-add">Add</div>\
|
||||
'),
|
||||
|
||||
itemTemplate: createTemplate('\
|
||||
<li rel="{{id}}">\
|
||||
<span class="bbf-list-text">{{text}}</span>\
|
||||
<div class="bbf-list-actions">\
|
||||
<button class="bbf-list-edit">Edit</button>\
|
||||
<button class="bbf-list-del">Delete</button>\
|
||||
</div>\
|
||||
</li>\
|
||||
'),
|
||||
|
||||
editorTemplate: createTemplate('\
|
||||
<div class="bbf-field">\
|
||||
<div class="bbf-list-editor"></div>\
|
||||
</div>\
|
||||
'),
|
||||
|
||||
events: {
|
||||
'click .bbf-list-add': 'addNewItem',
|
||||
'click .bbf-list-edit': 'editItem',
|
||||
'click .bbf-list-del': 'deleteItem'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
if (!this.schema) throw "Missing required option 'schema'";
|
||||
|
||||
this.schema.listType = this.schema.listType || 'Text';
|
||||
|
||||
if (this.schema.listType === 'NestedModel' && !this.schema.model)
|
||||
throw "Missing required option 'schema.model'";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
//Main element
|
||||
$el.html(this.template());
|
||||
|
||||
//Create list
|
||||
var self = this,
|
||||
data = this.value || [],
|
||||
schema = this.schema,
|
||||
itemToString = this.itemToString,
|
||||
itemTemplate = this.itemTemplate,
|
||||
listEl = $('ul', $el);
|
||||
|
||||
_.each(data, function(itemData) {
|
||||
var text = itemToString.call(self, itemData);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(itemTemplate({
|
||||
id: itemData.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Attach data
|
||||
$.data(li[0], 'data', itemData);
|
||||
|
||||
listEl.append(li);
|
||||
});
|
||||
|
||||
//Make sortable
|
||||
if (schema.sortable !== false) {
|
||||
listEl.sortable({
|
||||
axis: 'y',
|
||||
cursor: 'move',
|
||||
containment: 'parent'
|
||||
});
|
||||
|
||||
$el.addClass('bbf-list-sortable');
|
||||
}
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-add', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-plus' }
|
||||
});
|
||||
$('button.bbf-list-edit', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats an item for display in the list
|
||||
* For example objects, dates etc. can have a custom
|
||||
* itemToString method which says how it should be formatted.
|
||||
*/
|
||||
itemToString: function(data) {
|
||||
if (!data) return data;
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(data);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (this.schema.listType === 'NestedModel') {
|
||||
var model = new (this.schema.model)(data);
|
||||
|
||||
return model.toString();
|
||||
}
|
||||
|
||||
//Last resort, just return the data as is
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list if it is completed in the editor
|
||||
*/
|
||||
addNewItem: function(event) {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
this.openEditor(null, function(value, editor) {
|
||||
//Fire 'addItem' cancellable event
|
||||
triggerCancellableEvent(self, 'addItem', [value, editor], function() {
|
||||
var text = self.itemToString(value);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(self.itemTemplate({
|
||||
id: value.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', value);
|
||||
|
||||
$('ul', self.el).append(li);
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-edit', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
self.trigger('add', self, value);
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit an existing item in the list
|
||||
*/
|
||||
editItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
originalValue = $.data(li[0], 'data');
|
||||
|
||||
this.openEditor(originalValue, function(newValue, editor) {
|
||||
//Fire 'editItem' cancellable event
|
||||
triggerCancellableEvent(self, 'editItem', [newValue, editor], function() {
|
||||
//Update display
|
||||
$('.bbf-list-text', li).html(self.itemToString(newValue));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', newValue);
|
||||
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
data = $.data(li[0], 'data');
|
||||
|
||||
var confirmDelete = (this.schema.confirmDelete) ? this.schema.confirmDelete : false,
|
||||
confirmMsg = this.schema.confirmDeleteMsg || 'Are you sure?';
|
||||
|
||||
function remove() {
|
||||
triggerCancellableEvent(self, 'removeItem', [data], function() {
|
||||
li.remove();
|
||||
|
||||
self.trigger('remove', self, data);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.schema.confirmDelete) {
|
||||
if (confirm(confirmMsg)) remove();
|
||||
} else {
|
||||
remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the sub editor dialog
|
||||
* @param {Mixed} Data (if editing existing list item, null otherwise)
|
||||
* @param {Function} Save callback. receives: value
|
||||
*/
|
||||
openEditor: function(data, callback) {
|
||||
var self = this,
|
||||
schema = this.schema,
|
||||
listType = schema.listType || 'Text';
|
||||
|
||||
var editor = Form.helpers.createEditor(listType, {
|
||||
key: '',
|
||||
schema: schema,
|
||||
value: data
|
||||
}).render();
|
||||
|
||||
var container = this.editorContainer = $(this.editorTemplate());
|
||||
$('.bbf-list-editor', container).html(editor.el);
|
||||
|
||||
var saveAndClose = function() {
|
||||
var errs = editor.validate();
|
||||
if (errs) return;
|
||||
|
||||
callback(editor.getValue(), editor);
|
||||
container.dialog('close');
|
||||
};
|
||||
|
||||
var handleEnterPressed = function(event) {
|
||||
if (event.keyCode !== 13) return;
|
||||
|
||||
saveAndClose();
|
||||
};
|
||||
|
||||
$(container).dialog({
|
||||
resizable: false,
|
||||
modal: true,
|
||||
width: 500,
|
||||
title: data ? 'Edit item' : 'New item',
|
||||
buttons: {
|
||||
'OK': saveAndClose,
|
||||
'Cancel': function() {
|
||||
container.dialog('close');
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
self.editorContainer = null;
|
||||
|
||||
$(document).unbind('keydown', handleEnterPressed);
|
||||
|
||||
editor.remove();
|
||||
container.remove();
|
||||
|
||||
self.trigger('item:close', self, editor);
|
||||
self.trigger('item:blur', self, editor);
|
||||
self.trigger('blur', self);
|
||||
}
|
||||
});
|
||||
|
||||
this.trigger('item:open', this, editor);
|
||||
this.trigger('item:focus', this, editor);
|
||||
this.trigger('focus', this);
|
||||
|
||||
//Save and close dialog on Enter keypress
|
||||
$(document).bind('keydown', handleEnterPressed);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var data = [];
|
||||
|
||||
$('li', this.el).each(function(index, li) {
|
||||
data.push($.data(li, 'data'));
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
var item = this.$('li .bbf-list-edit').first();
|
||||
if (item.length > 0) {
|
||||
item.click();
|
||||
}
|
||||
else {
|
||||
this.addNewItem();
|
||||
}
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.editorContainer) this.editorContainer.dialog('close');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//Exports
|
||||
_.extend(Form.editors, exports);
|
||||
|
||||
})();
|
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/jquery-ui.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,579 @@
|
|||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
editors = Form.editors;
|
||||
|
||||
/**
|
||||
* LIST
|
||||
*
|
||||
* An array editor. Creates a list of other editor items.
|
||||
*
|
||||
* Special options:
|
||||
* @param {String} [options.schema.itemType] The editor type for each item in the list. Default: 'Text'
|
||||
* @param {String} [options.schema.confirmDelete] Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
|
||||
*/
|
||||
editors.List = editors.Base.extend({
|
||||
|
||||
events: {
|
||||
'click [data-action="add"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.addItem(null, true);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
if (!schema) throw "Missing required option 'schema'";
|
||||
|
||||
//List schema defaults
|
||||
this.schema = _.extend({
|
||||
listTemplate: 'list',
|
||||
listItemTemplate: 'listItem'
|
||||
}, schema);
|
||||
|
||||
//Determine the editor to use
|
||||
this.Editor = (function() {
|
||||
var type = schema.itemType;
|
||||
|
||||
//Default to Text
|
||||
if (!type) return editors.Text;
|
||||
|
||||
//Use List-specific version if available
|
||||
if (editors.List[type]) return editors.List[type];
|
||||
|
||||
//Or whichever was passed
|
||||
return editors[type];
|
||||
})();
|
||||
|
||||
this.items = [];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this,
|
||||
value = this.value || [];
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listTemplate]({
|
||||
items: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
//Store a reference to the list (item container)
|
||||
this.$list = $el.find('.bbf-tmp').parent().empty();
|
||||
|
||||
//Add existing items
|
||||
if (value.length) {
|
||||
_.each(value, function(itemValue) {
|
||||
self.addItem(itemValue);
|
||||
});
|
||||
}
|
||||
|
||||
//If no existing items create an empty one, unless the editor specifies otherwise
|
||||
else {
|
||||
if (!this.Editor.isAsync) this.addItem();
|
||||
}
|
||||
|
||||
this.setElement($el);
|
||||
this.$el.attr('id', this.id);
|
||||
this.$el.attr('name', this.key);
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list
|
||||
* @param {Mixed} [value] Value for the new item editor
|
||||
* @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
|
||||
*/
|
||||
addItem: function(value, userInitiated) {
|
||||
var self = this;
|
||||
|
||||
//Create the item
|
||||
var item = new editors.List.Item({
|
||||
list: this,
|
||||
schema: this.schema,
|
||||
value: value,
|
||||
Editor: this.Editor,
|
||||
key: this.key
|
||||
}).render();
|
||||
|
||||
var _addItem = function() {
|
||||
self.items.push(item);
|
||||
self.$list.append(item.el);
|
||||
|
||||
item.editor.on('all', function(event) {
|
||||
if (event === 'change') return;
|
||||
|
||||
// args = ["key:change", itemEditor, fieldEditor]
|
||||
var args = _.toArray(arguments);
|
||||
args[0] = 'item:' + event;
|
||||
args.splice(1, 0, self);
|
||||
// args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]
|
||||
|
||||
editors.List.prototype.trigger.apply(this, args);
|
||||
}, self);
|
||||
|
||||
item.editor.on('change', function() {
|
||||
if (!item.addEventTriggered) {
|
||||
item.addEventTriggered = true;
|
||||
this.trigger('add', this, item.editor);
|
||||
}
|
||||
this.trigger('item:change', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}, self);
|
||||
|
||||
item.editor.on('focus', function() {
|
||||
if (this.hasFocus) return;
|
||||
this.trigger('focus', this);
|
||||
}, self);
|
||||
item.editor.on('blur', function() {
|
||||
if (!this.hasFocus) return;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
|
||||
self.trigger('blur', self);
|
||||
}, 0);
|
||||
}, self);
|
||||
|
||||
if (userInitiated || value) {
|
||||
item.addEventTriggered = true;
|
||||
}
|
||||
|
||||
if (userInitiated) {
|
||||
self.trigger('add', self, item.editor);
|
||||
self.trigger('change', self);
|
||||
}
|
||||
};
|
||||
|
||||
//Check if we need to wait for the item to complete before adding to the list
|
||||
if (this.Editor.isAsync) {
|
||||
item.editor.on('readyToAdd', _addItem, this);
|
||||
}
|
||||
|
||||
//Most editors can be added automatically
|
||||
else {
|
||||
_addItem();
|
||||
}
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an item from the list
|
||||
* @param {List.Item} item
|
||||
*/
|
||||
removeItem: function(item) {
|
||||
//Confirm delete
|
||||
var confirmMsg = this.schema.confirmDelete;
|
||||
if (confirmMsg && !confirm(confirmMsg)) return;
|
||||
|
||||
var index = _.indexOf(this.items, item);
|
||||
|
||||
this.items[index].remove();
|
||||
this.items.splice(index, 1);
|
||||
|
||||
if (item.addEventTriggered) {
|
||||
this.trigger('remove', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}
|
||||
|
||||
if (!this.items.length && !this.Editor.isAsync) this.addItem();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var values = _.map(this.items, function(item) {
|
||||
return item.getValue();
|
||||
});
|
||||
|
||||
//Filter empty items
|
||||
return _.without(values, undefined, '');
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
if (this.items[0]) this.items[0].editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
var focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });
|
||||
|
||||
if (focusedItem) focusedItem.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Override default remove function in order to remove item views
|
||||
*/
|
||||
remove: function() {
|
||||
_.invoke(this.items, 'remove');
|
||||
|
||||
editors.Base.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Run validation
|
||||
*
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
validate: function() {
|
||||
if (!this.validators) return null;
|
||||
|
||||
//Collect errors
|
||||
var errors = _.map(this.items, function(item) {
|
||||
return item.validate();
|
||||
});
|
||||
|
||||
//Check if any item has errors
|
||||
var hasErrors = _.compact(errors).length ? true : false;
|
||||
if (!hasErrors) return null;
|
||||
|
||||
//If so create a shared error
|
||||
var fieldError = {
|
||||
type: 'list',
|
||||
message: 'Some of the items in the list failed validation',
|
||||
errors: errors
|
||||
};
|
||||
|
||||
return fieldError;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A single item in the list
|
||||
*
|
||||
* @param {editors.List} options.list The List editor instance this item belongs to
|
||||
* @param {Function} options.Editor Editor constructor function
|
||||
* @param {String} options.key Model key
|
||||
* @param {Mixed} options.value Value
|
||||
* @param {Object} options.schema Field schema
|
||||
*/
|
||||
editors.List.Item = Backbone.View.extend({
|
||||
events: {
|
||||
'click [data-action="remove"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.list.removeItem(this);
|
||||
},
|
||||
'keydown input[type=text]': function(event) {
|
||||
if(event.keyCode !== 13) return;
|
||||
event.preventDefault();
|
||||
this.list.addItem();
|
||||
this.list.$list.find("> li:last input").focus();
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.list = options.list;
|
||||
this.schema = options.schema || this.list.schema;
|
||||
this.value = options.value;
|
||||
this.Editor = options.Editor || editors.Text;
|
||||
this.key = options.key;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
//Create editor
|
||||
this.editor = new this.Editor({
|
||||
key: this.key,
|
||||
schema: this.schema,
|
||||
value: this.value,
|
||||
list: this.list,
|
||||
item: this
|
||||
}).render();
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listItemTemplate]({
|
||||
editor: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
$el.find('.bbf-tmp').replaceWith(this.editor.el);
|
||||
|
||||
//Replace the entire element so there isn't a wrapper tag
|
||||
this.setElement($el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
this.editor.blur();
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.editor.remove();
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
var value = this.getValue(),
|
||||
formValues = this.list.form ? this.list.form.getValue() : {},
|
||||
validators = this.schema.validators,
|
||||
getValidator = Form.helpers.getValidator;
|
||||
|
||||
if (!validators) return null;
|
||||
|
||||
//Run through validators until an error is found
|
||||
var error = null;
|
||||
_.every(validators, function(validator) {
|
||||
error = getValidator(validator)(value, formValues);
|
||||
|
||||
return error ? false : true;
|
||||
});
|
||||
|
||||
//Show/hide error
|
||||
if (error){
|
||||
this.setError(error);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
//Return error to be aggregated by list
|
||||
return error ? error : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a validation error
|
||||
*/
|
||||
setError: function(err) {
|
||||
this.$el.addClass(Form.classNames.error);
|
||||
this.$el.attr('title', err.message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide validation errors
|
||||
*/
|
||||
clearError: function() {
|
||||
this.$el.removeClass(Form.classNames.error);
|
||||
this.$el.attr('title', null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Modal object editor for use with the List editor.
|
||||
* To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
|
||||
*/
|
||||
editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
|
||||
events: {
|
||||
'click': 'openEditor'
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
|
||||
* @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
|
||||
* @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
|
||||
* @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
|
||||
*/
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//Dependencies
|
||||
if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
|
||||
|
||||
//Get nested schema if Object
|
||||
if (schema.itemType === 'Object') {
|
||||
if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
|
||||
|
||||
this.nestedSchema = schema.subSchema;
|
||||
}
|
||||
|
||||
//Get nested schema if NestedModel
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
if (!schema.model) throw 'Missing required option "schema.model"';
|
||||
|
||||
this.nestedSchema = schema.model.prototype.schema;
|
||||
if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the list item representation
|
||||
*/
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
//New items in the list are only rendered when the editor has been OK'd
|
||||
if (_.isEmpty(this.value)) {
|
||||
this.openEditor();
|
||||
}
|
||||
|
||||
//But items with values are added automatically
|
||||
else {
|
||||
this.renderSummary();
|
||||
|
||||
setTimeout(function() {
|
||||
self.trigger('readyToAdd');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the list item representation
|
||||
*/
|
||||
renderSummary: function() {
|
||||
var template = Form.templates['list.Modal'];
|
||||
|
||||
this.$el.html(template({
|
||||
summary: this.getStringValue()
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Function which returns a generic string representation of an object
|
||||
*
|
||||
* @param {Object} value
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
itemToString: function(value) {
|
||||
value = value || {};
|
||||
|
||||
//Pretty print the object keys and values
|
||||
var parts = [];
|
||||
_.each(this.nestedSchema, function(schema, key) {
|
||||
var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
|
||||
val = value[key];
|
||||
|
||||
if (_.isUndefined(val) || _.isNull(val)) val = '';
|
||||
|
||||
parts.push(desc + ': ' + val);
|
||||
});
|
||||
|
||||
return parts.join('<br />');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the string representation of the object value
|
||||
*/
|
||||
getStringValue: function() {
|
||||
var schema = this.schema,
|
||||
value = this.getValue();
|
||||
|
||||
if (_.isEmpty(value)) return '[Empty]';
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(value);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
return new (schema.model)(value).toString();
|
||||
}
|
||||
|
||||
//Otherwise use the generic method or custom overridden method
|
||||
return this.itemToString(value);
|
||||
},
|
||||
|
||||
openEditor: function() {
|
||||
var self = this;
|
||||
|
||||
var form = new Form({
|
||||
schema: this.nestedSchema,
|
||||
data: this.value
|
||||
});
|
||||
|
||||
var modal = this.modal = new Backbone.BootstrapModal({
|
||||
content: form,
|
||||
animate: true
|
||||
}).open();
|
||||
|
||||
this.trigger('open', this);
|
||||
this.trigger('focus', this);
|
||||
|
||||
modal.on('cancel', function() {
|
||||
this.modal = null;
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
}, this);
|
||||
|
||||
modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user clicks 'OK'.
|
||||
* Runs validation and tells the list when ready to add the item
|
||||
*/
|
||||
onModalSubmitted: function(form, modal) {
|
||||
var isNew = !this.value;
|
||||
|
||||
//Stop if there are validation errors
|
||||
var error = form.validate();
|
||||
if (error) return modal.preventClose();
|
||||
this.modal = null;
|
||||
|
||||
//If OK, render the list item
|
||||
this.value = form.getValue();
|
||||
|
||||
this.renderSummary();
|
||||
|
||||
if (isNew) this.trigger('readyToAdd');
|
||||
|
||||
this.trigger('change', this);
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.value;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.openEditor();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.modal) {
|
||||
this.modal.trigger('cancel');
|
||||
this.modal.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
//STATICS
|
||||
|
||||
//The modal adapter that creates and manages the modal dialog.
|
||||
//Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
|
||||
//Can be replaced with another adapter that implements the same interface.
|
||||
ModalAdapter: Backbone.BootstrapModal,
|
||||
|
||||
//Make the wait list for the 'ready' event before adding the item to the list
|
||||
isAsync: true
|
||||
});
|
||||
|
||||
})();
|
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/list.min.js
vendored
Normal file
1
sites/all/modules/civicrm/packages/backbone-forms/distribution/editors/list.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
43
sites/all/modules/civicrm/packages/backbone-forms/distribution/templates/bootstrap.css
vendored
Normal file
43
sites/all/modules/civicrm/packages/backbone-forms/distribution/templates/bootstrap.css
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list .bbf-add {
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.bbf-list .bbf-del {
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
95
sites/all/modules/civicrm/packages/backbone-forms/distribution/templates/bootstrap.js
vendored
Normal file
95
sites/all/modules/civicrm/packages/backbone-forms/distribution/templates/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Include this file _after_ the main backbone-forms file to override the default templates.
|
||||
* You only need to include templates you want to override.
|
||||
*
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
;(function() {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
|
||||
//TWITTER BOOTSTRAP TEMPLATES
|
||||
//Requires Bootstrap 2.x
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="form-horizontal">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
{{fields}}\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<div class="control-group field-{{key}}">\
|
||||
<label class="control-label" for="{{id}}">{{title}}</label>\
|
||||
<div class="controls">\
|
||||
{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<div class="field-{{key}}">\
|
||||
<div title="{{title}}" class="input-xlarge">{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul class="unstyled clearfix">{{items}}</ul>\
|
||||
<button class="btn bbf-add" data-action="add">Add</button>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li class="clearfix">\
|
||||
<div class="pull-left">{{editor}}</div>\
|
||||
<button type="button" class="btn bbf-del" data-action="remove">×</button>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<p>{{date}}</p>\
|
||||
<p>\
|
||||
<select data-type="hour" style="width: 4em">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min" style="width: 4em">{{mins}}</select>\
|
||||
</p>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'error' //Set on the field tag when validation fails
|
||||
});
|
||||
|
||||
|
||||
})();
|
|
@ -0,0 +1,140 @@
|
|||
/* Form */
|
||||
.bbf-form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
/* Field */
|
||||
.bbf-field {
|
||||
margin: 1em 0;
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bbf-field label {
|
||||
float: left;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-editor {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
}
|
||||
|
||||
.bbf-field input, .bbf-field textarea, .bbf-field select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-help {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-error {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bbf-field.bbf-error .bbf-editor {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
|
||||
/* Radio */
|
||||
.bbf-radio {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-radio input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-radio label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* Checkboxes */
|
||||
.bbf-checkboxes {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-checkboxes input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-checkboxes label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-editor-container {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-remove {
|
||||
float: right;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-actions {
|
||||
text-align: center;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime .bbf-date-container {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Include this file _after_ the main backbone-forms file to override the default templates.
|
||||
* You only need to include templates you want to override.
|
||||
*
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
;(function() {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
|
||||
//DEFAULT TEMPLATES
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="bbf-form">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
<ul>{{fields}}</ul>\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<li class="bbf-field field-{{key}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<li class="bbf-field bbf-nested-field field-{{key}}" title="{{title}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul>{{items}}</ul>\
|
||||
<div class="bbf-actions"><button type="button" data-action="add">Add</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li>\
|
||||
<button type="button" data-action="remove" class="bbf-remove">×</button>\
|
||||
<div class="bbf-editor-container">{{editor}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<div class="bbf-date-container">{{date}}</div>\
|
||||
<select data-type="hour">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min">{{mins}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'bbf-error'
|
||||
|
||||
});
|
||||
|
||||
|
||||
})();
|
17
sites/all/modules/civicrm/packages/backbone-forms/package.json
Executable file
17
sites/all/modules/civicrm/packages/backbone-forms/package.json
Executable file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "backbone-forms",
|
||||
"description": "Form framework for BackboneJS with nested forms, editable lists and validation",
|
||||
"author": {
|
||||
"name": "Charles Davison"
|
||||
},
|
||||
"version": "0.10.0",
|
||||
"dependencies": {
|
||||
"backbone": ">=0.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"buildify": "latest",
|
||||
"underscore": "latest",
|
||||
"uglify-js": "latest"
|
||||
},
|
||||
"main": "distribution.amd/backbone-forms.js"
|
||||
}
|
101
sites/all/modules/civicrm/packages/backbone-forms/scripts/build
Executable file
101
sites/all/modules/civicrm/packages/backbone-forms/scripts/build
Executable file
|
@ -0,0 +1,101 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var builder = require('buildify'),
|
||||
path = require('path');
|
||||
|
||||
var templateData = {
|
||||
version: '0.10.1'
|
||||
};
|
||||
|
||||
var baseDir = __dirname + '/..',
|
||||
sourceDir = baseDir + '/src',
|
||||
outputDir = baseDir + '/distribution';
|
||||
|
||||
//Files to concatenate into main file
|
||||
var fileList = [
|
||||
'form.js',
|
||||
'helpers.js',
|
||||
'validators.js',
|
||||
'field.js',
|
||||
'editors.js',
|
||||
'setup.js',
|
||||
'templates/default.js'
|
||||
];
|
||||
|
||||
//Concatenate the main files
|
||||
var mainContents = builder(sourceDir)
|
||||
.concat(fileList)
|
||||
.getContent();
|
||||
|
||||
//Save regular dev and mini versions
|
||||
builder(baseDir)
|
||||
.setContent(mainContents)
|
||||
.wrap('scripts/build-templates/main.js', templateData)
|
||||
.save('distribution/backbone-forms.js')
|
||||
.uglify()
|
||||
.save('distribution/backbone-forms.min.js');
|
||||
|
||||
//Main AMD file (for RequireJS)
|
||||
builder(baseDir)
|
||||
.setContent(mainContents)
|
||||
.wrap('scripts/build-templates/main.amd.js', templateData)
|
||||
.save('distribution.amd/backbone-forms.js')
|
||||
.uglify()
|
||||
.save('distribution.amd/backbone-forms.min.js');
|
||||
|
||||
|
||||
//Standalone template files
|
||||
['default', 'bootstrap'].forEach(function(template) {
|
||||
//HTML
|
||||
builder(baseDir)
|
||||
.load('src/templates/'+template+'.js')
|
||||
.wrap('scripts/build-templates/standalone-template.js')
|
||||
.save('distribution/templates/'+template+'.js');
|
||||
|
||||
//HTML (AMD)
|
||||
builder(baseDir)
|
||||
.load('src/templates/'+template+'.js')
|
||||
.wrap('scripts/build-templates/standalone-template.amd.js')
|
||||
.save('distribution.amd/templates/'+template+'.js');
|
||||
|
||||
//CSS
|
||||
builder(baseDir)
|
||||
.load('src/templates/'+template+'.css')
|
||||
.save('distribution/templates/'+template+'.css')
|
||||
.save('distribution.amd/templates/'+template+'.css');
|
||||
});
|
||||
|
||||
|
||||
//Extra editors
|
||||
['list', 'jquery-ui'].forEach(function(name) {
|
||||
//Save regular dev and mini versions
|
||||
builder(baseDir)
|
||||
.load('src/editors/'+name+'.js')
|
||||
.save('distribution/editors/'+name+'.js')
|
||||
.uglify()
|
||||
.save('distribution/editors/'+name+'.min.js');
|
||||
|
||||
//CSS file
|
||||
if (path.existsSync(baseDir + '/src/editors/'+name+'.css')) {
|
||||
builder(baseDir)
|
||||
.load('src/editors/'+name+'.css')
|
||||
.save('distribution/editors/'+name+'.css')
|
||||
.save('distribution.amd/editors/'+name+'.css');
|
||||
}
|
||||
|
||||
//Save AMD version (for RequireJS)
|
||||
builder(baseDir)
|
||||
.load('src/editors/'+name+'.js')
|
||||
.wrap('scripts/build-templates/simple.amd.js')
|
||||
.save('distribution.amd/editors/'+name+'.js')
|
||||
.uglify()
|
||||
.save('distribution.amd/editors/'+name+'.min.js');
|
||||
});
|
||||
|
||||
|
||||
//Modal adapters
|
||||
builder(baseDir)
|
||||
.load('lib/backbone.bootstrap-modal/src/backbone.bootstrap-modal.js')
|
||||
.save('distribution/adapters/backbone.bootstrap-modal.js')
|
||||
.uglify()
|
||||
.save('distribution/adapters/backbone.bootstrap-modal.min.js');
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Backbone Forms v{{version}}
|
||||
*
|
||||
* NOTE:
|
||||
* This version is for use with RequireJS
|
||||
* If using regular <script> tags to include your files, use backbone-forms.min.js
|
||||
*
|
||||
* Copyright (c) 2012 Charles Davison, Pow Media Ltd
|
||||
*
|
||||
* License and more information at:
|
||||
* http://github.com/powmedia/backbone-forms
|
||||
*/
|
||||
define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
|
||||
|
||||
{{body}}
|
||||
|
||||
|
||||
//Metadata
|
||||
Form.VERSION = '{{version}}';
|
||||
|
||||
//Exports
|
||||
Backbone.Form = Form;
|
||||
|
||||
return Form;
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* Backbone Forms v{{version}}
|
||||
*
|
||||
* Copyright (c) 2012 Charles Davison, Pow Media Ltd
|
||||
*
|
||||
* License and more information at:
|
||||
* http://github.com/powmedia/backbone-forms
|
||||
*/
|
||||
;(function(root) {
|
||||
|
||||
//DEPENDENCIES
|
||||
//CommonJS
|
||||
if (typeof exports !== 'undefined' && typeof require !== 'undefined') {
|
||||
var $ = root.jQuery || root.Zepto || root.ender || require('jquery'),
|
||||
_ = root._ || require('underscore'),
|
||||
Backbone = root.Backbone || require('backbone');
|
||||
}
|
||||
|
||||
//Browser
|
||||
else {
|
||||
var $ = root.jQuery,
|
||||
_ = root._,
|
||||
Backbone = root.Backbone;
|
||||
}
|
||||
|
||||
|
||||
//SOURCE
|
||||
{{body}}
|
||||
|
||||
|
||||
//Metadata
|
||||
Form.VERSION = '{{version}}';
|
||||
|
||||
|
||||
//Exports
|
||||
Backbone.Form = Form;
|
||||
|
||||
})(this);
|
|
@ -0,0 +1,5 @@
|
|||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
|
||||
{{body}}
|
||||
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
define(['jquery', 'underscore', 'backbone', 'backbone-forms'], function($, _, Backbone) {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
{{body}}
|
||||
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Include this file _after_ the main backbone-forms file to override the default templates.
|
||||
* You only need to include templates you want to override.
|
||||
*
|
||||
* Requirements when customising templates:
|
||||
* - Each template must have one 'parent' element tag.
|
||||
* - "data-type" attributes are required.
|
||||
* - The main placeholder tags such as the following are required: fieldsets, fields
|
||||
*/
|
||||
;(function() {
|
||||
var Form = Backbone.Form;
|
||||
|
||||
{{body}}
|
||||
|
||||
})();
|
2
sites/all/modules/civicrm/packages/backbone-forms/scripts/watch
Executable file
2
sites/all/modules/civicrm/packages/backbone-forms/scripts/watch
Executable file
|
@ -0,0 +1,2 @@
|
|||
#Watches the /src directory for changes and builds the distribution files
|
||||
supervisor --watch src,lib --no-restart-on exit --quiet --extensions 'js|css' --exec node scripts/build
|
1263
sites/all/modules/civicrm/packages/backbone-forms/src/editors.js
Normal file
1263
sites/all/modules/civicrm/packages/backbone-forms/src/editors.js
Normal file
File diff suppressed because it is too large
Load diff
65
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.css
vendored
Normal file
65
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.css
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
.bbf-jui-date input, .bbf-jui-datetime input {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime input {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-jui-datetime select {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bbf-jui-list {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-jui-list ul {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-jui-list li {
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
margin-top: -1px;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
.bbf-jui-list .bbf-list-sortable li {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-actions button {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-add {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.bbf-jui-list .bbf-list-editor {
|
||||
width: 98%;
|
||||
}
|
500
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.js
vendored
Normal file
500
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.js
vendored
Normal file
|
@ -0,0 +1,500 @@
|
|||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
Base = Form.editors.Base,
|
||||
createTemplate = Form.helpers.createTemplate,
|
||||
triggerCancellableEvent = Form.helpers.triggerCancellableEvent,
|
||||
exports = {};
|
||||
|
||||
/**
|
||||
* Additional editors that depend on jQuery UI
|
||||
*/
|
||||
|
||||
//DATE
|
||||
exports['jqueryui.Date'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-date',
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
//Cast to Date
|
||||
if (this.value && !_.isDate(this.value)) {
|
||||
this.value = new Date(this.value);
|
||||
}
|
||||
|
||||
//Set default date
|
||||
if (!this.value) {
|
||||
var date = new Date();
|
||||
date.setSeconds(0);
|
||||
date.setMilliseconds(0);
|
||||
|
||||
this.value = date;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
$el.html('<input>');
|
||||
|
||||
var input = $('input', $el);
|
||||
|
||||
input.datepicker({
|
||||
dateFormat: 'dd/mm/yy',
|
||||
showButtonPanel: true
|
||||
});
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Make sure setValue of this object is called, not of any objects extending it (e.g. DateTime)
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected date
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
$('input', this.el).datepicker('setDate', value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('show');
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
this.$('input').datepicker('hide');
|
||||
},
|
||||
|
||||
_observeDatepickerEvents: function() {
|
||||
var self = this;
|
||||
this.$('input').datepicker('option', 'onSelect', function() {
|
||||
self.trigger('change', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'onClose', function() {
|
||||
if (!self.hasFocus) return;
|
||||
self.trigger('blur', self);
|
||||
});
|
||||
this.$('input').datepicker('option', 'beforeShow', function() {
|
||||
if (self.hasFocus) return {};
|
||||
self.trigger('focus', self);
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//DATETIME
|
||||
exports['jqueryui.DateTime'] = exports['jqueryui.Date'].extend({
|
||||
|
||||
className: 'bbf-jui-datetime',
|
||||
|
||||
template: createTemplate('<select>{{hours}}</select> : <select>{{mins}}</select>'),
|
||||
|
||||
render: function() {
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
//Render the date element first
|
||||
exports['jqueryui.Date'].prototype.render.call(this);
|
||||
|
||||
//Setup hour options
|
||||
var hours = _.range(0, 24),
|
||||
hoursOptions = [];
|
||||
|
||||
_.each(hours, function(hour) {
|
||||
hoursOptions.push('<option value="'+hour+'">' + pad(hour) + '</option>');
|
||||
});
|
||||
|
||||
//Setup minute options
|
||||
var minsInterval = this.schema.minsInterval || 15,
|
||||
mins = _.range(0, 60, minsInterval),
|
||||
minsOptions = [];
|
||||
|
||||
_.each(mins, function(min) {
|
||||
minsOptions.push('<option value="'+min+'">' + pad(min) + '</option>');
|
||||
});
|
||||
|
||||
//Render time selects
|
||||
this.$el.append(this.template({
|
||||
hours: hoursOptions.join(),
|
||||
mins: minsOptions.join()
|
||||
}));
|
||||
|
||||
this._observeDatepickerEvents();
|
||||
|
||||
//Store references to selects
|
||||
this.$hours = $('select:eq(0)', this.el);
|
||||
this.$mins = $('select:eq(1)', this.el);
|
||||
|
||||
//Set time
|
||||
this.setValue(this.value);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {Date} Selected datetime
|
||||
*/
|
||||
getValue: function() {
|
||||
var input = $('input', this.el),
|
||||
date = input.datepicker('getDate');
|
||||
|
||||
date.setHours(this.$hours.val());
|
||||
date.setMinutes(this.$mins.val());
|
||||
date.setMilliseconds(0);
|
||||
|
||||
return date;
|
||||
},
|
||||
|
||||
setValue: function(date) {
|
||||
exports['jqueryui.Date'].prototype.setValue.call(this, date);
|
||||
|
||||
this.$hours.val(date.getHours());
|
||||
this.$mins.val(date.getMinutes());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//LIST
|
||||
exports['jqueryui.List'] = Base.extend({
|
||||
|
||||
className: 'bbf-jui-list',
|
||||
|
||||
//Note: The extra div around the <ul> is used to limit the drag area
|
||||
template: createTemplate('\
|
||||
<ul></ul>\
|
||||
<div><button class="bbf-list-add">Add</div>\
|
||||
'),
|
||||
|
||||
itemTemplate: createTemplate('\
|
||||
<li rel="{{id}}">\
|
||||
<span class="bbf-list-text">{{text}}</span>\
|
||||
<div class="bbf-list-actions">\
|
||||
<button class="bbf-list-edit">Edit</button>\
|
||||
<button class="bbf-list-del">Delete</button>\
|
||||
</div>\
|
||||
</li>\
|
||||
'),
|
||||
|
||||
editorTemplate: createTemplate('\
|
||||
<div class="bbf-field">\
|
||||
<div class="bbf-list-editor"></div>\
|
||||
</div>\
|
||||
'),
|
||||
|
||||
events: {
|
||||
'click .bbf-list-add': 'addNewItem',
|
||||
'click .bbf-list-edit': 'editItem',
|
||||
'click .bbf-list-del': 'deleteItem'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
Base.prototype.initialize.call(this, options);
|
||||
|
||||
if (!this.schema) throw "Missing required option 'schema'";
|
||||
|
||||
this.schema.listType = this.schema.listType || 'Text';
|
||||
|
||||
if (this.schema.listType === 'NestedModel' && !this.schema.model)
|
||||
throw "Missing required option 'schema.model'";
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var $el = this.$el;
|
||||
|
||||
//Main element
|
||||
$el.html(this.template());
|
||||
|
||||
//Create list
|
||||
var self = this,
|
||||
data = this.value || [],
|
||||
schema = this.schema,
|
||||
itemToString = this.itemToString,
|
||||
itemTemplate = this.itemTemplate,
|
||||
listEl = $('ul', $el);
|
||||
|
||||
_.each(data, function(itemData) {
|
||||
var text = itemToString.call(self, itemData);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(itemTemplate({
|
||||
id: itemData.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Attach data
|
||||
$.data(li[0], 'data', itemData);
|
||||
|
||||
listEl.append(li);
|
||||
});
|
||||
|
||||
//Make sortable
|
||||
if (schema.sortable !== false) {
|
||||
listEl.sortable({
|
||||
axis: 'y',
|
||||
cursor: 'move',
|
||||
containment: 'parent'
|
||||
});
|
||||
|
||||
$el.addClass('bbf-list-sortable');
|
||||
}
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-add', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-plus' }
|
||||
});
|
||||
$('button.bbf-list-edit', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', $el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Formats an item for display in the list
|
||||
* For example objects, dates etc. can have a custom
|
||||
* itemToString method which says how it should be formatted.
|
||||
*/
|
||||
itemToString: function(data) {
|
||||
if (!data) return data;
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(data);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (this.schema.listType === 'NestedModel') {
|
||||
var model = new (this.schema.model)(data);
|
||||
|
||||
return model.toString();
|
||||
}
|
||||
|
||||
//Last resort, just return the data as is
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list if it is completed in the editor
|
||||
*/
|
||||
addNewItem: function(event) {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
var self = this;
|
||||
|
||||
this.openEditor(null, function(value, editor) {
|
||||
//Fire 'addItem' cancellable event
|
||||
triggerCancellableEvent(self, 'addItem', [value, editor], function() {
|
||||
var text = self.itemToString(value);
|
||||
|
||||
//Create DOM element
|
||||
var li = $(self.itemTemplate({
|
||||
id: value.id || '',
|
||||
text: text
|
||||
}));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', value);
|
||||
|
||||
$('ul', self.el).append(li);
|
||||
|
||||
//jQuery UI buttonize
|
||||
$('button.bbf-list-edit', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-pencil' }
|
||||
});
|
||||
$('button.bbf-list-del', this.el).button({
|
||||
text: false,
|
||||
icons: { primary: 'ui-icon-trash' }
|
||||
});
|
||||
|
||||
self.trigger('add', self, value);
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit an existing item in the list
|
||||
*/
|
||||
editItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
originalValue = $.data(li[0], 'data');
|
||||
|
||||
this.openEditor(originalValue, function(newValue, editor) {
|
||||
//Fire 'editItem' cancellable event
|
||||
triggerCancellableEvent(self, 'editItem', [newValue, editor], function() {
|
||||
//Update display
|
||||
$('.bbf-list-text', li).html(self.itemToString(newValue));
|
||||
|
||||
//Store data
|
||||
$.data(li[0], 'data', newValue);
|
||||
|
||||
self.trigger('item:change', self, editor);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
deleteItem: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var self = this,
|
||||
li = $(event.target).closest('li'),
|
||||
data = $.data(li[0], 'data');
|
||||
|
||||
var confirmDelete = (this.schema.confirmDelete) ? this.schema.confirmDelete : false,
|
||||
confirmMsg = this.schema.confirmDeleteMsg || 'Are you sure?';
|
||||
|
||||
function remove() {
|
||||
triggerCancellableEvent(self, 'removeItem', [data], function() {
|
||||
li.remove();
|
||||
|
||||
self.trigger('remove', self, data);
|
||||
self.trigger('change', self);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.schema.confirmDelete) {
|
||||
if (confirm(confirmMsg)) remove();
|
||||
} else {
|
||||
remove();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the sub editor dialog
|
||||
* @param {Mixed} Data (if editing existing list item, null otherwise)
|
||||
* @param {Function} Save callback. receives: value
|
||||
*/
|
||||
openEditor: function(data, callback) {
|
||||
var self = this,
|
||||
schema = this.schema,
|
||||
listType = schema.listType || 'Text';
|
||||
|
||||
var editor = Form.helpers.createEditor(listType, {
|
||||
key: '',
|
||||
schema: schema,
|
||||
value: data
|
||||
}).render();
|
||||
|
||||
var container = this.editorContainer = $(this.editorTemplate());
|
||||
$('.bbf-list-editor', container).html(editor.el);
|
||||
|
||||
var saveAndClose = function() {
|
||||
var errs = editor.validate();
|
||||
if (errs) return;
|
||||
|
||||
callback(editor.getValue(), editor);
|
||||
container.dialog('close');
|
||||
};
|
||||
|
||||
var handleEnterPressed = function(event) {
|
||||
if (event.keyCode !== 13) return;
|
||||
|
||||
saveAndClose();
|
||||
};
|
||||
|
||||
$(container).dialog({
|
||||
resizable: false,
|
||||
modal: true,
|
||||
width: 500,
|
||||
title: data ? 'Edit item' : 'New item',
|
||||
buttons: {
|
||||
'OK': saveAndClose,
|
||||
'Cancel': function() {
|
||||
container.dialog('close');
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
self.editorContainer = null;
|
||||
|
||||
$(document).unbind('keydown', handleEnterPressed);
|
||||
|
||||
editor.remove();
|
||||
container.remove();
|
||||
|
||||
self.trigger('item:close', self, editor);
|
||||
self.trigger('item:blur', self, editor);
|
||||
self.trigger('blur', self);
|
||||
}
|
||||
});
|
||||
|
||||
this.trigger('item:open', this, editor);
|
||||
this.trigger('item:focus', this, editor);
|
||||
this.trigger('focus', this);
|
||||
|
||||
//Save and close dialog on Enter keypress
|
||||
$(document).bind('keydown', handleEnterPressed);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var data = [];
|
||||
|
||||
$('li', this.el).each(function(index, li) {
|
||||
data.push($.data(li, 'data'));
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
var item = this.$('li .bbf-list-edit').first();
|
||||
if (item.length > 0) {
|
||||
item.click();
|
||||
}
|
||||
else {
|
||||
this.addNewItem();
|
||||
}
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.editorContainer) this.editorContainer.dialog('close');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//Exports
|
||||
_.extend(Form.editors, exports);
|
||||
|
||||
})();
|
|
@ -0,0 +1,579 @@
|
|||
;(function() {
|
||||
|
||||
var Form = Backbone.Form,
|
||||
editors = Form.editors;
|
||||
|
||||
/**
|
||||
* LIST
|
||||
*
|
||||
* An array editor. Creates a list of other editor items.
|
||||
*
|
||||
* Special options:
|
||||
* @param {String} [options.schema.itemType] The editor type for each item in the list. Default: 'Text'
|
||||
* @param {String} [options.schema.confirmDelete] Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
|
||||
*/
|
||||
editors.List = editors.Base.extend({
|
||||
|
||||
events: {
|
||||
'click [data-action="add"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.addItem(null, true);
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
if (!schema) throw "Missing required option 'schema'";
|
||||
|
||||
//List schema defaults
|
||||
this.schema = _.extend({
|
||||
listTemplate: 'list',
|
||||
listItemTemplate: 'listItem'
|
||||
}, schema);
|
||||
|
||||
//Determine the editor to use
|
||||
this.Editor = (function() {
|
||||
var type = schema.itemType;
|
||||
|
||||
//Default to Text
|
||||
if (!type) return editors.Text;
|
||||
|
||||
//Use List-specific version if available
|
||||
if (editors.List[type]) return editors.List[type];
|
||||
|
||||
//Or whichever was passed
|
||||
return editors[type];
|
||||
})();
|
||||
|
||||
this.items = [];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this,
|
||||
value = this.value || [];
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listTemplate]({
|
||||
items: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
//Store a reference to the list (item container)
|
||||
this.$list = $el.find('.bbf-tmp').parent().empty();
|
||||
|
||||
//Add existing items
|
||||
if (value.length) {
|
||||
_.each(value, function(itemValue) {
|
||||
self.addItem(itemValue);
|
||||
});
|
||||
}
|
||||
|
||||
//If no existing items create an empty one, unless the editor specifies otherwise
|
||||
else {
|
||||
if (!this.Editor.isAsync) this.addItem();
|
||||
}
|
||||
|
||||
this.setElement($el);
|
||||
this.$el.attr('id', this.id);
|
||||
this.$el.attr('name', this.key);
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new item to the list
|
||||
* @param {Mixed} [value] Value for the new item editor
|
||||
* @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
|
||||
*/
|
||||
addItem: function(value, userInitiated) {
|
||||
var self = this;
|
||||
|
||||
//Create the item
|
||||
var item = new editors.List.Item({
|
||||
list: this,
|
||||
schema: this.schema,
|
||||
value: value,
|
||||
Editor: this.Editor,
|
||||
key: this.key
|
||||
}).render();
|
||||
|
||||
var _addItem = function() {
|
||||
self.items.push(item);
|
||||
self.$list.append(item.el);
|
||||
|
||||
item.editor.on('all', function(event) {
|
||||
if (event === 'change') return;
|
||||
|
||||
// args = ["key:change", itemEditor, fieldEditor]
|
||||
var args = _.toArray(arguments);
|
||||
args[0] = 'item:' + event;
|
||||
args.splice(1, 0, self);
|
||||
// args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]
|
||||
|
||||
editors.List.prototype.trigger.apply(this, args);
|
||||
}, self);
|
||||
|
||||
item.editor.on('change', function() {
|
||||
if (!item.addEventTriggered) {
|
||||
item.addEventTriggered = true;
|
||||
this.trigger('add', this, item.editor);
|
||||
}
|
||||
this.trigger('item:change', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}, self);
|
||||
|
||||
item.editor.on('focus', function() {
|
||||
if (this.hasFocus) return;
|
||||
this.trigger('focus', this);
|
||||
}, self);
|
||||
item.editor.on('blur', function() {
|
||||
if (!this.hasFocus) return;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
|
||||
self.trigger('blur', self);
|
||||
}, 0);
|
||||
}, self);
|
||||
|
||||
if (userInitiated || value) {
|
||||
item.addEventTriggered = true;
|
||||
}
|
||||
|
||||
if (userInitiated) {
|
||||
self.trigger('add', self, item.editor);
|
||||
self.trigger('change', self);
|
||||
}
|
||||
};
|
||||
|
||||
//Check if we need to wait for the item to complete before adding to the list
|
||||
if (this.Editor.isAsync) {
|
||||
item.editor.on('readyToAdd', _addItem, this);
|
||||
}
|
||||
|
||||
//Most editors can be added automatically
|
||||
else {
|
||||
_addItem();
|
||||
}
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an item from the list
|
||||
* @param {List.Item} item
|
||||
*/
|
||||
removeItem: function(item) {
|
||||
//Confirm delete
|
||||
var confirmMsg = this.schema.confirmDelete;
|
||||
if (confirmMsg && !confirm(confirmMsg)) return;
|
||||
|
||||
var index = _.indexOf(this.items, item);
|
||||
|
||||
this.items[index].remove();
|
||||
this.items.splice(index, 1);
|
||||
|
||||
if (item.addEventTriggered) {
|
||||
this.trigger('remove', this, item.editor);
|
||||
this.trigger('change', this);
|
||||
}
|
||||
|
||||
if (!this.items.length && !this.Editor.isAsync) this.addItem();
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
var values = _.map(this.items, function(item) {
|
||||
return item.getValue();
|
||||
});
|
||||
|
||||
//Filter empty items
|
||||
return _.without(values, undefined, '');
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.render();
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
if (this.items[0]) this.items[0].editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
var focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });
|
||||
|
||||
if (focusedItem) focusedItem.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Override default remove function in order to remove item views
|
||||
*/
|
||||
remove: function() {
|
||||
_.invoke(this.items, 'remove');
|
||||
|
||||
editors.Base.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Run validation
|
||||
*
|
||||
* @return {Object|Null}
|
||||
*/
|
||||
validate: function() {
|
||||
if (!this.validators) return null;
|
||||
|
||||
//Collect errors
|
||||
var errors = _.map(this.items, function(item) {
|
||||
return item.validate();
|
||||
});
|
||||
|
||||
//Check if any item has errors
|
||||
var hasErrors = _.compact(errors).length ? true : false;
|
||||
if (!hasErrors) return null;
|
||||
|
||||
//If so create a shared error
|
||||
var fieldError = {
|
||||
type: 'list',
|
||||
message: 'Some of the items in the list failed validation',
|
||||
errors: errors
|
||||
};
|
||||
|
||||
return fieldError;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A single item in the list
|
||||
*
|
||||
* @param {editors.List} options.list The List editor instance this item belongs to
|
||||
* @param {Function} options.Editor Editor constructor function
|
||||
* @param {String} options.key Model key
|
||||
* @param {Mixed} options.value Value
|
||||
* @param {Object} options.schema Field schema
|
||||
*/
|
||||
editors.List.Item = Backbone.View.extend({
|
||||
events: {
|
||||
'click [data-action="remove"]': function(event) {
|
||||
event.preventDefault();
|
||||
this.list.removeItem(this);
|
||||
},
|
||||
'keydown input[type=text]': function(event) {
|
||||
if(event.keyCode !== 13) return;
|
||||
event.preventDefault();
|
||||
this.list.addItem();
|
||||
this.list.$list.find("> li:last input").focus();
|
||||
}
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.list = options.list;
|
||||
this.schema = options.schema || this.list.schema;
|
||||
this.value = options.value;
|
||||
this.Editor = options.Editor || editors.Text;
|
||||
this.key = options.key;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
//Create editor
|
||||
this.editor = new this.Editor({
|
||||
key: this.key,
|
||||
schema: this.schema,
|
||||
value: this.value,
|
||||
list: this.list,
|
||||
item: this
|
||||
}).render();
|
||||
|
||||
//Create main element
|
||||
var $el = $(Form.templates[this.schema.listItemTemplate]({
|
||||
editor: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
$el.find('.bbf-tmp').replaceWith(this.editor.el);
|
||||
|
||||
//Replace the entire element so there isn't a wrapper tag
|
||||
this.setElement($el);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
this.editor.blur();
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
this.editor.remove();
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
validate: function() {
|
||||
var value = this.getValue(),
|
||||
formValues = this.list.form ? this.list.form.getValue() : {},
|
||||
validators = this.schema.validators,
|
||||
getValidator = Form.helpers.getValidator;
|
||||
|
||||
if (!validators) return null;
|
||||
|
||||
//Run through validators until an error is found
|
||||
var error = null;
|
||||
_.every(validators, function(validator) {
|
||||
error = getValidator(validator)(value, formValues);
|
||||
|
||||
return error ? false : true;
|
||||
});
|
||||
|
||||
//Show/hide error
|
||||
if (error){
|
||||
this.setError(error);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
//Return error to be aggregated by list
|
||||
return error ? error : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a validation error
|
||||
*/
|
||||
setError: function(err) {
|
||||
this.$el.addClass(Form.classNames.error);
|
||||
this.$el.attr('title', err.message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide validation errors
|
||||
*/
|
||||
clearError: function() {
|
||||
this.$el.removeClass(Form.classNames.error);
|
||||
this.$el.attr('title', null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Modal object editor for use with the List editor.
|
||||
* To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
|
||||
*/
|
||||
editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
|
||||
events: {
|
||||
'click': 'openEditor'
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
|
||||
* @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
|
||||
* @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
|
||||
* @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
|
||||
*/
|
||||
initialize: function(options) {
|
||||
editors.Base.prototype.initialize.call(this, options);
|
||||
|
||||
var schema = this.schema;
|
||||
|
||||
//Dependencies
|
||||
if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
|
||||
|
||||
//Get nested schema if Object
|
||||
if (schema.itemType === 'Object') {
|
||||
if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
|
||||
|
||||
this.nestedSchema = schema.subSchema;
|
||||
}
|
||||
|
||||
//Get nested schema if NestedModel
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
if (!schema.model) throw 'Missing required option "schema.model"';
|
||||
|
||||
this.nestedSchema = schema.model.prototype.schema;
|
||||
if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the list item representation
|
||||
*/
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
//New items in the list are only rendered when the editor has been OK'd
|
||||
if (_.isEmpty(this.value)) {
|
||||
this.openEditor();
|
||||
}
|
||||
|
||||
//But items with values are added automatically
|
||||
else {
|
||||
this.renderSummary();
|
||||
|
||||
setTimeout(function() {
|
||||
self.trigger('readyToAdd');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the list item representation
|
||||
*/
|
||||
renderSummary: function() {
|
||||
var template = Form.templates['list.Modal'];
|
||||
|
||||
this.$el.html(template({
|
||||
summary: this.getStringValue()
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Function which returns a generic string representation of an object
|
||||
*
|
||||
* @param {Object} value
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
itemToString: function(value) {
|
||||
value = value || {};
|
||||
|
||||
//Pretty print the object keys and values
|
||||
var parts = [];
|
||||
_.each(this.nestedSchema, function(schema, key) {
|
||||
var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
|
||||
val = value[key];
|
||||
|
||||
if (_.isUndefined(val) || _.isNull(val)) val = '';
|
||||
|
||||
parts.push(desc + ': ' + val);
|
||||
});
|
||||
|
||||
return parts.join('<br />');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the string representation of the object value
|
||||
*/
|
||||
getStringValue: function() {
|
||||
var schema = this.schema,
|
||||
value = this.getValue();
|
||||
|
||||
if (_.isEmpty(value)) return '[Empty]';
|
||||
|
||||
//If there's a specified toString use that
|
||||
if (schema.itemToString) return schema.itemToString(value);
|
||||
|
||||
//Otherwise check if it's NestedModel with it's own toString() method
|
||||
if (schema.itemType === 'NestedModel') {
|
||||
return new (schema.model)(value).toString();
|
||||
}
|
||||
|
||||
//Otherwise use the generic method or custom overridden method
|
||||
return this.itemToString(value);
|
||||
},
|
||||
|
||||
openEditor: function() {
|
||||
var self = this;
|
||||
|
||||
var form = new Form({
|
||||
schema: this.nestedSchema,
|
||||
data: this.value
|
||||
});
|
||||
|
||||
var modal = this.modal = new Backbone.BootstrapModal({
|
||||
content: form,
|
||||
animate: true
|
||||
}).open();
|
||||
|
||||
this.trigger('open', this);
|
||||
this.trigger('focus', this);
|
||||
|
||||
modal.on('cancel', function() {
|
||||
this.modal = null;
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
}, this);
|
||||
|
||||
modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the user clicks 'OK'.
|
||||
* Runs validation and tells the list when ready to add the item
|
||||
*/
|
||||
onModalSubmitted: function(form, modal) {
|
||||
var isNew = !this.value;
|
||||
|
||||
//Stop if there are validation errors
|
||||
var error = form.validate();
|
||||
if (error) return modal.preventClose();
|
||||
this.modal = null;
|
||||
|
||||
//If OK, render the list item
|
||||
this.value = form.getValue();
|
||||
|
||||
this.renderSummary();
|
||||
|
||||
if (isNew) this.trigger('readyToAdd');
|
||||
|
||||
this.trigger('change', this);
|
||||
|
||||
this.trigger('close', this);
|
||||
this.trigger('blur', this);
|
||||
},
|
||||
|
||||
getValue: function() {
|
||||
return this.value;
|
||||
},
|
||||
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
this.openEditor();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
if (this.modal) {
|
||||
this.modal.trigger('cancel');
|
||||
this.modal.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
//STATICS
|
||||
|
||||
//The modal adapter that creates and manages the modal dialog.
|
||||
//Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
|
||||
//Can be replaced with another adapter that implements the same interface.
|
||||
ModalAdapter: Backbone.BootstrapModal,
|
||||
|
||||
//Make the wait list for the 'ready' event before adding the item to the list
|
||||
isAsync: true
|
||||
});
|
||||
|
||||
})();
|
258
sites/all/modules/civicrm/packages/backbone-forms/src/field.js
Normal file
258
sites/all/modules/civicrm/packages/backbone-forms/src/field.js
Normal file
|
@ -0,0 +1,258 @@
|
|||
|
||||
//==================================================================================================
|
||||
//FIELD
|
||||
//==================================================================================================
|
||||
|
||||
Form.Field = (function() {
|
||||
|
||||
var helpers = Form.helpers,
|
||||
templates = Form.templates;
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
/**
|
||||
* @param {Object} Options
|
||||
* Required:
|
||||
* key {String} : The model attribute key
|
||||
* Optional:
|
||||
* schema {Object} : Schema for the field
|
||||
* value {Mixed} : Pass value when not using a model. Use getValue() to get out value
|
||||
* model {Backbone.Model} : Use instead of value, and use commit().
|
||||
* idPrefix {String} : Prefix to add to the editor DOM element's ID
|
||||
*/
|
||||
/**
|
||||
* Creates a new field
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Object} [options.schema] Field schema. Defaults to { type: 'Text' }
|
||||
* @param {Model} [options.model] Model the field relates to. Required if options.data is not set.
|
||||
* @param {String} [options.key] Model key/attribute the field relates to.
|
||||
* @param {Mixed} [options.value] Field value. Required if options.model is not set.
|
||||
* @param {String} [options.idPrefix] Prefix for the editor ID. By default, the model's CID is used.
|
||||
*
|
||||
* @return {Field}
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
this.form = options.form;
|
||||
this.key = options.key;
|
||||
this.value = options.value;
|
||||
this.model = options.model;
|
||||
|
||||
//Turn schema shorthand notation (e.g. 'Text') into schema object
|
||||
if (_.isString(options.schema)) options.schema = { type: options.schema };
|
||||
|
||||
//Set schema defaults
|
||||
this.schema = _.extend({
|
||||
type: 'Text',
|
||||
title: helpers.keyToTitle(this.key),
|
||||
template: 'field'
|
||||
}, options.schema);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Provides the context for rendering the field
|
||||
* Override this to extend the default context
|
||||
*
|
||||
* @param {Object} schema
|
||||
* @param {View} editor
|
||||
*
|
||||
* @return {Object} Locals passed to the template
|
||||
*/
|
||||
renderingContext: function(schema, editor) {
|
||||
return {
|
||||
key: this.key,
|
||||
title: schema.title,
|
||||
id: editor.id,
|
||||
type: schema.type,
|
||||
editor: '<b class="bbf-tmp-editor"></b>',
|
||||
help: '<b class="bbf-tmp-help"></b>',
|
||||
error: '<b class="bbf-tmp-error"></b>'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Renders the field
|
||||
*/
|
||||
render: function() {
|
||||
var schema = this.schema,
|
||||
templates = Form.templates;
|
||||
|
||||
//Standard options that will go to all editors
|
||||
var options = {
|
||||
form: this.form,
|
||||
key: this.key,
|
||||
schema: schema,
|
||||
idPrefix: this.options.idPrefix,
|
||||
id: this.getId()
|
||||
};
|
||||
|
||||
//Decide on data delivery type to pass to editors
|
||||
if (this.model) {
|
||||
options.model = this.model;
|
||||
} else {
|
||||
options.value = this.value;
|
||||
}
|
||||
|
||||
//Decide on the editor to use
|
||||
var editor = this.editor = helpers.createEditor(schema.type, options);
|
||||
|
||||
//Create the element
|
||||
var $field = $(templates[schema.template](this.renderingContext(schema, editor)));
|
||||
|
||||
//Remove <label> if it's not wanted
|
||||
if (schema.title === false) {
|
||||
$field.find('label[for="'+editor.id+'"]').first().remove();
|
||||
}
|
||||
|
||||
//Render editor
|
||||
$field.find('.bbf-tmp-editor').replaceWith(editor.render().el);
|
||||
|
||||
//Set help text
|
||||
this.$help = $('.bbf-tmp-help', $field).parent();
|
||||
this.$help.empty();
|
||||
if (this.schema.help) this.$help.html(this.schema.help);
|
||||
|
||||
//Create error container
|
||||
this.$error = $($('.bbf-tmp-error', $field).parent()[0]);
|
||||
if (this.$error) this.$error.empty();
|
||||
|
||||
//Add custom CSS class names
|
||||
if (this.schema.fieldClass) $field.addClass(this.schema.fieldClass);
|
||||
|
||||
//Add custom attributes
|
||||
if (this.schema.fieldAttrs) $field.attr(this.schema.fieldAttrs);
|
||||
|
||||
//Replace the generated wrapper tag
|
||||
this.setElement($field);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the ID that will be assigned to the editor
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getId: function() {
|
||||
var prefix = this.options.idPrefix,
|
||||
id = this.key;
|
||||
|
||||
//Replace periods with underscores (e.g. for when using paths)
|
||||
id = id.replace(/\./g, '_');
|
||||
|
||||
//If a specific ID prefix is set, use it
|
||||
if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
|
||||
if (_.isNull(prefix)) return id;
|
||||
|
||||
//Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
|
||||
if (this.model) return this.model.cid + '_' + id;
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the validity of the field
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
validate: function() {
|
||||
var error = this.editor.validate();
|
||||
|
||||
if (error) {
|
||||
this.setError(error.message);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the field into an error state, adding the error class and setting the error message
|
||||
*
|
||||
* @param {String} msg Error message
|
||||
*/
|
||||
setError: function(msg) {
|
||||
//Object and NestedModel types set their own errors internally
|
||||
if (this.editor.hasNestedForm) return;
|
||||
|
||||
var errClass = Form.classNames.error;
|
||||
|
||||
this.$el.addClass(errClass);
|
||||
|
||||
if (this.$error) {
|
||||
this.$error.html(msg);
|
||||
} else if (this.$help) {
|
||||
this.$help.html(msg);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the error state and reset the help message
|
||||
*/
|
||||
clearError: function() {
|
||||
var errClass = Form.classNames.error;
|
||||
|
||||
this.$el.removeClass(errClass);
|
||||
|
||||
// some fields (e.g., Hidden), may not have a help el
|
||||
if (this.$error) {
|
||||
this.$error.empty();
|
||||
} else if (this.$help) {
|
||||
this.$help.empty();
|
||||
|
||||
//Reset help text if available
|
||||
var helpMsg = this.schema.help;
|
||||
if (helpMsg) this.$help.html(helpMsg);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the model with the new value from the editor
|
||||
*/
|
||||
commit: function() {
|
||||
return this.editor.commit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the value from the editor
|
||||
*
|
||||
* @return {Mixed}
|
||||
*/
|
||||
getValue: function() {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set/change the value of the editor
|
||||
*
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
setValue: function(value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
this.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the field and editor views
|
||||
*/
|
||||
remove: function() {
|
||||
this.editor.remove();
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})();
|
389
sites/all/modules/civicrm/packages/backbone-forms/src/form.js
Normal file
389
sites/all/modules/civicrm/packages/backbone-forms/src/form.js
Normal file
|
@ -0,0 +1,389 @@
|
|||
|
||||
//==================================================================================================
|
||||
//FORM
|
||||
//==================================================================================================
|
||||
|
||||
var Form = (function() {
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
hasFocus: false,
|
||||
|
||||
/**
|
||||
* Creates a new form
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Model} [options.model] Model the form relates to. Required if options.data is not set
|
||||
* @param {Object} [options.data] Date to populate the form. Required if options.model is not set
|
||||
* @param {String[]} [options.fields] Fields to include in the form, in order
|
||||
* @param {String[]|Object[]} [options.fieldsets] How to divide the fields up by section. E.g. [{ legend: 'Title', fields: ['field1', 'field2'] }]
|
||||
* @param {String} [options.idPrefix] Prefix for editor IDs. By default, the model's CID is used.
|
||||
* @param {String} [options.template] Form template key/name
|
||||
* @param {String} [options.fieldsetTemplate] Fieldset template key/name
|
||||
* @param {String} [options.fieldTemplate] Field template key/name
|
||||
*
|
||||
* @return {Form}
|
||||
*/
|
||||
initialize: function(options) {
|
||||
//Check templates have been loaded
|
||||
if (!Form.templates.form) throw new Error('Templates not loaded');
|
||||
|
||||
//Get the schema
|
||||
this.schema = (function() {
|
||||
if (options.schema) return options.schema;
|
||||
|
||||
var model = options.model;
|
||||
if (!model) throw new Error('Could not find schema');
|
||||
|
||||
if (_.isFunction(model.schema)) return model.schema();
|
||||
|
||||
return model.schema;
|
||||
})();
|
||||
|
||||
//Option defaults
|
||||
options = _.extend({
|
||||
template: 'form',
|
||||
fieldsetTemplate: 'fieldset',
|
||||
fieldTemplate: 'field'
|
||||
}, options);
|
||||
|
||||
//Determine fieldsets
|
||||
if (!options.fieldsets) {
|
||||
var fields = options.fields || _.keys(this.schema);
|
||||
|
||||
options.fieldsets = [{ fields: fields }];
|
||||
}
|
||||
|
||||
//Store main attributes
|
||||
this.options = options;
|
||||
this.model = options.model;
|
||||
this.data = options.data;
|
||||
this.fields = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the form and all fields
|
||||
*/
|
||||
render: function() {
|
||||
var self = this,
|
||||
options = this.options,
|
||||
template = Form.templates[options.template];
|
||||
|
||||
//Create el from template
|
||||
var $form = $(template({
|
||||
fieldsets: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
//Render fieldsets
|
||||
var $fieldsetContainer = $('.bbf-tmp', $form);
|
||||
|
||||
_.each(options.fieldsets, function(fieldset) {
|
||||
$fieldsetContainer.append(self.renderFieldset(fieldset));
|
||||
});
|
||||
|
||||
$fieldsetContainer.children().unwrap();
|
||||
|
||||
//Set the template contents as the main element; removes the wrapper element
|
||||
this.setElement($form);
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a fieldset and the fields within it
|
||||
*
|
||||
* Valid fieldset definitions:
|
||||
* ['field1', 'field2']
|
||||
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
|
||||
*
|
||||
* @param {Object|Array} fieldset A fieldset definition
|
||||
*
|
||||
* @return {jQuery} The fieldset DOM element
|
||||
*/
|
||||
renderFieldset: function(fieldset) {
|
||||
var self = this,
|
||||
template = Form.templates[this.options.fieldsetTemplate],
|
||||
schema = this.schema,
|
||||
getNested = Form.helpers.getNested;
|
||||
|
||||
//Normalise to object
|
||||
if (_.isArray(fieldset)) {
|
||||
fieldset = { fields: fieldset };
|
||||
}
|
||||
|
||||
//Concatenating HTML as strings won't work so we need to insert field elements into a placeholder
|
||||
var $fieldset = $(template(_.extend({}, fieldset, {
|
||||
legend: '<b class="bbf-tmp-legend"></b>',
|
||||
fields: '<b class="bbf-tmp-fields"></b>'
|
||||
})));
|
||||
|
||||
//Set legend
|
||||
if (fieldset.legend) {
|
||||
$fieldset.find('.bbf-tmp-legend').replaceWith(fieldset.legend);
|
||||
}
|
||||
//or remove the containing tag if there isn't a legend
|
||||
else {
|
||||
$fieldset.find('.bbf-tmp-legend').parent().remove();
|
||||
}
|
||||
|
||||
var $fieldsContainer = $('.bbf-tmp-fields', $fieldset);
|
||||
|
||||
//Render fields
|
||||
_.each(fieldset.fields, function(key) {
|
||||
//Get the field schema
|
||||
var itemSchema = (function() {
|
||||
//Return a normal key or path key
|
||||
if (schema[key]) return schema[key];
|
||||
|
||||
//Return a nested schema, i.e. Object
|
||||
var path = key.replace(/\./g, '.subSchema.');
|
||||
return getNested(schema, path);
|
||||
})();
|
||||
|
||||
if (!itemSchema) throw "Field '"+key+"' not found in schema";
|
||||
|
||||
//Create the field
|
||||
var field = self.fields[key] = self.createField(key, itemSchema);
|
||||
|
||||
//Render the fields with editors, apart from Hidden fields
|
||||
var fieldEl = field.render().el;
|
||||
|
||||
field.editor.on('all', function(event) {
|
||||
// args = ["change", editor]
|
||||
var args = _.toArray(arguments);
|
||||
args[0] = key + ':' + event;
|
||||
args.splice(1, 0, this);
|
||||
// args = ["key:change", this=form, editor]
|
||||
|
||||
this.trigger.apply(this, args);
|
||||
}, self);
|
||||
|
||||
field.editor.on('change', function() {
|
||||
this.trigger('change', self);
|
||||
}, self);
|
||||
|
||||
field.editor.on('focus', function() {
|
||||
if (this.hasFocus) return;
|
||||
this.trigger('focus', this);
|
||||
}, self);
|
||||
field.editor.on('blur', function() {
|
||||
if (!this.hasFocus) return;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (_.find(self.fields, function(field) { return field.editor.hasFocus; })) return;
|
||||
self.trigger('blur', self);
|
||||
}, 0);
|
||||
}, self);
|
||||
|
||||
if (itemSchema.type !== 'Hidden') {
|
||||
$fieldsContainer.append(fieldEl);
|
||||
}
|
||||
});
|
||||
|
||||
$fieldsContainer = $fieldsContainer.children().unwrap();
|
||||
|
||||
return $fieldset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a field and returns it
|
||||
*
|
||||
* @param {String} key The key for the field in the form schema
|
||||
* @param {Object} schema Field schema
|
||||
*
|
||||
* @return {Field} The field view
|
||||
*/
|
||||
createField: function(key, schema) {
|
||||
schema.template = schema.template || this.options.fieldTemplate;
|
||||
|
||||
var options = {
|
||||
form: this,
|
||||
key: key,
|
||||
schema: schema,
|
||||
idPrefix: this.options.idPrefix,
|
||||
template: this.options.fieldTemplate
|
||||
};
|
||||
|
||||
if (this.model) {
|
||||
options.model = this.model;
|
||||
} else if (this.data) {
|
||||
options.value = this.data[key];
|
||||
} else {
|
||||
options.value = null;
|
||||
}
|
||||
|
||||
return new Form.Field(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate the data
|
||||
*
|
||||
* @return {Object} Validation errors
|
||||
*/
|
||||
validate: function() {
|
||||
var self = this,
|
||||
fields = this.fields,
|
||||
model = this.model,
|
||||
errors = {};
|
||||
|
||||
//Collect errors from schema validation
|
||||
_.each(fields, function(field) {
|
||||
var error = field.validate();
|
||||
if (error) {
|
||||
errors[field.key] = error;
|
||||
}
|
||||
});
|
||||
|
||||
//Get errors from default Backbone model validator
|
||||
if (model && model.validate) {
|
||||
var modelErrors = model.validate(this.getValue());
|
||||
|
||||
if (modelErrors) {
|
||||
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
|
||||
|
||||
//If errors are not in object form then just store on the error object
|
||||
if (!isDictionary) {
|
||||
errors._others = errors._others || [];
|
||||
errors._others.push(modelErrors);
|
||||
}
|
||||
|
||||
//Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
|
||||
if (isDictionary) {
|
||||
_.each(modelErrors, function(val, key) {
|
||||
//Set error on field if there isn't one already
|
||||
if (self.fields[key] && !errors[key]) {
|
||||
self.fields[key].setError(val);
|
||||
errors[key] = val;
|
||||
}
|
||||
|
||||
else {
|
||||
//Otherwise add to '_others' key
|
||||
errors._others = errors._others || [];
|
||||
var tmpErr = {};
|
||||
tmpErr[key] = val;
|
||||
errors._others.push(tmpErr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the model with all latest values.
|
||||
*
|
||||
* @return {Object} Validation errors
|
||||
*/
|
||||
commit: function() {
|
||||
//Validate
|
||||
var errors = this.validate();
|
||||
if (errors) return errors;
|
||||
|
||||
//Commit
|
||||
var modelError;
|
||||
this.model.set(this.getValue(), {
|
||||
error: function(model, e) {
|
||||
modelError = e;
|
||||
}
|
||||
});
|
||||
|
||||
if (modelError) return modelError;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all the field values as an object.
|
||||
* Use this method when passing data instead of objects
|
||||
*
|
||||
* @param {String} [key] Specific field value to get
|
||||
*/
|
||||
getValue: function(key) {
|
||||
//Return only given key if specified
|
||||
if (key) return this.fields[key].getValue();
|
||||
|
||||
//Otherwise return entire form
|
||||
var values = {};
|
||||
_.each(this.fields, function(field) {
|
||||
values[field.key] = field.getValue();
|
||||
});
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update field values, referenced by key
|
||||
* @param {Object|String} key New values to set, or property to set
|
||||
* @param val Value to set
|
||||
*/
|
||||
setValue: function(prop, val) {
|
||||
var data = {};
|
||||
if (typeof prop === 'string') {
|
||||
data[prop] = val;
|
||||
} else {
|
||||
data = prop;
|
||||
}
|
||||
|
||||
var key;
|
||||
for (key in this.schema) {
|
||||
if (data[key] !== undefined) {
|
||||
this.fields[key].setValue(data[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
var fieldset = this.options.fieldsets[0];
|
||||
if (fieldset) {
|
||||
var field;
|
||||
if (_.isArray(fieldset)) {
|
||||
field = fieldset[0];
|
||||
}
|
||||
else {
|
||||
field = fieldset.fields[0];
|
||||
}
|
||||
if (field) {
|
||||
this.fields[field].editor.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
var focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
|
||||
|
||||
if (focusedField) focusedField.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Override default remove function in order to remove embedded views
|
||||
*/
|
||||
remove: function() {
|
||||
var fields = this.fields;
|
||||
|
||||
for (var key in fields) {
|
||||
fields[key].remove();
|
||||
}
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
|
||||
trigger: function(event) {
|
||||
if (event === 'focus') {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
else if (event === 'blur') {
|
||||
this.hasFocus = false;
|
||||
}
|
||||
|
||||
return Backbone.View.prototype.trigger.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
212
sites/all/modules/civicrm/packages/backbone-forms/src/helpers.js
Normal file
212
sites/all/modules/civicrm/packages/backbone-forms/src/helpers.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
|
||||
//==================================================================================================
|
||||
//HELPERS
|
||||
//==================================================================================================
|
||||
|
||||
Form.helpers = (function() {
|
||||
|
||||
var helpers = {};
|
||||
|
||||
/**
|
||||
* Gets a nested attribute using a path e.g. 'user.name'
|
||||
*
|
||||
* @param {Object} obj Object to fetch attribute from
|
||||
* @param {String} path Attribute path e.g. 'user.name'
|
||||
* @return {Mixed}
|
||||
* @api private
|
||||
*/
|
||||
helpers.getNested = function(obj, path) {
|
||||
var fields = path.split(".");
|
||||
var result = obj;
|
||||
for (var i = 0, n = fields.length; i < n; i++) {
|
||||
result = result[fields[i]];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is used to transform the key from a schema into the title used in a label.
|
||||
* (If a specific title is provided it will be used instead).
|
||||
*
|
||||
* By default this converts a camelCase string into words, i.e. Camel Case
|
||||
* If you have a different naming convention for schema keys, replace this function.
|
||||
*
|
||||
* @param {String} Key
|
||||
* @return {String} Title
|
||||
*/
|
||||
helpers.keyToTitle = function(str) {
|
||||
//Add spaces
|
||||
str = str.replace(/([A-Z])/g, ' $1');
|
||||
|
||||
//Uppercase first character
|
||||
str = str.replace(/^./, function(str) { return str.toUpperCase(); });
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to compile a template with the {{mustache}} style tags. Template settings are reset
|
||||
* to user's settings when done to avoid conflicts.
|
||||
* @param {String} Template string
|
||||
* @return {Template} Compiled template
|
||||
*/
|
||||
helpers.compileTemplate = function(str) {
|
||||
//Store user's template options
|
||||
var _interpolateBackup = _.templateSettings.interpolate;
|
||||
|
||||
//Set custom template settings
|
||||
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
|
||||
|
||||
var template = _.template(str);
|
||||
|
||||
//Reset to users' template settings
|
||||
_.templateSettings.interpolate = _interpolateBackup;
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to create a template with the {{mustache}} style tags.
|
||||
* If context is passed in, the template will be evaluated.
|
||||
* @param {String} Template string
|
||||
* @param {Object} Optional; values to replace in template
|
||||
* @return {Template|String} Compiled template or the evaluated string
|
||||
*/
|
||||
helpers.createTemplate = function(str, context) {
|
||||
var template = helpers.compileTemplate(str);
|
||||
|
||||
if (!context) {
|
||||
return template;
|
||||
} else {
|
||||
return template(context);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the template compiler to the given function
|
||||
* @param {Function} Template compiler function
|
||||
*/
|
||||
helpers.setTemplateCompiler = function(compiler) {
|
||||
helpers.compileTemplate = compiler;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the templates to be used.
|
||||
*
|
||||
* If the templates passed in are strings, they will be compiled, expecting Mustache style tags,
|
||||
* i.e. <div>{{varName}}</div>
|
||||
*
|
||||
* You can also pass in previously compiled Underscore templates, in which case you can use any style
|
||||
* tags.
|
||||
*
|
||||
* @param {Object} templates
|
||||
* @param {Object} classNames
|
||||
*/
|
||||
helpers.setTemplates = function(templates, classNames) {
|
||||
var createTemplate = helpers.createTemplate;
|
||||
|
||||
Form.templates = Form.templates || {};
|
||||
Form.classNames = Form.classNames || {};
|
||||
|
||||
//Set templates, compiling them if necessary
|
||||
_.each(templates, function(template, key, index) {
|
||||
if (_.isString(template)) template = createTemplate(template);
|
||||
|
||||
Form.templates[key] = template;
|
||||
});
|
||||
|
||||
//Set class names
|
||||
_.extend(Form.classNames, classNames);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the editor constructor for a given schema 'type'.
|
||||
* Accepts strings for the default editors, or the reference to the constructor function
|
||||
* for custom editors
|
||||
*
|
||||
* @param {String|Function} The schema type e.g. 'Text', 'Select', or the editor constructor e.g. editors.Date
|
||||
* @param {Object} Options to pass to editor, including required 'key', 'schema'
|
||||
* @return {Mixed} An instance of the mapped editor
|
||||
*/
|
||||
helpers.createEditor = function(schemaType, options) {
|
||||
var constructorFn;
|
||||
|
||||
if (_.isString(schemaType)) {
|
||||
constructorFn = Form.editors[schemaType];
|
||||
} else {
|
||||
constructorFn = schemaType;
|
||||
}
|
||||
|
||||
return new constructorFn(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers an event that can be cancelled. Requires the user to invoke a callback. If false
|
||||
* is passed to the callback, the action does not run.
|
||||
*
|
||||
* NOTE: This helper uses private Backbone apis so can break when Backbone is upgraded
|
||||
*
|
||||
* @param {Mixed} Instance of Backbone model, view, collection to trigger event on
|
||||
* @param {String} Event name
|
||||
* @param {Array} Arguments to pass to the event handlers
|
||||
* @param {Function} Callback to run after the event handler has run.
|
||||
* If any of them passed false or error, this callback won't run
|
||||
*/
|
||||
helpers.triggerCancellableEvent = function(subject, event, args, callback) {
|
||||
//Return if there are no event listeners
|
||||
if (!subject._callbacks || !subject._callbacks[event]) return callback();
|
||||
|
||||
var next = subject._callbacks[event].next;
|
||||
if (!next) return callback();
|
||||
|
||||
var fn = next.callback,
|
||||
context = next.context || this;
|
||||
|
||||
//Add the callback that will be used when done
|
||||
args.push(callback);
|
||||
|
||||
fn.apply(context, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a validation function based on the type defined in the schema
|
||||
*
|
||||
* @param {RegExp|String|Function} validator
|
||||
* @return {Function}
|
||||
*/
|
||||
helpers.getValidator = function(validator) {
|
||||
var validators = Form.validators;
|
||||
|
||||
//Convert regular expressions to validators
|
||||
if (_.isRegExp(validator)) {
|
||||
return validators.regexp({ regexp: validator });
|
||||
}
|
||||
|
||||
//Use a built-in validator if given a string
|
||||
if (_.isString(validator)) {
|
||||
if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
|
||||
|
||||
return validators[validator]();
|
||||
}
|
||||
|
||||
//Functions can be used directly
|
||||
if (_.isFunction(validator)) return validator;
|
||||
|
||||
//Use a customised built-in validator if given an object
|
||||
if (_.isObject(validator) && validator.type) {
|
||||
var config = validator;
|
||||
|
||||
return validators[config.type](config);
|
||||
}
|
||||
|
||||
//Unkown validator type
|
||||
throw new Error('Invalid validator: ' + validator);
|
||||
};
|
||||
|
||||
|
||||
return helpers;
|
||||
|
||||
})();
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
//SETUP
|
||||
|
||||
//Add function shortcuts
|
||||
Form.setTemplates = Form.helpers.setTemplates;
|
||||
Form.setTemplateCompiler = Form.helpers.setTemplateCompiler;
|
||||
|
||||
Form.templates = {};
|
43
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.css
vendored
Normal file
43
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.css
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list .bbf-add {
|
||||
margin-top: -10px
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.bbf-list .bbf-del {
|
||||
margin-left: 4px
|
||||
}
|
||||
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
80
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.js
vendored
Normal file
80
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
|
||||
//TWITTER BOOTSTRAP TEMPLATES
|
||||
//Requires Bootstrap 2.x
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="form-horizontal">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
{{fields}}\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<div class="control-group field-{{key}}">\
|
||||
<label class="control-label" for="{{id}}">{{title}}</label>\
|
||||
<div class="controls">\
|
||||
{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<div class="field-{{key}}">\
|
||||
<div title="{{title}}" class="input-xlarge">{{editor}}\
|
||||
<div class="help-inline">{{error}}</div>\
|
||||
</div>\
|
||||
<div class="help-block">{{help}}</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul class="unstyled clearfix">{{items}}</ul>\
|
||||
<button class="btn bbf-add" data-action="add">Add</button>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li class="clearfix">\
|
||||
<div class="pull-left">{{editor}}</div>\
|
||||
<button type="button" class="btn bbf-del" data-action="remove">×</button>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<p>{{date}}</p>\
|
||||
<p>\
|
||||
<select data-type="hour" style="width: 4em">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min" style="width: 4em">{{mins}}</select>\
|
||||
</p>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'error' //Set on the field tag when validation fails
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
/* Form */
|
||||
.bbf-form {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
/* Field */
|
||||
.bbf-field {
|
||||
margin: 1em 0;
|
||||
list-style-type: none;
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bbf-field label {
|
||||
float: left;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-editor {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
}
|
||||
|
||||
.bbf-field input, .bbf-field textarea, .bbf-field select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-help {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bbf-field .bbf-error {
|
||||
margin-left: 25%;
|
||||
width: 74%;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bbf-field.bbf-error .bbf-editor {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
|
||||
/* Radio */
|
||||
.bbf-radio {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-radio input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-radio label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* Checkboxes */
|
||||
.bbf-checkboxes {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bbf-checkboxes input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.bbf-checkboxes label {
|
||||
float: none;
|
||||
}
|
||||
|
||||
|
||||
/* List */
|
||||
.bbf-list ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-error {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.bbf-list li {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-editor-container {
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.bbf-list li .bbf-remove {
|
||||
float: right;
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.bbf-list .bbf-actions {
|
||||
text-align: center;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* List.Modal */
|
||||
.bbf-list-modal {
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
width: 208px;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Date */
|
||||
.bbf-date .bbf-date {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-month {
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.bbf-date .bbf-year {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
|
||||
/* DateTime */
|
||||
.bbf-datetime .bbf-date-container {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bbf-datetime select {
|
||||
width: 4em;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
//DEFAULT TEMPLATES
|
||||
Form.setTemplates({
|
||||
|
||||
//HTML
|
||||
form: '\
|
||||
<form class="bbf-form">{{fieldsets}}</form>\
|
||||
',
|
||||
|
||||
fieldset: '\
|
||||
<fieldset>\
|
||||
<legend>{{legend}}</legend>\
|
||||
<ul>{{fields}}</ul>\
|
||||
</fieldset>\
|
||||
',
|
||||
|
||||
field: '\
|
||||
<li class="bbf-field field-{{key}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
nestedField: '\
|
||||
<li class="bbf-field bbf-nested-field field-{{key}}" title="{{title}}">\
|
||||
<label for="{{id}}">{{title}}</label>\
|
||||
<div class="bbf-editor">{{editor}}</div>\
|
||||
<div class="bbf-help">{{help}}</div>\
|
||||
<div class="bbf-error">{{error}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
list: '\
|
||||
<div class="bbf-list">\
|
||||
<ul>{{items}}</ul>\
|
||||
<div class="bbf-actions"><button type="button" data-action="add">Add</div>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
listItem: '\
|
||||
<li>\
|
||||
<button type="button" data-action="remove" class="bbf-remove">×</button>\
|
||||
<div class="bbf-editor-container">{{editor}}</div>\
|
||||
</li>\
|
||||
',
|
||||
|
||||
date: '\
|
||||
<div class="bbf-date">\
|
||||
<select data-type="date" class="bbf-date">{{dates}}</select>\
|
||||
<select data-type="month" class="bbf-month">{{months}}</select>\
|
||||
<select data-type="year" class="bbf-year">{{years}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
dateTime: '\
|
||||
<div class="bbf-datetime">\
|
||||
<div class="bbf-date-container">{{date}}</div>\
|
||||
<select data-type="hour">{{hours}}</select>\
|
||||
:\
|
||||
<select data-type="min">{{mins}}</select>\
|
||||
</div>\
|
||||
',
|
||||
|
||||
'list.Modal': '\
|
||||
<div class="bbf-list-modal">\
|
||||
{{summary}}\
|
||||
</div>\
|
||||
'
|
||||
}, {
|
||||
|
||||
//CLASSNAMES
|
||||
error: 'bbf-error'
|
||||
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
|
||||
//==================================================================================================
|
||||
//VALIDATORS
|
||||
//==================================================================================================
|
||||
|
||||
Form.validators = (function() {
|
||||
|
||||
var validators = {};
|
||||
|
||||
validators.errMessages = {
|
||||
required: 'Required',
|
||||
regexp: 'Invalid',
|
||||
email: 'Invalid email address',
|
||||
url: 'Invalid URL',
|
||||
match: 'Must match field "{{field}}"'
|
||||
};
|
||||
|
||||
validators.required = function(options) {
|
||||
options = _.extend({
|
||||
type: 'required',
|
||||
message: this.errMessages.required
|
||||
}, options);
|
||||
|
||||
return function required(value) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
if (value === null || value === undefined || value === false || value === '') return err;
|
||||
};
|
||||
};
|
||||
|
||||
validators.regexp = function(options) {
|
||||
if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
|
||||
|
||||
options = _.extend({
|
||||
type: 'regexp',
|
||||
message: this.errMessages.regexp
|
||||
}, options);
|
||||
|
||||
return function regexp(value) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
//Don't check empty values (add a 'required' validator for this)
|
||||
if (value === null || value === undefined || value === '') return;
|
||||
|
||||
if (!options.regexp.test(value)) return err;
|
||||
};
|
||||
};
|
||||
|
||||
validators.email = function(options) {
|
||||
options = _.extend({
|
||||
type: 'email',
|
||||
message: this.errMessages.email,
|
||||
regexp: /^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/
|
||||
}, options);
|
||||
|
||||
return validators.regexp(options);
|
||||
};
|
||||
|
||||
validators.url = function(options) {
|
||||
options = _.extend({
|
||||
type: 'url',
|
||||
message: this.errMessages.url,
|
||||
regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
|
||||
}, options);
|
||||
|
||||
return validators.regexp(options);
|
||||
};
|
||||
|
||||
validators.match = function(options) {
|
||||
if (!options.field) throw new Error('Missing required "field" options for "match" validator');
|
||||
|
||||
options = _.extend({
|
||||
type: 'match',
|
||||
message: this.errMessages.match
|
||||
}, options);
|
||||
|
||||
return function match(value, attrs) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
//Don't check empty values (add a 'required' validator for this)
|
||||
if (value === null || value === undefined || value === '') return;
|
||||
|
||||
if (value !== attrs[options.field]) return err;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
return validators;
|
||||
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue