|
|
|
@ -1,73 +1,122 @@ |
|
|
|
import { template, debounce } from 'underscore'; |
|
|
|
import { isEmpty, isArray, isString, debounce } from 'underscore'; |
|
|
|
import Backbone from 'backbone'; |
|
|
|
import ClassTagView from './ClassTagView'; |
|
|
|
|
|
|
|
export default Backbone.View.extend({ |
|
|
|
template: template(` |
|
|
|
<div id="<%= pfx %>up"> |
|
|
|
<div id="<%= pfx %>label"><%= label %></div> |
|
|
|
<div id="<%= pfx %>status-c"> |
|
|
|
<span id="<%= pfx %>input-c"> |
|
|
|
<div class="<%= ppfx %>field <%= ppfx %>select"> |
|
|
|
<span id="<%= ppfx %>input-holder"> |
|
|
|
<select id="<%= pfx %>states"> |
|
|
|
<option value=""><%= statesLabel %></option> |
|
|
|
</select> |
|
|
|
</span> |
|
|
|
<div class="<%= ppfx %>sel-arrow"> |
|
|
|
<div class="<%= ppfx %>d-s-arrow"></div> |
|
|
|
template({ |
|
|
|
labelInfo, |
|
|
|
labelStates, |
|
|
|
labelHead, |
|
|
|
iconSync, |
|
|
|
iconAdd, |
|
|
|
pfx, |
|
|
|
ppfx |
|
|
|
}) { |
|
|
|
return ` |
|
|
|
<div id="${pfx}up" class="${pfx}header"> |
|
|
|
<div id="${pfx}label" class="${pfx}header-label">${labelHead}</div> |
|
|
|
<div id="${pfx}status-c" class="${pfx}header-status"> |
|
|
|
<span id="${pfx}input-c" data-states-c> |
|
|
|
<div class="${ppfx}field ${ppfx}select"> |
|
|
|
<span id="${ppfx}input-holder"> |
|
|
|
<select id="${pfx}states" data-states> |
|
|
|
<option value="">${labelStates}</option> |
|
|
|
</select> |
|
|
|
</span> |
|
|
|
<div class="${ppfx}sel-arrow"> |
|
|
|
<div class="${ppfx}d-s-arrow"></div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div id="${pfx}tags-field" class="${ppfx}field"> |
|
|
|
<div id="${pfx}tags-c" data-selectors></div> |
|
|
|
<input id="${pfx}new" data-input/> |
|
|
|
<span id="${pfx}add-tag" class="${pfx}tags-btn ${pfx}tags-btn__add" data-add> |
|
|
|
${iconAdd} |
|
|
|
</span> |
|
|
|
<span class="${pfx}tags-btn ${pfx}tags-btn__sync" style="display: none" data-sync-style> |
|
|
|
${iconSync} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div id="<%= pfx %>tags-field" class="<%= ppfx %>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"><%= selectedLabel %></div> |
|
|
|
<div id="<%= pfx %>sel"></div> |
|
|
|
<div style="clear:both"></div> |
|
|
|
</div>`), |
|
|
|
|
|
|
|
events: {}, |
|
|
|
<div class="${pfx}sels-info"> |
|
|
|
<div class="${pfx}label-sel">${labelInfo}:</div> |
|
|
|
<div class="${pfx}sels" data-selected></div> |
|
|
|
<div style="clear:both"></div> |
|
|
|
</div>`; |
|
|
|
}, |
|
|
|
|
|
|
|
events: { |
|
|
|
'change [data-states]': 'stateChanged', |
|
|
|
'click [data-add]': 'startNewTag', |
|
|
|
'focusout [data-input]': 'endNewTag', |
|
|
|
'keyup [data-input]': 'onInputKeyUp', |
|
|
|
'click [data-sync-style]': 'syncStyle' |
|
|
|
}, |
|
|
|
|
|
|
|
initialize(o = {}) { |
|
|
|
this.config = o.config || {}; |
|
|
|
this.pfx = this.config.stylePrefix || ''; |
|
|
|
this.ppfx = this.config.pStylePrefix || ''; |
|
|
|
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'; |
|
|
|
const { em } = this.config; |
|
|
|
const emitter = this.getStyleEmitter(); |
|
|
|
const coll = this.collection; |
|
|
|
this.target = this.config.em; |
|
|
|
this.em = em; |
|
|
|
|
|
|
|
const toList = 'component:toggled component:update:classes'; |
|
|
|
const toListCls = 'component:update:classes change:state'; |
|
|
|
this.listenTo(em, toList, this.componentChanged); |
|
|
|
this.listenTo(emitter, 'styleManager:update', this.componentChanged); |
|
|
|
this.listenTo( |
|
|
|
em, |
|
|
|
'component:toggled component:update:classes', |
|
|
|
this.componentChanged |
|
|
|
); |
|
|
|
this.listenTo(em, 'component:update:classes', this.updateSelector); |
|
|
|
|
|
|
|
this.listenTo(this.collection, 'add', this.addNew); |
|
|
|
this.listenTo(this.collection, 'reset', this.renderClasses); |
|
|
|
this.listenTo(this.collection, 'remove', this.tagRemoved); |
|
|
|
|
|
|
|
this.listenTo(em, toListCls, this.__handleStateChange); |
|
|
|
this.listenTo(em, 'styleable:change change:device', this.checkSync); // component:styleUpdate
|
|
|
|
this.listenTo(coll, 'add', this.addNew); |
|
|
|
this.listenTo(coll, 'reset', this.renderClasses); |
|
|
|
this.listenTo(coll, 'remove', this.tagRemoved); |
|
|
|
this.delegateEvents(); |
|
|
|
}, |
|
|
|
|
|
|
|
syncStyle() { |
|
|
|
const { em } = this; |
|
|
|
const target = this.getTarget(); |
|
|
|
const cssC = em.get('CssComposer'); |
|
|
|
const opts = { noDisabled: 1 }; |
|
|
|
const selectors = this.getCommonSelectors({ opts }); |
|
|
|
const state = em.get('state'); |
|
|
|
const mediaText = em.getCurrentMedia(); |
|
|
|
const ruleComponents = []; |
|
|
|
const rule = |
|
|
|
cssC.get(selectors, state, mediaText) || |
|
|
|
cssC.add(selectors, state, mediaText); |
|
|
|
let style; |
|
|
|
|
|
|
|
this.getTargets().forEach(target => { |
|
|
|
const ruleComponent = cssC.getIdRule(target.getId(), { |
|
|
|
state, |
|
|
|
mediaText |
|
|
|
}); |
|
|
|
style = ruleComponent.getStyle(); |
|
|
|
ruleComponent.setStyle({}); |
|
|
|
ruleComponents.push(ruleComponent); |
|
|
|
}); |
|
|
|
|
|
|
|
style && rule.addStyle(style); |
|
|
|
em.trigger('component:toggled'); |
|
|
|
em.trigger('component:sync-style', { |
|
|
|
component: target, |
|
|
|
selectors, |
|
|
|
mediaText, |
|
|
|
rule, |
|
|
|
ruleComponents, |
|
|
|
state |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
getStyleEmitter() { |
|
|
|
const { em } = this; |
|
|
|
const sm = em && em.get('StyleManager'); |
|
|
|
@ -120,8 +169,8 @@ export default Backbone.View.extend({ |
|
|
|
* @param {Object} e |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
startNewTag(e) { |
|
|
|
this.$addBtn.get(0).style.display = 'none'; |
|
|
|
startNewTag() { |
|
|
|
this.$addBtn.css({ display: 'none' }); |
|
|
|
this.$input.show().focus(); |
|
|
|
}, |
|
|
|
|
|
|
|
@ -130,8 +179,8 @@ export default Backbone.View.extend({ |
|
|
|
* @param {Object} e |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
endNewTag(e) { |
|
|
|
this.$addBtn.get(0).style.display = ''; |
|
|
|
endNewTag() { |
|
|
|
this.$addBtn.css({ display: '' }); |
|
|
|
this.$input.hide().val(''); |
|
|
|
}, |
|
|
|
|
|
|
|
@ -145,31 +194,77 @@ export default Backbone.View.extend({ |
|
|
|
else if (e.keyCode === 27) this.endNewTag(); |
|
|
|
}, |
|
|
|
|
|
|
|
checkStates() { |
|
|
|
const state = this.em.getState(); |
|
|
|
const statesEl = this.getStates(); |
|
|
|
statesEl && statesEl.val(state); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* Triggered when component is changed |
|
|
|
* @param {Object} e |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
componentChanged: debounce(function(target) { |
|
|
|
target = target || this.getTarget(); |
|
|
|
this.compTarget = target; |
|
|
|
let validSelectors = []; |
|
|
|
|
|
|
|
if (target) { |
|
|
|
const state = target.get('state'); |
|
|
|
state && this.getStates().val(state); |
|
|
|
const selectors = target.getSelectors(); |
|
|
|
validSelectors = selectors.getValid(); |
|
|
|
componentChanged: debounce(function({ targets } = {}) { |
|
|
|
this.updateSelection(targets); |
|
|
|
}), |
|
|
|
|
|
|
|
updateSelection(targets) { |
|
|
|
let trgs = targets || this.getTargets(); |
|
|
|
trgs = isArray(trgs) ? trgs : [trgs]; |
|
|
|
let selectors = []; |
|
|
|
|
|
|
|
if (trgs && trgs.length) { |
|
|
|
selectors = this.getCommonSelectors({ targets: trgs }); |
|
|
|
this.checkSync({ validSelectors: selectors }); |
|
|
|
} |
|
|
|
|
|
|
|
this.collection.reset(selectors); |
|
|
|
this.updateStateVis(trgs); |
|
|
|
|
|
|
|
return selectors; |
|
|
|
}, |
|
|
|
|
|
|
|
getCommonSelectors({ targets, opts = {} } = {}) { |
|
|
|
const trgs = targets || this.getTargets(); |
|
|
|
const selectors = trgs |
|
|
|
.map(tr => tr.getSelectors && tr.getSelectors().getValid(opts)) |
|
|
|
.filter(i => i); |
|
|
|
return this._commonSelectors(...selectors); |
|
|
|
}, |
|
|
|
|
|
|
|
_commonSelectors(...args) { |
|
|
|
if (!args.length) return []; |
|
|
|
if (args.length === 1) return args[0]; |
|
|
|
if (args.length === 2) |
|
|
|
return args[0].filter(item => args[1].indexOf(item) >= 0); |
|
|
|
|
|
|
|
return args |
|
|
|
.slice(1) |
|
|
|
.reduce((acc, item) => this._commonSelectors(acc, item), args[0]); |
|
|
|
}, |
|
|
|
|
|
|
|
checkSync: debounce(function() { |
|
|
|
const { $btnSyncEl, config, collection } = this; |
|
|
|
const target = this.getTarget(); |
|
|
|
let hasStyle; |
|
|
|
|
|
|
|
if (target && config.componentFirst && collection.length) { |
|
|
|
const style = target.getStyle(); |
|
|
|
hasStyle = !isEmpty(style); |
|
|
|
} |
|
|
|
|
|
|
|
this.collection.reset(validSelectors); |
|
|
|
this.updateStateVis(target); |
|
|
|
$btnSyncEl && $btnSyncEl[hasStyle ? 'show' : 'hide'](); |
|
|
|
}), |
|
|
|
|
|
|
|
getTarget() { |
|
|
|
return this.target.getSelected(); |
|
|
|
}, |
|
|
|
|
|
|
|
getTargets() { |
|
|
|
return this.target.getSelectedAll(); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* Update states visibility. Hides states in case there is no tags |
|
|
|
* inside collection |
|
|
|
@ -178,32 +273,55 @@ export default Backbone.View.extend({ |
|
|
|
updateStateVis(target) { |
|
|
|
const em = this.em; |
|
|
|
const avoidInline = em && em.getConfig('avoidInlineStyle'); |
|
|
|
const display = this.collection.length || avoidInline ? 'block' : 'none'; |
|
|
|
const display = this.collection.length || avoidInline ? '' : 'none'; |
|
|
|
this.getStatesC().css('display', display); |
|
|
|
this.updateSelector(target); |
|
|
|
}, |
|
|
|
|
|
|
|
__handleStateChange() { |
|
|
|
this.updateSelector(this.getTargets()); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* Update selector helper |
|
|
|
* @return {this} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
updateSelector(target) { |
|
|
|
const { pfx, collection, el } = this; |
|
|
|
const selected = target || this.getTarget(); |
|
|
|
this.compTarget = selected; |
|
|
|
if (!selected || !selected.get) return; |
|
|
|
|
|
|
|
const state = selected.get('state'); |
|
|
|
const coll = collection; |
|
|
|
let result = coll.getFullString(selected.getSelectors().getStyleable()); |
|
|
|
result = |
|
|
|
result || |
|
|
|
selected.get('selectorsAdd') || |
|
|
|
(selected.getId ? `#${selected.getId()}` : ''); |
|
|
|
result += state ? `:${state}` : ''; |
|
|
|
const elSel = el.querySelector(`#${pfx}sel`); |
|
|
|
elSel && (elSel.innerHTML = result); |
|
|
|
updateSelector(targets) { |
|
|
|
const elSel = this.el.querySelector('[data-selected]'); |
|
|
|
const result = []; |
|
|
|
let trgs = targets || this.getTargets(); |
|
|
|
trgs = isArray(trgs) ? trgs : [trgs]; |
|
|
|
|
|
|
|
trgs.forEach(target => result.push(this.__getName(target))); |
|
|
|
elSel && (elSel.innerHTML = result.join(', ')); |
|
|
|
this.checkStates(); |
|
|
|
}, |
|
|
|
|
|
|
|
__getName(target) { |
|
|
|
const { pfx, config, em } = this; |
|
|
|
const { selectedName, componentFirst } = config; |
|
|
|
let result; |
|
|
|
|
|
|
|
if (isString(target)) { |
|
|
|
result = `<span class="${pfx}sel-gen">${target}</span>`; |
|
|
|
} else { |
|
|
|
if (!target || !target.get) return; |
|
|
|
const selectors = target.getSelectors().getStyleable(); |
|
|
|
const state = em.get('state'); |
|
|
|
const idRes = target.getId |
|
|
|
? `<span class="${pfx}sel-cmp">${target.getName()}</span><span class="${pfx}sel-id">#${target.getId()}</span>` |
|
|
|
: ''; |
|
|
|
result = this.collection.getFullString(selectors); |
|
|
|
result = result |
|
|
|
? `<span class="${pfx}sel-rule">${result}</span>` |
|
|
|
: target.get('selectorsAdd') || idRes; |
|
|
|
result = componentFirst && idRes ? idRes : result; |
|
|
|
result += state ? `<span class="${pfx}sel-state">:${state}</span>` : ''; |
|
|
|
result = selectedName ? selectedName({ result, state, target }) : result; |
|
|
|
} |
|
|
|
|
|
|
|
return result && `<span class="${pfx}sel">${result}</span>`; |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
@ -211,11 +329,10 @@ export default Backbone.View.extend({ |
|
|
|
* @param {Object} e |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
stateChanged(e) { |
|
|
|
if (this.compTarget) { |
|
|
|
this.compTarget.set('state', this.$states.val()); |
|
|
|
this.updateSelector(); |
|
|
|
} |
|
|
|
stateChanged(ev) { |
|
|
|
const { em } = this; |
|
|
|
const { value } = ev.target; |
|
|
|
em.set('state', value); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
@ -224,24 +341,21 @@ export default Backbone.View.extend({ |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
addNewTag(label) { |
|
|
|
const target = this.target; |
|
|
|
const component = this.compTarget; |
|
|
|
const { em } = this; |
|
|
|
|
|
|
|
if (!label.trim()) { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (!label.trim()) return; |
|
|
|
|
|
|
|
if (target) { |
|
|
|
const sm = target.get('SelectorManager'); |
|
|
|
if (em) { |
|
|
|
const sm = em.get('SelectorManager'); |
|
|
|
const model = sm.add({ label }); |
|
|
|
|
|
|
|
if (component) { |
|
|
|
const compCls = component.getSelectors(); |
|
|
|
compCls.add(model); |
|
|
|
this.getTargets().forEach(target => { |
|
|
|
target.getSelectors().add(model); |
|
|
|
this.collection.add(model); |
|
|
|
this.updateStateVis(); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
this.endNewTag(); |
|
|
|
}, |
|
|
|
|
|
|
|
@ -284,7 +398,7 @@ export default Backbone.View.extend({ |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
getClasses() { |
|
|
|
return this.$el.find(`#${this.pfx}tags-c`); |
|
|
|
return this.$el.find('[data-selectors]'); |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
@ -293,7 +407,10 @@ export default Backbone.View.extend({ |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
getStates() { |
|
|
|
if (!this.$states) this.$states = this.$el.find('#' + this.stateInputId); |
|
|
|
if (!this.$states) { |
|
|
|
const el = this.$el.find('[data-states]'); |
|
|
|
this.$states = el[0] && el; |
|
|
|
} |
|
|
|
return this.$states; |
|
|
|
}, |
|
|
|
|
|
|
|
@ -308,22 +425,28 @@ export default Backbone.View.extend({ |
|
|
|
}, |
|
|
|
|
|
|
|
render() { |
|
|
|
const { em, pfx, ppfx, $el } = this; |
|
|
|
$el.html( |
|
|
|
this.template({ |
|
|
|
selectedLabel: em.t('selectorManager.selected'), |
|
|
|
statesLabel: em.t('selectorManager.emptyState'), |
|
|
|
label: em.t('selectorManager.label'), |
|
|
|
pfx, |
|
|
|
ppfx |
|
|
|
}) |
|
|
|
); |
|
|
|
this.$input = $el.find('input#' + this.newInputId); |
|
|
|
this.$addBtn = $el.find('#' + this.addBtnId); |
|
|
|
const { em, pfx, ppfx, config, $el, el } = this; |
|
|
|
const { render, iconSync, iconAdd } = config; |
|
|
|
const tmpOpts = { |
|
|
|
iconSync, |
|
|
|
iconAdd, |
|
|
|
labelHead: em.t('selectorManager.label'), |
|
|
|
labelStates: em.t('selectorManager.emptyState'), |
|
|
|
labelInfo: em.t('selectorManager.selected'), |
|
|
|
ppfx, |
|
|
|
pfx, |
|
|
|
el |
|
|
|
}; |
|
|
|
$el.html(this.template(tmpOpts)); |
|
|
|
const renderRes = render && render(tmpOpts); |
|
|
|
renderRes && renderRes !== el && $el.empty().append(renderRes); |
|
|
|
this.$input = $el.find('[data-input]'); |
|
|
|
this.$addBtn = $el.find('[data-add]'); |
|
|
|
this.$classes = $el.find('#' + pfx + 'tags-c'); |
|
|
|
this.$states = $el.find('#' + this.stateInputId); |
|
|
|
this.$statesC = $el.find('#' + this.stateInputC); |
|
|
|
this.$states.append(this.getStateOptions()); |
|
|
|
this.$btnSyncEl = $el.find('[data-sync-style]'); |
|
|
|
this.$input.hide(); |
|
|
|
const statesEl = this.getStates(); |
|
|
|
statesEl && statesEl.append(this.getStateOptions()); |
|
|
|
this.renderClasses(); |
|
|
|
$el.attr('class', `${this.className} ${ppfx}one-bg ${ppfx}two-color`); |
|
|
|
return this; |
|
|
|
|