| 1 | /* Clipboard events and menu commands share atomic cell writes. */ |
| 2 | (function () { |
| 3 | 'use strict'; |
| 4 | |
| 5 | var surface = document.querySelector('[data-sheet-surface]'); |
| 6 | if (!surface) return; |
| 7 | |
| 8 | var MAX_CELLS = 500; |
| 9 | |
| 10 | function ready() { |
| 11 | var g = window.SheetsGrid; |
| 12 | if (!g) return; |
| 13 | |
| 14 | function say(text, tone) { |
| 15 | if (window.SheetsNote) window.SheetsNote.say(text, tone); |
| 16 | } |
| 17 | // The page names each chord in its platform's spelling (⌘C, Ctrl+C). |
| 18 | function key(name, fallback) { return surface.getAttribute('data-key-' + name) || fallback; } |
| 19 | |
| 20 | // Copy offsets relative formulas; Cut moves raw formulas without translation. |
| 21 | // Text equality is only provenance within this focused window. |
| 22 | var lastCopy = null, pendingCut = null; |
| 23 | |
| 24 | window.addEventListener('blur', function () { lastCopy = null; pendingCut = null; }); |
| 25 | document.addEventListener('keydown', function (e) { |
| 26 | if (e.key === 'Escape') pendingCut = null; |
| 27 | }, true); |
| 28 | |
| 29 | function box() { |
| 30 | var s = g.selection(); |
| 31 | if (!s) return null; |
| 32 | return { |
| 33 | r1: Math.min(s.r, s.r2), c1: Math.min(s.c, s.c2), |
| 34 | r2: Math.max(s.r, s.r2), c2: Math.max(s.c, s.c2) |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | var TSV = window.SheetsTSV; |
| 39 | if (!TSV) return; |
| 40 | |
| 41 | var writeAll = window.SheetsCellQueue; |
| 42 | if (!writeAll) return; |
| 43 | |
| 44 | function copyText() { |
| 45 | pendingCut = null; |
| 46 | var b = box(); |
| 47 | if (!b) return null; |
| 48 | var text = TSV.encode(b, g.rawAt); |
| 49 | lastCopy = { text: text, r: b.r1, c: b.c1 }; |
| 50 | return text; |
| 51 | } |
| 52 | |
| 53 | // Cut is a pending move. Nothing is deleted until the atomic paste succeeds. |
| 54 | function rememberCut(text, b) { |
| 55 | lastCopy = null; |
| 56 | pendingCut = { text: text, box: b }; |
| 57 | } |
| 58 | |
| 59 | function paste(text) { |
| 60 | if (!g.mayWrite) return; |
| 61 | var b = box(); |
| 62 | if (!b || !text) return; |
| 63 | var rows = TSV.decode(text); |
| 64 | var move = pendingCut && pendingCut.text === text ? pendingCut : null; |
| 65 | if (!move) pendingCut = null; |
| 66 | var from = (lastCopy && lastCopy.text === text) ? lastCopy : null; |
| 67 | var count = rows.length * (rows[0] ? rows[0].length : 0); |
| 68 | if (count > MAX_CELLS) { |
| 69 | say('That is ' + count + ' cells, and ' + MAX_CELLS + ' is the most this can paste at once.'); |
| 70 | return; |
| 71 | } |
| 72 | var jobs = [], dropped = 0, i, j, r, c, cell; |
| 73 | for (i = 0; i < rows.length; i++) { |
| 74 | for (j = 0; j < rows[i].length; j++) { |
| 75 | r = b.r1 + i; |
| 76 | c = b.c1 + j; |
| 77 | if (r >= g.rows || c >= g.cols) { dropped++; continue; } |
| 78 | cell = { r: r, c: c, raw: rows[i][j] }; |
| 79 | |
| 80 | if (from) { cell.fromR = from.r + i; cell.fromC = from.c + j; } |
| 81 | jobs.push(cell); |
| 82 | } |
| 83 | } |
| 84 | if (move) { |
| 85 | if (move.busy) return; |
| 86 | if (dropped) { |
| 87 | say('This move extends beyond the sheet. Nothing was changed.', 'warn'); |
| 88 | return; |
| 89 | } |
| 90 | var source = move.box; |
| 91 | for (r = source.r1; r <= source.r2; r++) { |
| 92 | for (c = source.c1; c <= source.c2; c++) { |
| 93 | // Destinations win when the source and destination overlap. |
| 94 | if (r < b.r1 || r >= b.r1 + rows.length || |
| 95 | c < b.c1 || c >= b.c1 + rows[0].length) { |
| 96 | jobs.push({ r: r, c: c, raw: '' }); |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | if (jobs.length > MAX_CELLS) { |
| 101 | say('This move exceeds the 500-cell atomic batch limit. Nothing was changed.', 'warn'); |
| 102 | return; |
| 103 | } |
| 104 | move.busy = true; |
| 105 | } |
| 106 | function post(path, body) { |
| 107 | if (move && TSV.encode(move.box, g.rawAt) !== move.text) { |
| 108 | say('The cut cells changed. Cut them again before moving them.', 'warn'); |
| 109 | return Promise.resolve(false); |
| 110 | } |
| 111 | return g.post(path, body); |
| 112 | } |
| 113 | writeAll(jobs, post, function () { |
| 114 | if (pendingCut === move) pendingCut = null; |
| 115 | g.focus(); |
| 116 | |
| 117 | if (dropped) { |
| 118 | say(dropped + (dropped === 1 ? ' cell' : ' cells') + |
| 119 | ' fell outside this sheet and were not pasted.', 'warn'); |
| 120 | } |
| 121 | }).then(function () { if (move) move.busy = false; }); |
| 122 | } |
| 123 | |
| 124 | function inTextField(el) { |
| 125 | return !!el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || |
| 126 | el.isContentEditable); |
| 127 | } |
| 128 | |
| 129 | function own(e) { |
| 130 | if (inTextField(e.target)) return false; |
| 131 | if (!e.clipboardData) return false; |
| 132 | |
| 133 | if (!g.selection()) return false; |
| 134 | var pageSel = window.getSelection && window.getSelection(); |
| 135 | if (pageSel && !pageSel.isCollapsed) return false; |
| 136 | e.preventDefault(); |
| 137 | e.stopImmediatePropagation(); |
| 138 | return true; |
| 139 | } |
| 140 | |
| 141 | // Stop the grid's keyboard handler, but allow native clipboard events. |
| 142 | document.addEventListener('keydown', function (e) { |
| 143 | if (!(e.ctrlKey || e.metaKey) || e.altKey || e.shiftKey) return; |
| 144 | var k = e.key.toLowerCase(); |
| 145 | if (k !== 'c' && k !== 'x' && k !== 'v') return; |
| 146 | if (inTextField(e.target)) return; |
| 147 | if (!g.selection()) return; |
| 148 | e.stopImmediatePropagation(); |
| 149 | }, true); |
| 150 | |
| 151 | // Even copying inside a text field cancels a previous pending move. |
| 152 | document.addEventListener('copy', function (e) { |
| 153 | pendingCut = null; |
| 154 | lastCopy = null; |
| 155 | if (!own(e)) return; |
| 156 | var text = copyText(); |
| 157 | if (text !== null) e.clipboardData.setData('text/plain', text); |
| 158 | }, true); |
| 159 | |
| 160 | document.addEventListener('cut', function (e) { |
| 161 | pendingCut = null; |
| 162 | lastCopy = null; |
| 163 | if (!own(e)) return; |
| 164 | if (!g.mayWrite) return; |
| 165 | var text = copyText(), b = box(); |
| 166 | if (text === null) return; |
| 167 | |
| 168 | lastCopy = null; |
| 169 | e.clipboardData.setData('text/plain', text); |
| 170 | rememberCut(text, b); |
| 171 | }, true); |
| 172 | |
| 173 | document.addEventListener('paste', function (e) { |
| 174 | if (!own(e)) return; |
| 175 | if (!g.mayWrite) return; |
| 176 | paste(e.clipboardData.getData('text/plain')); |
| 177 | }, true); |
| 178 | |
| 179 | window.SheetsClipboard = { |
| 180 | |
| 181 | copy: function () { |
| 182 | g.focus(); |
| 183 | if (document.execCommand && document.execCommand('copy')) return; |
| 184 | var text = copyText(); |
| 185 | if (text === null || !navigator.clipboard) return; |
| 186 | navigator.clipboard.writeText(text).catch(function () { |
| 187 | say('This browser would not let the menu copy. Press ' + key('copy', 'Ctrl+C') + ' instead.', 'warn'); |
| 188 | }); |
| 189 | }, |
| 190 | cut: function () { |
| 191 | if (!g.mayWrite) return; |
| 192 | g.focus(); |
| 193 | if (document.execCommand && document.execCommand('cut')) return; |
| 194 | var text = copyText(), b = box(); |
| 195 | lastCopy = null; |
| 196 | if (text === null || !navigator.clipboard) return; |
| 197 | navigator.clipboard.writeText(text).then(function () { |
| 198 | rememberCut(text, b); |
| 199 | }, function () { |
| 200 | say('This browser would not let the menu cut. Press ' + key('cut', 'Ctrl+X') + ' instead.', 'warn'); |
| 201 | }); |
| 202 | }, |
| 203 | paste: function () { |
| 204 | if (!navigator.clipboard || !navigator.clipboard.readText) { |
| 205 | say('This browser only pastes from the keyboard. Press ' + key('paste', 'Ctrl+V') + '.', 'warn'); |
| 206 | return; |
| 207 | } |
| 208 | navigator.clipboard.readText().then(paste, function () { |
| 209 | say('Reading the clipboard needs permission this browser did not give. Press ' + key('paste', 'Ctrl+V') + ' instead.', 'warn'); |
| 210 | }); |
| 211 | } |
| 212 | }; |
| 213 | } |
| 214 | |
| 215 | if (window.SheetsGrid) ready(); |
| 216 | else document.addEventListener('sheets:ready', ready, { once: true }); |
| 217 | }()); |
| 218 | |