oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

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