/**
* This class manages a drag-drop Dashboard similar to the legacy Ext JS Portal example.
* The user-directed layout of the Dashboard is preserved the Ext JS `stateful` mechanism
* to preserve potentially dynamic user sizing and collapsed states as well as order of
* items in their columns.
*/
Ext.define('Ext.dashboard.Dashboard', {
extend: 'Ext.panel.Panel',
xtype: 'dashboard',
requires: [
'Ext.layout.container.Dashboard',
'Ext.dashboard.DropZone',
'Ext.dashboard.Column',
'Ext.dashboard.Part'
],
isDashboard: true,
cls: Ext.baseCSSPrefix + 'dashboard',
bodyCls: Ext.baseCSSPrefix + 'dashboard-body',
defaultType: 'dashboard-column',
scrollable: true,
layout: {
type: 'dashboard'
},
stateful: false,
idSeed: 1,
config: {
/**
* @cfg {Object} parts
* An object keyed by `id` for the parts that can be created for this `Dashboard`.
*/
parts: null
},
renderConfig: {
/**
* @cfg {Number} maxColumns
* The maximum number of visible columns.
* @accessor
*/
maxColumns: 4
},
/**
* @cfg {Number[]} columnWidths
* An array designating the width of columns in your dashboard's default state as described
* by the {@link #cfg-defaultContent} property. For example:
*
* columnWidths: [
* 0.35,
* 0.40,
* 0.25
* ]
*
* As you can see, this array contains the default widths for the 3 columns in the dashboard's
* initial view. The column widths should total to an integer value, typically 1 as shown
* above. When column widths exceed 1, they will be wrapped effectively creating "rows". This
* means that if your column widths add up to more than 1, you would still want the first few
* to total 1 to ensure that the first row fills the dashboard space. This applies whenever
* the column widths extend past an integer value.
*
* **Note:** columnWidths will not be utilized if there is stateful information that
* dictates different user saved column widths.
*/
/**
* @cfg {Object[]} defaultContent
* An array of {@link Ext.dashboard.Part part} configuration objects that define your
* dashboard's default state. These should not be confused with component configurations.
*
* Each config object should also include:
*
* + `type` - The type of {@link Ext.dashboard.Part part} that you want to be generated.
* + `columnIndex` - The column position in which the {@link Ext.dashboard.Part part} should
* reside.
* + `height` - The desired height of the {@link Ext.dashboard.Part part} to be generated.
*
* The remaining properties are specific to your part's config object. For example:
*
* defaultContent: [{
* type: 'rss',
* columnIndex: 0,
* height: 500,
* feedUrl: 'http://feeds.feedburner.com/extblog'
* }, {
* type: 'stockTicker',
* columnIndex: 1,
* height: 300
* }, {
* type: 'stocks',
* columnIndex: 1,
* height: 300
* }, {
* type: 'rss',
* columnIndex: 2,
* height: 350,
* feedUrl: 'http://rss.cnn.com/rss/edition.rss'
* }]
*
* Default column widths are defined by {@link #cfg-columnWidths} and not in these
* part config objects.
*
* **Note:** defaultContent will not be utilized if there is stateful information that
* dictates different user saved positioning and componentry.
*/
/**
* @event validatedrop
*/
/**
* @event beforedragover
*/
/**
* @event dragover
*/
/**
* @event beforedrop
*/
/**
* @event drop
*/
initComponent: function() {
var me = this;
me.callParent();
me.addStateEvents('remove');
},
applyParts: function(parts, collection) {
var id, part;
if (!collection) {
collection = new Ext.util.Collection({
decoder: Ext.Factory.part
});
}
for (id in parts) {
part = parts[id];
if (Ext.isString(part)) {
part = {
type: part
};
}
part.id = id;
part.dashboard = this;
collection.add(part);
}
return collection;
},
/**
* @private
*/
getPart: function(type) {
var parts = this.getParts();
return parts.getByKey(type);
},
addNew: function(type, columnIndex, beforeAfter) {
var me = this,
part = me.getPart(type);
part.displayForm(null, null, function(config) {
config.type = type;
me.addView(config, columnIndex, beforeAfter);
});
},
addView: function(instance, columnIndex, beforeAfter) {
var me = this,
// We are only concerned with columns (ignore splitters).
items = me.query('dashboard-column'),
count = items.length,
index = columnIndex || 0,
view = instance.id ? instance : me.createView(instance),
columnWidths = me.columnWidths,
column;
if (!count) {
// if the layout is empty, assert a full initially
column = me.add(0, me.createColumn({
columnWidth: (Ext.isArray(columnWidths) ? columnWidths[0] : 1)
}));
items = [column];
count = 1;
}
if (index >= count) {
index = count - 1;
beforeAfter = 1; // after
}
if (!beforeAfter) {
column = items[index];
if (column) {
return column.add(view);
}
}
if (beforeAfter > 0) {
// after...
++index;
}
column = me.createColumn();
if (columnWidths) {
column.columnWidth = columnWidths[index] || (columnWidths[index] = 1);
}
if (!column.items) {
column.items = [];
}
column.items.push(view);
column = me.add(column);
return column.items.first();
},
createColumn: function(config) {
var cycle = this.cycleLayout;
return Ext.apply({
items: [],
bubbleEvents: ['add', 'childmove', 'resize'],
listeners: {
expand: cycle,
collapse: cycle,
scope: this
}
}, config);
},
createView: function(config) {
var me = this,
type = config.type,
part = me.getPart(type),
view = part.createView(config);
if (!view.id) {
view.id = me.id + '_' + type + (me.idSeed++);
}
view.bubbleEvents = Ext.Array.from(view.bubbleEvents).concat(['expand', 'collapse']);
view.stateful = me.stateful;
view.listeners = {
removed: this.onItemRemoved,
scope: this
};
return view;
},
initEvents: function() {
this.callParent();
this.dd = new Ext.dashboard.DropZone(this, this.dropConfig);
},
/**
* Readjust column/splitter heights for collapsing child panels
* @private
*/
cycleLayout: function() {
this.updateLayout();
},
doDestroy: function() {
if (this.dd) {
Ext.destroy(this.dd);
}
this.callParent();
},
//-------------------------------------------------
// State and Item Persistence
applyState: function(state) {
delete state.items;
this.callParent([state]);
// eslint-disable-next-line vars-on-top
var me = this,
columnWidths = state.columnWidths,
items = me.items.items,
length = items.length,
columnLength = columnWidths ? columnWidths.length : 0,
i;
// Splitters have not been inserted so the length is sans-splitter
if (columnLength) {
me.columnWidths = [];
for (i = 0; i < length; ++i) {
me.columnWidths.push(
items[i].columnWidth = (i < columnLength) ? columnWidths[i] : (1 / length)
);
}
}
},
getState: function() {
var me = this,
columnWidths = [],
items = me.items.items,
state = me.callParent() || {},
length = items.length,
i, item;
for (i = 0; i < length; ++i) {
if (!(item = items[i]).isSplitter) {
columnWidths.push(item.columnWidth);
}
}
state.idSeed = me.idSeed;
state.items = me.serializeItems();
// only overwrite column widths if they are defined in the state
if (columnWidths.length) {
state.columnWidths = me.columnWidths = columnWidths;
}
// no column widths are defined from the state, let's remove it so any defaults
// can be used instead
else {
delete state.columnWidths;
}
return state;
},
initItems: function() {
var me = this,
defaultContent = me.defaultContent,
state;
if (me.stateful) {
state = Ext.state.Manager.get(me.getStateId());
defaultContent = (state && state.items) || defaultContent;
}
if (!me.items && defaultContent) {
me.items = me.deserializeItems(defaultContent);
}
me.callParent();
},
deserializeItems: function(serialized) {
var me = this,
length = serialized.length,
columns = [],
columnWidths = me.columnWidths,
maxColumns = me.getMaxColumns(),
column, columnIndex, columnWidth, i, item, partConfig;
for (i = 0; i < length; ++i) {
partConfig = serialized[i];
columnIndex = Math.min(partConfig.columnIndex || 0, maxColumns - 1);
delete partConfig.columnIndex;
if (!(column = columns[columnIndex])) {
columns[columnIndex] = column = me.createColumn();
columnWidth = columnWidths && columnWidths[columnIndex];
if (columnWidth) {
column.columnWidth = columnWidth;
}
}
item = me.createView(partConfig);
column.items.push(item);
}
for (i = 0, length = columns.length; i < length; ++i) {
column = columns[i];
if (!column.columnWidth) {
column.columnWidth = 1 / length;
}
}
return columns;
},
serializeItem: function(item) {
return Ext.apply({
type: item.part.id,
id: item.id,
columnIndex: item.columnIndex
}, item._partConfig);
},
serializeItems: function() {
var me = this,
items = me.items.items,
length = items.length,
ret = [],
columnIndex = 0,
child, childItems, i, item, j, k;
for (i = 0; i < length; ++i) {
item = items[i];
if (!item.isSplitter) {
childItems = item.items.items;
for (j = 0, k = childItems.length; j < k; ++j) {
child = childItems[j];
child.columnIndex = columnIndex;
ret.push(me.serializeItem(child));
}
++columnIndex;
}
}
return ret;
},
onItemRemoved: function(item, column) {
// Removing items from a Dashboard is a persistent action, so we must remove the
// state data for it or leak it.
if (item.stateful && !item.isMoving) {
Ext.state.Manager.clear(item.getStateId());
}
}
});