tweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloud
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
8.5 KiB
302 lines
8.5 KiB
/** |
|
* A GridPanel class with live search support. |
|
* @author Nicolas Ferrero |
|
*/ |
|
Ext.define('Ext.ux.LiveSearchGridPanel', { |
|
extend: 'Ext.grid.Panel', |
|
requires: [ |
|
'Ext.toolbar.TextItem', |
|
'Ext.form.field.Checkbox', |
|
'Ext.form.field.Text', |
|
'Ext.ux.statusbar.StatusBar' |
|
], |
|
|
|
/** |
|
* @private |
|
* search value initialization |
|
*/ |
|
searchValue: null, |
|
|
|
/** |
|
* @private |
|
* The matched positions from the most recent search |
|
*/ |
|
matches: [], |
|
|
|
/** |
|
* @private |
|
* The current index matched. |
|
*/ |
|
currentIndex: null, |
|
|
|
/** |
|
* @private |
|
* The generated regular expression used for searching. |
|
*/ |
|
searchRegExp: null, |
|
|
|
/** |
|
* @private |
|
* Case sensitive mode. |
|
*/ |
|
caseSensitive: false, |
|
|
|
/** |
|
* @private |
|
* Regular expression mode. |
|
*/ |
|
regExpMode: false, |
|
|
|
/** |
|
* @cfg {String} matchCls |
|
* The matched string css classe. |
|
*/ |
|
matchCls: 'x-livesearch-match', |
|
|
|
defaultStatusText: 'Nothing Found', |
|
|
|
// Component initialization override: adds the top and bottom toolbars and setup headers renderer. |
|
initComponent: function() { |
|
var me = this; |
|
|
|
me.tbar = ['Search',{ |
|
xtype: 'textfield', |
|
name: 'searchField', |
|
hideLabel: true, |
|
width: 200, |
|
listeners: { |
|
change: { |
|
fn: me.onTextFieldChange, |
|
scope: this, |
|
buffer: 500 |
|
} |
|
} |
|
}, { |
|
xtype: 'button', |
|
text: '<', |
|
tooltip: 'Find Previous Row', |
|
handler: me.onPreviousClick, |
|
scope: me |
|
},{ |
|
xtype: 'button', |
|
text: '>', |
|
tooltip: 'Find Next Row', |
|
handler: me.onNextClick, |
|
scope: me |
|
}, '-', { |
|
xtype: 'checkbox', |
|
hideLabel: true, |
|
margin: '0 0 0 4px', |
|
handler: me.regExpToggle, |
|
scope: me |
|
}, 'Regular expression', { |
|
xtype: 'checkbox', |
|
hideLabel: true, |
|
margin: '0 0 0 4px', |
|
handler: me.caseSensitiveToggle, |
|
scope: me |
|
}, 'Case sensitive']; |
|
|
|
me.bbar = new Ext.ux.StatusBar({ |
|
defaultText: me.defaultStatusText, |
|
name: 'searchStatusBar' |
|
}); |
|
|
|
me.callParent(arguments); |
|
}, |
|
|
|
// afterRender override: it adds textfield and statusbar reference and start monitoring keydown events in textfield input |
|
afterRender: function() { |
|
var me = this; |
|
me.callParent(arguments); |
|
me.textField = me.down('textfield[name=searchField]'); |
|
me.statusBar = me.down('statusbar[name=searchStatusBar]'); |
|
|
|
me.view.on('cellkeydown', me.focusTextField, me); |
|
}, |
|
|
|
focusTextField: function(view, td, cellIndex, record, tr, rowIndex, e, eOpts) { |
|
if (e.getKey() === e.S) { |
|
e.preventDefault(); |
|
this.textField.focus(); |
|
} |
|
}, |
|
|
|
// detects html tag |
|
tagsRe: /<[^>]*>/gm, |
|
|
|
// DEL ASCII code |
|
tagsProtect: '\x0f', |
|
|
|
/** |
|
* In normal mode it returns the value with protected regexp characters. |
|
* In regular expression mode it returns the raw value except if the regexp is invalid. |
|
* @return {String} The value to process or null if the textfield value is blank or invalid. |
|
* @private |
|
*/ |
|
getSearchValue: function() { |
|
var me = this, |
|
value = me.textField.getValue(); |
|
|
|
if (value === '') { |
|
return null; |
|
} |
|
if (!me.regExpMode) { |
|
value = Ext.String.escapeRegex(value); |
|
} else { |
|
try { |
|
new RegExp(value); |
|
} catch (error) { |
|
me.statusBar.setStatus({ |
|
text: error.message, |
|
iconCls: 'x-status-error' |
|
}); |
|
return null; |
|
} |
|
// this is stupid |
|
if (value === '^' || value === '$') { |
|
return null; |
|
} |
|
} |
|
|
|
return value; |
|
}, |
|
|
|
/** |
|
* Finds all strings that matches the searched value in each grid cells. |
|
* @private |
|
*/ |
|
onTextFieldChange: function() { |
|
var me = this, |
|
count = 0, |
|
view = me.view, |
|
cellSelector = view.cellSelector, |
|
innerSelector = view.innerSelector, |
|
columns = me.visibleColumnManager.getColumns(); |
|
|
|
view.refresh(); |
|
// reset the statusbar |
|
me.statusBar.setStatus({ |
|
text: me.defaultStatusText, |
|
iconCls: '' |
|
}); |
|
|
|
me.searchValue = me.getSearchValue(); |
|
me.matches = []; |
|
me.currentIndex = null; |
|
|
|
if (me.searchValue !== null) { |
|
me.searchRegExp = new RegExp(me.getSearchValue(), 'g' + (me.caseSensitive ? '' : 'i')); |
|
|
|
me.store.each(function(record, idx) { |
|
var node = view.getNode(record); |
|
|
|
if (node) { |
|
Ext.Array.forEach(columns, function(column) { |
|
var cell = Ext.fly(node).down(column.getCellInnerSelector(), true), |
|
matches, cellHTML, |
|
seen; |
|
|
|
if (cell) { |
|
matches = cell.innerHTML.match(me.tagsRe); |
|
cellHTML = cell.innerHTML.replace(me.tagsRe, me.tagsProtect); |
|
|
|
// populate indexes array, set currentIndex, and replace wrap matched string in a span |
|
cellHTML = cellHTML.replace(me.searchRegExp, function(m) { |
|
++count; |
|
if (!seen) { |
|
me.matches.push({ |
|
record: record, |
|
column: column |
|
}); |
|
seen = true; |
|
} |
|
return '<span class="' + me.matchCls + '">' + m + '</span>'; |
|
}, me); |
|
// restore protected tags |
|
Ext.each(matches, function(match) { |
|
cellHTML = cellHTML.replace(me.tagsProtect, match); |
|
}); |
|
// update cell html |
|
cell.innerHTML = cellHTML; |
|
} |
|
}); |
|
} |
|
}, me); |
|
|
|
// results found |
|
if (count) { |
|
me.currentIndex = 0; |
|
me.gotoCurrent(); |
|
me.statusBar.setStatus({ |
|
text: Ext.String.format('{0} match{1} found.', count, count === 1 ? 'es' : ''), |
|
iconCls: 'x-status-valid' |
|
}); |
|
} |
|
} |
|
|
|
// no results found |
|
if (me.currentIndex === null) { |
|
me.getSelectionModel().deselectAll(); |
|
me.textField.focus(); |
|
} |
|
}, |
|
|
|
/** |
|
* Selects the previous row containing a match. |
|
* @private |
|
*/ |
|
onPreviousClick: function() { |
|
var me = this, |
|
matches = me.matches, |
|
len = matches.length, |
|
idx = me.currentIndex; |
|
|
|
if (len) { |
|
me.currentIndex = idx === 0 ? len - 1 : idx - 1; |
|
me.gotoCurrent(); |
|
} |
|
}, |
|
|
|
/** |
|
* Selects the next row containing a match. |
|
* @private |
|
*/ |
|
onNextClick: function() { |
|
var me = this, |
|
matches = me.matches, |
|
len = matches.length, |
|
idx = me.currentIndex; |
|
|
|
if (len) { |
|
me.currentIndex = idx === len - 1 ? 0 : idx + 1; |
|
me.gotoCurrent(); |
|
} |
|
}, |
|
|
|
/** |
|
* Switch to case sensitive mode. |
|
* @private |
|
*/ |
|
caseSensitiveToggle: function(checkbox, checked) { |
|
this.caseSensitive = checked; |
|
this.onTextFieldChange(); |
|
}, |
|
|
|
/** |
|
* Switch to regular expression mode |
|
* @private |
|
*/ |
|
regExpToggle: function(checkbox, checked) { |
|
this.regExpMode = checked; |
|
this.onTextFieldChange(); |
|
}, |
|
|
|
privates: { |
|
gotoCurrent: function() { |
|
var pos = this.matches[this.currentIndex]; |
|
this.getNavigationModel().setPosition(pos.record, pos.column); |
|
this.getSelectionModel().select(pos.record); |
|
} |
|
} |
|
});
|
|
|