mirror of https://github.com/artf/grapesjs.git
16 changed files with 1289 additions and 18 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,24 @@ |
|||||
|
define(function () { |
||||
|
return { |
||||
|
|
||||
|
// Style prefix
|
||||
|
stylePrefix : 'clm-', |
||||
|
|
||||
|
// Default classes
|
||||
|
selectors : [], |
||||
|
|
||||
|
// Label for classes
|
||||
|
label: 'Classes', |
||||
|
|
||||
|
// Label for states
|
||||
|
statesLabel: '- State -', |
||||
|
|
||||
|
// States
|
||||
|
states: [ |
||||
|
{ name: 'hover', label: 'Hover' }, |
||||
|
{ name: 'active', label: 'Click' }, |
||||
|
{ name: 'nth-of-type(2n)', label: 'Even/Odd' } |
||||
|
], |
||||
|
|
||||
|
}; |
||||
|
}); |
||||
@ -0,0 +1,164 @@ |
|||||
|
/** |
||||
|
* * [init](#init) |
||||
|
* * [add](#add) |
||||
|
* * [get](#get) |
||||
|
* * [getAll](#getall) |
||||
|
* * [render](#render) |
||||
|
* |
||||
|
* Selectors in GrapesJS are used in CSS Composer inside Rules and in Components as classes. To get better this concept let's take |
||||
|
* a look at this code: |
||||
|
* |
||||
|
* ```css
|
||||
|
* span > #send-btn.btn{ |
||||
|
* ... |
||||
|
* } |
||||
|
* ``` |
||||
|
* ```html
|
||||
|
* <span> |
||||
|
* <button id="send-btn" class="btn"></button> |
||||
|
* </span> |
||||
|
* ``` |
||||
|
* |
||||
|
* In this scenario we get: |
||||
|
* span -> selector of type `tag` |
||||
|
* send-btn -> selector of type `id` |
||||
|
* btn -> selector of type `class` |
||||
|
* |
||||
|
* So, for example, being `btn` the same class entity it'll be easier to refactor and track things. |
||||
|
* |
||||
|
* Before using methods you should get first the module from the editor instance, in this way: |
||||
|
* |
||||
|
* ```js
|
||||
|
* var selectorManager = editor.SelectorManager; |
||||
|
* ``` |
||||
|
* |
||||
|
* @module SelectorManager |
||||
|
*/ |
||||
|
define(function(require) { |
||||
|
|
||||
|
return function(config) { |
||||
|
var c = config || {}, |
||||
|
defaults = require('./config/config'); |
||||
|
Selectors = require('./model/Selectors'); |
||||
|
ClassTagsView = require('./view/ClassTagsView'); |
||||
|
var selectors, selectorTags; |
||||
|
|
||||
|
return { |
||||
|
|
||||
|
/** |
||||
|
* Name of the module |
||||
|
* @type {String} |
||||
|
* @private |
||||
|
*/ |
||||
|
name: 'SelectorManager', |
||||
|
|
||||
|
/** |
||||
|
* Indicates if module is public |
||||
|
* @type {Boolean} |
||||
|
* @private |
||||
|
*/ |
||||
|
public: true, |
||||
|
|
||||
|
/** |
||||
|
* Initialize module. Automatically called with a new instance of the editor |
||||
|
* @param {Object} config Configurations |
||||
|
* @param {Array<Object>} [config.selectors=[]] Default selectors |
||||
|
* @param {Array<Object>} [config.states=[]] Default states |
||||
|
* @param {String} [config.label='Classes'] Classes label |
||||
|
* @param {String} [config.statesLabel='- State -'] The empty state label |
||||
|
* @return {this} |
||||
|
* @example |
||||
|
* ... |
||||
|
* { |
||||
|
* selectors: [ |
||||
|
* {name:'myselector1'}, |
||||
|
* ... |
||||
|
* ], |
||||
|
* states: [{ |
||||
|
* name: 'hover', label: 'Hover' |
||||
|
* },{ |
||||
|
* name: 'active', label: 'Click' |
||||
|
* }], |
||||
|
* statesLabel: '- Selecte State -', |
||||
|
* } |
||||
|
*/ |
||||
|
init: function(conf) { |
||||
|
c = conf || {}; |
||||
|
|
||||
|
for (var name in defaults) { |
||||
|
if (!(name in c)) |
||||
|
c[name] = defaults[name]; |
||||
|
} |
||||
|
|
||||
|
var ppfx = c.pStylePrefix; |
||||
|
if(ppfx) |
||||
|
c.stylePrefix = ppfx + c.stylePrefix; |
||||
|
|
||||
|
selectors = new Selectors(c.selectors); |
||||
|
selectorTags = new ClassTagsView({ |
||||
|
collection: selectors, |
||||
|
config: c, |
||||
|
}); |
||||
|
return this; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Add new selector to collection if it's not already exists. Class type is a default one |
||||
|
* @param {String} name Selector name |
||||
|
* @param {Object} opts Selector options |
||||
|
* @param {String} [opts.label=''] Label for the selector, if it's not provided the label will be the same as the name |
||||
|
* @param {String} [opts.type='class'] Type of the selector. At the moment, only 'class' is available |
||||
|
* @return {Model} |
||||
|
* @example |
||||
|
* var selector = selectorManager.add('selectorName'); |
||||
|
* // Same as
|
||||
|
* var selector = selectorManager.add('selectorName', { |
||||
|
* type: 'class', |
||||
|
* label: 'selectorName' |
||||
|
* }); |
||||
|
* */ |
||||
|
add: function(name, opts){ |
||||
|
var obj = opts || {}; |
||||
|
obj.name = name; |
||||
|
return selectors.add(obj); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Get selector by its name |
||||
|
* @param {String} name Selector name |
||||
|
* @return {Model|null} |
||||
|
* @example |
||||
|
* var selector = selectorManager.get('selectorName'); |
||||
|
* */ |
||||
|
get: function(name) { |
||||
|
return selectors.where({name: name})[0]; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Get all selectors |
||||
|
* @return {Collection} |
||||
|
* */ |
||||
|
getAll: function() { |
||||
|
return selectors; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Render class selectors. If an array of selectors is provided a new instance of the collection will be rendered |
||||
|
* @param {Array<Object>} selectors |
||||
|
* @return {HTMLElement} |
||||
|
*/ |
||||
|
render: function(selectors) { |
||||
|
if(selectors){ |
||||
|
var view = new ClassTagsView({ |
||||
|
collection: new Selectors(selectors), |
||||
|
config: c, |
||||
|
}); |
||||
|
return view.render().el; |
||||
|
}else |
||||
|
return selectorTags.render().el; |
||||
|
}, |
||||
|
|
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
}); |
||||
@ -0,0 +1,32 @@ |
|||||
|
define(['backbone'], |
||||
|
function (Backbone) { |
||||
|
return Backbone.Model.extend({ |
||||
|
|
||||
|
idAttribute: 'name', |
||||
|
|
||||
|
defaults: { |
||||
|
name: '', |
||||
|
label: '', |
||||
|
type: 'class', |
||||
|
active: true, |
||||
|
}, |
||||
|
|
||||
|
initialize: function() { |
||||
|
this.set('name', this.escapeName(this.get('name'))); |
||||
|
var label = this.get('label').trim(); |
||||
|
if(!label) |
||||
|
this.set('label', this.get('name')); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Escape string |
||||
|
* @param {string} name |
||||
|
* |
||||
|
* @return {string} |
||||
|
*/ |
||||
|
escapeName: function(name) { |
||||
|
return name.toLowerCase().replace(/([^a-z0-9\w]+)/gi, '-'); |
||||
|
}, |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,9 @@ |
|||||
|
define(['backbone','./Selector'], |
||||
|
function (Backbone, Selector) { |
||||
|
|
||||
|
return Backbone.Collection.extend({ |
||||
|
|
||||
|
model: Selector, |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,5 @@ |
|||||
|
<span id="<%= pfx %>checkbox" class="fa"></span> |
||||
|
<span id="<%= pfx %>tag-label"> |
||||
|
<input class="<%= ppfx %>no-app" value="<%= label %>" <%= inputProp %>/> |
||||
|
</span> |
||||
|
<span id="<%= pfx %>close">⨯</span> |
||||
@ -0,0 +1,27 @@ |
|||||
|
<div id="<%= pfx %>up"> |
||||
|
<div id="<%= pfx %>label"><%= label %></div> |
||||
|
<div id="<%= pfx %>status-c"> |
||||
|
<span id="<%= pfx %>input-c"> |
||||
|
<div class="<%= pfx %>field <%= pfx %>select"> |
||||
|
<span id="<%= pfx %>input-holder"> |
||||
|
<select id="<%= pfx %>states"> |
||||
|
<option value=""><%= statesLabel %></option> |
||||
|
</select> |
||||
|
</span> |
||||
|
<div class="<%= pfx %>sel-arrow"> |
||||
|
<div class="<%= pfx %>d-s-arrow"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="<%= pfx %>tags-field"> |
||||
|
<div id="<%= pfx %>tags-c"></div> |
||||
|
<input id="<%= pfx %>new" /> |
||||
|
<span id="<%= pfx %>add-tag" class="fa fa-plus"></span> |
||||
|
</div> |
||||
|
<div id="<%= pfx %>sel-help"> |
||||
|
<div id="<%= pfx %>label">Selected</div> |
||||
|
<div id="<%= pfx %>sel"></div> |
||||
|
<div style="clear:both"></div> |
||||
|
</div> |
||||
@ -0,0 +1,129 @@ |
|||||
|
define(['backbone', 'text!./../template/classTag.html'], |
||||
|
function (Backbone, tagTemplate) { |
||||
|
/** |
||||
|
* @class ClassTagView |
||||
|
* */ |
||||
|
return Backbone.View.extend({ |
||||
|
|
||||
|
template: _.template(tagTemplate), |
||||
|
|
||||
|
events: {}, |
||||
|
|
||||
|
initialize: function(o) { |
||||
|
this.config = o.config || {}; |
||||
|
this.coll = o.coll || null; |
||||
|
this.pfx = this.config.stylePrefix || ''; |
||||
|
this.ppfx = this.config.pStylePrefix || ''; |
||||
|
this.inputProp = 'readonly'; |
||||
|
this.target = this.config.em; |
||||
|
this.className = this.pfx + 'tag'; |
||||
|
this.closeId = this.pfx + 'close'; |
||||
|
this.chkId = this.pfx + 'checkbox'; |
||||
|
this.labelId = this.pfx + 'tag-label'; |
||||
|
this.events['click #' + this.closeId ] = 'removeTag'; |
||||
|
this.events['click #' + this.chkId ] = 'changeStatus'; |
||||
|
this.events['dblclick #' + this.labelId ] = 'startEditTag'; |
||||
|
this.events['keypress #' + this.labelId + ' input'] = 'updateInputLabel'; |
||||
|
this.events['blur #' + this.labelId + ' input'] = 'endEditTag'; |
||||
|
|
||||
|
this.listenTo( this.model, 'change:active', this.updateStatus); |
||||
|
this.delegateEvents(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Start editing tag |
||||
|
*/ |
||||
|
startEditTag: function(){ |
||||
|
this.$labelInput.prop(this.inputProp, false); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* End editing tag. If the class typed already exists the |
||||
|
* old one will be restored otherwise will be changed |
||||
|
*/ |
||||
|
endEditTag: function(){ |
||||
|
var value = this.$labelInput.val(); |
||||
|
var next = this.model.escapeName(value); |
||||
|
|
||||
|
if(this.target){ |
||||
|
var clsm = this.target.get('SelectorManager'); |
||||
|
|
||||
|
if(clsm){ |
||||
|
if(clsm.get(next)) |
||||
|
this.$labelInput.val(this.model.get('label')); |
||||
|
else |
||||
|
this.model.set({ name: next, label: value}); |
||||
|
} |
||||
|
} |
||||
|
this.$labelInput.prop(this.inputProp, true); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Update status of the tag |
||||
|
*/ |
||||
|
changeStatus: function(){ |
||||
|
this.model.set('active', !this.model.get('active')); |
||||
|
this.target.trigger('targetClassUpdated'); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Remove tag from the selected component |
||||
|
* @param {Object} e |
||||
|
*/ |
||||
|
removeTag: function(e){ |
||||
|
var comp = this.target.get('selectedComponent'); |
||||
|
|
||||
|
if(comp) |
||||
|
comp.get('classes').remove(this.model); |
||||
|
|
||||
|
if(this.coll){ |
||||
|
this.coll.remove(this.model); |
||||
|
this.target.trigger('targetClassRemoved'); |
||||
|
} |
||||
|
|
||||
|
this.remove(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Update status of the checkbox |
||||
|
*/ |
||||
|
updateStatus: function(){ |
||||
|
if(!this.$chk) |
||||
|
this.$chk = this.$el.find('#' + this.pfx + 'checkbox'); |
||||
|
|
||||
|
if(this.model.get('active')){ |
||||
|
this.$chk.removeClass('fa-circle-o').addClass('fa-dot-circle-o'); |
||||
|
this.$el.removeClass('opac50'); |
||||
|
}else{ |
||||
|
this.$chk.removeClass('fa-dot-circle-o').addClass('fa-circle-o'); |
||||
|
this.$el.addClass('opac50'); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Update label's input |
||||
|
*/ |
||||
|
updateInputLabel: function(){ |
||||
|
if(!this.$labelInput) |
||||
|
this.$labelInput = this.$el.find('input'); |
||||
|
var size = this.$labelInput.val().length - 2; |
||||
|
size = size < 1 ? 1 : size; |
||||
|
this.$labelInput.attr('size', size); |
||||
|
}, |
||||
|
|
||||
|
/** @inheritdoc */ |
||||
|
render : function(){ |
||||
|
this.$el.html( this.template({ |
||||
|
label: this.model.get('label'), |
||||
|
pfx: this.pfx, |
||||
|
ppfx: this.ppfx, |
||||
|
inputProp: this.inputProp, |
||||
|
})); |
||||
|
this.updateStatus(); |
||||
|
this.$el.attr('class', this.className); |
||||
|
this.updateInputLabel(); |
||||
|
return this; |
||||
|
}, |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,280 @@ |
|||||
|
define(['backbone', 'text!./../template/classTags.html', './ClassTagView'], |
||||
|
function (Backbone, tagsTemplate, ClassTagView) { |
||||
|
/** |
||||
|
* @class ClassTagsView |
||||
|
* */ |
||||
|
return Backbone.View.extend({ |
||||
|
|
||||
|
template: _.template(tagsTemplate), |
||||
|
|
||||
|
events: {}, |
||||
|
|
||||
|
initialize: function(o) { |
||||
|
this.config = o.config || {}; |
||||
|
this.pfx = this.config.stylePrefix || ''; |
||||
|
this.className = this.pfx + 'tags'; |
||||
|
this.addBtnId = this.pfx + 'add-tag'; |
||||
|
this.newInputId = this.pfx + 'new'; |
||||
|
this.stateInputId = this.pfx + 'states'; |
||||
|
this.stateInputC = this.pfx + 'input-c'; |
||||
|
this.states = this.config.states || []; |
||||
|
this.events['click #' + this.addBtnId] = 'startNewTag'; |
||||
|
this.events['blur #' + this.newInputId] = 'endNewTag'; |
||||
|
this.events['keyup #' + this.newInputId] = 'onInputKeyUp'; |
||||
|
this.events['change #' + this.stateInputId] = 'stateChanged'; |
||||
|
|
||||
|
this.target = this.config.em; |
||||
|
|
||||
|
this.listenTo(this.target ,'change:selectedComponent',this.componentChanged); |
||||
|
this.listenTo(this.target, 'targetClassUpdated', this.updateSelector); |
||||
|
|
||||
|
this.listenTo(this.collection, 'add', this.addNew); |
||||
|
this.listenTo(this.collection, 'reset', this.renderClasses); |
||||
|
this.listenTo(this.collection, 'remove', this.tagRemoved); |
||||
|
|
||||
|
this.delegateEvents(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Triggered when a tag is removed from collection |
||||
|
* @param {Object} model Removed model |
||||
|
*/ |
||||
|
tagRemoved: function(model){ |
||||
|
this.updateStateVis(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Create select input with states |
||||
|
* @return {string} String of options |
||||
|
*/ |
||||
|
getStateOptions: function(){ |
||||
|
var strInput = ''; |
||||
|
for(var i = 0; i < this.states.length; i++){ |
||||
|
strInput += '<option value="' + this.states[i].name + '">' + this.states[i].label + '</option>'; |
||||
|
} |
||||
|
return strInput; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Add new model |
||||
|
* @param {Object} model |
||||
|
*/ |
||||
|
addNew: function(model){ |
||||
|
this.addToClasses(model); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Start tag creation |
||||
|
* @param {Object} e |
||||
|
* |
||||
|
*/ |
||||
|
startNewTag: function(e) { |
||||
|
this.$addBtn.hide(); |
||||
|
this.$input.show().focus(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* End tag creation |
||||
|
* @param {Object} e |
||||
|
* |
||||
|
*/ |
||||
|
endNewTag: function(e) { |
||||
|
this.$addBtn.show(); |
||||
|
this.$input.hide().val(''); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Checks what to do on keyup event |
||||
|
* @param {Object} e |
||||
|
*/ |
||||
|
onInputKeyUp: function(e) { |
||||
|
if (e.keyCode === 13) |
||||
|
this.addNewTag(this.$input.val()); |
||||
|
else if(e.keyCode === 27) |
||||
|
this.endNewTag(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Triggered when component is changed |
||||
|
* @param {Object} e |
||||
|
*/ |
||||
|
componentChanged: function(e){ |
||||
|
this.compTarget = this.target.get('selectedComponent'); |
||||
|
if(this.compTarget) |
||||
|
this.getStates().val(this.compTarget.get('state')); |
||||
|
var models = this.compTarget ? this.compTarget.get('classes').models : []; |
||||
|
this.collection.reset(models); |
||||
|
this.updateStateVis(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Update states visibility. Hides states in case there is no tags |
||||
|
* inside collection |
||||
|
*/ |
||||
|
updateStateVis: function(){ |
||||
|
if(this.collection.length) |
||||
|
this.getStatesC().css('display','block'); |
||||
|
else |
||||
|
this.getStatesC().css('display','none'); |
||||
|
this.updateSelector(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Udpate selector helper |
||||
|
* @return {this} |
||||
|
* @private |
||||
|
*/ |
||||
|
updateSelector: function(){ |
||||
|
this.compTarget = this.target.get('selectedComponent'); |
||||
|
if(!this.compTarget || !this.compTarget.get) |
||||
|
return; |
||||
|
var result = ''; |
||||
|
var models = this.compTarget.get('classes'); |
||||
|
models.each(function(model){ |
||||
|
if(model.get('active')) |
||||
|
result += '.' + model.get('name'); |
||||
|
}); |
||||
|
var state = this.compTarget.get('state'); |
||||
|
result = state ? result + ':' + state : result; |
||||
|
var el = this.el.querySelector('#' + this.pfx + 'sel'); |
||||
|
if(el) |
||||
|
el.innerHTML = result; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Triggered when the select with states is changed |
||||
|
* @param {Object} e |
||||
|
*/ |
||||
|
stateChanged: function(e){ |
||||
|
if(this.compTarget){ |
||||
|
this.compTarget.set('state', this.$states.val()); |
||||
|
if(this.target) |
||||
|
this.target.trigger('targetStateUpdated'); |
||||
|
this.updateSelector(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Add new tag to collection, if possible, and to the component |
||||
|
* @param {Object} e |
||||
|
*/ |
||||
|
addNewTag: function(name){ |
||||
|
if(!name) |
||||
|
return; |
||||
|
|
||||
|
if(this.target){ |
||||
|
var cm = this.target.get('SelectorManager'); |
||||
|
var model = cm.add(name); |
||||
|
|
||||
|
if(this.compTarget){ |
||||
|
var targetCls = this.compTarget.get('classes'); |
||||
|
var lenB = targetCls.length; |
||||
|
targetCls.add(model); |
||||
|
var lenA = targetCls.length; |
||||
|
this.collection.add(model); |
||||
|
|
||||
|
if(lenA > lenB) |
||||
|
this.target.trigger('targetClassAdded'); |
||||
|
|
||||
|
this.updateStateVis(); |
||||
|
} |
||||
|
} |
||||
|
this.endNewTag(); |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Add new object to collection |
||||
|
* @param {Object} model Model |
||||
|
* @param {Object} fragmentEl Fragment collection |
||||
|
* |
||||
|
* @return {Object} Object created |
||||
|
* */ |
||||
|
addToClasses: function(model, fragmentEl) { |
||||
|
var fragment = fragmentEl || null; |
||||
|
|
||||
|
var view = new ClassTagView({ |
||||
|
model: model, |
||||
|
config: this.config, |
||||
|
coll: this.collection, |
||||
|
}); |
||||
|
var rendered = view.render().el; |
||||
|
|
||||
|
if(fragment) |
||||
|
fragment.appendChild(rendered); |
||||
|
else |
||||
|
this.getClasses().append(rendered); |
||||
|
|
||||
|
return rendered; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Render the collection of classes |
||||
|
* @return {this} |
||||
|
*/ |
||||
|
renderClasses: function() { |
||||
|
var fragment = document.createDocumentFragment(); |
||||
|
|
||||
|
this.collection.each(function(model){ |
||||
|
this.addToClasses(model, fragment); |
||||
|
},this); |
||||
|
|
||||
|
if(this.getClasses()) |
||||
|
this.getClasses().empty().append(fragment); |
||||
|
|
||||
|
return this; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Return classes element |
||||
|
* @return {HTMLElement} |
||||
|
* @private |
||||
|
*/ |
||||
|
getClasses: function() { |
||||
|
if(!this.$classes) |
||||
|
this.$classes = this.$el.find('#' + this.pfx + 'tags-c'); |
||||
|
return this.$classes; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Return states element |
||||
|
* @return {HTMLElement} |
||||
|
* @private |
||||
|
*/ |
||||
|
getStates: function() { |
||||
|
if(!this.$states) |
||||
|
this.$states = this.$el.find('#' + this.stateInputId); |
||||
|
return this.$states; |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Return states container element |
||||
|
* @return {HTMLElement} |
||||
|
* @private |
||||
|
*/ |
||||
|
getStatesC: function() { |
||||
|
if(!this.$statesC) |
||||
|
this.$statesC = this.$el.find('#' + this.stateInputC); |
||||
|
return this.$statesC; |
||||
|
}, |
||||
|
|
||||
|
|
||||
|
/** @inheritdoc */ |
||||
|
render : function(){ |
||||
|
this.$el.html( this.template({ |
||||
|
label: this.config.label, |
||||
|
statesLabel: this.config.statesLabel, |
||||
|
pfx: this.pfx, |
||||
|
})); |
||||
|
this.$input = this.$el.find('input#' + this.newInputId); |
||||
|
this.$addBtn = this.$el.find('#' + this.addBtnId); |
||||
|
this.$classes = this.$el.find('#' + this.pfx + 'tags-c'); |
||||
|
this.$states = this.$el.find('#' + this.stateInputId); |
||||
|
this.$statesC = this.$el.find('#' + this.stateInputC); |
||||
|
this.$states.append(this.getStateOptions()); |
||||
|
this.renderClasses(); |
||||
|
this.$el.attr('class', this.className); |
||||
|
return this; |
||||
|
}, |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,127 @@ |
|||||
|
|
||||
|
define(['GrapesJS', 'SelectorManager/model/Selectors', 'SelectorManager/view/ClassTagsView'], |
||||
|
function(GrapesJS, Selectors, ClassTagsView) { |
||||
|
|
||||
|
return { |
||||
|
run : function(){ |
||||
|
describe('E2E tests', function() { |
||||
|
|
||||
|
/** |
||||
|
* Create tags viewer |
||||
|
* @param {Object} ctx |
||||
|
*/ |
||||
|
var instClassTagViewer = function(ctx){ |
||||
|
var $clm; |
||||
|
var clm = ctx.gjs.editor.get('SelectorManager'); |
||||
|
if(clm){ |
||||
|
$clm = new ClassTagsView({ |
||||
|
collection: new Selectors([]), |
||||
|
config: { |
||||
|
em: ctx.gjs.editor |
||||
|
}, |
||||
|
}).render(); |
||||
|
ctx.$fixture.append($clm.el); |
||||
|
} |
||||
|
return $clm; |
||||
|
}; |
||||
|
|
||||
|
before(function () { |
||||
|
this.$fixtures = $("#fixtures"); |
||||
|
this.$fixture = $('<div id="SelectorManager-fixture"></div>'); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
this.gjs = GrapesJS.init({ |
||||
|
stylePrefix: '', |
||||
|
storage: { autoload: 0, type:'none' }, |
||||
|
assetManager: { |
||||
|
storageType: 'none', |
||||
|
}, |
||||
|
container: '#SelectorManager-fixture', |
||||
|
}); |
||||
|
this.$fixture.empty().appendTo(this.$fixtures); |
||||
|
this.gjs.render(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete this.gjs; |
||||
|
}); |
||||
|
|
||||
|
after(function () { |
||||
|
this.$fixture.remove(); |
||||
|
}); |
||||
|
|
||||
|
describe('Interaction with Components', function() { |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
this.wrapper = this.gjs.editor.get('Components').getWrapper().get('components'); |
||||
|
this.$clm = instClassTagViewer(this); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete this.wrapper; |
||||
|
delete this.$clm; |
||||
|
}); |
||||
|
|
||||
|
it('Assign correctly new class to component', function() { |
||||
|
var model = this.wrapper.add({}); |
||||
|
model.get('classes').length.should.equal(0); |
||||
|
this.gjs.editor.set('selectedComponent', model); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
model.get('classes').length.should.equal(1); |
||||
|
model.get('classes').at(0).get('name').should.equal('test'); |
||||
|
}); |
||||
|
|
||||
|
it('Classes from components are correctly imported inside main container', function() { |
||||
|
var model = this.wrapper.add([ |
||||
|
{ classes: ['test11', 'test12', 'test13'] }, |
||||
|
{ classes: ['test11', 'test22', 'test22'] }, |
||||
|
]); |
||||
|
this.gjs.editor.get('SelectorManager').getAll().length.should.equal(4); |
||||
|
}); |
||||
|
|
||||
|
it('Class imported into component is the same model from main container', function() { |
||||
|
var model = this.wrapper.add({ classes: ['test1'] }); |
||||
|
var clModel = model.get('classes').at(0); |
||||
|
var clModel2 = this.gjs.editor.get('SelectorManager').getAll().at(0); |
||||
|
clModel.should.deep.equal(clModel2); |
||||
|
}); |
||||
|
|
||||
|
it('Can assign only one time the same class on selected component and the class viewer', function() { |
||||
|
var model = this.wrapper.add({}); |
||||
|
this.gjs.editor.set('selectedComponent', model); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
model.get('classes').length.should.equal(1); |
||||
|
model.get('classes').at(0).get('name').should.equal('test'); |
||||
|
this.$clm.collection.length.should.equal(1); |
||||
|
this.$clm.collection.at(0).get('name').should.equal('test'); |
||||
|
}); |
||||
|
|
||||
|
it('Removing from container removes also from selected component', function() { |
||||
|
var model = this.wrapper.add({}); |
||||
|
this.gjs.editor.set('selectedComponent', model); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
this.$clm.getClasses().find('.tag #close').trigger('click') |
||||
|
model.get('classes').length.should.equal(0); |
||||
|
}); |
||||
|
|
||||
|
it("Trigger correctly event on target with new class add", function() { |
||||
|
var spy = sinon.spy(); |
||||
|
var model = this.wrapper.add({}); |
||||
|
this.gjs.editor.set('selectedComponent', model); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
this.gjs.editor.on("targetClassAdded", spy); |
||||
|
this.$clm.addNewTag('test'); |
||||
|
spy.called.should.equal(false); |
||||
|
this.$clm.addNewTag('test2'); |
||||
|
spy.called.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
}); |
||||
@ -0,0 +1,94 @@ |
|||||
|
var modulePath = './../../../test/specs/selector_manager'; |
||||
|
|
||||
|
define([ |
||||
|
'SelectorManager', |
||||
|
modulePath + '/model/SelectorModels', |
||||
|
modulePath + '/view/ClassTagView', |
||||
|
modulePath + '/view/ClassTagsView', |
||||
|
modulePath + '/e2e/ClassManager' |
||||
|
], |
||||
|
function( |
||||
|
SelectorManager, |
||||
|
Models, |
||||
|
ClassTagView, |
||||
|
ClassTagsView, |
||||
|
e2e |
||||
|
) { |
||||
|
|
||||
|
describe('SelectorManager', function() { |
||||
|
|
||||
|
describe('Main', function() { |
||||
|
|
||||
|
var obj; |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
obj = new SelectorManager().init(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete obj; |
||||
|
}); |
||||
|
|
||||
|
it('Object exists', function() { |
||||
|
obj.should.be.exist; |
||||
|
}); |
||||
|
|
||||
|
it('No selectors inside', function() { |
||||
|
obj.getAll().length.should.equal(0); |
||||
|
}); |
||||
|
|
||||
|
it('Able to add default selectors', function() { |
||||
|
var cm = new SelectorManager().init({ |
||||
|
selectors: ['test1', 'test2', 'test3'], |
||||
|
}); |
||||
|
cm.getAll().length.should.equal(3); |
||||
|
}); |
||||
|
|
||||
|
it('Add new selector', function() { |
||||
|
obj.add('test'); |
||||
|
obj.getAll().length.should.equal(1); |
||||
|
}); |
||||
|
|
||||
|
it('Default new selector is a class type', function() { |
||||
|
obj.add('test'); |
||||
|
obj.get('test').get('type').should.equal('class'); |
||||
|
}); |
||||
|
|
||||
|
it('Check name property', function() { |
||||
|
var name = 'test'; |
||||
|
var sel = obj.add(name); |
||||
|
sel.get('name').should.equal(name); |
||||
|
sel.get('label').should.equal(name); |
||||
|
}); |
||||
|
|
||||
|
it('Add 2 selectors', function() { |
||||
|
obj.add('test'); |
||||
|
obj.add('test2'); |
||||
|
obj.getAll().length.should.equal(2); |
||||
|
}); |
||||
|
|
||||
|
it('Adding 2 selectors with the same name is not possible', function() { |
||||
|
obj.add('test'); |
||||
|
obj.add('test'); |
||||
|
obj.getAll().length.should.equal(1); |
||||
|
}); |
||||
|
|
||||
|
it('Get selector', function() { |
||||
|
var name = 'test'; |
||||
|
var sel = obj.add(name); |
||||
|
obj.get(name).should.deep.equal(sel); |
||||
|
}); |
||||
|
|
||||
|
it('Get empty class', function() { |
||||
|
(obj.get('test') === undefined).should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
Models.run(); |
||||
|
ClassTagView.run(); |
||||
|
ClassTagsView.run(); |
||||
|
e2e.run(); |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,53 @@ |
|||||
|
var path = 'SelectorManager/model/'; |
||||
|
define([path + 'Selector', |
||||
|
path + 'Selectors'], |
||||
|
function(Selector, Selectors) { |
||||
|
|
||||
|
return { |
||||
|
run : function(){ |
||||
|
describe('Selector', function() { |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
this.obj = new Selector(); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete this.obj; |
||||
|
}); |
||||
|
|
||||
|
it('Has name property', function() { |
||||
|
this.obj.has('name').should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Has label property', function() { |
||||
|
this.obj.has('label').should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Has active property', function() { |
||||
|
this.obj.has('active').should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('escapeName test', function() { |
||||
|
this.obj.escapeName('@Te sT*').should.equal('-te-st-'); |
||||
|
}); |
||||
|
|
||||
|
it('Name is corrected at instantiation', function() { |
||||
|
this.obj = new Selector({ name: '@Te sT*'}); |
||||
|
this.obj.get('name').should.equal('-te-st-'); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
describe('Selectors', function() { |
||||
|
|
||||
|
it('Creates collection item correctly', function() { |
||||
|
var c = new Selectors(); |
||||
|
var m = c.add({}); |
||||
|
m.should.be.an.instanceOf(Selector); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
}); |
||||
@ -0,0 +1,132 @@ |
|||||
|
var path = 'SelectorManager/view/'; |
||||
|
define([path + 'ClassTagView', 'SelectorManager/model/Selectors'], |
||||
|
function(ClassTagView, Selectors) { |
||||
|
|
||||
|
return { |
||||
|
run : function(){ |
||||
|
describe('ClassTagView', function() { |
||||
|
|
||||
|
var obj; |
||||
|
var fixture; |
||||
|
var fixtures; |
||||
|
var testLabel; |
||||
|
var coll; |
||||
|
|
||||
|
before(function () { |
||||
|
fixtures = $("#fixtures"); |
||||
|
fixture = $('<div class="classtag-fixture"></div>'); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
coll = new Selectors(); |
||||
|
testLabel = 'TestLabel'; |
||||
|
var model = coll.add({ |
||||
|
name: 'test', |
||||
|
label: testLabel, |
||||
|
}); |
||||
|
obj = new ClassTagView({ |
||||
|
config : {}, |
||||
|
model: model, |
||||
|
coll: coll |
||||
|
}); |
||||
|
obj.target = { get:function(){} }; |
||||
|
_.extend(obj.target, Backbone.Events); |
||||
|
fixture.empty().appendTo(fixtures); |
||||
|
fixture.html(obj.render().el); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete obj.model; |
||||
|
}); |
||||
|
|
||||
|
after(function () { |
||||
|
fixture.remove(); |
||||
|
}); |
||||
|
|
||||
|
it('Object exists', function() { |
||||
|
ClassTagView.should.be.exist; |
||||
|
}); |
||||
|
|
||||
|
it('Not empty', function() { |
||||
|
var $el = obj.$el; |
||||
|
$el.html().should.not.be.empty; |
||||
|
}); |
||||
|
|
||||
|
it('Not empty', function() { |
||||
|
var $el = obj.$el; |
||||
|
$el.html().should.contain(testLabel); |
||||
|
}); |
||||
|
|
||||
|
describe('Should be rendered correctly', function() { |
||||
|
|
||||
|
it('Has close button', function() { |
||||
|
var $el = obj.$el; |
||||
|
$el.find('#close').should.have.property(0); |
||||
|
}); |
||||
|
it('Has checkbox', function() { |
||||
|
var $el = obj.$el; |
||||
|
$el.find('#checkbox').should.have.property(0); |
||||
|
}); |
||||
|
it('Has label', function() { |
||||
|
var $el = obj.$el; |
||||
|
$el.find('#tag-label').should.have.property(0); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
it('Could be removed', function() { |
||||
|
var spy = sinon.spy(); |
||||
|
obj.config.target = { get:function(){} }; |
||||
|
sinon.stub(obj.config.target, 'get').returns(0); |
||||
|
obj.$el.find('#close').trigger('click'); |
||||
|
fixture.html().should.be.empty; |
||||
|
}); |
||||
|
|
||||
|
it('On remove triggers event', function() { |
||||
|
var spy = sinon.spy(); |
||||
|
sinon.stub(obj.target, 'get').returns(0); |
||||
|
obj.target.on("targetClassRemoved", spy); |
||||
|
obj.$el.find('#close').trigger('click'); |
||||
|
spy.called.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Checkbox toggles status', function() { |
||||
|
var spy = sinon.spy(); |
||||
|
obj.model.on("change:active", spy); |
||||
|
obj.model.set('active', true); |
||||
|
obj.$el.find('#checkbox').trigger('click'); |
||||
|
obj.model.get('active').should.equal(false); |
||||
|
spy.called.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('On toggle triggers event', function() { |
||||
|
var spy = sinon.spy(); |
||||
|
sinon.stub(obj.target, 'get').returns(0); |
||||
|
obj.target.on("targetClassUpdated", spy); |
||||
|
obj.$el.find('#checkbox').trigger('click'); |
||||
|
spy.called.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Label input is disabled', function() { |
||||
|
var inputProp = obj.inputProp; |
||||
|
obj.$labelInput.prop(inputProp).should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('On double click label input is enable', function() { |
||||
|
var inputProp = obj.inputProp; |
||||
|
obj.$el.find('#tag-label').trigger('dblclick'); |
||||
|
obj.$labelInput.prop(inputProp).should.equal(false); |
||||
|
}); |
||||
|
|
||||
|
it('On blur label input turns back disabled', function() { |
||||
|
var inputProp = obj.inputProp; |
||||
|
obj.$el.find('#tag-label').trigger('dblclick'); |
||||
|
obj.endEditTag(); |
||||
|
obj.$labelInput.prop(inputProp).should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
}); |
||||
@ -0,0 +1,195 @@ |
|||||
|
var path = 'SelectorManager/view/'; |
||||
|
define([path + 'ClassTagsView', 'SelectorManager/model/Selectors'], |
||||
|
function(ClassTagsView, Selectors) { |
||||
|
|
||||
|
return { |
||||
|
run : function(){ |
||||
|
describe('ClassTagsView', function() { |
||||
|
|
||||
|
var view; |
||||
|
var fixture; |
||||
|
var fixtures; |
||||
|
var testLabel; |
||||
|
var coll; |
||||
|
var target; |
||||
|
|
||||
|
before(function () { |
||||
|
fixtures = $("#fixtures"); |
||||
|
fixture = $('<div class="classtag-fixture"></div>'); |
||||
|
}); |
||||
|
|
||||
|
beforeEach(function () { |
||||
|
target = { get: function(){} }; |
||||
|
coll = new Selectors(); |
||||
|
_.extend(target, Backbone.Events); |
||||
|
|
||||
|
view = new ClassTagsView({ |
||||
|
config : { em: target }, |
||||
|
collection: coll |
||||
|
}); |
||||
|
|
||||
|
this.targetStub = { |
||||
|
add: function(v){ return {name: v}; } |
||||
|
}; |
||||
|
|
||||
|
this.compTargetStub = { |
||||
|
get: function(){ return { add: function(){} }} |
||||
|
}; |
||||
|
|
||||
|
fixture.empty().appendTo(fixtures); |
||||
|
fixture.html(view.render().el); |
||||
|
this.btnAdd = view.$el.find('#' + view.addBtnId); |
||||
|
this.input = view.$el.find('input#' + view.newInputId); |
||||
|
this.$tags = fixture.find('#tags-c'); |
||||
|
this.$states = fixture.find('#states'); |
||||
|
this.$statesC = fixture.find('#input-c'); |
||||
|
}); |
||||
|
|
||||
|
afterEach(function () { |
||||
|
delete view.collection; |
||||
|
}); |
||||
|
|
||||
|
after(function () { |
||||
|
fixture.remove(); |
||||
|
}); |
||||
|
|
||||
|
it('Object exists', function() { |
||||
|
ClassTagsView.should.be.exist; |
||||
|
}); |
||||
|
|
||||
|
it('Not tags inside', function() { |
||||
|
this.$tags.html().should.equal(''); |
||||
|
}); |
||||
|
|
||||
|
it('Add new tag triggers correct method', function() { |
||||
|
sinon.stub(view, "addToClasses"); |
||||
|
coll.add({ name: 'test' }); |
||||
|
view.addToClasses.calledOnce.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Start new tag creation', function() { |
||||
|
this.btnAdd.click(); |
||||
|
(this.btnAdd.css('display') == 'none').should.equal(true); |
||||
|
(this.input.css('display') !== 'none').should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Stop tag creation', function() { |
||||
|
this.btnAdd.click(); |
||||
|
this.input.val('test') |
||||
|
this.input.blur(); |
||||
|
(this.btnAdd.css('display') !== 'none').should.equal(true); |
||||
|
(this.input.css('display') == 'none').should.equal(true); |
||||
|
this.input.val().should.equal(''); |
||||
|
}); |
||||
|
|
||||
|
it('Check keyup of ESC on input', function() { |
||||
|
this.btnAdd.click(); |
||||
|
sinon.stub(view, "addNewTag"); |
||||
|
this.input.trigger({ |
||||
|
type: 'keyup', |
||||
|
keyCode: 13 |
||||
|
}); |
||||
|
view.addNewTag.calledOnce.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Check keyup on ENTER on input', function() { |
||||
|
this.btnAdd.click(); |
||||
|
sinon.stub(view, "endNewTag"); |
||||
|
this.input.trigger({ |
||||
|
type: 'keyup', |
||||
|
keyCode: 27 |
||||
|
}); |
||||
|
view.endNewTag.calledOnce.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it('Collection changes on update of target', function() { |
||||
|
coll.add({ name: 'test' }); |
||||
|
target.trigger('change:selectedComponent'); |
||||
|
coll.length.should.equal(0); |
||||
|
}); |
||||
|
|
||||
|
it('Collection reacts on reset', function() { |
||||
|
coll.add([{ name: 'test1' }, { name: 'test2' }]); |
||||
|
sinon.stub(view, "addToClasses"); |
||||
|
coll.trigger('reset'); |
||||
|
view.addToClasses.calledTwice.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it("Don't accept empty tags", function() { |
||||
|
view.addNewTag(''); |
||||
|
this.$tags.html().should.equal(''); |
||||
|
}); |
||||
|
|
||||
|
it("Accept new tags", function() { |
||||
|
sinon.stub(target, "get").returns(this.targetStub); |
||||
|
view.compTarget = this.compTargetStub; |
||||
|
view.addNewTag('test'); |
||||
|
view.compTarget = this.compTargetStub; |
||||
|
view.addNewTag('test2'); |
||||
|
this.$tags.children().length.should.equal(2); |
||||
|
}); |
||||
|
|
||||
|
it("New tag correctly added", function() { |
||||
|
coll.add({ label: 'test' }); |
||||
|
this.$tags.children().first().find('#tag-label input').val().should.equal('test'); |
||||
|
}); |
||||
|
|
||||
|
it("States are hidden in case no tags", function() { |
||||
|
view.updateStateVis(); |
||||
|
this.$statesC.css('display').should.equal('none'); |
||||
|
}); |
||||
|
|
||||
|
it("States are visible in case of more tags inside", function() { |
||||
|
coll.add({ label: 'test' }); |
||||
|
view.updateStateVis(); |
||||
|
this.$statesC.css('display').should.equal('block'); |
||||
|
}); |
||||
|
|
||||
|
it("Update state visibility on new tag", function() { |
||||
|
sinon.stub(view, "updateStateVis"); |
||||
|
sinon.stub(target, "get").returns(this.targetStub); |
||||
|
view.compTarget = this.compTargetStub; |
||||
|
view.addNewTag('test'); |
||||
|
view.updateStateVis.called.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it("Update state visibility on removing of the tag", function() { |
||||
|
sinon.stub(target, "get").returns(this.targetStub); |
||||
|
view.compTarget = this.compTargetStub; |
||||
|
view.addNewTag('test'); |
||||
|
sinon.stub(view, "updateStateVis"); |
||||
|
coll.remove(coll.at(0)); |
||||
|
view.updateStateVis.calledOnce.should.equal(true); |
||||
|
}); |
||||
|
|
||||
|
it("Output correctly state options", function() { |
||||
|
var view = new ClassTagsView({ |
||||
|
config : { |
||||
|
em: target, |
||||
|
states: [ { name: 'testName', label: 'testLabel' } ], |
||||
|
}, |
||||
|
collection: coll |
||||
|
}); |
||||
|
view.getStateOptions().should.equal('<option value="testName">testLabel</option>'); |
||||
|
}); |
||||
|
|
||||
|
describe('Should be rendered correctly', function() { |
||||
|
it('Has label', function() { |
||||
|
view.$el.find('#label').should.have.property(0); |
||||
|
}); |
||||
|
it('Has tags container', function() { |
||||
|
view.$el.find('#tags-c').should.have.property(0); |
||||
|
}); |
||||
|
it('Has add button', function() { |
||||
|
view.$el.find('#add-tag').should.have.property(0); |
||||
|
}); |
||||
|
it('Has states input', function() { |
||||
|
view.$el.find('#states').should.have.property(0); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
}); |
||||
Loading…
Reference in new issue