@seliseblocks/mailcraft 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/DOCS.md +8 -0
- package/dist/mailcraft-editor.bundle.js +69 -57
- package/dist/mailcraft-editor.bundle.js.map +3 -3
- package/package.json +2 -2
- package/src/core/code-tools.js +302 -0
- package/src/core/editor-core.js +61 -0
- package/src/core/export.js +5 -2
- package/src/core/i18n/ar.js +8 -0
- package/src/core/i18n/bg.js +8 -0
- package/src/core/i18n/bn.js +8 -0
- package/src/core/i18n/ca.js +8 -0
- package/src/core/i18n/cs.js +8 -0
- package/src/core/i18n/da.js +8 -0
- package/src/core/i18n/de-CH.js +8 -0
- package/src/core/i18n/de.js +8 -0
- package/src/core/i18n/dz.js +8 -0
- package/src/core/i18n/el.js +8 -0
- package/src/core/i18n/en.js +8 -0
- package/src/core/i18n/es.js +8 -0
- package/src/core/i18n/et.js +8 -0
- package/src/core/i18n/fi.js +8 -0
- package/src/core/i18n/fr.js +8 -0
- package/src/core/i18n/hr.js +8 -0
- package/src/core/i18n/hu.js +8 -0
- package/src/core/i18n/index.js +83 -83
- package/src/core/i18n/it.js +8 -0
- package/src/core/i18n/lt.js +8 -0
- package/src/core/i18n/lv.js +8 -0
- package/src/core/i18n/nb.js +8 -0
- package/src/core/i18n/nl.js +8 -0
- package/src/core/i18n/pl.js +8 -0
- package/src/core/i18n/pt.js +8 -0
- package/src/core/i18n/ro.js +8 -0
- package/src/core/i18n/ru.js +8 -0
- package/src/core/i18n/sk.js +8 -0
- package/src/core/i18n/sl.js +8 -0
- package/src/core/i18n/sv.js +8 -0
- package/src/core/i18n/tr.js +8 -0
- package/src/core/i18n/uk.js +8 -0
- package/src/core/icons.js +2 -0
- package/src/core/ids.js +1 -1
- package/src/core/layout-style.js +100 -100
- package/src/core/parse.js +10 -10
- package/src/core/placeholder.js +15 -15
- package/src/core/variables.js +11 -11
- package/src/mailcraft-editor.js +468 -16
- package/src/render/focus-preserve.js +158 -158
- package/src/render/rte.js +27 -2
- package/src/render/story.js +415 -415
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seliseblocks/mailcraft",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Framework-agnostic drag-and-drop email template editor, packaged as a zero-dependency Web Component.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SELISE Digital Platforms",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
62
|
"build": "node build.js",
|
|
63
|
-
"test": "node test/storage.test.mjs && node test/export.test.mjs && node test/templates.test.mjs && node test/system.test.mjs && node test/toolbar.test.mjs && node test/editor-dom.test.mjs && node test/css-cascade.test.mjs && node test/render-dom.test.mjs && node test/import-html.test.mjs && node test/interaction.test.mjs && node test/focus-preserve.test.mjs && node test/shortcuts.test.mjs && node test/import-structure.test.mjs && node test/roundtrip.test.mjs",
|
|
63
|
+
"test": "node test/storage.test.mjs && node test/export.test.mjs && node test/templates.test.mjs && node test/system.test.mjs && node test/toolbar.test.mjs && node test/editor-dom.test.mjs && node test/css-cascade.test.mjs && node test/render-dom.test.mjs && node test/import-html.test.mjs && node test/interaction.test.mjs && node test/focus-preserve.test.mjs && node test/shortcuts.test.mjs && node test/import-structure.test.mjs && node test/roundtrip.test.mjs && node test/code-tools.test.mjs",
|
|
64
64
|
"prepublishOnly": "npm run build && npm test",
|
|
65
65
|
"coverage": "c8 --reporter=text --reporter=text-summary --src=src --all --exclude=\"test/**\" --exclude=\"examples/**\" --exclude=\"build.js\" npm test"
|
|
66
66
|
},
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure string helpers behind the Code view's editor features: a conservative
|
|
3
|
+
* HTML pretty-printer (Format), a tag scanner that powers the code <-> preview
|
|
4
|
+
* inspect link, plain-text search, and <mark> insertion into already
|
|
5
|
+
* syntax-highlighted lines. No DOM anywhere -- everything here is
|
|
6
|
+
* string-in/string-out so it runs (and is tested) in bare Node.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
10
|
+
|
|
11
|
+
/** Raw-text elements: the parser treats their contents as text, so the scanner and formatter must never read tags inside them. */
|
|
12
|
+
const RAW_TAGS = new Set(['style', 'script', 'textarea', 'title']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The only tags between which the formatter may *invent* a line break where
|
|
16
|
+
* the source had no whitespace at all. Chosen because whitespace-only text is
|
|
17
|
+
* invisible there: table internals (the parser lifts it out of the row grid),
|
|
18
|
+
* and metadata that never renders. Deliberately absent: div, span, a, img --
|
|
19
|
+
* whitespace between inline or inline-block elements paints as a visible gap,
|
|
20
|
+
* and hybrid email columns (inline-block divs) actually break on a stray
|
|
21
|
+
* space, so those boundaries only ever break where whitespace already existed.
|
|
22
|
+
*/
|
|
23
|
+
const SAFE_BREAK = new Set(['html', 'head', 'body', 'meta', 'title', 'link', 'style', 'script', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'ul', 'ol', 'li']);
|
|
24
|
+
|
|
25
|
+
/** whitespace-significant containers: everything inside is emitted verbatim. */
|
|
26
|
+
const PRE_TAGS = new Set(['pre', 'textarea']);
|
|
27
|
+
|
|
28
|
+
/** Index of the `>` closing the tag that starts before `from`, skipping quoted attribute values (`<img alt="a > b">`). -1 when the tag never closes. */
|
|
29
|
+
function tagEnd(s, from) {
|
|
30
|
+
let quote = '';
|
|
31
|
+
for (let j = from; j < s.length; j++) {
|
|
32
|
+
const ch = s[j];
|
|
33
|
+
if (quote) { if (ch === quote) quote = ''; }
|
|
34
|
+
else if (ch === '"' || ch === "'") quote = ch;
|
|
35
|
+
else if (ch === '>') return j;
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TAG_NAME = /^[a-zA-Z][a-zA-Z0-9-]*/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lexes html into { kind, text, tag?, selfClose? } tokens: 'tag' (an open
|
|
44
|
+
* tag), 'close', 'comment' (whole comment, conditional comments included),
|
|
45
|
+
* 'decl' (doctype / CDATA / processing instruction), 'raw' (the verbatim
|
|
46
|
+
* contents of a raw-text element) and 'text'. Lenient by design -- a stray
|
|
47
|
+
* `<` stays part of the surrounding text.
|
|
48
|
+
*/
|
|
49
|
+
function tokenize(src) {
|
|
50
|
+
const s = String(src == null ? '' : src);
|
|
51
|
+
const lower = s.toLowerCase();
|
|
52
|
+
const n = s.length;
|
|
53
|
+
const tokens = [];
|
|
54
|
+
const pushText = (from, to) => { if (to > from) tokens.push({ kind: 'text', text: s.slice(from, to) }); };
|
|
55
|
+
let last = 0;
|
|
56
|
+
let i = 0;
|
|
57
|
+
while (i < n) {
|
|
58
|
+
const lt = s.indexOf('<', i);
|
|
59
|
+
if (lt === -1) break;
|
|
60
|
+
const c = s[lt + 1];
|
|
61
|
+
if (c === '!' || c === '?') {
|
|
62
|
+
let end; let kind = 'decl';
|
|
63
|
+
if (s.startsWith('<!--', lt)) { const at = s.indexOf('-->', lt + 4); end = at === -1 ? n : at + 3; kind = 'comment'; }
|
|
64
|
+
else { const at = tagEnd(s, lt + 1); end = at === -1 ? n : at + 1; }
|
|
65
|
+
pushText(last, lt);
|
|
66
|
+
tokens.push({ kind, text: s.slice(lt, end) });
|
|
67
|
+
last = i = end;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (c === '/' && TAG_NAME.test(s.slice(lt + 2, lt + 3))) {
|
|
71
|
+
const at = tagEnd(s, lt + 2);
|
|
72
|
+
const end = at === -1 ? n : at + 1;
|
|
73
|
+
const tag = TAG_NAME.exec(lower.slice(lt + 2, lt + 32))[0];
|
|
74
|
+
pushText(last, lt);
|
|
75
|
+
tokens.push({ kind: 'close', tag, text: s.slice(lt, end) });
|
|
76
|
+
last = i = end;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c && TAG_NAME.test(c)) {
|
|
80
|
+
const at = tagEnd(s, lt + 1);
|
|
81
|
+
const end = at === -1 ? n : at + 1;
|
|
82
|
+
const tag = TAG_NAME.exec(lower.slice(lt + 1, lt + 32))[0];
|
|
83
|
+
const selfClose = s[end - 2] === '/';
|
|
84
|
+
pushText(last, lt);
|
|
85
|
+
tokens.push({ kind: 'tag', tag, text: s.slice(lt, end), selfClose });
|
|
86
|
+
last = i = end;
|
|
87
|
+
if (RAW_TAGS.has(tag) && !selfClose) {
|
|
88
|
+
const closeAt = lower.indexOf('</' + tag, end);
|
|
89
|
+
const stop = closeAt === -1 ? n : closeAt;
|
|
90
|
+
if (stop > end) tokens.push({ kind: 'raw', tag, text: s.slice(end, stop) });
|
|
91
|
+
last = i = stop;
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
i = lt + 1;
|
|
96
|
+
}
|
|
97
|
+
pushText(last, n);
|
|
98
|
+
return tokens;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A text run that is exactly one handlebars block tag ({{#if …}}, {{/each}}) -- worth its own line, like the structural rows it usually sits between. */
|
|
102
|
+
const isLogicToken = (text) => /^\{\{[#/][^{}]*\}\}$/.test(text);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pretty-prints html without changing what it renders: two-space indentation,
|
|
106
|
+
* one structural tag per line. The rules that keep it rendering-identical:
|
|
107
|
+
*
|
|
108
|
+
* 1. Non-whitespace text is copied byte for byte -- never reflowed.
|
|
109
|
+
* 2. Whitespace between nodes is only *normalized* (to newline + indent)
|
|
110
|
+
* where whitespace already existed; a break is *invented* only between
|
|
111
|
+
* SAFE_BREAK tags, where whitespace-only text is invisible.
|
|
112
|
+
* 3. Raw-text contents (<style>, <script>) and everything inside <pre> /
|
|
113
|
+
* <textarea> pass through verbatim.
|
|
114
|
+
* 4. Comments are atomic, so MSO conditionals survive whole.
|
|
115
|
+
*
|
|
116
|
+
* Idempotent: formatting formatted output returns it unchanged.
|
|
117
|
+
*/
|
|
118
|
+
export function formatHtml(src) {
|
|
119
|
+
const tokens = tokenize(src);
|
|
120
|
+
// Split text into whitespace gaps (formatting material) and content cores (untouchable).
|
|
121
|
+
const items = [];
|
|
122
|
+
for (const tk of tokens) {
|
|
123
|
+
if (tk.kind !== 'text') { items.push(tk); continue; }
|
|
124
|
+
const m = /^(\s*)([\s\S]*?)(\s*)$/.exec(tk.text);
|
|
125
|
+
if (m[2] === '') { items.push({ kind: 'gap', text: tk.text }); continue; }
|
|
126
|
+
if (m[1]) items.push({ kind: 'gap', text: m[1] });
|
|
127
|
+
items.push({ kind: 'text', text: m[2] });
|
|
128
|
+
if (m[3]) items.push({ kind: 'gap', text: m[3] });
|
|
129
|
+
}
|
|
130
|
+
const breaky = (it) => !!it && (
|
|
131
|
+
((it.kind === 'tag' || it.kind === 'close') && SAFE_BREAK.has(it.tag))
|
|
132
|
+
|| it.kind === 'decl'
|
|
133
|
+
|| (it.kind === 'text' && isLogicToken(it.text))
|
|
134
|
+
);
|
|
135
|
+
let out = '';
|
|
136
|
+
let started = false;
|
|
137
|
+
const stack = [];
|
|
138
|
+
let prev = null;
|
|
139
|
+
let gap = null;
|
|
140
|
+
for (const it of items) {
|
|
141
|
+
if (it.kind === 'gap') { gap = (gap || '') + it.text; continue; }
|
|
142
|
+
if (it.kind === 'close') {
|
|
143
|
+
const at = stack.lastIndexOf(it.tag);
|
|
144
|
+
if (at !== -1) stack.length = at; // implicitly closes unclosed children too
|
|
145
|
+
}
|
|
146
|
+
const inPre = stack.some((tag) => PRE_TAGS.has(tag));
|
|
147
|
+
const brk = started && !inPre && (
|
|
148
|
+
(gap !== null && gap !== '')
|
|
149
|
+
|| (breaky(prev) && breaky(it))
|
|
150
|
+
|| (breaky(prev) && it.kind === 'comment')
|
|
151
|
+
|| (prev && prev.kind === 'comment' && breaky(it))
|
|
152
|
+
);
|
|
153
|
+
if (started && brk) out += '\n' + ' '.repeat(stack.length);
|
|
154
|
+
else if (started && gap) out += gap; // an inline gap survives verbatim
|
|
155
|
+
out += it.text;
|
|
156
|
+
if (it.kind === 'tag' && !it.selfClose && !VOID_TAGS.has(it.tag) && !RAW_TAGS.has(it.tag)) stack.push(it.tag);
|
|
157
|
+
prev = it;
|
|
158
|
+
gap = null;
|
|
159
|
+
started = true;
|
|
160
|
+
}
|
|
161
|
+
if (!started) return String(src == null ? '' : src);
|
|
162
|
+
return out.endsWith('\n') ? out : out + '\n';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scans html for elements without parsing it into a DOM. Returns
|
|
167
|
+
* `{ els, byTag }`: `els` in document order, each
|
|
168
|
+
* `{ tag, nth, openStart, openEnd, closeStart, closeEnd, parent }` where
|
|
169
|
+
* `nth` is its index among same-tag elements (the key the inspect link uses
|
|
170
|
+
* to find the same element in the preview document, immune to the parser's
|
|
171
|
+
* inserted <tbody>s), `parent` is an index into `els` (-1 at the root), and
|
|
172
|
+
* offsets are into the source string. Unclosed elements end where their
|
|
173
|
+
* parent closes (or at EOF); raw-text contents are skipped, so a `</td>`
|
|
174
|
+
* inside a style string is never mistaken for markup.
|
|
175
|
+
*/
|
|
176
|
+
export function scanElements(src) {
|
|
177
|
+
const s = String(src == null ? '' : src);
|
|
178
|
+
const lower = s.toLowerCase();
|
|
179
|
+
const n = s.length;
|
|
180
|
+
const els = [];
|
|
181
|
+
const byTag = Object.create(null);
|
|
182
|
+
const stack = [];
|
|
183
|
+
let i = 0;
|
|
184
|
+
while (i < n) {
|
|
185
|
+
const lt = s.indexOf('<', i);
|
|
186
|
+
if (lt === -1) break;
|
|
187
|
+
const c = s[lt + 1];
|
|
188
|
+
if (s.startsWith('<!--', lt)) { const at = s.indexOf('-->', lt + 4); i = at === -1 ? n : at + 3; continue; }
|
|
189
|
+
if (c === '!' || c === '?') { const at = tagEnd(s, lt + 1); i = at === -1 ? n : at + 1; continue; }
|
|
190
|
+
if (c === '/' && TAG_NAME.test(s.slice(lt + 2, lt + 3))) {
|
|
191
|
+
const at = tagEnd(s, lt + 2);
|
|
192
|
+
const end = at === -1 ? n : at + 1;
|
|
193
|
+
const tag = TAG_NAME.exec(lower.slice(lt + 2, lt + 32))[0];
|
|
194
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
195
|
+
if (els[stack[k]].tag !== tag) continue;
|
|
196
|
+
while (stack.length > k + 1) { const idx = stack.pop(); els[idx].closeStart = els[idx].closeEnd = lt; }
|
|
197
|
+
const idx = stack.pop();
|
|
198
|
+
els[idx].closeStart = lt;
|
|
199
|
+
els[idx].closeEnd = end;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
i = end;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (c && TAG_NAME.test(c)) {
|
|
206
|
+
const at = tagEnd(s, lt + 1);
|
|
207
|
+
const end = at === -1 ? n : at + 1;
|
|
208
|
+
const tag = TAG_NAME.exec(lower.slice(lt + 1, lt + 32))[0];
|
|
209
|
+
const selfClose = s[end - 2] === '/';
|
|
210
|
+
const list = byTag[tag] || (byTag[tag] = []);
|
|
211
|
+
const el = { tag, nth: list.length, openStart: lt, openEnd: end, closeStart: end, closeEnd: end, parent: stack.length ? stack[stack.length - 1] : -1 };
|
|
212
|
+
els.push(el);
|
|
213
|
+
list.push(el);
|
|
214
|
+
i = end;
|
|
215
|
+
if (selfClose || VOID_TAGS.has(tag)) continue;
|
|
216
|
+
if (RAW_TAGS.has(tag)) {
|
|
217
|
+
const closeAt = lower.indexOf('</' + tag, end);
|
|
218
|
+
if (closeAt === -1) { el.closeStart = el.closeEnd = n; i = n; }
|
|
219
|
+
else {
|
|
220
|
+
const cgt = tagEnd(s, closeAt + 2);
|
|
221
|
+
el.closeStart = closeAt;
|
|
222
|
+
el.closeEnd = cgt === -1 ? n : cgt + 1;
|
|
223
|
+
i = el.closeEnd;
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
stack.push(els.length - 1);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
i = lt + 1;
|
|
231
|
+
}
|
|
232
|
+
while (stack.length) { const idx = stack.pop(); els[idx].closeStart = els[idx].closeEnd = n; }
|
|
233
|
+
return { els, byTag };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The innermost element whose source range contains character `offset`, or null. */
|
|
237
|
+
export function elementAtOffset(scan, offset) {
|
|
238
|
+
let best = null;
|
|
239
|
+
for (const e of scan.els) {
|
|
240
|
+
if (e.openStart > offset) break; // els are in openStart order
|
|
241
|
+
if (offset < Math.max(e.closeEnd, e.openEnd)) best = e;
|
|
242
|
+
}
|
|
243
|
+
return best;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Case-insensitive plain-text occurrences of `query` in `src`, capped so a
|
|
248
|
+
* one-letter query on a huge document cannot stall the repaint. Falls back to
|
|
249
|
+
* case-sensitive matching for the rare locale-sensitive strings whose
|
|
250
|
+
* lowercase form changes length (e.g. 'İ'), where offsets would drift.
|
|
251
|
+
*/
|
|
252
|
+
export function findMatches(src, query, cap = 5000) {
|
|
253
|
+
const out = [];
|
|
254
|
+
const raw = String(src == null ? '' : src);
|
|
255
|
+
const q0 = String(query == null ? '' : query);
|
|
256
|
+
if (!q0) return out;
|
|
257
|
+
let s = raw.toLowerCase();
|
|
258
|
+
let q = q0.toLowerCase();
|
|
259
|
+
if (s.length !== raw.length || q.length !== q0.length) { s = raw; q = q0; }
|
|
260
|
+
let i = 0;
|
|
261
|
+
while (out.length < cap) {
|
|
262
|
+
const at = s.indexOf(q, i);
|
|
263
|
+
if (at === -1) break;
|
|
264
|
+
out.push({ start: at, end: at + q.length });
|
|
265
|
+
i = at + q.length;
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const MARK_CSS = 'background:rgba(245,158,11,0.28);border-radius:2px;color:inherit;padding:0;margin:0';
|
|
271
|
+
const MARK_CUR_CSS = 'background:rgba(245,158,11,0.6);outline:1.5px solid rgba(180,83,9,0.85);border-radius:2px;color:inherit;padding:0;margin:0';
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Inserts <mark> around `ranges` (offsets into the *text* of one source line,
|
|
275
|
+
* `{ start, end, cur }`) into that line's already-highlighted html. Walks the
|
|
276
|
+
* html counting text positions -- an entity counts as the one source character
|
|
277
|
+
* it escapes, tags count as nothing -- and closes/reopens marks at tag
|
|
278
|
+
* boundaries so the highlighter's own spans stay properly nested.
|
|
279
|
+
*/
|
|
280
|
+
export function markRanges(html, ranges) {
|
|
281
|
+
if (!ranges || !ranges.length || !html) return html;
|
|
282
|
+
const token = /(<[^>]*>)|(&[a-zA-Z][a-zA-Z0-9]*;|&#[0-9]+;|&#x[0-9a-fA-F]+;)|([\s\S])/g;
|
|
283
|
+
let out = '';
|
|
284
|
+
let pos = 0;
|
|
285
|
+
let open = null;
|
|
286
|
+
let m;
|
|
287
|
+
while ((m = token.exec(html))) {
|
|
288
|
+
if (m[1]) {
|
|
289
|
+
if (open) { out += '</mark>'; open = null; }
|
|
290
|
+
out += m[1];
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
let r = null;
|
|
294
|
+
for (const range of ranges) { if (pos >= range.start && pos < range.end) { r = range; break; } }
|
|
295
|
+
if (open && open !== r) { out += '</mark>'; open = null; }
|
|
296
|
+
if (r && !open) { out += '<mark style="' + (r.cur ? MARK_CUR_CSS : MARK_CSS) + '">'; open = r; }
|
|
297
|
+
out += m[0];
|
|
298
|
+
pos += 1;
|
|
299
|
+
}
|
|
300
|
+
if (open) out += '</mark>';
|
|
301
|
+
return out;
|
|
302
|
+
}
|
package/src/core/editor-core.js
CHANGED
|
@@ -423,11 +423,48 @@ export class EditorCore {
|
|
|
423
423
|
if (this.onFormatChange) this.onFormatChange();
|
|
424
424
|
};
|
|
425
425
|
document.addEventListener('selectionchange', this.onSelect);
|
|
426
|
+
/**
|
|
427
|
+
* Click-outside fallback for closing the RTE toolbar. The blur path
|
|
428
|
+
* (`blockCtx.onBlur`, canvas.js) only runs if the edited block still holds
|
|
429
|
+
* focus at the moment of the outside press -- but several toolbar controls
|
|
430
|
+
* legitimately move focus to themselves (the Text style / Merge Tags
|
|
431
|
+
* selects, the color inputs, the link popover's href field). Dismiss one
|
|
432
|
+
* of those without committing and no block blur can ever fire again, so
|
|
433
|
+
* `state.editing` -- and the toolbar -- stayed open no matter where the
|
|
434
|
+
* user clicked. A completed click whose composed path contains neither the
|
|
435
|
+
* edited block nor the toolbar closes the edit explicitly.
|
|
436
|
+
*
|
|
437
|
+
* `click`, deliberately not `pointerdown`: by click time the press's
|
|
438
|
+
* native blur/focus transition has fully settled, so this never rebuilds
|
|
439
|
+
* the canvas mid-gesture (the dropped-click problem documented in
|
|
440
|
+
* `blockCtx.onFocus`, canvas.js) and never races focus-preserve into
|
|
441
|
+
* refocusing -- and thereby reopening -- the block it just closed. It also
|
|
442
|
+
* ignores scrollbar drags, which emit no click.
|
|
443
|
+
*/
|
|
444
|
+
this.onOutsideClick = (e) => {
|
|
445
|
+
if (!this.state.editing || this.rendering) return;
|
|
446
|
+
// A drag-selection that starts inside the block but ends outside it
|
|
447
|
+
// fires its click on a common ancestor -- but the block keeps focus
|
|
448
|
+
// through such a drag, while a genuine outside press blurs it first
|
|
449
|
+
// (and the blur pipeline has then already handled the close).
|
|
450
|
+
const active = this.exportRoot && this.exportRoot.activeElement;
|
|
451
|
+
if (active && active === this.editEl) return;
|
|
452
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
453
|
+
for (const n of path) {
|
|
454
|
+
if (!n || n.nodeType !== 1) continue;
|
|
455
|
+
if (n.getAttribute && n.getAttribute('data-mc-content') === this.state.editing) return;
|
|
456
|
+
if (n.hasAttribute && n.hasAttribute('data-rte-root')) return;
|
|
457
|
+
}
|
|
458
|
+
this.closeEditing();
|
|
459
|
+
};
|
|
460
|
+
if (this.exportRoot) this.exportRoot.addEventListener('click', this.onOutsideClick);
|
|
426
461
|
}
|
|
427
462
|
|
|
428
463
|
unmountKeyboard() {
|
|
429
464
|
window.removeEventListener('keydown', this.onKey);
|
|
430
465
|
document.removeEventListener('selectionchange', this.onSelect);
|
|
466
|
+
if (this.exportRoot && this.onOutsideClick) this.exportRoot.removeEventListener('click', this.onOutsideClick);
|
|
467
|
+
this.onOutsideClick = null;
|
|
431
468
|
}
|
|
432
469
|
|
|
433
470
|
unmount() {
|
|
@@ -450,6 +487,30 @@ export class EditorCore {
|
|
|
450
487
|
|
|
451
488
|
// ---- rich text editing ----------------------------------------------
|
|
452
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Explicitly ends the active rich-text edit: commits the live content the
|
|
492
|
+
* way `blockCtx.onBlur` (canvas.js) would, then clears `editing`/`linkDraft`.
|
|
493
|
+
* Used by the click-outside fallback (`mountKeyboard`), which fires exactly
|
|
494
|
+
* when the block no longer holds focus, so no blur will ever arrive to do
|
|
495
|
+
* this. Any blur this close itself provokes is deliberately swallowed via
|
|
496
|
+
* `rteActive`: the commit below is the single commit path -- letting onBlur
|
|
497
|
+
* also run would compare against the same `editOriginal` and push a second
|
|
498
|
+
* undo entry for the same change.
|
|
499
|
+
*/
|
|
500
|
+
closeEditing() {
|
|
501
|
+
const id = this.state.editing;
|
|
502
|
+
if (!id) return;
|
|
503
|
+
const elNode = this.editEl;
|
|
504
|
+
const val = elNode && elNode.isConnected && this.editKey
|
|
505
|
+
? (this.editPlain ? elNode.textContent : elNode.innerHTML)
|
|
506
|
+
: null;
|
|
507
|
+
this.rteActive = true;
|
|
508
|
+
if (elNode && this.exportRoot && this.exportRoot.activeElement === elNode) elNode.blur();
|
|
509
|
+
this.rteActive = false;
|
|
510
|
+
if (val !== null && val !== this.editOriginal) this.setProp(id, this.editKey, val);
|
|
511
|
+
if (this.state.editing === id) this.setState({ editing: null, linkDraft: null });
|
|
512
|
+
}
|
|
513
|
+
|
|
453
514
|
exec(cmd, arg) {
|
|
454
515
|
this.rteActive = true;
|
|
455
516
|
try {
|
package/src/core/export.js
CHANGED
|
@@ -114,12 +114,15 @@ export function decorateLogicTags(html) {
|
|
|
114
114
|
const chip = (kind, expr, end) => {
|
|
115
115
|
const color = kind === 'each' ? '#7c3aed' : '#0e7490';
|
|
116
116
|
const word = end ? (kind === 'each' ? 'END LOOP' : 'END IF') : (kind === 'each' ? 'REPEAT EACH' : 'SHOW IF');
|
|
117
|
-
|
|
117
|
+
// data-mc-deco marks decoration-only nodes so the code view's inspect
|
|
118
|
+
// link can skip them when counting elements (they exist only in the
|
|
119
|
+
// preview document, never in the source string being mapped).
|
|
120
|
+
return '<span data-mc-deco="" style="display:inline-flex;align-items:center;gap:7px;box-sizing:border-box;border:1.5px dashed ' + color + ';border-radius:7px;background:' + color + '14;padding:4px 10px;margin:2px 0;color:' + color + ';font-family:ui-monospace,monospace;font-size:9.5px;font-weight:700;letter-spacing:0.12em;">'
|
|
118
121
|
+ (end ? '⏶ ' : '⏷ ') + word
|
|
119
122
|
+ (expr ? ' <span style="font-weight:400;font-size:11px;letter-spacing:0;">{{ ' + esc(expr) + ' }}</span>' : '')
|
|
120
123
|
+ '</span>';
|
|
121
124
|
};
|
|
122
|
-
const bandRow = (kind, expr, end) => '<tr><td colspan="99" style="padding:2px 8px;">' + chip(kind, expr, end) + '</td></tr>';
|
|
125
|
+
const bandRow = (kind, expr, end) => '<tr><td colspan="99" data-mc-deco="" style="padding:2px 8px;">' + chip(kind, expr, end) + '</td></tr>';
|
|
123
126
|
let s = String(html || '');
|
|
124
127
|
// Between-row tags first (adjacent to a <tr> or after a </tr>), looped for
|
|
125
128
|
// the same adjacency reason as import-html's foldLogicWrappers.
|
package/src/core/i18n/ar.js
CHANGED
|
@@ -151,6 +151,14 @@ export const AR = {
|
|
|
151
151
|
'code.applyNote':
|
|
152
152
|
'يحلّل زر التطبيق كل صف جدول من المستوى الأعلى إلى صف لوحة يمكنك تحديده وإعادة ترتيبه وحذفه — الترميز المكتوب يدويًا يبقى سليمًا.',
|
|
153
153
|
'code.meta': '{kb} كيلوبايت · {lines} أسطر',
|
|
154
|
+
'code.format': 'تنسيق المصدر (المسافات البادئة فقط)',
|
|
155
|
+
'code.wrap': 'التفاف النص',
|
|
156
|
+
'code.find': 'بحث',
|
|
157
|
+
'code.replace': 'استبدال',
|
|
158
|
+
'code.replaceAll': 'استبدال الكل',
|
|
159
|
+
'code.prevMatch': 'النتيجة السابقة',
|
|
160
|
+
'code.nextMatch': 'النتيجة التالية',
|
|
161
|
+
'code.goToLine': 'الانتقال إلى سطر',
|
|
154
162
|
'toast.sourceReloaded': 'تمت إعادة تحميل المصدر من اللوحة',
|
|
155
163
|
'toast.sourceAppliedOne': 'تم تطبيق المصدر — صف واحد على اللوحة',
|
|
156
164
|
'toast.sourceAppliedMany': 'تم تطبيق المصدر — {rows} صفوف على اللوحة',
|
package/src/core/i18n/bg.js
CHANGED
|
@@ -131,6 +131,14 @@ export const BG = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Прилагането анализира всеки ред на таблица от най-високо ниво в ред на платното, който все още можете да избирате, преподреждате и изтривате — ръчно написаната маркировка преминава непроменена.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} реда',
|
|
134
|
+
'code.format': 'Форматиране на кода (само отстъпи)',
|
|
135
|
+
'code.wrap': 'Пренасяне на редове',
|
|
136
|
+
'code.find': 'Търсене',
|
|
137
|
+
'code.replace': 'Замяна',
|
|
138
|
+
'code.replaceAll': 'Замяна на всички',
|
|
139
|
+
'code.prevMatch': 'Предишно съвпадение',
|
|
140
|
+
'code.nextMatch': 'Следващо съвпадение',
|
|
141
|
+
'code.goToLine': 'Към ред',
|
|
134
142
|
'toast.sourceReloaded': 'Изходният код е презареден от платното',
|
|
135
143
|
'toast.sourceAppliedOne': 'Изходният код е приложен — 1 ред на платното',
|
|
136
144
|
'toast.sourceAppliedMany': 'Изходният код е приложен — {rows} реда на платното',
|
package/src/core/i18n/bn.js
CHANGED
|
@@ -150,6 +150,14 @@ export const BN = {
|
|
|
150
150
|
'code.applyNote':
|
|
151
151
|
'প্রয়োগ করলে প্রতিটি টেবিল সারি একটি ক্যানভাস সারিতে পার্স হয় যা আপনি এখনও নির্বাচন, পুনর্বিন্যাস ও মুছতে পারবেন — হাতে-লেখা মার্কআপ অক্ষত থাকে।',
|
|
152
152
|
'code.meta': '{kb} KB · {lines}টি লাইন',
|
|
153
|
+
'code.format': 'সোর্স ফরম্যাট করুন (শুধু ইন্ডেন্টেশন)',
|
|
154
|
+
'code.wrap': 'ওয়ার্ড র্যাপ',
|
|
155
|
+
'code.find': 'খুঁজুন',
|
|
156
|
+
'code.replace': 'প্রতিস্থাপন',
|
|
157
|
+
'code.replaceAll': 'সব প্রতিস্থাপন',
|
|
158
|
+
'code.prevMatch': 'আগের মিল',
|
|
159
|
+
'code.nextMatch': 'পরের মিল',
|
|
160
|
+
'code.goToLine': 'লাইনে যান',
|
|
153
161
|
'toast.sourceReloaded': 'ক্যানভাস থেকে সোর্স পুনরায় লোড হয়েছে',
|
|
154
162
|
'toast.sourceAppliedOne': 'সোর্স প্রয়োগ হয়েছে — ক্যানভাসে ১টি সারি',
|
|
155
163
|
'toast.sourceAppliedMany': 'সোর্স প্রয়োগ হয়েছে — ক্যানভাসে {rows}টি সারি',
|
package/src/core/i18n/ca.js
CHANGED
|
@@ -131,6 +131,14 @@ export const CA = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Aplicar analitza cada fila de taula de nivell superior en una fila de llenç que encara podeu seleccionar, reordenar i eliminar — el marcatge escrit a mà sobreviu al procés.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} línies',
|
|
134
|
+
'code.format': 'Formata el codi font (només sagnat)',
|
|
135
|
+
'code.wrap': 'Ajustament de línia',
|
|
136
|
+
'code.find': 'Cerca',
|
|
137
|
+
'code.replace': 'Substitueix',
|
|
138
|
+
'code.replaceAll': 'Substitueix-ho tot',
|
|
139
|
+
'code.prevMatch': 'Coincidència anterior',
|
|
140
|
+
'code.nextMatch': 'Coincidència següent',
|
|
141
|
+
'code.goToLine': 'Vés a la línia',
|
|
134
142
|
'toast.sourceReloaded': 'Font recarregada des del llenç',
|
|
135
143
|
'toast.sourceAppliedOne': 'Font aplicada — 1 fila al llenç',
|
|
136
144
|
'toast.sourceAppliedMany': 'Font aplicada — {rows} files al llenç',
|
package/src/core/i18n/cs.js
CHANGED
|
@@ -131,6 +131,14 @@ export const CS = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Použití rozebere každý řádek tabulky nejvyšší úrovně do řádku plátna, který lze stále vybírat, přeuspořádávat a mazat — ručně psaný kód proces přežije beze změny.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} řádků',
|
|
134
|
+
'code.format': 'Formátovat zdroj (pouze odsazení)',
|
|
135
|
+
'code.wrap': 'Zalamování řádků',
|
|
136
|
+
'code.find': 'Najít',
|
|
137
|
+
'code.replace': 'Nahradit',
|
|
138
|
+
'code.replaceAll': 'Nahradit vše',
|
|
139
|
+
'code.prevMatch': 'Předchozí shoda',
|
|
140
|
+
'code.nextMatch': 'Další shoda',
|
|
141
|
+
'code.goToLine': 'Přejít na řádek',
|
|
134
142
|
'toast.sourceReloaded': 'Zdroj znovu načten z plátna',
|
|
135
143
|
'toast.sourceAppliedOne': 'Zdroj použit — 1 řádek na plátně',
|
|
136
144
|
'toast.sourceAppliedMany': 'Zdroj použit — {rows} řádků na plátně',
|
package/src/core/i18n/da.js
CHANGED
|
@@ -131,6 +131,14 @@ export const DA = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Anvend fortolker hver tabelrække på øverste niveau til en lærredsrække, du stadig kan markere, omarrangere og slette — håndskrevet kode overlever processen uændret.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} linjer',
|
|
134
|
+
'code.format': 'Formatér kilden (kun indrykning)',
|
|
135
|
+
'code.wrap': 'Tekstombrydning',
|
|
136
|
+
'code.find': 'Find',
|
|
137
|
+
'code.replace': 'Erstat',
|
|
138
|
+
'code.replaceAll': 'Erstat alle',
|
|
139
|
+
'code.prevMatch': 'Forrige match',
|
|
140
|
+
'code.nextMatch': 'Næste match',
|
|
141
|
+
'code.goToLine': 'Gå til linje',
|
|
134
142
|
'toast.sourceReloaded': 'Kilde genindlæst fra lærredet',
|
|
135
143
|
'toast.sourceAppliedOne': 'Kilde anvendt — 1 række på lærredet',
|
|
136
144
|
'toast.sourceAppliedMany': 'Kilde anvendt — {rows} rækker på lærredet',
|
package/src/core/i18n/de-CH.js
CHANGED
|
@@ -131,6 +131,14 @@ export const DE_CH = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Beim Anwenden wird jede oberste Tabellenzeile in eine Canvas-Zeile umgewandelt, die du weiterhin auswählen, neu anordnen und löschen kannst — handgeschriebenes Markup übersteht den Vorgang unverändert.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} Zeilen',
|
|
134
|
+
'code.format': 'Quelltext formatieren (nur Einrückung)',
|
|
135
|
+
'code.wrap': 'Zeilenumbruch',
|
|
136
|
+
'code.find': 'Suchen',
|
|
137
|
+
'code.replace': 'Ersetzen',
|
|
138
|
+
'code.replaceAll': 'Alle ersetzen',
|
|
139
|
+
'code.prevMatch': 'Vorheriger Treffer',
|
|
140
|
+
'code.nextMatch': 'Nächster Treffer',
|
|
141
|
+
'code.goToLine': 'Gehe zu Zeile',
|
|
134
142
|
'toast.sourceReloaded': 'Quelltext aus der Canvas neu geladen',
|
|
135
143
|
'toast.sourceAppliedOne': 'Quelltext angewendet — 1 Zeile auf der Canvas',
|
|
136
144
|
'toast.sourceAppliedMany': 'Quelltext angewendet — {rows} Zeilen auf der Canvas',
|
package/src/core/i18n/de.js
CHANGED
|
@@ -131,6 +131,14 @@ export const DE = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Beim Anwenden wird jede oberste Tabellenzeile in eine Canvas-Zeile umgewandelt, die du weiterhin auswählen, neu anordnen und löschen kannst — handgeschriebenes Markup übersteht den Vorgang unverändert.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} Zeilen',
|
|
134
|
+
'code.format': 'Quelltext formatieren (nur Einrückung)',
|
|
135
|
+
'code.wrap': 'Zeilenumbruch',
|
|
136
|
+
'code.find': 'Suchen',
|
|
137
|
+
'code.replace': 'Ersetzen',
|
|
138
|
+
'code.replaceAll': 'Alle ersetzen',
|
|
139
|
+
'code.prevMatch': 'Vorheriger Treffer',
|
|
140
|
+
'code.nextMatch': 'Nächster Treffer',
|
|
141
|
+
'code.goToLine': 'Gehe zu Zeile',
|
|
134
142
|
'toast.sourceReloaded': 'Quelltext aus der Canvas neu geladen',
|
|
135
143
|
'toast.sourceAppliedOne': 'Quelltext angewendet — 1 Zeile auf der Canvas',
|
|
136
144
|
'toast.sourceAppliedMany': 'Quelltext angewendet — {rows} Zeilen auf der Canvas',
|
package/src/core/i18n/dz.js
CHANGED
|
@@ -153,6 +153,14 @@ export const DZ = {
|
|
|
153
153
|
'code.applyNote':
|
|
154
154
|
'ལག་ལེན་འཐབ་པའི་སྐབས་ཐིག་ཁྲམ་ཐིག་ཁྲམ་རེ་རེ་སེལ་འཐུ, གོ་རིམ་བསྒྱུར, ད་རུང་བསུབ་བཏུབ་པའི་ཐིག་ཁྲམ་ཅིག་ལུ་བཤད་བཤག — ལག་གིས་བྲིས་པའི་མཱརཀ་པ་ཧེ་མ་བཟུམ་ལུས་འོང༌།',
|
|
155
155
|
'code.meta': '{kb} KB · ཐིག་ {lines}',
|
|
156
|
+
'code.format': 'རྩ་འཛིན་བཟོ་རྣམ་སྒྲིག་ནི (ནང་བསྐུམ་རྐྱངམ་གཅིག)',
|
|
157
|
+
'code.wrap': 'ཚིག་ཐིག་བསྒྱིར་ནི',
|
|
158
|
+
'code.find': 'འཚོལ་ཞིབ',
|
|
159
|
+
'code.replace': 'ཚབ་བཙུགས',
|
|
160
|
+
'code.replaceAll': 'ཆ་མཉམ་ཚབ་བཙུགས',
|
|
161
|
+
'code.prevMatch': 'ཧེ་མའི་མཐུན་པ',
|
|
162
|
+
'code.nextMatch': 'ཤུལ་མའི་མཐུན་པ',
|
|
163
|
+
'code.goToLine': 'ཐིག་ལུ་འགྱོ',
|
|
156
164
|
'toast.sourceReloaded': 'ཐིག་ཁྲམ་ནང་ལས་རྩ་འཛིན་སླར་འདྲེན་བྱུང',
|
|
157
165
|
'toast.sourceAppliedOne': 'རྩ་འཛིན་ལག་ལེན་འཐབ་ཡོད — ཐིག་ཁྲམ་ནང་ཐིག་ ༡',
|
|
158
166
|
'toast.sourceAppliedMany': 'རྩ་འཛིན་ལག་ལེན་འཐབ་ཡོད — ཐིག་ཁྲམ་ནང་ཐིག་ {rows}',
|
package/src/core/i18n/el.js
CHANGED
|
@@ -131,6 +131,14 @@ export const EL = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Η εφαρμογή αναλύει κάθε γραμμή πίνακα ανώτατου επιπέδου σε γραμμή καμβά που μπορείτε ακόμη να επιλέξετε, να αναδιατάξετε και να διαγράψετε — ο χειρόγραφος κώδικας επιβιώνει αναλλοίωτος.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} γραμμές',
|
|
134
|
+
'code.format': 'Μορφοποίηση πηγαίου κώδικα (μόνο εσοχές)',
|
|
135
|
+
'code.wrap': 'Αναδίπλωση λέξεων',
|
|
136
|
+
'code.find': 'Εύρεση',
|
|
137
|
+
'code.replace': 'Αντικατάσταση',
|
|
138
|
+
'code.replaceAll': 'Αντικατάσταση όλων',
|
|
139
|
+
'code.prevMatch': 'Προηγούμενη αντιστοιχία',
|
|
140
|
+
'code.nextMatch': 'Επόμενη αντιστοιχία',
|
|
141
|
+
'code.goToLine': 'Μετάβαση στη γραμμή',
|
|
134
142
|
'toast.sourceReloaded': 'Η πηγή επαναφορτώθηκε από τον καμβά',
|
|
135
143
|
'toast.sourceAppliedOne': 'Η πηγή εφαρμόστηκε — 1 γραμμή στον καμβά',
|
|
136
144
|
'toast.sourceAppliedMany': 'Η πηγή εφαρμόστηκε — {rows} γραμμές στον καμβά',
|
package/src/core/i18n/en.js
CHANGED
|
@@ -178,6 +178,14 @@ export const EN = {
|
|
|
178
178
|
'code.applyNote':
|
|
179
179
|
'Apply parses each top-level table row back into a canvas row you can still select, reorder and delete — hand-written markup survives the round trip.',
|
|
180
180
|
'code.meta': '{kb} KB · {lines} lines',
|
|
181
|
+
'code.format': 'Format source (indentation only)',
|
|
182
|
+
'code.wrap': 'Word wrap',
|
|
183
|
+
'code.find': 'Find',
|
|
184
|
+
'code.replace': 'Replace',
|
|
185
|
+
'code.replaceAll': 'Replace all',
|
|
186
|
+
'code.prevMatch': 'Previous match',
|
|
187
|
+
'code.nextMatch': 'Next match',
|
|
188
|
+
'code.goToLine': 'Go to line',
|
|
181
189
|
'toast.sourceReloaded': 'Source reloaded from canvas',
|
|
182
190
|
'toast.sourceAppliedOne': 'Source applied — 1 row back on the canvas',
|
|
183
191
|
'toast.sourceAppliedMany': 'Source applied — {rows} rows back on the canvas',
|
package/src/core/i18n/es.js
CHANGED
|
@@ -131,6 +131,14 @@ export const ES = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Aplicar analiza cada fila de tabla de nivel superior en una fila de lienzo que aún puedes seleccionar, reordenar y eliminar — el marcado escrito a mano sobrevive el proceso.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} líneas',
|
|
134
|
+
'code.format': 'Formatear el código (solo sangría)',
|
|
135
|
+
'code.wrap': 'Ajuste de línea',
|
|
136
|
+
'code.find': 'Buscar',
|
|
137
|
+
'code.replace': 'Reemplazar',
|
|
138
|
+
'code.replaceAll': 'Reemplazar todo',
|
|
139
|
+
'code.prevMatch': 'Coincidencia anterior',
|
|
140
|
+
'code.nextMatch': 'Coincidencia siguiente',
|
|
141
|
+
'code.goToLine': 'Ir a la línea',
|
|
134
142
|
'toast.sourceReloaded': 'Origen recargado desde el lienzo',
|
|
135
143
|
'toast.sourceAppliedOne': 'Origen aplicado — 1 fila en el lienzo',
|
|
136
144
|
'toast.sourceAppliedMany': 'Origen aplicado — {rows} filas en el lienzo',
|
package/src/core/i18n/et.js
CHANGED
|
@@ -131,6 +131,14 @@ export const ET = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Rakendamine analüüsib iga ülataseme tabelirea lõuendireaks, mida saad endiselt valida, ümber korraldada ja kustutada — käsitsi kirjutatud kood jääb protsessis muutumatuks.',
|
|
133
133
|
'code.meta': '{kb} KB · {lines} rida',
|
|
134
|
+
'code.format': 'Vorminda lähtekood (ainult taane)',
|
|
135
|
+
'code.wrap': 'Reamurdmine',
|
|
136
|
+
'code.find': 'Otsi',
|
|
137
|
+
'code.replace': 'Asenda',
|
|
138
|
+
'code.replaceAll': 'Asenda kõik',
|
|
139
|
+
'code.prevMatch': 'Eelmine vaste',
|
|
140
|
+
'code.nextMatch': 'Järgmine vaste',
|
|
141
|
+
'code.goToLine': 'Mine reale',
|
|
134
142
|
'toast.sourceReloaded': 'Allikas laaditi lõuendilt uuesti',
|
|
135
143
|
'toast.sourceAppliedOne': 'Allikas rakendatud — 1 rida lõuendil',
|
|
136
144
|
'toast.sourceAppliedMany': 'Allikas rakendatud — {rows} rida lõuendil',
|
package/src/core/i18n/fi.js
CHANGED
|
@@ -131,6 +131,14 @@ export const FI = {
|
|
|
131
131
|
'code.applyNote':
|
|
132
132
|
'Käytä jäsentää jokaisen ylimmän tason taulukkorivin kangasriviksi, jonka voit yhä valita, järjestää uudelleen ja poistaa — käsin kirjoitettu koodi säilyy prosessin läpi muuttumattomana.',
|
|
133
133
|
'code.meta': '{kb} kt · {lines} riviä',
|
|
134
|
+
'code.format': 'Muotoile lähdekoodi (vain sisennys)',
|
|
135
|
+
'code.wrap': 'Rivitys',
|
|
136
|
+
'code.find': 'Etsi',
|
|
137
|
+
'code.replace': 'Korvaa',
|
|
138
|
+
'code.replaceAll': 'Korvaa kaikki',
|
|
139
|
+
'code.prevMatch': 'Edellinen osuma',
|
|
140
|
+
'code.nextMatch': 'Seuraava osuma',
|
|
141
|
+
'code.goToLine': 'Siirry riville',
|
|
134
142
|
'toast.sourceReloaded': 'Lähde ladattu uudelleen kankaalta',
|
|
135
143
|
'toast.sourceAppliedOne': 'Lähde käytössä — 1 rivi kankaalla',
|
|
136
144
|
'toast.sourceAppliedMany': 'Lähde käytössä — {rows} riviä kankaalla',
|