Ext.define('Ext.app.bindinspector.ComponentDetail', { extend: 'Ext.panel.Panel', alias: 'widget.bindinspector-componentdetail', requires: [ 'Ext.form.field.Display', 'Ext.grid.Panel', 'Ext.layout.container.VBox', 'Ext.form.field.Checkbox', 'Ext.app.bindinspector.Util' ], layout: 'border', //borderRegionSwapMin: 1000, // see onCtResize activeCls: Ext.baseCSSPrefix + 'bindinspector-stub-active', descriptorCls: Ext.baseCSSPrefix + 'bindinspector-descriptor', multipleCls: Ext.baseCSSPrefix + 'bindinspector-mult-val', directCls: Ext.baseCSSPrefix + 'bindinspector-direct-val', inheritedCls: Ext.baseCSSPrefix + 'bindinspector-inherited-val', componentKeyCls: Ext.baseCSSPrefix + 'bindinspector-comp-key', componentDescCls: Ext.baseCSSPrefix + 'bindinspector-comp-desc', componentValCls: Ext.baseCSSPrefix + 'bindinspector-comp-val', lastItemCls: Ext.baseCSSPrefix + 'bindinspector-last-item', // the default view for the view model preview tab for when a component is clicked, but has no viewModel vmPreviewDefault: { xtype: 'container', region: 'east', split: true, width: '50%', cls: Ext.baseCSSPrefix + 'bindinspector-prev-default', padding: 20, layout: { type: 'hbox', align: 'middle', pack: 'center' }, items: [{ xtype: 'component', flex: 1, html: 'Select a component with a ViewModel (or inherited ViewModel) from the ComponentList to view the ViewModel details' }] }, initComponent: function() { var me = this, parentCt = me.up('bindinspector-container'), comp = me.component || {}, env = me.env, publishes = comp.publishes, bindings = comp.bindings, title = 'Bindings ⇒ ', vm = env.getInheritedVM(comp), bindData, publishesTbar; // the default view for the bindings preview tab for when a component is clicked, but has no bindings me.bindingsPreviewDefault = { xtype: 'panel', border: false, region: 'center', cls: Ext.baseCSSPrefix + 'bindinspector-prev-default', bodyPadding: 20, layout: { type: 'hbox', align: 'middle', pack: 'center' }, items: [{ xtype: 'component', flex: 1, html: 'Select a component with bindings from the ComponentList to view the bindings details along with the component\'s inherited ViewModel' }], dockedItems: [{ xtype: 'toolbar', dock: 'top', defaultButtonUI: 'default', items: ['->', { text: 'Open in dedicated tab', handler: function (btn) { btn.up('bindinspector-container').onComponentDblclick(null, comp); } }] }] }; // add the component's reference to the title if it has a reference if (comp.reference) { title += '[' + comp.reference + '] • '; } // build the bindings supporting data if the component has bindings and does not already have its bindData bindData = comp.bindData || Ext.app.bindinspector.Util.buildBindData(bindings); // add any publishes info the the top of the Component Detail grid if (publishes) { publishesTbar = [{ xtype: 'component', html: 'Publishes: <span style="color: #5C5C5C;font-size: 14px;line-height: 14px;">' + Ext.Object.getKeys(publishes).join(' • ') + '</span>' }]; } // the config for the bindings grid itself (shown when a component with bindings is selected from the ComponentList tree) me.bindingsGrid = { xtype: 'gridpanel', title: title += comp.id, header: { items: [{ xtype: 'button', text: 'Open in dedicated tab', ui: 'default-toolbar', handler: function (btn) { btn.up('bindinspector-container').onComponentDblclick(null, comp); } }] }, region: 'center', cls: Ext.baseCSSPrefix + 'bindinspector-compdetail-grid', flex: 1, hideHeaders: true, store: { model: me.BindingModel, data: bindData }, columns: [{ flex: 1, scope: me, renderer: me.bindingRenderer }], tbar: publishesTbar, bbar: [{ xtype: 'checkboxfield', itemId: 'highlightToggle', boxLabel: 'Highlight VM target nodes on binding selection', checked: true, listeners: { scope: me, change: me.onHighlightChange } }], viewConfig: { stripeRows: false, trackOver: false, getRowClass: function (record, index, rowParams, store) { var cls = []; if (index === store.getCount() - 1) { cls.push(me.lastItemCls); } return cls.join(' '); } }, listeners: { scope: me, cellclick: me.onCellClick, selectionchange: me.onSelectionChange } }; // the ViewModelDetail tree (shown when a component with a View Model is selected from the ComponentList tree) me.viewModelTree = { xtype: 'bindinspector-viewmodeldetail', itemId: 'vm-' + vm.id, vm: vm, region: 'east', split: true, width: '50%', height: '50%' }; me.items = []; me.items.push(bindings ? me.bindingsGrid : me.bindingsPreviewDefault); me.items.push(vm ? me.viewModelTree : me.vmPreviewDefault); me.callParent(arguments); }, // renderer for the bindings colum to show the bindings key, descriptor, and value bindingRenderer: function (v, meta, rec) { var me = this, binding = rec.get('binding'), key = rec.get('key'), descriptor = me.descriptorRenderer(rec.get('descriptor'), meta, rec), value = Ext.app.bindinspector.Util.valueRenderer(rec.get('value')), src = '', bindingType = 'Direct'; // provide a default output for empty / null bindings values if (Ext.isEmpty(value) || value === 'null') { value = '<i>No value found</i>'; } key = '<span class="' + me.componentKeyCls + '">' + key + ': </span>'; descriptor = '<span class="' + me.componentDescCls + '">' + descriptor + '</span>'; value = '<span class="' + me.componentValCls + '">' + value + '</span>'; if (binding.isTemplateBinding) { bindingType = 'Template'; } else if (binding.isMultiBinding) { bindingType = 'Multi' } bindingType = Ext.util.Format.format('<div data-qtip="Binding Type" data-qclass="' + Ext.baseCSSPrefix + 'componentlist-tip" class="' + Ext.baseCSSPrefix + 'bindinspector-bind-type">{0}</div>', bindingType); return key + descriptor + '<br>' + value + bindingType; }, // handler for the highlight or don't highlight checkbox in the component detail grid // selecting a binding detail node will highlight the source from the view model if the // checkbox is checked onHighlightChange: function (field) { var vmDetail = this.down('bindinspector-viewmodeldetail'), compDetail = this.down('gridpanel'), selModel = compDetail.getSelectionModel(); // if the checkbox is not checked then remove the highlighting from the view model detail view if (!field.checked) { this.onSelectionChange(selModel, null, true) } else { // else re-apply the highlighting this.onSelectionChange(selModel, selModel.getSelection()); } }, // when the component detail node is selected highlight (if allowed by the highlight checkbox) // the root data node from the view model detail tree onSelectionChange: function (selModel, selected, clear) { var vmDetail = this.down('bindinspector-viewmodeldetail'), store = vmDetail.getStore(), tokens, binding, highlight, store, root, targets, highlighted; if ((selected && selected.length === 0) || clear === true) { store.suspendEvents(); vmDetail.getStore().getRootNode().cascadeBy({ before: function (node) { node.set('highlighted', false); } }); store.resumeEvents(); vmDetail.getView().refresh(); return; } selected = selected[0]; tokens = selected.get('tokens'); binding = selected.get('binding'); highlight = this.down('#highlightToggle').checked; store = vmDetail.getStore(); root = store.getRootNode(); targets = []; // get the node or nodes from which the bindings are bound if (tokens && highlight) { if (binding.isTemplateBinding) { Ext.Array.forEach(tokens, function(token) { targets.push(root.findChild('name', token[0])); }, this); } else if (binding.isMultiBinding) { // TODO } else { targets.push(root.findChild('name', tokens[0])); } } store.suspendEvents(); root.cascadeBy({ before: function (node) { highlighted = targets.length === 0; Ext.Array.forEach(targets, function (target) { if (node === target || node.isAncestor(target)) { highlighted = true; } }); // this is set so that the view model detail panel's getRowClass // can apply the styling depending on whether the node is bound to this // particular binding (-1 highlights / fades since false means undecorated) node.set('highlighted', highlighted || -1); } }); store.resumeEvents(); vmDetail.getView().refresh(); // select / focus the first targetable view model tree node vmDetail.getSelectionModel().select(targets[0]); }, // if the cell click is on a descriptor token focus on its data node in the // view model tree onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) { var target = e.getTarget('.' + this.activeCls), path; if (target) { path = target.getAttribute('data-path'); this.showPath(path); } }, showPath: function(path) { this.selectPath(this.down('bindinspector-viewmodeldetail'), path); }, // select the view model tree's node using the passed path // which is furnished by the onCellClick -> showPath method selectPath: function(tab, path) { var node = tab.getRootNode(), parts = path.split('.'), len = parts.length, i; for (i = 0; node && i < len; ++i) { node = this.getChildByKey(node, parts[i]); } if (node) { tab.getSelectionModel().select(node); } }, getChildByKey: function(node, key) { var childNodes = node.childNodes; if (childNodes) { return Ext.Array.findBy(childNodes, function(child) { return child.get('name') === key; }); } return null; }, // extracts the descriptor markup for the component detail grid descriptorRenderer: function(v, meta, rec) { var binding = rec.get('binding'), descriptor = rec.get('descriptor'), tokens = rec.get('tokens'); v = v || ''; if (binding.isTemplateBinding) { Ext.Array.forEach(tokens, function(token) { var tokenRe = new RegExp('{' + token.join('\\.') + '}', 'g'); v = v.replace(tokenRe, this.parseTokens(token)); }, this); } else if (binding.isMultiBinding) { // TODO } else { return v.replace(descriptor, this.parseTokens(tokens)); } return Ext.String.htmlEncode(v); }, // decorates the descriptor markup using the descriptor tokens from the binding parseTokens: function(tokens) { var me = this, out = [], vm = me.env.getInheritedVM(me.component), currPath = '', currParent = vm.rootStub, direct = false, inherited = false, addlCls = '', tip = '', baseToken = '', ownerVms, len, vmPlural; tokens = tokens || []; Ext.Array.forEach(tokens, function(token) { var stub = Ext.app.bindinspector.Util.getChildStub(token, currParent), cls = '', value; if (stub) { value = stub.value; if (value !== undefined) { cls = me.activeCls; } } else { // TODO Never here... } out.push('<span data-path="' + currPath + token + '" class="stub ' + cls + '">' + token + '</span>'); currPath += token + '.'; currParent = stub; }, me); // determine whether a binding's source can be found on the nearest view model, is // inherited from some ancestor view model, or is on both this view model and an ancestor if (tokens[0]) { baseToken = tokens[0]; ownerVMs = vm.dataMap[tokens[0]].ownerVMs; len = ownerVMs.length; Ext.Array.forEach(ownerVMs, function (v) { if (v.id === vm.id) { direct = true; } if (v.id !== vm.id) { inherited = true; } }); } // set the class for the descriptor markup depending on whether the date source // is direct, inherited, or direct, but with ancestor view models also posessing the data if (direct && inherited) { addlCls += ' ' + me.multipleCls; vmPlural = len > 1 ? 'VMs' : 'VM'; tip = 'data-qclass="' + Ext.baseCSSPrefix + 'componentlist-tip" data-qtip="<b>' + baseToken + '</b> provided by this VM and ' + (len - 1) + ' ancestor ' + vmPlural + '"'; } else if (direct) { addlCls += ' ' + me.directCls; tip = 'data-qclass="' + Ext.baseCSSPrefix + 'componentlist-tip" data-qtip="<b>' + baseToken + '</b> is provided by this VM"'; } else if (inherited) { addlCls += ' ' + me.inheritedCls; vmPlural = len > 1 ? 'VMs' : 'VM'; tip = 'data-qclass="' + Ext.baseCSSPrefix + 'componentlist-tip" data-qtip="<b>' + baseToken + '</b> is provided by ' + len + ' ancestor ' + vmPlural + '"'; } return '<span ' + tip + 'class="' + me.descriptorCls + addlCls + '">{' + out.join('.') + '}</span>'; } }, function() { this.prototype.BindingModel = Ext.define(null, { extend: 'Ext.data.Model', fields: ['key', 'descriptor', 'tokens', 'value', 'binding'] }); });