smeditor 0.3.0 → 0.3.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/app/assets/javascripts/smeditor.js +551 -125
- data/app/assets/stylesheets/smeditor.css +140 -1
- data/lib/smeditor/rails/sanitizer.rb +2 -2
- data/lib/smeditor/rails/version.rb +1 -1
- metadata +2 -2
|
@@ -23,9 +23,10 @@ const ICONS = {
|
|
|
23
23
|
code: '<path d="m6 5-3 3 3 3"/><path d="m10 5 3 3-3 3"/>',
|
|
24
24
|
eraser: '<path d="m9 3 5 5-6 6H4l-1-1z"/><path d="m6 6 5 5"/><path d="M9 14h5"/>',
|
|
25
25
|
link: '<path d="M7 9a3 3 0 0 0 4 0l2-2a3 3 0 1 0-4-4L8 4"/><path d="M9 7a3 3 0 0 0-4 0l-2 2a3 3 0 1 0 4 4l1-1"/>',
|
|
26
|
-
highlight: '<path d="M3
|
|
27
|
-
textColor: '<
|
|
28
|
-
|
|
26
|
+
highlight: '<path d="M3.5 11.5l2.5-.8 6-6-1.7-1.7-6 6z"/><path d="M9 4.3l1.7 1.7"/>',
|
|
27
|
+
textColor: '<path d="M4 12 8 2.5 12 12"/><path d="M5.6 8.5h4.8"/>',
|
|
28
|
+
underlineColor: '<path d="M4.5 2.5v4.5a3.5 3.5 0 0 0 7 0V2.5"/>',
|
|
29
|
+
background: '<path d="M3 7.5 7.5 3l5 5-4.5 4.5z"/><path d="M13 11.2c0 .8-.5 1.3-1 1.3s-1-.5-1-1.3.5-1.5 1-2.2c.5.7 1 1.4 1 2.2z" fill="currentColor"/>',
|
|
29
30
|
blockquote: '<path d="M3 5v3c0 1.5-.7 2.5-2 3"/><path d="M9 5v3c0 1.5-.7 2.5-2 3"/>',
|
|
30
31
|
codeBlock: '<rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="m5 6.5-2 1.5 2 1.5"/><path d="m11 6.5 2 1.5-2 1.5"/>',
|
|
31
32
|
hr: '<path d="M2 8h12"/><path d="M2 4h8"/><path d="M2 12h6"/>',
|
|
@@ -90,7 +91,7 @@ function kitExtensions(kit, uploadUrl) {
|
|
|
90
91
|
const upload = uploadHandler(uploadUrl);
|
|
91
92
|
if (!upload)
|
|
92
93
|
return [...base];
|
|
93
|
-
return base.map((extension) => extension.name === "image" ? starter_kit_1.ImageExtension.configure({ upload }) : extension);
|
|
94
|
+
return base.map((extension) => extension.name === "image" && starter_kit_1.ImageExtension.configure ? starter_kit_1.ImageExtension.configure({ upload }) : extension);
|
|
94
95
|
}
|
|
95
96
|
function run(editor, command, ...args) {
|
|
96
97
|
const fn = editor.commands[command];
|
|
@@ -127,6 +128,7 @@ function commandButton(editor, iconName, title, command, active, label) {
|
|
|
127
128
|
const value = active();
|
|
128
129
|
el.dataset.active = value ? "true" : "false";
|
|
129
130
|
el.classList.toggle("is-active", value);
|
|
131
|
+
el.setAttribute("aria-pressed", String(value));
|
|
130
132
|
}
|
|
131
133
|
};
|
|
132
134
|
el.__smeditorRefresh = refresh;
|
|
@@ -145,10 +147,13 @@ function dropdown(ariaLabel, triggerContent, items, options = {}) {
|
|
|
145
147
|
trigger.title = ariaLabel;
|
|
146
148
|
const label = document.createElement("span");
|
|
147
149
|
label.className = "smeditor-dropdown__label";
|
|
148
|
-
if (typeof triggerContent === "string")
|
|
150
|
+
if (typeof triggerContent === "string") {
|
|
151
|
+
label.classList.add("smeditor-dropdown__label--text");
|
|
149
152
|
label.textContent = triggerContent;
|
|
153
|
+
}
|
|
150
154
|
else
|
|
151
155
|
label.appendChild(triggerContent);
|
|
156
|
+
let currentIcon = "";
|
|
152
157
|
trigger.appendChild(label);
|
|
153
158
|
let summary = null;
|
|
154
159
|
if (options.summary) {
|
|
@@ -179,7 +184,7 @@ function dropdown(ariaLabel, triggerContent, items, options = {}) {
|
|
|
179
184
|
menu.className = "smeditor-dropdown__menu" + (options.align === "right" ? " is-right" : "");
|
|
180
185
|
menu.setAttribute("role", "menu");
|
|
181
186
|
menu.setAttribute("aria-label", ariaLabel);
|
|
182
|
-
for (const spec of items()) {
|
|
187
|
+
for (const spec of items(close)) {
|
|
183
188
|
if (spec.separatorBefore) {
|
|
184
189
|
const sep = document.createElement("div");
|
|
185
190
|
sep.className = "smeditor-dropdown__separator";
|
|
@@ -230,6 +235,11 @@ function dropdown(ariaLabel, triggerContent, items, options = {}) {
|
|
|
230
235
|
menu.appendChild(item);
|
|
231
236
|
}
|
|
232
237
|
wrapper.appendChild(menu);
|
|
238
|
+
// Keep the popover on screen: flip it to the trigger's right edge
|
|
239
|
+
// when it would overflow the viewport (last toolbar items, bubble).
|
|
240
|
+
if (options.align !== "right" && menu.getBoundingClientRect().right > window.innerWidth - 8) {
|
|
241
|
+
menu.classList.add("is-right");
|
|
242
|
+
}
|
|
233
243
|
};
|
|
234
244
|
trigger.addEventListener("mousedown", (event) => event.preventDefault());
|
|
235
245
|
trigger.addEventListener("click", (event) => {
|
|
@@ -245,8 +255,22 @@ function dropdown(ariaLabel, triggerContent, items, options = {}) {
|
|
|
245
255
|
};
|
|
246
256
|
document.addEventListener("pointerdown", outside, true);
|
|
247
257
|
const refresh = () => {
|
|
258
|
+
var _a;
|
|
248
259
|
if (summary && options.summary)
|
|
249
260
|
summary.textContent = options.summary();
|
|
261
|
+
if (options.labelText) {
|
|
262
|
+
const text = options.labelText();
|
|
263
|
+
label.textContent = text;
|
|
264
|
+
trigger.title = `${ariaLabel}: ${text}`;
|
|
265
|
+
}
|
|
266
|
+
if (options.icon) {
|
|
267
|
+
const name = options.icon();
|
|
268
|
+
if (name !== currentIcon) {
|
|
269
|
+
currentIcon = name;
|
|
270
|
+
label.replaceChildren(svgIcon(name));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
(_a = options.decorate) === null || _a === void 0 ? void 0 : _a.call(options, trigger);
|
|
250
274
|
if (options.active) {
|
|
251
275
|
const value = options.active();
|
|
252
276
|
trigger.dataset.active = value ? "true" : "false";
|
|
@@ -254,42 +278,128 @@ function dropdown(ariaLabel, triggerContent, items, options = {}) {
|
|
|
254
278
|
}
|
|
255
279
|
};
|
|
256
280
|
wrapper.__smeditorRefresh = refresh;
|
|
281
|
+
wrapper.__smeditorClose = close;
|
|
257
282
|
wrapper.__smeditorDispose = () => document.removeEventListener("pointerdown", outside, true);
|
|
258
283
|
refresh();
|
|
259
284
|
return wrapper;
|
|
260
285
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
const PALETTE = [
|
|
287
|
+
"#1a1a1f", "#6b7280", "#ef4444", "#f97316", "#eab308",
|
|
288
|
+
"#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
|
|
289
|
+
"#ffffff", "#d1d5db", "#fecaca", "#fed7aa", "#fef08a",
|
|
290
|
+
"#bbf7d0", "#a5f3fc", "#bfdbfe", "#ddd6fe", "#fbcfe8",
|
|
291
|
+
];
|
|
292
|
+
/** Text colour options on a fill; null = automatic contrast. */
|
|
293
|
+
const FILL_TEXT = [["Auto", null], ["White", "#ffffff"], ["Dark", "#1a1a1f"]];
|
|
294
|
+
function sameColor(a, b) {
|
|
295
|
+
return typeof a === "string" && typeof b === "string" && a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The text colour the user chose for a fill: null when it is the
|
|
299
|
+
* automatic contrast colour for the current fill (so re-filling with a
|
|
300
|
+
* new colour keeps the text readable), otherwise the explicit colour.
|
|
301
|
+
*/
|
|
302
|
+
function fillTextChoice(attrs) {
|
|
303
|
+
const fill = typeof (attrs === null || attrs === void 0 ? void 0 : attrs.color) === "string" ? attrs.color : null;
|
|
304
|
+
const text = typeof (attrs === null || attrs === void 0 ? void 0 : attrs.textColor) === "string" ? attrs.textColor : null;
|
|
305
|
+
return fill && text && !sameColor(text, (0, core_1.contrastTextColor)(fill)) ? text : null;
|
|
306
|
+
}
|
|
307
|
+
function panelButton(text, className, action) {
|
|
308
|
+
const el = document.createElement("button");
|
|
309
|
+
el.type = "button";
|
|
310
|
+
el.className = className;
|
|
311
|
+
el.textContent = text;
|
|
312
|
+
el.addEventListener("mousedown", (event) => event.preventDefault());
|
|
313
|
+
el.addEventListener("click", action);
|
|
314
|
+
return el;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Colour picker dropdown. The trigger carries a bar filled with the colour
|
|
318
|
+
* applied to the current selection, the palette marks that colour, and a
|
|
319
|
+
* labelled "Remove" button clears it — so the state of the selection is
|
|
320
|
+
* always visible and every applied colour can be undone from here.
|
|
321
|
+
*/
|
|
322
|
+
function colorDropdown(editor, spec) {
|
|
323
|
+
const current = () => editor.getMarkAttributes(spec.mark);
|
|
324
|
+
const icon = document.createElement("span");
|
|
325
|
+
icon.className = "smeditor-color-trigger";
|
|
326
|
+
icon.appendChild(svgIcon(spec.icon));
|
|
327
|
+
const bar = document.createElement("span");
|
|
328
|
+
bar.className = "smeditor-color-trigger__bar";
|
|
329
|
+
icon.appendChild(bar);
|
|
330
|
+
const wrapper = dropdown(spec.label, icon, (close) => {
|
|
331
|
+
const attrs = current();
|
|
332
|
+
const panel = document.createElement("div");
|
|
333
|
+
panel.className = "smeditor-color-panel";
|
|
334
|
+
panel.setAttribute("role", "group");
|
|
335
|
+
panel.setAttribute("aria-label", spec.label);
|
|
336
|
+
const title = document.createElement("div");
|
|
337
|
+
title.className = "smeditor-color-panel__title";
|
|
338
|
+
title.textContent = spec.label;
|
|
339
|
+
panel.appendChild(title);
|
|
269
340
|
const grid = document.createElement("div");
|
|
270
341
|
grid.className = "smeditor-color-grid";
|
|
271
|
-
|
|
272
|
-
const clear = document.createElement("button");
|
|
273
|
-
clear.type = "button";
|
|
274
|
-
clear.className = "smeditor-color-grid__swatch smeditor-color-grid__swatch--clear";
|
|
275
|
-
clear.title = `Clear ${ariaLabel.toLowerCase()}`;
|
|
276
|
-
clear.setAttribute("aria-label", clear.title);
|
|
277
|
-
clear.addEventListener("mousedown", (e) => e.preventDefault());
|
|
278
|
-
clear.addEventListener("click", () => run(editor, unsetCommand));
|
|
279
|
-
grid.appendChild(clear);
|
|
280
|
-
for (const color of colors) {
|
|
342
|
+
for (const color of PALETTE) {
|
|
281
343
|
const swatch = document.createElement("button");
|
|
282
344
|
swatch.type = "button";
|
|
283
345
|
swatch.className = "smeditor-color-grid__swatch";
|
|
284
346
|
swatch.style.backgroundColor = color;
|
|
285
347
|
swatch.title = color;
|
|
286
|
-
swatch.setAttribute("aria-label", `${
|
|
287
|
-
|
|
288
|
-
swatch.
|
|
348
|
+
swatch.setAttribute("aria-label", `${spec.label}: ${color}`);
|
|
349
|
+
const selected = sameColor(attrs === null || attrs === void 0 ? void 0 : attrs.color, color);
|
|
350
|
+
swatch.classList.toggle("is-active", selected);
|
|
351
|
+
swatch.setAttribute("aria-pressed", String(selected));
|
|
352
|
+
swatch.addEventListener("mousedown", (event) => event.preventDefault());
|
|
353
|
+
swatch.addEventListener("click", () => {
|
|
354
|
+
run(editor, spec.set, spec.textOn ? { color, textColor: fillTextChoice(attrs) } : { color });
|
|
355
|
+
close();
|
|
356
|
+
});
|
|
289
357
|
grid.appendChild(swatch);
|
|
290
358
|
}
|
|
291
|
-
|
|
292
|
-
|
|
359
|
+
panel.appendChild(grid);
|
|
360
|
+
if (spec.textOn) {
|
|
361
|
+
const label = document.createElement("div");
|
|
362
|
+
label.className = "smeditor-color-panel__title";
|
|
363
|
+
label.textContent = spec.textOn;
|
|
364
|
+
const chips = document.createElement("div");
|
|
365
|
+
chips.className = "smeditor-color-panel__widths";
|
|
366
|
+
const choice = fillTextChoice(attrs);
|
|
367
|
+
for (const [text, value] of FILL_TEXT) {
|
|
368
|
+
const chip = panelButton(text, "smeditor-color-panel__chip", () => {
|
|
369
|
+
var _a;
|
|
370
|
+
run(editor, spec.set, { color: (_a = attrs === null || attrs === void 0 ? void 0 : attrs.color) !== null && _a !== void 0 ? _a : PALETTE[7], textColor: value });
|
|
371
|
+
close();
|
|
372
|
+
});
|
|
373
|
+
if (value)
|
|
374
|
+
chip.style.setProperty("--sme-chip-swatch", value);
|
|
375
|
+
chip.setAttribute("aria-label", `${spec.textOn}: ${text}`);
|
|
376
|
+
const selected = Boolean(attrs) && choice === value;
|
|
377
|
+
chip.classList.toggle("is-active", selected);
|
|
378
|
+
chip.setAttribute("aria-pressed", String(selected));
|
|
379
|
+
chips.appendChild(chip);
|
|
380
|
+
}
|
|
381
|
+
panel.append(label, chips);
|
|
382
|
+
}
|
|
383
|
+
const clear = panelButton(`Remove ${spec.label.toLowerCase()}`, "smeditor-color-panel__clear", () => {
|
|
384
|
+
run(editor, spec.unset);
|
|
385
|
+
close();
|
|
386
|
+
});
|
|
387
|
+
clear.disabled = !(attrs === null || attrs === void 0 ? void 0 : attrs.color);
|
|
388
|
+
panel.appendChild(clear);
|
|
389
|
+
return [{ label: "", content: panel }];
|
|
390
|
+
}, {
|
|
391
|
+
active: () => { var _a; return Boolean((_a = current()) === null || _a === void 0 ? void 0 : _a.color); },
|
|
392
|
+
decorate: (trigger) => {
|
|
393
|
+
var _a;
|
|
394
|
+
const color = (_a = current()) === null || _a === void 0 ? void 0 : _a.color;
|
|
395
|
+
const value = typeof color === "string" ? color : "";
|
|
396
|
+
bar.style.backgroundColor = value;
|
|
397
|
+
bar.dataset.empty = value ? "false" : "true";
|
|
398
|
+
trigger.title = value ? `${spec.label}: ${value}` : spec.label;
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
wrapper.classList.add("smeditor-dropdown--color");
|
|
402
|
+
return wrapper;
|
|
293
403
|
}
|
|
294
404
|
function blockTypeSummary(editor) {
|
|
295
405
|
if ((0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "blockquote"))
|
|
@@ -339,11 +449,81 @@ function tablePicker(editor) {
|
|
|
339
449
|
return [{ label: "", content: box }];
|
|
340
450
|
});
|
|
341
451
|
}
|
|
342
|
-
function
|
|
343
|
-
return
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
452
|
+
function normalizeValue(value) {
|
|
453
|
+
return String(value !== null && value !== void 0 ? value : "").replace(/["'\s]/g, "").toLowerCase();
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Value picker (font family, size, line height). The trigger shows the
|
|
457
|
+
* value applied to the selection instead of a generic caption, and the
|
|
458
|
+
* matching menu item is marked as checked.
|
|
459
|
+
*/
|
|
460
|
+
function valueDropdown(spec) {
|
|
461
|
+
const selected = () => {
|
|
462
|
+
const value = spec.current();
|
|
463
|
+
const match = value === null ? null : spec.values.find(([, v]) => v !== null && normalizeValue(v) === normalizeValue(value));
|
|
464
|
+
const name = match ? match[0].split(" · ")[0] : value;
|
|
465
|
+
return { value, label: spec.showValue ? value : name };
|
|
466
|
+
};
|
|
467
|
+
return dropdown(spec.ariaLabel, spec.placeholder, () => {
|
|
468
|
+
const { value } = selected();
|
|
469
|
+
return spec.values.map(([text, v]) => ({
|
|
470
|
+
label: text,
|
|
471
|
+
active: () => (v === null ? value === null : value !== null && normalizeValue(v) === normalizeValue(value)),
|
|
472
|
+
action: () => spec.apply(v),
|
|
473
|
+
}));
|
|
474
|
+
}, {
|
|
475
|
+
labelText: () => { var _a; return (_a = selected().label) !== null && _a !== void 0 ? _a : spec.placeholder; },
|
|
476
|
+
active: () => spec.current() !== null,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function markValue(editor, mark, attr) {
|
|
480
|
+
var _a;
|
|
481
|
+
const value = (_a = editor.getMarkAttributes(mark)) === null || _a === void 0 ? void 0 : _a[attr];
|
|
482
|
+
return typeof value === "string" && value ? value : null;
|
|
483
|
+
}
|
|
484
|
+
function blockValue(editor, attr) {
|
|
485
|
+
const value = (0, core_1.getBlockAttr)(editor.getJSON(), editor.getSelection(), attr);
|
|
486
|
+
return typeof value === "string" && value ? value : null;
|
|
487
|
+
}
|
|
488
|
+
function currentAlign(editor) {
|
|
489
|
+
var _a;
|
|
490
|
+
return (_a = blockValue(editor, "textAlign")) !== null && _a !== void 0 ? _a : "left";
|
|
491
|
+
}
|
|
492
|
+
function currentListIcon(editor) {
|
|
493
|
+
const inside = (name) => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), name);
|
|
494
|
+
if (inside("task_list"))
|
|
495
|
+
return "task";
|
|
496
|
+
if (inside("ordered_list"))
|
|
497
|
+
return "ordered";
|
|
498
|
+
return "bullet";
|
|
499
|
+
}
|
|
500
|
+
function inlineMarkButtons(editor) {
|
|
501
|
+
const buttons = [
|
|
502
|
+
commandButton(editor, "bold", "Bold", () => run(editor, "toggleBold"), () => editor.isActive("bold")),
|
|
503
|
+
commandButton(editor, "italic", "Italic", () => run(editor, "toggleItalic"), () => editor.isActive("italic")),
|
|
504
|
+
commandButton(editor, "underline", "Underline", () => run(editor, "toggleUnderline"), () => editor.isActive("underline")),
|
|
505
|
+
commandButton(editor, "strike", "Strike-through", () => run(editor, "toggleStrike"), () => editor.isActive("strike")),
|
|
506
|
+
];
|
|
507
|
+
return buttons;
|
|
508
|
+
}
|
|
509
|
+
function linkButton(editor) {
|
|
510
|
+
return commandButton(editor, "link", "Link", () => {
|
|
511
|
+
if (editor.isActive("link"))
|
|
512
|
+
return run(editor, "unsetLink");
|
|
513
|
+
const href = window.prompt("Link URL", "https://");
|
|
514
|
+
return href ? run(editor, "setLink", { href }) : false;
|
|
515
|
+
}, () => editor.isActive("link"));
|
|
516
|
+
}
|
|
517
|
+
const COLOR_SPECS = [
|
|
518
|
+
{ label: "Text color", icon: "textColor", mark: "text_color", set: "setTextColor", unset: "unsetTextColor" },
|
|
519
|
+
{ label: "Fill", icon: "background", mark: "background_color", set: "setBackgroundColor", unset: "unsetBackgroundColor", textOn: "Text on fill" },
|
|
520
|
+
{ label: "Highlight", icon: "highlight", mark: "highlight", set: "setHighlight", unset: "unsetHighlight", textOn: "Text on highlight" },
|
|
521
|
+
{ label: "Underline color", icon: "underlineColor", mark: "underline", set: "setUnderlineColor", unset: "unsetUnderlineColor" },
|
|
522
|
+
];
|
|
523
|
+
function colorDropdowns(editor, marks) {
|
|
524
|
+
return COLOR_SPECS
|
|
525
|
+
.filter((spec) => marks.includes(spec.mark) && hasCommand(editor, spec.set) && hasCommand(editor, spec.unset))
|
|
526
|
+
.map((spec) => colorDropdown(editor, spec));
|
|
347
527
|
}
|
|
348
528
|
function buildToolbar(editor, kit, uploadUrl) {
|
|
349
529
|
const toolbar = document.createElement("div");
|
|
@@ -368,22 +548,26 @@ function buildToolbar(editor, kit, uploadUrl) {
|
|
|
368
548
|
{ label: "Bullet List", icon: "bullet", shortcut: "⌘⇧8", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "bullet_list"), action: () => run(editor, "toggleBulletList") },
|
|
369
549
|
{ label: "Ordered List", icon: "ordered", shortcut: "⌘⇧7", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "ordered_list"), action: () => run(editor, "toggleOrderedList") },
|
|
370
550
|
{ label: "Task List", icon: "task", shortcut: "⌘⇧9", active: () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "task_list"), action: () => run(editor, "toggleTaskList") },
|
|
371
|
-
], {
|
|
551
|
+
], {
|
|
552
|
+
icon: () => currentListIcon(editor),
|
|
553
|
+
active: () => ["bullet_list", "ordered_list", "task_list"].some((name) => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), name)),
|
|
554
|
+
});
|
|
372
555
|
add(block, lists, divider());
|
|
373
|
-
add(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return href ? run(editor, "setLink", { href }) : false;
|
|
378
|
-
}, () => editor.isActive("link")), divider());
|
|
556
|
+
add(...inlineMarkButtons(editor), commandButton(editor, "eraser", "Clear formatting", () => run(editor, "clearFormatting")), linkButton(editor), divider());
|
|
557
|
+
const alignItem = (label, icon, align) => ({
|
|
558
|
+
label, icon, active: () => currentAlign(editor) === align, action: () => run(editor, "setTextAlign", { align }),
|
|
559
|
+
});
|
|
379
560
|
const align = dropdown("Alignment", svgIcon("alignLeft"), () => [
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
561
|
+
alignItem("Left", "alignLeft", "left"),
|
|
562
|
+
alignItem("Center", "alignCenter", "center"),
|
|
563
|
+
alignItem("Right", "alignRight", "right"),
|
|
564
|
+
alignItem("Justify", "alignJustify", "justify"),
|
|
384
565
|
{ label: "Increase indent", icon: "indent", action: () => run(editor, "indent"), separatorBefore: true },
|
|
385
566
|
{ label: "Decrease indent", icon: "outdent", action: () => run(editor, "outdent") },
|
|
386
|
-
]
|
|
567
|
+
], {
|
|
568
|
+
icon: () => { var _a; return ((_a = { center: "alignCenter", right: "alignRight", justify: "alignJustify" }[currentAlign(editor)]) !== null && _a !== void 0 ? _a : "alignLeft"); },
|
|
569
|
+
active: () => currentAlign(editor) !== "left",
|
|
570
|
+
});
|
|
387
571
|
add(align, divider());
|
|
388
572
|
add(commandButton(editor, "blockquote", "Blockquote", () => run(editor, "toggleBlockquote"), () => (0, core_1.selectionInsideWrapper)(editor.getJSON(), editor.getSelection(), "blockquote")), commandButton(editor, "codeBlock", "Code block", () => run(editor, "setCodeBlock"), () => editor.isActive("code_block")), commandButton(editor, "hr", "Horizontal rule", () => run(editor, "insertHorizontalRule")));
|
|
389
573
|
const image = dropdown("Insert image", svgIcon("image"), () => {
|
|
@@ -409,7 +593,28 @@ function buildToolbar(editor, kit, uploadUrl) {
|
|
|
409
593
|
add(image, tablePicker(editor));
|
|
410
594
|
if (kit === "full") {
|
|
411
595
|
add(divider());
|
|
412
|
-
add(
|
|
596
|
+
add(...colorDropdowns(editor, ["text_color", "background_color", "highlight", "underline"]));
|
|
597
|
+
add(valueDropdown({
|
|
598
|
+
ariaLabel: "Font family",
|
|
599
|
+
placeholder: "Font",
|
|
600
|
+
values: [["Default", null], ["Sans-serif", "ui-sans-serif, system-ui, sans-serif"], ["Serif", "ui-serif, Georgia, serif"], ["Monospace", "ui-monospace, Menlo, monospace"], ["Georgia", "Georgia, serif"], ["Courier", "'Courier New', monospace"]],
|
|
601
|
+
current: () => markValue(editor, "font_family", "family"),
|
|
602
|
+
apply: (value) => (value === null ? run(editor, "unsetFontFamily") : run(editor, "setFontFamily", { family: value })),
|
|
603
|
+
}), valueDropdown({
|
|
604
|
+
ariaLabel: "Font size",
|
|
605
|
+
placeholder: "Size",
|
|
606
|
+
showValue: true,
|
|
607
|
+
values: [["Default", null], ["Small · 13px", "13px"], ["Normal · 16px", "16px"], ["Large · 20px", "20px"], ["Huge · 28px", "28px"]],
|
|
608
|
+
current: () => markValue(editor, "font_size", "size"),
|
|
609
|
+
apply: (value) => (value === null ? run(editor, "unsetFontSize") : run(editor, "setFontSize", { size: value })),
|
|
610
|
+
}), valueDropdown({
|
|
611
|
+
ariaLabel: "Line height",
|
|
612
|
+
placeholder: "Spacing",
|
|
613
|
+
showValue: true,
|
|
614
|
+
values: [["Default", null], ["Tight · 1", "1"], ["Snug · 1.15", "1.15"], ["Normal · 1.5", "1.5"], ["Relaxed · 2", "2"]],
|
|
615
|
+
current: () => blockValue(editor, "lineHeight"),
|
|
616
|
+
apply: (value) => run(editor, "setLineHeight", { value }),
|
|
617
|
+
}));
|
|
413
618
|
if (hasCommand(editor, "addComment")) {
|
|
414
619
|
add(commandButton(editor, "", "Comment", () => {
|
|
415
620
|
const body = window.prompt("New comment");
|
|
@@ -424,7 +629,7 @@ function buildToolbar(editor, kit, uploadUrl) {
|
|
|
424
629
|
{ label: "Superscript", icon: "superscript", active: () => editor.isActive("superscript"), action: () => run(editor, "toggleSuperscript") },
|
|
425
630
|
{ label: "Increase indent", icon: "indent", action: () => run(editor, "indent"), separatorBefore: true },
|
|
426
631
|
{ label: "Decrease indent", icon: "outdent", action: () => run(editor, "outdent") },
|
|
427
|
-
], { align: "right" });
|
|
632
|
+
], { align: "right", active: () => ["code", "subscript", "superscript"].some((mark) => editor.isActive(mark)) });
|
|
428
633
|
add(more);
|
|
429
634
|
}
|
|
430
635
|
const refresh = () => {
|
|
@@ -477,24 +682,29 @@ function buildBubbleMenu(editor, surface, container) {
|
|
|
477
682
|
menu.style.position = "absolute";
|
|
478
683
|
menu.style.display = "none";
|
|
479
684
|
menu.style.zIndex = "2147483000";
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
commandButton(editor, "
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
685
|
+
// Same controls (and the same live state) as the matching toolbar ones:
|
|
686
|
+
// inline marks, link, the colour pickers the kit provides, and a clear
|
|
687
|
+
// button that removes every inline style from the selection at once.
|
|
688
|
+
const buttons = [...inlineMarkButtons(editor)];
|
|
689
|
+
if (hasCommand(editor, "toggleCode")) {
|
|
690
|
+
buttons.push(commandButton(editor, "code", "Inline code", () => run(editor, "toggleCode"), () => editor.isActive("code")));
|
|
691
|
+
}
|
|
692
|
+
buttons.push(linkButton(editor));
|
|
693
|
+
const colors = colorDropdowns(editor, ["text_color", "background_color", "underline"]);
|
|
694
|
+
if (colors.length)
|
|
695
|
+
buttons.push(divider(), ...colors);
|
|
696
|
+
if (hasCommand(editor, "clearFormatting")) {
|
|
697
|
+
buttons.push(divider(), commandButton(editor, "eraser", "Clear formatting", () => run(editor, "clearFormatting")));
|
|
698
|
+
}
|
|
492
699
|
menu.append(...buttons);
|
|
493
700
|
// Keeping pointer-down inside the menu from focusing a button preserves the
|
|
494
701
|
// editor's text selection, which the formatting command needs.
|
|
495
702
|
menu.addEventListener("mousedown", (event) => event.preventDefault());
|
|
496
703
|
let frame = 0;
|
|
497
704
|
const hide = () => {
|
|
705
|
+
var _a, _b;
|
|
706
|
+
for (const button of buttons)
|
|
707
|
+
(_b = (_a = button).__smeditorClose) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
498
708
|
menu.style.display = "none";
|
|
499
709
|
menu.style.visibility = "hidden";
|
|
500
710
|
};
|
|
@@ -533,8 +743,11 @@ function buildBubbleMenu(editor, surface, container) {
|
|
|
533
743
|
let viewportLeft = anchor.left + anchor.width / 2 - rect.width / 2;
|
|
534
744
|
const maxViewportLeft = Math.max(margin, viewportWidth - rect.width - margin);
|
|
535
745
|
viewportLeft = Math.max(margin, Math.min(viewportLeft, maxViewportLeft));
|
|
746
|
+
// Never cover the main toolbar: when there is no room between the top
|
|
747
|
+
// of the writing surface and the selection, open below the selection.
|
|
748
|
+
const topLimit = Math.max(margin, surface.getBoundingClientRect().top);
|
|
536
749
|
let viewportTop = anchor.top - rect.height - offset;
|
|
537
|
-
if (viewportTop <
|
|
750
|
+
if (viewportTop < topLimit)
|
|
538
751
|
viewportTop = anchor.bottom + offset;
|
|
539
752
|
if (viewportTop + rect.height > viewportHeight - margin) {
|
|
540
753
|
viewportTop = Math.max(margin, anchor.top - rect.height - offset);
|
|
@@ -552,7 +765,14 @@ function buildBubbleMenu(editor, surface, container) {
|
|
|
552
765
|
const selectionTarget = selectionRoot && selectionRoot !== document
|
|
553
766
|
? selectionRoot
|
|
554
767
|
: null;
|
|
768
|
+
const hideOnBlur = () => requestAnimationFrame(() => {
|
|
769
|
+
const active = surface.getRootNode().activeElement;
|
|
770
|
+
if (active !== surface && !menu.contains(active))
|
|
771
|
+
hide();
|
|
772
|
+
});
|
|
555
773
|
const disposeSelection = editor.on("selectionUpdate", scheduleUpdate);
|
|
774
|
+
const disposeUpdate = editor.on("update", scheduleUpdate);
|
|
775
|
+
surface.addEventListener("blur", hideOnBlur);
|
|
556
776
|
document.addEventListener("selectionchange", scheduleUpdate);
|
|
557
777
|
selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.addEventListener("selectionchange", scheduleUpdate);
|
|
558
778
|
surface.addEventListener("pointerup", scheduleUpdate);
|
|
@@ -560,9 +780,14 @@ function buildBubbleMenu(editor, surface, container) {
|
|
|
560
780
|
window.addEventListener("resize", scheduleUpdate);
|
|
561
781
|
window.addEventListener("scroll", scheduleUpdate, true);
|
|
562
782
|
menu.__smeditorDispose = () => {
|
|
783
|
+
var _a, _b;
|
|
563
784
|
if (frame)
|
|
564
785
|
cancelAnimationFrame(frame);
|
|
565
786
|
disposeSelection();
|
|
787
|
+
disposeUpdate();
|
|
788
|
+
surface.removeEventListener("blur", hideOnBlur);
|
|
789
|
+
for (const button of buttons)
|
|
790
|
+
(_b = (_a = button).__smeditorDispose) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
566
791
|
document.removeEventListener("selectionchange", scheduleUpdate);
|
|
567
792
|
selectionTarget === null || selectionTarget === void 0 ? void 0 : selectionTarget.removeEventListener("selectionchange", scheduleUpdate);
|
|
568
793
|
surface.removeEventListener("pointerup", scheduleUpdate);
|
|
@@ -701,8 +926,8 @@ if (typeof window !== "undefined") {
|
|
|
701
926
|
* stay internal so we can evolve them without breaking consumers.
|
|
702
927
|
*/
|
|
703
928
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
704
|
-
exports.
|
|
705
|
-
exports.VERSION = exports.getBlockAttr = exports.setBlockAttrs = exports.selectionInsideWrapper = exports.toggleWrap = exports.unwrapBlock = exports.wrapBlocks = void 0;
|
|
929
|
+
exports.resolveSelectionTarget = exports.removeNodeAt = exports.mapNodeAt = exports.descendToLeafPath = exports.parentAt = exports.nodeAt = exports.isContainerBlock = exports.isLeafBlock = exports.splitBlock = exports.removeMarkFromInlineRange = exports.setMarkOnInlineRange = exports.applyMarkToInlineRange = exports.replaceInlineRange = exports.deleteInlineRange = exports.insertInlineAt = exports.splitInlineAt = exports.blockVisibleText = exports.inlineLength = exports.visibleLength = exports.stripBlockPrefix = exports.getBlockText = exports.leafBlocksInSelection = exports.setBlockType = exports.clearMarksAcrossSelection = exports.unsetMarkAcrossSelection = exports.setMarkAcrossSelection = exports.toggleMarkInDoc = exports.selectionMarkAttrs = exports.selectionHasMark = exports.hasMark = exports.emptyDocument = exports.docsEqual = exports.cloneDoc = exports.EventEmitter = exports.History = exports.hardenLinkAttrs = exports.sanitizeLinkTarget = exports.sanitizeURL = exports.sanitizeCSSTextStrokeWidth = exports.sanitizeCSSLineHeight = exports.sanitizeCSSFontSize = exports.sanitizeCSSFontFamily = exports.contrastTextColor = exports.sanitizeCSSColor = exports.escapeHTML = exports.parseHTML = exports.serializeToHTML = exports.flattenExtensions = exports.compileSchema = exports.createEditor = void 0;
|
|
930
|
+
exports.VERSION = exports.getBlockAttr = exports.setBlockAttrs = exports.selectionInsideWrapper = exports.toggleWrap = exports.unwrapBlock = exports.wrapBlocks = exports.selectionBlockRange = exports.normalizeDeepPoint = void 0;
|
|
706
931
|
var editor_js_1 = require("./editor.js");
|
|
707
932
|
Object.defineProperty(exports, "createEditor", { enumerable: true, get: function () { return editor_js_1.createEditor; } });
|
|
708
933
|
var schema_js_1 = require("./schema.js");
|
|
@@ -714,6 +939,7 @@ Object.defineProperty(exports, "parseHTML", { enumerable: true, get: function ()
|
|
|
714
939
|
Object.defineProperty(exports, "escapeHTML", { enumerable: true, get: function () { return serializer_js_1.escapeHTML; } });
|
|
715
940
|
var css_js_1 = require("./css.js");
|
|
716
941
|
Object.defineProperty(exports, "sanitizeCSSColor", { enumerable: true, get: function () { return css_js_1.sanitizeCSSColor; } });
|
|
942
|
+
Object.defineProperty(exports, "contrastTextColor", { enumerable: true, get: function () { return css_js_1.contrastTextColor; } });
|
|
717
943
|
Object.defineProperty(exports, "sanitizeCSSFontFamily", { enumerable: true, get: function () { return css_js_1.sanitizeCSSFontFamily; } });
|
|
718
944
|
Object.defineProperty(exports, "sanitizeCSSFontSize", { enumerable: true, get: function () { return css_js_1.sanitizeCSSFontSize; } });
|
|
719
945
|
Object.defineProperty(exports, "sanitizeCSSLineHeight", { enumerable: true, get: function () { return css_js_1.sanitizeCSSLineHeight; } });
|
|
@@ -731,6 +957,7 @@ Object.defineProperty(exports, "docsEqual", { enumerable: true, get: function ()
|
|
|
731
957
|
Object.defineProperty(exports, "emptyDocument", { enumerable: true, get: function () { return doc_utils_js_1.emptyDocument; } });
|
|
732
958
|
Object.defineProperty(exports, "hasMark", { enumerable: true, get: function () { return doc_utils_js_1.hasMark; } });
|
|
733
959
|
Object.defineProperty(exports, "selectionHasMark", { enumerable: true, get: function () { return doc_utils_js_1.selectionHasMark; } });
|
|
960
|
+
Object.defineProperty(exports, "selectionMarkAttrs", { enumerable: true, get: function () { return doc_utils_js_1.selectionMarkAttrs; } });
|
|
734
961
|
Object.defineProperty(exports, "toggleMarkInDoc", { enumerable: true, get: function () { return doc_utils_js_1.toggleMarkInDoc; } });
|
|
735
962
|
Object.defineProperty(exports, "setMarkAcrossSelection", { enumerable: true, get: function () { return doc_utils_js_1.setMarkAcrossSelection; } });
|
|
736
963
|
Object.defineProperty(exports, "unsetMarkAcrossSelection", { enumerable: true, get: function () { return doc_utils_js_1.unsetMarkAcrossSelection; } });
|
|
@@ -1079,6 +1306,11 @@ class Editor {
|
|
|
1079
1306
|
}
|
|
1080
1307
|
return map;
|
|
1081
1308
|
}
|
|
1309
|
+
getMarkAttributes(name) {
|
|
1310
|
+
if (!this.schema.marks[name])
|
|
1311
|
+
return null;
|
|
1312
|
+
return (0, doc_utils_js_1.selectionMarkAttrs)(this.doc, this.selection, name);
|
|
1313
|
+
}
|
|
1082
1314
|
isActive(name, attrs) {
|
|
1083
1315
|
var _a, _b;
|
|
1084
1316
|
// Mark active check
|
|
@@ -1696,6 +1928,8 @@ function sanitizePastedStyleValue(name, value) {
|
|
|
1696
1928
|
if (name === "background-color" || name === "background") {
|
|
1697
1929
|
return (0, css_js_1.sanitizeCSSColor)(value);
|
|
1698
1930
|
}
|
|
1931
|
+
if (name === "text-decoration-color")
|
|
1932
|
+
return (0, css_js_1.sanitizeCSSColor)(value);
|
|
1699
1933
|
if (name === "font-family")
|
|
1700
1934
|
return (0, css_js_1.sanitizeCSSFontFamily)(value);
|
|
1701
1935
|
if (name === "font-size")
|
|
@@ -2077,33 +2311,66 @@ exports.parseHTML = parseHTML;
|
|
|
2077
2311
|
function serializeToHTML(doc, schema) {
|
|
2078
2312
|
if (!doc || doc.type !== "doc" || !doc.content)
|
|
2079
2313
|
return "";
|
|
2080
|
-
return doc.content
|
|
2314
|
+
return renderChildren(doc.content, schema);
|
|
2081
2315
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2316
|
+
/**
|
|
2317
|
+
* Marks of a node in a stable outer→inner order (`rank`, then schema
|
|
2318
|
+
* registration order). A fixed order means `<mark><span style="color">` never flips
|
|
2319
|
+
* to `<span style="color"><mark>` between renders, which used to make
|
|
2320
|
+
* the DOM churn (and the caret jump) after every formatting change.
|
|
2321
|
+
*/
|
|
2322
|
+
function orderedMarks(node, schema) {
|
|
2323
|
+
var _a;
|
|
2324
|
+
const order = Object.keys(schema.marks);
|
|
2325
|
+
return ((_a = node.marks) !== null && _a !== void 0 ? _a : [])
|
|
2326
|
+
.filter((mark) => { var _a; return Boolean((_a = schema.marks[mark.type]) === null || _a === void 0 ? void 0 : _a.toDOM); })
|
|
2327
|
+
.sort((a, b) => {
|
|
2328
|
+
var _a, _b;
|
|
2329
|
+
return ((_a = schema.marks[a.type].rank) !== null && _a !== void 0 ? _a : 0) - ((_b = schema.marks[b.type].rank) !== null && _b !== void 0 ? _b : 0) ||
|
|
2330
|
+
order.indexOf(a.type) - order.indexOf(b.type);
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
function markKey(mark) {
|
|
2334
|
+
var _a;
|
|
2335
|
+
return mark ? `${mark.type}:${JSON.stringify((_a = mark.attrs) !== null && _a !== void 0 ? _a : {})}` : "";
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Render sibling nodes, merging runs that share the same mark into one
|
|
2339
|
+
* element: "a", "b" both highlighted render as `<mark>ab</mark>`, not
|
|
2340
|
+
* `<mark>a</mark><mark>b</mark>` (which showed visible seams between the
|
|
2341
|
+
* pieces of one highlight / background / outline).
|
|
2342
|
+
*/
|
|
2343
|
+
function renderChildren(nodes, schema, depth = 0) {
|
|
2344
|
+
let out = "";
|
|
2345
|
+
let i = 0;
|
|
2346
|
+
while (i < nodes.length) {
|
|
2347
|
+
const mark = orderedMarks(nodes[i], schema)[depth];
|
|
2348
|
+
if (!mark) {
|
|
2349
|
+
out += renderNode(nodes[i], schema);
|
|
2350
|
+
i += 1;
|
|
2351
|
+
continue;
|
|
2094
2352
|
}
|
|
2095
|
-
|
|
2353
|
+
const key = markKey(mark);
|
|
2354
|
+
let j = i + 1;
|
|
2355
|
+
while (j < nodes.length && markKey(orderedMarks(nodes[j], schema)[depth]) === key)
|
|
2356
|
+
j += 1;
|
|
2357
|
+
const inner = renderChildren(nodes.slice(i, j), schema, depth + 1);
|
|
2358
|
+
out += wrapWithSpec(schema.marks[mark.type].toDOM(mark), inner);
|
|
2359
|
+
i = j;
|
|
2096
2360
|
}
|
|
2361
|
+
return out;
|
|
2362
|
+
}
|
|
2363
|
+
function renderNode(node, schema) {
|
|
2364
|
+
var _a, _b, _c;
|
|
2365
|
+
// Marks are applied by renderChildren, which groups equal neighbours.
|
|
2366
|
+
if (node.type === "text")
|
|
2367
|
+
return escapeHTML((_a = node.text) !== null && _a !== void 0 ? _a : "");
|
|
2097
2368
|
const spec = schema.nodes[node.type];
|
|
2098
2369
|
if (!(spec === null || spec === void 0 ? void 0 : spec.toDOM)) {
|
|
2099
2370
|
// Unknown node — render children if any.
|
|
2100
|
-
return ((_b = node.content) !== null && _b !== void 0 ? _b : [])
|
|
2101
|
-
.map((c) => renderNode(c, schema))
|
|
2102
|
-
.join("");
|
|
2371
|
+
return renderChildren((_b = node.content) !== null && _b !== void 0 ? _b : [], schema);
|
|
2103
2372
|
}
|
|
2104
|
-
let inner = ((_c = node.content) !== null && _c !== void 0 ? _c : [])
|
|
2105
|
-
.map((c) => renderNode(c, schema))
|
|
2106
|
-
.join("");
|
|
2373
|
+
let inner = renderChildren((_c = node.content) !== null && _c !== void 0 ? _c : [], schema);
|
|
2107
2374
|
// An empty leaf block (a blank paragraph, an empty table cell) must
|
|
2108
2375
|
// still render with a <br> inside it — otherwise the element
|
|
2109
2376
|
// collapses to zero height in contenteditable and the caret can't
|
|
@@ -2853,6 +3120,7 @@ exports.stripBlockPrefix = stripBlockPrefix;
|
|
|
2853
3120
|
exports.forEachTextNode = forEachTextNode;
|
|
2854
3121
|
exports.hasMark = hasMark;
|
|
2855
3122
|
exports.selectionHasMark = selectionHasMark;
|
|
3123
|
+
exports.selectionMarkAttrs = selectionMarkAttrs;
|
|
2856
3124
|
exports.textsInSelection = textsInSelection;
|
|
2857
3125
|
exports.toggleMarkInDoc = toggleMarkInDoc;
|
|
2858
3126
|
exports.setMarkAcrossSelection = setMarkAcrossSelection;
|
|
@@ -2989,6 +3257,23 @@ function selectionHasMark(doc, selection, type, attrs) {
|
|
|
2989
3257
|
return false;
|
|
2990
3258
|
return texts.every((t) => hasMark(t, type, attrs));
|
|
2991
3259
|
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Attributes of a mark shared by the whole selection (the word under a
|
|
3262
|
+
* bare caret). Returns the attrs of the first matching mark when every
|
|
3263
|
+
* selected text node carries a mark of `type`, otherwise null. Toolbars
|
|
3264
|
+
* use it to show the current colour / font / size of the selection.
|
|
3265
|
+
*/
|
|
3266
|
+
function selectionMarkAttrs(doc, selection, type) {
|
|
3267
|
+
var _a;
|
|
3268
|
+
if (!selection)
|
|
3269
|
+
return null;
|
|
3270
|
+
const texts = leafRangesInSelection(doc, selection).flatMap(({ node, from, to }) => { var _a; return textNodesInInlineRange((_a = node.content) !== null && _a !== void 0 ? _a : [], from, to); });
|
|
3271
|
+
const marks = texts.map((t) => { var _a, _b; return (_b = (_a = t.marks) === null || _a === void 0 ? void 0 : _a.find((m) => m.type === type)) !== null && _b !== void 0 ? _b : null; });
|
|
3272
|
+
const first = marks[0];
|
|
3273
|
+
if (!first || marks.some((m) => m === null))
|
|
3274
|
+
return null;
|
|
3275
|
+
return { ...((_a = first.attrs) !== null && _a !== void 0 ? _a : {}) };
|
|
3276
|
+
}
|
|
2992
3277
|
/** Collect every text node intersecting the selection. */
|
|
2993
3278
|
function textsInSelection(doc, selection) {
|
|
2994
3279
|
const result = [];
|
|
@@ -3101,9 +3386,8 @@ function toggleMarkInDoc(doc, selection, type, attrs) {
|
|
|
3101
3386
|
* Behaviour:
|
|
3102
3387
|
* - a real range → the mark is set on just that span, replacing any
|
|
3103
3388
|
* existing mark of the same type so colours/sizes swap cleanly;
|
|
3104
|
-
* - a collapsed caret → the mark is applied to the
|
|
3105
|
-
* caret
|
|
3106
|
-
* something predictable.
|
|
3389
|
+
* - a collapsed caret → the mark is applied to the word under the
|
|
3390
|
+
* caret (the whole block only when no word is there).
|
|
3107
3391
|
*
|
|
3108
3392
|
* Returns a new document, or null when nothing changed.
|
|
3109
3393
|
*/
|
|
@@ -3174,8 +3458,12 @@ function leafRangesInSelection(doc, selection) {
|
|
|
3174
3458
|
let from;
|
|
3175
3459
|
let to;
|
|
3176
3460
|
if (collapsed) {
|
|
3177
|
-
|
|
3178
|
-
|
|
3461
|
+
// A bare caret targets the word it sits in (or touches), like
|
|
3462
|
+
// Google Docs / Word. Only when there is no word under the caret
|
|
3463
|
+
// (empty block, caret between spaces) does the whole block apply.
|
|
3464
|
+
const word = wordRangeAt(node, Math.min(start.offset, len));
|
|
3465
|
+
from = word ? word[0] : 0;
|
|
3466
|
+
to = word ? word[1] : len;
|
|
3179
3467
|
}
|
|
3180
3468
|
else {
|
|
3181
3469
|
const isStart = samePath(path, start.path);
|
|
@@ -3187,6 +3475,21 @@ function leafRangesInSelection(doc, selection) {
|
|
|
3187
3475
|
})
|
|
3188
3476
|
.filter(({ from, to }) => to > from);
|
|
3189
3477
|
}
|
|
3478
|
+
const WORD_CHAR = /[\p{L}\p{N}_'\u2019-]/u;
|
|
3479
|
+
/**
|
|
3480
|
+
* The [from, to) visible-offset range of the word at `offset` in a leaf
|
|
3481
|
+
* block, or null when the caret touches no word characters.
|
|
3482
|
+
*/
|
|
3483
|
+
function wordRangeAt(block, offset) {
|
|
3484
|
+
const text = (0, transform_js_1.blockVisibleText)(block);
|
|
3485
|
+
let from = Math.max(0, Math.min(offset, text.length));
|
|
3486
|
+
let to = from;
|
|
3487
|
+
while (from > 0 && WORD_CHAR.test(text[from - 1]))
|
|
3488
|
+
from--;
|
|
3489
|
+
while (to < text.length && WORD_CHAR.test(text[to]))
|
|
3490
|
+
to++;
|
|
3491
|
+
return to > from ? [from, to] : null;
|
|
3492
|
+
}
|
|
3190
3493
|
function inlineContentLength(content) {
|
|
3191
3494
|
// Use the same visible-width measure as splitInlineAt / setMarkOnInlineRange:
|
|
3192
3495
|
// an atomic inline node (a mention pill) contributes its full label
|
|
@@ -3724,6 +4027,7 @@ exports.sanitizeCSSTextStrokeWidth = sanitizeCSSTextStrokeWidth;
|
|
|
3724
4027
|
exports.sanitizeURL = sanitizeURL;
|
|
3725
4028
|
exports.sanitizeLinkTarget = sanitizeLinkTarget;
|
|
3726
4029
|
exports.hardenLinkAttrs = hardenLinkAttrs;
|
|
4030
|
+
exports.contrastTextColor = contrastTextColor;
|
|
3727
4031
|
const MAX_COLOR_LENGTH = 80;
|
|
3728
4032
|
const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
3729
4033
|
const CSS_IDENTIFIER = /^[a-zA-Z]+$/;
|
|
@@ -3919,6 +4223,33 @@ function normaliseProtocols(protocols) {
|
|
|
3919
4223
|
.map((protocol) => protocol.trim().replace(/:$/, "").toLowerCase())
|
|
3920
4224
|
.filter(Boolean));
|
|
3921
4225
|
}
|
|
4226
|
+
const LIGHT_TEXT = "#ffffff";
|
|
4227
|
+
const DARK_TEXT = "#1a1a1f";
|
|
4228
|
+
function rgbChannels(color) {
|
|
4229
|
+
const hex = /^#([0-9a-f]{3,8})$/i.exec(color);
|
|
4230
|
+
if (hex) {
|
|
4231
|
+
const raw = hex[1];
|
|
4232
|
+
const full = raw.length <= 4 ? raw.slice(0, 3).split("").map((c) => c + c).join("") : raw.slice(0, 6);
|
|
4233
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
4234
|
+
}
|
|
4235
|
+
const rgb = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/i.exec(color);
|
|
4236
|
+
return rgb ? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])] : null;
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* A text colour that stays readable on `fill`: white on dark fills,
|
|
4240
|
+
* near-black on light ones (WCAG relative luminance). Formats it can't
|
|
4241
|
+
* measure (named colours, hsl) get white.
|
|
4242
|
+
*/
|
|
4243
|
+
function contrastTextColor(fill) {
|
|
4244
|
+
const rgb = rgbChannels(String(fill).trim());
|
|
4245
|
+
if (!rgb)
|
|
4246
|
+
return LIGHT_TEXT;
|
|
4247
|
+
const [r, g, b] = rgb.map((v) => {
|
|
4248
|
+
const c = v / 255;
|
|
4249
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
4250
|
+
});
|
|
4251
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 0.4 ? DARK_TEXT : LIGHT_TEXT;
|
|
4252
|
+
}
|
|
3922
4253
|
|
|
3923
4254
|
},{}],
|
|
3924
4255
|
11:[function(require,module,exports){
|
|
@@ -4580,21 +4911,51 @@ exports.default = exports.ItalicExtension;
|
|
|
4580
4911
|
/**
|
|
4581
4912
|
* @smeditor/extension-underline
|
|
4582
4913
|
*
|
|
4583
|
-
* Renders to `<u
|
|
4914
|
+
* Renders to `<u>`, or `<u style="text-decoration-color: …">` when the
|
|
4915
|
+
* underline has its own colour; parses both `<u>` and the legacy CSS form
|
|
4584
4916
|
* `<span style="text-decoration: underline">`. Shortcut: Mod-u.
|
|
4917
|
+
*
|
|
4918
|
+
* The underline renders innermost (rank 2), so an uncoloured underline
|
|
4919
|
+
* follows the colour of the text it sits under.
|
|
4585
4920
|
*/
|
|
4586
4921
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4587
4922
|
exports.UnderlineExtension = void 0;
|
|
4588
4923
|
const core_1 = require("@smeditor/core");
|
|
4924
|
+
function decorationColor(style) {
|
|
4925
|
+
const m = /(?:^|;)\s*text-decoration-color\s*:\s*([^;]+)/i.exec(style);
|
|
4926
|
+
return m ? (0, core_1.sanitizeCSSColor)(m[1].trim()) : null;
|
|
4927
|
+
}
|
|
4928
|
+
function colorAttrs(el) {
|
|
4929
|
+
var _a;
|
|
4930
|
+
const color = decorationColor((_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "");
|
|
4931
|
+
return color ? { color } : {};
|
|
4932
|
+
}
|
|
4933
|
+
function applyUnderline(editor, color) {
|
|
4934
|
+
const selection = editor.getSelection();
|
|
4935
|
+
const next = (0, core_1.setMarkAcrossSelection)(editor.getJSON(), selection, {
|
|
4936
|
+
type: "underline",
|
|
4937
|
+
...(color ? { attrs: { color } } : {}),
|
|
4938
|
+
});
|
|
4939
|
+
if (!next)
|
|
4940
|
+
return false;
|
|
4941
|
+
editor.dispatch({ doc: next, selection, addToHistory: true });
|
|
4942
|
+
return true;
|
|
4943
|
+
}
|
|
4589
4944
|
exports.UnderlineExtension = {
|
|
4590
4945
|
name: "underline",
|
|
4591
4946
|
marks: [
|
|
4592
4947
|
{
|
|
4593
4948
|
name: "underline",
|
|
4594
4949
|
inclusive: true,
|
|
4595
|
-
|
|
4950
|
+
rank: 2,
|
|
4951
|
+
attrs: { color: { default: null } },
|
|
4952
|
+
toDOM: (mark) => {
|
|
4953
|
+
var _a;
|
|
4954
|
+
const color = (0, core_1.sanitizeCSSColor)((_a = mark.attrs) === null || _a === void 0 ? void 0 : _a.color);
|
|
4955
|
+
return color ? ["u", { style: `text-decoration-color: ${color}` }, 0] : ["u", 0];
|
|
4956
|
+
},
|
|
4596
4957
|
parseDOM: [
|
|
4597
|
-
{ tag: "u" },
|
|
4958
|
+
{ tag: "u", getAttrs: (el) => colorAttrs(el) },
|
|
4598
4959
|
{
|
|
4599
4960
|
// Catch <span style="text-decoration: underline"> from MS Word
|
|
4600
4961
|
// / Google Docs paste. We don't try to be exhaustive — common
|
|
@@ -4603,7 +4964,7 @@ exports.UnderlineExtension = {
|
|
|
4603
4964
|
getAttrs: (el) => {
|
|
4604
4965
|
var _a;
|
|
4605
4966
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
4606
|
-
return /text-decoration
|
|
4967
|
+
return /text-decoration(?:-line)?:\s*[^;]*underline/i.test(style) ? colorAttrs(el) : false;
|
|
4607
4968
|
},
|
|
4608
4969
|
},
|
|
4609
4970
|
],
|
|
@@ -4621,6 +4982,16 @@ exports.UnderlineExtension = {
|
|
|
4621
4982
|
});
|
|
4622
4983
|
return true;
|
|
4623
4984
|
},
|
|
4985
|
+
/** setUnderlineColor({ color }) — underlines the selection in `color`. */
|
|
4986
|
+
setUnderlineColor: (opts) => (editor) => {
|
|
4987
|
+
const color = (0, core_1.sanitizeCSSColor)(opts === null || opts === void 0 ? void 0 : opts.color);
|
|
4988
|
+
return color ? applyUnderline(editor, color) : false;
|
|
4989
|
+
},
|
|
4990
|
+
/**
|
|
4991
|
+
* unsetUnderlineColor() — keeps the underline but drops its own
|
|
4992
|
+
* colour, so it follows the text colour again.
|
|
4993
|
+
*/
|
|
4994
|
+
unsetUnderlineColor: () => (editor) => editor.isActive("underline") ? applyUnderline(editor, null) : false,
|
|
4624
4995
|
},
|
|
4625
4996
|
keyboardShortcuts: {
|
|
4626
4997
|
"Mod-u": (editor) => {
|
|
@@ -4649,6 +5020,8 @@ exports.StrikeExtension = {
|
|
|
4649
5020
|
marks: [
|
|
4650
5021
|
{
|
|
4651
5022
|
name: "strike",
|
|
5023
|
+
// Innermost, so the line takes the colour of the text it crosses.
|
|
5024
|
+
rank: 2,
|
|
4652
5025
|
inclusive: true,
|
|
4653
5026
|
toDOM: () => ["s", 0],
|
|
4654
5027
|
parseDOM: [
|
|
@@ -8613,8 +8986,10 @@ exports.default = exports.SuperscriptExtension;
|
|
|
8613
8986
|
*
|
|
8614
8987
|
* Background-color highlighter mark. Renders as `<mark>` (semantic).
|
|
8615
8988
|
* Optional `color` attr — when set, becomes
|
|
8616
|
-
* `<mark style="background-color: COLOR"
|
|
8617
|
-
*
|
|
8989
|
+
* `<mark style="background-color: COLOR; color: TEXT">`, where TEXT keeps
|
|
8990
|
+
* the text readable on the highlight: white on dark colours, near-black
|
|
8991
|
+
* on light ones, or `textColor` when given. When unset, plain `<mark>`
|
|
8992
|
+
* uses the theme's default yellow.
|
|
8618
8993
|
*
|
|
8619
8994
|
* Security: only safe-looking CSS color tokens are accepted (hex,
|
|
8620
8995
|
* rgb/rgba, hsl/hsla, named keywords). Anything else is rejected at
|
|
@@ -8629,25 +9004,29 @@ exports.HighlightExtension = {
|
|
|
8629
9004
|
{
|
|
8630
9005
|
name: "highlight",
|
|
8631
9006
|
inclusive: true,
|
|
8632
|
-
attrs: { color: { default: null } },
|
|
9007
|
+
attrs: { color: { default: null }, textColor: { default: null } },
|
|
8633
9008
|
toDOM: (mark) => {
|
|
8634
|
-
var _a;
|
|
9009
|
+
var _a, _b, _c;
|
|
8635
9010
|
const color = (0, core_1.sanitizeCSSColor)((_a = mark.attrs) === null || _a === void 0 ? void 0 : _a.color);
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
9011
|
+
if (!color)
|
|
9012
|
+
return ["mark", 0];
|
|
9013
|
+
const text = (_c = (0, core_1.sanitizeCSSColor)((_b = mark.attrs) === null || _b === void 0 ? void 0 : _b.textColor)) !== null && _c !== void 0 ? _c : (0, core_1.contrastTextColor)(color);
|
|
9014
|
+
return ["mark", { style: `background-color: ${color}; color: ${text}` }, 0];
|
|
8639
9015
|
},
|
|
8640
9016
|
parseDOM: [
|
|
8641
9017
|
{
|
|
8642
9018
|
tag: "mark",
|
|
8643
9019
|
getAttrs: (el) => {
|
|
8644
|
-
var _a;
|
|
9020
|
+
var _a, _b, _c;
|
|
8645
9021
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
8646
9022
|
const m = /background(?:-color)?\s*:\s*([^;]+)/i.exec(style);
|
|
8647
9023
|
if (!m)
|
|
8648
9024
|
return null;
|
|
8649
9025
|
const color = (0, core_1.sanitizeCSSColor)(m[1]);
|
|
8650
|
-
|
|
9026
|
+
if (!color)
|
|
9027
|
+
return false;
|
|
9028
|
+
const text = /(?:^|;)\s*color\s*:\s*([^;]+)/i.exec(style);
|
|
9029
|
+
return { color, textColor: (_c = (0, core_1.sanitizeCSSColor)((_b = text === null || text === void 0 ? void 0 : text[1]) === null || _b === void 0 ? void 0 : _b.trim())) !== null && _c !== void 0 ? _c : (0, core_1.contrastTextColor)(color) };
|
|
8651
9030
|
},
|
|
8652
9031
|
},
|
|
8653
9032
|
],
|
|
@@ -8655,19 +9034,24 @@ exports.HighlightExtension = {
|
|
|
8655
9034
|
],
|
|
8656
9035
|
commands: {
|
|
8657
9036
|
/**
|
|
8658
|
-
* setHighlight({ color }) — highlights the selected text.
|
|
8659
|
-
* colour it applies (and replaces) a coloured highlight on just
|
|
8660
|
-
* the selected range
|
|
9037
|
+
* setHighlight({ color, textColor? }) — highlights the selected text.
|
|
9038
|
+
* With a colour it applies (and replaces) a coloured highlight on just
|
|
9039
|
+
* the selected range, making the text readable on it (automatic
|
|
9040
|
+
* white / near-black, or `textColor`) and removing any text colour or
|
|
9041
|
+
* fill there; without one it toggles a plain `<mark>`.
|
|
8661
9042
|
*/
|
|
8662
9043
|
setHighlight: (opts = {}) => (editor) => {
|
|
9044
|
+
var _a;
|
|
8663
9045
|
const color = opts.color ? (0, core_1.sanitizeCSSColor)(opts.color) : null;
|
|
8664
9046
|
const selection = editor.getSelection();
|
|
8665
9047
|
if (!selection)
|
|
8666
9048
|
return false;
|
|
8667
9049
|
if (color) {
|
|
8668
|
-
const
|
|
9050
|
+
const textColor = (_a = (0, core_1.sanitizeCSSColor)(opts.textColor)) !== null && _a !== void 0 ? _a : (0, core_1.contrastTextColor)(color);
|
|
9051
|
+
const cleared = ["text_color", "background_color"].reduce((doc, mark) => { var _a; return (_a = (0, core_1.unsetMarkAcrossSelection)(doc, selection, mark)) !== null && _a !== void 0 ? _a : doc; }, editor.getJSON());
|
|
9052
|
+
const next = (0, core_1.setMarkAcrossSelection)(cleared, selection, {
|
|
8669
9053
|
type: "highlight",
|
|
8670
|
-
attrs: { color },
|
|
9054
|
+
attrs: { color, textColor },
|
|
8671
9055
|
});
|
|
8672
9056
|
if (!next)
|
|
8673
9057
|
return false;
|
|
@@ -8716,6 +9100,9 @@ exports.TextColorExtension = {
|
|
|
8716
9100
|
marks: [
|
|
8717
9101
|
{
|
|
8718
9102
|
name: "text_color",
|
|
9103
|
+
// Inside fills, so an explicit text colour wins over the fill's
|
|
9104
|
+
// automatic one.
|
|
9105
|
+
rank: 1,
|
|
8719
9106
|
inclusive: true,
|
|
8720
9107
|
attrs: { color: { default: null } },
|
|
8721
9108
|
toDOM: (mark) => {
|
|
@@ -8727,8 +9114,16 @@ exports.TextColorExtension = {
|
|
|
8727
9114
|
{
|
|
8728
9115
|
tag: "*",
|
|
8729
9116
|
getAttrs: (el) => {
|
|
8730
|
-
var _a;
|
|
8731
|
-
|
|
9117
|
+
var _a, _b;
|
|
9118
|
+
// A fill span's colour is the fill's readable text colour,
|
|
9119
|
+
// not a separate text colour (see extension-background-color).
|
|
9120
|
+
// Same for a coloured <mark>: its colour belongs to the highlight.
|
|
9121
|
+
if (el.tagName.toLowerCase() === "mark")
|
|
9122
|
+
return false;
|
|
9123
|
+
const classes = ((_a = el.getAttribute("class")) !== null && _a !== void 0 ? _a : "").split(/\s+/);
|
|
9124
|
+
if (classes.includes("smeditor-fill") || classes.includes("smeditor-text-outline"))
|
|
9125
|
+
return false;
|
|
9126
|
+
const style = (_b = el.getAttribute("style")) !== null && _b !== void 0 ? _b : "";
|
|
8732
9127
|
// Match `color:` only at the start of a declaration so a
|
|
8733
9128
|
// `background-color:` declaration is never mistaken for it.
|
|
8734
9129
|
const m = /(?:^|;)\s*color\s*:\s*([^;]+)/i.exec(style);
|
|
@@ -8882,27 +9277,50 @@ exports.default = exports.TextStrokeExtension;
|
|
|
8882
9277
|
/**
|
|
8883
9278
|
* @smeditor/extension-background-color
|
|
8884
9279
|
*
|
|
8885
|
-
*
|
|
8886
|
-
*
|
|
8887
|
-
*
|
|
8888
|
-
*
|
|
9280
|
+
* Fills the area behind the selected text and keeps the text readable on
|
|
9281
|
+
* it: the text becomes white on dark fills and near-black on light ones
|
|
9282
|
+
* (or a colour chosen explicitly). Renders as
|
|
9283
|
+
* `<span class="smeditor-fill" style="background-color: …; color: …">`.
|
|
9284
|
+
*
|
|
9285
|
+
* The class tells the text-colour mark that this span's `color` belongs
|
|
9286
|
+
* to the fill, so it isn't parsed as a separate text colour. Plain
|
|
9287
|
+
* `background-color` spans (older content, pastes) are still read; their
|
|
9288
|
+
* text gets the automatic contrast colour.
|
|
9289
|
+
*
|
|
9290
|
+
* Distinct from `highlight` (`<mark>`): highlight is a semantic marker
|
|
9291
|
+
* and doesn't touch the text colour.
|
|
8889
9292
|
*/
|
|
8890
9293
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8891
|
-
exports.BackgroundColorExtension = void 0;
|
|
9294
|
+
exports.BackgroundColorExtension = exports.FILL_CLASS = void 0;
|
|
9295
|
+
exports.isFillElement = isFillElement;
|
|
8892
9296
|
const core_1 = require("@smeditor/core");
|
|
9297
|
+
exports.FILL_CLASS = "smeditor-fill";
|
|
9298
|
+
/** Class used for filled text by 0.3.2 of the Rails adapter; read as a fill. */
|
|
9299
|
+
const LEGACY_FILL_CLASSES = [exports.FILL_CLASS, "smeditor-text-outline"];
|
|
9300
|
+
/** True when an element is a fill span rendered by this extension. */
|
|
9301
|
+
function isFillElement(el) {
|
|
9302
|
+
var _a;
|
|
9303
|
+
const classes = ((_a = el.getAttribute("class")) !== null && _a !== void 0 ? _a : "").split(/\s+/);
|
|
9304
|
+
return LEGACY_FILL_CLASSES.some((name) => classes.includes(name));
|
|
9305
|
+
}
|
|
9306
|
+
function declaration(style, name) {
|
|
9307
|
+
const m = new RegExp(`(?:^|;)\\s*${name}\\s*:\\s*([^;]+)`, "i").exec(style);
|
|
9308
|
+
return m ? m[1].trim() : null;
|
|
9309
|
+
}
|
|
8893
9310
|
exports.BackgroundColorExtension = {
|
|
8894
9311
|
name: "background_color",
|
|
8895
9312
|
marks: [
|
|
8896
9313
|
{
|
|
8897
9314
|
name: "background_color",
|
|
8898
9315
|
inclusive: true,
|
|
8899
|
-
attrs: { color: { default: null } },
|
|
9316
|
+
attrs: { color: { default: null }, textColor: { default: null } },
|
|
8900
9317
|
toDOM: (mark) => {
|
|
8901
|
-
var _a;
|
|
9318
|
+
var _a, _b, _c;
|
|
8902
9319
|
const color = (0, core_1.sanitizeCSSColor)((_a = mark.attrs) === null || _a === void 0 ? void 0 : _a.color);
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
9320
|
+
if (!color)
|
|
9321
|
+
return ["span", 0];
|
|
9322
|
+
const text = (_c = (0, core_1.sanitizeCSSColor)((_b = mark.attrs) === null || _b === void 0 ? void 0 : _b.textColor)) !== null && _c !== void 0 ? _c : (0, core_1.contrastTextColor)(color);
|
|
9323
|
+
return ["span", { class: exports.FILL_CLASS, style: `background-color: ${color}; color: ${text}` }, 0];
|
|
8906
9324
|
},
|
|
8907
9325
|
parseDOM: [
|
|
8908
9326
|
{
|
|
@@ -8912,26 +9330,34 @@ exports.BackgroundColorExtension = {
|
|
|
8912
9330
|
if (el.tagName.toLowerCase() === "mark")
|
|
8913
9331
|
return false;
|
|
8914
9332
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
8915
|
-
const
|
|
8916
|
-
if (!
|
|
9333
|
+
const color = (0, core_1.sanitizeCSSColor)(declaration(style, "background(?:-color)?"));
|
|
9334
|
+
if (!color)
|
|
8917
9335
|
return false;
|
|
8918
|
-
const
|
|
8919
|
-
return color ?
|
|
9336
|
+
const text = isFillElement(el) ? (0, core_1.sanitizeCSSColor)(declaration(style, "color")) : null;
|
|
9337
|
+
return { color, textColor: text !== null && text !== void 0 ? text : (0, core_1.contrastTextColor)(color) };
|
|
8920
9338
|
},
|
|
8921
9339
|
},
|
|
8922
9340
|
],
|
|
8923
9341
|
},
|
|
8924
9342
|
],
|
|
8925
9343
|
commands: {
|
|
8926
|
-
/**
|
|
9344
|
+
/**
|
|
9345
|
+
* setBackgroundColor({ color, textColor? }) — fills the selected text.
|
|
9346
|
+
* The text becomes `textColor`, or white / near-black automatically
|
|
9347
|
+
* for contrast; any text colour or highlight on the range is removed
|
|
9348
|
+
* so nothing paints over the fill.
|
|
9349
|
+
*/
|
|
8927
9350
|
setBackgroundColor: (opts) => (editor) => {
|
|
9351
|
+
var _a;
|
|
8928
9352
|
const color = (0, core_1.sanitizeCSSColor)(opts === null || opts === void 0 ? void 0 : opts.color);
|
|
8929
9353
|
if (!color)
|
|
8930
9354
|
return false;
|
|
9355
|
+
const textColor = (_a = (0, core_1.sanitizeCSSColor)(opts === null || opts === void 0 ? void 0 : opts.textColor)) !== null && _a !== void 0 ? _a : (0, core_1.contrastTextColor)(color);
|
|
8931
9356
|
const selection = editor.getSelection();
|
|
8932
|
-
const
|
|
9357
|
+
const cleared = ["text_color", "highlight"].reduce((doc, mark) => { var _a; return (_a = (0, core_1.unsetMarkAcrossSelection)(doc, selection, mark)) !== null && _a !== void 0 ? _a : doc; }, editor.getJSON());
|
|
9358
|
+
const next = (0, core_1.setMarkAcrossSelection)(cleared, selection, {
|
|
8933
9359
|
type: "background_color",
|
|
8934
|
-
attrs: { color },
|
|
9360
|
+
attrs: { color, textColor },
|
|
8935
9361
|
});
|
|
8936
9362
|
if (!next)
|
|
8937
9363
|
return false;
|