UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,565 lines (1,494 loc) 43.9 kB
/** * @fileoverview * Implementation of object inspector. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /utils/kekule.domUtils.js * requires /xbrowsers/kekule.x.js * requires /widgets/operation/kekule.operations.js * requires /widgets/kekule.widget.base.js * requires /widgets/commonCtrls/kekule.widget.formControls.js * requires /widgets/advCtrls/kekule.widget.valueListEditors.js * requires /widgets/advCtrls/objInspector/kekule.widget.objInspector.propEditors.js * requires /widgets/advCtrls/objInspector/kekule.widget.objInspector.operations.js */ (function(){ "use strict"; var DU = Kekule.DomUtils; var EU = Kekule.HtmlElementUtils; var SU = Kekule.StyleUtils; var CNS = Kekule.Widget.HtmlClassNames; /** @ignore */ Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, { PROPLISTEDITOR: 'K-PropListEditor', PROPLISTEDITOR_PROPEXPANDMARKER: 'K-PropListEditor-PropExpandMarker', PROPLISTEDITOR_PROPEXPANDED: 'K-PropListEditor-PropExpanded', PROPLISTEDITOR_PROPCOLLAPSED: 'K-PropListEditor-PropCollapsed', PROPLISTEDITOR_INDENTDECORATOR: 'K-PropListEditor-IndentDecorator', PROPLISTEDITOR_READONLY: 'K-PropListEditor-ReadOnly', OBJINSPECTOR: 'K-ObjInspector', OBJINSPECTOR_FLEX_LAYOUT: 'K-ObjInspector-Flex-Layout', OBJINSPECTOR_SUBPART: 'K-ObjInspector-SubPart', OBJINSPECTOR_OBJSINFOPANEL: 'K-ObjInspector-ObjsInfoPanel', OBJINSPECTOR_PROPINFOPANEL: 'K-ObjInspector-PropInfoPanel', OBJINSPECTOR_PROPINFO_TITLE: 'K-ObjInspector-PropInfoPanel-Title', OBJINSPECTOR_PROPINFO_DESCRIPTION: 'K-ObjInspector-PropInfoPanel-Description', OBJINSPECTOR_PROPLISTEDITOR_CONTAINER: 'K-ObjInspector-PropListEditorContainer' }); /** * An value list editor to edit object properties. * @class * @augments Kekule.Widget.ValueListEditor * * @property {Array} objects Objects inspected. * @property {String} keyField Which text should be displayed in key column of inspector. * Value can set to be 'name', 'title' or any other field of propInfo. If such field can not be found, name will be used instead. * @property {String} sortField How to sort rows in prop list. * Value can set to be 'key', 'name', 'title' or null, respectively sort by property name, title, or no sort. * @property {Bool} enableLiveUpdate Whether the editor update its value automatically when inspected objects changed. * @property {String} subPropNamePadding CSS padding-left style for indention of sub property name. Default is 1em. * @property {Bool} enableOperHistory Whether undo/redo function is enabled. Undo/redo will be functional only if property operHistory is also set. * @property {Kekule.OperationHistory} operHistory History of operations. Used to enable undo/redo function. */ /** * Invoked when the active row edit is finished and property is changed. Event param of it has field: {propertyInfo, propertyEditor, oldValue, newValue}. * @name Kekule.Widget.ObjPropListEditor#propertyChange * @event */ Kekule.Widget.ObjPropListEditor = Class.create(Kekule.Widget.ValueListEditor, /** @lends Kekule.Widget.ObjPropListEditor# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.ObjPropListEditor', /** @private */ PARENT_ROW_FIELD: '__$parentRow__', /** @private */ SUB_ROWS_FIELD: '__$subRows__', /** @constructs */ initialize: function(/*$super, */parentOrElementOrDocument) { this.tryApplySuper('initialize', [parentOrElementOrDocument]) /* $super(parentOrElementOrDocument) */; this.setValueDisplayMode(Kekule.Widget.ValueListEditor.ValueDisplayMode.SIMPLE); this.setPropStoreFieldValue('displayedPropScopes', [Class.PropertyScope.PUBLISHED]); this.setEnableLiveUpdate(true); this.setSubPropNamePadding('1em'); this.setUseKeyHint(true); }, /** @private */ initProperties: function() { this.defineProp('objects', {'dataType': DataType.ARRAY, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC, 'setter': function(value) { var objs = Kekule.ArrayUtils.toArray(value); var olds = this.getObjects(); this.setPropStoreFieldValue('objects', objs); this.inspectedObjectsChanged(objs, olds); } }); this.defineProp('displayedPropScopes', {'dataType': DataType.ARRAY, 'setter': function(value) { this.setPropStoreFieldValue('displayedPropScopes', value); this.updateAll(); } }); this.defineProp('enableLiveUpdate', {'dataType': DataType.BOOL}); this.defineProp('subPropNamePadding', {'dataType': DataType.STRING}); this.defineProp('activePropEditor', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null, 'scope': Class.PropertyScope.PUBLIC, 'getter': function() { var row = this.getActiveRow(); if (row) return this.getRowPropEditor(row); else return null; } }); this.defineProp('keyField', {'dataType': DataType.STRING}); this.defineProp('sortField', {'dataType': DataType.STRING}); this.defineProp('enableOperHistory', {'dataType': DataType.BOOL, 'serializable': false}); this.defineProp('operHistory', {'dataType': 'Kekule.OperationHistory', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC}); }, /** @private */ doFinalize: function(/*$super*/) { this.tryApplySuper('doFinalize') /* $super() */; }, /** @ignore */ initPropValues: function(/*$super*/) { this.tryApplySuper('initPropValues') /* $super() */; this.setKeyField('title'); this.setSortField('key'); }, /** @ignore */ doGetWidgetClassName: function(/*$super*/) { return this.tryApplySuper('doGetWidgetClassName') /* $super() */ + ' ' + CNS.PROPLISTEDITOR; }, /** * Notify objects property has been changed and need to update the whole inspector. * @private */ inspectedObjectsChanged: function(objects, oldObjects) { var objs = Kekule.ArrayUtils.toArray(objects); if (oldObjects) this._uninstallObjectsEventHandlers(oldObjects) if (objects) this._installObjectsEventHandlers(objs); this.updateAll(objs); }, /** @private */ _installObjectsEventHandlers: function(objects) { for (var i = 0, l = objects.length; i < l; ++i) { var obj = objects[i]; if (obj.addEventListener) obj.addEventListener('change', this.reactObjectsChange, this); } }, /** @private */ _uninstallObjectsEventHandlers: function(objects) { for (var i = 0, l = objects.length; i < l; ++i) { var obj = objects[i]; if (obj.removeEventListener) obj.removeEventListener('change', this.reactObjectsChange, this); } }, /** @private */ reactObjectsChange: function(e) { if (this.getEnableLiveUpdate()) { //this.updateAll(); var changedPropNames = e.changedPropNames; this.updateValues(changedPropNames); } }, /** @private */ getKeyCellExpandMarker: function(keyCell, canCreate) { var result = null; var wrapper = this.getCellContentWrapper(keyCell, canCreate); if (wrapper) { var children = DU.getDirectChildElems(wrapper) || []; for (var i = 0, l = children.length; i < l; ++i) { var child = children[i]; if (EU.hasClass(child, CNS.PROPLISTEDITOR_PROPEXPANDMARKER)) { result = child; break; } } if (!result && canCreate) // create new { var doc = this.getDocument(); result = doc.createElement('span'); EU.addClass(result, CNS.PROPLISTEDITOR_PROPEXPANDMARKER); var refChild = DU.getChildNodesOfTypes(wrapper, [Node.ELEMENT_NODE, Node.TEXT_NODE])[0]; wrapper.insertBefore(result, refChild); } } return result; }, /** @private */ getKeyCellIndentDecorator: function(keyCell, canCreate) { var result = null; var wrapper = this.getCellContentWrapper(keyCell, canCreate); if (wrapper) { var children = DU.getDirectChildElems(wrapper) || []; for (var i = 0, l = children.length; i < l; ++i) { var child = children[i]; if (EU.hasClass(child, CNS.PROPLISTEDITOR_INDENTDECORATOR)) { result = child; break; } } if (!result && canCreate) // create new { var doc = this.getDocument(); result = doc.createElement('div'); EU.addClass(result, CNS.PROPLISTEDITOR_INDENTDECORATOR); var refChild = DU.getChildNodesOfTypes(wrapper, [Node.ELEMENT_NODE, Node.TEXT_NODE])[0]; wrapper.insertBefore(result, refChild); } } return result; }, /** * Override from ValueListEditor, add additional padding and expand mark before text. * @private */ setRowKeyCellContent: function(/*$super, */row, keyText, keyHint) { var result = this.tryApplySuper('setRowKeyCellContent', [row, keyText, keyHint]) /* $super(row, keyText, keyHint) */; var cell = this.getKeyCell(row); // insert expand mark var marker = this.getKeyCellExpandMarker(cell, true); // already inserted in this method // insert indentDecorator //var decorator = this.getKeyCellIndentDecorator(cell, true); // consider row sub level, set different padding and decoration var subLevel = this.getRowSubLevel(row); //if (subLevel > 0) { var spadding = this.getSubPropNamePadding(); if (spadding) { var sDecorationWidth = Kekule.StyleUtils.multiplyUnitsValue(spadding, subLevel); spadding = Kekule.StyleUtils.multiplyUnitsValue(spadding, subLevel); var keyCell = this.getKeyCell(row); var wrapper = this.getCellContentWrapper(keyCell, true); wrapper.style.paddingLeft = spadding; //decorator.style.width = sDecorationWidth; } } return result; }, /** * Force to update object inspector rows based on current objects. */ updateAll: function(objs) { if (!objs) objs = this.getObjects(); if (!objs || !objs.length) { //console.log('clear'); this.clear(); return; } this.setActiveRow(null); this.collapseAllRows(); // IMPORTANT, otherwise rows may be removed dynamically in the following loop and cause rowcount to be inacurrate var obj = objs[0]; /* var propNames = this.getDisplayPropNames(objs); var superClass = this.getCommonSuperClass(objs); */ var propEditors = this.getDisplayPropEditors(objs); this.sortPropEditors(propEditors); /* // construct hash var hash = {}; for (var i = 0, l = propNames.length; i < l; ++i) { var propName = propNames[i]; var propValue = this.getObjsPropValue(objs, propName); hash[propName] = propValue; } this.setHash(hash); */ var multiple = objs.length > 1; var rows = this.getRows(); var rowCount = rows.length; //var l = propNames.length; var l = propEditors.length; var i = 0; var currRowIndex = 0; var valueDisplayMode = this.getValueDisplayMode(); for (var i = 0; i < l; ++i) { /* var propName = propNames[i]; var propInfo = obj.getPropInfo(propName); var propEditor = this.getPropEditor(superClass, propInfo); propEditor.setObjects(objs); propEditor.setPropertyInfo(propInfo); */ var propEditor = propEditors[i]; propEditor.setValueTextMode(valueDisplayMode); /* if (propEditor instanceof Kekule.PropertyEditor.ObjectExEditor) { console.log('ObjectEx editor', propName, propInfo.dataType); } */ if (!this.isPropEditorVisible(propEditor)) continue; var row = (currRowIndex < rowCount)? rows[currRowIndex]: this.appendRow(); this.setRowPropEditor(row, /*propName, objs,*/ propEditor); ++currRowIndex; } // delete more rows if (rowCount > currRowIndex) { for (var i = rowCount - 1; i >= currRowIndex; --i) { this.removeRow(rows[i]); } } return this; }, /** * Update only values in widget. * @param {Array} propNames If this property is set, only those properties will be updated. * @private */ updateValues: function(propNames) { var limitedProps = propNames && propNames.length; var rows = this.getRows(); for (var i = 0, l = rows.length; i < l; ++i) { var row = rows[i]; if (limitedProps) { var propEditor = this.getRowPropEditor(row); // TODO: need further check, why some rows do not have propEditor (e.g., absCoord3D property of sub group in molecule). if (!propEditor || (propNames.indexOf(propEditor.getPropertyName()) < 0)) continue; } this.updateRowPropValue(row); } }, /** @private */ updateRowPropValue: function(row) { var propEditor = this.getRowPropEditor(row); var newValue = propEditor.getValueText(); var data = this.getRowData(row); data.value = newValue; this.setRowData(row, data); this.updateRowExpandState(row); // if has sub property rows, update too var subRows = this.getSubPropRows(row); if (subRows) { //console.log(propEditor.getPropertyName(), 'sub row', subRows.length); for (var i = 0, l = subRows.length; i < l; ++i) { this.updateRowPropValue(subRows[i]); } } }, /** * Set row property editor and modify row display according to it. * @param {HTMLElement} row * @param {Object} propEditor * @private */ setRowPropEditor: function(row, /*propName, objects,*/ propEditor) { /* if (!objects) objects = this.getObjects(); var obj = objects[0]; if (obj) { var propInfo = obj.getPropInfo? obj.getPropInfo(propName): null; //var propEditor = this.getPropEditor(propInfo); //console.log('after set', propEditor, propEditor.getObjects(), propEditor.getPropInfo()); var data = { 'key': propEditor.getPropertyName(), //propName, 'value': propEditor.getValueText(), 'title': propEditor.getTitle(), //'propInfo': propInfo, 'propEditor': propEditor }; this.setRowData(row, data); } */ var title; var titleField = this.getKeyField() || 'title'; // default is title if (titleField) { if (titleField === 'name') title = propEditor.getPropertyName(); else if (titleField === 'title') title = propEditor.getTitle(); else { var propInfo = propEditor.getPropertyInfo(); title = propInfo[titleField]; } } if (!title) title = propEditor.getTitle() || propEditor.getPropertyName(); var data = { 'key': propEditor.getPropertyName(), //propName, 'value': propEditor.getValueText(), 'title': title, 'hint': propEditor.getHint(), //'propInfo': propInfo, 'propEditor': propEditor, 'expanded': false }; /* var hasSubProps = propEditor.hasSubPropertyEditors(); if (hasSubProps) { EU.addClass(row, CNS.PROPLISTEDITOR_PROPCOLLAPSED); } */ var readOnly = propEditor.isReadOnly(); if (readOnly) EU.addClass(row, CNS.PROPLISTEDITOR_READONLY); else EU.removeClass(row, CNS.PROPLISTEDITOR_READONLY); this.setRowData(row, data); this.updateRowExpandState(row); }, /* @private */ /* getPropDisplayTitle: function(propInfo) { if (propInfo) return propInfo.title || propInfo.name; else return null; }, */ /* @private */ /* getRowPropInfo: function(row) { return this.getRowData(row).propInfo; }, */ /** * Returns key name like 'parentPropName.childPropName'. * @param {HTMLElement} row * @returns {String} */ getRowCascadeKey: function(row) { if (row && this.getRowData(row)) { var result = this.getRowData(row).key; var parentRow = this.getParentPropRow(row); if (parentRow) { result = this.getRowCascadeKey(parentRow) + '.' + result; } return result; } else return null; }, /** * Returns cascade key of current selected row. * @returns {String} */ getActiveRowCascadeKey: function() { var row = this.getActiveRow(); return row && this.getRowCascadeKey(row); }, /** @private */ getRowPropEditor: function(row) { var data = this.getRowData(row); if (data) return data.propEditor; else { //console.log(row.tagName, row); return null; } }, /** * Returns property editor instance associated with this row. * @param {Class} objClass * @param {Object} propInfo * @private */ getPropEditor: function(objClass, propInfo) { // debug //return new Kekule.PropertyEditor.SimpleEditor(); //return new Kekule.PropertyEditor.SelectEditor(); var c = Kekule.PropertyEditor.findEditorClass(objClass, propInfo); if (c) return new c(); else return null; }, /** * Returns property editor instance associated with a native JavaScript type (object, array...). * @param {String} propType * @param {String} propName * @private */ getPropEditorForType: function(propType, propName) { var c = Kekule.PropertyEditor.findEditorClassForType(propType, propName); if (c) return new c(); else return null; }, /** * Check if row on propEditor should be displayed in prop list editor. * @param {Object} propEditor * @returns {Bool} * @private */ isPropEditorVisible: function(propEditor) { var result = true; var objs = propEditor.getObjects(); var multiple = objs.length > 1; //result = !(multiple && (!(propEditor.getAttributes() & Kekule.PropertyEditor.EditorAttributes.MULTIOBJS))); result = !multiple || (propEditor.getAttributes() & Kekule.PropertyEditor.EditorAttributes.MULTIOBJS); // check scope if (result) { var propInfo = propEditor.getPropertyInfo(); if (propInfo) { var propScope = propInfo.scope || Class.PropertyScope.DEFAULT; if (propScope) { var displayScopes = this.getDisplayedPropScopes(); if (displayScopes) result = displayScopes.indexOf(propScope) >= 0; } } } return result; }, /** * Check if a row has sub properties and can be expanded. * @param {HTMLElement} row * @returns {Bool} */ isRowExpandable: function(row) { var propEditor = this.getRowPropEditor(row); return propEditor? propEditor.hasSubPropertyEditors(): false; }, /** * Check if a row with sub properties is expanded. * @param {HTMLElement} row */ isRowExpanded: function(row) { var data = this.getRowData(row); return !!data.expanded; }, /** @private */ updateRowExpandState: function(row) { var canExpand = this.isRowExpandable(row); if (!canExpand) // remove unnessseary sub rows { this.removeSubPropertyRows(row); } EU.removeClass(row, CNS.PROPLISTEDITOR_PROPCOLLAPSED); EU.removeClass(row, CNS.PROPLISTEDITOR_PROPEXPANDED); if (canExpand) { if (this.isRowExpanded(row)) EU.addClass(row, CNS.PROPLISTEDITOR_PROPEXPANDED); else EU.addClass(row, CNS.PROPLISTEDITOR_PROPCOLLAPSED); } }, /** * Expand row with sub properties. * @param {HTMLElement} row */ expandRow: function(row) { this._doExpandRow(row); return this; }, /** @private */ _doExpandRow: function(row) { if (this.isRowExpandable(row) && (!this.isRowExpanded(row))) { EU.removeClass(row, CNS.PROPLISTEDITOR_PROPCOLLAPSED); EU.addClass(row, CNS.PROPLISTEDITOR_PROPEXPANDED); var data = this.getRowData(row); data.expanded = true; var propEditor = this.getRowPropEditor(row); //console.log(this.getDisplayedPropScopes()); var subPropEditors = propEditor.getSubPropertyEditors(this.getDisplayedPropScopes()); row[this.SUB_ROWS_FIELD] = this.addSubPropertyRows(row, subPropEditors); return row[this.SUB_ROWS_FIELD]; } else return null; }, /** * Collapse row with sub properties. * @param {HTMLElement} row */ collapseRow: function(row) { if (this.isRowExpandable(row) && this.isRowExpanded(row)) { EU.removeClass(row, CNS.PROPLISTEDITOR_PROPEXPANDED); EU.addClass(row, CNS.PROPLISTEDITOR_PROPCOLLAPSED); var data = this.getRowData(row); data.expanded = false; this.removeSubPropertyRows(row); } return this; }, /** * Expand a collapsed row or collapse an expanded row. * @param {HTMLElement} row */ toggleRowExpandState: function(row) { if (this.isRowExpanded(row)) { this.collapseRow(row); } else { this.expandRow(row); } return this; }, /** * Collapse all rows in editor. */ collapseAllRows: function() { var rows = this.getRows(); for (var i = 0, l = rows.length; i < l; ++i) { this.collapseRow(rows[i]); } return this; }, /** * Expand all rows in editor. * @param {Bool} cascade */ expandAllRows: function(cascade) { var self = this; var doExpandAll = function(rows, cascade) { for (var i = 0, l = rows.length; i < l; ++i) { var addedRows = self._doExpandRow(rows[i]); if (addedRows && cascade) { doExpandAll(addedRows, cascade); } } } var rows = this.getRows(); doExpandAll(rows, cascade); return this; }, /** @private */ addSubPropertyRows: function(parentRow, propEditors) { var refRow = this.getNextRow(parentRow); var subRows = []; if (propEditors && propEditors.length) { /* propEditors.sort(function(a, b){ var na = a.getPropertyName(); var nb = b.getPropertyName(); return (na < nb)? -1: (na > nb)? 1: 0; }); */ this.sortPropEditors(propEditors); for (var i = 0, l = propEditors.length; i < l; ++i) { var subPropEditor = propEditors[i]; if (!this.isPropEditorVisible(subPropEditor)) continue; subRows.push(this.addSubPropRow(parentRow, subPropEditor, refRow)); } } return subRows; }, /** @private */ addSubPropRow: function(parentRow, propEditor, refRow) { var result = this.insertRowBefore(null, refRow); result[this.PARENT_ROW_FIELD] = parentRow; this.setRowPropEditor(result, propEditor); return result; }, /** @private */ removeSubPropertyRows: function(parentRow) { var subRows = this.getSubPropRows(parentRow); for (var i = subRows.length - 1; i >= 0; --i) { this.removeSubPropertyRows(subRows[i]); this.removeRow(subRows[i]); } parentRow[this.SUB_ROWS_FIELD] = null; }, /** @private */ getSubPropRows: function(row) { return row[this.SUB_ROWS_FIELD] || []; }, /** @private */ getParentPropRow: function(row) { return row[this.PARENT_ROW_FIELD]; }, /** @private */ getRowSubLevel: function(row) { var result = 0; var parentRow = this.getParentPropRow(row); while (parentRow) { ++result; parentRow = this.getParentPropRow(parentRow); } return result; }, // override methods /* @private */ getValueCellText: function(/*$super, */row, data) { /* if (data.value === undefined) return ''; else return $super(row, data); */ return data.value; }, /** @private */ finishEditing: function() { var row = this.getActiveRow(); var propEditor = this.getRowPropEditor(row); //console.log('finish editing', propEditor.getPropertyName()); if (propEditor) { var operHistory = this.getOperHistory(); var enableHistory = this.getEnableOperHistory() && operHistory; var oldValue = propEditor.getValue(); var modified = propEditor.saveEditValue(); if (modified) { if (enableHistory) { var oper = new Kekule.Widget.PropEditorModifyOperation(propEditor, propEditor.getValue(), oldValue); operHistory.push(oper); } //if (!this.getEnableLiveUpdate()) this.updateValues(); // invoke event this.invokeEvent('propertyChange', { 'propertyEditor': propEditor, 'propertyInfo': propEditor.getPropertyInfo(), 'oldValue': oldValue, 'newValue': propEditor.getValue() }); // invoke row edit event, as we not inherit finishEditing method from value list editor this.invokeEvent('editFinish', {'row': row}); } } return this; }, /** @private */ doCreateValueEditWidget: function(/*$super, */row) { //var result = $super(row); var propEditor = this.getRowPropEditor(row); // use propEditor to create edit widget var result = propEditor.createEditWidget(this); if (result) { if (this.getReadOnly() || propEditor.isReadOnly()) { if (result.setReadOnly) result.setReadOnly(true); } } else // widget not created by propEditor, create a readonly default one { result = this.doCreateDefaultValueEditWidget(row); } return result; }, /** @private */ doCreateDefaultValueEditWidget: function(row) { var result = new Kekule.Widget.TextBox(this); result.setReadOnly(true); var value = this.getRowData(row).value; result.setValue(value); return result; }, // oper history methods /** * Check if operation history can be used. * @returns {Bool} */ isOperHistoryAvailable: function() { return this.getOperHistory() && this.getEnableOperHistory(); }, /** * Undo a property setting operation. */ undo: function() { if (this.isOperHistoryAvailable()) this.getOperHistory().undo(); return this; }, /** * Redo a property setting operation. */ redo: function() { if (this.isOperHistoryAvailable()) this.getOperHistory().redo(); return this; }, // assoc methods /** * Returns common super class of all objects. * @param {Array} objects * @returns {Class} * @private */ getCommonSuperClass: function(objects) { return ClassEx.getCommonSuperClass(objects); }, /** * Returns common property names of all objects. * @param {Array} objects * @returns {Array} * @private */ getCommonPropNames: function(objects) { var result = []; var superClass = this.getCommonSuperClass(objects); if (superClass) { // TODO: we may filter out prop of unneed scope here //var propList = ClassEx.getAllPropList(superClass); var propList = ClassEx.getPropListOfScopes(superClass, this.getDisplayedPropScopes()); for (var i = 0, l = propList.getLength(); i < l; ++i) { var propInfo = propList.getPropInfoAt(i); result.push(propInfo.name); } //result.sort(); } else // no common super class { // TODO: if objects are plain object (not ObjectEx), how to handle } return result; }, /** * Returns property names that need to be shown in object inspector first level. * @param {Array} objects * @returns {Array} * @private */ getDisplayPropNames: function(objects) { return this.getCommonPropNames(objects); }, /** * Returns property editors that need to be shown in object inspector first level. * @param {Array} objects * @returns {Array} * @private */ getDisplayPropEditors: function(objects) { var result = []; //var propNames = this.getDisplayPropNames(objects); var superClass = this.getCommonSuperClass(objects); if (superClass) // is ObjectEx { /* var obj = objects[0]; for (var i = 0, l = propNames.length; i < l; ++i) { var propName = propNames[i]; var propInfo = obj.getPropInfo(propName); var propEditor = this.getPropEditor(superClass, propInfo); propEditor.setObjects(objects); propEditor.setPropertyInfo(propInfo); result.push(propEditor); } */ var basePropEditor = this.getPropEditor(null, {'dataType': ClassEx.getClassName(superClass)}); if (basePropEditor) { basePropEditor.setObjects(objects); result = result.concat(basePropEditor.getSubPropertyEditors(this.getDisplayedPropScopes()) || []); } } else // no super class, maybe objects are plain object? { if (objects.length === 1) // now only handle one object { var obj = objects[0]; if (DataType.isObjectValue(obj)) { var basePropEditor = this.getPropEditorForType('object'); if (basePropEditor) { basePropEditor.setObjects(/*objects*/obj); result = result.concat(basePropEditor.getSubPropertyEditors() || []); } } } } //console.log(basePropEditor.getClassName()); return result; }, /** * Sort property editors. * @param {Array} propEditors * @private */ sortPropEditors: function(propEditors) { var sortField = this.getSortField(); if (!sortField) // no sort return; if (sortField === 'key') sortField === this.getKeyField() || 'title'; var getSortFieldValue = function(propInfo, sortField) { return propInfo[sortField] || propInfo.name; // if value not found, fall back to name }; propEditors.sort(function(a, b){ var na = getSortFieldValue(a.getPropertyInfo(), sortField); var nb = getSortFieldValue(b.getPropertyInfo(), sortField); return (na < nb)? -1: (na > nb)? 1: 0; }); }, /** * Returns property or field value of object. * @param {Object} obj A plain object or instance of ObjectEx. * @param {String} propName * @returns {Variant} */ getObjPropValue: function(obj, propName) { if (obj instanceof ObjectEx) { return obj.getPropValue(propName); } else if (obj instanceof Object) { return obj[propName]; } else return undefined; }, /** * Returns property value of multiple objects. * If all objects have the same property value, this value will be returned. * Otherwise this method will return undefined. * @param {Array} objects * @param {String} propName * @returns {Variant} */ getObjsPropValue: function(objects, propName) { var obj = objects[0]; var result = this.getObjPropValue(obj, propName); for (var i = 1, l = objects.length; i < l; ++i) { var obj = objects[i]; var value = this.getObjPropValue(obj, propName); if (value !== result) return undefined; } return result; }, // UI event handlers /** @private */ react_click: function(/*$super, */e) // use $super to avoid override ValueListEditor.react_click { var target = e.getTarget(); // if click on expand marker of row var row = this.getParentRow(target); if (row) { var expandMarker = this.getKeyCellExpandMarker(this.getKeyCell(row), false); if (target === expandMarker) { this.toggleRowExpandState(row); } } this.tryApplySuper('react_click', [e]) /* $super(e) */; }, /** @private */ react_dblclick: function(e) { var target = e.getTarget(); var cell = this.getParentCell(target); var handled = false; if (cell) { var row = this.getParentRow(cell); if (cell === this.getKeyCell(row)) // double click on key cell { this.toggleRowExpandState(row); handled = true; } } }, /** @private */ react_keydown: function(/*$super, */e) // avoid overwrite ValueListEditor.react_keydown { this.tryApplySuper('react_keydown', [e]) /* $super(e) */; var keyCode = e.getKeyCode(); var noModifier = !(e.getAltKey() || e.getShiftKey() || e.getCtrlKey()); if (noModifier) { var currRow = this.getActiveRow(); if (currRow) { if (keyCode === Kekule.X.Event.KeyCode.RIGHT) // expand row { this.expandRow(currRow); } else if (keyCode === Kekule.X.Event.KeyCode.LEFT) { this.collapseRow(currRow); } } } } }); /** * An object inspector with prop editor and associated components. * @class * @augments Kekule.Widget.BaseWidget * * @property {Array} objects Objects inspected. * @property {Bool} showObjsInfoPanel * @property {Bool} showPropInfoPanel * @property {String} keyField Which text should be displayed in key column of inspector. * Value can set to be 'name', 'title' or any other field of propInfo. If such field can not be found, name will be used instead. * @property {String} sortField How to sort rows in prop list. * Value can set to be 'key', 'name', 'title' or null, respectively sort by property name, title, or no sort. * @property {Bool} enableOperHistory Whether undo/redo function is enabled. Undo/redo will be functional only if property operHistory is also set. * @property {Kekule.OperationHistory} operHistory History of operations. Used to enable undo/redo function. */ Kekule.Widget.ObjectInspector = Class.create(Kekule.Widget.BaseWidget, /** @lends Kekule.Widget.ObjectInspector# */ { /** @private */ CLASS_NAME: 'Kekule.Widget.ObjectInspector', /** @constructs */ initialize: function(/*$super, */parentOrElementOrDocument) { this._propListElem = null; this._objsInfoElem = null; this._propInfoElem = null; // important, must init before $super, as $super may bind element and set those values this.setPropStoreFieldValue('showObjsInfoPanel', true); this.setPropStoreFieldValue('showPropInfoPanel', true); this.tryApplySuper('initialize', [parentOrElementOrDocument]) /* $super(parentOrElementOrDocument) */; }, /** @private */ initProperties: function() { this.defineProp('objects', {'dataType': DataType.ARRAY, 'scope': Class.PropertyScope.PUBLIC, 'setter': function(value) { var objs = Kekule.ArrayUtils.toArray(value); this.setPropStoreFieldValue('objects', objs); this.inspectedObjectsChanged(objs); } }); this.defineProp('readOnly', {'dataType': DataType.BOOL, 'getter': function() { var propEditor = this.getPropEditor(); return propEditor && propEditor.getReadOnly(); }, 'setter': function(value) { var propEditor = this.getPropEditor(); if (propEditor) propEditor.setReadOnly(value); } }); // private properties this.defineProp('propEditor', {'dataType': 'Kekule.Widget.ObjPropListEditor', 'serializable': false, 'setter': null, 'scope': Class.PropertyScope.PRIVATE}); this.defineProp('showObjsInfoPanel', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('showObjsInfoPanel', value); SU.setDisplay(this._objsInfoElem, !!value); this._updateChildElemSize(); } }); this.defineProp('showPropInfoPanel', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('showPropInfoPanel', value); SU.setDisplay(this._propInfoElem, !!value); this._updateChildElemSize(); } }); this.defineProp('keyField', {'dataType': DataType.STRING, 'getter': function() { var pe = this.getPropEditor(); return pe && pe.getKeyField(); }, 'setter': function(value) { var pe = this.getPropEditor(); if (pe) pe.setKeyField(value); } }); this.defineProp('sortField', {'dataType': DataType.STRING, 'getter': function() { var pe = this.getPropEditor(); return pe && pe.getSortField(); }, 'setter': function(value) { var pe = this.getPropEditor(); if (pe) pe.setSortField(value); } }); this.defineProp('activeRow', {'dataType': DataType.OBJECT, 'serializable': false, 'scope': Class.PropertyScope.PUBLIC, 'getter': function() { var pe = this.getPropEditor(); return pe && pe.getActiveRow(); }, 'setter': function(value) { var pe = this.getPropEditor(); if (pe) pe.setActiveRow(value); } }); this.defineProp('enableOperHistory', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { var propEditor = this.getPropEditor(); return propEditor? propEditor.getEnableOperHistory(): false; }, 'setter': function(value) { var propEditor = this.getPropEditor(); return propEditor? propEditor.setEnableOperHistory(value): null; } }); this.defineProp('operHistory', {'dataType': 'Kekule.OperationHistory', 'serializable': false, 'scope': Class.PropertyScope.PUBLIC, 'getter': function() { var propEditor = this.getPropEditor(); return propEditor? propEditor.getOperHistory(): null; }, 'setter': function(value) { var propEditor = this.getPropEditor(); return propEditor? propEditor.setOperHistory(value): null; } }); }, doFinalize: function(/*$super*/) { this._finalizeChildWidgets(); this.tryApplySuper('doFinalize') /* $super() */; }, /** @ignore */ getChildrenHolderElement: function(/*$super*/) { return this._propListElem || this.tryApplySuper('getChildrenHolderElement') /* $super() */; }, /** @ignore */ doGetWidgetClassName: function() { var result = CNS.OBJINSPECTOR; if (this._isUsingFlexLayout()) result += ' ' + CNS.OBJINSPECTOR_FLEX_LAYOUT; return result; }, /** @private */ _isUsingFlexLayout: function() { return !!Kekule.BrowserFeature.cssFlex; }, /** @ignore */ doBindElement: function(/*$super, */element) { this.tryApplySuper('doBindElement', [element]) /* $super(element) */; this._updateChildElemSize(); }, /** @ignore */ doCreateRootElement: function(doc) { var result = doc.createElement('div'); return result; }, /** @ignore */ doCreateSubElements: function(/*$super, */doc, element) { var result = this.tryApplySuper('doCreateSubElements', [doc, element]) /* $super(doc, element) */ || []; var objsInfoElem = this._createSubPartElem(doc); EU.addClass(objsInfoElem, CNS.OBJINSPECTOR_OBJSINFOPANEL); element.appendChild(objsInfoElem); this._objsInfoElem = objsInfoElem; var propInfoElem = this._createSubPartElem(doc); EU.addClass(propInfoElem, CNS.OBJINSPECTOR_PROPINFOPANEL); element.appendChild(propInfoElem); this._propInfoElem = propInfoElem; var propListEditorContainer = this._createSubPartElem(doc); EU.addClass(propListEditorContainer, CNS.OBJINSPECTOR_PROPLISTEDITOR_CONTAINER); element.appendChild(propListEditorContainer); this._propListElem = propListEditorContainer; this._createChildWidgets(propListEditorContainer); return result.concat([objsInfoElem, propInfoElem, propListEditorContainer]); }, /** @private */ _createSubPartElem: function(doc) { var result = doc.createElement('div'); result.className = CNS.OBJINSPECTOR_SUBPART; return result; }, /** @private */ _createChildWidgets: function(parentElem) { this._finalizeChildWidgets(); // free old ones // property editor var propEditor = new Kekule.Widget.ObjPropListEditor(this); //propEditor.appendToElem(parentElem); propEditor.appendToWidget(this); var self = this; propEditor.addEventListener('activeRowChange', function(e) { self._updatePropInfo(); } ); this.setPropStoreFieldValue('propEditor', propEditor); }, /** @private */ _finalizeChildWidgets: function() { var propEditor = this.getPropEditor(); if (propEditor) propEditor.finalize(); }, /** @private */ inspectedObjectsChanged: function(objs) { var propEditor = this.getPropEditor(); if (propEditor) propEditor.setObjects(objs); // change info in objInfoElem this._updateObjectsInfo(objs); this._updatePropInfo(); }, /** @private */ _updateObjectsInfo: function(objs) { if (this._objsInfoElem) { var text; if (objs.length <= 0) { text = Kekule.$L('WidgetTexts.S_INSPECT_NONE'); //Kekule.WidgetTexts.S_INSPECT_NONE; } else if (objs.length === 1) { var obj = objs[0]; var id = obj.getId? obj.getId(): null; var stype = DataType.getType(obj); text = id? /*Kekule.WidgetTexts.S_INSPECT_ID_OBJECT*/Kekule.$L('WidgetTexts.S_INSPECT_ID_OBJECT').format(id, stype): /*Kekule.WidgetTexts.S_INSPECT_ANONYMOUS_OBJECT*/Kekule.$L('WidgetTexts.S_INSPECT_ANONYMOUS_OBJECT').format(stype); } else { text = /*Kekule.WidgetTexts.S_INSPECT_OBJECTS*/Kekule.$L('WidgetTexts.S_INSPECT_OBJECTS').format(objs.length); } DU.setElementText(this._objsInfoElem, text); } }, /** @private */ _updatePropInfo: function() { if (this._propInfoElem) { // clear first this._propInfoElem.innerHTML = ''; // then fill info var propEditor = this.getPropEditor().getActivePropEditor(); if (propEditor) { var title = propEditor.getTitle(); var description = propEditor.getDescription() || propEditor.getPropertyType() || ''; var doc = this._propInfoElem.ownerDocument; var elem = doc.createElement('div'); elem.className = CNS.OBJINSPECTOR_PROPINFO_TITLE; DU.setElementText(elem, title); this._propInfoElem.appendChild(elem); var elem = doc.createElement('div'); elem.className = CNS.OBJINSPECTOR_PROPINFO_DESCRIPTION; DU.setElementText(elem, description); this._propInfoElem.appendChild(elem); } } }, // operation history method /** * Undo a property setting operation. */ undo: function() { if (this.getPropEditor()) this.getPropEditor().undo(); return this; }, /** * Redo a property setting operation. */ redo: function() { if (this.getPropEditor()) this.getPropEditor().redo(); return this; }, // utils shortcut methods of PropListEditor /** * Returns key name like 'parentPropName.childPropName'. * @param {HTMLElement} row * @returns {String} */ getRowCascadeKey: function(row) { var pe = this.getPropEditor(); return pe && pe.getRowCascadeKey(row); }, /** * Returns cascade key of current selected row. * @returns {String} */ getActiveRowCascadeKey: function() { var pe = this.getPropEditor(); return pe && pe.getActiveRowCascadeKey(); }, /** * Check if a row has sub properties and can be expanded. * @param {HTMLElement} row * @returns {Bool} */ isRowExpandable: function(row) { var pe = this.getPropEditor(); return pe && pe.isRowExpandable(row); }, /** * Check if a row with sub properties is expanded. * @param {HTMLElement} row */ isRowExpanded: function(row) { var pe = this.getPropEditor(); return pe && pe.isRowExpanded(row); }, /** * Expand row with sub properties. * @param {HTMLElement} row */ expandRow: function(row) { var pe = this.getPropEditor(); if (pe) pe.expandRow(row); return this; }, /** * Collapse row with sub properties. * @param {HTMLElement} row */ collapseRow: function(row) { var pe = this.getPropEditor(); if (pe) pe.collapseRow(row); return this; }, /** * Expand a collapsed row or collapse an expanded row. * @param {HTMLElement} row */ toggleRowExpandState: function(row) { var pe = this.getPropEditor(); if (pe) pe.toggleRowExpandState(row); return this; }, /** * Collapse all rows in inspector. */ collapseAllRows: function() { var pe = this.getPropEditor(); if (pe) pe.collapseAllRows(); return this; }, /** * Expand all rows in editor. * @param {Bool} cascade */ expandAllRows: function(cascade) { var pe = this.getPropEditor(); if (pe) pe.expandAllRows(cascade); return this; }, /** @private */ _updateChildElemSize: function() { var self = this; if (!this._isUsingFlexLayout()) { // IMPORTANT, use set time out to let browser update DOM, else height often get 0 setTimeout(function() { var top; if (self.getShowObjsInfoPanel()) top = SU.getComputedStyle(self._objsInfoElem, 'height'); else top = 0; var bottom; if (self.getShowPropInfoPanel()) bottom = SU.getComputedStyle(self._propInfoElem, 'height'); else bottom = 0; self._propListElem.style.top = top; self._propListElem.style.bottom = bottom; }, 100); } }, /** @ignore */ setUseCornerDecoration: function(/*$super, */value) { var result = this.tryApplySuper('setUseCornerDecoration', [value]) /* $super(value) */; if (value) // use corner decoration, handle topmost and bottommost element { } return result; } }); })();