diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index f0d17203c..1a00f019e 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -5,291 +5,321 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var core = require('web.core'); var _t = core._t; var session = require('web.session'); + var pyUtils = require('web.py_utils'); + var Domain = require('web.Domain'); ListRenderer.include({ - events: _.extend({ - 'keyup .oe_search_input': '_onKeyUp', - 'click .oe_clear_search': '_onClearSearch' - }, ListRenderer.prototype.events), + events: _.extend({}, ListRenderer.prototype.events, { + 'keyup .oe_search_input': '_onSearchKeyUp', + 'click .oe_clear_search': '_onClearSearchClick' + }), + /** + * @override + */ _renderView: function () { var self = this; return this._super.apply(this, arguments).then(function () { - self.$('.o_list_table').addClass('o_list_table_ungrouped'); - if (self.arch.tag == 'tree' && self.$el.hasClass('o_list_view')) { - // Check if the search input already exists - if (!self.$el.find('.oe_search_input').length) { - var search = '
' + - '' + - '' + - '' + - '' + - '
'; - self.$el.find('table').addClass('oe_table_search'); - self.$el.prepend($(search)); - - // Initialize search functionality - self._initializeSearch(); - } + // Add search box only to list views + if (self._shouldAddSearchBox()) { + self._addSearchBox(); } }); }, - _initializeSearch: function() { - var self = this; - this._searchTimeout = null; - this._currentSearchValue = ''; - this._isSearching = false; - this._originalRecords = null; - this._searchMode = false; + /** + * Check if we should add search box + */ + _shouldAddSearchBox: function() { + return ( + this.arch && + this.arch.tag === 'tree' && + this.$el && + this.$el.hasClass('o_list_view') && + !this.$el.find('.oe_search_input').length + ); }, - _onKeyUp: function (event) { + /** + * Add search box to the view + */ + _addSearchBox: function() { + var searchHtml = this._getSearchBoxHtml(); + this.$el.prepend($(searchHtml)); + this._initializeSearchState(); + }, + + /** + * Get search box HTML + */ + _getSearchBoxHtml: function() { + return '
' + + '' + + '' + + '' + + '' + + '
'; + }, + + /** + * Initialize search state + */ + _initializeSearchState: function() { + this._searchState = { + timeout: null, + currentValue: '', + isSearching: false, + originalData: null, + lastSearchDomain: null + }; + }, + + /** + * Handle search input keyup + */ + _onSearchKeyUp: function(event) { var self = this; var value = $(event.currentTarget).val(); // Clear previous timeout - if (this._searchTimeout) { - clearTimeout(this._searchTimeout); + if (this._searchState && this._searchState.timeout) { + clearTimeout(this._searchState.timeout); } - // Store current value to prevent clearing - this._currentSearchValue = value; + // Initialize state if needed + if (!this._searchState) { + this._initializeSearchState(); + } - // Add delay to avoid too many requests - this._searchTimeout = setTimeout(function() { - if (!self._isSearching) { - self._performSearch(value); - } + // Store current value + this._searchState.currentValue = value; + + // Debounce search + this._searchState.timeout = setTimeout(function() { + self._executeSearch(value); }, 500); }, - _onClearSearch: function(event) { + /** + * Handle clear button click + */ + _onClearSearchClick: function(event) { this._clearSearch(); }, - _performSearch: function (searchValue) { + /** + * Execute the search + */ + _executeSearch: function(searchValue) { var self = this; - var value = (searchValue || '').toLowerCase().trim(); + var value = (searchValue || '').trim(); + + console.log('=== Starting Search ==='); + console.log('Search value:', value); // Prevent concurrent searches - if (this._isSearching) { + if (this._searchState && this._searchState.isSearching) { + console.log('Search already in progress, skipping...'); return; } - // Show/hide clear button - this.$el.find('.oe_clear_search').toggle(!!value); + // Update UI + this.$('.oe_clear_search').toggle(!!value); + // Clear search if empty if (!value) { this._clearSearch(); return; } - // Show loading indicator - this.$el.find('.oe_search_loading').show(); - this.$el.find('.oe_search_count').hide(); - this._isSearching = true; - - // Store original records if not already stored - if (!this._originalRecords && this.state.data.records) { - this._originalRecords = this.state.data.records.slice(); + // Validate environment + if (!this._validateSearchEnvironment()) { + return; } - // Get model and fields info - var model = this.state.model; - var fields = this._getSearchableFields(); + // Start search + this._searchState.isSearching = true; + this._showLoading(); - if (fields.length === 0) { - this._isSearching = false; - this.$el.find('.oe_search_loading').hide(); - console.warn('No searchable fields found for model:', model); + // Store original data if first search + if (!this._searchState.originalData) { + this._searchState.originalData = this._captureCurrentData(); + } + + // Build and execute search + this._buildAndExecuteSearch(value); + }, + + /** + * Validate that we can perform search + */ + _validateSearchEnvironment: function() { + // Check model + if (!this.state || !this.state.model) { + console.error('No model found in state'); + this._showError(_t('Cannot search: No model information')); + return false; + } + + // Check if we have field information + var fields = this._getFieldsInfo(); + if (!fields) { + console.error('No fields information found'); + this._showError(_t('Cannot search: No fields information')); + return false; + } + + return true; + }, + + /** + * Get fields information from various possible locations + */ + _getFieldsInfo: function() { + // Try different locations where fields might be stored + if (this.state && this.state.fields) { + return this.state.fields; + } + if (this.state && this.state.fieldsInfo && this.state.fieldsInfo.list) { + return this.state.fieldsInfo.list; + } + if (this.fieldsInfo && this.fieldsInfo.list) { + return this.fieldsInfo.list; + } + return null; + }, + + /** + * Capture current data state + */ + _captureCurrentData: function() { + // Try to get current records from various locations + if (this.state && this.state.data) { + return { + records: this.state.data.records ? this.state.data.records.slice() : [], + count: this.state.count || 0, + domain: this.state.domain ? this.state.domain.slice() : [] + }; + } + return null; + }, + + /** + * Build and execute the search + */ + _buildAndExecuteSearch: function(value) { + var self = this; + + // Get searchable fields + var searchableFields = this._identifySearchableFields(); + console.log('Searchable fields:', searchableFields); + + if (!searchableFields || searchableFields.length === 0) { + console.warn('No searchable fields found'); + this._fallbackToClientSearch(value); return; } // Build search domain - var domain = this._buildSearchDomain(value, fields); + var searchDomain = this._constructSearchDomain(value, searchableFields); + console.log('Search domain:', JSON.stringify(searchDomain)); - // Add existing domain if any - var baseDomain = this.state.domain || []; - if (baseDomain.length > 0) { - // Combine base domain with search domain - domain = baseDomain.concat(domain); + if (!searchDomain || searchDomain.length === 0) { + console.warn('Could not build search domain'); + this._fallbackToClientSearch(value); + return; } - // Get only visible and stored fields to avoid computed fields issues - var fieldsToRead = this._getFieldsToRead(); + // Store last search domain + this._searchState.lastSearchDomain = searchDomain; - // Build order string safely - var orderBy = false; - if (this.state.orderedBy && this.state.orderedBy.length > 0 && this.state.orderedBy[0]) { - var order = this.state.orderedBy[0]; - if (order.name) { - orderBy = order.name; - if (order.asc !== undefined) { - orderBy += order.asc ? ' ASC' : ' DESC'; - } - } - } + // Get base domain and combine + var baseDomain = this._getBaseDomain(); + var finalDomain = this._combineDomains(baseDomain, searchDomain); + console.log('Final domain:', JSON.stringify(finalDomain)); - // Perform RPC search with specific fields only - NO LIMIT - this._rpc({ - model: model, - method: 'search_read', - args: [domain], - kwargs: { - fields: fieldsToRead, // Only read necessary fields - limit: false, // No limit - get all records - offset: 0, - context: this.state.context || session.user_context, - order: orderBy - } - }).then(function(result) { - self._updateListWithSearchResults(result, value); - }).catch(function(error) { - console.error('Search error:', error); - self._showError(_t('Search failed. Please try again.')); - }).finally(function() { - self._isSearching = false; - self.$el.find('.oe_search_loading').hide(); - // Restore search value - self.$el.find('.oe_search_input').val(self._currentSearchValue); - }); + // Execute RPC + this._executeRPCSearch(finalDomain, value); }, - _getFieldsToRead: function() { - var self = this; - var fields = ['id']; // Always include ID - - // Get only visible, stored fields from columns - _.each(this.columns, function(column) { - if (!column.invisible && column.attrs && column.attrs.name) { - var fieldName = column.attrs.name; - var field = self.state.fields[fieldName]; - - // Only include stored fields or safe field types - if (field) { - // Skip computed fields unless they're stored - if (field.compute && !field.store) { - return; // Skip this field - } - - // For many2one, we only need the ID and display_name - if (field.type === 'many2one') { - fields.push(fieldName); - } - // For other fields, include if they're stored - else if (field.store !== false) { - fields.push(fieldName); - } + /** + * Get base domain from state + */ + _getBaseDomain: function() { + if (this.state && this.state.domain) { + // Clone the domain to avoid mutations + if (Array.isArray(this.state.domain)) { + return this.state.domain.slice(); + } + if (typeof this.state.domain === 'string') { + try { + return pyUtils.eval('domain', this.state.domain); + } catch (e) { + console.error('Error parsing domain:', e); + return []; } } - }); - - // Always try to include display_name if available - if (self.state.fields.display_name && !fields.includes('display_name')) { - fields.push('display_name'); } - - // Always try to include name if available - if (self.state.fields.name && !fields.includes('name')) { - fields.push('name'); - } - - return _.uniq(fields); + return []; }, - _buildSearchDomain: function(value, fields) { - var domain = []; - var orConditions = []; - - // Normalize search value for Arabic - var normalizedValue = this._normalizeArabic(value); - - _.each(fields, function(field) { - // For char, text, and html fields - if (['char', 'text', 'html'].includes(field.type)) { - orConditions.push([field.name, 'ilike', value]); - // Also search with normalized value if different - if (normalizedValue !== value) { - orConditions.push([field.name, 'ilike', normalizedValue]); - } - } - // For many2one fields - search in the name - else if (field.type === 'many2one') { - // Use the field name directly with ilike - orConditions.push([field.name, 'ilike', value]); - } - // For selection fields - else if (field.type === 'selection') { - orConditions.push([field.name, 'ilike', value]); - } - // For number fields (if value is numeric) - else if (['integer', 'float', 'monetary'].includes(field.type)) { - if (!isNaN(value) && value !== '') { - orConditions.push([field.name, '=', parseFloat(value)]); - } - } - }); - - // Create OR domain - if (orConditions.length > 0) { - // Add '|' operators for OR condition - for (var i = 1; i < orConditions.length; i++) { - domain.push('|'); - } - domain = domain.concat(orConditions); + /** + * Combine base and search domains + */ + _combineDomains: function(baseDomain, searchDomain) { + if (!baseDomain || baseDomain.length === 0) { + return searchDomain; + } + if (!searchDomain || searchDomain.length === 0) { + return baseDomain; } - return domain; + // Combine with AND operator + return ['&'].concat(baseDomain).concat(searchDomain); }, - _getSearchableFields: function() { + /** + * Identify searchable fields + */ + _identifySearchableFields: function() { var self = this; var fields = []; - var addedFields = {}; // Track added fields to avoid duplicates + var fieldsInfo = this._getFieldsInfo(); - // Priority 1: Get visible stored fields from columns - _.each(this.columns, function(column) { - if (!column.invisible && column.attrs && column.attrs.name) { - var fieldName = column.attrs.name; - var field = self.state.fields[fieldName]; - - if (field && !addedFields[fieldName]) { - // Only searchable in stored fields or specific field types - var isSearchable = ( - field.searchable !== false && - (field.store !== false || field.type === 'many2one') && - (!field.compute || field.store) // Skip non-stored computed fields - ); + if (!fieldsInfo) { + return []; + } + + // First try to get fields from visible columns + if (this.columns && Array.isArray(this.columns)) { + this.columns.forEach(function(column) { + if (column && !column.invisible && column.attrs && column.attrs.name) { + var fieldName = column.attrs.name; + var fieldInfo = fieldsInfo[fieldName]; - if (isSearchable) { + if (fieldInfo && self._isFieldSearchable(fieldInfo)) { fields.push({ name: fieldName, - type: field.type, - string: field.string + type: fieldInfo.type, + string: fieldInfo.string || fieldName }); - addedFields[fieldName] = true; } } - } - }); + }); + } - // Priority 2: Add common searchable fields if not enough fields - if (fields.length < 3) { - var commonFields = ['name', 'display_name', 'reference', 'code', 'ref', 'description']; - _.each(commonFields, function(fieldName) { - if (self.state.fields[fieldName] && !addedFields[fieldName]) { - var field = self.state.fields[fieldName]; - if (field.searchable !== false && - field.store !== false && - (!field.compute || field.store)) { - fields.push({ - name: fieldName, - type: field.type, - string: field.string - }); - addedFields[fieldName] = true; - } + // If no fields from columns, try common fields + if (fields.length === 0) { + ['name', 'display_name', 'reference', 'code'].forEach(function(fieldName) { + var fieldInfo = fieldsInfo[fieldName]; + if (fieldInfo && self._isFieldSearchable(fieldInfo)) { + fields.push({ + name: fieldName, + type: fieldInfo.type, + string: fieldInfo.string || fieldName + }); } }); } @@ -297,123 +327,375 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { return fields; }, - _updateListWithSearchResults: function(records, searchValue) { + /** + * Check if field is searchable + */ + _isFieldSearchable: function(fieldInfo) { + // Skip computed non-stored fields + if (fieldInfo.compute && !fieldInfo.store) { + return false; + } + // Skip non-searchable fields + if (fieldInfo.searchable === false) { + return false; + } + // Skip binary fields + if (['binary', 'image'].includes(fieldInfo.type)) { + return false; + } + return true; + }, + + /** + * Construct search domain + */ + _constructSearchDomain: function(value, fields) { + var conditions = []; var self = this; - // Update the count display + // Escape special characters for safe search + var safeValue = this._escapeSearchValue(value); + var normalizedValue = this._normalizeArabic(safeValue); + + fields.forEach(function(field) { + // Text-based fields + if (['char', 'text', 'html'].includes(field.type)) { + conditions.push([field.name, 'ilike', safeValue]); + if (normalizedValue && normalizedValue !== safeValue) { + conditions.push([field.name, 'ilike', normalizedValue]); + } + } + // Selection fields + else if (field.type === 'selection') { + conditions.push([field.name, 'ilike', safeValue]); + } + // Number fields + else if (['integer', 'float', 'monetary'].includes(field.type)) { + var numValue = parseFloat(value); + if (!isNaN(numValue)) { + conditions.push([field.name, '=', numValue]); + } + } + // Many2one - only search by ID for now + else if (field.type === 'many2one') { + var intValue = parseInt(value); + if (!isNaN(intValue)) { + conditions.push([field.name, '=', intValue]); + } + } + }); + + // Build OR domain + if (conditions.length === 0) { + return []; + } + if (conditions.length === 1) { + return conditions[0]; + } + + // Add OR operators + var domain = []; + for (var i = 1; i < conditions.length; i++) { + domain.push('|'); + } + return domain.concat(conditions); + }, + + /** + * Escape special characters in search value + */ + _escapeSearchValue: function(value) { + // Remove dangerous characters that might break domain + return value.replace(/[%_\\]/g, '\\$&'); + }, + + /** + * Execute RPC search + */ + _executeRPCSearch: function(domain, searchValue) { + var self = this; + var model = this.state.model; + var fields = this._getFieldsToRead(); + + console.log('Executing RPC search...'); + console.log('Model:', model); + console.log('Fields to read:', fields); + + this._rpc({ + model: model, + method: 'search_read', + args: [domain], + kwargs: { + fields: fields, + limit: false, + offset: 0, + context: this._getSearchContext() + } + }).then(function(result) { + console.log('Search completed. Found:', result.length, 'records'); + self._handleSearchResults(result, searchValue); + }).catch(function(error) { + console.error('RPC Error:', error); + self._handleSearchError(error, searchValue); + }).finally(function() { + self._searchState.isSearching = false; + self._hideLoading(); + }); + }, + + /** + * Get fields to read + */ + _getFieldsToRead: function() { + var fields = ['id']; + var fieldsInfo = this._getFieldsInfo(); + + if (!fieldsInfo) { + return ['id', 'display_name']; + } + + // Add visible column fields + if (this.columns) { + this.columns.forEach(function(column) { + if (column && !column.invisible && column.attrs && column.attrs.name) { + var fieldName = column.attrs.name; + var fieldInfo = fieldsInfo[fieldName]; + + // Skip computed non-stored fields + if (!fieldInfo || (fieldInfo.compute && !fieldInfo.store)) { + return; + } + + fields.push(fieldName); + } + }); + } + + // Ensure we have display_name + if (!fields.includes('display_name') && fieldsInfo.display_name) { + fields.push('display_name'); + } + + return _.uniq(fields); + }, + + /** + * Get search context + */ + _getSearchContext: function() { + var context = _.extend({}, this.state.context || {}, session.user_context || {}); + // Remove active_test to include inactive records + delete context.active_test; + return context; + }, + + /** + * Handle search results + */ + _handleSearchResults: function(records, searchValue) { var count = records.length; - var $countEl = this.$el.find('.oe_search_count'); if (count > 0) { - var message = _t('Found: ') + count + _t(' records'); - // Show warning if too many records - if (count > 1000) { - message += ' ' + _t('(Large result set, may affect performance)'); - } - $countEl.text(message).show(); - - // Update the view with new records - this.state.data.records = records; - this._searchMode = true; - - // Re-render the body with new records - this._renderBody().then(function() { - // Restore search input value - self.$el.find('.oe_search_input').val(self._currentSearchValue); - self.$el.find('.oe_clear_search').show(); - $countEl.show(); - }).catch(function(error) { - console.error('Error rendering search results:', error); - self._showError(_t('Error displaying results')); - }); - + this._updateView(records); + this._showResultCount(count); } else { - $countEl.text(_t('No records found')).show(); this._showNoResults(searchValue); } }, - _showNoResults: function(searchValue) { - // Hide all data rows - this.$el.find('.o_data_row').hide(); - - // Remove existing no results message - this.$el.find('.oe_no_results').remove(); - - // Add no results message - var colspan = this.$el.find('thead tr:first th').length || 1; - var noResultsHtml = '' + - '' + - '' + - '

' + _t('No records found') + '

' + - '

' + _t('No results for: ') + '' + _.escape(searchValue) + '

' + - '

' + _t('Try different keywords or check your filters') + '

' + - ''; - this.$el.find('tbody').append(noResultsHtml); + /** + * Handle search error + */ + _handleSearchError: function(error, searchValue) { + console.error('Search failed, falling back to client search'); + this._showError(_t('Server search failed. Searching in visible records...')); + this._fallbackToClientSearch(searchValue); }, - _showError: function(message) { - var $countEl = this.$el.find('.oe_search_count'); - $countEl.text(message).css('color', '#dc3545').show(); + /** + * Update view with search results + */ + _updateView: function(records) { + // This is the tricky part - we need to update the view + // Different versions of Odoo handle this differently - setTimeout(function() { - $countEl.css('color', '#6c757d'); - }, 3000); - }, - - _clearSearch: function() { - var self = this; + if (this.state && this.state.data) { + this.state.data.records = records; + } - // Clear input - this.$el.find('.oe_search_input').val(''); - this.$el.find('.oe_clear_search').hide(); - this.$el.find('.oe_search_count').hide(); - this.$el.find('.oe_search_loading').hide(); - - // Remove no results message - this.$el.find('.oe_no_results').remove(); - - // Reset search state - this._currentSearchValue = ''; - this._searchMode = false; - - // If we have original records, restore them - if (this._originalRecords) { - this.state.data.records = this._originalRecords; - this._renderBody().then(function() { - self._originalRecords = null; - }).catch(function(error) { - console.error('Error restoring original records:', error); - // Just show all rows as fallback - self.$el.find('.o_data_row').show(); - }); - } else { - // Just show all rows - this.$el.find('.o_data_row').show(); + // Try to re-render + if (typeof this._renderBody === 'function') { + try { + this._renderBody(); + } catch (e) { + console.error('Error rendering body:', e); + } } }, - _normalizeArabic: function(text) { - if (!text) return text; + /** + * Fallback to client-side search + */ + _fallbackToClientSearch: function(value) { + console.log('Performing client-side search...'); - // Normalizing Arabic text by removing common variations - return text - .replace(/[\u064B-\u065F]/g, '') // Remove diacritics - .replace(/[\u0660-\u0669]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030)) // Arabic-Indic digits - .replace(/[\u06F0-\u06F9]/g, (d) => String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030)) // Extended Arabic-Indic - .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') // Normalize Alef - .replace(/[\u0629]/g, 'ه') // Teh Marbuta to Heh - .replace(/[\u064A\u0626\u0649]/g, 'ي') // Normalize Yeh - .replace(/[\u0624\u0648]/g, 'و'); // Normalize Waw + var searchLower = value.toLowerCase(); + var normalizedSearch = this._normalizeArabic(searchLower); + var visibleCount = 0; + + this.$('.o_data_row').each(function() { + var $row = $(this); + var text = $row.text().toLowerCase(); + var normalizedText = this._normalizeArabic(text); + + var match = text.includes(searchLower) || normalizedText.includes(normalizedSearch); + $row.toggle(match); + if (match) visibleCount++; + }.bind(this)); + + this._showResultCount(visibleCount, true); }, - destroy: function () { - // Clear timeout if exists - if (this._searchTimeout) { - clearTimeout(this._searchTimeout); + /** + * Clear search + */ + _clearSearch: function() { + console.log('Clearing search...'); + + // Clear UI + this.$('.oe_search_input').val(''); + this.$('.oe_clear_search').hide(); + this.$('.oe_search_count').hide(); + this.$('.oe_no_results').remove(); + + // Reset state + if (this._searchState) { + this._searchState.currentValue = ''; + this._searchState.lastSearchDomain = null; + + // Restore original data + if (this._searchState.originalData) { + this._restoreOriginalData(); + } + } + + // Show all rows + this.$('.o_data_row').show(); + }, + + /** + * Restore original data + */ + _restoreOriginalData: function() { + if (!this._searchState || !this._searchState.originalData) { + return; + } + + var originalData = this._searchState.originalData; + + if (this.state && this.state.data) { + this.state.data.records = originalData.records; + } + + // Re-render if possible + if (typeof this._renderBody === 'function') { + try { + this._renderBody(); + } catch (e) { + console.error('Error restoring view:', e); + } + } + + this._searchState.originalData = null; + }, + + /** + * Show loading indicator + */ + _showLoading: function() { + this.$('.oe_search_loading').show(); + this.$('.oe_search_count').hide(); + }, + + /** + * Hide loading indicator + */ + _hideLoading: function() { + this.$('.oe_search_loading').hide(); + }, + + /** + * Show result count + */ + _showResultCount: function(count, isClientSide) { + var message = _t('Found: ') + count + _t(' records'); + if (isClientSide) { + message = _t('Showing: ') + count + _t(' visible records'); + } + this.$('.oe_search_count').text(message).show(); + }, + + /** + * Show no results message + */ + _showNoResults: function(searchValue) { + this.$('.o_data_row').hide(); + this.$('.oe_no_results').remove(); + + var $tbody = this.$('tbody'); + var colspan = this.$('thead tr:first th').length || 1; + + var $noResults = $('' + + '' + + '' + + '

' + _t('No results found') + '

' + + '

' + _.escape(searchValue) + '

' + + ''); + + $tbody.append($noResults); + this.$('.oe_search_count').text(_t('No records found')).show(); + }, + + /** + * Show error message + */ + _showError: function(message) { + this.$('.oe_search_count').text(message).css('color', 'red').show(); + setTimeout(function() { + this.$('.oe_search_count').css('color', ''); + }.bind(this), 3000); + }, + + /** + * Normalize Arabic text + */ + _normalizeArabic: function(text) { + if (!text) return ''; + + return String(text) + .replace(/[\u064B-\u065F]/g, '') // Remove diacritics + .replace(/[\u0660-\u0669]/g, function(d) { + return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030); + }) + .replace(/[\u06F0-\u06F9]/g, function(d) { + return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030); + }) + .replace(/[\u0622\u0623\u0625\u0627]/g, 'ا') + .replace(/[\u0629]/g, 'ه') + .replace(/[\u064A\u0626\u0649]/g, 'ي') + .replace(/[\u0624\u0648]/g, 'و'); + }, + + /** + * @override + */ + destroy: function() { + if (this._searchState && this._searchState.timeout) { + clearTimeout(this._searchState.timeout); } return this._super.apply(this, arguments); } }); + return ListRenderer; });