Added pagination, improved the navigation code
This commit is contained in:
parent
2bd40037d1
commit
d8ad718f1d
|
|
@ -1,5 +1,3 @@
|
|||
# Documentation drafr for the Table widget
|
||||
|
||||
# Client-side table widget (table.xxx.js)
|
||||
|
||||
## Code organization
|
||||
|
|
@ -38,7 +36,11 @@ SubClass.prototype.someMethod = function() {
|
|||
|
||||
### Namespaces
|
||||
|
||||
All classes for the table widget are be defined in the **$.oc.table** namespace. Cell processors are be defined in the **$.oc.table.processor** namespace. The client and server memory data sources are defined in the **$.oc.table.datasource** namespace.
|
||||
All classes for the table widget are be defined in the **$.oc.table** namespace. There are several namespaces in this namespace:
|
||||
|
||||
- **$.oc.table.processor** - cell processors
|
||||
- **$.oc.table.datasource** - data sources
|
||||
- **$.oc.table.helper** - helper classes
|
||||
|
||||
### Client-side performance and memory usage considerations
|
||||
|
||||
|
|
@ -68,15 +70,32 @@ Any `DIV` elements that have the `data-control="table"` attributes are automatic
|
|||
|
||||
### Options
|
||||
|
||||
The options below are listed in the JavaScript notation. Corresponding data attribtues would look like `data-client-data-source-class`.
|
||||
The options below are listed in the JavaScript notation. Corresponding data attributes would look like `data-client-data-source-class`.
|
||||
|
||||
- `clientDataSourceСlass` (default is **client**)- specifies the client-side data source class. There are two data source classes supported on the client side - **client** and **server**.
|
||||
- `data` - specifies the data in JSON format for the **client**.
|
||||
- `recordsPerPage` - specifies how many records per page to display. If the value is not defined or `false` or `null`, the pagination feature is disabled and all records are displayed.
|
||||
- `recordsPerPage` - specifies how many records per page to display. If the value is not defined or `false` or `null`, the pagination feature is disabled and all records are displayed. Pagination and `rowSorting` cannot be used in the same time.
|
||||
- `columns` - column definitions in JSON format, see the server-side column definition format below.
|
||||
- `rowSorting` - enables the drag & drop row sorting. The sorting cannot be used with the pagination (`recordsPerPage` is not `null` or `false`).
|
||||
|
||||
## Client-side helper classes
|
||||
|
||||
Some auxiliary code is factored out from the table class to helper classes. The helper classes are defined in the **$.oc.table.helper** namespace.
|
||||
|
||||
- **table.helper.navigation.js** - implements the keyboard navigation within the table and pagination.
|
||||
|
||||
## Data sources ($.oc.table.datasource)
|
||||
|
||||
### Adding and removing records
|
||||
|
||||
Adding and removing records is an asynchronous process that involves updating records in the dataset.
|
||||
|
||||
When a user adds a record, the table object calls the `addRecord(data, offset, count, onSuccess)` method of the data source. The data source adds an empty record to the underlying data set and calls the `onSuccess` callback parameter passed to the method. In the `onSuccess` handler the table object rebuilds the table and focuses a field in the new row.
|
||||
|
||||
When user deletes a record, the table object calls the `deleteRecord(index, offset, count, onSuccess)` method of the data source. The data source removes the record from the underlying dataset and calls the `onSuccess` callback parameter, passing records of the current page (determined with the `offset` and `count` parameters) to the callback.
|
||||
|
||||
The `onSuccess` callback parameters are: data (records), count.
|
||||
|
||||
### Client memory data source ($.oc.table.datasource.client)
|
||||
|
||||
The client memory data sources keeps the data in the client memory. The data is loaded from the control element's `data` property (`data-data` attribute) and posted back with the form data.
|
||||
|
|
@ -107,8 +126,8 @@ The table object calls the `onFocus()` method of the cell processors when a cell
|
|||
|
||||
Columns are defined as array with the `columns` property. The array keys correspond the column identifiers. The array elements are associative arrays with the following keys:
|
||||
|
||||
- title
|
||||
- type (string, checkbox, dropdown, autocomplete)
|
||||
- width
|
||||
- readonly
|
||||
- options (for drop-down elements and autocomplete types)
|
||||
- `title`
|
||||
- `type` (string, checkbox, dropdown, autocomplete)
|
||||
- `width`
|
||||
- `readonly`
|
||||
- `options` (for drop-down elements and autocomplete types)
|
||||
|
|
@ -30,35 +30,48 @@
|
|||
/*
|
||||
* Fetches records from the underlying data source and
|
||||
* passes them to the onSuccess callback function.
|
||||
* The onSuccess callback parameters: records, totalCount.
|
||||
*/
|
||||
Base.prototype.getRecords = function(offset, count, onSuccess) {
|
||||
onSuccess([])
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the total number of records in the underlying set
|
||||
* Creates a record with the passed data and returns the updated page records
|
||||
* to the onSuccess callback function.
|
||||
*
|
||||
* - recordData - the record fields
|
||||
* - offset - the current page's first record index (zero-based)
|
||||
* - count - number of records to return
|
||||
* - onSuccess - a callback function to execute when the updated data gets available.
|
||||
*
|
||||
* The onSuccess callback parameters: records, totalCount.
|
||||
*/
|
||||
Base.prototype.count = function() {
|
||||
return 0
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a record with the passed data and returns the new record index.
|
||||
*/
|
||||
Base.prototype.createRecord = function(recordData) {
|
||||
return 0
|
||||
Base.prototype.createRecord = function(recordData, offset, count, onSuccess) {
|
||||
onSuccess([], 0)
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates a record with the specified index with the passed data
|
||||
*
|
||||
* - index - the record index in the dataset (primary key, etc)
|
||||
* - recordData - the record fields.
|
||||
*/
|
||||
Base.prototype.updateRecord = function(index, recordData) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Deletes a record with the specified index
|
||||
* Deletes a record with the specified index.
|
||||
*
|
||||
* - index - the record index in the dataset (primary key, etc).
|
||||
* - offset - the current page's first record index (zero-based)
|
||||
* - count - number of records to return
|
||||
* - onSuccess - a callback function to execute when the updated data gets available.
|
||||
*
|
||||
* The onSuccess callback parameters: records, totalCount.
|
||||
*/
|
||||
Base.prototype.deleteRecord = function(index) {
|
||||
Base.prototype.deleteRecord = function(index, offset, count, onSuccess) {
|
||||
onSuccess([], 0)
|
||||
}
|
||||
|
||||
$.oc.table.datasource.base = Base;
|
||||
|
|
|
|||
|
|
@ -37,33 +37,28 @@
|
|||
this.data = null
|
||||
}
|
||||
|
||||
/*
|
||||
* Fetches records from the underlying data source and
|
||||
* passes them to the onSuccess callback function.
|
||||
*/
|
||||
Client.prototype.getRecords = function(offset, count, onSuccess) {
|
||||
onSuccess(this.data)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the total number of records in the underlying set
|
||||
*/
|
||||
Client.prototype.count = function() {
|
||||
return this.data.length
|
||||
if (!count) {
|
||||
// Return all records
|
||||
onSuccess(this.data, this.data.length)
|
||||
} else {
|
||||
// Return a subset of records
|
||||
onSuccess(this.data.slice(offset, offset+count), this.data.length)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a record with the passed data and returns the new record index.
|
||||
*/
|
||||
Client.prototype.createRecord = function(recordData) {
|
||||
return 0;
|
||||
Client.prototype.createRecord = function(recordData, offset, count, onSuccess) {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Updates a record with the specified index with the passed data
|
||||
*/
|
||||
Client.prototype.updateRecord = function(index, recordData) {
|
||||
// console.log('Update recird', index, recordData)
|
||||
|
||||
}
|
||||
|
||||
$.oc.table.datasource.client = Client
|
||||
|
|
|
|||
|
|
@ -20,10 +20,16 @@
|
|||
var Navigation = function(tableObj) {
|
||||
// Reference to the table object
|
||||
this.tableObj = tableObj
|
||||
|
||||
|
||||
// The current page index
|
||||
this.pageIndex = 0
|
||||
|
||||
// Event handlers
|
||||
this.keydownHandler = this.onKeydown.bind(this)
|
||||
|
||||
// Number of pages in the pagination
|
||||
this.pageCount = 0
|
||||
|
||||
this.init()
|
||||
};
|
||||
|
||||
|
|
@ -48,6 +54,112 @@
|
|||
this.keydownHandler = null
|
||||
}
|
||||
|
||||
// PAGINATION
|
||||
// ============================
|
||||
|
||||
Navigation.prototype.paginationEnabled = function() {
|
||||
return this.tableObj.options.recordsPerPage != null &&
|
||||
this.tableObj.options.recordsPerPage != false
|
||||
}
|
||||
|
||||
Navigation.prototype.getPageFirstRowOffset = function() {
|
||||
return this.pageIndex * this.tableObj.options.recordsPerPage
|
||||
}
|
||||
|
||||
Navigation.prototype.buildPagination = function(recordCount) {
|
||||
if (!this.paginationEnabled())
|
||||
return
|
||||
|
||||
var paginationContainer = this.tableObj.el.querySelector('.pagination'),
|
||||
newPaginationContainer = false,
|
||||
curRecordCount = 0
|
||||
|
||||
this.pageCount = this.calculatePageCount(recordCount, this.tableObj.options.recordsPerPage)
|
||||
|
||||
if (!paginationContainer) {
|
||||
paginationContainer = document.createElement('div')
|
||||
paginationContainer.setAttribute('class', 'pagination')
|
||||
newPaginationContainer = true
|
||||
} else
|
||||
curRecordCount = paginationContainer.getAttribute('data-record-count')
|
||||
|
||||
// Generate the new page list only if the record count has changed
|
||||
if (newPaginationContainer || curRecordCount != recordCount) {
|
||||
paginationContainer.setAttribute('data-record-count', recordCount)
|
||||
|
||||
var pageList = this.buildPaginationLinkList(recordCount,
|
||||
this.tableObj.options.recordsPerPage,
|
||||
this.pageIndex)
|
||||
|
||||
if (!newPaginationContainer)
|
||||
paginationContainer.replaceChild(paginationContainer.children[0], pageList)
|
||||
else {
|
||||
paginationContainer.appendChild(pageList)
|
||||
this.tableObj.el.appendChild(paginationContainer)
|
||||
}
|
||||
} else {
|
||||
// Do not re-generate the pages if the record count hasn't changed,
|
||||
// but mark the new active item in the pagination list
|
||||
|
||||
this.markActiveLinkItem(paginationContainer, this.pageIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.prototype.calculatePageCount = function(recordCount, recordsPerPage) {
|
||||
var pageCount = Math.ceil(recordCount/recordsPerPage)
|
||||
|
||||
if (!pageCount)
|
||||
pageCount = 1
|
||||
|
||||
return pageCount
|
||||
}
|
||||
|
||||
Navigation.prototype.buildPaginationLinkList = function(recordCount, recordsPerPage, pageIndex) {
|
||||
// This method could be refactored and moved to a pagination
|
||||
// helper if we want to support other pagination markup options.
|
||||
|
||||
var pageCount = this.calculatePageCount(recordCount, recordsPerPage),
|
||||
pageList = document.createElement('ul')
|
||||
|
||||
for (var i=0; i < pageCount; i++) {
|
||||
var item = document.createElement('li'),
|
||||
link = document.createElement('a')
|
||||
|
||||
if (i == pageIndex)
|
||||
item.setAttribute('class', 'active')
|
||||
|
||||
link.innerText = i+1
|
||||
link.setAttribute('data-page-index', i)
|
||||
link.setAttribute('href', '#')
|
||||
|
||||
item.appendChild(link)
|
||||
pageList.appendChild(item)
|
||||
}
|
||||
|
||||
return pageList
|
||||
}
|
||||
|
||||
Navigation.prototype.markActiveLinkItem = function(paginationContainer, pageIndex) {
|
||||
// This method could be refactored and moved to a pagination
|
||||
// helper if we want to support other pagination markup options.
|
||||
|
||||
var activeItem = paginationContainer.querySelector('.active'),
|
||||
list = paginationContainer.children[0]
|
||||
|
||||
activeItem.setAttribute('class', '')
|
||||
|
||||
for (var i=0, len = list.children.length; i < len; i++) {
|
||||
if (i == pageIndex)
|
||||
list.children[i].setAttribute('class', 'active')
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.prototype.gotoPage = function(pageIndex, onSuccess) {
|
||||
this.pageIndex = pageIndex
|
||||
|
||||
this.tableObj.updateDataTable(onSuccess)
|
||||
}
|
||||
|
||||
// KEYBOARD NAVIGATION
|
||||
// ============================
|
||||
|
||||
|
|
@ -63,13 +175,27 @@
|
|||
row.nextElementSibling :
|
||||
row.parentNode.children[row.parentNode.children.length - 1]
|
||||
|
||||
if (!newRow)
|
||||
return
|
||||
|
||||
var cell = newRow.children[this.tableObj.activeCell.cellIndex]
|
||||
if (newRow) {
|
||||
var cell = newRow.children[this.tableObj.activeCell.cellIndex]
|
||||
|
||||
if (cell)
|
||||
this.tableObj.focusCell(cell)
|
||||
if (cell)
|
||||
this.tableObj.focusCell(cell)
|
||||
} else {
|
||||
// Try to switch to the previous page if that's possible
|
||||
|
||||
if (!this.paginationEnabled())
|
||||
return
|
||||
|
||||
if (this.pageIndex < this.pageCount-1) {
|
||||
var cellIndex = this.tableObj.activeCell.cellIndex,
|
||||
self = this
|
||||
|
||||
this.gotoPage(this.pageIndex+1, function navUpPageSuccess(){
|
||||
self.focusCell('top', cellIndex)
|
||||
self = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.prototype.navigateUp = function(ev) {
|
||||
|
|
@ -84,13 +210,27 @@
|
|||
row.previousElementSibling :
|
||||
row.parentNode.children[0]
|
||||
|
||||
if (!newRow)
|
||||
return
|
||||
if (newRow) {
|
||||
var cell = newRow.children[this.tableObj.activeCell.cellIndex]
|
||||
|
||||
var cell = newRow.children[this.tableObj.activeCell.cellIndex]
|
||||
if (cell)
|
||||
this.tableObj.focusCell(cell)
|
||||
} else {
|
||||
// Try to switch to the previous page if that's possible
|
||||
|
||||
if (cell)
|
||||
this.tableObj.focusCell(cell)
|
||||
if (!this.paginationEnabled())
|
||||
return
|
||||
|
||||
if (this.pageIndex > 0) {
|
||||
var cellIndex = this.tableObj.activeCell.cellIndex,
|
||||
self = this
|
||||
|
||||
this.gotoPage(this.pageIndex-1, function navUpPageSuccess(){
|
||||
self.focusCell('bottom', cellIndex)
|
||||
self = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.prototype.navigateLeft = function(ev) {
|
||||
|
|
@ -159,6 +299,25 @@
|
|||
this.tableObj.stopEvent(ev)
|
||||
}
|
||||
|
||||
Navigation.prototype.focusCell = function(rowReference, cellIndex) {
|
||||
var row = null,
|
||||
dataTable = this.tableObj.dataTable
|
||||
|
||||
if (rowReference == 'bottom') {
|
||||
row = dataTable.children[dataTable.children.length-1]
|
||||
}
|
||||
else if (rowReference == 'top') {
|
||||
row = dataTable.children[0]
|
||||
}
|
||||
|
||||
if (!row)
|
||||
return
|
||||
|
||||
var cell = row.children[cellIndex]
|
||||
if (cell)
|
||||
this.tableObj.focusCell(cell)
|
||||
}
|
||||
|
||||
// EVENT HANDLERS
|
||||
// ============================
|
||||
|
||||
|
|
@ -177,5 +336,25 @@
|
|||
return this.navigateNext(ev)
|
||||
}
|
||||
|
||||
Navigation.prototype.onClick = function(ev) {
|
||||
// The navigation object uses the table's click handler
|
||||
// and doesn't register own click handler.
|
||||
|
||||
var target = this.tableObj.getEventTarget(ev, 'A')
|
||||
|
||||
if (!target)
|
||||
return
|
||||
|
||||
var pageIndex = target.getAttribute('data-page-index')
|
||||
|
||||
if (pageIndex === null)
|
||||
return
|
||||
|
||||
this.gotoPage(pageIndex)
|
||||
this.tableObj.stopEvent(ev)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
$.oc.table.helper.navigation = Navigation;
|
||||
}(window.jQuery);
|
||||
|
|
@ -35,11 +35,6 @@
|
|||
// A reference to the currently active table cell
|
||||
this.activeCell = null
|
||||
|
||||
// The current first record index.
|
||||
// This index is zero based and has nothing to do
|
||||
// with the database identifiers or any underlying data.
|
||||
this.offset = 0
|
||||
|
||||
// The index of the row which is being edited at the moment.
|
||||
// This index corresponds the data source row index which
|
||||
// uniquely identifies the row in the data set. When the
|
||||
|
|
@ -47,7 +42,7 @@
|
|||
// the previously edited record to the data source.
|
||||
this.editedRowIndex = null
|
||||
|
||||
// A reference to the data table.
|
||||
// A reference to the data table
|
||||
this.dataTable = null
|
||||
|
||||
// Event handlers
|
||||
|
|
@ -172,21 +167,19 @@
|
|||
return headersTable
|
||||
}
|
||||
|
||||
Table.prototype.updateDataTable = function() {
|
||||
Table.prototype.updateDataTable = function(onSuccess) {
|
||||
var self = this;
|
||||
|
||||
this.getRecords(function onSuccess(records){
|
||||
self.buildDataTable(records)
|
||||
this.fetchRecords(function onSuccessClosure(records, totalCount){
|
||||
self.buildDataTable(records, totalCount)
|
||||
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
})
|
||||
}
|
||||
|
||||
Table.prototype.buildDataTable = function(records) {
|
||||
// Completely remove the existing data table. By convention there should
|
||||
// be no event handlers or references bound to it.
|
||||
if (this.dataTable !== null)
|
||||
this.dataTable.parentNode.removeChild(this.dataTable);
|
||||
|
||||
this.dataTable = document.createElement('table')
|
||||
Table.prototype.buildDataTable = function(records, totalCount) {
|
||||
var dataTable = document.createElement('table')
|
||||
|
||||
for (var i = 0, len = records.length; i < len; i++) {
|
||||
var row = document.createElement('tr')
|
||||
|
|
@ -213,16 +206,24 @@
|
|||
row.appendChild(cell)
|
||||
}
|
||||
|
||||
this.dataTable.appendChild(row)
|
||||
dataTable.appendChild(row)
|
||||
}
|
||||
|
||||
// Build the data table
|
||||
this.el.appendChild(this.dataTable)
|
||||
// Inject the data table to the DOM or replace the existing table
|
||||
if (this.dataTable !== null)
|
||||
this.el.replaceChild(dataTable, this.dataTable)
|
||||
else
|
||||
this.el.appendChild(dataTable)
|
||||
|
||||
this.dataTable = dataTable
|
||||
|
||||
// Update the pagination links
|
||||
this.navigation.buildPagination(totalCount)
|
||||
}
|
||||
|
||||
Table.prototype.getRecords = function(onSuccess) {
|
||||
return this.dataSource.getRecords(
|
||||
this.offset,
|
||||
Table.prototype.fetchRecords = function(onSuccess) {
|
||||
this.dataSource.getRecords(
|
||||
this.navigation.getPageFirstRowOffset(),
|
||||
this.options.recordsPerPage,
|
||||
onSuccess)
|
||||
}
|
||||
|
|
@ -262,6 +263,18 @@
|
|||
editedRow.setAttribute('data-dirty', 0)
|
||||
}
|
||||
|
||||
/*
|
||||
* Removes editor from the currently edited cell and commits the row if needed.
|
||||
*/
|
||||
Table.prototype.unfocusTable = function() {
|
||||
if (this.activeCellProcessor)
|
||||
this.activeCellProcessor.onUnfocus()
|
||||
|
||||
this.commitEditedRow()
|
||||
this.activeCellProcessor = null
|
||||
this.activeCell = null
|
||||
}
|
||||
|
||||
/*
|
||||
* Calls the onFocus() method for the cell processor responsible for the
|
||||
* newly focused cell. Commit the previous edited row to the data source
|
||||
|
|
@ -302,6 +315,9 @@
|
|||
// ============================
|
||||
|
||||
Table.prototype.onClick = function(ev) {
|
||||
if (this.navigation.onClick(ev) === false)
|
||||
return
|
||||
|
||||
var target = this.getEventTarget(ev, 'TD')
|
||||
|
||||
if (!target)
|
||||
|
|
@ -317,6 +333,9 @@
|
|||
// ============================
|
||||
|
||||
Table.prototype.dispose = function() {
|
||||
// Remove an editor and commit the data if needed
|
||||
this.unfocusTable()
|
||||
|
||||
// Dispose the data source and clean up the reference
|
||||
this.dataSource.dispose()
|
||||
this.dataSource = null
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
/*
|
||||
* Forces the processor to hide the editor when the user navigates
|
||||
* away from the cell. Processors can update the sell value in this method.
|
||||
* Processors must clear the reference to the active cell in this method.
|
||||
*/
|
||||
Base.prototype.onUnfocus = function() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,27 @@
|
|||
this.buildEditor(cellElement)
|
||||
}
|
||||
|
||||
/*
|
||||
* Forces the processor to hide the editor when the user navigates
|
||||
* away from the cell. Processors can update the sell value in this method.
|
||||
* Processors must clear the reference to the active cell in this method.
|
||||
*/
|
||||
StringProcessor.prototype.onUnfocus = function() {
|
||||
if (!this.activeCell)
|
||||
return
|
||||
|
||||
var editor = this.activeCell.querySelector('.string-input')
|
||||
if (editor) {
|
||||
// Update the cell value and remove the editor
|
||||
this.tableObj.setCellValue(this.activeCell, editor.value)
|
||||
this.setViewContainerValue(this.activeCell, editor.value)
|
||||
editor.parentNode.removeChild(editor)
|
||||
}
|
||||
|
||||
this.showViewContainer(this.activeCell)
|
||||
this.activeCell = null
|
||||
}
|
||||
|
||||
StringProcessor.prototype.buildEditor = function(cellElement) {
|
||||
// Hide the view container
|
||||
this.hideViewContainer(this.activeCell)
|
||||
|
|
@ -79,26 +100,6 @@
|
|||
window.setTimeout(this.focusTimeoutHandler, 0)
|
||||
}
|
||||
|
||||
/*
|
||||
* Forces the processor to hide the editor when the user navigates
|
||||
* away from the cell.
|
||||
*/
|
||||
StringProcessor.prototype.onUnfocus = function() {
|
||||
if (!this.activeCell)
|
||||
return
|
||||
|
||||
var editor = this.activeCell.querySelector('.string-input')
|
||||
if (editor) {
|
||||
// Update the cell value and remove the editor
|
||||
this.tableObj.setCellValue(this.activeCell, editor.value)
|
||||
this.setViewContainerValue(this.activeCell, editor.value)
|
||||
editor.parentNode.removeChild(editor)
|
||||
}
|
||||
|
||||
this.showViewContainer(this.activeCell)
|
||||
this.activeCell = null
|
||||
}
|
||||
|
||||
/*
|
||||
* Determines if the keyboard navigation in the specified direction is allowed
|
||||
* by the cell processor. Some processors could reject the navigation, for example
|
||||
|
|
@ -157,7 +158,8 @@
|
|||
if (document.selection) {
|
||||
var range = input.createTextRange()
|
||||
|
||||
setTimeout(function(){
|
||||
setTimeout(function() {
|
||||
// Asynchronous layout update, better performance
|
||||
range.collapse(true)
|
||||
range.moveStart("character", position)
|
||||
range.moveEnd("character", 0)
|
||||
|
|
@ -166,7 +168,8 @@
|
|||
}
|
||||
|
||||
if (input.selectionStart !== undefined) {
|
||||
setTimeout(function(){
|
||||
setTimeout(function() {
|
||||
// Asynchronous layout update
|
||||
input.selectionStart = position
|
||||
input.selectionEnd = position
|
||||
}, 0)
|
||||
|
|
|
|||
Loading…
Reference in New Issue