diff --git a/.gitignore b/.gitignore index 7b3a704..de39bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ Icon? ehthumbs.db Thumbs.db +.project +.settings +JS_libraries diff --git a/src/reports/schemas/SampleGroups.r b/src/reports/schemas/SampleGroups.r index d307460..39b40cd 100644 --- a/src/reports/schemas/SampleGroups.r +++ b/src/reports/schemas/SampleGroups.r @@ -7,6 +7,8 @@ if ( path != '' ){ suppressMessages( ws <- openWorkspace( path ) ); txt <- paste( unique( getSampleGroups(ws)[[1]] ), collapse=';' ); # unique(getSampleGroups(ws)[,1:2]); # alternative ? + print("Test Message printed to Console."); + } #sg<-merge(getSamples(ws),getSampleGroups(ws),by="sampleID") diff --git a/src/views/CardLayoutTest.webpart.xml b/src/views/CardLayoutTest.webpart.xml new file mode 100644 index 0000000..a153c06 --- /dev/null +++ b/src/views/CardLayoutTest.webpart.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/views/ComboTest.webpart.xml b/src/views/ComboTest.webpart.xml new file mode 100644 index 0000000..f253c42 --- /dev/null +++ b/src/views/ComboTest.webpart.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/views/FlowLayoutTest.webpart.xml b/src/views/FlowLayoutTest.webpart.xml new file mode 100644 index 0000000..c630ba5 --- /dev/null +++ b/src/views/FlowLayoutTest.webpart.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/views/HybridComboTest.html b/src/views/HybridComboTest.html new file mode 100644 index 0000000..cb1ab9a --- /dev/null +++ b/src/views/HybridComboTest.html @@ -0,0 +1,7 @@ +
+ + diff --git a/src/views/HybridComboTest.view.xml b/src/views/HybridComboTest.view.xml new file mode 100644 index 0000000..95dbb01 --- /dev/null +++ b/src/views/HybridComboTest.view.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/HybridComboTest.webpart.xml b/src/views/HybridComboTest.webpart.xml new file mode 100644 index 0000000..d781369 --- /dev/null +++ b/src/views/HybridComboTest.webpart.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/views/cardLayoutTest.html b/src/views/cardLayoutTest.html new file mode 100644 index 0000000..b0e317a --- /dev/null +++ b/src/views/cardLayoutTest.html @@ -0,0 +1,57 @@ +
+ + + + diff --git a/src/views/cardLayoutTest.view.xml b/src/views/cardLayoutTest.view.xml new file mode 100644 index 0000000..d291fd9 --- /dev/null +++ b/src/views/cardLayoutTest.view.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/comboTest.html b/src/views/comboTest.html new file mode 100644 index 0000000..4762c86 --- /dev/null +++ b/src/views/comboTest.html @@ -0,0 +1,23 @@ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ diff --git a/src/views/comboTest.view.xml b/src/views/comboTest.view.xml new file mode 100644 index 0000000..c968f5b --- /dev/null +++ b/src/views/comboTest.view.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/extjs4begin.html b/src/views/extjs4begin.html new file mode 100644 index 0000000..c3fc5e8 --- /dev/null +++ b/src/views/extjs4begin.html @@ -0,0 +1,72 @@ + + + + + + + + diff --git a/src/views/extjs4begin.view.xml b/src/views/extjs4begin.view.xml new file mode 100644 index 0000000..a977e49 --- /dev/null +++ b/src/views/extjs4begin.view.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/extjs4begin.webpart.xml b/src/views/extjs4begin.webpart.xml new file mode 100644 index 0000000..de1f1f4 --- /dev/null +++ b/src/views/extjs4begin.webpart.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/views/flowLayoutTest.html b/src/views/flowLayoutTest.html new file mode 100644 index 0000000..ce6e47b --- /dev/null +++ b/src/views/flowLayoutTest.html @@ -0,0 +1,57 @@ +
+ + + + diff --git a/src/views/flowLayoutTest.view.xml b/src/views/flowLayoutTest.view.xml new file mode 100644 index 0000000..83f6d21 --- /dev/null +++ b/src/views/flowLayoutTest.view.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/web/CardLayoutTest.js b/src/web/CardLayoutTest.js new file mode 100644 index 0000000..505b694 --- /dev/null +++ b/src/web/CardLayoutTest.js @@ -0,0 +1,60 @@ + + + +Ext4.define('CardLayoutTest', { + extend : 'Ext.panel.Panel', + constructor : function(config) { + config = config || {}; // Make sure config is not totally null. + config = Ext4.applyIf(config, { + // width : 700, + height : 120, + layout : { + type : 'card' + }, + defaults: {bodyStyle:'padding:15px'}, + tbar: [ + { + id: 'move-prev', + text: '< Back', + handler: function(btn) { + navigate(btn.up("panel"), "prev"); + }, + disabled: true + }, + { + id: 'move-next', + text: 'Next >', + handler: function(btn) { + navigate(btn.up("panel"), "next"); + } + } + ], + // the panels (or "cards") within the layout + items: [{ + id: 'card-0', + html: '

Welcome to the Wizard!

Step 1 of 3

' + },{ + id: 'card-1', + html: '

Step 2 of 3

' + },{ + id: 'card-2', + html: '

Congratulations!

Step 3 of 3 - Complete

' + }] + + }); + this.callParent([ config ]); + } +}); + +var navigate = function(panel, direction){ + // This routine could contain business logic required to manage the navigation steps. + // It would call setActiveItem as needed, manage navigation button state, handle any + // branching logic that might be required, handle alternate actions like cancellation + // or finalization, etc. A complete wizard implementation could get pretty + // sophisticated depending on the complexity required, and should probably be + // done as a subclass of CardLayout in a real-world implementation. + var layout = panel.getLayout(); + layout[direction](); + Ext4.getCmp('move-prev').setDisabled(!layout.getPrev()); + Ext4.getCmp('move-next').setDisabled(!layout.getNext()); +}; \ No newline at end of file diff --git a/src/web/ComboTest.js b/src/web/ComboTest.js new file mode 100644 index 0000000..a35b15f --- /dev/null +++ b/src/web/ComboTest.js @@ -0,0 +1,561 @@ +function init() { + + // ClearButton from: + // http://www.eekboom.de/ClearButton.html + + var numbers = Ext4.create('Ext.data.Store', { + fields : ['name', 'digit'], + data : [{ + "name" : "One", + "digit" : "1" + }, { + "name" : "Two", + "digit" : "2" + }, { + "name" : "Uno is the name of the digit I'm looking for", + "digit" : "1" + }] + }); + + var states = Ext4.create('Ext.data.Store', { + fields : ['abbreviation', 'name'], + data : [{ + name : 'ALABAMA', + abbreviation : 'AL' + }, { + name : 'ALASKA', + abbreviation : 'AK' + }, { + name : 'AMERICAN SAMOA', + abbreviation : 'AS' + }, { + name : 'ARIZONA', + abbreviation : 'AZ' + }, { + name : 'ARKANSAS', + abbreviation : 'AR' + }, { + name : 'CALIFORNIA', + abbreviation : 'CA' + }, { + name : 'COLORADO', + abbreviation : 'CO' + }, { + name : 'CONNECTICUT', + abbreviation : 'CT' + }, { + name : 'DELAWARE', + abbreviation : 'DE' + }, { + name : 'DISTRICT OF COLUMBIA', + abbreviation : 'DC' + }, { + name : 'FEDERATED STATES OF MICRONESIA', + abbreviation : 'FM' + }, { + name : 'FLORIDA', + abbreviation : 'FL' + }, { + name : 'GEORGIA', + abbreviation : 'GA' + }, { + name : 'GUAM', + abbreviation : 'GU' + }, { + name : 'HAWAII', + abbreviation : 'HI' + }, { + name : 'IDAHO', + abbreviation : 'ID' + }, { + name : 'ILLINOIS', + abbreviation : 'IL' + }, { + name : 'INDIANA', + abbreviation : 'IN' + }, { + name : 'IOWA', + abbreviation : 'IA' + }, { + name : 'KANSAS', + abbreviation : 'KS' + }, { + name : 'KENTUCKY', + abbreviation : 'KY' + }, { + name : 'LOUISIANA', + abbreviation : 'LA' + }, { + name : 'MAINE', + abbreviation : 'ME' + }, { + name : 'MARSHALL ISLANDS', + abbreviation : 'MH' + }, { + name : 'MARYLAND', + abbreviation : 'MD' + }, { + name : 'MASSACHUSETTS', + abbreviation : 'MA' + }, { + name : 'MICHIGAN', + abbreviation : 'MI' + }, { + name : 'MINNESOTA', + abbreviation : 'MN' + }, { + name : 'MISSISSIPPI', + abbreviation : 'MS' + }, { + name : 'MISSOURI', + abbreviation : 'MO' + }, { + name : 'MONTANA', + abbreviation : 'MT' + }, { + name : 'NEBRASKA', + abbreviation : 'NE' + }, { + name : 'NEVADA', + abbreviation : 'NV' + }, { + name : 'NEW HAMPSHIRE', + abbreviation : 'NH' + }, { + name : 'NEW JERSEY', + abbreviation : 'NJ' + }, { + name : 'NEW MEXICO', + abbreviation : 'NM' + }, { + name : 'NEW YORK', + abbreviation : 'NY' + }, { + name : 'NORTH CAROLINA', + abbreviation : 'NC' + }, { + name : 'NORTH DAKOTA', + abbreviation : 'ND' + }, { + name : 'NORTHERN MARIANA ISLANDS', + abbreviation : 'MP' + }, { + name : 'OHIO', + abbreviation : 'OH' + }, { + name : 'OKLAHOMA', + abbreviation : 'OK' + }, { + name : 'OREGON', + abbreviation : 'OR' + }, { + name : 'PALAU', + abbreviation : 'PW' + }, { + name : 'PENNSYLVANIA', + abbreviation : 'PA' + }, { + name : 'PUERTO RICO', + abbreviation : 'PR' + }, { + name : 'RHODE ISLAND', + abbreviation : 'RI' + }, { + name : 'SOUTH CAROLINA', + abbreviation : 'SC' + }, { + name : 'SOUTH DAKOTA', + abbreviation : 'SD' + }, { + name : 'TENNESSEE', + abbreviation : 'TN' + }, { + name : 'TEXAS', + abbreviation : 'TX' + }, { + name : 'UTAH', + abbreviation : 'UT' + }, { + name : 'VERMONT', + abbreviation : 'VT' + }, { + name : 'VIRGIN ISLANDS', + abbreviation : 'VI' + }, { + name : 'VIRGINIA', + abbreviation : 'VA' + }, { + name : 'WASHINGTON', + abbreviation : 'WA' + }, { + name : 'WEST VIRGINIA', + abbreviation : 'WV' + }, { + name : 'WISCONSIN', + abbreviation : 'WI' + }, { + name : 'WYOMING', + abbreviation : 'WY' + }] + }); + + var myComboStandard = Ext4.create('Ext.form.ComboBox', { + fieldLabel : 'Choose Name', + store : numbers, + queryMode : 'local', + displayField : 'name', + valueField : 'digit' + // plugins:['combo-autowidth'] + // plugins: ['clearbutton'], + /* + * Below is dude's suggestion at Stack Overflow for showing + * checkboxes - in combination with CSS. Doesn't seem to work: + */ + /* + * listConfig : { getInnerTpl : function() { return '
{fieldName}
'; } } + */ + }); + + var resultStandard = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'Standard Ext JS 4 Combo', + width : 300, + height : 150, + bodyPadding : 10, + renderTo : 'my-target1', + items : [myComboStandard, resultStandard] + }); + + myComboStandard.on('select', function(combo, records, eOpts) { + resultStandard.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// + + var myComboLabKeyBoxSelect = Ext4.create('Ext4.ux.form.field.BoxSelect', { + fieldLabel : 'Choose Name', + store : numbers, + queryMode : 'local', + displayField : 'name', + valueField : 'digit', + plugins : ['combo-autowidth'], + width : 350 + }); + + var resultLabKeyBoxSelect = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'LabKey Box Select Combo', + width : 500, + height : 150, + bodyPadding : 10, + renderTo : 'my-target2', + items : [myComboLabKeyBoxSelect, resultLabKeyBoxSelect] + }); + + myComboLabKeyBoxSelect.on('select', function(combo, records, eOpts) { + resultLabKeyBoxSelect.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// + + // https://github.com/kveeiv/extjs-boxselect + var myComboBoxSelect203 = Ext4.create('Ext4.ux.form.field.BoxSelect203', { + fieldLabel : 'Choose Name', + //store : numbers, + //queryMode : 'local', + //displayField : 'name', + //valueField : 'digit', + + store : states, + queryMode : 'local', + displayField : 'name', + valueField : 'abbreviation', + //grow: true, + //stacked: true, + + width : 380, + plugins : ['clearbutton'] + // plugins: new Ext4.ux.form.field.ClearButton({animateClearButton: + // false, + // hideClearButtonWhenEmpty: false, + // hideClearButtonWhenMouseOut:false}) + + }); + + var resultBoxSelect203 = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'Box Select Combo - version 2.03', + width : 500, + //height : 150, + bodyPadding : 10, + renderTo : 'my-target3', + items : [myComboBoxSelect203, resultBoxSelect203] + + }); + + myComboBoxSelect203.on('select', function(combo, records, eOpts) { + resultBoxSelect203.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// + + // server/internal/webapp/extWidgets/LabkeyCombo.js + var myComboLabKeyExt4Combo = Ext4.create('LABKEY.ext4.ComboBox', { + fieldLabel : 'Choose Name', + store : numbers, + queryMode : 'local', + displayField : 'name', + valueField : 'digit', + // LabKey turns on combo-autowidth plugin by default. + // plugins:['combo-autowidth'], + // width : 50, + multiSelect : true + }); + + var resultLabKeyExt4Combo = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'LabKey Ext4 Combo', + width : 500, + height : 150, + bodyPadding : 10, + renderTo : 'my-target4', + items : [myComboLabKeyExt4Combo, resultLabKeyExt4Combo] + }); + + myComboLabKeyExt4Combo.on('select', function(combo, records, eOpts) { + resultLabKeyExt4Combo.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// + // https://github.com/krikrou/ComboFieldBox + + var myCombo_ComboFieldBox = Ext4.create('Ext4.ux.ComboFieldBox', { + queryMode : 'local', + //fieldLabel : 'Choose Name', + //store : numbers, + displayField : 'name', + fieldLabel : 'Choose State', + store : states, + valueField : 'abbreviation', + plugins : ['selectedCount'], + // LabKey turns on combo-autowidth plugin by default. + // plugins:['combo-autowidth'], + // width : 50, + width : 300, + multiSelect : true, + autoScroll: true + }); + + var result_ComboFieldBox = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'ComboFieldBox', + width : 500, + //height : 150, + bodyPadding : 10, + renderTo : 'my-target5', + items : [myCombo_ComboFieldBox, result_ComboFieldBox] + }); + + myCombo_ComboFieldBox.on('select', function(combo, records, eOpts) { + result_ComboFieldBox.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// + var myComboLabKeyCheckCombo = Ext4.create('Ext.ux.CheckCombo', { + addAllSelector : true, + fieldLabel : 'Choose Name', + store : numbers, + queryMode : 'local', + displayField : 'name', + valueField : 'digit', + // LabKey turns on combo-autowidth plugin by default. + // plugins:['combo-autowidth'], + // width : 50, + width : 300, + multiSelect : true + }); + + var resultLabKeyCheckCombo = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'LabKey CheckCombo', + width : 500, + height : 150, + bodyPadding : 10, + renderTo : 'my-target6', + items : [myComboLabKeyCheckCombo, resultLabKeyCheckCombo] + }); + + myComboLabKeyCheckCombo.on('select', function(combo, records, eOpts) { + if (records[0]) { + resultLabKeyCheckCombo.setValue(records[0].data.digit); + } else { + resultLabKeyCheckCombo.setValue(""); + } + }); + + // /////////////////////////////////////////////////////////////////// + + + + var myComboCheckCombo_Jan13_2013 = Ext4.create('Ext4.ux.CheckCombo', { + addAllSelector : true, + fieldLabel : 'Choose Name', + //store : numbers, + store : states, + queryMode : 'local', + //displayField : 'name', + //valueField : 'digit', + displayField : 'name', + valueField : 'abbreviation', + // LabKey turns on combo-autowidth plugin by default. + // plugins:['combo-autowidth'], + // width : 50, + width : 300, + multiSelect : true + }); + + var resultCheckCombo_Jan13_2013 = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'CheckCombo from January 13, 2013', + width : 500, + height : 150, + bodyPadding : 10, + renderTo : 'my-target7', + items : [myComboCheckCombo_Jan13_2013, + resultCheckCombo_Jan13_2013] + }); + + myComboCheckCombo_Jan13_2013.on('select', function(combo, records, eOpts) { + if (records[0]) { + resultCheckCombo_Jan13_2013.setValue(records[0].data.digit); + } else { + resultCheckCombo_Jan13_2013.setValue(""); + } + }); + + // /////////////////////////////////////////////////////////////////// + + + + var myComboSelectedCountPlugin = Ext4.create('Ext.form.ComboBox', { + disabled : false, + plugins : ['selectedCount'], + fieldLabel : 'Choose State', + labelAlign : 'top', + store : states, + queryMode : 'local', + editable : false, + displayField : 'name', + valueField : 'abbreviation', + // renderTo: Ext.getBody(), + multiSelect : true, + maxSelections : 3, + width : 400 + }); + + var resultSelectedCountPlugin = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'State', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'selectedCount plugin', + width : 500, + height : 150, + bodyPadding : 10, + renderTo : 'my-target8', + items : [myComboSelectedCountPlugin, resultSelectedCountPlugin] + }); + + myComboSelectedCountPlugin.on('select', function(combo, records, eOpts) { + if (records[0]) { + resultSelectedCountPlugin.setValue(records[0].data.abbreviation); + } else { + resultSelectedCountPlugin.setValue(""); + } + + }); + + // /////////////////////////////////////////////////////////////////// + + // https://github.com/kveeiv/extjs-boxselect + var myComboBoxSelect203SelectedCount = Ext4.create('Ext4.ux.form.field.BoxSelect203', { + fieldLabel : 'Choose Name', + //store : numbers, + //queryMode : 'local', + //displayField : 'name', + //valueField : 'digit', + + store : states, + queryMode : 'local', + displayField : 'name', + valueField : 'abbreviation', + grow: true, + //stacked: true, + layout: 'fit', + width : 380, + autoScroll: true, + //plugins : ['clearbutton'] + plugins : ['selectedCount'] + // plugins: new Ext4.ux.form.field.ClearButton({animateClearButton: + // false, + // hideClearButtonWhenEmpty: false, + // hideClearButtonWhenMouseOut:false}) + + }); + + var resultBoxSelect203SelectedCount = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'Box Select Combo - version 2.03 with Selected Count Plugin', + width : 500, + height : 400, + bodyPadding : 10, + renderTo : 'my-target9', + layout: 'form', + autoScroll: true, + items : [myComboBoxSelect203SelectedCount + //, resultBoxSelect203SelectedCount + ] + + }); + + myComboBoxSelect203SelectedCount.on('select', function(combo, records, eOpts) { + resultBoxSelect203SelectedCount.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// +} diff --git a/src/web/ExtJS4OpenCytoPreprocessing.js b/src/web/ExtJS4OpenCytoPreprocessing.js new file mode 100644 index 0000000..a2966f0 --- /dev/null +++ b/src/web/ExtJS4OpenCytoPreprocessing.js @@ -0,0 +1,1177 @@ +// vim: sw=4:ts=4:nu:nospell:fdc=4 +/* jslint vars: true */ +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +// Ext4.namespace('LABKEY', 'LABKEY.ext'); +Ext4.define('LABKEY.ext.ExtJS4OpenCytoPreprocessing', { + extend : 'Ext.panel.Panel', + constructor : function(config) { + + config = Ext4.applyIf(config, { + border : false, + boxMinWidth : 370, + frame : false, + items : [], + layout : 'fit', + webPartDivId : config.webPartDivId, + renderTo : config.webPartDivId, + width : document.getElementById(config.webPartDivId).offsetWidth + }); + + this.callParent([config]); + + }, // end constructor + + initComponent : function() { + var me = this; + // Ext4.log({level: 'warn'}, 'A Warning'); + // Ext4.log( 'A Warning', {level: 'warn'}); + this.callParent(); + + this.addEvents({ + 'preprocessed' : true + }); + + // ////////////////////////////////// + // Generate necessary HTML divs // + // ////////////////////////////////// + + $('#' + me.webPartDivId).append('
' + + + '
' + + ); + + // /////////////////////////////////// + // Variables // + // /////////////////////////////////// + var pnlGlobal = this; + + this.rootPath = undefined; + this.maskGlobal = undefined; + this.notSorting = undefined; + this.selectedStudyVars = undefined; + + // /////////////////////////////////// + // Strings // + // /////////////////////////////////// + var strngErrorContactWithLink = ' Please, contact the developer, if you have questions.'; + + // ///////////////////////////////// + // Stores // + // ///////////////////////////////// + this.strSampleGroup = Ext4.create('Ext.data.ArrayStore', { + autoLoad : false, + data : [], + fields : [{ + name : 'SampleGroup', + type : 'string' + }] + }); + + this.strXML = Ext4.create('LABKEY.ext4.data.Store', { + autoLoad : true, + listeners : { + load : function() { + if (this.getCount() == 0) { + me.cbXml.disable(); + me.tfAnalysisName.disable(); + pnlWorkspaces.getEl().mask( + 'Seems like you have not imported any XML files, click here to do so.' + + strngErrorContactWithLink, + 'infoMask'); + } + } + }, + queryName : 'XmlFiles', + remoteSort : false, + schemaName : 'exp', + sort : 'FileName' + }); + + this.strngSqlStartTable = 'SELECT DISTINCT FCSFiles.Name AS FileName'; + this.strngSqlEndTable = ' FROM FCSFiles' + + ' WHERE FCSFiles.Run.FCSFileCount != 0 AND FCSFiles.Run.ProtocolStep = \'Keywords\''; + + this.strFilteredTable = Ext4.create('LABKEY.ext4.data.Store', { + listeners : { + load : function() { + if (me.notSorting) { + me.notSorting = false; + } + me.updateTableStatus(); + } + }, + // nullRecord: { + // displayColumn: 'myDisplayColumn', + // nullCaption: '0' + // }, + remoteSort : false, + schemaName : 'flow', + sortInfo : { + field : 'FileName', + direction : 'ASC' + }, + sql : me.strngSqlStartTable + me.strngSqlEndTable + }); + + // //////////////////////////////////////////////////////////////// + // Queries and associated functionality // + // //////////////////////////////////////////////////////////////// + LABKEY.Query.selectRows({ + columns : ['RootPath'], + failure : onFailure, + queryName : 'RootPath', + schemaName : 'flow', + success : function(data) { + var count = data.rowCount; + if (count == 1) { + this.rootPath = data.rows[0].RootPath; + } else if (count < 1) { + // disable all + me.btnNext.disable(); + me.cbStudyVarName.disable(); + me.pnlMain.getEl().mask( + 'Seems like you have not imported any FCS files, click here to do so.' + + strngErrorContactWithLink, + 'infoMask'); + } else { + // disable all + me.btnNext.disable(); + me.cbStudyVarName.disable(); + me.pnlMain.getEl().mask( + 'Cannot retrieve the path for the data files: it is non-unique.' + + strngErrorContactWithLink, + 'infoMask'); + } + } + }); + + this.pnlTable = this.pnlInitPnlTable(this.strFilteredTable, [ + + /* + * this.rowNumberer, { dataIndex : 'FileName', header : 'File + * Name', autoWidth : true } + */ + ]); + + this.pnlComp = Ext4.create('Ext.Panel', { + defaults : { + style : 'padding-bottom: 4px; padding-right: 4px; padding-left: 4px;' + }, + items : [], + title : 'Compensation' + + }); + + // /////////////////////////////////// + // Session instantiation // + // /////////////////////////////////// + LABKEY.Report.createSession({ + failure : onFailure, + success : function(data) { + me.reportSessionId = data.reportSessionId; + me.add(me.pnlInitMainPanel()); + } + }); + }, + + // /////////////////////////////////// + // Functions // + // /////////////////////////////////// + updateInfoStatus : function(text, code) { + var me = this; + // me.cmpStatus.update(text); + me.cmpStatus.setText(text); + if (text != '') { + if (code == -1) { + me.cmpStatus.getEl().setStyle({ + color : 'red' + }); + me.cmpStatus.getEl().frame("ff0000", 1, { + duration : 3 + }); // RED ERROR + } else if (code == 1) { + me.cmpStatus.getEl().setStyle({ + color : 'black' + }); + } else { + me.cmpStatus.getEl().setStyle({ + color : 'black' + }); + me.cmpStatus.getEl().frame(); + } + } + }, + + updateTableStatus : function() { + var me = this; + var selectionModel = me.pnlTable.getSelectionModel(); + var selectedCount = me.pnlTable.getSelectionModel().getCount(); + + // Update the table's title + if (selectedCount == 1) { + me.pnlTable.setTitle(selectedCount + ' file is currently chosen'); + } else { + me.pnlTable.setTitle(selectedCount + ' files are currently chosen'); + } + + // Manage the 'check all' icon state + // innerHd doesn't exist in Ext JS 4. + /* + * var t = Ext4.fly(me.pnlTable.getView().innerHd) + * .child('.x-grid4-hd-checker'); var isChecked = + * t.hasClass('x-grid4-hd-checker-on'); var totalCount = + * me.pnlTable.getStore().getCount(); + * + * if (selectedCount != totalCount & isChecked) { + * t.removeClass('x-grid4-hd-checker-on'); } else if (selectedCount == + * totalCount & !isChecked) { t.addClass('x-grid4-hd-checker-on'); } + */ + }, + + setStudyVars : function() { + var me = this; + me.rowNumberer = Ext4.create('Ext.grid.RowNumberer', { + autoWidth : true + // , minAutoWidth:'30px' + }); + var temp = me.cbStudyVarName.getValue(); + if (temp != me.selectedStudyVars) { + me.selectedStudyVars = temp; + + var i, len, c, curLabel, curValue, curFlag, tempSQL, newColumns; + + // Grab the choices array + // getValueEx() defined on SuperBoxSelect + // var arrayStudyVars = me.cbStudyVarName.getValueEx(); + + var arrayStudyVars = []; + Ext4.each(me.cbStudyVarName.valueModels, function(value) { + + arrayStudyVars.push(value.data); + }); + /* + * newColumns = [me.rowNumberer, me.smCheckBox, { dataIndex : + * 'FileName', header : 'File Name' }]; + */ + newColumns = [me.rowNumberer, { + dataIndex : 'FileName', + header : 'File Name', + autoWidth : true, + cls : 'open-cyto-fcs-header' + }]; + + tempSQL = me.strngSqlStartTable; + + len = arrayStudyVars.length; + + for (i = 0; i < len; i++) { + c = arrayStudyVars[i]; + curLabel = c.Display; + curValue = LABKEY.QueryKey.encodePart(c.Value); + curFlag = curLabel.slice(-2, -1); + + if (curFlag == 'l') { // External study variable + curLabel = curLabel.slice(0, -11); + tempSQL += ', FCSFiles.Sample."' + curLabel + '" AS "' + + curValue + '"'; + curLabel += ' (External)'; + } else if (curFlag == 'd') { // Keyword study variable + curLabel = curLabel.slice(0, -10); + tempSQL += ', FCSFiles.Keyword."' + curLabel + '" AS "' + + curValue + '"'; + curLabel += ' (Keyword)'; + } else { + i = len; + onFailure({ + exception : 'there was an error while executing this command: data format mismatch.' + }); + } + + newColumns.push({ + dataIndex : curValue, + // text : curLabel, + header : curLabel, + autoWidth : true, + cls : 'open-cyto-fcs-header' + }); + + } // end of for ( i = 0; i < len; i ++ ) loop + + tempSQL += me.strngSqlEndTable; + + me.strFilteredTable.sql = tempSQL; + me.strFilteredTable.load(); + + this.notSorting = true; + console + .log('Reconfiguring Filtered Table Store with new column selections.'); + // this.pnlTable.reconfigure(me.strFilteredTable, newColumns); + me.tableContainer.remove(me.pnlTable, true); + me.pnlTable = me.pnlInitPnlTable(me.strFilteredTable, newColumns); + me.tableContainer.add(me.pnlTable); + // me.pnlTable.hide(); + me.pnlTable.show(); + me.pnlTable.doLayout(); + + // me.pnlTable.getSelectionModel().selectAll(); + /* + * this.pnlTable.getStore(), // new //Ext.grid.ColumnModel( { items : + * newColumns, defaults : { dragable : false, hideable : false, + * resizable : true, sortable : true, tooltip : 'double click the + * separator between two column headers to fit the column width to + * its contents' } })); + */ + if (me.cbSampleGroup.getValue() != '' & me.cbXml.getValue() != '') { + me.btnProcess.setDisabled(false); + } + } + }, // end of setStudyVars() + + pnlInitMainPanel : function() { + var me = this; + // /////////////////////////////////// + // Buttons // + // /////////////////////////////////// + me.btnProcess = Ext4.create('Ext.button.Button', { + disabled : true, + handler : function() { + + if (me.tfAnalysisName.getValue() == '') { + me.updateInfoStatus('Empty analysis name is not allowed', + -1); + + me.tfAnalysisName.focus(); + me.tfAnalysisName.getEl().frame("ff0000", 1, { + duration : 3 + }); + } else if (me.tfAnalysisDescription.getValue() == '') { + me.updateInfoStatus( + 'Empty analysis description is not allowed', -1); + + me.tfAnalysisDescription.focus(); + me.tfAnalysisDescription.getEl().frame("ff0000", 1, { + duration : 3 + }); + } else { + if (me.cbSampleGroup.getValue() != '') { + this.setDisabled(true); + me.cbXml.setDisabled(true); + // me.cbSampleGroup.setDisabled(true); + me.tfAnalysisName.setDisabled(true); + me.tfAnalysisDescription.setDisabled(true); + + me.maskGlobal.msg = 'Generating and saving the analysis data, please, wait...'; + me.maskGlobal.show(); + + var records = me.pnlTable.getSelectionModel() + .getSelection(); + + me.preprocessData(records); + } + } + + }, + text : 'Process' + }); + + var btnBack = Ext4.create('Ext.button.Button', { + disabled : true, + text : '< Back' + }); + + this.btnNext = Ext4.create('Ext.button.Button', { + text : 'Next >' + }); + + // this.cmpStatus = Ext4.create('Ext.toolbar.Item', { + this.cmpStatus = Ext4.create('Ext.toolbar.TextItem', { + // this.cmpStatus = Ext4.create('Ext.Component', { + // html : 'Set the study variables and click \'Next\'', + // text : 'Set the study variables and click Next', + text : 'Set the study variables and click \'Next\'', + style : { + paddingLeft : '10px' + } + }); + + this.tableContainer = Ext4.create('Ext.container.Container', { + items : [this.pnlTable], + layout : 'fit' + }); + + var pnlMain = Ext4.create('Ext.Panel', { + activeItem : 0, + autoHeight : true, + bodyPadding : 0, + margin : 0, + bodyStyle : { + paddingTop : '3px', + 'border-top-width' : '1px', + 'border-right-width' : '1px', + 'border-bottom-width' : '1px', + 'border-left-width' : '1px' + }, + border : 1, + defaults : { + autoHeight : true, + hideMode : 'offsets', + border : 1, + paddingTop : '3px', + 'border-top-width' : '1px', + 'border-right-width' : '1px', + 'border-bottom-width' : '1px', + 'border-left-width' : '1px' + }, + deferredRender : false, + forceLayout : true, + items : [me.pnlInitStudyVarsPanel(), + me.pnlInitWorkspacesPanel(), this.tableContainer, + this.pnlComp], + layout : { + type : 'card' + /* + * , defaultMargins : { top : 0, right : 40, bottom : 0, + * left : 0 } + */ + }, + listeners : { + afterrender : function() { + me.maskGlobal = new Ext4.LoadMask(this.getEl(), { + msgCls : 'x-mask-loading-custom' + }); + } + }, + tbar : Ext4.create('Ext.toolbar.Toolbar', { + style : 'margin-left: 2px; margin-right: 6px;', + //margin: '0 6 0 2', + items : [me.btnProcess, btnBack, me.btnNext, + me.cmpStatus] + }) + + }); + + var navHandler = function(direction) { + var oldIndex = pnlMain.items + .indexOf(pnlMain.getLayout().activeItem); + var newIndex = oldIndex + ((direction === 'next') ? 1 : -1); + var layout = pnlMain.getLayout(); + layout[direction](); + + if (newIndex == 0) { + btnBack.setDisabled(true); + me.btnProcess.setDisabled(true); + } + + if (newIndex == 1) { + btnBack.setDisabled(false); + if (oldIndex == 0) { + me.setStudyVars(); + } + } + + // if ( newIndex == 2 ){ me.btnNext.setDisabled(false); } + + // Disable Next button if there no cards after it, othwise enable + // it. + me.btnNext.setDisabled(!layout.getNext()); + // Disable Comp Panel + if (newIndex == 3) { + me.btnNext.setDisabled(true); + console.log("Grid Columns"); + console.dir(me.pnlTable.headerCt.getGridColumns()); + } + + me.updateInfoStatus(''); + + }; + + btnBack.on('click', Ext4.bind(navHandler, pnlMain, ["prev"])); + me.btnNext.on('click', Ext4.bind(navHandler, pnlMain, ["next"])); + + this.pnlMain = pnlMain; + // redundant, yes. Refactor later. + return pnlMain; + }, + + pnlInitStudyVarsPanel : function() { + var me = this; + var listStudyVars = []; + + function fetchKeywords() { + LABKEY.Query.selectRows({ + columns : ['Name'], + filterArray : [ + LABKEY.Filter.create('Name', 'DISPLAY;BS;MS', + LABKEY.Filter.Types.CONTAINS_NONE_OF), + LABKEY.Filter + .create( + 'Name', + ['$', 'LASER', 'EXPORT', 'CST', + 'CYTOMETER', 'EXPORT', + 'FJ_', 'CREATOR', + 'TUBE NAME', + 'WINDOW EXTENSION', + 'SPILL'], + LABKEY.Filter.Types.DOES_NOT_START_WITH)], + queryName : 'Keyword', + schemaName : 'flow', + success : function(data) { + var toAdd; + Ext4.each(data.rows, function(r) { + toAdd = r.Name; + listStudyVars.push(['K', + toAdd + ' (Keyword)', + 'RowId/Keyword/' + toAdd]); + }); + me.strStudyVarName.loadData(listStudyVars, true); + }, + failure : onFailure + }); + } + + LABKEY.Query.getQueries({ + schemaName : 'Samples', + success : function(queriesInfo) { + var queries = queriesInfo.queries, count = queries.length, j; + for (j = 0; j < count; j++) { + if (queries[j].name == 'Samples') { + j = count; + } + } + + if (j == count + 1) { + LABKEY.Domain.get(function(DomainDesign) { + var toAdd; + Ext4.each(DomainDesign.fields, + function(r) { + toAdd = r.name; + listStudyVars + .push([ + 'E', + toAdd + + ' (External)', + 'Sample/' + + toAdd]); + }); + + fetchKeywords(); + }, fetchKeywords, 'Samples', 'Samples'); + } else { + fetchKeywords(); + } + }, + failure : fetchKeywords + }); + this.cmpStudyVars = Ext4.create('Ext.Component', { + cls : 'bold-text', + headerCssClass : 'simple-panel-header', + html : 'Select the study variables that are of interest for this project:', + layout : 'fit' + }); + + Ext4.define('StudyVarNameModel', { + extend : 'Ext.data.Model', + fields : [{ + name : 'Flag', + type : 'string' + }, { + name : 'Display', + type : 'string' + }, { + name : 'Value', + type : 'string' + }] + }); + this.strStudyVarName = Ext4.create('Ext.data.ArrayStore', { + // model: 'StudyVarNameModel', + // data : [], + fields : ['Flag', 'Display', 'Value'], + sortInfo : { + field : 'Flag', + direction : 'ASC' + } + }); + + // /////////////////////////////////// + // ComboBoxes / TextFields // + // /////////////////////////////////// + this.cbStudyVarName = + // Ext4.create('Ext.ux.form.ExtJS4SuperBoxSelect',{ + // Ext4.create('Ext4.ux.ComboFieldBox', { + // Ext4.create('Ext4.ux.form.field.BoxSelect', { + // Ext4.create('Ext4.ux.form.field.BoxSelect203', { + // Ext4.create('Ext.form.ComboBox', { + // Ext4.create('Ext.ux.CheckCombo', { + Ext4.create('Ext4.ux.CheckCombo', { + multiSelect : true, + addAllSelector : true, + noData : true, + allowBlank : true, + // autoSelect : false, + displayField : 'Display', + emptyText : 'Select...', + // forceSelection : true, + typeAhead : false, + editable : false, + getAllValuesAsArray : function() { + var c = []; + + Ext4.each(this.store.data.items, function(r) { + c.push(r.data.Value); + }); + + // usedRecords is a variable of the SuperBoxSelect. + /* + * Ext4.each(this.usedRecords.items, function(r) { + * c.push(r.data.Value); }); + */ + return c; + }, + // lazyInit : false, + listeners : { + additem : function() { + console.log('cbStudyVarName addItem event'); + me.updateInfoStatus( + 'Set the study variables and click \'Next\'', 1); + }, + clear : function() { + console.log('cbStudyVarName clear event'); + me + .updateInfoStatus('Set the study variables and click \'Next\''); + }, + // focus: function (){ // display the dropdown on focus + // this.expand(); + // }, + removeitem : function() { + console.log('cbStudyVarName removeitem event'); + me.updateInfoStatus( + 'Set the study variables and click \'Next\'', 1); + } + }, + // minChars : 0, + queryMode : 'local', + resizable : false, + store : me.strStudyVarName, + // supressClearValueRemoveEvents : true, + // triggerAction : 'all', + // typeAhead : true, + valueField : 'Value', + layout : { + type : 'fit' + /* + * , defaultMargins : { top : 0, right : 80, bottom : 0, left : + * 0 } + */ + } + }); + + var pnlStudyVars = Ext4.create('Ext.Panel', { + cls : 'shaded-panel', + header : { + xtype : 'header', + ui : 'default-large' + }, + defaults : { + style : 'padding-bottom: 4px; padding-right: 4px; padding-left: 4px;' + }, + items : [me.cmpStudyVars, Ext4.create('Ext.Panel', { + border : false, + items : [me.cbStudyVarName], + style:'padding-right: 4px;', + layout: 'fit' + })], + title : 'Configuration' + }); + + return pnlStudyVars; + }, + + pnlInitWorkspacesPanel : function() { + + var me = this; + + me.lastlySelectedXML = undefined; + + // me.cbXml = Ext4.create('Ext.form.field.ComboBox', { + me.cbXml = Ext4.create('Ext4.ux.form.field.ClearableComboBox', { + allowBlank : true, + displayField : 'FileName', + emptyText : 'Select', + forceSelection : true, + listeners : { + change : function() { + // sample groups obtaining logic here? + if (this.getValue() == '') { + me.cbSampleGroup.setDisabled(true); + me.btnProcess.setDisabled(true); + + this.focus(); + } else { + me.cbSampleGroup.setDisabled(false); + + if (me.cbSampleGroup.getValue() != '') { + me.btnProcess.setDisabled(false); + + me.tfAnalysisName.focus(); // working? + } else { + me.cbSampleGroup.focus(); // working? + } + } + }, + cleared : function() { + me.cbSampleGroup.setDisabled(true); + me.btnProcess.setDisabled(true); + this.focus(); + }, + select : function(c, r, i) { + var value = this.getValue(); + + if (value != me.lastlySelectedXML) { + + me.maskGlobal.msg = 'Obtaining the available sample groups, please, wait...'; + me.maskGlobal.show(); + + this.setDisabled(true); // to prevent interaction with + // that combo while the mask is + // on + me.tfAnalysisName.setDisabled(true); + // to prevent interaction with that + // combo while the mask is on + me.tfAnalysisDescription.setDisabled(true); + // to prevent interaction with that combo + // while the mask is on + // do we need to also disable the navigation buttons ? + + // when we have an example with multiple xml workspaces, + // then probably need to clear out the me.cbSampleGroup + // ('s store) + + var path = decodeURI(value).slice(5); + me.fetchSampleGroups(path); + + } else { + me.cbSampleGroup.setDisabled(false); + + if (me.cbSampleGroup.getValue() != '') { + me.btnProcess.setDisabled(false); + this.triggerBlur(); + me.tfAnalysisName.focus(); + } else { + this.triggerBlur(); + me.cbSampleGroup.focus(); + } + } + } + }, + minChars : 0, + queryMode : 'local', + store : me.strXML, + // tpl: '
{FileName:htmlEncode}
', + triggerAction : 'all', + typeAhead : true, + valueField : 'FilePath', + width : 200 + }); + + me.cbSampleGroup // = Ext4.create('Ext.form.field.ComboBox', { + = Ext4.create('Ext4.ux.form.field.ClearableComboBox', { + // Ext4.create('Ext.ux.CheckCombo', { + allowBlank : true, + disabled : true, + displayField : 'SampleGroup', + emptyText : 'Select...', + forceSelection : true, + listeners : { + change : function(combo, newValue, oldValue, eOpts) { + if (this.getValue() != '') { + me.btnProcess.setDisabled(false); + me.tfAnalysisName.focus(); // working? + } else { + me.btnProcess.setDisabled(true); + this.focus(); + } + }, + cleared : function() { + me.btnProcess.setDisabled(true); + this.focus(); + }, + select : function(combo, records, eOpts) { + me.btnProcess.setDisabled(false); + combo.triggerBlur(); + me.tfAnalysisName.focus(); + } + }, + minChars : 0, + queryMode : 'local', + store : me.strSampleGroup, + // tpl: '
{SampleGroup:htmlEncode}
', + // triggerAction : 'all', + typeAhead : true, + valueField : 'SampleGroup', + width : 200 + }); + + me.strSampleGroup.on({ + 'datachanged' : function() { + me.cbSampleGroup.enable(); + me.cbSampleGroup.focus(); + me.cbSampleGroup.expand(); + console + .log("listened to strSampleGroup store datachanged event"); + } + }); + + me.tfAnalysisName = Ext4.create('Ext.form.TextField', { + allowBlank : true, + emptyText : 'Type...', + width : 200 + }); + + me.tfAnalysisDescription = Ext4.create('Ext.form.TextField', { + allowBlank : true, + emptyText : 'Type...', + width : 200 + }); + + // /////////////////////////////////// + // Web parts // + // /////////////////////////////////// + + this.wpParseConfig = { + reportId : 'module:OpenCytoPreprocessing/OpenCytoPreprocessing.r', + // showSection: 'textOutput', // comment out to show debug output + title : 'ParseDiv' + }; + + this.wpParse = new LABKEY.WebPart({ + failure : function(errorInfo, options, responseObj) { + me.maskGlobal.hide(); + + me.cbXml.setDisabled(false); + me.cbSampleGroup.setDisabled(false); + me.tfAnalysisName.setDisabled(false); + me.tfAnalysisDescription.setDisabled(false); + + me.btnProcess.setDisabled(false); + + onFailure(errorInfo, options, responseObj); + }, + frame : 'none', + partConfig : me.wpParseConfig, + partName : 'Report', + renderTo : 'wpParse' + this.webPartDivId, + success : function() { + me.maskGlobal.hide(); + + me.cbXml.setDisabled(false); + me.cbSampleGroup.setDisabled(false); + me.tfAnalysisName.setDisabled(false); + me.tfAnalysisDescription.setDisabled(false); + + me.btnProcess.setDisabled(false); + + // var activeIndex = this.items.indexOf( + // this.getLayout().activeItem ) + direction; + me.pnlMain.getLayout().setActiveItem(1); + + me.fireEvent('preprocessed'); + } + }); + + // /////////////////////////////////// + // Panels, Containers, Components // + // /////////////////////////////////// + + var pnlWorkspace = Ext4.create('Ext.Panel', { + border : false, + cls : 'simple-panel-header', + items : [me.cbXml], + layout : 'fit', + title : 'Select the workspace:' + }); + + var pnlSampleGroup = Ext4.create('Ext.Panel', { + border : false, + headerCssClass : 'simple-panel-header', + items : [me.cbSampleGroup], + layout : 'fit', + title : 'Select the sample group:' + }); + + var pnlAnalysisName = Ext4.create('Ext.Panel', { + border : false, + headerCssClass : 'simple-panel-header', + items : [me.tfAnalysisName], + layout : 'fit', + title : 'Enter analysis name:' + }); + + var pnlAnalysisDescription = Ext4.create('Ext.Panel', { + border : false, + cls : 'simple-panel-header', + headerCssClass : 'simple-panel-header', + items : [me.tfAnalysisDescription], + layout : 'fit', + title : 'Enter analysis description:' + }); + + var pnlList = Ext4.create('Ext.Panel', { + border : false, + layout : { + type : 'column' + }, + defaults : { + /* + * bodyStyle : 'padding: 2px', width : 200, margin : '0 + * 15 0 15', + */ + bodyStyle : 'padding: 2px', + width : 200, + margin : '0 15 0 15', + + header : { + xtype : 'header', + ui : 'default-large' + } + }, + id : 'ulList' + this.webPartDivId, + items : [pnlWorkspace, pnlSampleGroup, pnlAnalysisName, + pnlAnalysisDescription] + }); + + var pnlWorkspaces = Ext4.create('Ext.Panel', { + header : { + xtype : 'header', + ui : 'default-large' + }, + cls : 'shaded-panel', + defaults : { + hideMode : 'visibility', // ? why not offsets ? + style : 'padding-bottom: 4px; padding-right: 4px; padding-left: 4px;' + }, + // disabled: true, + // forceLayout: true, + items : [pnlList, Ext4.create('Ext.Component', { + id : 'wpParse' + this.webPartDivId + })], + title : 'Workspaces' + }); + return pnlWorkspaces; + }, + + fetchSampleGroups : function(path) { + var me = this; + + me.xmlPath = path; + + var scriptConfig = { + success : function(result) { + var rConsole = result.console; + var errors = result.errors; + var outputParams = result.outputParams; + + me.maskGlobal.hide(); + + me.cbXml.setDisabled(false); + me.tfAnalysisName.setDisabled(false); + me.tfAnalysisDescription.setDisabled(false); + + if (errors && errors.length > 0) { + Ext4.MessageBox.show({ + title : "Error in fetchSampleGroups()", + msg : "Error in Rserve engine:

" + + errors[0].replace(/\n/g, '

'), + buttons : Ext4.MessageBox.OK, + resizable : true + }); + } else { + /* + * console.log("fetchSampleGroups() success."); + * console.log('resultObj:') console.dir(result); + * console.log('outputParams:') console.dir(outputParams); + */ + var inputArray = outputParams[0].value; + + me.cbSampleGroup.setDisabled(false); + inputArray = inputArray.replace(/\n/g, '').replace( + 'All Samples;', '').split(';'); + + var len = inputArray.length; + for (var i = 0; i < len; i++) { + inputArray[i] = [inputArray[i]]; + } + me.strSampleGroup.loadData(inputArray); + me.lastlySelectedXML = me.cbXml.getValue(); + } + }, + failure : function(errorInfo, options, responseObj) { + console.log("fetchSampleGroups() failure."); + me.maskGlobal.hide(); + + me.cbXml.setDisabled(false); + me.tfAnalysisName.setDisabled(false); + me.tfAnalysisDescription.setDisabled(false); + + onFailure(errorInfo, options, responseObj); + }, + + inputParams : { + // showSection: 'textOutput', // comment out to show debug + // output + path : path + }, + reportId : 'module:OpenCytoPreprocessing/SampleGroups.r', + reportSessionId : me.reportSessionId, + scope : me + }; + + LABKEY.Report.execute(scriptConfig); + }, + + preprocessData : function(records) { + var me = this; + var files = []; + Ext4.each(records, function(record) { + files.push(record.data.FileName); + }); + me.wpParseConfig.files = files.join(';'); + + me.wpParseConfig.xmlPath = me.xmlPath; + me.wpParseConfig.sampleGroupName = me.cbSampleGroup.getValue(); + me.wpParseConfig.analysisName = me.tfAnalysisName.getValue(); + me.wpParseConfig.analysisDescription = me.tfAnalysisDescription + .getValue(); + me.wpParseConfig.studyVars = me.cbStudyVarName.getValue(); + me.wpParseConfig.allStudyVars = me.cbStudyVarName.getAllValuesAsArray() + .join(); + console.log('Selected StudyVars: ' + me.wpParseConfig.allStudyVars); + me.wpParseConfig.rootPath = Ext4.util.Format.undef(this.rootPath); + me.wpParseConfig.reportSessionId = me.reportSessionId; + me.wpParse.render(); + }, + + smInitSmCheckBox : function() { + var me = this; + var smCheckBox = Ext4.create('Ext.selection.CheckboxModel', { + checkOnly : false, + injectCheckbox : 1, // Place the checkbox in the 2nd column + // of the grid. + listeners : { + deselect : { + fn : this.updateTableStatus, + scope : me + }, + select : { + fn : this.updateTableStatus, + scope : me + }, + selectionChange : function(obj, selected, eOpts) { + // console.log('selected'); + // console.dir(selected); + //console.log('selected records count =' + // + selected.length); + } + }, + sortable : true + }); + return smCheckBox; + }, + + pnlInitPnlTable : function(store, columns) { + var me = this; + me.smCheckBox = me.smInitSmCheckBox(); + + var pnlTable = Ext4.create('Ext.grid.Panel', { + autoScroll : true, + // frame: true, + columnLines : true, + columns : columns, + selModel : me.smCheckBox, + height : 200, + border : 1, + margin : 1, + cls : 'open-cyto-fcs-table', + loadMask : { + msg : 'Loading data...', + msgCls : 'x-mask-loading-custom' + }, + plugins : [Ext4.create('Ext.ux.ColumnAutoWidthPlugin', {}) + // , Ext4.create('Ext.ux.grid.FilterBar',{}) + ], + store : store, + stripeRows : true, + title : 'Files', + header : { + xtype : 'header' + }, + viewConfig : { + emptyText : 'No rows to display', + splitHandleWidth : 10, + border : 1, + margin : 1 + }, + listeners : { + afterrender : { + fn : function() { + console + .log('pnlTable afterrender event detected'); + }, + scope : me + }, + viewready : { + fn : function() { + console + .log('pnlTable viewready event detected'); + this.pnlTable.getSelectionModel().selectAll(); + }, + scope : me + }, + destroy : { + fn : function() { + console.log('pnlTable destroy event detected'); + }, + scope : me + } + } + }); + return pnlTable; + }, + + resize : function() { + // webPartContentWidth = + // document.getElementById(this.webPartDivId).offsetWidth; + + // if ( typeof resizableImage != 'undefined' ){ + // if ( $('#resultImage').width() > 2/3*pnlStudyVars.getWidth() ){ + // resizableImage.resizeTo( 2/3*pnlStudyVars.getWidth(), + // 2/3*pnlStudyVars.getWidth() ); + // } + // } + } +}); // end ExtJS4OpenCytoPreprocessing Panel class diff --git a/src/web/FlowLayoutTest.js b/src/web/FlowLayoutTest.js new file mode 100644 index 0000000..cac71ae --- /dev/null +++ b/src/web/FlowLayoutTest.js @@ -0,0 +1,15 @@ +Ext4.define('FlowLayoutTest', { + extend : 'Ext.panel.Panel', + constructor : function(config) { + config = config || {}; // Make sure config is not totally null. + config = Ext4.applyIf(config, { + // width : 700, + height : 500, + layout : { + type : 'column' + }, + defaults: {bodyStyle:'padding:15px'} + }); + this.callParent([ config ]); + } +}); diff --git a/src/web/HybridComboTest.js b/src/web/HybridComboTest.js new file mode 100644 index 0000000..e0372f1 --- /dev/null +++ b/src/web/HybridComboTest.js @@ -0,0 +1,253 @@ +function init() { + + // ClearButton from: + // http://www.eekboom.de/ClearButton.html + + var numbers = Ext4.create('Ext.data.Store', { + fields : ['name', 'digit'], + data : [{ + "name" : "One", + "digit" : "1" + }, { + "name" : "Two", + "digit" : "2" + }, { + "name" : "Uno is the name of the digit I'm looking for", + "digit" : "1" + }] + }); + + var states = Ext4.create('Ext.data.Store', { + fields : ['abbreviation', 'name'], + data : [{ + name : 'ALABAMA', + abbreviation : 'AL' + }, { + name : 'ALASKA', + abbreviation : 'AK' + }, { + name : 'AMERICAN SAMOA', + abbreviation : 'AS' + }, { + name : 'ARIZONA', + abbreviation : 'AZ' + }, { + name : 'ARKANSAS', + abbreviation : 'AR' + }, { + name : 'CALIFORNIA', + abbreviation : 'CA' + }, { + name : 'COLORADO', + abbreviation : 'CO' + }, { + name : 'CONNECTICUT', + abbreviation : 'CT' + }, { + name : 'DELAWARE', + abbreviation : 'DE' + }, { + name : 'DISTRICT OF COLUMBIA', + abbreviation : 'DC' + }, { + name : 'FEDERATED STATES OF MICRONESIA', + abbreviation : 'FM' + }, { + name : 'FLORIDA', + abbreviation : 'FL' + }, { + name : 'GEORGIA', + abbreviation : 'GA' + }, { + name : 'GUAM', + abbreviation : 'GU' + }, { + name : 'HAWAII', + abbreviation : 'HI' + }, { + name : 'IDAHO', + abbreviation : 'ID' + }, { + name : 'ILLINOIS', + abbreviation : 'IL' + }, { + name : 'INDIANA', + abbreviation : 'IN' + }, { + name : 'IOWA', + abbreviation : 'IA' + }, { + name : 'KANSAS', + abbreviation : 'KS' + }, { + name : 'KENTUCKY', + abbreviation : 'KY' + }, { + name : 'LOUISIANA', + abbreviation : 'LA' + }, { + name : 'MAINE', + abbreviation : 'ME' + }, { + name : 'MARSHALL ISLANDS', + abbreviation : 'MH' + }, { + name : 'MARYLAND', + abbreviation : 'MD' + }, { + name : 'MASSACHUSETTS', + abbreviation : 'MA' + }, { + name : 'MICHIGAN', + abbreviation : 'MI' + }, { + name : 'MINNESOTA', + abbreviation : 'MN' + }, { + name : 'MISSISSIPPI', + abbreviation : 'MS' + }, { + name : 'MISSOURI', + abbreviation : 'MO' + }, { + name : 'MONTANA', + abbreviation : 'MT' + }, { + name : 'NEBRASKA', + abbreviation : 'NE' + }, { + name : 'NEVADA', + abbreviation : 'NV' + }, { + name : 'NEW HAMPSHIRE', + abbreviation : 'NH' + }, { + name : 'NEW JERSEY', + abbreviation : 'NJ' + }, { + name : 'NEW MEXICO', + abbreviation : 'NM' + }, { + name : 'NEW YORK', + abbreviation : 'NY' + }, { + name : 'NORTH CAROLINA', + abbreviation : 'NC' + }, { + name : 'NORTH DAKOTA', + abbreviation : 'ND' + }, { + name : 'NORTHERN MARIANA ISLANDS', + abbreviation : 'MP' + }, { + name : 'OHIO', + abbreviation : 'OH' + }, { + name : 'OKLAHOMA', + abbreviation : 'OK' + }, { + name : 'OREGON', + abbreviation : 'OR' + }, { + name : 'PALAU', + abbreviation : 'PW' + }, { + name : 'PENNSYLVANIA', + abbreviation : 'PA' + }, { + name : 'PUERTO RICO', + abbreviation : 'PR' + }, { + name : 'RHODE ISLAND', + abbreviation : 'RI' + }, { + name : 'SOUTH CAROLINA', + abbreviation : 'SC' + }, { + name : 'SOUTH DAKOTA', + abbreviation : 'SD' + }, { + name : 'TENNESSEE', + abbreviation : 'TN' + }, { + name : 'TEXAS', + abbreviation : 'TX' + }, { + name : 'UTAH', + abbreviation : 'UT' + }, { + name : 'VERMONT', + abbreviation : 'VT' + }, { + name : 'VIRGIN ISLANDS', + abbreviation : 'VI' + }, { + name : 'VIRGINIA', + abbreviation : 'VA' + }, { + name : 'WASHINGTON', + abbreviation : 'WA' + }, { + name : 'WEST VIRGINIA', + abbreviation : 'WV' + }, { + name : 'WISCONSIN', + abbreviation : 'WI' + }, { + name : 'WYOMING', + abbreviation : 'WY' + }] + }); + + + // https://github.com/kveeiv/extjs-boxselect + var myComboBoxSelect203SelectedCount = Ext4.create('Ext4.ux.form.field.BoxSelect203', { + fieldLabel : 'Choose Name', + //store : numbers, + //queryMode : 'local', + //displayField : 'name', + //valueField : 'digit', + + store : states, + queryMode : 'local', + displayField : 'name', + valueField : 'abbreviation', + grow: true, + //stacked: true, + layout: 'form', + width : 380, + //plugins : ['clearbutton'] + plugins : ['selectedCount'], + autoScroll: true + // plugins: new Ext4.ux.form.field.ClearButton({animateClearButton: + // false, + // hideClearButtonWhenEmpty: false, + // hideClearButtonWhenMouseOut:false}) + + }); + + var resultBoxSelect203SelectedCount = Ext4.create('Ext.form.field.Text', { + fieldLabel : 'Number', + value : 'Nothing yet' + }); + + Ext4.create('Ext.form.Panel', { + title : 'Box Select Combo - version 2.03 with Selected Count Plugin', + width : 500, + //height : 400, + bodyPadding : 10, + renderTo : 'my-target9', + autoScroll: true, + layout: 'form', + items : [myComboBoxSelect203SelectedCount + //, resultBoxSelect203SelectedCount + ] + + }); + + myComboBoxSelect203SelectedCount.on('select', function(combo, records, eOpts) { + resultBoxSelect203SelectedCount.setValue(records[0].data.digit); + }); + + // /////////////////////////////////////////////////////////////////// +} diff --git a/src/web/OpenCyto/ExtJS4ClearableComboBox.js b/src/web/OpenCyto/ExtJS4ClearableComboBox.js new file mode 100644 index 0000000..9c505ff --- /dev/null +++ b/src/web/OpenCyto/ExtJS4ClearableComboBox.js @@ -0,0 +1,146 @@ +// vim: sw=4:ts=4:nu:nospell:fdc=4 +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* +// --- A ComboBox with a secondary trigger button that clears the contents of the ComboBox + +Ext4.define('Ext.form.ExtJS4ClearableComboBox', { + extend : 'Ext.ux.ExtJS4ResizableCombo', + alias: 'clearcombo', +//Ext.form.ExtJS4ClearableComboBox = Ext.extend(Ext.ux.ExtJS4ResizableCombo, { + initComponent: function() { + this.triggerConfig = { + tag:'span', cls:'x-form-twin-triggers', cn:[ + {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger"}, + {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger x-form-clear-trigger"} + ]}; + //Ext.form.ExtJS4ClearableComboBox.superclass.initComponent.call(this); + this.callParent(); + }, + onTrigger2Click : function() + { + console.log("clearing ClearableComboBox"); + this.collapse(); + this.reset(); // clear contents of combobox + this.fireEvent('cleared'); // send notification that contents have been cleared + }, + + getTrigger: Ext4.form.TwinTriggerField.prototype.getTrigger, + initTrigger: Ext4.form.TwinTriggerField.prototype.initTrigger, + onTrigger1Click: Ext.ux.ExtJS4ResizableCombo.prototype.onTriggerClick, + trigger1Class: Ext.ux.ExtJS4ResizableCombo.prototype.triggerClass +}); +//Ext4.reg('clearcombo', Ext.form.ExtJS4ClearableComboBox); +*/ + +/** +From: http://stackoverflow.com/questions/13830537/extjs4-add-an-empty-option-in-a-combobox +*/ +Ext4.define('Ext4.ux.form.field.ClearableComboBox', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.clearcombo', + + trigger2Cls: 'x4-form-clear-trigger', + + initComponent: function () { + var me = this; + + + me.addEvents( + /** + * @event beforeclear + * + * @param {FilterCombo} FilterCombo The filtercombo that triggered the event + */ + 'beforeclear', + /** + * @event beforeclear + * + * @param {FilterCombo} FilterCombo The filtercombo that triggered the event + */ + 'clear' + ); + + me.callParent(arguments); + + me.on('specialkey', this.onSpecialKeyDown, me); + me.on('select', function (me, rec) { + me.onShowClearTrigger(true); + }, me); + me.on('afterrender', function () { me.onShowClearTrigger(false); }, me); + }, + + /** + * @private onSpecialKeyDown + * eventhandler for special keys + */ + onSpecialKeyDown: function (obj, e, opt) { + if ( e.getKey() == e.ESC ) + { + this.clear(); + } + }, + + onShowClearTrigger: function (show) { + var me = this; + + if (show) { + me.triggerEl.each(function (el, c, i) { + if (i === 1) { + el.setWidth(el.originWidth, false); + el.setVisible(true); + me.active = true; + } + }); + } else { + me.triggerEl.each(function (el, c, i) { + if (i === 1) { + el.originWidth = el.getWidth(); + el.setWidth(0, false); + el.setVisible(false); + me.active = false; + } + }); + } + // ToDo -> Version specific methods + if (Ext4.lastRegisteredVersion.shortVersion > 407) { + me.updateLayout(); + } else { + me.updateEditState(); + } + }, + + /** + * @override onTrigger2Click + * eventhandler + */ + onTrigger2Click: function (args) { + this.clear(); + }, + + /** + * @private clear + * clears the current search + */ + clear: function () { + var me = this; + me.fireEvent('beforeclear', me); + me.clearValue(); + me.onShowClearTrigger(false); + me.fireEvent('clear', me); + } +}); \ No newline at end of file diff --git a/src/web/OpenCyto/ExtJS4OpenCyto.css b/src/web/OpenCyto/ExtJS4OpenCyto.css new file mode 100644 index 0000000..b980ff6 --- /dev/null +++ b/src/web/OpenCyto/ExtJS4OpenCyto.css @@ -0,0 +1,378 @@ +/* + vim: ts=4:sw=4:nu:fdc=4:nospell +*/ +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@charset 'utf-8'; + +/* Error code from R */ +pre { + /*text-align: center;*/ +} + +/* Class specs */ + +.right-text { + text-align: right; +} + +.left-text { + text-align: left; +} + +/* Centered text */ +.centered-text, .bold-centered-text, .sortable-list li, .x-grid4-hd-row { + text-align: center !important; +} + +/* Bold text */ +.bold-text, .bold-centered-text, .sortable-list li, .infoMask div, .x4-mask-loading-custom div, .ux-lovcombo-list-item-all { + font-weight: bold; +} + +/* Auto margin */ +.centered-table { + margin: auto; +} + +.x4-btn { + margin-right: 0px !important; +} + +/* Zero margin */ +.x4-tab-strip, .sortable-list, .ulList { + margin: 0; +} + +/* Zero padding */ +.x4-tab-panel-header, .sortable-list, .ulList { + padding: 0; +} + +/* Hidden class */ +.hidden { + display: none; + visibility: hidden; +} + + +.simple-panel-header { + background-color: white !important; + border-bottom-width: 0px !important; + font-family: verdana, arial, helvetica, sans-serif; + padding-bottom: 3px; + padding-left: 0px; +} + +/* Change colors of Panels and Panel Headers from their dark originals */ + +div.x4-reset div.open-cyto.x4-panel { + background-image: url(white_85_trans.png); +} + +div.x4-reset div.open-cyto.x4-panel div.x4-panel-header.x4-panel-header-default { + background-image: url(white_85_trans.png); +} + +div.x4-reset div.open-cyto.x4-panel div.x4-panel-header.x4-panel-header-default.x4-panel-header-default-top { + box-shadow: none; +} + +div.x4-reset div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel.x4-grid div.x4-panel-header.x4-panel-header-default { + background-image: url(white_85_trans.png); +} + +div.x4-reset div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel.x4-grid div.x4-panel-header.x4-panel-header-default.x4-panel-header-default-top { + box-shadow: none; +} + +/* Add bottom border to toolbar */ +div.x4-reset div.open-cyto.x4-panel div.x4-docked-top { + border-bottom-width: 1px !important; +} + +/* Attempt to clip background image of toolbar to something less thn its default length. */ +div.x4-reset div.open-cyto.x4-panel div.x4-toolbar.x4-docked { + background-clip: content-box; + -moz-box-sizing: content-box; +} + + +div.x4-reset div.open-cyto.x4-panel .x4-panel-header-horizontal { + /* padding: 3px 5px 4px; */ + padding: 3px 0px 0px; +} + +/* +div.x4-reset div.open-cyto.x4-panel .shaded-panel>div.x4-panel-header>div.x4-panel-header-body { + + background-color: WhiteSmoke; + background-image: none; + border-color: WhiteSmoke; + box-shadow: 0px 1px 0px 0px WhiteSmoke inset; + + background-image: url(white_85_trans.png); + +} + +div.x4-reset div.open-cyto.x4-panel .x4-panel-header, div.x4-reset div.open-cyto.x4-panel .x4-panel-header-body +{ + + background-color: white; + background-image: none; + border-color: WhiteSmoke; + box-shadow: 0px 1px 0px 0px white inset; + + background-image: url(white_85_trans.png); + + color: black !important; + border-bottom-width: 0px !important; + font-family: verdana, arial, helvetica, sans-serif; + padding-bottom: 3px; + padding-left: 0px; + z-index: 1; +} +*/ +div.x4-reset div.open-cyto.x4-panel div.x4-panel.shaded-panel { + background-image: url(white_85_trans.png); +} + + + +/* Make the header fonts bigger. Unfortunately when setting a value for the font-size, + * the calculated size of the header text is not + * increased so that a long title will not be given enough space to render. + * So we increase the font-size indirectly by changing the header's UI style to the built-in + * type 'default-larger' during component construction. + * */ + +div.x4-reset div.open-cyto .x4-panel-header-text-default-large { + /* font-size: 120%; */ + font-weight: bold !important; +} + + +/* Make panel text black */ +div.x4-reset div.open-cyto .x4-panel-header-text-default, div.x4-reset div.open-cyto .x4-panel-header-text-default-large { + color: black !important; +} + + +/* Clear Gray border above ComboBoxes. */ +div.x4-reset div.open-cyto.x4-panel .x4-panel-body-default { + border-color: white; +} + +/* Maintain Gray border around FCS Grid */ +div.x4-reset div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel div.x4-panel-header { + border-color: grey; + /* Omit LabKey's default box-shadow color */ + box-shadow: 'inset 0px 1px 0px 0px'; +} + +div.x4-reset div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel div.x4-panel-header.x4-panel-header-default-top { + /* Omit LabKey's default box-shadow color */ + box-shadow: 'inset 0px 1px 0px 0px'; +} + +.x4-reset div.open-cyto.x4-panel .x4-toolbar-default { + background-image: url(white_85_trans.png); +} + +.x4-reset open-cyto.x4-panel .x4-panel-header-default-top { + /* Omit LabKey's default box-shadow color */ + box-shadow: 0 1px 0 0 inset !important; +} + +/* + * Create a border around FCS Files Grid + */ +div.x4-reset div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel.x4-grid { + border-color: grey; + border-width: 1px; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; +} + + +div.open-cyto.x4-panel div.open-cyto-fcs-table.x4-panel.x4-grid div.x4-panel-body.x4-grid-body.x4-panel-body-default { + border-color: grey; +} + +/* + * Increase height of grid column headers to match height of checkbox column. + */ +div.open-cyto-fcs-header.x4-column-header>div.x4-column-header-inner { + line-height: 18px !important; +} + +.x4-mask-loading-custom { + border-left: 0px; + left: 0 !important; + margin-left: 0px; + padding-left: 0px; + width: 100%; + z-index: 9000; +} + +.x4-mask-loading-custom div { + background: no-repeat 5px 5px; + background-color: white !important; + background-image: url(../ext-4.1.0/resources/themes/images/default/grid/loading.gif) !important; + /*line-height:16px;*/ + padding: 5px 10px 5px 25px; + text-align: center; +} + +.infoMask { + width: 100% !important; + z-index: 9000; +} + +.infoMask div { + background-color: white; + cursor: default; + text-align: center; +} + +ul.x4-tab-strip-top { + padding-top: 0px; +} + +.x4-box-inner { + /*margin-top: 2px;*/ +} + +/* Ext4 */ +.x4-btn-inner { + padding-left: 4px; + padding-right: 4px; +} + + +/* Tweaks for jQuery drag-and-drop in particular and list in general */ +.draggableHandle { + cursor: move; +} + +.ui-state-highlight { + width: 1em; + line-height: 1.2em; +} + +.ui-sortable-helper, .sortable-list, .ulList { + list-style-type: none; +} + +.sortable-list li, .ulList li { + margin: 3px 5px 3px 5px; /* 0.2em 0.3em 0.2em 0.3em */ + padding: 1px; + float: left; +} + +.sortable-list li { + font-size: 1em; +} +/**********************************/ + +/* Ext TabPanel fixes */ +.x4-tab-strip { + padding-left: 5px; +} + +.x4-tab-right { + border-bottom-width: 0px !important; +} +/********************************/ + +/* Ext.Resizable fix */ +.xresizable-wrap { + margin: 0 auto; +} +/********************************/ + +/* ExtJS GridPanel */ +/* Override standard grid styles (add color to vertical grid lines) */ +.x-grid4-col { + border-left: 1px solid #EEEEEE; + border-right: 1px solid #D2D2D2; +} + +/* Also remove padding from table data (to compensate for added grid lines) */ +.x-grid4-row td, .x-grid4-summary-row td { + padding-left: 0px; + padding-right: 0px; +} + +.x-grid4-header-offset { + width: auto !important; +} + +/* ? */ +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .x-grid4-cell { + box-sizing: border-box; + } +} +/*************************************/ + +/* Fix so that the buttons on toolbars look normal (not text) */ +.x4-toolbar .x4-btn-tl,.x4-toolbar .x4-btn-tr,.x4-toolbar .x4-btn-tc,.x4-toolbar .x4-btn-ml,.x4-toolbar .x4-btn-mr,.x4-toolbar .x4-btn-mc,.x4-toolbar .x4-btn-bl,.x4-toolbar .x4-btn-br,.x4-toolbar .x4-btn-bc { + background-image:url(../gtheme/img/lkbtn.gif) ; +} +/* Fix so that the buttons on toolbars look normal (not text) - Really, for Ext JS 4*/ +div.x4-reset div.open-cyto.x4-panel div.x4-toolbar div.x4-btn { + border-width: 1px; + border-style: solid; + padding: 0px 2px 0px 2px; + margin: 0px 2px 0px 2px; + border-color: grey; +} + +.x4-toolbar .x4-btn-tl{ + background-position: 0 0; +} +.x4-toolbar .x4-btn-tr{ + background-position: -3px 0; +} +.x4-toolbar .x4-btn-tc{ + background-position: 0 -6px; +} + +.x4-toolbar .x4-btn-ml{ + background-position: 0px -24px; +} +.x4-toolbar .x4-btn-mr{ + background-position: -3px -24px; +} + +.x4-toolbar .x4-btn-mc{ + background-position: 0 -1096px; +} +.x4-toolbar .x4-btn-bl{ + background-position: 0 -3px; +} +.x4-toolbar .x4-btn-br{ + background-position: -3px -3px; +} +.x4-toolbar .x4-btn-bc{ + background-position: 0 -15px; +} + +/**********************************/ diff --git a/src/web/OpenCyto/ExtJS4OpenCyto.js b/src/web/OpenCyto/ExtJS4OpenCyto.js new file mode 100644 index 0000000..298857e --- /dev/null +++ b/src/web/OpenCyto/ExtJS4OpenCyto.js @@ -0,0 +1,346 @@ +// vim: sw=4:ts=4:nu:nospell:fdc=4 +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function removeById(elId) { + $( '#' + elId ).remove(); +}; + +function removeByClass(className) { + $( '.' + className ).remove(); +}; + +function captureEvents(observable) { + Ext4.util.Observable.capture( + observable, + function(eventName) { + console.info(eventName); + }, + this + ); +}; + +function onFailure(errorInfo, options, responseObj){ + var strngErrorContact = ' Please, contact ldashevs@fhcrc.org, if you have questions.'; + + if (errorInfo && errorInfo.exception) + Ext4.Msg.alert('Error', 'Failure: ' + errorInfo.exception + strngErrorContact); + else { + if ( responseObj != undefined ){ + Ext4.Msg.alert('Error', 'Failure: ' + responseObj.statusText + strngErrorContact); + } else { + Ext4.Msg.alert('Error', 'Failure: ' + errorInfo.statusText + (errorInfo.timedout==true?', timed out.':'') + strngErrorContact); + } + } +}; + +Ext4.Ajax.timeout = 60 * 60 * 1000; // override the timeout to be 60 mintues; value is in milliseconds + +Ext4.QuickTips.init(); + +// IE 7 compatibility +Object.keys = Object.keys || (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !{toString:null}.propertyIsEnumerable("toString"), + DontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + DontEnumsLength = DontEnums.length; + + return function (o) { + if (((typeof o != "object") && (typeof o != "function")) || (o === null)) + throw new TypeError("Object.keys called on a non-object"); + + var result = []; + for (var name in o) { + if (hasOwnProperty.call(o, name)) + result.push(name); + } + + if (hasDontEnumBug) { + for (var i = 0; i < DontEnumsLength; i++) { + if (hasOwnProperty.call(o, DontEnums[i])) + result.push(DontEnums[i]); + } + } + + return result; + }; +})(); + +// Search in the middle of words / case insensitive +//Ext4.override (Ext.ux.form.SuperBoxSelect, { +/* +Ext4.override (Ext4.ux.form.field.BoxSelect203, { +//Ext4.override (Ext4.ux.form.field.BoxSelect, { + anyMatch: true, + caseSensitive: false, + //override doQuery function + doQuery : function(q, forceAll){ + + if(q === undefined || q === null){ + q = ''; + } + + var qe = { + query: q, + forceAll: forceAll, + combo: this, + cancel:false + }; + + if(this.fireEvent('beforequery', qe)===false || qe.cancel){ + return false; + } + + q = qe.query; + forceAll = qe.forceAll; + if(forceAll === true || (q.length >= this.minChars)){ + if(this.lastQuery !== q){ + this.lastQuery = q; + if(this.queryMode == 'local'){ + this.selectedIndex = -1; + if(forceAll){ + this.store.clearFilter(); + }else{ + this.store.filter(this.displayField, q, this.anyMatch, this.caseSensitive); + } + this.onLoad(); + }else{ + this.store.baseParams[this.queryParam] = q; + this.store.load({ + params: this.getParams(q) + }); + this.expand(); + } + }else{ + this.selectedIndex = -1; + this.onLoad(); + } + } + }, + onTypeAhead: function() { + var nodes = this.view.getNodes(); + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + var d = this.view.getRecord(n).data; + var re = new RegExp('(.*?)(' + '' + Ext4.String.escapeRegex(this.getRawValue()) + ')(.*)', this.caseSensitive ? '' : 'i'); + var h = d[this.displayField]; + + h=h.replace(re, '$1$2$3'); + n.innerHTML=h; + } + } +}); + +*/ + +/* + * Override to set Tab titles centered (can do any other customizations here) + */ + +Ext4.override (Ext4.TabPanel, { +//Ext4.TabPanel.override({ + + tabStripInnerStyle : 'text-align: center;', + + onRender : function(ct, position){ + //Ext.TabPanel.superclass.onRender.call(this, ct, position); + this.callParent(ct, position); + + if(this.plain){ + var pos = this.tabPosition == 'top' ? 'header' : 'footer'; + this[pos].addClass('x-tab-panel-'+pos+'-plain'); + } + + var st = this[this.stripTarget]; + + this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{ + tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}}); + + var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); + st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); + this.strip = new Ext.Element(this.stripWrap.dom.firstChild); + + + this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge', cn: [{tag: 'span', cls: 'x-tab-strip-text', cn: ' '}]}); + this.strip.createChild({cls:'x-clear'}); + + this.body.addClass('x-tab-panel-body-'+this.tabPosition); + + + if(!this.itemTpl){ + var tt = new Ext4.Template( + '

  • ', + '', + '{text}', + '
  • ' + ); + tt.disableFormats = true; + tt.compile(); + Ext.tab.Panel.addMembers({itemTpl: tt}); + } + + this.items.each(this.initTab, this); + }, + + initTab : function(item, index){ + var before = this.strip.dom.childNodes[index], + p = this.getTemplateArgs(item); + p.tabStripInnerStyle = this.tabStripInnerStyle; + var el = before ? + this.itemTpl.insertBefore(before, p) : + this.itemTpl.append(this.strip, p), + cls = 'x-tab-strip-over', + tabEl = Ext4.get(el); + + tabEl.hover(function(){ + if(!item.disabled){ + tabEl.addClass(cls); + } + }, function(){ + tabEl.removeClass(cls); + }); + + if(item.tabTip){ + tabEl.child('span.x-tab-strip-text', true).qtip = item.tabTip; + } + item.tabEl = el; + + + tabEl.select('a').on('click', function(e){ + if(!e.getPageX()){ + this.onStripMouseDown(e); + } + }, this, {preventDefault: true}); + + item.on({ + scope: this, + disable: this.onItemDisabled, + enable: this.onItemEnabled, + titlechange: this.onItemTitleChanged, + iconchange: this.onItemIconChanged, + beforeshow: this.onBeforeShowItem + }); + } + +}); + +// Remove elements from an array by values +Array.prototype.remove = function() { + var what, a = arguments, L = a.length, ax; + while (L && this.length) { + what = a[--L]; + while ((ax = this.indexOf(what)) !== -1) { + this.splice(ax, 1); + } + } + return this; +}; + +// IE8 and below +if(!Array.prototype.indexOf) { + Array.prototype.indexOf = function(what, i) { + i = i || 0; + var L = this.length; + while (i < L) { + if(this[i] === what) return i; + ++i; + } + return -1; + }; +} + + +// ? First column non-moveable + +Ext4.override(Ext4.grid.header.DragZone, { + getDragData: function (e) { + var header = e.getTarget('.'+this.colHeaderCls), + headerCmp, + ddel; + + if (header) { + headerCmp = Ext4.getCmp(header.id); + if (!this.headerCt.dragging && headerCmp.draggable && !(headerCmp.isOnLeftEdge(e) || headerCmp.isOnRightEdge(e))) { + ddel = document.createElement('div'); + ddel.innerHTML = Ext4.getCmp(header.id).text; + return { + ddel: ddel, + header: headerCmp + }; + } + } + return false; + + + } +}); + + /* + var t = Ext.lib.Event.getTarget(e); + var h = this.view.findHeaderCell(t); + if (h && (this.grid.colModel.config[this.view.getCellIndex(h)].dragable !== false)) { + return { + ddel: h.firstChild, + header: h + }; + } + return false; + */ + + +// ? // +/* Haven't found an equivalent for this in Ext JS4... + +Ext.CustomColumnModel = Ext.extend(Ext.grid.ColumnModel, { + moveColumn: function (oldIndex, newIndex) { + if (oldIndex == 0 || newIndex == 0) { + // Do nothing. + } + else { + var c = this.config[oldIndex]; + this.config.splice(oldIndex, 1); + this.config.splice(newIndex, 0, c); + this.dataMap = null; + this.fireEvent("columnmoved", this, oldIndex, newIndex); + } + } +}); +*/ +// Empty item in a ComboBox should now appear full height with this fix +/*Ext.override (Ext.form.ComboBox, { + initList : (function() { + if (!this.tpl) { + this.tpl = new Ext.XTemplate( + '
    {', + this.displayField, + ':this.blank}
    ', + { + blank : function(value) { + return value === '' ? ' ' + : value; + } + }); + } + }).createSequence(Ext.form.ComboBox.prototype.initList) + });*/ diff --git a/src/web/OpenCyto/ExtJS4ResizableCombo.js b/src/web/OpenCyto/ExtJS4ResizableCombo.js new file mode 100644 index 0000000..75a356a --- /dev/null +++ b/src/web/OpenCyto/ExtJS4ResizableCombo.js @@ -0,0 +1,75 @@ +// vim: sw=4:ts=4:nu:nospell:fdc=4 +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.ux.ExtJS4ResizableCombo = + + +Ext4.define('Ext.ux.ExtJS4ResizableCombo', { + extend : 'Ext.form.field.ComboBox', + alias: 'resizable-combo', +//Ext.ux.ExtJS4ResizableCombo = Ext.extend(Ext.form.ComboBox, { + initComponent: function(){ + //Ext.ux.ExtJS4ResizableCombo.superclass.initComponent.call(this); + this.callParent(); + this.on('afterrender', this.resizeToFitContent, this); + this.store.on({ + 'datachanged': this.resizeToFitContent, + 'add': this.resizeToFitContent, + 'remove': this.resizeToFitContent, + 'load': this.resizeToFitContent, + 'update': this.resizeToFitContent, + buffer: 10, + scope: this + }); + }, + resizeToFitContent: function(){ + if (!this.elMetrics){ + //this.elMetrics = Ext4.util.TextMetrics.createInstance(this.getEl()); + this.elMetrics = new Ext4.util.TextMetrics(this.getEl()); + } + //var m = this.elMetrics, width = 0, el = this.el, s = this.getSize(); + var m = this.elMetrics, width = 0, el = this.getEl(), s; + if(el) { + s = this.getEl().getSize(); + this.store.each(function (r) { + var text = r.get(this.displayField); + width = Math.max(width, m.getWidth( Ext4.util.Format.htmlEncode(text) )); + }, this); + if (el) { + width += el.getBorderWidth('lr'); + width += el.getPadding('lr'); + } + if (this.trigger) { + width += this.trigger.getWidth(); + } + s.width = width; + width += 3*Ext4.getScrollBarWidth() + 20; + this.listWidth = width; + this.minListWidth = width; + if ( this.list != undefined ){ + this.list.setSize(width); + } + if ( this.innerList != undefined ){ + this.innerList.setSize(width); + } + } + else { + console.error("el for this ResizablecomboBox was not defined"); + } + } +}); +//Ext.reg('resizable-combo', Ext.ux.ExtJS4ResizableCombo); diff --git a/src/web/OpenCyto/OpenCyto.css b/src/web/OpenCyto/OpenCyto.css index 847163b..bcaac99 100644 --- a/src/web/OpenCyto/OpenCyto.css +++ b/src/web/OpenCyto/OpenCyto.css @@ -35,7 +35,7 @@ pre { } /* Centered text */ -.centered-text, .bold-centered-text, .sortable-list li, .x-grid3-hd-row, { +.centered-text, .bold-centered-text, .sortable-list li, .x-grid3-hd-row { text-align: center !important; } diff --git a/src/web/OpenCyto/SuperBoxSelect/ExtJS4SuperBoxSelect.js b/src/web/OpenCyto/SuperBoxSelect/ExtJS4SuperBoxSelect.js new file mode 100644 index 0000000..932eed5 --- /dev/null +++ b/src/web/OpenCyto/SuperBoxSelect/ExtJS4SuperBoxSelect.js @@ -0,0 +1,1894 @@ +// vim: sw=4:ts=4:nu:nospell:fdc=4 +/* + * Copyright 2012 Fred Hutchinson Cancer Research Center + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +Ext.namespace('Ext.ux.form'); +/** + *

    + * SuperBoxSelect is an extension of the ComboBox component that displays + * selected items as labelled boxes within the form field. As seen on facebook, + * hotmail and other sites. + *

    + *

    + * The SuperBoxSelect component was inspired by the BoxSelect component found + * here: http://efattal.fr/en/extjs/extuxboxselect/ + *

    + * + * @author Dan Humphrey + * @class Ext.ux.form.SuperBoxSelect + * @extends Ext.form.ComboBox + * @constructor + * @component + * @version 1.0b + * @license TBA + * + */ +Ext.ux.form.SuperBoxSelect = function(config) { + Ext.ux.form.SuperBoxSelect.superclass.constructor.call(this, config); + this.addEvents( + /** + * Fires before an item is added to the component via user + * interaction. Return false from the callback function to prevent + * the item from being added. + * + * @event beforeadditem + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + * @param {Mixed} + * value The value of the item to be added + */ + 'beforeadditem', + + /** + * Fires after a new item is added to the component. + * + * @event additem + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + * @param {Mixed} + * value The value of the item which was added + * @param {Record} + * record The store record which was added + */ + 'additem', + + /** + * Fires when the allowAddNewData config is set to true, and a user + * attempts to add an item that is not in the data store. + * + * @event newitem + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + * @param {Mixed} + * value The new item's value + */ + 'newitem', + + /** + * Fires when an item's remove button is clicked. Return false from + * the callback function to prevent the item from being removed. + * + * @event beforeremoveitem + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + * @param {Mixed} + * value The value of the item to be removed + */ + 'beforeremoveitem', + + /** + * Fires after an item has been removed. + * + * @event removeitem + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + * @param {Mixed} + * value The value of the item which was removed + * @param {Record} + * record The store record which was removed + */ + 'removeitem', + /** + * Fires after the component values have been cleared. + * + * @event clear + * @memberOf Ext.ux.form.SuperBoxSelect + * @param {SuperBoxSelect} + * this + */ + 'clear'); + +}; +/** + * @private hide from doc gen + */ +Ext.ux.form.SuperBoxSelect = Ext.extend(Ext.ux.form.SuperBoxSelect, + Ext.form.ComboBox, { + maxCustomHeight : null, + /** + * @cfg {Boolean} allowAddNewData When set to true, allows items to + * be added (via the setValueEx and addItem methods) that do + * not already exist in the data store. Defaults to false. + */ + allowAddNewData : false, + + /** + * @cfg {Boolean} backspaceDeletesLastItem When set to false, the + * BACKSPACE key will focus the last selected item. When set to + * true, the last item will be immediately deleted. Defaults to + * true. + */ + backspaceDeletesLastItem : true, + + /** + * @cfg {String} classField The underlying data field that will be + * used to supply an additional class to each item. + */ + classField : null, + + /** + * @cfg {String} clearBtnCls An additional class to add to the + * in-field clear button. + */ + clearBtnCls : '', + + /** + * @cfg {String/XTemplate} displayFieldTpl A template for rendering + * the displayField in each selected item. Defaults to null. + */ + displayFieldTpl : null, + + /** + * @cfg {String} extraItemCls An additional css class to apply to + * each item. + */ + extraItemCls : 'x-panel-header', + + /** + * @cfg {String/Object/Function} extraItemStyle Additional css + * style(s) to apply to each item. Should be a valid argument + * to Ext.Element.applyStyles. + */ + extraItemStyle : '', + + /** + * @cfg {String} expandBtnCls An additional class to add to the + * in-field expand button. + */ + expandBtnCls : '', + + /** + * @cfg {Boolean} fixFocusOnTabSelect When set to true, the + * component will not lose focus when a list item is selected + * with the TAB key. Defaults to true. + */ + fixFocusOnTabSelect : true, + + /** + * @cfg {Boolean} navigateItemsWithTab When set to true the tab key + * will navigate between selected items. Defaults to true. + */ + navigateItemsWithTab : true, + + /** + * @cfg {Boolean} pinList When set to true the select list will be + * pinned to allow for multiple selections. Defaults to true. + */ + pinList : true, + + /** + * @cfg {Boolean} preventDuplicates When set to true unique item + * values will be enforced. Defaults to true. + */ + preventDuplicates : true, + + /** + * @cfg {String} queryValuesDelimiter Used to delimit multiple + * values queried from the server when mode is remote. + */ + queryValuesDelimiter : '|', + + /** + * @cfg {String} queryValuesIndicator A request variable that is + * sent to the server (as true) to indicate that we are + * querying values rather than display data (as used in + * autocomplete) when mode is remote. + */ + queryValuesInidicator : 'valuesqry', + + /** + * @cfg {Boolean} removeValuesFromStore When set to true, selected + * records will be removed from the store. Defaults to true. + */ + removeValuesFromStore : true, + + /** + * @cfg {String} renderFieldBtns When set to true, will render + * in-field buttons for clearing the component, and displaying + * the list for selection. Defaults to true. + */ + renderFieldBtns : true, + + /** + * @cfg {Boolean} stackItems When set to true, the items will be + * stacked 1 per line. Defaults to false which displays the + * items inline. + */ + stackItems : false, + + /** + * @cfg {String} styleField The underlying data field that will be + * used to supply additional css styles to each item. + */ + styleField : null, + + /** + * @cfg {Boolean} supressClearValueRemoveEvents When true, the + * removeitem event will not be fired for each item when the + * clearValue method is called, or when the clear button is + * used. Defaults to false. + */ + supressClearValueRemoveEvents : false, + + /** + * @cfg {String/Boolean} validationEvent The event that should + * initiate field validation. Set to false to disable automatic + * validation (defaults to 'blur'). + */ + validationEvent : 'blur', + + /** + * @cfg {String} valueDelimiter The delimiter to use when joining + * and splitting value arrays and strings. + */ + valueDelimiter : ',', + initComponent : function() { + Ext.apply(this, { + items : new Ext.util.MixedCollection(false), + usedRecords : new Ext.util.MixedCollection(false), + addedRecords : [], + remoteLookup : [], + hideTrigger : true, + grow : false, + resizable : false, + multiSelectMode : false, + preRenderValue : null + }); + + if (this.transform) { + this.doTransform(); + } + + Ext.ux.form.SuperBoxSelect.superclass.initComponent.call(this); + if (this.mode === 'remote' && this.store) { + this.store.on('load', this.onStoreLoad, this); + } + this.on('render', this.resizeToFitContent, this); + }, + resizeToFitContent: function(){ + if (!this.elMetrics){ + this.elMetrics = Ext.util.TextMetrics.createInstance(this.getEl()); + } + var m = this.elMetrics, width = 0, el = this.el, s = this.getSize(); + this.store.each(function (r) { + var text = r.get(this.displayField); + width = Math.max(width, m.getWidth( Ext.util.Format.htmlEncode(text) )); + }, this); + if (el) { + width += el.getBorderWidth('lr'); + width += el.getPadding('lr'); + } + if (this.trigger) { + width += this.trigger.getWidth(); + } + s.width = width; + width += 3*Ext.getScrollBarWidth() + 20; + this.listWidth = width; + this.minListWidth = width; + if ( this.list != undefined ){ + this.list.setSize(width); + } + if ( this.innerList != undefined ){ + this.innerList.setSize(width); + } + this.store.on({ + 'datachanged': this.resizeToFitContent, + 'add': this.resizeToFitContent, + 'remove': this.resizeToFitContent, + 'load': this.resizeToFitContent, + 'update': this.resizeToFitContent, + buffer: 10, + scope: this + }); + }, + onRender : function(ct, position) { + Ext.ux.form.SuperBoxSelect.superclass.onRender.call(this, ct, + position); + + this.el.dom.removeAttribute('name'); + + var extraClass = (this.stackItems === true) + ? 'x-superboxselect-stacked' + : ''; + if (this.renderFieldBtns) { + extraClass += ' x-superboxselect-display-btns'; + } + this.el.removeClass('x-form-text') + .addClass('x-superboxselect-input-field'); + + this.wrapEl = this.el.wrap({ + tag : 'ul' + }); + + this.outerWrapEl = this.wrapEl.wrap({ + tag : 'div', + cls : 'x-form-text x-superboxselect ' + extraClass, + style : Ext.isEmpty(this.maxCustomHeight) + ? 'height: auto;' + : 'height: ' + this.maxCustomHeight + 'px;' + }); + + this.inputEl = this.el.wrap({ + tag : 'li', + cls : 'x-superboxselect-input' + }); + + if (this.renderFieldBtns) { + this.setupFieldButtons().manageClearBtn(); + } + + this.setupFormInterception(); + + if (this.preRenderValue) { + this.setValue(this.preRenderValue); + this.preRenderValue = null; + } + }, + onStoreLoad : function(store, records, options) { + // accomodating for bug in Ext 3.0.0 where + // options.params are empty + var q = options.params[this.queryParam] + || store.baseParams[this.queryParam] || "", isValuesQuery = options.params[this.queryValuesInidicator] + || store.baseParams[this.queryValuesInidicator]; + + if (this.removeValuesFromStore) { + this.store.each(function(record) { + if (this.usedRecords.containsKey(record + .get(this.valueField))) { + this.store.remove(record); + } + }, this); + } + // queried values + if (isValuesQuery) { + var params = q.split(this.queryValuesDelimiter); + Ext.each(params, function(p) { + this.remoteLookup.remove(p); + var rec = this.findRecord(this.valueField, p); + if (rec) { + this.addRecord(rec); + } + }, this); + + if (this.setOriginal) { + this.setOriginal = false; + this.originalValue = this.getValue(); + } + } + + // queried display (autocomplete) & addItem + if (q !== '' && this.allowAddNewData) { + Ext.each(this.remoteLookup, function(r) { + if (typeof r == "object" && r[this.displayField] == q) { + this.remoteLookup.remove(r); + if (records.length + && records[0].get(this.displayField) === q) { + this.addRecord(records[0]); + return; + } + var rec = this.createRecord(r); + this.store.add(rec); + this.addRecord(rec); + this.addedRecords.push(rec); // keep + // track + // of + // records + // added + // to + // store + (function() { + if (this.isExpanded()) { + this.collapse(); + } + }).defer(10, this); + return; + } + }, this); + } + + var toAdd = []; + if (q === '') { + Ext.each(this.addedRecords, function(rec) { + if (this.preventDuplicates + && this.usedRecords.containsKey(rec + .get(this.valueField))) { + return; + } + toAdd.push(rec); + + }, this); + + } else { + var re = new RegExp(Ext.escapeRe(q) + '.*', 'i'); + Ext.each(this.addedRecords, function(rec) { + if (this.preventDuplicates + && this.usedRecords.containsKey(rec + .get(this.valueField))) { + return; + } + if (re.test(rec.get(this.displayField))) { + toAdd.push(rec); + } + }, this); + } + this.store.add(toAdd); + this.store.sort(this.displayField, 'ASC'); + + if (this.store.getCount() === 0 && this.isExpanded()) { + this.collapse(); + } + + }, + doTransform : function() { + var s = Ext.getDom(this.transform), transformValues = []; + if (!this.store) { + this.mode = 'local'; + var d = [], opts = s.options; + for (var i = 0, len = opts.length; i < len; i++) { + var o = opts[i], value = (Ext.isIE && !Ext.isIE8 ? o + .getAttributeNode('value').specified : o + .hasAttribute('value')) ? o.value : o.text, cls = (Ext.isIE + && !Ext.isIE8 + ? o.getAttributeNode('class').specified + : o.hasAttribute('class')) ? o.className : '', style = (Ext.isIE + && !Ext.isIE8 + ? o.getAttributeNode('style').specified + : o.hasAttribute('style')) ? o.style : ''; + if (o.selected) { + transformValues.push(value); + } + d.push([value, o.text, cls, style.cssText]); + } + this.store = new Ext.data.SimpleStore({ + 'id' : 0, + fields : ['value', 'text', 'cls', 'style'], + data : d + }); + Ext.apply(this, { + valueField : 'value', + displayField : 'text', + classField : 'cls', + styleField : 'style' + }); + } + + if (transformValues.length) { + this.value = transformValues.join(','); + } + }, + setupFieldButtons : function() { + this.buttonWrap = this.outerWrapEl.createChild({ + cls : 'x-superboxselect-btns' + }); + + this.buttonClear = this.buttonWrap.createChild({ + tag : 'div', + cls : 'x-superboxselect-btn-clear ' + + this.clearBtnCls + }); + + this.buttonExpand = this.buttonWrap.createChild({ + tag : 'div', + cls : 'x-superboxselect-btn-expand ' + + this.expandBtnCls + }); + + this.initButtonEvents(); + + return this; + }, + initButtonEvents : function() { + this.buttonClear.addClassOnOver('x-superboxselect-btn-over') + .on('click', function(e) { + e.stopEvent(); + if (this.disabled) { + return; + } + this.clearValue(); + this.el.focus(); + }, this); + + this.buttonExpand.addClassOnOver('x-superboxselect-btn-over') + .on('click', function(e) { + e.stopEvent(); + if (this.disabled) { + return; + } + if (this.isExpanded()) { + this.multiSelectMode = false; + } else if (this.pinList) { + this.multiSelectMode = true; + } + this.onTriggerClick(); + }, this); + }, + removeButtonEvents : function() { + this.buttonClear.removeAllListeners(); + this.buttonExpand.removeAllListeners(); + return this; + }, + clearCurrentFocus : function() { + if (this.currentFocus) { + this.currentFocus.onLnkBlur(); + this.currentFocus = null; + } + return this; + }, + initEvents : function() { + var el = this.el; + + el.on({ + click : this.onClick, + focus : this.clearCurrentFocus, + blur : this.onBlur, + + keydown : this.onKeyDownHandler, + keyup : this.onKeyUpBuffered, + + scope : this + }); + + this.on({ + collapse : this.onCollapse, + expand : this.clearCurrentFocus, + scope : this + }); + + this.wrapEl.on('click', this.onWrapClick, this); + this.outerWrapEl.on('click', this.onWrapClick, this); + + this.inputEl.focus = function() { + el.focus(); + }; + + Ext.ux.form.SuperBoxSelect.superclass.initEvents.call(this); + + Ext.apply(this.keyNav, { + tab : function(e) { + if (this.fixFocusOnTabSelect + && this.isExpanded()) { + e.stopEvent(); + el.blur(); + this.onViewClick(false); + this.focus(false, 10); + return true; + } + + this.onViewClick(false); + if (el.dom.value !== '') { + this.setRawValue(''); + } + + return true; + }, + + down : function(e) { + if (!this.isExpanded() && !this.currentFocus) { + this.onTriggerClick(); + } else { + this.inKeyMode = true; + this.selectNext(); + } + }, + + enter : function() { + } + }); + }, + + onClick : function() { + this.clearCurrentFocus(); + this.collapse(); + this.autoSize(); + }, + + beforeBlur : Ext.form.ComboBox.superclass.beforeBlur, + + onFocus : function() { + this.outerWrapEl.addClass(this.focusClass); + + Ext.ux.form.SuperBoxSelect.superclass.onFocus.call(this); + }, + + onBlur : function() { + this.outerWrapEl.removeClass(this.focusClass); + + this.clearCurrentFocus(); + + if (this.el.dom.value !== '') { + this.applyEmptyText(); + this.autoSize(); + } + + Ext.ux.form.SuperBoxSelect.superclass.onBlur.call(this); + }, + + onCollapse : function() { + this.view.clearSelections(); + this.multiSelectMode = false; + }, + + onWrapClick : function(e) { + e.stopEvent(); + this.collapse(); + this.el.focus(); + this.clearCurrentFocus(); + }, + markInvalid : function(msg) { + var elp, t; + return; + if (!this.rendered || this.preventMark) { + return; + } + this.outerWrapEl.addClass(this.invalidClass); + msg = msg || this.invalidText; + + switch (this.msgTarget) { + case 'qtip' : + Ext.apply(this.el.dom, { + qtip : msg, + qclass : 'x-form-invalid-tip' + }); + Ext.apply(this.wrapEl.dom, { + qtip : msg, + qclass : 'x-form-invalid-tip' + }); + if (Ext.QuickTips) { // fix for floating editors + // interacting with DND + Ext.QuickTips.enable(); + } + break; + case 'title' : + this.el.dom.title = msg; + this.wrapEl.dom.title = msg; + this.outerWrapEl.dom.title = msg; + break; + case 'under' : + if (!this.errorEl) { + elp = this.getErrorCt(); + if (!elp) { // field has no container el + this.el.dom.title = msg; + break; + } + this.errorEl = elp.createChild({ + cls : 'x-form-invalid-msg' + }); + this.errorEl.setWidth(elp.getWidth(true) - 20); + } + this.errorEl.update(msg); + Ext.form.Field.msgFx[this.msgFx].show(this.errorEl, + this); + break; + case 'side' : + if (!this.errorIcon) { + elp = this.getErrorCt(); + if (!elp) { // field has no container el + this.el.dom.title = msg; + break; + } + this.errorIcon = elp.createChild({ + cls : 'x-form-invalid-icon' + }); + } + this.alignErrorIcon(); + Ext.apply(this.errorIcon.dom, { + qtip : msg, + qclass : 'x-form-invalid-tip' + }); + this.errorIcon.show(); + this.on('resize', this.alignErrorIcon, this); + break; + default : + t = Ext.getDom(this.msgTarget); + t.innerHTML = msg; + t.style.display = this.msgDisplay; + break; + } + this.fireEvent('invalid', this, msg); + }, + clearInvalid : function() { + if (!this.rendered || this.preventMark) { // not + // rendered + return; + } + this.outerWrapEl.removeClass(this.invalidClass); + switch (this.msgTarget) { + case 'qtip' : + this.el.dom.qtip = ''; + this.wrapEl.dom.qtip = ''; + break; + case 'title' : + this.el.dom.title = ''; + this.wrapEl.dom.title = ''; + this.outerWrapEl.dom.title = ''; + break; + case 'under' : + if (this.errorEl) { + Ext.form.Field.msgFx[this.msgFx].hide(this.errorEl, + this); + } + break; + case 'side' : + if (this.errorIcon) { + this.errorIcon.dom.qtip = ''; + this.errorIcon.hide(); + this.un('resize', this.alignErrorIcon, this); + } + break; + default : + var t = Ext.getDom(this.msgTarget); + t.innerHTML = ''; + t.style.display = 'none'; + break; + } + this.fireEvent('valid', this); + }, + alignErrorIcon : function() { + if (this.wrap) { + this.errorIcon.alignTo(this.wrap, 'tl-tr', [ + Ext.isIE ? 5 : 2, 3]); + } + }, + expand : function() { + if (this.isExpanded() || !this.hasFocus) { + return; + } + this.list.alignTo(this.outerWrapEl, this.listAlign).show(); + this.innerList.setOverflow('auto'); // necessary for FF + // 2.0/Mac + Ext.getDoc().on({ + mousewheel : this.collapseIf, + mousedown : this.collapseIf, + scope : this + }); + this.fireEvent('expand', this); + }, + restrictHeight : function() { + var inner = this.innerList.dom, st = inner.scrollTop, list = this.list; + + inner.style.height = ''; + + var pad = list.getFrameWidth('tb') + + (this.resizable ? this.handleHeight : 0) + + this.assetHeight, h = Math.max(inner.clientHeight, + inner.offsetHeight, inner.scrollHeight), ha = this + .getPosition()[1] + - Ext.getBody().getScroll().top, hb = Ext.lib.Dom + .getViewHeight() + - ha - this.getSize().height, space = Math.max(ha, hb, + this.minHeight || 0) + - list.shadowOffset - pad - 5; + + h = Math.min(h, space, this.maxHeight); + this.innerList.setHeight(h); + + list.beginUpdate(); + list.setHeight(h + pad); + list.alignTo(this.outerWrapEl, this.listAlign); + list.endUpdate(); + + if (this.multiSelectMode) { + inner.scrollTop = st; + } + }, + validateValue : function(val) { + if (this.items.getCount() === 0) { + if (this.allowBlank) { + this.clearInvalid(); + return true; + } else { + this.markInvalid(this.blankText); + return false; + } + } else { + this.clearInvalid(); + return true; + } + }, + setupFormInterception : function() { + var form; + this.findParentBy(function(p) { + if (p.getForm) { + form = p.getForm(); + } + }); + if (form) { + var formGet = form.getValues; + form.getValues = function(asString) { + if (this.items.getCount() > 0) { + this.el.dom.disabled = true; + } + var oldVal = this.el.dom.value; + this.setRawValue(''); + var vals = formGet.call(form, asString); + this.el.dom.disabled = false; + this.setRawValue(oldVal); + return vals; + }.createDelegate(this); + } + }, + onResize : function(w, h, rw, rh) { + var reduce = Ext.isIE6 ? 4 : Ext.isIE7 ? 1 : Ext.isIE8 ? 1 : 0; + + this._width = w; + this.outerWrapEl.setWidth(w - reduce); + if (this.renderFieldBtns) { + reduce += (this.buttonWrap.getWidth() + 20); + this.wrapEl.setWidth(w - reduce); + } + Ext.ux.form.SuperBoxSelect.superclass.onResize.call(this, w, h, + rw, rh); + this.autoSize(); + }, + onEnable : function() { + Ext.ux.form.SuperBoxSelect.superclass.onEnable.call(this); + this.items.each(function(item) { + item.enable(); + }); + if (this.renderFieldBtns) { + this.initButtonEvents(); + } + }, + onDisable : function() { + Ext.ux.form.SuperBoxSelect.superclass.onDisable.call(this); + this.items.each(function(item) { + item.disable(); + }); + if (this.renderFieldBtns) { + this.removeButtonEvents(); + } + }, + /** + * Clears all values from the component. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name clearValue + * @param {Boolean} + * supressRemoveEvent [Optional] When true, the + * 'removeitem' event will not fire for each item that is + * removed. + */ + clearValue : function(supressRemoveEvent) { + Ext.ux.form.SuperBoxSelect.superclass.clearValue.call(this); + this.preventMultipleRemoveEvents = supressRemoveEvent + || this.supressClearValueRemoveEvents || false; + this.removeAllItems(); + this.fireEvent('clear', this); + return this; + }, + onKeyUp : function(e) { + if (this.editable !== false && !e.isSpecialKey() + && (!e.hasModifier() || e.shiftKey)) { + this.lastKey = e.getKey(); + this.dqTask.delay(this.queryDelay); + } + }, + onKeyDownHandler : function(e, t) { + + var toDestroy, nextFocus, idx; + if ((e.getKey() === e.DELETE || e.getKey() === e.SPACE) + && this.currentFocus) { + e.stopEvent(); + toDestroy = this.currentFocus; + this.on('expand', function() { + this.collapse(); + }, this, { + single : true + }); + idx = this.items.indexOfKey(this.currentFocus.key); + + this.clearCurrentFocus(); + + if (idx < (this.items.getCount() - 1)) { + nextFocus = this.items.itemAt(idx + 1); + } + + toDestroy.preDestroy(true); + if (nextFocus) { + (function() { + nextFocus.onLnkFocus(); + this.currentFocus = nextFocus; + }).defer(200, this); + } + + return true; + } + + var val = this.el.dom.value, it, ctrl = e.ctrlKey; + if (e.getKey() === e.ENTER) { + e.stopEvent(); + if (val !== "") { + if (ctrl || !this.isExpanded()) { // ctrl+enter + // for new + // items + this.view.clearSelections(); + this.collapse(); + this.setRawValue(''); + this.fireEvent('newitem', this, val); + } else { + this.onViewClick(); + // removed from 3.0.1 + if (this.unsetDelayCheck) { + this.delayedCheck = true; + this.unsetDelayCheck.defer(10, this); + } + } + } else { + if (!this.isExpanded()) { + return; + } + this.onViewClick(); + // removed from 3.0.1 + if (this.unsetDelayCheck) { + this.delayedCheck = true; + this.unsetDelayCheck.defer(10, this); + } + } + return true; + } + + if (val !== '') { + this.autoSize(); + return; + } + + // select first item + if (e.getKey() === e.HOME) { + e.stopEvent(); + if (this.items.getCount() > 0) { + this.collapse(); + it = this.items.get(0); + it.el.focus(); + + } + return true; + } + // backspace remove + if (e.getKey() === e.BACKSPACE) { + e.stopEvent(); + if (this.currentFocus) { + toDestroy = this.currentFocus; + this.on('expand', function() { + this.collapse(); + }, this, { + single : true + }); + + idx = this.items.indexOfKey(toDestroy.key); + + this.clearCurrentFocus(); + if (idx < (this.items.getCount() - 1)) { + nextFocus = this.items.itemAt(idx + 1); + } + + toDestroy.preDestroy(true); + + if (nextFocus) { + (function() { + nextFocus.onLnkFocus(); + this.currentFocus = nextFocus; + }).defer(200, this); + } + + return; + } else { + it = this.items.get(this.items.getCount() - 1); + if (it) { + if (this.backspaceDeletesLastItem) { + this.on('expand', function() { + this.collapse(); + }, this, { + single : true + }); + it.preDestroy(true); + } else { + if (this.navigateItemsWithTab) { + it.onElClick(); + } else { + this.on('expand', function() { + this.collapse(); + this.currentFocus = it; + this.currentFocus.onLnkFocus + .defer( + 20, + this.currentFocus); + }, this, { + single : true + }); + } + } + } + return true; + } + } + + if (!e.isNavKeyPress()) { + this.multiSelectMode = false; + this.clearCurrentFocus(); + return; + } + // arrow nav + if (e.getKey() === e.LEFT + || (e.getKey() === e.UP && !this.isExpanded())) { + e.stopEvent(); + this.collapse(); + // get last item + it = this.items.get(this.items.getCount() - 1); + if (this.navigateItemsWithTab) { + // focus last el + if (it) { + it.focus(); + } + } else { + // focus prev item + if (this.currentFocus) { + idx = this.items.indexOfKey(this.currentFocus.key); + this.clearCurrentFocus(); + + if (idx !== 0) { + this.currentFocus = this.items.itemAt(idx - 1); + this.currentFocus.onLnkFocus(); + } + } else { + this.currentFocus = it; + if (it) { + it.onLnkFocus(); + } + } + } + return true; + } + if (e.getKey() === e.DOWN) { + if (this.currentFocus) { + this.collapse(); + e.stopEvent(); + idx = this.items.indexOfKey(this.currentFocus.key); + if (idx == (this.items.getCount() - 1)) { + this.clearCurrentFocus.defer(10, this); + } else { + this.clearCurrentFocus(); + this.currentFocus = this.items.itemAt(idx + 1); + if (this.currentFocus) { + this.currentFocus.onLnkFocus(); + } + } + return true; + } + } + if (e.getKey() === e.RIGHT) { + this.collapse(); + it = this.items.itemAt(0); + if (this.navigateItemsWithTab) { + // focus first el + if (it) { + it.focus(); + } + } else { + if (this.currentFocus) { + idx = this.items.indexOfKey(this.currentFocus.key); + this.clearCurrentFocus(); + if (idx < (this.items.getCount() - 1)) { + this.currentFocus = this.items.itemAt(idx + 1); + if (this.currentFocus) { + this.currentFocus.onLnkFocus(); + } + } + } else { + this.currentFocus = it; + if (it) { + it.onLnkFocus(); + } + } + } + } + }, + onKeyUpBuffered : function(e) { + if (!e.isNavKeyPress()) { + this.autoSize(); + } + }, + reset : function() { + Ext.ux.form.SuperBoxSelect.superclass.reset.call(this); + this.addedRecords = []; + this.autoSize().setRawValue(''); + this.el.focus(); + }, + applyEmptyText : function() { + if (this.items.getCount() > 0) { + this.el.removeClass(this.emptyClass); + this.setRawValue(''); + return this; + } + if (this.rendered && this.emptyText + && this.getRawValue().length < 1) { + this.setRawValue(this.emptyText); + this.el.addClass(this.emptyClass); + } + return this; + }, + /** + * @private + * + * Use clearValue instead + */ + removeAllItems : function() { + this.items.each(function(item) { + item.preDestroy(true); + }, this); + this.manageClearBtn(); + return this; + }, + resetStore : function() { + this.store.clearFilter(); + if (!this.removeValuesFromStore) { + return this; + } + this.usedRecords.each(function(rec) { + this.store.add(rec); + }, this); + this.sortStore(); + return this; + }, + sortStore : function() { + var ss = this.store.getSortState(); + if (ss && ss.field) { + this.store.sort(ss.field, ss.direction); + } + return this; + }, + getCaption : function(dataObject) { + if (typeof this.displayFieldTpl === 'string') { + this.displayFieldTpl = new Ext.XTemplate(this.displayFieldTpl); + } + var caption, recordData = dataObject instanceof Ext.data.Record + ? dataObject.data + : dataObject; + + if (this.displayFieldTpl) { + caption = this.displayFieldTpl.apply(recordData); + } else if (this.displayField) { + caption = recordData[this.displayField]; + } + + return caption; + }, + addRecord : function(record) { + var display = record.data[this.displayField], caption = this + .getCaption(record), val = record.data[this.valueField], cls = this.classField + ? record.data[this.classField] + : '', style = this.styleField + ? record.data[this.styleField] + : ''; + + if (this.removeValuesFromStore) { + this.usedRecords.add(val, record); + this.store.remove(record); + } + + this.addItemBox(val, display, caption, cls, style); + this.fireEvent('additem', this, val, record); + }, + createRecord : function(recordData) { + if (!this.recordConstructor) { + var recordFields = [{ + name : this.valueField + }, { + name : this.displayField + }]; + if (this.classField) { + recordFields.push({ + name : this.classField + }); + } + if (this.styleField) { + recordFields.push({ + name : this.styleField + }); + } + this.recordConstructor = Ext.data.Record + .create(recordFields); + } + return new this.recordConstructor(recordData); + }, + /** + * Adds an array of items to the SuperBoxSelect component if the + * {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set + * to true. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name addItem + * @param {Array} + * newItemObjects An Array of object literals containing + * the property names and values for an item. The + * property names must match those specified in + * {@link #Ext.ux.form.SuperBoxSelect-displayField}, + * {@link #Ext.ux.form.SuperBoxSelect-valueField} and + * {@link #Ext.ux.form.SuperBoxSelect-classField} + */ + addItems : function(newItemObjects) { + if (Ext.isArray(newItemObjects)) { + Ext.each(newItemObjects, function(item) { + this.addItem(item); + }, this); + } else { + this.addItem(newItemObjects); + } + }, + /** + * Adds an item to the SuperBoxSelect component if the + * {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set + * to true. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name addItem + * @param {Object} + * newItemObject An object literal containing the + * property names and values for an item. The property + * names must match those specified in + * {@link #Ext.ux.form.SuperBoxSelect-displayField}, + * {@link #Ext.ux.form.SuperBoxSelect-valueField} and + * {@link #Ext.ux.form.SuperBoxSelect-classField} + */ + addItem : function(newItemObject) { + + var val = newItemObject[this.valueField]; + + if (this.disabled) { + return false; + } + if (this.preventDuplicates && this.hasValue(val)) { + return; + } + + // use existing record if found + var record = this.findRecord(this.valueField, val); + if (record) { + this.addRecord(record); + return; + } else if (!this.allowAddNewData) { // else it's a new + // item + return; + } + + if (this.mode === 'remote') { + this.remoteLookup.push(newItemObject); + this.doQuery(val, false, false); + return; + } + + var rec = this.createRecord(newItemObject); + this.store.add(rec); + this.addRecord(rec); + + return true; + }, + addItemBox : function(itemVal, itemDisplay, itemCaption, itemClass, + itemStyle) { + var parseStyle = function(s) { + var ret = ''; + if (typeof s == 'function') { + ret = s.call(); + } else if (typeof s == 'object') { + for (var p in s) { + ret += p + ':' + s[p] + ';'; + } + } else if (typeof s == 'string') { + ret = s + ';'; + } + return ret; + }; + var itemKey = Ext.id(null, 'sbx-item'); + var box = new Ext.ux.form.SuperBoxSelectItem({ + owner : this, + disabled : this.disabled, + renderTo : this.wrapEl, + cls : this.extraItemCls + ' ' + itemClass, + style : parseStyle(this.extraItemStyle) + ' ' + + itemStyle, + caption : itemCaption, + display : itemDisplay, + value : itemVal, + key : itemKey, + listeners : { + 'remove' : function(item) { + if (this.fireEvent('beforeremoveitem', + this, item.value) === false) { + return; + } + this.items.removeKey(item.key); + if (this.removeValuesFromStore) { + if (this.usedRecords + .containsKey(item.value)) { + this.store.add(this.usedRecords + .get(item.value)); + this.usedRecords + .removeKey(item.value); + this.sortStore(); + if (this.view) { + this.view.render(); + } + } + } + if (!this.preventMultipleRemoveEvents) { + this.fireEvent + .defer( + 250, + this, + [ + 'removeitem', + this, + item.value, + this + .findInStore(item.value)]); + } + this.preventMultipleRemoveEvents = false; + }, + destroy : function() { + this.collapse(); + this.autoSize().manageClearBtn() + .validateValue(); + }, + scope : this + } + }); + box.render(); + + box.hidden = this.el.insertSibling({ + tag : 'input', + type : 'hidden', + value : itemVal, + name : (this.hiddenName || this.name) + }, 'before'); + + this.items.add(itemKey, box); + this.applyEmptyText().autoSize().manageClearBtn() + .validateValue(); + }, + manageClearBtn : function() { + if (!this.renderFieldBtns || !this.rendered) { + return this; + } + var cls = 'x-superboxselect-btn-hide'; + if (this.items.getCount() === 0) { + this.buttonClear.addClass(cls); + } else { + this.buttonClear.removeClass(cls); + } + return this; + }, + findInStore : function(val) { + var index = this.store.find(this.valueField, val); + if (index > -1) { + return this.store.getAt(index); + } + return false; + }, + /** + * Returns a String value containing a concatenated list of item + * values. The list is concatenated with the + * {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter}. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name getValue + * @return {String} a String value containing a concatenated list of + * item values. + */ + getValue : function() { + var ret = []; + this.items.each(function(item) { + ret.push(item.value); + }); + return ret.join(this.valueDelimiter); + }, + /** + * Iterates and selects all elements available Must take into + * consideration the ones that are already selected plus the ones + * from the store. + */ + selectAll : function() { + var ret = this.getValuesAsArray(); + var valueField = this.valueField; + Ext.iterate(this.store.data.items, function(item) { + ret.push(item.get(valueField)); + }); + if (!Ext.isEmpty(ret)) { + this.setValue(ret.join(this.valueDelimiter)); + } + }, + selectItems : function(array) { + var ret = []; + Ext.iterate(array, function(item) { + ret.push(item) + }); + if (!Ext.isEmpty(ret)) { + this.setValue(ret.join(this.valueDelimiter)); + } + }, + /** + * Returns an Array containing the elements of the + * {@link #Ext.ux.form.SuperBoxSelect-valueField} + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name getValueAsArray + * @return {Array} an array of item objects. + */ + getValuesAsArray : function() { + var ret = []; + this.items.each(function(item) { + ret.push(item.value); + }); + return ret; + }, + /** + * Returns an Array of item objects containing the + * {@link #Ext.ux.form.SuperBoxSelect-displayField}, + * {@link #Ext.ux.form.SuperBoxSelect-valueField} and + * {@link #Ext.ux.form.SuperBoxSelect-classField} properties. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name getValueEx + * @return {Array} an array of item objects. + */ + getValueEx : function() { + var ret = []; + this.items.each(function(item) { + var newItem = {}; + newItem[this.valueField] = item.value; + newItem[this.displayField] = item.display; + newItem[this.classField] = item.cls; + ret.push(newItem); + }, this); + return ret; + }, + // private + initValue : function() { + + Ext.ux.form.SuperBoxSelect.superclass.initValue.call(this); + if (this.mode === 'remote') { + this.setOriginal = true; + } + }, + /** + * Sets the value of the SuperBoxSelect component. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name setValue + * @param {String|Array} + * value An array of item values, or a String value + * containing a delimited list of item values. (The list + * should be delimited with the + * {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter) + */ + setValue : function(value) { + if (!this.rendered) { + this.preRenderValue = value; + return; + } + + var values = Ext.isArray(value) ? value : value + .split(this.valueDelimiter); + this.removeAllItems().resetStore(); + + // reset remoteLookup because setValue should overwrite + // everything + // inc pending data + this.remoteLookup = []; + + Ext.each(values, function(val) { + var record = this.findRecord(this.valueField, val); + if (record) { + this.addRecord(record); + } else if (this.mode === 'remote') { + this.remoteLookup.push(val); + } + }, this); + + if (this.mode === 'remote') { + var q = this.remoteLookup.join(this.queryValuesDelimiter); + this.doQuery(q, false, true); // 3rd param to + // specify a values + // query + } + + }, + /** + * Sets the value of the SuperBoxSelect component, adding new items + * that don't exist in the data store if the + * {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set + * to true. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name setValue + * @param {Array} + * data An Array of item objects containing the + * {@link #Ext.ux.form.SuperBoxSelect-displayField}, + * {@link #Ext.ux.form.SuperBoxSelect-valueField} and + * {@link #Ext.ux.form.SuperBoxSelect-classField} + * properties. + */ + setValueEx : function(data) { + this.removeAllItems().resetStore(); + + if (!Ext.isArray(data)) { + data = [data]; + } + Ext.each(data, function(item) { + this.addItem(item); + }, this); + }, + /** + * Returns true if the SuperBoxSelect component has a selected item + * with a value matching the 'val' parameter. + * + * @methodOf Ext.ux.form.SuperBoxSelect + * @name hasValue + * @param {Mixed} + * val The value to test. + * @return {Boolean} true if the component has the selected value, + * false otherwise. + */ + hasValue : function(val) { + var has = false; + this.items.each(function(item) { + if (item.value == val) { + has = true; + return false; + } + }, this); + return has; + }, + onSelect : function(record, index) { + var val = record.data[this.valueField]; + + if (this.preventDuplicates && this.hasValue(val)) { + return; + } + + this.setRawValue(''); + this.lastSelectionText = ''; + + if (this.fireEvent('beforeadditem', this, val) !== false) { + this.addRecord(record); + } + if (this.store.getCount() === 0 || !this.multiSelectMode) { + this.collapse(); + } else { + this.restrictHeight(); + } + }, + onDestroy : function() { + this.items.each(function(item) { + item.preDestroy(true); + }, this); + + if (this.renderFieldBtns) { + Ext.destroy(this.buttonClear, this.buttonExpand, + this.buttonWrap); + } + + Ext.destroy(this.inputEl, this.wrapEl, this.outerWrapEl); + + Ext.ux.form.SuperBoxSelect.superclass.onDestroy.call(this); + }, + autoSize : function() { + if (!this.rendered) { + return this; + } + if (!this.metrics) { + this.metrics = Ext.util.TextMetrics.createInstance(this.el); + } + var el = this.el, v = el.dom.value, d = document + .createElement('div'); + + if (v === "" && this.emptyText && this.items.getCount() < 1) { + v = this.emptyText; + } + d.appendChild(document.createTextNode(v)); + v = d.innerHTML; + d = null; + v += " "; + var w = Math.max(this.metrics.getWidth(v) + 24, 24); + if (typeof this._width != 'undefined') { + w = Math.min(this._width, w); + } + this.el.setWidth(w); + + if (Ext.isIE) { + this.el.dom.style.top = '0'; + } + return this; + }, + doQuery : function(q, forceAll, valuesQuery) { + q = Ext.isEmpty(q) ? '' : q; + var qe = { + query : q, + forceAll : forceAll, + combo : this, + cancel : false + }; + if (this.fireEvent('beforequery', qe) === false || qe.cancel) { + return false; + } + q = qe.query; + forceAll = qe.forceAll; + if (forceAll === true || (q.length >= this.minChars)) { + if (this.lastQuery !== q) { + this.lastQuery = q; + if (this.mode == 'local') { + this.selectedIndex = -1; + if (forceAll) { + this.store.clearFilter(); + } else { + this.store.filter(this.displayField, q); + } + this.onLoad(); + } else { + + this.store.baseParams[this.queryParam] = q; + this.store.baseParams[this.queryValuesInidicator] = valuesQuery; + this.store.load({ + params : this.getParams(q) + }); + this.expand(); + } + } else { + this.selectedIndex = -1; + this.onLoad(); + } + } + } + }); +Ext.reg('superboxselect', Ext.ux.form.SuperBoxSelect); +/* + * @private + */ +Ext.ux.form.SuperBoxSelectItem = function(config) { + Ext.apply(this, config); + Ext.ux.form.SuperBoxSelectItem.superclass.constructor.call(this); +}; +/* + * @private + */ +Ext.ux.form.SuperBoxSelectItem = Ext.extend(Ext.ux.form.SuperBoxSelectItem, + Ext.Component, { + initComponent : function() { + Ext.ux.form.SuperBoxSelectItem.superclass.initComponent + .call(this); + }, + onElClick : function(e) { + var o = this.owner; + o.clearCurrentFocus().collapse(); + if (o.navigateItemsWithTab) { + this.focus(); + } else { + o.el.dom.focus(); + var that = this; + (function() { + this.onLnkFocus(); + o.currentFocus = this; + }).defer(10, this); + } + }, + + onLnkClick : function(e) { + if (e) { + e.stopEvent(); + } + this.preDestroy(); + if (!this.owner.navigateItemsWithTab) { + this.owner.el.focus(); + } + }, + onLnkFocus : function() { + this.el.addClass("x-superboxselect-item-focus"); + this.owner.outerWrapEl.addClass("x-form-focus"); + }, + + onLnkBlur : function() { + this.el.removeClass("x-superboxselect-item-focus"); + this.owner.outerWrapEl.removeClass("x-form-focus"); + }, + + enableElListeners : function() { + this.el.on('click', this.onElClick, this, { + stopEvent : true + }); + + this.el.addClassOnOver('x-superboxselect-item-hover'); + }, + + enableLnkListeners : function() { + this.lnk.on({ + click : this.onLnkClick, + focus : this.onLnkFocus, + blur : this.onLnkBlur, + scope : this + }); + }, + + enableAllListeners : function() { + this.enableElListeners(); + this.enableLnkListeners(); + }, + disableAllListeners : function() { + this.el.removeAllListeners(); + this.lnk.un('click', this.onLnkClick, this); + this.lnk.un('focus', this.onLnkFocus, this); + this.lnk.un('blur', this.onLnkBlur, this); + }, + onRender : function(ct, position) { + + Ext.ux.form.SuperBoxSelectItem.superclass.onRender.call(this, + ct, position); + + var el = this.el; + if (el) { + el.remove(); + } + + this.el = el = ct.createChild({ + tag : 'li' + }, ct.last()); + el.addClass('x-superboxselect-item'); + + var btnEl = this.owner.navigateItemsWithTab ? (Ext.isSafari + ? 'button' + : 'a') : 'span'; + var itemKey = this.key; + + Ext.apply(el, { + focus : function() { + var c = this.down(btnEl + + '.x-superboxselect-item-close'); + if (c) { + c.focus(); + } + }, + preDestroy : function() { + this.preDestroy(); + }.createDelegate(this) + }); + + this.enableElListeners(); + + el.update(this.caption); + + var cfg = { + tag : btnEl, + 'class' : 'x-superboxselect-item-close', + tabIndex : this.owner.navigateItemsWithTab ? '0' : '-1' + }; + if (btnEl === 'a') { + cfg.href = '#'; + } + this.lnk = el.createChild(cfg); + + if (!this.disabled) { + this.enableLnkListeners(); + } else { + this.disableAllListeners(); + } + + this.on({ + disable : this.disableAllListeners, + enable : this.enableAllListeners, + scope : this + }); + + this.setupKeyMap(); + }, + setupKeyMap : function() { + new Ext.KeyMap(this.lnk, [{ + key : [Ext.EventObject.BACKSPACE, + Ext.EventObject.DELETE, + Ext.EventObject.SPACE], + fn : this.preDestroy, + scope : this + }, { + key : [Ext.EventObject.RIGHT, Ext.EventObject.DOWN], + fn : function() { + this.moveFocus('right'); + }, + scope : this + }, { + key : [Ext.EventObject.LEFT, Ext.EventObject.UP], + fn : function() { + this.moveFocus('left'); + }, + scope : this + }, { + key : [Ext.EventObject.HOME], + fn : function() { + var l = this.owner.items.get(0).el.focus(); + if (l) { + l.el.focus(); + } + }, + scope : this + }, { + key : [Ext.EventObject.END], + fn : function() { + this.owner.el.focus(); + }, + scope : this + }, { + key : Ext.EventObject.ENTER, + fn : function() { + } + }]).stopEvent = true; + }, + moveFocus : function(dir) { + var el = this.el[dir == 'left' ? 'prev' : 'next']() + || this.owner.el; + + el.focus.defer(100, el); + }, + + preDestroy : function(supressEffect) { + if (this.fireEvent('remove', this) === false) { + return; + } + var actionDestroy = function() { + if (this.owner.navigateItemsWithTab) { + this.moveFocus('right'); + } + this.hidden.remove(); + this.hidden = null; + this.destroy(); + }; + + if (supressEffect) { + actionDestroy.call(this); + } else { + this.el.hide({ + duration : 0.2, + callback : actionDestroy, + scope : this + }); + } + return this; + }, + onDestroy : function() { + Ext.destroy(this.lnk, this.el); + + Ext.ux.form.SuperBoxSelectItem.superclass.onDestroy.call(this); + } + }); diff --git a/src/web/OpenCyto/ux/BoxSelect/BoxSelect.css b/src/web/OpenCyto/ux/BoxSelect/BoxSelect.css new file mode 100644 index 0000000..fc18d8b --- /dev/null +++ b/src/web/OpenCyto/ux/BoxSelect/BoxSelect.css @@ -0,0 +1,82 @@ +/* +.x4-boundlist-item img.chkCombo { + background: transparent url(../../..ext4.1.0/resources/themes/images/default/menu/unchecked.gif); +} +.x4-boundlist-selected img.chkCombo{ + background: transparent url(../../../ext-4.1.0/resources/themes/images/default/menu/checked.gif); +} + */ +/* Above two are dude's suggestion at Stack Overflow for showing checkboxes. Doesn't seem to work: */ +.x-boxselect.x4-form-text.x4-form-field { + height: auto; + cursor: text; + padding: 0; + overflow-y: auto; +} +.x-boxselect ul.x-boxselect-list { + padding: 1px 3px; +} +ul.x-boxselect-list.x-boxselect-singleselect { + white-space: nowrap; + overflow: hidden; +} +.x-boxselect-input, .x-boxselect-item { + display: inline-block; + position: relative; + *display:inline; /* IE7 */ + zoom:1; /* IE */ +} +.x-boxselect-input input, .x-boxselect-input div { + border: none; + background: none; + line-height: 18px !important; + height: 20px; + width: 100%; +} +.x-boxselect-emptyinput { + display: none; +} +.x-boxselect-stacked .x-boxselect-item { + display: block; +} +.x-boxselect-item { + -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; o-border-radius: 6px; khtml-border-radius: 6px; border: 1px solid #CAD8F3; + padding: 0px 1px 0px 5px !important; + margin: 1px 1px 1px 0 !important; + /*background-color: #DEE7F8;*/ + /*background-color: transparent;*/ + cursor: default; +} + +.x4-field:not(.x4-item-disabled) .x-boxselect-item:hover { + background: #BBCEF1; + border: 1px solid #6D95E0; +} +.x4-field:not(.x4-item-disabled) .x-boxselect-item.selected { + border: 1px solid #316AD3 !important; + background: #658FDC !important; +} + +.x-boxselect-item-text { + line-height: 16px; + padding-right: 20px !important; +} +.x-boxselect-item-close { + cursor: pointer; +} +.x-boxselect-hideselections .x-boundlist-selected { + display: none; +} +/* Why did I have to bring this in my ext-all.css? */ +.x-tab-close-btn { + background: url("../../../ext-4.1.0/resources/themes/images/default/tab/tab-default-close.gif") no-repeat scroll 0 0 transparent; + font-size: 0; + height: 11px; + line-height: 0; + opacity: 0.6; + position: absolute; + right: 2px; + text-indent: -999px; + top: 2px; + width: 11px; +} \ No newline at end of file diff --git a/src/web/OpenCyto/ux/BoxSelect/BoxSelect.js b/src/web/OpenCyto/ux/BoxSelect/BoxSelect.js new file mode 100644 index 0000000..f2d3f58 --- /dev/null +++ b/src/web/OpenCyto/ux/BoxSelect/BoxSelect.js @@ -0,0 +1,1662 @@ +/** + * BoxSelect for ExtJS 4.1, a combo box improved for multiple value querying, selection and management. + * + * A friendlier combo box for multiple selections that creates easily individually + * removable labels for each selection, as seen on facebook and other sites. Querying + * and type-ahead support are also improved for multiple selections. + * + * Options and usage mostly remain consistent with the standard + * [ComboBox](http://docs.sencha.com/ext-js/4-1/#!/api/Ext.form.field.ComboBox) control. + * Some default configuration options have changed, but most should still work properly + * if overridden unless otherwise noted. + * + * Please note, this component does not support versions of ExtJS earlier than 4.1. + * + * Inspired by the [SuperBoxSelect component for ExtJS 3](http://technomedia.co.uk/SuperBoxSelect/examples3.html), + * which in turn was inspired by the [BoxSelect component for ExtJS 2](http://efattal.fr/en/extjs/extuxboxselect/). + * + * Various contributions and suggestions made by many members of the ExtJS community which can be seen + * in the [official user extension forum post](http://www.sencha.com/forum/showthread.php?134751-Ext.ux.form.field.BoxSelect). + * + * Many thanks go out to all of those who have contributed, this extension would not be + * possible without your help. + * + * See [AUTHORS.txt](../AUTHORS.TXT) for a list of major contributors + * + * @author kvee_iv http://www.sencha.com/forum/member.php?29437-kveeiv + * @version 2.0.3 + * @requires BoxSelect.css + * @xtype boxselect + * + */ +Ext4.define('Ext4.ux.form.field.BoxSelect203', { + extend:'Ext.form.field.ComboBox', + alias: ['widget.comboboxselect203', 'widget.boxselect203'], + requires: ['Ext.selection.Model', 'Ext.data.Store'], + + // + // Begin configuration options related to the underlying store + // + + /** + * @cfg {String} valueParam + * The name of the parameter used to load unknown records into the store. If left unspecified, {@link #valueField} + * will be used. + */ + + // + // End of configuration options related to the underlying store + // + + + + // + // Begin configuration options related to selected values + // + + /** + * @cfg {Boolean} + * If set to `true`, allows the combo field to hold more than one value at a time, and allows selecting multiple + * items from the dropdown list. The combo's text field will show all selected values using the template + * defined by {@link #labelTpl}. + * + + */ + multiSelect: true, + + /** + * @cfg {String/Ext.XTemplate} labelTpl + * The [XTemplate](http://docs.sencha.com/ext-js/4-1/#!/api/Ext.XTemplate) to use for the inner + * markup of the labelled items. Defaults to the configured {@link #displayField} + */ + + /** + * @cfg + * @inheritdoc + * + * When {@link #forceSelection} is `false`, new records can be created by the user as they + * are typed. These records are **not** added to the combo's store. This creation + * is triggered by typing the configured 'delimiter', and can be further configured using the + * {@link #createNewOnEnter} and {@link #createNewOnBlur} configuration options. + * + * This functionality is primarily useful with BoxSelect components for things + * such as an email address. + */ + forceSelection: true, + + /** + * @cfg {Boolean} + * Has no effect if {@link #forceSelection} is `true`. + * + * With {@link #createNewOnEnter} set to `true`, the creation described in + * {@link #forceSelection} will also be triggered by the 'enter' key. + */ + createNewOnEnter: false, + + /** + * @cfg {Boolean} + * Has no effect if {@link #forceSelection} is `true`. + * + * With {@link #createNewOnBlur} set to `true`, the creation described in + * {@link #forceSelection} will also be triggered when the field loses focus. + * + * Please note that this behavior is also affected by the configuration options + * {@link #autoSelect} and {@link #selectOnTab}. If those are true and an existing + * item would have been selected as a result, the partial text the user has entered will + * be discarded and the existing item will be added to the selection. + */ + createNewOnBlur: false, + + /** + * @cfg {Boolean} + * Has no effect if {@link #multiSelect} is `false`. + * + * Controls the formatting of the form submit value of the field as returned by {@link #getSubmitValue} + * + * - `true` for the field value to submit as a json encoded array in a single GET/POST variable + * - `false` for the field to submit as an array of GET/POST variables + */ + encodeSubmitValue: false, + + // + // End of configuration options related to selected values + // + + + + // + // Configuration options related to pick list behavior + // + + /** + * @cfg {Boolean} + * `true` to activate the trigger when clicking in empty space in the field. Note that the + * subsequent behavior of this is controlled by the field's {@link #triggerAction}. + * This behavior is similar to that of a basic ComboBox with {@link #editable} `false`. + */ + triggerOnClick: true, + + /** + * @cfg {Boolean} + * - `true` to have each selected value fill to the width of the form field + * - `false to have each selected value size to its displayed contents + */ + stacked: false, + + /** + * @cfg {Boolean} + * Has no effect if {@link #multiSelect} is `false` + * + * `true` to keep the pick list expanded after each selection from the pick list + * `false` to automatically collapse the pick list after a selection is made + */ + pinList: true, + + /** + * @cfg {Boolean} + * True to hide the currently selected values from the drop down list. These items are hidden via + * css to maintain simplicity in store and filter management. + * + * - `true` to hide currently selected values from the drop down pick list + * - `false` to keep the item in the pick list as a selected item + */ + filterPickList: false, + + // + // End of configuration options related to pick list behavior + // + + + + // + // Configuration options related to text field behavior + // + + /** + * @cfg + * @inheritdoc + */ + selectOnFocus: true, + + /** + * @cfg {Boolean} + * + * `true` if this field should automatically grow and shrink vertically to its content. + * Note that this overrides the natural trigger grow functionality, which is used to size + * the field horizontally. + */ + grow: true, + + /** + * @cfg {Number/Boolean} + * Has no effect if {@link #grow} is `false` + * + * The minimum height to allow when {@link #grow} is `true`, or `false` to allow for + * natural vertical growth based on the current selected values. See also {@link #growMax}. + */ + growMin: false, + + /** + * @cfg {Number/Boolean} + * Has no effect if {@link #grow} is `false` + * + * The maximum height to allow when {@link #grow} is `true`, or `false` to allow for + * natural vertical growth based on the current selected values. See also {@link #growMin}. + */ + growMax: false, + + /** + * @cfg growAppend + * @hide + * Currently unsupported by BoxSelect since this is used for horizontal growth and + * BoxSelect only supports vertical growth. + */ + /** + * @cfg growToLongestValue + * @hide + * Currently unsupported by BoxSelect since this is used for horizontal growth and + * BoxSelect only supports vertical growth. + */ + + // + // End of configuration options related to text field behavior + // + + + // + // Event signatures + // + + /** + * @event autosize + * Fires when the **{@link #autoSize}** function is triggered and the field is resized according to the + * {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. This event provides a hook for the + * developer to apply additional logic at runtime to resize the field if needed. + * @param {Ext.ux.form.field.BoxSelect} this This BoxSelect field + * @param {Number} height The new field height + */ + + // + // End of event signatures + // + + + + // + // Configuration options that will break things if messed with + // + + /** + * @private + */ + fieldSubTpl: [ + '
    ', + '', + '
    ', + { + compiled: true, + disableFormats: true + } + ], + + /** + * @private + */ + childEls: [ 'listWrapper', 'itemList', 'inputEl', 'inputElCt', 'emptyEl' ], + + /** + * @private + */ + componentLayout: 'boxselectfield203', + + /** + * @private + */ + emptyInputCls: 'x-boxselect-emptyinput', + + /** + * @inheritdoc + * + * Initialize additional settings and enable simultaneous typeAhead and multiSelect support + * @protected + */ + initComponent: function() { + var me = this, + typeAhead = me.typeAhead; + + if (typeAhead && !me.editable) { + Ext4.Error.raise('If typeAhead is enabled the combo must be editable: true -- please change one of those settings.'); + } + + Ext4.apply(me, { + typeAhead: false + }); + + me.callParent(); + + me.typeAhead = typeAhead; + + me.selectionModel = new Ext4.selection.Model({ + store: me.valueStore, + mode: 'MULTI', + lastFocused: null, + onSelectChange: function(record, isSelected, suppressEvent, commitFn) { + commitFn(); + } + }); + + if (!Ext4.isEmpty(me.delimiter) && me.multiSelect) { + me.delimiterRegexp = new RegExp(String(me.delimiter).replace(/[$%()*+.?\[\\\]{|}]/g, "\\$&")); + } + }, + + /** + * Register events for management controls of labelled items + * @protected + */ + initEvents: function() { + var me = this; + + me.callParent(arguments); + + if (!me.enableKeyEvents) { + me.mon(me.inputEl, 'keydown', me.onKeyDown, me); + } + me.mon(me.inputEl, 'paste', me.onPaste, me); + me.mon(me.listWrapper, 'click', me.onItemListClick, me); + + // I would prefer to use relayEvents here to forward these events on, but I want + // to pass the field instead of exposing the underlying selection model + me.mon(me.selectionModel, { + 'selectionchange': function(selModel, selectedRecs) { + me.applyMultiselectItemMarkup(); + me.fireEvent('valueselectionchange', me, selectedRecs); + }, + 'focuschange': function(selectionModel, oldFocused, newFocused) { + me.fireEvent('valuefocuschange', me, oldFocused, newFocused); + }, + scope: me + }); + }, + + /** + * @inheritdoc + * + * Create a store for the records of our current value based on the main store's model + * @protected + */ + onBindStore: function(store, initial) { + var me = this; + + if (store) { + me.valueStore = new Ext4.data.Store({ + model: store.model, + proxy: { + type: 'memory' + } + }); + me.mon(me.valueStore, 'datachanged', me.applyMultiselectItemMarkup, me); + if (me.selectionModel) { + me.selectionModel.bindStore(me.valueStore); + } + } + }, + + /** + * @inheritdoc + * + * Remove the selected value store and associated listeners + * @protected + */ + onUnbindStore: function(store) { + var me = this, + valueStore = me.valueStore; + + if (valueStore) { + if (me.selectionModel) { + me.selectionModel.setLastFocused(null); + me.selectionModel.deselectAll(); + me.selectionModel.bindStore(null); + } + me.mun(valueStore, 'datachanged', me.applyMultiselectItemMarkup, me); + valueStore.destroy(); + me.valueStore = null; + } + + me.callParent(arguments); + }, + + /** + * @inheritdoc + * + * Add refresh tracking to the picker for selection management + * @protected + */ + createPicker: function() { + var me = this, + picker = me.callParent(arguments); + + me.mon(picker, { + 'beforerefresh': me.onBeforeListRefresh, + scope: me + }); + + if (me.filterPickList) { + picker.addCls('x-boxselect-hideselections'); + } + + return picker; + }, + + /** + * @inheritdoc + * + * Clean up selected values management controls + * @protected + */ + onDestroy: function() { + var me = this; + + Ext4.destroyMembers(me, 'valueStore', 'selectionModel'); + + me.callParent(arguments); + }, + + /** + * Add empty text support to initial render. + * @protected + */ + getSubTplData: function() { + var me = this, + data = me.callParent(), + isEmpty = me.emptyText && data.value.length < 1; + + data.value = ''; + if (isEmpty) { + data.emptyText = me.emptyText; + data.emptyCls = me.emptyCls; + data.inputElCls = me.emptyInputCls; + } else { + data.emptyText = ''; + data.emptyCls = me.emptyInputCls; + data.inputElCls = ''; + } + + return data; + }, + + /** + * @inheritdoc + * + * Overridden to avoid use of placeholder, as our main input field is often empty + * @protected + */ + afterRender: function() { + var me = this; + + if (Ext4.supports.Placeholder && me.inputEl && me.emptyText) { + delete me.inputEl.dom.placeholder; + } + + me.bodyEl.applyStyles('vertical-align:top'); + + if (me.grow) { + if (Ext4.isNumber(me.growMin) && (me.growMin > 0)) { + me.listWrapper.applyStyles('min-height:'+me.growMin+'px'); + } + if (Ext4.isNumber(me.growMax) && (me.growMax > 0)) { + me.listWrapper.applyStyles('max-height:'+me.growMax+'px'); + } + } + + if (me.stacked === true) { + me.itemList.addCls('x-boxselect-stacked'); + } + + if (!me.multiSelect) { + me.itemList.addCls('x-boxselect-singleselect'); + } + + me.applyMultiselectItemMarkup(); + + me.callParent(arguments); + }, + + /** + * Overridden to search entire unfiltered store since already selected values + * can span across multiple store page loads and other filtering. Overlaps + * some with {@link #isFilteredRecord}, but findRecord is used by the base component + * for various logic so this logic is applied here as well. + * @protected + */ + findRecord: function(field, value) { + var ds = this.store, + matches; + + if (!ds) { + return false; + } + + matches = ds.queryBy(function(rec, id) { + return rec.isEqual(rec.get(field), value); + }); + + return (matches.getCount() > 0) ? matches.first() : false; + }, + + /** + * Overridden to map previously selected records to the "new" versions of the records + * based on value field, if they are part of the new store load + * @protected + */ + onLoad: function() { + var me = this, + valueField = me.valueField, + valueStore = me.valueStore, + changed = false; + + if (valueStore) { + if (!Ext4.isEmpty(me.value) && (valueStore.getCount() == 0)) { + me.setValue(me.value, false, true); + } + + valueStore.suspendEvents(); + valueStore.each(function(rec) { + var r = me.findRecord(valueField, rec.get(valueField)), + i = r ? valueStore.indexOf(rec) : -1; + if (i >= 0) { + valueStore.removeAt(i); + valueStore.insert(i, r); + changed = true; + } + }); + valueStore.resumeEvents(); + if (changed) { + valueStore.fireEvent('datachanged', valueStore); + } + } + + me.callParent(arguments); + }, + + /** + * Used to determine if a record is filtered out of the current store's data set, + * for determining if a currently selected value should be retained. + * + * Slightly complicated logic. A record is considered filtered and should be retained if: + * + * - It is not in the combo store and the store has no filter or it is in the filtered data set + * (Happens when our selected value is just part of a different load, page or query) + * - It is not in the combo store and forceSelection is false and it is in the value store + * (Happens when our selected value was created manually) + * + * @private + */ + isFilteredRecord: function(record) { + var me = this, + store = me.store, + valueField = me.valueField, + storeRecord, + filtered = false; + + storeRecord = store.findExact(valueField, record.get(valueField)); + + filtered = ((storeRecord === -1) && (!store.snapshot || (me.findRecord(valueField, record.get(valueField)) !== false))); + + filtered = filtered || (!filtered && (storeRecord === -1) && (me.forceSelection !== true) && + (me.valueStore.findExact(valueField, record.get(valueField)) >= 0)); + + return filtered; + }, + + /** + * @inheritdoc + * + * Overridden to allow for continued querying with multiSelect selections already made + * @protected + */ + doRawQuery: function() { + var me = this, + rawValue = me.inputEl.dom.value; + + if (me.multiSelect) { + rawValue = rawValue.split(me.delimiter).pop(); + } + + this.doQuery(rawValue, false, true); + }, + + /** + * When the picker is refreshing, we should ignore selection changes. Otherwise + * the value of our field will be changing just because our view of the choices is. + * @protected + */ + onBeforeListRefresh: function() { + this.ignoreSelection++; + }, + + /** + * When the picker is refreshing, we should ignore selection changes. Otherwise + * the value of our field will be changing just because our view of the choices is. + * @protected + */ + onListRefresh: function() { + this.callParent(arguments); + if (this.ignoreSelection > 0) { + --this.ignoreSelection; + } + }, + + /** + * Overridden to preserve current labelled items when list is filtered/paged/loaded + * and does not include our current value. See {@link #isFilteredRecord} + * @private + */ + onListSelectionChange: function(list, selectedRecords) { + var me = this, + valueStore = me.valueStore, + mergedRecords = [], + i; + + // Only react to selection if it is not called from setValue, and if our list is + // expanded (ignores changes to the selection model triggered elsewhere) + if ((me.ignoreSelection <= 0) && me.isExpanded) { + // Pull forward records that were already selected or are now filtered out of the store + valueStore.each(function(rec) { + if (Ext4.Array.contains(selectedRecords, rec) || me.isFilteredRecord(rec)) { + mergedRecords.push(rec); + } + }); + mergedRecords = Ext4.Array.merge(mergedRecords, selectedRecords); + + i = Ext4.Array.intersect(mergedRecords, valueStore.getRange()).length; + if ((i != mergedRecords.length) || (i != me.valueStore.getCount())) { + me.setValue(mergedRecords, false); + if (!me.multiSelect || !me.pinList) { + Ext4.defer(me.collapse, 1, me); + } + if (valueStore.getCount() > 0) { + me.fireEvent('select', me, valueStore.getRange()); + } + } + me.inputEl.focus(); + if (!me.pinList) { + me.inputEl.dom.value = ''; + } + if (me.selectOnFocus) { + me.inputEl.dom.select(); + } + } + }, + + /** + * Overridden to use valueStore instead of valueModels, for inclusion of + * filtered records. See {@link #isFilteredRecord} + * @private + */ + syncSelection: function() { + var me = this, + picker = me.picker, + valueField = me.valueField, + pickStore, selection, selModel; + + if (picker) { + pickStore = picker.store; + + // From the value, find the Models that are in the store's current data + selection = []; + if (me.valueStore) { + me.valueStore.each(function(rec) { + var i = pickStore.findExact(valueField, rec.get(valueField)); + if (i >= 0) { + selection.push(pickStore.getAt(i)); + } + }); + } + + // Update the selection to match + me.ignoreSelection++; + selModel = picker.getSelectionModel(); + selModel.deselectAll(); + if (selection.length > 0) { + selModel.select(selection); + } + if (me.ignoreSelection > 0) { + --me.ignoreSelection; + } + } + }, + + /** + * Overridden to align to itemList size instead of inputEl + */ + doAlign: function(){ + var me = this, + picker = me.picker, + aboveSfx = '-above', + isAbove; + + me.picker.alignTo(me.listWrapper, me.pickerAlign, me.pickerOffset); + // add the {openCls}-above class if the picker was aligned above + // the field due to hitting the bottom of the viewport + isAbove = picker.el.getY() < me.inputEl.getY(); + me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx); + picker[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx); + }, + + /** + * Overridden to preserve scroll position of pick list when list is realigned + */ + alignPicker: function() { + var me = this, + picker = me.picker, + pickerScrollPos = picker.getTargetEl().dom.scrollTop; + + me.callParent(arguments); + + if (me.isExpanded) { + if (me.matchFieldWidth) { + // Auto the height (it will be constrained by min and max width) unless there are no records to display. + picker.setWidth(me.listWrapper.getWidth()); + } + + picker.getTargetEl().dom.scrollTop = pickerScrollPos; + } + }, + + /** + * Get the current cursor position in the input field, for key-based navigation + * @private + */ + getCursorPosition: function() { + var cursorPos; + if (Ext4.isIE) { + cursorPos = document.selection.createRange(); + cursorPos.collapse(true); + cursorPos.moveStart("character", -this.inputEl.dom.value.length); + cursorPos = cursorPos.text.length; + } else { + cursorPos = this.inputEl.dom.selectionStart; + } + return cursorPos; + }, + + /** + * Check to see if the input field has selected text, for key-based navigation + * @private + */ + hasSelectedText: function() { + var sel, range; + if (Ext4.isIE) { + sel = document.selection; + range = sel.createRange(); + return (range.parentElement() == this.inputEl.dom); + } else { + return this.inputEl.dom.selectionStart != this.inputEl.dom.selectionEnd; + } + }, + + /** + * Handles keyDown processing of key-based selection of labelled items. + * Supported keyboard controls: + * + * - If pick list is expanded + * + * - `CTRL-A` will select all the items in the pick list + * + * - If the cursor is at the beginning of the input field and there are values present + * + * - `CTRL-A` will highlight all the currently selected values + * - `BACKSPACE` and `DELETE` will remove any currently highlighted selected values + * - `RIGHT` and `LEFT` will move the current highlight in the appropriate direction + * - `SHIFT-RIGHT` and `SHIFT-LEFT` will add to the current highlight in the appropriate direction + * + * @protected + */ + onKeyDown: function(e, t) { + var me = this, + key = e.getKey(), + rawValue = me.inputEl.dom.value, + valueStore = me.valueStore, + selModel = me.selectionModel, + stopEvent = false; + + if (me.readOnly || me.disabled || !me.editable) { + return; + } + + if (me.isExpanded && (key == e.A && e.ctrlKey)) { + // CTRL-A when picker is expanded - add all items in current picker store page to current value + me.select(me.getStore().getRange()); + selModel.setLastFocused(null); + selModel.deselectAll(); + me.collapse(); + me.inputEl.focus(); + stopEvent = true; + } else if ((valueStore.getCount() > 0) && + ((rawValue == '') || ((me.getCursorPosition() === 0) && !me.hasSelectedText()))) { + // Keyboard navigation of current values + var lastSelectionIndex = (selModel.getCount() > 0) ? valueStore.indexOf(selModel.getLastSelected() || selModel.getLastFocused()) : -1; + + if ((key == e.BACKSPACE) || (key == e.DELETE)) { + if (lastSelectionIndex > -1) { + if (selModel.getCount() > 1) { + lastSelectionIndex = -1; + } + me.valueStore.remove(selModel.getSelection()); + } else { + me.valueStore.remove(me.valueStore.last()); + } + selModel.clearSelections(); + me.setValue(me.valueStore.getRange()); + if (lastSelectionIndex > 0) { + selModel.select(lastSelectionIndex - 1); + } + stopEvent = true; + } else if ((key == e.RIGHT) || (key == e.LEFT)) { + if ((lastSelectionIndex == -1) && (key == e.LEFT)) { + selModel.select(valueStore.last()); + stopEvent = true; + } else if (lastSelectionIndex > -1) { + if (key == e.RIGHT) { + if (lastSelectionIndex < (valueStore.getCount() - 1)) { + selModel.select(lastSelectionIndex + 1, e.shiftKey); + stopEvent = true; + } else if (!e.shiftKey) { + selModel.setLastFocused(null); + selModel.deselectAll(); + stopEvent = true; + } + } else if ((key == e.LEFT) && (lastSelectionIndex > 0)) { + selModel.select(lastSelectionIndex - 1, e.shiftKey); + stopEvent = true; + } + } + } else if (key == e.A && e.ctrlKey) { + selModel.selectAll(); + stopEvent = e.A; + } + me.inputEl.focus(); + } + + if (stopEvent) { + me.preventKeyUpEvent = stopEvent; + e.stopEvent(); + return; + } + + // Prevent key up processing for enter if it is being handled by the picker + if (me.isExpanded && (key == e.ENTER) && me.picker.highlightedItem) { + me.preventKeyUpEvent = true; + } + + if (me.enableKeyEvents) { + me.callParent(arguments); + } + + if (!e.isSpecialKey() && !e.hasModifier()) { + me.selectionModel.setLastFocused(null); + me.selectionModel.deselectAll(); + me.inputEl.focus(); + } + }, + + /** + * Handles auto-selection and creation of labelled items based on this field's + * delimiter, as well as the keyUp processing of key-based selection of labelled items. + * @protected + */ + onKeyUp: function(e, t) { + var me = this, + rawValue = me.inputEl.dom.value; + + if (me.preventKeyUpEvent) { + e.stopEvent(); + if ((me.preventKeyUpEvent === true) || (e.getKey() === me.preventKeyUpEvent)) { + delete me.preventKeyUpEvent; + } + return; + } + + if (me.multiSelect && (me.delimiterRegexp && me.delimiterRegexp.test(rawValue)) || + ((me.createNewOnEnter === true) && e.getKey() == e.ENTER)) { + rawValue = Ext4.Array.clean(rawValue.split(me.delimiterRegexp)); + me.inputEl.dom.value = ''; + me.setValue(me.valueStore.getRange().concat(rawValue)); + me.inputEl.focus(); + } + + me.callParent([e,t]); + }, + + /** + * Handles auto-selection of labelled items based on this field's delimiter when pasting + * a list of values in to the field (e.g., for email addresses) + * @protected + */ + onPaste: function(e, t) { + var me = this, + rawValue = me.inputEl.dom.value, + clipboard = (e && e.browserEvent && e.browserEvent.clipboardData) ? e.browserEvent.clipboardData : false; + + if (me.multiSelect && (me.delimiterRegexp && me.delimiterRegexp.test(rawValue))) { + if (clipboard && clipboard.getData) { + if (/text\/plain/.test(clipboard.types)) { + rawValue = clipboard.getData('text/plain'); + } else if (/text\/html/.test(clipboard.types)) { + rawValue = clipboard.getData('text/html'); + } + } + + rawValue = Ext4.Array.clean(rawValue.split(me.delimiterRegexp)); + me.inputEl.dom.value = ''; + me.setValue(me.valueStore.getRange().concat(rawValue)); + me.inputEl.focus(); + } + }, + + /** + * Overridden to handle key navigation of pick list when list is filtered. Because we + * want to avoid complexity that could be introduced by modifying the store's contents, + * (e.g., always having to search back through and remove values when they might + * be re-sent by the server, adding the values back in their previous position when + * they are removed from the current selection, etc.), we handle this filtering + * via a simple css rule. However, for the moment since those DOM nodes still exist + * in the list we have to hijack the highlighting methods for the picker's BoundListKeyNav + * to appropriately skip over these hidden nodes. This is a less than ideal solution, + * but it centralizes all of the complexity of this problem in to this one method. + * @protected + */ + onExpand: function() { + var me = this, + keyNav = me.listKeyNav; + + me.callParent(arguments); + + if (keyNav || !me.filterPickList) { + return; + } + keyNav = me.listKeyNav; + keyNav.highlightAt = function(index) { + var boundList = this.boundList, + item = boundList.all.item(index), + len = boundList.all.getCount(), + direction; + + if (item && item.hasCls('x-boundlist-selected')) { + if ((index == 0) || !boundList.highlightedItem || (boundList.indexOf(boundList.highlightedItem) < index)) { + direction = 1; + } else { + direction = -1; + } + do { + index = index + direction; + item = boundList.all.item(index); + } while ((index > 0) && (index < len) && item.hasCls('x-boundlist-selected')); + + if (item.hasCls('x-boundlist-selected')) { + return; + } + } + + if (item) { + item = item.dom; + boundList.highlightItem(item); + boundList.getTargetEl().scrollChildIntoView(item, false); + } + }; + }, + + /** + * Overridden to get and set the DOM value directly for type-ahead suggestion (bypassing get/setRawValue) + * @protected + */ + onTypeAhead: function() { + var me = this, + displayField = me.displayField, + inputElDom = me.inputEl.dom, + valueStore = me.valueStore, + boundList = me.getPicker(), + record, newValue, len, selStart; + + if (me.filterPickList) { + var fn = this.createFilterFn(displayField, inputElDom.value, true, false, false); + record = me.store.findBy(function(rec) { + return ((valueStore.indexOfId(rec.getId()) === -1) && fn(rec)); + }); + record = (record === -1) ? false : me.store.getAt(record); + } else { + record = me.store.findRecord(displayField, inputElDom.value, 0, true, false, false); + } + + if (record) { + newValue = record.get(displayField); + len = newValue.length; + selStart = inputElDom.value.length; + boundList.highlightItem(boundList.getNode(record)); + if (selStart !== 0 && selStart !== len) { + inputElDom.value = newValue; + me.selectText(selStart, newValue.length); + } + } + }, + + + /** + * Delegation control for selecting and removing labelled items or triggering list collapse/expansion + * @protected + */ + onItemListClick: function(evt, el, o) { + var me = this, + itemEl = evt.getTarget('.x-boxselect-item'), + closeEl = itemEl ? evt.getTarget('.x-boxselect-item-close') : false; + + if (me.readOnly || me.disabled) { + return; + } + + evt.stopPropagation(); + + if (itemEl) { + if (closeEl) { + me.removeByListItemNode(itemEl); + if (me.valueStore.getCount() > 0) { + me.fireEvent('select', me, me.valueStore.getRange()); + } + } else { + me.toggleSelectionByListItemNode(itemEl, evt.shiftKey); + } + me.inputEl.focus(); + } else { + if (me.selectionModel.getCount() > 0) { + me.selectionModel.setLastFocused(null); + me.selectionModel.deselectAll(); + } + if (me.triggerOnClick) { + me.onTriggerClick(); + } + } + }, + + /** + * Build the markup for the labelled items. Template must be built on demand due to ComboBox initComponent + * lifecycle for the creation of on-demand stores (to account for automatic valueField/displayField setting) + * @private + */ + getMultiSelectItemMarkup: function() { + var me = this; + + if (!me.multiSelectItemTpl) { + if (!me.labelTpl) { + me.labelTpl = Ext4.create('Ext.XTemplate', + '{[values.' + me.displayField + ']}' + ); + } else if (Ext4.isString(me.labelTpl) || Ext4.isArray(me.labelTpl)) { + me.labelTpl = Ext4.create('Ext.XTemplate', me.labelTpl); + } + + me.multiSelectItemTpl = [ + '', + '
  • ', + ' selected', + '', + '" qtip="{[typeof values === "string" ? values : values.' + me.displayField + ']}">' , + '
    {[typeof values === "string" ? values : this.getItemLabel(values)]}
    ', + '
    ' , + '
  • ' , + '
    ', + { + compile: true, + disableFormats: true, + isSelected: function(value) { + var i = me.valueStore.findExact(me.valueField, value); + if (i >= 0) { + return me.selectionModel.isSelected(me.valueStore.getAt(i)); + } + return false; + }, + getItemLabel: function(values) { + return me.getTpl('labelTpl').apply(values); + } + } + ]; + } + + return this.getTpl('multiSelectItemTpl').apply(Ext4.Array.pluck(this.valueStore.getRange(), 'data')); + }, + + /** + * Update the labelled items rendering + * @private + */ + applyMultiselectItemMarkup: function() { + var me = this, + itemList = me.itemList, + item; + + if (itemList) { + while ((item = me.inputElCt.prev()) != null) { + item.remove(); + } + me.inputElCt.insertHtml('beforeBegin', me.getMultiSelectItemMarkup()); + } + + Ext4.Function.defer(function() { + if (me.picker && me.isExpanded) { + me.alignPicker(); + } + if (me.hasFocus && me.inputElCt && me.listWrapper) { + me.inputElCt.scrollIntoView(me.listWrapper); + } + }, 15); + }, + + /** + * Returns the record from valueStore for the labelled item node + */ + getRecordByListItemNode: function(itemEl) { + var me = this, + itemIdx = 0, + searchEl = me.itemList.dom.firstChild; + + while (searchEl && searchEl.nextSibling) { + if (searchEl == itemEl) { + break; + } + itemIdx++; + searchEl = searchEl.nextSibling; + } + itemIdx = (searchEl == itemEl) ? itemIdx : false; + + if (itemIdx === false) { + return false; + } + + return me.valueStore.getAt(itemIdx); + }, + + /** + * Toggle of labelled item selection by node reference + */ + toggleSelectionByListItemNode: function(itemEl, keepExisting) { + var me = this, + rec = me.getRecordByListItemNode(itemEl), + selModel = me.selectionModel; + + if (rec) { + if (selModel.isSelected(rec)) { + if (selModel.isFocused(rec)) { + selModel.setLastFocused(null); + } + selModel.deselect(rec); + } else { + selModel.select(rec, keepExisting); + } + } + }, + + /** + * Removal of labelled item by node reference + */ + removeByListItemNode: function(itemEl) { + var me = this, + rec = me.getRecordByListItemNode(itemEl); + + if (rec) { + me.valueStore.remove(rec); + me.setValue(me.valueStore.getRange()); + } + }, + + /** + * @inheritdoc + * Intercept calls to getRawValue to pretend there is no inputEl for rawValue handling, + * so that we can use inputEl for user input of just the current value. + */ + getRawValue: function() { + var me = this, + inputEl = me.inputEl, + result; + me.inputEl = false; + result = me.callParent(arguments); + me.inputEl = inputEl; + return result; + }, + + /** + * @inheritdoc + * Intercept calls to setRawValue to pretend there is no inputEl for rawValue handling, + * so that we can use inputEl for user input of just the current value. + */ + setRawValue: function(value) { + var me = this, + inputEl = me.inputEl, + result; + + me.inputEl = false; + result = me.callParent([value]); + me.inputEl = inputEl; + + return result; + }, + + /** + * Adds a value or values to the current value of the field + * @param {Mixed} value The value or values to add to the current value, see {@link #setValue} + */ + addValue: function(value) { + var me = this; + if (value) { + me.setValue(Ext4.Array.merge(me.value, Ext4.Array.from(value))); + } + }, + + /** + * Removes a value or values from the current value of the field + * @param {Mixed} value The value or values to remove from the current value, see {@link #setValue} + */ + removeValue: function(value) { + var me = this; + + if (value) { + me.setValue(Ext4.Array.difference(me.value, Ext4.Array.from(value))); + } + }, + + /** + * Sets the specified value(s) into the field. The following value formats are recognised: + * + * - Single Values + * + * - A string associated to this field's configured {@link #valueField} + * - A record containing at least this field's configured {@link #valueField} and {@link #displayField} + * + * - Multiple Values + * + * - If {@link #multiSelect} is `true`, a string containing multiple strings as + * specified in the Single Values section above, concatenated in to one string + * with each entry separated by this field's configured {@link #delimiter} + * - An array of strings as specified in the Single Values section above + * - An array of records as specified in the Single Values section above + * + * In any of the string formats above, the following occurs if an associated record cannot be found: + * + * 1. If {@link #forceSelection} is `false`, a new record of the {@link #store}'s configured model type + * will be created using the given value as the {@link #displayField} and {@link #valueField}. + * This record will be added to the current value, but it will **not** be added to the store. + * 2. If {@link #forceSelection} is `true` and {@link #queryMode} is `remote`, the list of unknown + * values will be submitted as a call to the {@link #store}'s load as a parameter named by + * the {@link #valueParam} with values separated by the configured {@link #delimiter}. + * ** This process will cause setValue to asynchronously process. ** This will only be attempted + * once. Any unknown values that the server does not return records for will be removed. + * 3. Otherwise, unknown values will be removed. + * + * @param {Mixed} value The value(s) to be set, see method documentation for details + * @return {Ext.form.field.Field/Boolean} this, or `false` if asynchronously querying for unknown values + */ + setValue: function(value, doSelect, skipLoad) { + var me = this, + valueStore = me.valueStore, + valueField = me.valueField, + record, len, i, valueRecord, h, + unknownValues = []; + + if (Ext4.isEmpty(value)) { + value = null; + } + if (Ext4.isString(value) && me.multiSelect) { + value = value.split(me.delimiter); + } + value = Ext4.Array.from(value, true); + + for (i = 0, len = value.length; i < len; i++) { + record = value[i]; + if (!record || !record.isModel) { + valueRecord = valueStore.findExact(valueField, record); + if (valueRecord >= 0) { + value[i] = valueStore.getAt(valueRecord); + } else { + valueRecord = me.findRecord(valueField, record); + if (!valueRecord) { + if (me.forceSelection) { + unknownValues.push(record); + } else { + valueRecord = {}; + valueRecord[me.valueField] = record; + valueRecord[me.displayField] = record; + valueRecord = new me.valueStore.model(valueRecord); + } + } + if (valueRecord) { + value[i] = valueRecord; + } + } + } + } + + if ((skipLoad !== true) && (unknownValues.length > 0) && (me.queryMode === 'remote')) { + var params = {}; + params[me.valueParam || me.valueField] = unknownValues.join(me.delimiter); + me.store.load({ + params: params, + callback: function() { + if (me.itemList) { + me.itemList.unmask(); + } + me.setValue(value, doSelect, true); + me.autoSize(); + me.lastQuery = false; + } + }); + return false; + } + + // For single-select boxes, use the last good (formal record) value if possible + if (!me.multiSelect && (value.length > 0)) { + for (i = value.length - 1; i >= 0; i--) { + if (value[i].isModel) { + value = value[i]; + break; + } + } + if (Ext4.isArray(value)) { + value = value[value.length - 1]; + } + } + + return me.callParent([value, doSelect]); + }, + + /** + * Returns the records for the field's current value + * @return {Array} The records for the field's current value + */ + getValueRecords: function() { + return this.valueStore.getRange(); + }, + + /** + * @inheritdoc + * Overridden to optionally allow for submitting the field as a json encoded array. + */ + getSubmitData: function() { + var me = this, + val = me.callParent(arguments); + + if (me.multiSelect && me.encodeSubmitValue && val && val[me.name]) { + val[me.name] = Ext4.encode(val[me.name]); + } + + return val; + }, + + /** + * Overridden to clear the input field if we are auto-setting a value as we blur. + * @protected + */ + mimicBlur: function() { + var me = this; + + if (me.selectOnTab && me.picker && me.picker.highlightedItem) { + me.inputEl.dom.value = ''; + } + + me.callParent(arguments); + }, + + /** + * Overridden to handle partial-input selections more directly + */ + assertValue: function() { + var me = this, + rawValue = me.inputEl.dom.value, + rec = !Ext4.isEmpty(rawValue) ? me.findRecordByDisplay(rawValue) : false, + value = false; + + if (!rec && !me.forceSelection && me.createNewOnBlur && !Ext4.isEmpty(rawValue)) { + value = rawValue; + } else if (rec) { + value = rec; + } + + if (value) { + me.addValue(value); + } + + me.inputEl.dom.value = ''; + + me.collapse(); + }, + + /** + * Expand record values for evaluating change and fire change events for UI to respond to + */ + checkChange: function() { + if (!this.suspendCheckChange && !this.isDestroyed) { + var me = this, + valueStore = me.valueStore, + lastValue = me.lastValue || '', + valueField = me.valueField, + newValue = Ext4.Array.map(Ext4.Array.from(me.value), function(val) { + if (val.isModel) { + return val.get(valueField); + } + return val; + }, this).join(this.delimiter), + isEqual = me.isEqual(newValue, lastValue); + + if (!isEqual || ((newValue.length > 0 && valueStore.getCount() < newValue.length))) { + valueStore.suspendEvents(); + valueStore.removeAll(); + if (Ext4.isArray(me.valueModels)) { + valueStore.add(me.valueModels); + } + valueStore.resumeEvents(); + valueStore.fireEvent('datachanged', valueStore); + + if (!isEqual) { + me.lastValue = newValue; + me.fireEvent('change', me, newValue, lastValue); + me.onChange(newValue, lastValue); + } + } + } + }, + + /** + * Overridden to be more accepting of varied value types + */ + isEqual: function(v1, v2) { + var fromArray = Ext4.Array.from, + valueField = this.valueField, + i, len, t1, t2; + + v1 = fromArray(v1); + v2 = fromArray(v2); + len = v1.length; + + if (len !== v2.length) { + return false; + } + + for(i = 0; i < len; i++) { + t1 = v1[i].isModel ? v1[i].get(valueField) : v1[i]; + t2 = v2[i].isModel ? v2[i].get(valueField) : v2[i]; + if (t1 !== t2) { + return false; + } + } + + return true; + }, + + /** + * Overridden to use value (selection) instead of raw value and to avoid the use of placeholder + */ + applyEmptyText : function() { + var me = this, + emptyText = me.emptyText, + inputEl, isEmpty; + + if (me.rendered && emptyText) { + isEmpty = Ext4.isEmpty(me.value) && !me.hasFocus; + inputEl = me.inputEl; + if (isEmpty) { + inputEl.dom.value = ''; + me.emptyEl.update(emptyText); + me.emptyEl.addCls(me.emptyCls); + me.emptyEl.removeCls(me.emptyInputCls); + me.listWrapper.addCls(me.emptyCls); + me.inputEl.addCls(me.emptyInputCls); + } else { + me.emptyEl.addCls(me.emptyInputCls); + me.emptyEl.removeCls(me.emptyCls); + me.listWrapper.removeCls(me.emptyCls); + me.inputEl.removeCls(me.emptyInputCls); + } + me.autoSize(); + } + }, + + /** + * Overridden to use inputEl instead of raw value and to avoid the use of placeholder + */ + preFocus : function(){ + var me = this, + inputEl = me.inputEl, + emptyText = me.emptyText, + isEmpty = (inputEl.dom.value == ''); + + me.emptyEl.addCls(me.emptyInputCls); + me.emptyEl.removeCls(me.emptyCls); + me.listWrapper.removeCls(me.emptyCls); + me.inputEl.removeCls(me.emptyInputCls); + + if (me.selectOnFocus || isEmpty) { + inputEl.dom.select(); + } + }, + + /** + * Intercept calls to onFocus to add focusCls, because the base field + * classes assume this should be applied to inputEl + */ + onFocus: function() { + var me = this, + focusCls = me.focusCls, + itemList = me.itemList; + + if (focusCls && itemList) { + itemList.addCls(focusCls); + } + + me.callParent(arguments); + }, + + /** + * Intercept calls to onBlur to remove focusCls, because the base field + * classes assume this should be applied to inputEl + */ + onBlur: function() { + var me = this, + focusCls = me.focusCls, + itemList = me.itemList; + + if (focusCls && itemList) { + itemList.removeCls(focusCls); + } + + me.callParent(arguments); + }, + + /** + * Intercept calls to renderActiveError to add invalidCls, because the base + * field classes assume this should be applied to inputEl + */ + renderActiveError: function() { + var me = this, + invalidCls = me.invalidCls, + itemList = me.itemList, + hasError = me.hasActiveError(); + + if (invalidCls && itemList) { + itemList[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field'); + } + + me.callParent(arguments); + }, + + /** + * Initiate auto-sizing for height based on {@link #grow}, if applicable. + */ + autoSize: function() { + var me = this, + height; + + if (me.grow && me.rendered) { + me.autoSizing = true; + me.updateLayout(); + } + + return me; + }, + + /** + * Track height change to fire {@link #event-autosize} event, when applicable. + */ + afterComponentLayout: function() { + var me = this, + width; + + if (me.autoSizing) { + height = me.getHeight(); + if (height !== me.lastInputHeight) { + if (me.isExpanded) { + me.alignPicker(); + } + me.fireEvent('autosize', me, height); + me.lastInputHeight = height; + delete me.autoSizing; + } + } + } +}); + +/** + * Ensures the input element takes up the maximum amount of remaining list width, + * or the entirety of the list width if too little space remains. In this case, + * the list height will be automatically increased to accomodate the new line. This + * growth will not occur if {@link Ext.ux.form.field.BoxSelect#multiSelect} or + * {@link Ext.ux.form.field.BoxSelect#grow} is false. + */ +Ext4.define('Ext4.ux.layout.component.field.BoxSelectField203', { + /* Begin Definitions */ + alias: ['layout.boxselectfield203'], + extend: 'Ext.layout.component.field.Trigger', + + /* End Definitions */ + + type: 'boxselectfield203', + + /*For proper calculations we need our field to be sized.*/ + waitForOuterWidthInDom:true, + + beginLayout: function(ownerContext) { + var me = this, + owner = me.owner; + + me.callParent(arguments); + + ownerContext.inputElCtContext = ownerContext.getEl('inputElCt'); + owner.inputElCt.setStyle('width',''); + + me.skipInputGrowth = !owner.grow || !owner.multiSelect; + }, + + beginLayoutFixed: function(ownerContext, width, suffix) { + var me = this, + owner = ownerContext.target; + + owner.triggerEl.setStyle('height', '24px'); + + me.callParent(arguments); + + if (ownerContext.heightModel.fixed && ownerContext.lastBox) { + owner.listWrapper.setStyle('height', ownerContext.lastBox.height+'px'); + owner.itemList.setStyle('height', '100%'); + } + /*No inputElCt calculations here!*/ + }, + + /*Calculate and cache value of input container.*/ + publishInnerWidth:function(ownerContext) { + var me = this, + owner = me.owner, + width = owner.itemList.getWidth(true) - 10, + lastEntry = owner.inputElCt.prev(null, true); + + if (lastEntry && !owner.stacked) { + lastEntry = Ext4.fly(lastEntry); + width = width - lastEntry.getOffsetsTo(lastEntry.up(''))[0] - lastEntry.getWidth(); + } + + if (!me.skipInputGrowth && (width < 35)) { + width = width - 10; + } else if (width < 1) { + width = 1; + } + ownerContext.inputElCtContext.setWidth(width); + } +}); diff --git a/src/web/OpenCyto/ux/CheckCombo/CheckCombo.css b/src/web/OpenCyto/ux/CheckCombo/CheckCombo.css new file mode 100644 index 0000000..d7f6f7c --- /dev/null +++ b/src/web/OpenCyto/ux/CheckCombo/CheckCombo.css @@ -0,0 +1,15 @@ +/* check combo */ +.x4-combo-checker { + background-position: 50% -2px; + margin-left: 1px; + background-color: transparent; + background-image: url("../../../ext-4.1.0/resources/themes/images/default/grid/unchecked.gif"); + background-position: -1px -1px; + background-repeat: no-repeat; + height: 14px; + width: 14px; + display: inline-block; +} +.x4-boundlist-selected .x4-combo-checker { + background-image: url("../../../ext-4.1.0/resources/themes/images/default/grid/checked.gif"); +} \ No newline at end of file diff --git a/src/web/OpenCyto/ux/CheckCombo/CheckCombo.js b/src/web/OpenCyto/ux/CheckCombo/CheckCombo.js new file mode 100644 index 0000000..8e74cab --- /dev/null +++ b/src/web/OpenCyto/ux/CheckCombo/CheckCombo.js @@ -0,0 +1,240 @@ +/** + * From: http://www.sencha.com/forum/showthread.php?198862-Ext.ux.CheckCombo + * http://extjs.dariofilkovic.com/ + * Version from: January 14 2003 + */ +Ext4.define('Ext4.ux.CheckCombo', { + extend : 'Ext.form.field.ComboBox', + alias : 'widget.checkcombo', + multiSelect : true, + allSelector : false, + noData : false, + noDataText : 'No combo data', + addAllSelector : false, + allSelectorHidden : false, + enableKeyEvents : true, + afterExpandCheck : false, + allText : 'All', + oldValue : '', + listeners : { + /* uncomment if you want to reload store on every combo expand + beforequery: function(qe) + { + this.store.removeAll(); + delete qe.combo.lastQuery; + }, + */ + focus : function(cpt) { + cpt.oldValue = cpt.getValue(); + }, + keydown : function(cpt, e, eOpts) { + var value = cpt.getRawValue(), oldValue = cpt.oldValue; + + if (value != oldValue) + cpt.setValue(''); + } + }, + createPicker : function() { + var me = this, picker, menuCls = Ext4.baseCSSPrefix + 'menu', opts = Ext4.apply({ + pickerField : me, + selModel : { + mode : me.multiSelect ? 'SIMPLE' : 'SINGLE' + }, + floating : true, + hidden : true, + ownerCt : me.ownerCt, + cls : me.el.up('.' + menuCls) ? menuCls : '', + store : me.store, + displayField : me.displayField, + focusOnToFront : false, + pageSize : me.pageSize, + tpl : [''] + }, me.listConfig, me.defaultListConfig); + + picker = me.picker = Ext4.create('Ext.view.BoundList', opts); + if (me.pageSize) { + picker.pagingToolbar.on('beforechange', me.onPageChange, me); + } + + me.mon(picker, { + itemclick : me.onItemClick, + refresh : me.onListRefresh, + scope : me + }); + + me.mon(picker.getSelectionModel(), { + 'beforeselect' : me.onBeforeSelect, + 'beforedeselect' : me.onBeforeDeselect, + 'selectionchange' : me.onListSelectionChange, + scope : me + }); + + me.store.on('load', function(store) { + if (store.getTotalCount() == 0) { + me.allSelectorHidden = true; + if (me.allSelector != false) + me.allSelector.setStyle('display', 'none'); + if (me.noData != false) + me.noData.setStyle('display', 'block'); + } else { + me.allSelectorHidden = false; + if (me.allSelector != false) + me.allSelector.setStyle('display', 'block'); + if (me.noData != false) + me.noData.setStyle('display', 'none'); + } + }); + + return picker; + }, + reset : function() { + var me = this; + + me.setValue(''); + }, + setValue : function(value) { + this.value = value; + if (!value) { + if (this.allSelector != false) + this.allSelector.removeCls('x4-boundlist-selected'); + return this.callParent(arguments); + } + + if ( typeof value == 'string') { + var me = this, records = [], vals = value.split(','); + + if (value == '') { + if (me.allSelector != false) + me.allSelector.removeCls('x4-boundlist-selected'); + } else { + if (vals.length == me.store.getCount() && vals.length != 0) { + if (me.allSelector != false) + me.allSelector.addCls('x4-boundlist-selected'); + else + me.afterExpandCheck = true; + } + } + + Ext4.each(vals, function(val) { + var record = me.store.getById(parseInt(val)); + if (record) + records.push(record); + }); + + return me.setValue(records); + } else + return this.callParent(arguments); + }, + getValue : function() { + if ( typeof this.value == 'object') + return this.value.join(','); + else + return this.value; + }, + getSubmitValue : function() { + return this.getValue(); + }, + expand : function() { + var me = this, bodyEl, picker, collapseIf; + + if (me.rendered && !me.isExpanded && !me.isDestroyed) { + bodyEl = me.bodyEl; + picker = me.getPicker(); + collapseIf = me.collapseIf; + + // show the picker and set isExpanded flag + picker.show(); + me.isExpanded = true; + me.alignPicker(); + bodyEl.addCls(me.openCls); + + if (me.noData == false) { + me.noData = picker.getEl().down('.x4-boundlist-list-ct').insertHtml('beforeBegin', '
    ' + me.noDataText + '
    ', true); + } + + if (me.addAllSelector == true && me.allSelector == false) { + me.allSelector = picker.getEl().down('.x4-boundlist-list-ct').insertHtml('beforeBegin', '
      ' + me.allText + '
    ', true); + me.allSelector.on('click', function(e) { + if (me.allSelector.hasCls('x4-boundlist-selected')) { + me.allSelector.removeCls('x4-boundlist-selected'); + me.setValue(''); + me.fireEvent('select', me, []); + } else { + var records = []; + me.store.each(function(record) { + records.push(record); + }); + me.allSelector.addCls('x4-boundlist-selected'); + me.select(records); + me.fireEvent('select', me, records); + } + }); + + if (me.allSelectorHidden == true) + me.allSelector.hide(); + else + me.allSelector.show(); + + if (me.afterExpandCheck == true) { + me.allSelector.addCls('x4-boundlist-selected'); + me.afterExpandCheck = false; + } + } + + // monitor clicking and mousewheel + me.mon(Ext4.getDoc(), { + mousewheel : collapseIf, + mousedown : collapseIf, + scope : me + }); + Ext4.EventManager.onWindowResize(me.alignPicker, me); + me.fireEvent('expand', me); + me.onExpand(); + } else { + me.fireEvent('expand', me); + me.onExpand(); + } + }, + alignPicker : function() { + var me = this, picker = me.getPicker(); + + me.callParent(); + + if (me.addAllSelector == true) { + var height = picker.getHeight(); + height = parseInt(height) + 20; + picker.setHeight(height); + picker.getEl().setStyle('height', height + 'px'); + } + }, + onListSelectionChange : function(list, selectedRecords) { + var me = this, isMulti = me.multiSelect, hasRecords = selectedRecords.length > 0; + // Only react to selection if it is not called from setValue, and if our list is + // expanded (ignores changes to the selection model triggered elsewhere) + if (!me.ignoreSelection && me.isExpanded) { + if (!isMulti) { + Ext4.defer(me.collapse, 1, me); + } + /* + * Only set the value here if we're in multi selection mode or we have + * a selection. Otherwise setValue will be called with an empty value + * which will cause the change event to fire twice. + */ + if (isMulti || hasRecords) { + me.setValue(selectedRecords, false); + } + if (hasRecords) { + me.fireEvent('select', me, selectedRecords); + } + me.inputEl.focus(); + + if (me.addAllSelector == true && me.allSelector != false) { + if (selectedRecords.length == me.store.getTotalCount()) + me.allSelector.addCls('x4-boundlist-selected'); + else + me.allSelector.removeCls('x4-boundlist-selected'); + } + + } + } +}); \ No newline at end of file diff --git a/src/web/OpenCyto/ux/ClearButton/ClearButton.css b/src/web/OpenCyto/ux/ClearButton/ClearButton.css new file mode 100644 index 0000000..0ca7689 --- /dev/null +++ b/src/web/OpenCyto/ux/ClearButton/ClearButton.css @@ -0,0 +1,48 @@ +.ext-ux-clearbutton { + width: 12px; + height: 12px; + background-image: url(../images/clear-text-icon.gif); + background-position: 0 0; + background-repeat: no-repeat; + -moz-user-focus: ignore; /* https://developer.mozilla.org/en/CSS/-moz-user-focus */ + cursor: pointer; + position: absolute; + overflow: hidden; /* IE 6 :-( */ + margin-top: 4px; + background-color: white; +} + +.ext-ux-clearbutton-mouse-over-input { + background-position: 0 -12px; +} + +.ext-ux-clearbutton-mouse-over-button { + background-position: 0 -24px; +} + +.ext-ux-clearbutton-mouse-down { + background-position: 0 -36px; +} + +.ext-ux-clearbutton-on { + opacity: 1; + visibility: visible; + transition: opacity .35s linear; + -webkit-transition: opacity .35s linear; + -moz-transition: opacity .35s linear; + -o-transition: opacity .35s linear; + -ms-transition: opacity .35s linear; + -khtml-transition: opacity .35s linear; +} + +.ext-ux-clearbutton-off { + opacity: 0; + visibility: hidden; + transition: opacity .35s linear, visibility .0s linear .35s; + -webkit-transition: opacity .35s linear, visibility .0s linear .35s; + -moz-transition: opacity .35s linear, visibility .0s linear .35s; + -o-transition: opacity .35s linear, visibility .0s linear .35s; + -ms-transition: opacity .35s linear, visibility .0s linear .35s; + -khtml-transition: opacity .35s linear, visibility .0s linear .35s; +} + diff --git a/src/web/OpenCyto/ux/ClearButton/ClearButton.js b/src/web/OpenCyto/ux/ClearButton/ClearButton.js new file mode 100644 index 0000000..4922a34 --- /dev/null +++ b/src/web/OpenCyto/ux/ClearButton/ClearButton.js @@ -0,0 +1,415 @@ +(function() { + /** + * @class Ext.ux.form.field.ClearButton + * + * Plugin for text components that shows a "clear" button over the text field. + * When the button is clicked the text field is set empty. + * Icon image and positioning can be controlled using CSS. + * Works with Ext.form.field.Text, Ext.form.field.TextArea, Ext.form.field.ComboBox and Ext.form.field.Date. + * + * Plugin alias is 'clearbutton' (use "plugins: 'clearbutton'" in GridPanel config). + * + * @author Stephen Friedrich + * @author Fabian Urban + * + * @copyright (c) 2011 Fortis IT Services GmbH + * @license Ext.ux.form.field.ClearButton is released under the + * Apache License, Version 2.0. + * + */ + Ext4.define('Ext4.ux.form.field.ClearButton', { + alias: 'plugin.clearbutton', + + /** + * @cfg {Boolean} Hide the clear button when the field is empty (default: true). + */ + hideClearButtonWhenEmpty: true, + + /** + * @cfg {Boolean} Hide the clear button until the mouse is over the field (default: true). + */ + hideClearButtonWhenMouseOut: true, + + /** + * @cfg {Boolean} When the clear buttons is hidden/shown, this will animate the button to its new state (using opacity) (default: true). + */ + animateClearButton: true, + + /** + * @cfg {Boolean} Empty the text field when ESC is pressed while the text field is focused. + */ + clearOnEscape: true, + + /** + * @cfg {String} CSS class used for the button div. + * Also used as a prefix for other classes (suffixes: '-mouse-over-input', '-mouse-over-button', '-mouse-down', '-on', '-off') + */ + clearButtonCls: 'ext-ux-clearbutton', + + /** + * The text field (or text area, combo box, date field) that we are attached to + */ + textField: null, + + /** + * Will be set to true if animateClearButton is true and the browser supports CSS 3 transitions + * @private + */ + animateWithCss3: false, + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Set up and tear down + // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + + constructor: function(cfg) { + Ext4.apply(this, cfg); + + this.callParent(arguments); + }, + + /** + * Called by plug-in system to initialize the plugin for a specific text field (or text area, combo box, date field). + * Most all the setup is delayed until the component is rendered. + */ + init: function(textField) { + this.textField = textField; + if (!textField.rendered) { + textField.on('afterrender', this.handleAfterRender, this); + } + else { + // probably an existing input element transformed to extjs field + this.handleAfterRender(); + } + }, + + /** + * After the field has been rendered sets up the plugin (create the Element for the clear button, attach listeners). + * @private + */ + handleAfterRender: function(textField) { + this.isTextArea = (this.textField.inputEl.dom.type.toLowerCase() == 'textarea'); + + this.createClearButtonEl(); + this.addListeners(); + + this.repositionClearButton(); + this.updateClearButtonVisibility(); + + this.addEscListener(); + }, + + /** + * Creates the Element and DOM for the clear button + */ + createClearButtonEl: function() { + var animateWithClass = this.animateClearButton && this.animateWithCss3; + this.clearButtonEl = this.textField.bodyEl.createChild({ + tag: 'div', + cls: this.clearButtonCls + }); + if(this.animateClearButton) { + this.animateWithCss3 = this.supportsCssTransition(this.clearButtonEl); + } + if(this.animateWithCss3) { + this.clearButtonEl.addCls(this.clearButtonCls + '-off'); + } + else { + this.clearButtonEl.setStyle('visibility', 'hidden'); + } + }, + + /** + * Returns true iff the browser supports CSS 3 transitions + * @param el an element that is checked for support of the "transition" CSS property (considering any + * vendor prefixes) + */ + supportsCssTransition: function(el) { + var styles = ['transitionProperty', 'WebkitTransitionProperty', 'MozTransitionProperty', + 'OTransitionProperty', 'msTransitionProperty', 'KhtmlTransitionProperty']; + + var style = el.dom.style; + for(var i = 0, length = styles.length; i < length; ++i) { + if(style[styles[i]] !== 'undefined') { + // Supported property will result in empty string + return true; + } + } + return false; + }, + + /** + * If config option "clearOnEscape" is true, then add a key listener that will clear this field + */ + addEscListener: function() { + if (!this.clearOnEscape) { + return; + } + + // Using a KeyMap did not work: ESC is swallowed by combo box and date field before it reaches our own KeyMap + this.textField.inputEl.on('keydown', + function(e) { + if (e.getKey() == Ext4.EventObject.ESC) { + if (this.textField.isExpanded) { + // Let combo box or date field first remove the popup + return; + } + // No idea why the defer is necessary, but otherwise the call to setValue('') is ignored + Ext4.Function.defer(this.textField.setValue, 1, this.textField, ['']); + e.stopEvent(); + } + }, + this); + }, + + /** + * Adds listeners to the field, its input element and the clear button to handle resizing, mouse over/out events, click events etc. + */ + addListeners: function() { + // listeners on input element (DOM/El level) + var textField = this.textField; + var bodyEl = textField.bodyEl; + bodyEl.on('mouseover', this.handleMouseOverInputField, this); + bodyEl.on('mouseout', this.handleMouseOutOfInputField, this); + + // listeners on text field (component level) + textField.on('destroy', this.handleDestroy, this); + textField.on('resize', this.repositionClearButton, this); + textField.on('change', function() { + this.repositionClearButton(); + this.updateClearButtonVisibility(); + }, this); + + // listeners on clear button (DOM/El level) + var clearButtonEl = this.clearButtonEl; + clearButtonEl.on('mouseover', this.handleMouseOverClearButton, this); + clearButtonEl.on('mouseout', this.handleMouseOutOfClearButton, this); + clearButtonEl.on('mousedown', this.handleMouseDownOnClearButton, this); + clearButtonEl.on('mouseup', this.handleMouseUpOnClearButton, this); + clearButtonEl.on('click', this.handleMouseClickOnClearButton, this); + }, + + /** + * When the field is destroyed, we also need to destroy the clear button Element to prevent memory leaks. + */ + handleDestroy: function() { + this.clearButtonEl.destroy(); + }, + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Mouse event handlers + // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Tada - the real action: If user left clicked on the clear button, then empty the field + */ + handleMouseClickOnClearButton: function(event, htmlElement, object) { + if (!this.isLeftButton(event)) { + return; + } + this.textField.setValue(''); + this.textField.focus(); + }, + + handleMouseOverInputField: function(event, htmlElement, object) { + this.clearButtonEl.addCls(this.clearButtonCls + '-mouse-over-input'); + if (event.getRelatedTarget() == this.clearButtonEl.dom) { + // Moused moved to clear button and will generate another mouse event there. + // Handle it here to avoid duplicate updates (else animation will break) + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-over-button'); + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-down'); + } + this.updateClearButtonVisibility(); + }, + + handleMouseOutOfInputField: function(event, htmlElement, object) { + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-over-input'); + if (event.getRelatedTarget() == this.clearButtonEl.dom) { + // Moused moved from clear button and will generate another mouse event there. + // Handle it here to avoid duplicate updates (else animation will break) + this.clearButtonEl.addCls(this.clearButtonCls + '-mouse-over-button'); + } + this.updateClearButtonVisibility(); + }, + + handleMouseOverClearButton: function(event, htmlElement, object) { + event.stopEvent(); + if (this.textField.bodyEl.contains(event.getRelatedTarget())) { + // has been handled in handleMouseOutOfInputField() to prevent double update + return; + } + this.clearButtonEl.addCls(this.clearButtonCls + '-mouse-over-button'); + this.updateClearButtonVisibility(); + }, + + handleMouseOutOfClearButton: function(event, htmlElement, object) { + event.stopEvent(); + if (this.textField.bodyEl.contains(event.getRelatedTarget())) { + // will be handled in handleMouseOverInputField() to prevent double update + return; + } + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-over-button'); + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-down'); + this.updateClearButtonVisibility(); + }, + + handleMouseDownOnClearButton: function(event, htmlElement, object) { + if (!this.isLeftButton(event)) { + return; + } + this.clearButtonEl.addCls(this.clearButtonCls + '-mouse-down'); + }, + + handleMouseUpOnClearButton: function(event, htmlElement, object) { + if (!this.isLeftButton(event)) { + return; + } + this.clearButtonEl.removeCls(this.clearButtonCls + '-mouse-down'); + }, + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // + // Utility methods + // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Repositions the clear button element based on the textfield.inputEl element + * @private + */ + repositionClearButton: function() { + var clearButtonEl = this.clearButtonEl; + if (!clearButtonEl) { + return; + } + var clearButtonPosition = this.calculateClearButtonPosition(this.textField); + clearButtonEl.dom.style.right = clearButtonPosition.right + 'px'; + clearButtonEl.dom.style.top = clearButtonPosition.top + 'px'; + }, + + /** + * Calculates the position of the clear button based on the textfield.inputEl element + * @private + */ + calculateClearButtonPosition: function(textField) { + var positions = textField.inputEl.getBox(true, true); + var top = positions.y; + var right = positions.x; + if (this.fieldHasScrollBar()) { + right += Ext4.getScrollBarWidth(); + } + if (this.textField.triggerWrap) { + right += this.textField.getTriggerWidth(); + } + return { + right: right, + top: top + }; + }, + + /** + * Checks if the field we are attached to currently has a scrollbar + */ + fieldHasScrollBar: function() { + if (!this.isTextArea) { + return false; + } + + var inputEl = this.textField.inputEl; + var overflowY = inputEl.getStyle('overflow-y'); + if (overflowY == 'hidden' || overflowY == 'visible') { + return false; + } + if (overflowY == 'scroll') { + return true; + } + //noinspection RedundantIfStatementJS + if (inputEl.dom.scrollHeight <= inputEl.dom.clientHeight) { + return false; + } + return true; + }, + + + /** + * Small wrapper around clearButtonEl.isVisible() to handle setVisible animation that may still be in progress. + */ + isButtonCurrentlyVisible: function() { + if (this.animateClearButton && this.animateWithCss3) { + return this.clearButtonEl.hasCls(this.clearButtonCls + '-on'); + } + + // This should not be necessary (see Element.setVisible/isVisible), but else there is confusion about visibility + // when moving the mouse out and _quickly_ over then input again. + /* + var cachedVisible = Ext.core.Element.data(this.clearButtonEl.dom, 'isVisible'); + if (typeof(cachedVisible) == 'boolean') { + return cachedVisible; + } + */ + return this.clearButtonEl.isVisible(); + }, + + /** + * Checks config options and current mouse status to determine if the clear button should be visible. + */ + shouldButtonBeVisible: function() { + if (this.hideClearButtonWhenEmpty && Ext4.isEmpty(this.textField.getValue())) { + return false; + } + + var clearButtonEl = this.clearButtonEl; + //noinspection RedundantIfStatementJS + if (this.hideClearButtonWhenMouseOut + && !clearButtonEl.hasCls(this.clearButtonCls + '-mouse-over-button') + && !clearButtonEl.hasCls(this.clearButtonCls + '-mouse-over-input')) { + return false; + } + + return true; + }, + + /** + * Called after any event that may influence the clear button visibility. + */ + updateClearButtonVisibility: function() { + var oldVisible = this.isButtonCurrentlyVisible(); + var newVisible = this.shouldButtonBeVisible(); + + var clearButtonEl = this.clearButtonEl; + if (oldVisible != newVisible) { + if(this.animateClearButton && this.animateWithCss3) { + this.clearButtonEl.removeCls(this.clearButtonCls + (oldVisible ? '-on' : '-off')); + clearButtonEl.addCls(this.clearButtonCls + (newVisible ? '-on' : '-off')); + } + else { + clearButtonEl.stopAnimation(); + clearButtonEl.setVisible(newVisible, this.animateClearButton); + } + + // Set background-color of clearButton to same as field's background-color (for those browsers/cases + // where the padding-right (see below) does not work) + clearButtonEl.setStyle('background-color', this.textField.inputEl.getStyle('background-color')); + + // Adjust padding-right of the input tag to make room for the button + // IE (up to v9) just ignores this and Gecko handles padding incorrectly with textarea scrollbars + if (!(this.isTextArea && Ext4.isGecko) && !Ext4.isIE) { + // See https://bugzilla.mozilla.org/show_bug.cgi?id=157846 + var deltaPaddingRight = clearButtonEl.getWidth() - this.clearButtonEl.getMargin('l'); + var currentPaddingRight = this.textField.inputEl.getPadding('r'); + var factor = (newVisible ? +1 : -1); + this.textField.inputEl.dom.style.paddingRight = (currentPaddingRight + factor * deltaPaddingRight) + 'px'; + } + } + }, + + isLeftButton: function(event) { + return event.button === 0; + } + + }); + +})(); diff --git a/src/web/OpenCyto/ux/ClearButton/ClearButtonDemoPanel.js b/src/web/OpenCyto/ux/ClearButton/ClearButtonDemoPanel.js new file mode 100644 index 0000000..e6cf1dd --- /dev/null +++ b/src/web/OpenCyto/ux/ClearButton/ClearButtonDemoPanel.js @@ -0,0 +1,66 @@ +(function() { + Ext.define('app.widgets.ClearButtonDemoPanel', { + extend: 'Ext.form.Panel', + + constructor: function(config) { + var comboStore = Ext.create('Ext.data.ArrayStore', { + fields: [ + {name: 'value'} + ], + data: [ + ['male'], + ['female'], + ['not sure'] + ] + }); + + var defaultConfig = { + frame:true, + title: 'Clear Button Demo Form', + width: 260, + height: 200, + defaultType: 'textfield', + fieldDefaults: { + labelWidth: 80 + }, + items:[ + { + xtype: 'textfield', + fieldLabel: 'Search', + emptyText: 'Enter search term', + plugins: new Ext.ux.form.field.ClearButton({animateClearButton: false}), + value: 'clear button w/o animation' + }, + { + xtype: 'combo', + plugins: ['clearbutton'], + fieldLabel: 'Select', + displayField: 'value', + store: comboStore, + queryMode: 'local', + typeAhead: true, + value: 'not sure' + }, + { + xtype: 'datefield', + fieldLabel: 'Best Before', + plugins: ['clearbutton'], + value: new Date() + }, + { + xtype: 'textarea', + fieldLabel: 'Notes', + plugins: ['clearbutton'], + value: 'The shincha season, depending upon the region of the plantation, is from early April to late May (around the 88th day after the spring equinox). It is considered that the ideal color of the sencha beverage is a greenish golden color.' + } + ] + }; + + Ext.applyIf(config, defaultConfig); + + this.callParent(arguments); + } + } + ); + +})(); diff --git a/src/web/OpenCyto/ux/ClearButton/clear-text-icon.gif b/src/web/OpenCyto/ux/ClearButton/clear-text-icon.gif new file mode 100644 index 0000000..697c95a Binary files /dev/null and b/src/web/OpenCyto/ux/ClearButton/clear-text-icon.gif differ diff --git a/src/web/OpenCyto/ux/ColumnAutoWidthPlugin.js b/src/web/OpenCyto/ux/ColumnAutoWidthPlugin.js new file mode 100644 index 0000000..015131b --- /dev/null +++ b/src/web/OpenCyto/ux/ColumnAutoWidthPlugin.js @@ -0,0 +1,190 @@ +/* + * From: + * http://dnorman.github.com/ExtJS-Patches/example/ColumnAutoWidthPlugin.html + */ +(function(){ + +var squishCls = Ext4.id(null,'colAutoWidth'); + +Ext4.util.CSS.createStyleSheet([ + 'table.' + squishCls + '{ table-layout: auto !important; width: auto !important; }', + 'table.' + squishCls + ' .x-grid-header-row th { width: auto !important; }', + 'table.' + squishCls + ' .x-grid-cell { width: auto !important; }', + '.x-grid-header-ct.' + squishCls + ' .x-column-header { width: auto !important; }' +].join(''), squishCls ); + +Ext4.define('Ext.ux.ColumnAutoWidthPlugin', { + alias: 'plugin.ux.columnautowidth', + extend: 'Ext.AbstractPlugin', + mixins: {observable: 'Ext.util.Observable'}, + autoUpdate: true, + allColumns: false, + constructor: function(config) { + var me = this; + Ext4.apply(me, config); + + me.viewChangeDT = new Ext4.util.DelayedTask(function(){ me.refresh() }); + me.addEvents('beforecolumnresize', 'columnresize'); + me.mixins.observable.constructor.call(me); + + }, + init: function(grid) { + var me = this; + me.disable(); + me.grid = grid; + grid.columnAutoWidthPlugin = me; + me.enable(); + }, + destroy: function() { + this.clearListeners(); + }, + enable: function() { + var me = this, grid = me.grid; + + if (me.autoUpdate && me.disabled && grid){ + var view = grid.getView(); + + me.mon(view, 'refresh', me.onViewChange, me ); + me.mon(view, 'itemadd', me.onViewChange, me ); + me.mon(view, 'itemremove', me.onViewChange, me ); + me.mon(view, 'itemupdate', me.onViewChange, me ); + me.mon(view, 'afteritemexpand', me.onViewChange, me ); + + me.mon(grid, 'columnshow', me.onColumnChange, me);//, { buffer: 100 }); + } + + me.callParent(); + }, + disable: function() { + this.clearManagedListeners(); + this.callParent(); + }, + suspend: function(){ + this.suspendAutoSize = true; + }, + resume: function(refresh){ + this.suspendAutoSize = false; + if ( refresh ) this.viewChangeDT.delay(300); + }, + onColumnChange: function(ct, column) { + if( this.suspendAutoSize ) return; + console.log('ColumnAutoWidthPlugin','onColumnChange'); + if ( column.autoWidth ) this.doAutoSize([column]); + }, + onViewChange: function() { + this.viewChangeDT.delay(300); + }, + refresh: function() { + var me = this, grid = me.grid; + if ( me.suspendAutoSize || !grid.rendered || !grid.getView().rendered ) return; + + if( grid.view.isExpandingOrCollapsing ){ + me.viewChangeDT.delay(300); + return; + } + + var cols = me.getAutoCols(); + if ( cols.length ) me.doAutoSize( cols ); + }, + getAutoCols: function(){ + var me = this, cols = me.grid.columns, out = [], i = cols.length, col; + + while(i--){ + col = cols[i]; + if ( me.allColumns || col.autoWidth ) out.push(col); + } + + return out; + }, + getTableResizers: function() { + var els = this.grid.getView().getEl().query( '.' + Ext4.baseCSSPrefix + 'grid-table-resizer'); + + // Grouping feature - first table wraps everything and can be ignored + if (els.length > 1 && Ext4.fly(els[0]).contains(els[1])) { + els.shift(); + } + + return els; + }, + getColumnResizers: function(column, config) { + // Grab the rows (one per table) that are used to size the columns + var els = this.grid.getEl().query( '.' + Ext4.baseCSSPrefix + 'grid-col-resizer-' + column.id); + + // Grouping feature - first table wraps everything and needs to be ignored + if (els.length > 1 && Ext4.fly(els[0]).parent('table').contains(els[1])) { + els.shift(); + } + + return els; + }, + getHeaderWidth: function(column){ + //var el = this.grid.el.down( '#' + column.id + ' .' + Ext4.baseCSSPrefix + 'column-header-inner' ); + if(column.el){ + return column.el.getTextWidth() + column.el.getFrameWidth('lr'); + } + + return 0; + }, + doAutoSize: function( resizeCols ){ + var me = this, + view = me.grid.getView(); + + if( me.suspendAutoSize ) return; + + var start = new Date().getTime(); + var restoreScroll = me.grid.getEl().cacheScrollValues(); + + Ext4.batchLayouts(function(){ + + me.grid.headerCt.el.addCls( squishCls ); + var tableResizers = me.getTableResizers() + // set the table resizers to auto + Ext4.each( tableResizers , function(el) { + el = Ext4.fly(el).addCls( squishCls ); + }); + // console.log('ColumnAutoWidthPlugin','autofy table resizers took', 0-start + (start = new Date().getTime()), 'ms'); + + // no further dom changes beyond this point - to avoid reflows + + // console.log('ColumnAutoWidthPlugin','autofy column resizers took', 0-start + (start = new Date().getTime()), 'ms'); + + Ext4.each(resizeCols, function(col){ + var els = me.getColumnResizers(col), newWidth = 0; + + if( col.el ) newWidth = Ext4.num(col.el.dom.scrollWidth,0); + + Ext4.each(els, function(el) { + newWidth = Math.max(el.scrollWidth, newWidth); // scrollwidth should be cheaper + }); + + newWidth = Math.max( newWidth , col.minAutoWidth || 0 ); + if( col.maxAutoWidth ) newWidth = Math.min(col.maxAutoWidth, newWidth ); + + if( newWidth == col.width ){ + // + } else if( col.el ){ + col.setWidth( newWidth ); + } else { + col.width = newWidth; + } + }); + + // console.log('ColumnAutoWidthPlugin','measure and set took', 0-start + (start = new Date().getTime()), 'ms'); + + // put the table resizers back how you found them + + me.grid.headerCt.el.removeCls( squishCls ); + Ext4.each( tableResizers , function(el) { + el = Ext4.fly(el).removeCls( squishCls ); + }); + //console.log('ColumnAutoWidthPlugin','restore table layout took', 0-start + (start = new Date().getTime()), 'ms'); + }); + + restoreScroll(); + console.log('ColumnAutoWidthPlugin','doAutoSize took', 0-start + (start = new Date().getTime()), 'ms'); + + } + +}); + +})(); \ No newline at end of file diff --git a/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.css b/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.css new file mode 100644 index 0000000..83b10c8 --- /dev/null +++ b/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.css @@ -0,0 +1,104 @@ +/** + * http://www.sencha.com/forum/showthread.php?195773-Ext.ux.ComboFieldBox-intuitive-multi-select-combobox-for-4.1&p=78026 + * https://github.com/krikrou/ComboFieldBox + * + * @var {color} $base-color + * The base color to be used throughout the theme. + */ +/* line 7, ../../scss/form/combofieldbox.scss */ +.x-form-field.x-boxselect { + padding: 0px 3px; +} + +/* line 11, ../../scss/form/combofieldbox.scss */ +.x-boxselect ul.x-boxselect-list { + height: auto; + cursor: text; + min-height: 20px; + padding: 0px; + overflow-y: auto; + overflow-x: hidden; +} +/* line 19, ../../scss/form/combofieldbox.scss */ +.x-boxselect input { + border: none; + background: none; + line-height: 18px; + height: 20px; +} + +/* line 26, ../../scss/form/combofieldbox.scss */ +.x-boxselect-input, .x-boxselect-item { + display: inline-block; + position: relative; + *display: inline; + /* IE7 */ + zoom: 1; + /* IE */ +} + +/* line 32, ../../scss/form/combofieldbox.scss */ +.x-boxselect-stacked .x-boxselect-item { + display: block; +} + +/* line 35, ../../scss/form/combofieldbox.scss */ +.x-boxselect-item { + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + -o-border-radius: 6px; + -ms-border-radius: 6px; + -khtml-border-radius: 6px; + border-radius: 6px; + border: 1px solid #8fafe0; + padding: 0px 1px 0px 5px !important; + margin: 1px 1px 1px 0; + background-color: #dfe8f6; + cursor: default; +} + +/* line 43, ../../scss/form/combofieldbox.scss */ +.x-field:not(.x-item-disabled) .x-boxselect-item:hover { + background: #b7cbeb; + border: 1px solid #6892d4; +} + +/* line 49, ../../scss/form/combofieldbox.scss */ +.x-field:not(.x-item-disabled) .x-boxselect-item.x-item-selected { + background: #6892d4; + color: white; + border: 1px solid #0557be !important; +} + +/* line 56, ../../scss/form/combofieldbox.scss */ +.x-boxselect-item-text { + line-height: 16px; + padding-right: 20px; +} + +/* line 60, ../../scss/form/combofieldbox.scss */ +.x-boxselect-item-close { + cursor: pointer; +} + +/* line 63, ../../scss/form/combofieldbox.scss */ +.x-boxselect .empty { + color: gray; +} + +/* line 66, ../../scss/form/combofieldbox.scss */ +.x-boxselect-icon, .x-boundlist-icon { + background-repeat: no-repeat !important; + background-position: 2px center !important; +} + +/* line 70, ../../scss/form/combofieldbox.scss */ +.x-boxselect-icon { + text-indent: 17px !important; +} + +/* line 73, ../../scss/form/combofieldbox.scss */ +.x-boundlist-icon { + text-indent: 20px !important; +} + diff --git a/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.js b/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.js new file mode 100644 index 0000000..d40b0a5 --- /dev/null +++ b/src/web/OpenCyto/ux/ComboFieldBox/ComboFieldBox.js @@ -0,0 +1,370 @@ +/** + * ComboFieldBox + * + * http://www.sencha.com/forum/showthread.php?195773-Ext.ux.ComboFieldBox-intuitive-multi-select-combobox-for-4.1&p=78026 + * https://github.com/krikrou/ComboFieldBox + */ +Ext4.define('Ext4.ux.ComboFieldBox', { + extend : 'Ext.form.field.ComboBox', + alias : 'widget.combofieldbox', + //requires: ['Ext4.ux.ComboView'], + multiSelect: true, + /** + * @cfg + * maximum height for inputEl. + */ + maxHeight: 150, + /** + * @cfg + * name of field used for description/tooltip + */ + descField: null, + /** + * @cfg + * config object passed to the view + * viewCfg: {}, + */ + /** + * @cfg {String} iconClsField + * The underlying iconCls field name to bind to this ComboBox. + * iconClsField: '', + */ + /** + * @cfg {Boolean} createNewOnEnter + * When forceSelection is false, new records can be created by the user. This configuration + * option has no effect if forceSelection is true, which is the default. + */ + createNewOnEnter: false, + /** + * @cfg {Boolean} forceSelection + * override parent config. If force selection is set to false and + */ + forceSelection: true, + /** + * @cfg {Boolean} selectOnTab + * Whether the Tab key should select the currently highlighted item. + */ + selectOnTab : false, + /** + * @cfg {String} trigger1Cls + * css class for the first trigger. To have just one trigger acting like in usual combo, set trigger1Cls to null. First trigger clears all values + */ + trigger1Cls : Ext4.baseCSSPrefix + 'form-clear-trigger', + /** + * @cfg {String} trigger2Cls + * css class for the second trigger. To have just one trigger, set trigger1Cls to null. + */ + trigger2Cls : Ext4.baseCSSPrefix + 'form-combo-trigger', + + /** + * @cfg {String} listIconCls + * css class to use when an iconClsField is set. This class is injected into getInnerTpl method when constructing the comboBox boundList + */ + listIconCls : 'x-boundlist-icon', + /** + * @cfg {String} delimiter + * string delimiter. default ' ' overrides parent class ', '. Used in setvalue (if value is a string) to split values. + */ + delimiter: ' ', + fieldSubTpl: [ + '', + '
    readonly="readonly"', + ' disabled="disabled"', + ' tabIndex="{tabIdx}"', + ' name="{name}"', + ' style="{fieldStyle}"', + ' placeholder="{placeholder}"', + ' size="{size}"', + 'class="{fieldCls} {typeCls} x-boxselect" autocomplete="off" />', + '
    ', + { + compiled: true, + disableFormats: true + } + ], + getSubTplData: function () { + var me = this, + fieldStyle = me.getFieldStyle(), + ret = me.callParent(arguments); + ret.fieldStyle = (fieldStyle || '') + ';overflow:auto;height:'+ (me.height ? (me.height + 'px;') : 'auto;') + (me.maxHeight ? ('max-height:' + me.maxHeight + 'px;') : ''); + delete me.height; //need to delete height for the correct component height to be recalculated on layout. + return ret; + }, + alignPicker: function () { + var me = this, + picker = me.getPicker(), + w = me.triggerWidth; + me.callParent(arguments); + if (me.isExpanded && me.matchFieldWidth) { + picker.setWidth(me.bodyEl.getWidth() - (me.trigger2Cls ? (2 * w) : w)); + } + }, + initComponent: function () { + var me = this; + if(!me.trigger1Cls) { + me.onTrigger1Click = null; + me.trigger2Cls = null; + } + me.bindStore(me.store || 'ext-empty-store', true); + me.getValueStore(); + var selModel = me.multiSelect ? {selModel: {mode: 'SIMPLE', enableKeyNav: false}} : {selModel: {mode: 'SINGLE',enableKeyNav: false}}; + me.listConfig = Ext4.apply(me.listConfig || {}, selModel); + if(me.iconClsField || me.descField) { + Ext4.apply(me.listConfig, { + getInnerTpl: function(displayField) { + return '
    {' + me.displayField +'}
    '; + } + }); + } + me.rawValue = me.value; + me.callParent(arguments); + }, + onTrigger1Click : function() { + var me = this; + me.setValue(""); + me.collapse(); + }, + setValueStore: function(store) { + this.valueStore = store; + }, + getValueStore: function() { + var me = this; + return me.valueStore || (me.valueStore = me.createValueStore()); + }, + createValueStore: function() { + return this.valueStore = new Ext4.data.Store({ + model: this.store.model + }); + }, + /** + * get all field values from value store and re-set combobox values + */ + setStoreValues: function() { + var me = this, + st = me.getValueStore(); + me.setValue(st.data.extractValues(me.valueField || st.valueField, 'data')); + me.syncSelection(); + }, + getValueModels: function () { + return this.valueModels || []; + }, + afterSetValue: function (){ + var me = this; + me.valueStore.removeAll(); + me.valueStore.add(me.getValueModels()); + if (me.isExpanded) { + me.alignPicker(); + } + me.syncSelection(); + me.updateLayout(); + }, + assertValue: Ext4.emptyFn, + setValue: function (value, action) { + var me = this, + st = me.store; + if(Ext4.isString(value)) {value = value.split(me.delimiter)}; + if(me.tempValue) { + var picker = me.getPicker(), + oldPr = picker.preserveScrollOnRefresh; + value = Ext4.Array.unique(value.concat(me.tempValue)); + var val = me.store.data.extractValues(me.valueField, 'data'); + if(me.typeAhead && (me.store.getCount() == 1)) { + var v = me.store.getAt(0).get(me.valueField); + me.tempMulti != true ? value = [v] : value.push(v); + me._needCollapse = true; + } + me.store.data.addAll(Ext4.Array.filter(me.valueStore.data.items, function(i) {return (Ext4.Array.indexOf(val,i.data[me.valueField]) < 0)})); + picker.preserveScrollOnRefresh = true; + if(me.picker.refresh) {me.picker.refresh()} + picker.preserveScrollOnRefresh = oldPr; + } + me.callParent([value, false]); + me.value = value; // need to reset the value here: in case the store is not yeat loaded and multiSelect == true, me.value is set to [] during the callParent. + if(st.getCount() > 0 ) {return me.afterSetValue()} + if(!st.isLoading() && me._isStoreLoadCalled !== true) { + st.load(); + me._isStoreLoadCalled = true; + } + st.on('load', me.afterSetValue, me, {single: true}); + }, + getRawValue: function () { + return Ext4.value(this.rawValue, ''); + }, + doRawQuery: function() { + var me = this, + qe; + if(me.view && me.typeAhead && (qe = me.view.inputEl.getValue())) { + me.tempValue = me.value; + me.tempMulti = me.multiSelect; + me.multiSelect = true; + this.doQuery(qe, false, true); + me.multiSelect = me.tempMulti; + delete me.tempMulti; + if(me._needCollapse){ + me.collapse(); + delete me._needCollapse; + } + else { + me.onExpand(); + me._preventClear = true; + me.view.inputEl.focus(); + me.view.inputEl.dom.value=qe; + delete me._preventClear; + } + delete me.tempValue; + } + }, + onBlur: function() { + var me = this; + me.view.inputEl.dom.value =''; + me.view.inputEl.setWidth(10); + if(me.view.emptyEl) {me.view.emptyEl.show()}; + if(me.picker && me.isExpanded && me._preventCollapse != true) {me.onTriggerClick()}; + }, + onFocus: function() { + var me = this, + view = me.view; + me.callParent(arguments); + view.inputEl.setWidth(view.inputWidth); + if(me._preventClear != true) { + me.store.clearFilter(); + if(me.picker && me.picker.refresh) {me.picker.refresh()} + } + if(view.emptyEl) { + view.emptyEl.setVisibilityMode(Ext4.dom.AbstractElement.DISPLAY); + view.emptyEl.hide() + } + me.view.focus(); + }, + + buildKeyNav: function() { + var me = this, + selectOnTab = me.selectOnTab, + picker = me.getPicker(); + return new Ext4.view.BoundListKeyNav(picker.el, { + boundList: picker, + forceKeyDown: true, + tab: function(e) { + if (selectOnTab || (me.typeAhead && (me.view.inputEl.dom.value || me.tempValue)) ) { + this.selectHighlighted(e); + } + me.onTriggerClick(); + return true + }, + esc: function(e) { + me.onTriggerClick() + } + }); + }, + onExpand: function() { + var me = this, + keyNav = me.listKeyNav, + selectOnTab = me.selectOnTab, + node, + picker = me.getPicker(); + // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab. + if(!keyNav){ keyNav = me.listKeyNav = me.buildKeyNav(); } + // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab + if (selectOnTab) { + me.ignoreMonitorTab = true; + } + Ext4.defer(keyNav.enable, 3, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker + me.highlightFirstNode() + + }, + highlightFirstNode: function(select) { + var me = this, + picker = me.getPicker(); + me._preventCollapse = true; + picker.focus(); + delete me.preventCollapse; + if(picker.getNode && (node = picker.getNode(0))){ + picker.highlightItem(node); + } + + }, + onCollapse: function() { + var me = this; + me.callParent(arguments); + me.view.focus(); + }, + createRecord: function(rawValue) { + var me= this, rec = {}, val = rawValue.split(me.delimiter).join('') ; + rec[me.valueField] = rawValue; + rec[me.displayField] = rawValue; + return rec + }, + afterComponentLayout : function() { + var me = this; + me.callParent(arguments); + if (!me.view) { + var selectBoxOnTab = me.selectBoxOnTab, + del = function(e) { + if(me.readOnly || me.disabled || !me.editable || me.view.inputEl.dom.value ) {return} + var selected = selModel.getSelection()[0]; + if(selected) { + var idx = Ext4.Array.indexOf(me.view,me.view.getNode(selected)); + selModel.onNavKey.call(selModel, 1); + me.getValueStore().remove(selected); + me.setStoreValues(); + selModel.select(idx); + me.view.focus() + } + return true; + }; + me.view = new Ext4.ux.ComboView(Ext4.apply({ + store: me.valueStore, + emptyText: me.emptyText || '', + field: me, + renderTo: me.inputEl + }, me.viewCfg)); + var selModel = me.view.selModel; + var boxKeyNav= me.boxKeyNav = new Ext4.view.BoundListKeyNav(me.view.el, { + boundList: me.view, + forceKeyDown: true, + down : function(e) { + if(me.isExpanded && me.view.inputEl.getValue()) {return me.picker.focus()} + me.onTriggerClick(); + }, + right: function(e) { + selModel.onNavKey.call(selModel, +1) + }, + left: function(e) { + selModel.onNavKey.call(selModel, -1) + }, + enter: function(e) { + if(me.readOnly || me.disabled || !me.editable) {return} + if (me.multiSelect && me.createNewOnEnter == true && e.getKey() == e.ENTER && (rawValue = e.target.value) && (!Ext4.isEmpty(rawValue))) { + var rec = me.store.findExact(me.valueField, rawValue); + if(rec < 0) { + rec= me.store.add(me.createRecord(rawValue)); + } + me.getValueStore().add(rec); + me.setStoreValues(); + } + me.view.focus(); + }, + tab: function(e) { + if(me.isExpanded && e.target.value ){ + me.highlightFirstNode(); + if(me.typeAhead) { + me.listKeyNav.selectHighlighted(e) + } + } + return true + }, + del: del, + space: del + }); + Ext4.defer(boxKeyNav.enable, 1, boxKeyNav); + } + }, + onDestroy: function() { + var me = this; + if(me.view) { + Ext4.destroy(me.view, me.boxKeyNav); + } + me.callParent(arguments); + } +}); diff --git a/src/web/OpenCyto/ux/ComboFieldBox/ComboView.js b/src/web/OpenCyto/ux/ComboFieldBox/ComboView.js new file mode 100644 index 0000000..0c1ae9a --- /dev/null +++ b/src/web/OpenCyto/ux/ComboFieldBox/ComboView.js @@ -0,0 +1,135 @@ +/** + * http://www.sencha.com/forum/showthread.php?195773-Ext.ux.ComboFieldBox-intuitive-multi-select-combobox-for-4.1&p=78026 + * https://github.com/krikrou/ComboFieldBox* + * ComboView + */ +Ext4.define('Ext4.ux.ComboView', { + extend : 'Ext.view.View', + alias : 'widget.comboview', + /** + * @cfg {Boolean} maxLength + * maximum length for viewItems. If text is longer, it gets 'ellipsisied'. + */ + maxLength: 18, + /** + * @cfg {Boolean} removeOnDblClick + * true to deselect viewItem on double click + */ + removeOnDblClick: true, + /** + * @cfg {Boolean} inputWidth + * width for the inputfield + */ + inputWidth: 40, + itemSelector: 'li.x-boxselect-item', + closeCls: 'x-boxselect-item-close', + /** + * Set Xtemplate fot the ComboView (called if me.tpl is not existing) + * @returns {Ext.XTemplate} Returns template + */ + setTpl: function() { + var me = this, + field = me.field, + displayField = field.displayField, + descField = field.descField, + iconClsField = field.iconClsField; + me.tpl = new Ext4.XTemplate( + '', { + compiled: true, + disableFormats: true, + length: me.maxLength, + ellipsis: function (txt) { + return Ext4.String.ellipsis(txt, this.length) + }, + emptyText: me.emptyText, + empty : function(values) { + return '' + (values.length ? '' : this.emptyText )+ '' + } + }); + delete me.emptyText; + return me.tpl; + }, + initComponent: function () { + var me = this; + if (!me.tpl) {me.tpl= me.setTpl()} + if (!me.selModel) { + me.selModel = {enableKeyNav: false}; + } + me.callParent(arguments) + }, + renderSelectors: { + inputEl: 'input', + emptyEl: 'span.empty' + }, + getFocusEl: function () { + return this.inputEl + }, + addFocusListener: function (force) { + var me = this, focusEl; + if (!me.focusListenerAdded) { + me.callParent(); // force argument only valid in ComboView + me.field.el.on({ + click: me.field.onFocus, + scope: me.field + }) + } + if ((focusEl = me.getFocusEl()) && force) { + focusEl.on({ + focus: me.field.onFocus, + blur: me.field.onBlur, + scope: me.field + }); + } + }, + onItemClick: function (r, h, i, e, o) { + if (e.getTarget('.' + this.closeCls)) { + return this.onDataChange(r, 'remove') + } + this.highlightItem(h) + }, + onItemDblClick: function (r, h, i, e, o) { + if (this.removeOnDblClick) { + this.onDataChange(r, 'remove') + } + }, + onDataChange: function (r, action) { + var me = this; + if(me.field.readOnly || me.field.disabled) {return} + if (action == 'remove') { + me.store.remove(r) + } + me.field.setStoreValues() + }, + listeners: { + refresh: { + fn: function () { + var me = this; + this.applyRenderSelectors(); + this.addFocusListener(this); + } + } + }, + onDestroy: function () { + var me = this, + focusEl; + if (focusEl = me.getFocusEl()) { + focusEl.clearListeners() + } + } +/*::::*/ +}); + + + diff --git a/src/web/OpenCyto/ux/SelectedCount.css b/src/web/OpenCyto/ux/SelectedCount.css new file mode 100644 index 0000000..e614fd2 --- /dev/null +++ b/src/web/OpenCyto/ux/SelectedCount.css @@ -0,0 +1,20 @@ +.chkbox { + height: 13px; + width: 13px; + background: black url('../../ext-4.1.0/resources/themes/images/default/form/checkbox.gif') no-repeat -13px 0; + float: left; + margin: 0 5px 0 0; +} +.x4-boundlist-selected .chkbox { + background-position: -13px -13px; +} + +.sel-all { + padding: 3px; + background-color: lemonchiffon; + padding-left: 10px; + cursor: pointer; + border: 1px solid gray; + border-radius: 5px; +} + diff --git a/src/web/OpenCyto/ux/SelectedCount.js b/src/web/OpenCyto/ux/SelectedCount.js new file mode 100644 index 0000000..1336c78 --- /dev/null +++ b/src/web/OpenCyto/ux/SelectedCount.js @@ -0,0 +1,68 @@ +/** + * http://stackoverflow.com/questions/14890330/select-all-in-extjs-4-0-combobox + */ +Ext4.define('comboSelectedCount', { + alias : 'plugin.selectedCount', + init : function(combo) { + var fl = combo.getFieldLabel(), allSelected = false, id = combo.getId() + '-toolbar-panel'; + + Ext4.apply(combo, { + listConfig : { + tpl : new Ext4.XTemplate('
    {' + combo.displayField + '}
    ') + } + }); + var toolbar = Ext4.create('Ext.toolbar.Toolbar', { + items : [{ + text : 'Select all', + icon : '../../ext-4.1.0/resources/themes/images/default/menu/checked.gif', + handler : function(btn, e) { + if (!allSelected) { + combo.select(combo.getStore().getRange()); + combo.setSelectedCount(combo.getStore().getRange().length); + btn.setText('Deselect all...'); + allSelected = true; + } else { + combo.reset(); + btn.setText('Select all...'); + allSelected = false; + } + e.stopEvent(); + } + }, '-', { + xtype : 'textfield', + enableKeyEvents : true, + emptyText : 'enter search term', + listeners : { + keyup : function(field, e) { + combo.getStore().clearFilter(); + if (field.getValue()) { + // S.E.L - Modified to support inner substring search + var re = new RegExp(field.getValue(), "i"); + combo.getStore().filter(combo.displayField, re); + } + } + } + }] + }); + combo.on({ + select : function(me, records) { + var len = records.length, store = combo.getStore(); + combo.setSelectedCount(len); + }, + beforedeselect : function(me, record, index) { + me.setFieldLabel(fl); + }, + expand : { + fn : function() { + var dropdown = Ext4.get(id).dom.parentElement; + var container = Ext4.DomHelper.insertBefore(dropdown, '
    ', true); + toolbar.render(container); + }, + single : true + } + }); + combo.setSelectedCount = function(count) { + combo.setFieldLabel(fl + ' (' + count + ' selected)'); + } + } +}); \ No newline at end of file diff --git a/src/web/OpenCyto/ux/images/clear-text-icon.gif b/src/web/OpenCyto/ux/images/clear-text-icon.gif new file mode 100644 index 0000000..697c95a Binary files /dev/null and b/src/web/OpenCyto/ux/images/clear-text-icon.gif differ diff --git a/src/web/OpenCyto/white_85_trans.png b/src/web/OpenCyto/white_85_trans.png new file mode 100644 index 0000000..8c2a064 Binary files /dev/null and b/src/web/OpenCyto/white_85_trans.png differ