Final fix: Prevent full view reload - Update body content only to preserve search input
This commit is contained in:
parent
7146c96da3
commit
7a906739e3
|
|
@ -8,7 +8,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
ListRenderer.include({
|
||||
events: _.extend({}, ListRenderer.prototype.events, {
|
||||
'keyup .oe_search_input': '_onSearchKeyUp',
|
||||
'input .oe_search_input': '_onSearchInput', // Add input event
|
||||
'input .oe_search_input': '_onSearchInput',
|
||||
'click .oe_clear_search': '_onClearSearchClick'
|
||||
}),
|
||||
|
||||
|
|
@ -17,55 +17,46 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
*/
|
||||
_renderView: function () {
|
||||
var self = this;
|
||||
// Store search value before render
|
||||
var previousSearchValue = '';
|
||||
if (this._search && this._search.value) {
|
||||
previousSearchValue = this._search.value;
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
// Only add search box to tree views
|
||||
// Check if search box already exists and has a value
|
||||
var existingInput = self.$el.find('.oe_search_input');
|
||||
var hasExistingValue = existingInput.length && existingInput.val();
|
||||
|
||||
// Only add search box to tree views if not exists
|
||||
if (self.arch && self.arch.tag === 'tree' &&
|
||||
self.$el && self.$el.hasClass('o_list_view') &&
|
||||
!self.$el.find('.oe_search_input').length) {
|
||||
!existingInput.length) {
|
||||
|
||||
self._addSearchBox();
|
||||
self._initSearchState();
|
||||
}
|
||||
// Restore search value if exists
|
||||
self._restoreSearchInput();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override - Hook into render body to preserve search
|
||||
*/
|
||||
_renderBody: function () {
|
||||
var searchValue = this._search ? this._search.value : '';
|
||||
var self = this;
|
||||
|
||||
var result = this._super.apply(this, arguments);
|
||||
|
||||
// Check if result is a Promise
|
||||
if (result && typeof result.then === 'function') {
|
||||
return result.then(function(res) {
|
||||
// Restore search value after render
|
||||
if (searchValue) {
|
||||
setTimeout(function() {
|
||||
self._restoreSearchInput();
|
||||
}, 0);
|
||||
|
||||
// Restore previous search value if we had one
|
||||
if (previousSearchValue || hasExistingValue) {
|
||||
if (!self._search) {
|
||||
self._initSearchState();
|
||||
}
|
||||
return res;
|
||||
});
|
||||
} else {
|
||||
// If not a promise, restore immediately
|
||||
if (searchValue) {
|
||||
setTimeout(function() {
|
||||
self._restoreSearchInput();
|
||||
}, 0);
|
||||
self._search.value = previousSearchValue || hasExistingValue;
|
||||
self._restoreSearchInput();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add search box to view
|
||||
*/
|
||||
_addSearchBox: function() {
|
||||
// Check if already exists
|
||||
if (this.$el.find('.oe_search_container').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="oe_search_container" style="display: flex; align-items: center; margin: 8px; background: #f8f9fa; padding: 10px; border-radius: 4px;">' +
|
||||
'<input type="text" class="oe_search_input" placeholder="' + _t('Search...') + '" style="flex: 1; border: 1px solid #ccc; height: 32px; padding: 0 12px; border-radius: 4px;">' +
|
||||
'<button class="btn btn-sm btn-secondary oe_clear_search ml-2" style="display: none;">' + _t('Clear') + '</button>' +
|
||||
|
|
@ -79,22 +70,26 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
* Initialize search state
|
||||
*/
|
||||
_initSearchState: function() {
|
||||
this._search = {
|
||||
timer: null,
|
||||
value: '',
|
||||
active: false,
|
||||
originalData: null,
|
||||
originalDomain: null,
|
||||
filteredIds: null,
|
||||
preserveInput: true // Flag to preserve input value
|
||||
};
|
||||
if (!this._search) {
|
||||
this._search = {
|
||||
timer: null,
|
||||
value: '',
|
||||
active: false,
|
||||
originalData: null,
|
||||
originalDomain: null,
|
||||
filteredIds: null,
|
||||
isFiltered: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle search input event (for immediate value tracking)
|
||||
* Handle search input event
|
||||
*/
|
||||
_onSearchInput: function(e) {
|
||||
// Store value immediately on any input
|
||||
if (!this._search) {
|
||||
this._initSearchState();
|
||||
}
|
||||
this._search.value = $(e.currentTarget).val().trim();
|
||||
},
|
||||
|
||||
|
|
@ -105,6 +100,10 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
var self = this;
|
||||
var value = $(e.currentTarget).val().trim();
|
||||
|
||||
if (!this._search) {
|
||||
this._initSearchState();
|
||||
}
|
||||
|
||||
// Clear previous timer
|
||||
if (this._search.timer) {
|
||||
clearTimeout(this._search.timer);
|
||||
|
|
@ -118,7 +117,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
|
||||
// Require at least 2 characters or empty to search
|
||||
if (value.length === 1) {
|
||||
return; // Don't search for single character
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
|
|
@ -157,13 +156,10 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Store original data and domain on first search
|
||||
if (!this._search.originalData) {
|
||||
// Store original data on first search
|
||||
if (!this._search.originalData && !this._search.isFiltered) {
|
||||
this._storeOriginalData();
|
||||
}
|
||||
if (!this._search.originalDomain) {
|
||||
this._search.originalDomain = this.state.domain || [];
|
||||
}
|
||||
|
||||
// Start search
|
||||
this._search.active = true;
|
||||
|
|
@ -173,8 +169,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
var domain = this._buildSearchDomain(value);
|
||||
|
||||
// Add base domain if exists
|
||||
if (this._search.originalDomain && this._search.originalDomain.length > 0) {
|
||||
domain = ['&'].concat(this._search.originalDomain).concat(domain);
|
||||
var baseDomain = this._search.originalDomain || this.state.domain || [];
|
||||
if (baseDomain.length > 0) {
|
||||
domain = ['&'].concat(baseDomain).concat(domain);
|
||||
}
|
||||
|
||||
// Get fields to read
|
||||
|
|
@ -187,12 +184,11 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
args: [domain],
|
||||
kwargs: {
|
||||
fields: fields,
|
||||
limit: false, // Get all records for count
|
||||
limit: false,
|
||||
context: this.state.context || {}
|
||||
}
|
||||
}).then(function(results) {
|
||||
// Always preserve the search value
|
||||
self._search.preserveInput = true;
|
||||
self._search.isFiltered = true;
|
||||
self._displayResults(results);
|
||||
}).catch(function(error) {
|
||||
console.error('Search failed:', error);
|
||||
|
|
@ -200,10 +196,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
}).finally(function() {
|
||||
self._search.active = false;
|
||||
self._showLoading(false);
|
||||
// Always restore search value after operations
|
||||
setTimeout(function() {
|
||||
self._restoreSearchInput();
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -233,12 +225,11 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
}
|
||||
});
|
||||
|
||||
// Build OR domain properly
|
||||
// Build OR domain
|
||||
if (conditions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Always return as array
|
||||
if (conditions.length === 1) {
|
||||
return conditions;
|
||||
}
|
||||
|
|
@ -256,9 +247,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
*/
|
||||
_getSearchableFields: function() {
|
||||
var fields = [];
|
||||
var self = this;
|
||||
|
||||
// Get field definitions
|
||||
var fieldDefs = this.state.fields || {};
|
||||
|
||||
// Get from visible columns first
|
||||
|
|
@ -315,154 +303,95 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
},
|
||||
|
||||
/**
|
||||
* Display search results
|
||||
* Display search results - OPTIMIZED VERSION
|
||||
*/
|
||||
_displayResults: function(records) {
|
||||
var self = this;
|
||||
var count = records.length;
|
||||
|
||||
// Show count - KEEP IT VISIBLE
|
||||
// Show count
|
||||
this.$('.oe_search_count').text(_t('Found: ') + count + _t(' records')).show();
|
||||
|
||||
if (count === 0) {
|
||||
this._showNoResults();
|
||||
// Still preserve search input
|
||||
this._restoreSearchInput();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store filtered IDs for later use
|
||||
// Store filtered IDs
|
||||
this._search.filteredIds = records.map(function(r) { return r.id; });
|
||||
|
||||
// Re-render the list with pagination
|
||||
this._renderFilteredRecordsWithPagination(records);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render filtered records with proper pagination
|
||||
*/
|
||||
_renderFilteredRecordsWithPagination: function(records) {
|
||||
var self = this;
|
||||
|
||||
// Get current limit (default 80)
|
||||
// Get pagination settings
|
||||
var limit = this.state.limit || 80;
|
||||
var offset = 0;
|
||||
|
||||
// Get IDs for filtered records
|
||||
var filteredIds = records.map(function(r) { return r.id; });
|
||||
|
||||
// Create domain to filter by IDs
|
||||
var idDomain = [['id', 'in', filteredIds]];
|
||||
|
||||
// Try to use parent's reload method with proper pagination
|
||||
if (this.getParent() && typeof this.getParent().reload === 'function') {
|
||||
try {
|
||||
// Update state temporarily
|
||||
this.state.domain = idDomain;
|
||||
|
||||
// Store total count for pagination
|
||||
this.state.count = records.length;
|
||||
|
||||
// Reload with pagination settings
|
||||
this.getParent().reload({
|
||||
domain: idDomain,
|
||||
context: this.state.context,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}).then(function() {
|
||||
// Force restore search input and count after reload
|
||||
setTimeout(function() {
|
||||
self._forceRestoreSearchState();
|
||||
self.$('.oe_search_count').text(_t('Found: ') + records.length + _t(' records')).show();
|
||||
}, 100);
|
||||
}).catch(function(err) {
|
||||
console.warn('Parent reload failed:', err);
|
||||
self._renderRecordsWithPagination(records, limit, offset);
|
||||
});
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
console.warn('Reload method failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: render with pagination
|
||||
this._renderRecordsWithPagination(records, limit, offset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render records with pagination (fallback)
|
||||
*/
|
||||
_renderRecordsWithPagination: function(records, limit, offset) {
|
||||
var self = this;
|
||||
|
||||
// Get records for current page
|
||||
var pageRecords = records.slice(offset, offset + limit);
|
||||
|
||||
// Update state
|
||||
if (this.state && this.state.data) {
|
||||
this.state.data.records = pageRecords;
|
||||
this.state.count = records.length;
|
||||
this.state.res_ids = pageRecords.map(function(r) { return r.id; });
|
||||
// Update state WITHOUT triggering full reload
|
||||
if (this.state) {
|
||||
// Update data
|
||||
if (this.state.data) {
|
||||
this.state.data.records = pageRecords;
|
||||
}
|
||||
this.state.count = count;
|
||||
this.state.res_ids = this._search.filteredIds;
|
||||
|
||||
// Update domain for pagination to work
|
||||
this.state.domain = [['id', 'in', this._search.filteredIds]];
|
||||
}
|
||||
|
||||
// Try to render using _renderBody
|
||||
if (typeof this._renderBody === 'function') {
|
||||
// Render only the body content
|
||||
this._renderBodyContent(pageRecords);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render body content without full reload
|
||||
*/
|
||||
_renderBodyContent: function(records) {
|
||||
var self = this;
|
||||
|
||||
// Remove existing rows
|
||||
this.$('tbody .o_data_row').remove();
|
||||
this.$('.oe_no_results').remove();
|
||||
|
||||
// Check if we have _renderRows method (Odoo 14)
|
||||
if (typeof this._renderRows === 'function') {
|
||||
try {
|
||||
// Remove existing rows
|
||||
this.$('tbody .o_data_row').remove();
|
||||
this.$('.oe_no_results').remove();
|
||||
|
||||
// Render new rows
|
||||
var $rows = this._renderRows();
|
||||
this.$('tbody').append($rows);
|
||||
} catch (err) {
|
||||
console.warn('_renderRows failed:', err);
|
||||
this._fallbackRender(records);
|
||||
}
|
||||
} else if (typeof this._renderBody === 'function') {
|
||||
try {
|
||||
// Call _renderBody but handle result properly
|
||||
var result = this._renderBody();
|
||||
|
||||
// Handle both promise and non-promise returns
|
||||
// Handle promise if returned
|
||||
if (result && typeof result.then === 'function') {
|
||||
result.then(function() {
|
||||
self._forceRestoreSearchState();
|
||||
self.$('.oe_search_count').text(_t('Found: ') + records.length + _t(' records')).show();
|
||||
// Body rendered
|
||||
});
|
||||
} else {
|
||||
self._forceRestoreSearchState();
|
||||
self.$('.oe_search_count').text(_t('Found: ') + records.length + _t(' records')).show();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Render error:', err);
|
||||
this._fallbackRender(pageRecords);
|
||||
console.warn('_renderBody failed:', err);
|
||||
this._fallbackRender(records);
|
||||
}
|
||||
} else {
|
||||
this._fallbackRender(pageRecords);
|
||||
this._fallbackRender(records);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Force restore search state (input value and UI)
|
||||
*/
|
||||
_forceRestoreSearchState: function() {
|
||||
if (this._search && this._search.value && this._search.preserveInput) {
|
||||
var $input = this.$('.oe_search_input');
|
||||
if ($input.length && $input.val() !== this._search.value) {
|
||||
$input.val(this._search.value);
|
||||
$input.focus(); // Keep focus on input
|
||||
// Set cursor position to end
|
||||
var input = $input[0];
|
||||
if (input.setSelectionRange) {
|
||||
var len = this._search.value.length;
|
||||
input.setSelectionRange(len, len);
|
||||
}
|
||||
}
|
||||
this.$('.oe_clear_search').show();
|
||||
|
||||
// Update pager if exists
|
||||
if (this.getParent() && this.getParent().pager) {
|
||||
this.getParent().pager.updateState({
|
||||
current_min: 1,
|
||||
size: this.state.count,
|
||||
limit: this.state.limit
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore search input value
|
||||
*/
|
||||
_restoreSearchInput: function() {
|
||||
// Use force restore for consistency
|
||||
this._forceRestoreSearchState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Fallback render using DOM manipulation
|
||||
*/
|
||||
|
|
@ -481,7 +410,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
// Hide all rows first
|
||||
$rows.hide();
|
||||
|
||||
// Show matching rows (limited to page size)
|
||||
// Show matching rows
|
||||
var shown = 0;
|
||||
var limit = this.state.limit || 80;
|
||||
|
||||
|
|
@ -497,12 +426,9 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
}
|
||||
});
|
||||
|
||||
// Show message if no visible rows
|
||||
if (shown === 0) {
|
||||
this._showNoResults();
|
||||
}
|
||||
|
||||
this._forceRestoreSearchState();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -521,23 +447,11 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
if (record.data && record.data.id) return record.data.id;
|
||||
}
|
||||
|
||||
// Method 3: Look for Odoo's internal data
|
||||
// Method 3: Odoo's internal data
|
||||
if ($row[0] && $row[0].dataset) {
|
||||
if ($row[0].dataset.id) return parseInt($row[0].dataset.id);
|
||||
}
|
||||
|
||||
// Method 4: Check first cell for ID field
|
||||
var $idCell = $row.find('td[data-field="id"]');
|
||||
if ($idCell.length) {
|
||||
var idText = $idCell.text().trim();
|
||||
if (idText && !isNaN(idText)) return parseInt(idText);
|
||||
}
|
||||
|
||||
// Method 5: Extract from class
|
||||
var classes = $row.attr('class') || '';
|
||||
var match = classes.match(/o_data_row_(\d+)/);
|
||||
if (match) return parseInt(match[1]);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
|
|
@ -573,17 +487,12 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
if (visible === 0) {
|
||||
this._showNoResults();
|
||||
}
|
||||
|
||||
this._forceRestoreSearchState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear search and restore original view
|
||||
*/
|
||||
_clearSearch: function() {
|
||||
// Don't preserve input when clearing
|
||||
this._search.preserveInput = false;
|
||||
|
||||
// Clear UI
|
||||
this.$('.oe_search_input').val('');
|
||||
this.$('.oe_clear_search').hide();
|
||||
|
|
@ -593,17 +502,13 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
// Clear state
|
||||
this._search.value = '';
|
||||
this._search.filteredIds = null;
|
||||
this._search.isFiltered = false;
|
||||
|
||||
// Restore original domain
|
||||
if (this._search.originalDomain !== null) {
|
||||
this.state.domain = this._search.originalDomain;
|
||||
}
|
||||
|
||||
// Restore original data with proper pagination
|
||||
// Restore original data
|
||||
if (this._search.originalData) {
|
||||
this._restoreOriginalDataWithPagination();
|
||||
this._restoreOriginalData();
|
||||
} else {
|
||||
// Just show all rows respecting pagination
|
||||
// Show all rows with pagination
|
||||
var $rows = this.$('.o_data_row');
|
||||
var limit = this.state.limit || 80;
|
||||
|
||||
|
|
@ -611,11 +516,6 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
$(this).toggle(index < limit);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-enable preserve for future searches
|
||||
setTimeout(function() {
|
||||
this._search.preserveInput = true;
|
||||
}.bind(this), 100);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -631,85 +531,63 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
limit: this.state.limit,
|
||||
offset: this.state.offset || 0
|
||||
};
|
||||
this._search.originalDomain = this.state.domain ? this.state.domain.slice() : [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore original data with pagination
|
||||
* Restore original data
|
||||
*/
|
||||
_restoreOriginalDataWithPagination: function() {
|
||||
_restoreOriginalData: function() {
|
||||
if (!this._search.originalData) return;
|
||||
|
||||
var self = this;
|
||||
var orig = this._search.originalData;
|
||||
|
||||
// Restore state
|
||||
if (this.state) {
|
||||
this.state.domain = this._search.originalDomain || [];
|
||||
this.state.count = orig.count;
|
||||
this.state.limit = orig.limit;
|
||||
this.state.offset = orig.offset;
|
||||
}
|
||||
|
||||
// Try parent reload for clean restore
|
||||
if (this.getParent() && typeof this.getParent().reload === 'function') {
|
||||
this.getParent().reload({
|
||||
domain: this._search.originalDomain || [],
|
||||
context: this.state.context,
|
||||
limit: orig.limit,
|
||||
offset: orig.offset
|
||||
}).then(function() {
|
||||
// Clear search data
|
||||
self._search.originalData = null;
|
||||
self._search.originalDomain = null;
|
||||
}).catch(function() {
|
||||
// Fallback
|
||||
self._restoreOriginalRecords(orig);
|
||||
});
|
||||
} else {
|
||||
this._restoreOriginalRecords(orig);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore original records (fallback)
|
||||
*/
|
||||
_restoreOriginalRecords: function(orig) {
|
||||
// Update state
|
||||
if (this.state && this.state.data) {
|
||||
this.state.data.records = orig.records;
|
||||
if (this.state.data) {
|
||||
this.state.data.records = orig.records;
|
||||
}
|
||||
this.state.count = orig.count;
|
||||
this.state.res_ids = orig.res_ids;
|
||||
this.state.limit = orig.limit;
|
||||
this.state.offset = orig.offset;
|
||||
this.state.domain = this._search.originalDomain || [];
|
||||
}
|
||||
|
||||
// Re-render
|
||||
if (typeof this._renderBody === 'function') {
|
||||
var result = this._renderBody();
|
||||
// Handle both promise and non-promise
|
||||
if (!result || typeof result.then !== 'function') {
|
||||
// Not a promise, already rendered
|
||||
}
|
||||
} else {
|
||||
// Show original rows with pagination
|
||||
var $rows = this.$('.o_data_row');
|
||||
var limit = orig.limit || 80;
|
||||
|
||||
$rows.each(function(index) {
|
||||
$(this).toggle(index < limit);
|
||||
});
|
||||
}
|
||||
// Render the restored data
|
||||
this._renderBodyContent(orig.records);
|
||||
|
||||
// Clear stored data
|
||||
this._search.originalData = null;
|
||||
this._search.originalDomain = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore search input value
|
||||
*/
|
||||
_restoreSearchInput: function() {
|
||||
if (this._search && this._search.value) {
|
||||
var $input = this.$('.oe_search_input');
|
||||
if ($input.length && $input.val() !== this._search.value) {
|
||||
$input.val(this._search.value);
|
||||
}
|
||||
this.$('.oe_clear_search').toggle(!!this._search.value);
|
||||
|
||||
// Also restore count if we have filtered results
|
||||
if (this._search.isFiltered && this._search.filteredIds) {
|
||||
this.$('.oe_search_count')
|
||||
.text(_t('Found: ') + this._search.filteredIds.length + _t(' records'))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show loading state
|
||||
*/
|
||||
_showLoading: function(show) {
|
||||
this.$('.oe_search_loading').toggle(show);
|
||||
// Don't hide count when loading
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -734,7 +612,7 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) {
|
|||
},
|
||||
|
||||
/**
|
||||
* Normalize Arabic text for better search
|
||||
* Normalize Arabic text
|
||||
*/
|
||||
_normalizeArabic: function(text) {
|
||||
if (!text) return '';
|
||||
|
|
|
|||
Loading…
Reference in New Issue