UNPKG

16.7 kBJavaScriptView Raw
1(function ($) {
2 // register namespace
3 $.extend(true, window, {
4 "Slick": {
5 "CellExternalCopyManager": CellExternalCopyManager
6 }
7 });
8
9
10 function CellExternalCopyManager(options) {
11 /*
12 This manager enables users to copy/paste data from/to an external Spreadsheet application
13 such as MS-Excel® or OpenOffice-Spreadsheet.
14
15 Since it is not possible to access directly the clipboard in javascript, the plugin uses
16 a trick to do it's job. After detecting the keystroke, we dynamically create a textarea
17 where the browser copies/pastes the serialized data.
18
19 options:
20 copiedCellStyle : sets the css className used for copied cells. default : "copied"
21 copiedCellStyleLayerKey : sets the layer key for setting css values of copied cells. default : "copy-manager"
22 dataItemColumnValueExtractor : option to specify a custom column value extractor function
23 dataItemColumnValueSetter : option to specify a custom column value setter function
24 clipboardCommandHandler : option to specify a custom handler for paste actions
25 includeHeaderWhenCopying : set to true and the plugin will take the name property from each column (which is usually what appears in your header) and put that as the first row of the text that's copied to the clipboard
26 bodyElement: option to specify a custom DOM element which to will be added the hidden textbox. It's useful if the grid is inside a modal dialog.
27 onCopyInit: optional handler to run when copy action initializes
28 onCopySuccess: optional handler to run when copy action is complete
29 newRowCreator: function to add rows to table if paste overflows bottom of table, if this function is not provided new rows will be ignored.
30 readOnlyMode: suppresses paste
31 */
32 var _grid;
33 var _self = this;
34 var _copiedRanges;
35 var _options = options || {};
36 var _copiedCellStyleLayerKey = _options.copiedCellStyleLayerKey || "copy-manager";
37 var _copiedCellStyle = _options.copiedCellStyle || "copied";
38 var _clearCopyTI = 0;
39 var _bodyElement = _options.bodyElement || document.body;
40 var _onCopyInit = _options.onCopyInit || null;
41 var _onCopySuccess = _options.onCopySuccess || null;
42
43 var keyCodes = {
44 'C': 67,
45 'V': 86,
46 'ESC': 27,
47 'INSERT': 45
48 };
49
50 function init(grid) {
51 _grid = grid;
52 _grid.onKeyDown.subscribe(handleKeyDown);
53
54 // we need a cell selection model
55 var cellSelectionModel = grid.getSelectionModel();
56 if (!cellSelectionModel){
57 throw new Error("Selection model is mandatory for this plugin. Please set a selection model on the grid before adding this plugin: grid.setSelectionModel(new Slick.CellSelectionModel())");
58 }
59 // we give focus on the grid when a selection is done on it.
60 // without this, if the user selects a range of cell without giving focus on a particular cell, the grid doesn't get the focus and key stroke handles (ctrl+c) don't work
61 cellSelectionModel.onSelectedRangesChanged.subscribe(function(e, args){
62 _grid.focus();
63 });
64 }
65
66 function destroy() {
67 _grid.onKeyDown.unsubscribe(handleKeyDown);
68 }
69
70 function getDataItemValueForColumn(item, columnDef) {
71 if (_options.dataItemColumnValueExtractor) {
72 var dataItemColumnValueExtractorValue = _options.dataItemColumnValueExtractor(item, columnDef);
73
74 if (dataItemColumnValueExtractorValue)
75 return dataItemColumnValueExtractorValue;
76 }
77
78 var retVal = '';
79
80 // if a custom getter is not defined, we call serializeValue of the editor to serialize
81 if (columnDef.editor){
82 var editorArgs = {
83 'container':$("<p>"), // a dummy container
84 'column':columnDef,
85 'position':{'top':0, 'left':0}, // a dummy position required by some editors
86 'grid':_grid
87 };
88 var editor = new columnDef.editor(editorArgs);
89 editor.loadValue(item);
90 retVal = editor.serializeValue();
91 editor.destroy();
92 } else {
93 retVal = item[columnDef.field];
94 }
95
96 return retVal;
97 }
98
99 function setDataItemValueForColumn(item, columnDef, value) {
100 if (_options.dataItemColumnValueSetter) {
101 return _options.dataItemColumnValueSetter(item, columnDef, value);
102 }
103
104 // if a custom setter is not defined, we call applyValue of the editor to unserialize
105 if (columnDef.editor){
106 var editorArgs = {
107 'container':$("body"), // a dummy container
108 'column':columnDef,
109 'position':{'top':0, 'left':0}, // a dummy position required by some editors
110 'grid':_grid
111 };
112 var editor = new columnDef.editor(editorArgs);
113 editor.loadValue(item);
114 editor.applyValue(item, value);
115 editor.destroy();
116 } else {
117 item[columnDef.field] = value;
118 }
119 }
120
121
122 function _createTextBox(innerText){
123 var ta = document.createElement('textarea');
124 ta.style.position = 'absolute';
125 ta.style.left = '-1000px';
126 ta.style.top = document.body.scrollTop + 'px';
127 ta.value = innerText;
128 _bodyElement.appendChild(ta);
129 ta.select();
130
131 return ta;
132 }
133
134 function _decodeTabularData(_grid, ta){
135 var columns = _grid.getColumns();
136 var clipText = ta.value;
137 var clipRows = clipText.split(/[\n\f\r]/);
138 // trim trailing CR if present
139 if (clipRows[clipRows.length - 1]==="") { clipRows.pop(); }
140
141 var clippedRange = [];
142 var j = 0;
143
144 _bodyElement.removeChild(ta);
145 for (var i=0; i<clipRows.length; i++) {
146 if (clipRows[i]!=="")
147 clippedRange[j++] = clipRows[i].split("\t");
148 else
149 clippedRange[i] = [""];
150 }
151 var selectedCell = _grid.getActiveCell();
152 var ranges = _grid.getSelectionModel().getSelectedRanges();
153 var selectedRange = ranges && ranges.length ? ranges[0] : null; // pick only one selection
154 var activeRow = null;
155 var activeCell = null;
156
157 if (selectedRange){
158 activeRow = selectedRange.fromRow;
159 activeCell = selectedRange.fromCell;
160 } else if (selectedCell){
161 activeRow = selectedCell.row;
162 activeCell = selectedCell.cell;
163 } else {
164 // we don't know where to paste
165 return;
166 }
167
168 var oneCellToMultiple = false;
169 var destH = clippedRange.length;
170 var destW = clippedRange.length ? clippedRange[0].length : 0;
171 if (clippedRange.length == 1 && clippedRange[0].length == 1 && selectedRange){
172 oneCellToMultiple = true;
173 destH = selectedRange.toRow - selectedRange.fromRow +1;
174 destW = selectedRange.toCell - selectedRange.fromCell +1;
175 }
176 var availableRows = _grid.getData().length - activeRow;
177 var addRows = 0;
178
179 // ignore new rows if we don't have a "newRowCreator"
180 if(availableRows < destH && _options.newRowCreator)
181 {
182 var d = _grid.getData();
183 for(addRows = 1; addRows <= destH - availableRows; addRows++)
184 d.push({});
185 _grid.setData(d);
186 _grid.render();
187 }
188
189 var overflowsBottomOfGrid = activeRow + destH > _grid.getDataLength();
190
191 if (_options.newRowCreator && overflowsBottomOfGrid) {
192
193 var newRowsNeeded = activeRow + destH - _grid.getDataLength();
194
195 _options.newRowCreator(newRowsNeeded);
196
197 }
198
199 var clipCommand = {
200
201 isClipboardCommand: true,
202 clippedRange: clippedRange,
203 oldValues: [],
204 cellExternalCopyManager: _self,
205 _options: _options,
206 setDataItemValueForColumn: setDataItemValueForColumn,
207 markCopySelection: markCopySelection,
208 oneCellToMultiple: oneCellToMultiple,
209 activeRow: activeRow,
210 activeCell: activeCell,
211 destH: destH,
212 destW: destW,
213 maxDestY: _grid.getDataLength(),
214 maxDestX: _grid.getColumns().length,
215 h: 0,
216 w: 0,
217
218 execute: function() {
219 this.h=0;
220 for (var y = 0; y < this.destH; y++){
221 this.oldValues[y] = [];
222 this.w=0;
223 this.h++;
224 for (var x = 0; x < this.destW; x++){
225 this.w++;
226 var desty = activeRow + y;
227 var destx = activeCell + x;
228
229 if (desty < this.maxDestY && destx < this.maxDestX ) {
230 var nd = _grid.getCellNode(desty, destx);
231 var dt = _grid.getDataItem(desty);
232 this.oldValues[y][x] = dt[columns[destx]['field']];
233 if (oneCellToMultiple)
234 this.setDataItemValueForColumn(dt, columns[destx], clippedRange[0][0]);
235 else
236 this.setDataItemValueForColumn(dt, columns[destx], clippedRange[y] ? clippedRange[y][x] : '');
237 _grid.updateCell(desty, destx);
238 _grid.onCellChange.notify({
239 row: desty,
240 cell: destx,
241 item: dt,
242 grid: _grid
243 });
244
245 }
246 }
247 }
248
249 var bRange = {
250 'fromCell': activeCell,
251 'fromRow': activeRow,
252 'toCell': activeCell+this.w-1,
253 'toRow': activeRow+this.h-1
254 };
255
256 this.markCopySelection([bRange]);
257 _grid.getSelectionModel().setSelectedRanges([bRange]);
258 this.cellExternalCopyManager.onPasteCells.notify({ranges: [bRange]});
259 },
260
261 undo: function() {
262 for (var y = 0; y < this.destH; y++){
263 for (var x = 0; x < this.destW; x++){
264 var desty = activeRow + y;
265 var destx = activeCell + x;
266
267 if (desty < this.maxDestY && destx < this.maxDestX ) {
268 var nd = _grid.getCellNode(desty, destx);
269 var dt = _grid.getDataItem(desty);
270 if (oneCellToMultiple)
271 this.setDataItemValueForColumn(dt, columns[destx], this.oldValues[0][0]);
272 else
273 this.setDataItemValueForColumn(dt, columns[destx], this.oldValues[y][x]);
274 _grid.updateCell(desty, destx);
275 _grid.onCellChange.notify({
276 row: desty,
277 cell: destx,
278 item: dt,
279 grid: _grid
280 });
281 }
282 }
283 }
284
285 var bRange = {
286 'fromCell': activeCell,
287 'fromRow': activeRow,
288 'toCell': activeCell+this.w-1,
289 'toRow': activeRow+this.h-1
290 };
291
292 this.markCopySelection([bRange]);
293 _grid.getSelectionModel().setSelectedRanges([bRange]);
294 this.cellExternalCopyManager.onPasteCells.notify({ranges: [bRange]});
295
296 if(addRows > 1){
297 var d = _grid.getData();
298 for(; addRows > 1; addRows--)
299 d.splice(d.length - 1, 1);
300 _grid.setData(d);
301 _grid.render();
302 }
303 }
304 };
305
306 if(_options.clipboardCommandHandler) {
307 _options.clipboardCommandHandler(clipCommand);
308 }
309 else {
310 clipCommand.execute();
311 }
312 }
313
314
315 function handleKeyDown(e, args) {
316 var ranges;
317 if (!_grid.getEditorLock().isActive() || _grid.getOptions().autoEdit) {
318 if (e.which == keyCodes.ESC) {
319 if (_copiedRanges) {
320 e.preventDefault();
321 clearCopySelection();
322 _self.onCopyCancelled.notify({ranges: _copiedRanges});
323 _copiedRanges = null;
324 }
325 }
326
327 if ((e.which === keyCodes.C || e.which === keyCodes.INSERT) && (e.ctrlKey || e.metaKey) && !e.shiftKey) { // CTRL+C or CTRL+INS
328 if (_onCopyInit) {
329 _onCopyInit.call();
330 }
331 ranges = _grid.getSelectionModel().getSelectedRanges();
332 if (ranges.length !== 0) {
333 _copiedRanges = ranges;
334 markCopySelection(ranges);
335 _self.onCopyCells.notify({ranges: ranges});
336
337 var columns = _grid.getColumns();
338 var clipText = "";
339
340 for (var rg = 0; rg < ranges.length; rg++){
341 var range = ranges[rg];
342 var clipTextRows = [];
343 for (var i=range.fromRow; i< range.toRow+1 ; i++){
344 var clipTextCells = [];
345 var dt = _grid.getDataItem(i);
346
347 if (clipTextRows === "" && _options.includeHeaderWhenCopying) {
348 var clipTextHeaders = [];
349 for (var j = range.fromCell; j < range.toCell + 1 ; j++) {
350 if (columns[j].name.length > 0)
351 clipTextHeaders.push(columns[j].name);
352 }
353 clipTextRows.push(clipTextHeaders.join("\t"));
354 }
355
356 for (var j=range.fromCell; j< range.toCell+1 ; j++){
357 clipTextCells.push(getDataItemValueForColumn(dt, columns[j]));
358 }
359 clipTextRows.push(clipTextCells.join("\t"));
360 }
361 clipText += clipTextRows.join("\r\n") + "\r\n";
362 }
363
364 if(window.clipboardData) {
365 window.clipboardData.setData("Text", clipText);
366 return true;
367 }
368 else {
369 var focusEl = document.activeElement;
370
371 var ta = _createTextBox(clipText);
372
373 ta.focus();
374
375 setTimeout(function(){
376 _bodyElement.removeChild(ta);
377 // restore focus
378 if (focusEl)
379 focusEl.focus();
380 else
381 console.log("Not element to restore focus to after copy?");
382
383 }, 100);
384
385 if (_onCopySuccess) {
386 var rowCount = 0;
387 // If it's cell selection, use the toRow/fromRow fields
388 if (ranges.length === 1) {
389 rowCount = (ranges[0].toRow + 1) - ranges[0].fromRow;
390 }
391 else {
392 rowCount = ranges.length;
393 }
394 _onCopySuccess.call(this, rowCount);
395 }
396
397 return false;
398 }
399 }
400 }
401
402 if (!_options.readOnlyMode && (
403 (e.which === keyCodes.V && (e.ctrlKey || e.metaKey) && !e.shiftKey)
404 || (e.which === keyCodes.INSERT && e.shiftKey && !e.ctrlKey)
405 )) { // CTRL+V or Shift+INS
406 var ta = _createTextBox('');
407
408 setTimeout(function(){
409 _decodeTabularData(_grid, ta);
410 }, 100);
411
412 return false;
413 }
414 }
415 }
416
417 function markCopySelection(ranges) {
418 clearCopySelection();
419
420 var columns = _grid.getColumns();
421 var hash = {};
422 for (var i = 0; i < ranges.length; i++) {
423 for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
424 hash[j] = {};
425 for (var k = ranges[i].fromCell; k <= ranges[i].toCell && k<columns.length; k++) {
426 hash[j][columns[k].id] = _copiedCellStyle;
427 }
428 }
429 }
430 _grid.setCellCssStyles(_copiedCellStyleLayerKey, hash);
431 clearTimeout(_clearCopyTI);
432 _clearCopyTI = setTimeout(function(){
433 _self.clearCopySelection();
434 }, 2000);
435 }
436
437 function clearCopySelection() {
438 _grid.removeCellCssStyles(_copiedCellStyleLayerKey);
439 }
440
441 $.extend(this, {
442 "init": init,
443 "destroy": destroy,
444 "pluginName": "CellExternalCopyManager",
445
446 "clearCopySelection": clearCopySelection,
447 "handleKeyDown":handleKeyDown,
448
449 "onCopyCells": new Slick.Event(),
450 "onCopyCancelled": new Slick.Event(),
451 "onPasteCells": new Slick.Event()
452 });
453 }
454})(jQuery);