backbone-combobox
Advanced tools
Comparing version 0.1.0 to 0.1.1
12
index.js
@@ -1,5 +0,7 @@ | ||
module.exports = { | ||
View: require('./src/comboboxView'), | ||
Model: require('./src/comboboxModel'), | ||
DropdownView: require('./src/dropdownView') | ||
}; | ||
define(function(require, exports, module) { | ||
module.exports = { | ||
View: require('./src/comboboxView'), | ||
Model: require('./src/comboboxModel'), | ||
DropdownView: require('./src/dropdownView') | ||
}; | ||
}); |
{ | ||
"name": "backbone-combobox", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Backbone Combobox component", | ||
@@ -8,3 +8,3 @@ "main": "index.js", | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"build-sass": "node-sass src/style.scss dist/style.css" | ||
"dev": "webpack-dev-server --content-base demo" | ||
}, | ||
@@ -30,5 +30,11 @@ "repository": { | ||
"classnames": "^2.2.5", | ||
"css-loader": "^0.23.1", | ||
"jquery": "^2.2.4", | ||
"lodash": "^4.13.1" | ||
"lodash": "^4.13.1", | ||
"node-sass": "^3.7.0", | ||
"sass-loader": "^3.2.0", | ||
"style-loader": "^0.13.1", | ||
"text-loader": "0.0.1", | ||
"webpack": "^1.13.1" | ||
} | ||
} |
backbone-tree | ||
============= | ||
A node module providing mixins, model and collection which may help working with tree structures. | ||
Backbone based Select component. | ||
@@ -14,2 +14,6 @@ More info soon... | ||
## Sample | ||
![Sample](https://raw.githubusercontent.com/SlideWorx/backbone-combobox/master/demo/combobox.gif) | ||
## Usage | ||
@@ -21,18 +25,18 @@ | ||
// create model instance, fill it with data and select item | ||
var model = new Combobox.Model(); | ||
var model = new Combobox.Model({}); | ||
model.setData([ | ||
{id: '1', text: 'Main'}, | ||
{id: '1.0', text: 'Subelement', parentId: '1'}, | ||
{id: '1.1', text: 'Subelement', parendId: '1'} | ||
{id: '1', text: '1 (title specified)', title: 'Title text'}, | ||
{id: '2', text: '2'}, | ||
{id: '3', text: '3 (disabled)', disabled: true}, | ||
{id: '1.1', text: '1.1', parentId: '1'}, | ||
{id: '1.2', text: '1.2', parentId: '1'} | ||
// ... | ||
]); | ||
model.set('selectedId', '1.1'); | ||
model.set({selectedId: '1.1'}); | ||
// create view instance and pass it model created above | ||
var new Combobox.View({ | ||
model: model | ||
}); | ||
// create view instance and pass it model created above | ||
var view = new Combobox.View({ model: model }); | ||
// render and append | ||
$('#combobox-target').empty().append(this.view.render().el); | ||
view.render().$el.appendTo('#target'); | ||
``` | ||
@@ -42,2 +46,6 @@ | ||
First of all: `npm install` | ||
Then to see example use of combobox please install `webpack-dev-server` and then run `npm run dev`. | ||
Please use [mversion](https://github.com/mikaelbr/mversion) (`npm install -g mversion`) to bump version numbers. | ||
@@ -47,2 +55,9 @@ | ||
* 0.1.0 Initial release | ||
### 0.1.1 (03.06.2016) | ||
* Updated README.md | ||
* Added demo code | ||
* `npm run dev` for development environment | ||
* Changed way of including template files (tpl -> text) | ||
### 0.1.0 (02.06.2016) | ||
Initial release |
@@ -1,208 +0,210 @@ | ||
var Backbone = require('backbone'); | ||
var _ = require('lodash'); | ||
var TreeCollection = require('backbone-tree').Collection; | ||
define(function(require, exports, module) { | ||
var Backbone = require('backbone'); | ||
var _ = require('lodash'); | ||
var TreeCollection = require('backbone-tree').Collection; | ||
var VALIDATE = {validate: true}; | ||
var validateMsgTmpl = _.template( | ||
'Property `<%- prop %>` by convention should contain <%- type %> value (<%- value %>)' | ||
); | ||
var VALIDATE = {validate: true}; | ||
var validateMsgTmpl = _.template( | ||
'Property `<%- prop %>` by convention should contain <%- type %> value (<%- value %>)' | ||
); | ||
var startingWithIsOrHasRegex = /^(is|has).+$/; | ||
var endingWithTextRegex = /^.+Text$/; | ||
var startingWithIsOrHasRegex = /^(is|has).+$/; | ||
var endingWithTextRegex = /^.+Text$/; | ||
module.exports = Backbone.Model.extend({ | ||
defaults: function() { | ||
return { | ||
// Opens or closes dropdowns | ||
isOpen: false, | ||
// Put toggle into loading state. Also causes closing opened dropdown. | ||
isLoading: false, | ||
// disables toggle element. Immediately. | ||
isDisabled: false, | ||
// displays dropdowns animating them (adding specific CSS classes to dropdowns) | ||
isAnimated: true, | ||
// indicates if toggle element should be highlighted | ||
isWarning: false, | ||
// indicates that something wrong has happen | ||
hasError: false, | ||
module.exports = Backbone.Model.extend({ | ||
defaults: function() { | ||
return { | ||
// Opens or closes dropdowns | ||
isOpen: false, | ||
// Put toggle into loading state. Also causes closing opened dropdown. | ||
isLoading: false, | ||
// disables toggle element. Immediately. | ||
isDisabled: false, | ||
// displays dropdowns animating them (adding specific CSS classes to dropdowns) | ||
isAnimated: true, | ||
// indicates if toggle element should be highlighted | ||
isWarning: false, | ||
// indicates that something wrong has happen | ||
hasError: false, | ||
// if hasNotSelectedItem === true, null points to that value | ||
selectedId: null, | ||
// if hasNotSelectedItem === true, null points to that value | ||
selectedId: null, | ||
// creates an item to represent not selected value | ||
hasNotSelectedItem: true, | ||
// this text is displayed as not selected value. More details | ||
notSelectedText: '- select -', | ||
// creates an item to represent not selected value | ||
hasNotSelectedItem: true, | ||
// this text is displayed as not selected value. More details | ||
notSelectedText: '- select -', | ||
// text displayed to user when loading state has been switched on | ||
loadingText: 'loading...', | ||
// text displayed to user when loading state has been switched on | ||
loadingText: 'loading...', | ||
// use this to style toggle and dropdowns. Eg. try 'inline'. | ||
theme: '', | ||
// use this to style toggle and dropdowns. Eg. try 'inline'. | ||
theme: '', | ||
_data: new TreeCollection() | ||
}; | ||
}, | ||
_data: new TreeCollection() | ||
}; | ||
}, | ||
initialize: function() { | ||
// if items has been changed (potentially removed) check if selected item still exists | ||
this.listenTo(this.getData(), 'reset remove update change:id', this.checkIfSelectedIdStillExists); | ||
initialize: function() { | ||
// if items has been changed (potentially removed) check if selected item still exists | ||
this.listenTo(this.getData(), 'reset remove update change:id', this.checkIfSelectedIdStillExists); | ||
// listen to change on disabled property | ||
this.listenTo(this, 'change:isDisabled change:isLoading', this.closeDropdownWhenTrue); | ||
}, | ||
// listen to change on disabled property | ||
this.listenTo(this, 'change:isDisabled change:isLoading', this.closeDropdownWhenTrue); | ||
}, | ||
/* @TODO move to provider */ | ||
validate: function(attrs) { | ||
var errors = []; | ||
/* @TODO move to provider */ | ||
validate: function(attrs) { | ||
var errors = []; | ||
// check conventions | ||
_.forEach(attrs, function(value, property) { | ||
// starting with 'is' or 'has' should be boolean type | ||
if (startingWithIsOrHasRegex.test(property) && !_.isBoolean(value)) { | ||
errors.push(validateMsgTmpl({prop: property, value: value, type: 'boolean'})); | ||
} | ||
// check conventions | ||
_.forEach(attrs, function(value, property) { | ||
// starting with 'is' or 'has' should be boolean type | ||
if (startingWithIsOrHasRegex.test(property) && !_.isBoolean(value)) { | ||
errors.push(validateMsgTmpl({prop: property, value: value, type: 'boolean'})); | ||
} | ||
if (endingWithTextRegex.test(property) && !_.isString(value)) { | ||
errors.push(validateMsgTmpl({prop: property, value: value, type: 'string'})); | ||
if (endingWithTextRegex.test(property) && !_.isString(value)) { | ||
errors.push(validateMsgTmpl({prop: property, value: value, type: 'string'})); | ||
} | ||
}); | ||
if (errors.length) { | ||
return errors; | ||
} | ||
}); | ||
}, | ||
if (errors.length) { | ||
return errors; | ||
} | ||
}, | ||
checkIfSelectedIdStillExists: function() { | ||
var newProps = {hasError: false}; | ||
checkIfSelectedIdStillExists: function() { | ||
var newProps = {hasError: false}; | ||
if (this.get('selectedId')) { | ||
var selectedItem = this.findItem(this.get('selectedId')); | ||
if (this.get('selectedId')) { | ||
var selectedItem = this.findItem(this.get('selectedId')); | ||
if (!selectedItem) { | ||
newProps.hasError = true; | ||
newProps.selectedId = null; | ||
} else { | ||
newProps.hasError = !!selectedItem.get('disabled') | ||
if (!selectedItem) { | ||
newProps.hasError = true; | ||
newProps.selectedId = null; | ||
} else { | ||
newProps.hasError = !!selectedItem.get('disabled') | ||
} | ||
} | ||
} | ||
this.set(newProps, VALIDATE); | ||
}, | ||
this.set(newProps, VALIDATE); | ||
}, | ||
getLabel: function() { | ||
var selectedItem = this.getSelectedItem(); | ||
var label = selectedItem && selectedItem.get('text') ? selectedItem.get('text') : ' '; | ||
getLabel: function() { | ||
var selectedItem = this.getSelectedItem(); | ||
var label = selectedItem && selectedItem.get('text') ? selectedItem.get('text') : ' '; | ||
return this.get('isLoading') && this.get('loadingText') ? this.get('loadingText') : label; | ||
}, | ||
return this.get('isLoading') && this.get('loadingText') ? this.get('loadingText') : label; | ||
}, | ||
closeDropdownWhenTrue: function(model, property) { | ||
if (!!property) { | ||
this.toggleOpen(false); | ||
} | ||
}, | ||
closeDropdownWhenTrue: function(model, property) { | ||
if (!!property) { | ||
this.toggleOpen(false); | ||
} | ||
}, | ||
isLoading: function() { | ||
return this.get('isLoading') === true; | ||
}, | ||
isLoading: function() { | ||
return this.get('isLoading') === true; | ||
}, | ||
hasData: function() { | ||
return !this.getData().isEmpty(); | ||
}, | ||
hasData: function() { | ||
return !this.getData().isEmpty(); | ||
}, | ||
getData: function() { | ||
return this.get('_data'); | ||
}, | ||
getData: function() { | ||
return this.get('_data'); | ||
}, | ||
setData: function(data) { | ||
this.getData().reset(data); | ||
}, | ||
setData: function(data) { | ||
this.getData().reset(data); | ||
}, | ||
findItem: function(searchFor) { | ||
if (this.isLoading()) { | ||
return this.getLoadingItem(); | ||
} | ||
findItem: function(searchFor) { | ||
if (this.isLoading()) { | ||
return this.getLoadingItem(); | ||
} | ||
if (_.isString(searchFor) || _.isNumber(searchFor)) { | ||
return this.getData().get(searchFor); | ||
} | ||
if (_.isString(searchFor) || _.isNumber(searchFor)) { | ||
return this.getData().get(searchFor); | ||
} | ||
// search for item on current item | ||
return this.getData().findWhere(searchFor); | ||
}, | ||
// search for item on current item | ||
return this.getData().findWhere(searchFor); | ||
}, | ||
/** | ||
* Selects item. | ||
* | ||
* @param {string|number|object} id Id of selected item or query which finds item. | ||
* @param {boolean} force Force select item. May be used when you want to select disabled item. | ||
* @param {boolean} remainOpen Should dropdown remain in the same open state after setting selected item. | ||
*/ | ||
selectItem: function(id, force, remainOpen) { | ||
var item = this.findItem(id); | ||
/** | ||
* Selects item. | ||
* | ||
* @param {string|number|object} id Id of selected item or query which finds item. | ||
* @param {boolean} force Force select item. May be used when you want to select disabled item. | ||
* @param {boolean} remainOpen Should dropdown remain in the same open state after setting selected item. | ||
*/ | ||
selectItem: function(id, force, remainOpen) { | ||
var item = this.findItem(id); | ||
if (!item) { | ||
if (this.get('hasNotSelectedItem') === true) { | ||
item = this.getNotSelectedItem(); | ||
} else { | ||
throw new Error('Can\'t select item which couldn\'t be found'); | ||
if (!item) { | ||
if (this.get('hasNotSelectedItem') === true) { | ||
item = this.getNotSelectedItem(); | ||
} else { | ||
throw new Error('Can\'t select item which couldn\'t be found'); | ||
} | ||
} | ||
} | ||
if (item.get('disabled') === true && !force) { | ||
return; | ||
} | ||
if (item.get('disabled') === true && !force) { | ||
return; | ||
} | ||
this.set({ | ||
selectedId: item.get('id'), | ||
hasError: item.get('disabled') === true | ||
}, VALIDATE); | ||
this.set({ | ||
selectedId: item.get('id'), | ||
hasError: item.get('disabled') === true | ||
}, VALIDATE); | ||
if (!remainOpen) { | ||
this.toggleOpen(false); | ||
} | ||
}, | ||
if (!remainOpen) { | ||
this.toggleOpen(false); | ||
} | ||
}, | ||
getSelectedItem: function() { | ||
if (this.get('selectedId') === null && this.get('hasNotSelectedItem') === true) { | ||
return this.getNotSelectedItem(); | ||
} | ||
getSelectedItem: function() { | ||
if (this.get('selectedId') === null && this.get('hasNotSelectedItem') === true) { | ||
return this.getNotSelectedItem(); | ||
} | ||
return this.findItem(this.get('selectedId')); | ||
}, | ||
return this.findItem(this.get('selectedId')); | ||
}, | ||
getNotSelectedItem: function() { | ||
return new Backbone.Model({ | ||
id: null, | ||
text: this.get('notSelectedText') | ||
}); | ||
}, | ||
getNotSelectedItem: function() { | ||
return new Backbone.Model({ | ||
id: null, | ||
text: this.get('notSelectedText') | ||
}); | ||
}, | ||
getLoadingItem: function() { | ||
return new Backbone.Model({ | ||
id: null, | ||
text: this.get('loadingText') | ||
}); | ||
}, | ||
getLoadingItem: function() { | ||
return new Backbone.Model({ | ||
id: null, | ||
text: this.get('loadingText') | ||
}); | ||
}, | ||
getRootItems: function() { | ||
var items = this.getData().filter(function(item) { | ||
var pId = item.get('parentId'); | ||
return _.isNull(pId) || _.isUndefined(pId) || pId === false; | ||
}); | ||
getRootItems: function() { | ||
var items = this.getData().filter(function(item) { | ||
var pId = item.get('parentId'); | ||
return _.isNull(pId) || _.isUndefined(pId) || pId === false; | ||
}); | ||
if (this.get('hasNotSelectedItem') === true) { | ||
var notSelecteItem = this.getNotSelectedItem(); | ||
items = [notSelecteItem].concat(items); | ||
} | ||
if (this.get('hasNotSelectedItem') === true) { | ||
var notSelecteItem = this.getNotSelectedItem(); | ||
items = [notSelecteItem].concat(items); | ||
} | ||
return items; | ||
}, | ||
return items; | ||
}, | ||
toggleOpen: function(forceValue) { | ||
this.set({ | ||
isOpen: typeof forceValue === 'undefined' ? !this.get('isOpen') : !!forceValue | ||
}, VALIDATE); | ||
} | ||
toggleOpen: function(forceValue) { | ||
this.set({ | ||
isOpen: typeof forceValue === 'undefined' ? !this.get('isOpen') : !!forceValue | ||
}, VALIDATE); | ||
} | ||
}); | ||
}); |
@@ -1,98 +0,100 @@ | ||
var Backbone = require('backbone'); | ||
var _ = require('lodash'); | ||
var toggleTemplate = _.template(require('txt!./comboboxView.ejs')); | ||
var dropdownTemplate = _.template(require('txt!./dropdownView.ejs')); | ||
var DropdownView = require('./dropdownView'); | ||
var classnames = require('classnames'); | ||
define(function(require, exports, module) { | ||
var Backbone = require('backbone'); | ||
var _ = require('lodash'); | ||
var toggleTemplate = _.template(require('text!./comboboxView.ejs')); | ||
var dropdownTemplate = _.template(require('text!./dropdownView.ejs')); | ||
var DropdownView = require('./dropdownView'); | ||
var classnames = require('classnames'); | ||
// properties of model which changes causes call of render function | ||
var renderProperties = ['selectedId', 'isDisabled', 'isLoading', 'isOpen', 'theme', 'isWarning', 'hasError']; | ||
var renderProperties = ['selectedId', 'isDisabled', 'isLoading', 'isOpen', 'theme', 'isWarning', 'hasError']; | ||
module.exports = Backbone.View.extend({ | ||
toggleTemplate: toggleTemplate, | ||
dropdownTemplate: dropdownTemplate, | ||
dropdownView: DropdownView, | ||
module.exports = Backbone.View.extend({ | ||
toggleTemplate: toggleTemplate, | ||
dropdownTemplate: dropdownTemplate, | ||
dropdownView: DropdownView, | ||
className: 'c-combobox', | ||
className: 'c-combobox', | ||
events: { | ||
'click .js-combobox__toggle:not(.is-disabled)': 'toggle' | ||
}, | ||
events: { | ||
'click .js-combobox__toggle:not(.is-disabled)': 'toggle' | ||
}, | ||
initialize: function() { | ||
this._dropdownView = null; | ||
this | ||
.listenTo(this.model, 'change:isOpen', this.toggleDropDown) | ||
.listenTo(this.model, 'change:' + renderProperties.join(' change:'), this.render) | ||
.listenTo(this.model.getData(), 'reset', this.render) | ||
}, | ||
initialize: function() { | ||
this._dropdownView = null; | ||
this | ||
.listenTo(this.model, 'change:isOpen', this.toggleDropDown) | ||
.listenTo(this.model, 'change:' + renderProperties.join(' change:'), this.render) | ||
.listenTo(this.model.getData(), 'reset', this.render) | ||
}, | ||
toggleDropDown: function(model, isOpen) { | ||
if (isOpen) { | ||
this.renderDropdown(); | ||
} else { | ||
this.removeDropdown(); | ||
} | ||
}, | ||
toggleDropDown: function(model, isOpen) { | ||
if (isOpen) { | ||
this.renderDropdown(); | ||
} else { | ||
this.removeDropdown(); | ||
} | ||
}, | ||
render: function() { | ||
var classes = { | ||
'is-disabled': this.model.get('isDisabled'), | ||
'is-open': this.model.get('isOpen'), | ||
'is-loading': this.model.get('isLoading'), | ||
'is-warning': this.model.get('isWarning'), | ||
'has-error': this.model.get('hasError') | ||
}; | ||
render: function() { | ||
var classes = { | ||
'is-disabled': this.model.get('isDisabled'), | ||
'is-open': this.model.get('isOpen'), | ||
'is-loading': this.model.get('isLoading'), | ||
'is-warning': this.model.get('isWarning'), | ||
'has-error': this.model.get('hasError') | ||
}; | ||
if (!!this.model.get('theme')) { | ||
this.$el.addClass('t-combobox--' + this.model.get('theme')); | ||
} else { | ||
// removes all found theme classes | ||
// this.$el.removeClass(function(index, css) { | ||
// return (css.match() || []).join(' '); | ||
// }); | ||
this.el.setAttribute('class', this.el.getAttribute('class').replace(/\bt-combobox--\S+/g, '')); | ||
} | ||
if (!!this.model.get('theme')) { | ||
this.$el.addClass('t-combobox--' + this.model.get('theme')); | ||
} else { | ||
// removes all found theme classes | ||
// this.$el.removeClass(function(index, css) { | ||
// return (css.match() || []).join(' '); | ||
// }); | ||
this.el.setAttribute('class', this.el.getAttribute('class').replace(/\bt-combobox--\S+/g, '')); | ||
} | ||
var selectedItem = this.model.getSelectedItem(); | ||
var selectedItem = this.model.getSelectedItem(); | ||
this.$el.html( | ||
this.toggleTemplate({ | ||
classNames: classnames(classes), | ||
selected: selectedItem, | ||
label: this.model.getLabel() | ||
}) | ||
); | ||
this.$el.html( | ||
this.toggleTemplate({ | ||
classNames: classnames(classes), | ||
selected: selectedItem, | ||
label: this.model.getLabel() | ||
}) | ||
); | ||
return this; | ||
}, | ||
return this; | ||
}, | ||
toggle: function(e) { | ||
e.stopPropagation(); | ||
this.model.toggleOpen(); | ||
}, | ||
toggle: function(e) { | ||
e.stopPropagation(); | ||
this.model.toggleOpen(); | ||
}, | ||
close: function() { | ||
this.model.toggleOpen(false); | ||
}, | ||
close: function() { | ||
this.model.toggleOpen(false); | ||
}, | ||
renderDropdown: function() { | ||
this.removeDropdown(); | ||
renderDropdown: function() { | ||
this.removeDropdown(); | ||
this._dropdownView = new this.dropdownView({ | ||
template: this.dropdownTemplate, | ||
model: this.model, | ||
items: this.model.getRootItems(), | ||
right: false | ||
}); | ||
this._dropdownView = new this.dropdownView({ | ||
template: this.dropdownTemplate, | ||
model: this.model, | ||
items: this.model.getRootItems(), | ||
right: false | ||
}); | ||
this._dropdownView.render().$el.appendTo('body'); | ||
this._dropdownView.setPosition(this.$el, true); | ||
}, | ||
this._dropdownView.render().$el.appendTo('body'); | ||
this._dropdownView.setPosition(this.$el, true); | ||
}, | ||
removeDropdown: function() { | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
removeDropdown: function() { | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
} | ||
} | ||
} | ||
}); | ||
}); |
@@ -1,295 +0,297 @@ | ||
var Backbone = require('backbone'); | ||
var $ = require('jquery'); | ||
var _ = require('lodash'); | ||
var classnames = require('classnames'); | ||
define(function(require, exports, module) { | ||
var Backbone = require('backbone'); | ||
var $ = require('jquery'); | ||
var _ = require('lodash'); | ||
var classnames = require('classnames'); | ||
var WINDOW = $(window); | ||
var DOCUMENT = $(document); | ||
var BODY = $('body'); | ||
var WINDOW = $(window); | ||
var DOCUMENT = $(document); | ||
var BODY = $('body'); | ||
var ANIMATION_CLASSES = { | ||
below: 'c-combobox__dropdown--from-top', | ||
onRight: 'c-combobox__dropdown--from-left', | ||
onLeft: 'c-combobox__dropdown--from-right', | ||
above: 'c-combobox__dropdown--from-bottom' | ||
}; | ||
var ANIMATION_CLASSES = { | ||
below: 'c-combobox__dropdown--from-top', | ||
onRight: 'c-combobox__dropdown--from-left', | ||
onLeft: 'c-combobox__dropdown--from-right', | ||
above: 'c-combobox__dropdown--from-bottom' | ||
}; | ||
module.exports = Backbone.View.extend({ | ||
className: 'c-combobox__dropdown', | ||
tagName: 'ul', | ||
module.exports = Backbone.View.extend({ | ||
className: 'c-combobox__dropdown', | ||
tagName: 'ul', | ||
events: { | ||
'click .js-combobox__item': 'selectItem', | ||
'mouseenter .js-combobox__item': 'makeActive', | ||
'mouseleave .js-combobox__item': 'removeActive', | ||
'mouseenter .js-combobox__item.has-children': 'displayChildren' | ||
}, | ||
events: { | ||
'click .js-combobox__item': 'selectItem', | ||
'mouseenter .js-combobox__item': 'makeActive', | ||
'mouseleave .js-combobox__item': 'removeActive', | ||
'mouseenter .js-combobox__item.has-children': 'displayChildren' | ||
}, | ||
initialize: function(options) { | ||
this.template = options.template; | ||
this.items = options.items; | ||
this.right = typeof options.right !== 'undefined' ? !!options.right : true; | ||
this.first = typeof options.first !== 'undefined' ? !!options.first : true; | ||
this.active = false; | ||
initialize: function(options) { | ||
this.template = options.template; | ||
this.items = options.items; | ||
this.right = typeof options.right !== 'undefined' ? !!options.right : true; | ||
this.first = typeof options.first !== 'undefined' ? !!options.first : true; | ||
this.active = false; | ||
if (this.first) { | ||
this.captureClickOnBodyHandler = this._closeCombobox.bind(this); | ||
BODY[0].addEventListener('click', this.captureClickOnBodyHandler, true); | ||
if (this.first) { | ||
this.captureClickOnBodyHandler = this._closeCombobox.bind(this); | ||
BODY[0].addEventListener('click', this.captureClickOnBodyHandler, true); | ||
this.listenTo(this.model, 'change:isOpen', this.toggleEventListener); | ||
} | ||
}, | ||
this.listenTo(this.model, 'change:isOpen', this.toggleEventListener); | ||
} | ||
}, | ||
// event handlers | ||
// -------------- | ||
selectItem: function(ev) { | ||
var index = ev.target.getAttribute('cid'); | ||
var selectedItem = this.items[index]; | ||
// event handlers | ||
// -------------- | ||
selectItem: function(ev) { | ||
var index = ev.target.getAttribute('cid'); | ||
var selectedItem = this.items[index]; | ||
this.model.selectItem(selectedItem.get('id')); | ||
}, | ||
this.model.selectItem(selectedItem.get('id')); | ||
}, | ||
makeActive: function() { | ||
this.active = true; | ||
this.removeDropdown(); | ||
}, | ||
makeActive: function() { | ||
this.active = true; | ||
this.removeDropdown(); | ||
}, | ||
removeActive: function() { | ||
this.active = false; | ||
}, | ||
removeActive: function() { | ||
this.active = false; | ||
}, | ||
displayChildren: function(event) { | ||
var $currentTarget = this.$(event.currentTarget); | ||
var hoveredModel = this.items[$currentTarget.attr('cid')]; | ||
var children = hoveredModel.treeGetChildren(); | ||
displayChildren: function(event) { | ||
var $currentTarget = this.$(event.currentTarget); | ||
var hoveredModel = this.items[$currentTarget.attr('cid')]; | ||
var children = hoveredModel.treeGetChildren(); | ||
this.renderDropdown(children, $currentTarget); | ||
}, | ||
this.renderDropdown(children, $currentTarget); | ||
}, | ||
/** | ||
* Depending on model's isOpen property function will add or remove eventListener at capturing phase. | ||
* | ||
* @param {object} model Dropdown (Backbone) model. | ||
* @param {boolean} isOpen Flag indicating that dropdown is open. | ||
*/ | ||
toggleEventListener: function(model, isOpen) { | ||
if (isOpen) { | ||
// handle event at capturing phase (first phase going from bottom up) | ||
this.captureClickOnBodyHandler = this._closeCombobox.bind(this); | ||
BODY[0].addEventListener('click', this.captureClickOnBodyHandler, true); | ||
} else { | ||
// unbind click listener from body | ||
BODY[0].removeEventListener('click', this.captureClickOnBodyHandler, true); | ||
} | ||
}, | ||
/** | ||
* Depending on model's isOpen property function will add or remove eventListener at capturing phase. | ||
* | ||
* @param {object} model Dropdown (Backbone) model. | ||
* @param {boolean} isOpen Flag indicating that dropdown is open. | ||
*/ | ||
toggleEventListener: function(model, isOpen) { | ||
if (isOpen) { | ||
// handle event at capturing phase (first phase going from bottom up) | ||
this.captureClickOnBodyHandler = this._closeCombobox.bind(this); | ||
BODY[0].addEventListener('click', this.captureClickOnBodyHandler, true); | ||
} else { | ||
// unbind click listener from body | ||
BODY[0].removeEventListener('click', this.captureClickOnBodyHandler, true); | ||
} | ||
}, | ||
_closeCombobox: function(event) { | ||
// if something else than dropdown item has been clicked... | ||
if (!$(event.target).hasClass('js-combobox__item')) { | ||
// prevent propagation of event to bubble up | ||
event.stopImmediatePropagation(); | ||
_closeCombobox: function(event) { | ||
// if something else than dropdown item has been clicked... | ||
if (!$(event.target).hasClass('js-combobox__item')) { | ||
// prevent propagation of event to bubble up | ||
event.stopImmediatePropagation(); | ||
// hide all dropdowns | ||
this.closeWithoutSelecting(); | ||
} | ||
}, | ||
// hide all dropdowns | ||
this.closeWithoutSelecting(); | ||
} | ||
}, | ||
render: function() { | ||
this.$el.html(this.template( | ||
this.items.map(this._itemMapper.bind(this)) | ||
)); | ||
render: function() { | ||
this.$el.html(this.template( | ||
this.items.map(this._itemMapper.bind(this)) | ||
)); | ||
if (!!this.model.get('theme')) { | ||
this.$el.addClass('t-combobox--' + this.model.get('theme')); | ||
} | ||
if (!!this.model.get('theme')) { | ||
this.$el.addClass('t-combobox--' + this.model.get('theme')); | ||
} | ||
WINDOW | ||
.one('resize.combobox', this.closeWithoutSelecting.bind(this)) | ||
.one('keyup.combobox', this.closeWithoutSelecting.bind(this)); | ||
WINDOW | ||
.one('resize.combobox', this.closeWithoutSelecting.bind(this)) | ||
.one('keyup.combobox', this.closeWithoutSelecting.bind(this)); | ||
DOCUMENT | ||
.on('wheel.combobox', this.checkWheelEvent.bind(this)); | ||
DOCUMENT | ||
.on('wheel.combobox', this.checkWheelEvent.bind(this)); | ||
return this; | ||
}, | ||
return this; | ||
}, | ||
closeWithoutSelecting: function() { | ||
this.model.set({isOpen: false}, {validate: true}); | ||
}, | ||
closeWithoutSelecting: function() { | ||
this.model.set({isOpen: false}, {validate: true}); | ||
}, | ||
checkWheelEvent: function() { | ||
// event will be called on each opened dropdown so that we have to distinguish if it's active dropdown | ||
if (this.active) { | ||
this.removeDropdown(); | ||
} | ||
}, | ||
/* @todo Move to some provider or model */ | ||
_itemMapper: function(item) { | ||
var selectedItem = this.model.getSelectedItem(); | ||
var mItem = item.toJSON(); | ||
var additionalClasses = {}; | ||
checkWheelEvent: function() { | ||
// event will be called on each opened dropdown so that we have to distinguish if it's active dropdown | ||
if (this.active) { | ||
this.removeDropdown(); | ||
} | ||
}, | ||
/* @todo Move to some provider or model */ | ||
_itemMapper: function(item) { | ||
var selectedItem = this.model.getSelectedItem(); | ||
var mItem = item.toJSON(); | ||
var additionalClasses = {}; | ||
if (!item.get('title')) { // if there is not specified title - copy text value. | ||
mItem.title = item.get('text'); | ||
} | ||
if (!item.get('title')) { // if there is not specified title - copy text value. | ||
mItem.title = item.get('text'); | ||
} | ||
// has-children | ||
additionalClasses['has-children'] = _.isFunction(item.treeGetChildren) && !_.isEmpty(item.treeGetChildren()); | ||
// has-children | ||
additionalClasses['has-children'] = _.isFunction(item.treeGetChildren) && !_.isEmpty(item.treeGetChildren()); | ||
// is-selected & is-selected-path | ||
if (selectedItem && selectedItem.get('id') !== null) { // if has selected item | ||
if (item.get('id') !== null) { // if mapped item isn't notSelectedItem | ||
additionalClasses['is-selected'] = item.get('id') === selectedItem.get('id'); | ||
additionalClasses['is-selected-path'] = item.hasTreeAncestor(selectedItem); | ||
// is-selected & is-selected-path | ||
if (selectedItem && selectedItem.get('id') !== null) { // if has selected item | ||
if (item.get('id') !== null) { // if mapped item isn't notSelectedItem | ||
additionalClasses['is-selected'] = item.get('id') === selectedItem.get('id'); | ||
additionalClasses['is-selected-path'] = item.hasTreeAncestor(selectedItem); | ||
} | ||
} | ||
} | ||
// is-not-selectable | ||
additionalClasses['is-not-selectable'] = item.get('disabled') === true; | ||
// is-not-selectable | ||
additionalClasses['is-not-selectable'] = item.get('disabled') === true; | ||
mItem.class = classnames(mItem.class, additionalClasses); | ||
mItem.class = classnames(mItem.class, additionalClasses); | ||
return mItem; | ||
}, | ||
return mItem; | ||
}, | ||
remove: function() { | ||
// take off attached combobox events when first dropdown has been removed | ||
if (this.first) { | ||
WINDOW.off('.combobox'); | ||
DOCUMENT.off('.combobox'); | ||
} | ||
remove: function() { | ||
// take off attached combobox events when first dropdown has been removed | ||
if (this.first) { | ||
WINDOW.off('.combobox'); | ||
DOCUMENT.off('.combobox'); | ||
} | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
this._dropdownView = undefined; | ||
} | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
this._dropdownView = undefined; | ||
} | ||
Backbone.View.prototype.remove.call(this); | ||
}, | ||
Backbone.View.prototype.remove.call(this); | ||
}, | ||
renderDropdown: function(children, $target) { | ||
this.removeDropdown(); | ||
renderDropdown: function(children, $target) { | ||
this.removeDropdown(); | ||
this._dropdownView = new this.constructor({ | ||
template: this.template, | ||
first: false, | ||
model: this.model, | ||
items: children, | ||
right: (this.first) ? true : this.right // passing this value causes that 'zig-zag' submenu effect | ||
}); | ||
this._dropdownView = new this.constructor({ | ||
template: this.template, | ||
first: false, | ||
model: this.model, | ||
items: children, | ||
right: (this.first) ? true : this.right // passing this value causes that 'zig-zag' submenu effect | ||
}); | ||
this._dropdownView.render().$el.appendTo('body'); | ||
this._dropdownView.setPosition($target, false); | ||
}, | ||
this._dropdownView.render().$el.appendTo('body'); | ||
this._dropdownView.setPosition($target, false); | ||
}, | ||
removeDropdown: function() { | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
} | ||
}, | ||
removeDropdown: function() { | ||
if (this._dropdownView) { | ||
this._dropdownView.remove(); | ||
} | ||
}, | ||
/** | ||
* Sets position of view according to some other view. | ||
* | ||
* Function in general try to position view the way it fits on screen. It will will bump menu to other side | ||
* if it won't fit. If this happens it will store corresponding value in local variable (this.right). | ||
* | ||
* Depending if it's first or next item position View on the side or below/above parent item. | ||
* | ||
* Also if model has `isAnimated` set to true it will add correct classes to dropdown. | ||
* | ||
* @param {object} $parentView JQuery pointer to DOM element. | ||
* @param {boolean} first Flag indicating if this view should be treated as first dropdown. | ||
* | ||
* @todo Move to provider. | ||
*/ | ||
setPosition: function($parentView, first) { | ||
var animationClass; | ||
this.first = first; | ||
/** | ||
* Sets position of view according to some other view. | ||
* | ||
* Function in general try to position view the way it fits on screen. It will will bump menu to other side | ||
* if it won't fit. If this happens it will store corresponding value in local variable (this.right). | ||
* | ||
* Depending if it's first or next item position View on the side or below/above parent item. | ||
* | ||
* Also if model has `isAnimated` set to true it will add correct classes to dropdown. | ||
* | ||
* @param {object} $parentView JQuery pointer to DOM element. | ||
* @param {boolean} first Flag indicating if this view should be treated as first dropdown. | ||
* | ||
* @todo Move to provider. | ||
*/ | ||
setPosition: function($parentView, first) { | ||
var animationClass; | ||
this.first = first; | ||
if (this.model.get('isAnimated')) { | ||
this.$el.removeClass(_.values(ANIMATION_CLASSES).join(' ')); | ||
} | ||
if (this.model.get('isAnimated')) { | ||
this.$el.removeClass(_.values(ANIMATION_CLASSES).join(' ')); | ||
} | ||
var parentRect = $parentView[0].getBoundingClientRect(); | ||
var parentRect = $parentView[0].getBoundingClientRect(); | ||
// It's required to set `min-width` here because width of element can be too small. | ||
this.$el.css({minWidth: parentRect.width}); | ||
// It's required to set `min-width` here because width of element can be too small. | ||
this.$el.css({minWidth: parentRect.width}); | ||
var dropdownRect = this.$el[0].getBoundingClientRect(); | ||
var dropdownRect = this.$el[0].getBoundingClientRect(); | ||
var properties = { | ||
top: this.first ? parentRect.bottom : parentRect.top | ||
}; | ||
var properties = { | ||
top: this.first ? parentRect.bottom : parentRect.top | ||
}; | ||
// Logic below flips dropdown when it won't fit within window and applies correct animation classes | ||
// === | ||
// Logic below flips dropdown when it won't fit within window and applies correct animation classes | ||
// === | ||
// Vertical checks | ||
// --- | ||
if (this.first) { | ||
animationClass = ANIMATION_CLASSES.below; | ||
// Vertical checks | ||
// --- | ||
if (this.first) { | ||
animationClass = ANIMATION_CLASSES.below; | ||
if (parentRect.bottom + dropdownRect.height > window.innerHeight) { | ||
properties.top = parentRect.top - dropdownRect.height; | ||
animationClass = ANIMATION_CLASSES.above; | ||
if (parentRect.bottom + dropdownRect.height > window.innerHeight) { | ||
properties.top = parentRect.top - dropdownRect.height; | ||
animationClass = ANIMATION_CLASSES.above; | ||
} | ||
} | ||
} | ||
// Horizontal checks | ||
// --- | ||
if (this.right === true && this.first === true) { | ||
properties.left = parentRect.right - dropdownRect.width; | ||
// Horizontal checks | ||
// --- | ||
if (this.right === true && this.first === true) { | ||
properties.left = parentRect.right - dropdownRect.width; | ||
// dropdown won't fit within window | ||
if (parentRect.right - dropdownRect.width < 0) { | ||
this.right = false; | ||
// dropdown won't fit within window | ||
if (parentRect.right - dropdownRect.width < 0) { | ||
this.right = false; | ||
properties.left = parentRect.left; | ||
properties.left = parentRect.left; | ||
} | ||
} | ||
} | ||
if (this.right === true && this.first === false) { | ||
animationClass = ANIMATION_CLASSES.onRight; | ||
properties.left = parentRect.right; | ||
if (this.right === true && this.first === false) { | ||
animationClass = ANIMATION_CLASSES.onRight; | ||
properties.left = parentRect.right; | ||
// dropdown won't fit within window | ||
if (properties.left + dropdownRect.width > window.innerWidth) { | ||
this.right = false; | ||
// dropdown won't fit within window | ||
if (properties.left + dropdownRect.width > window.innerWidth) { | ||
this.right = false; | ||
properties.left = parentRect.left - dropdownRect.width; | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
properties.left = parentRect.left - dropdownRect.width; | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
} | ||
} | ||
} | ||
if (this.right === false && this.first === true) { | ||
properties.left = parentRect.left; | ||
if (this.right === false && this.first === true) { | ||
properties.left = parentRect.left; | ||
// dropdown won't fit within window | ||
if (parentRect.left + dropdownRect.width > window.innerWidth) { | ||
this.right = true; | ||
// dropdown won't fit within window | ||
if (parentRect.left + dropdownRect.width > window.innerWidth) { | ||
this.right = true; | ||
properties.left = parentRect.right; | ||
properties.left = parentRect.right; | ||
} | ||
} | ||
} | ||
if (this.right === false && this.first === false) { | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
properties.left = parentRect.left - dropdownRect.width; | ||
if (this.right === false && this.first === false) { | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
properties.left = parentRect.left - dropdownRect.width; | ||
// dropdown won't fit within window | ||
if (properties.left < 0) { | ||
this.right = true; | ||
// dropdown won't fit within window | ||
if (properties.left < 0) { | ||
this.right = true; | ||
properties.left = parentRect.right; | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
properties.left = parentRect.right; | ||
animationClass = ANIMATION_CLASSES.onLeft; | ||
} | ||
} | ||
} | ||
this.$el.css(properties); | ||
this.$el.css(properties); | ||
if (this.model.get('isAnimated') === true) { | ||
this.$el.addClass(animationClass); | ||
if (this.model.get('isAnimated') === true) { | ||
this.$el.addClass(animationClass); | ||
} | ||
} | ||
} | ||
}); | ||
}); |
@@ -0,6 +1,11 @@ | ||
var path = require('path'); | ||
module.exports = { | ||
entry: './entry.js', | ||
entry: { | ||
app: ['./demo/demo.js'] | ||
}, | ||
output: { | ||
path: './dist', | ||
filename: 'index.js' | ||
path: path.resolve(__dirname, 'demo'), | ||
publicPath: '/assets/', | ||
filename: 'bundle.js' | ||
}, | ||
@@ -7,0 +12,0 @@ module: { |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1147572
26
28656
60
11
5
5