smeditor 0.2.5 → 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 +34 -0
- data/README.md +3 -0
- data/app/assets/javascripts/smeditor.js +571 -135
- data/app/assets/stylesheets/smeditor.css +146 -37
- data/lib/generators/smeditor/templates/initializer.rb +3 -0
- data/lib/smeditor/rails/config.rb +3 -0
- data/lib/smeditor/rails/form_helper.rb +26 -10
- 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);
|
|
@@ -595,10 +820,15 @@ function bootMount(mount) {
|
|
|
595
820
|
const kit = mount.dataset.smeditorKit === "full" ? "full" : "starter";
|
|
596
821
|
const uploadUrl = mount.dataset.smeditorUploadUrl || undefined;
|
|
597
822
|
const placeholder = mount.dataset.smeditorPlaceholder || undefined;
|
|
598
|
-
const
|
|
823
|
+
const configuredTheme = mount.dataset.smeditorTheme;
|
|
824
|
+
const inheritedTheme = ((_b = (_a = mount.parentElement) === null || _a === void 0 ? void 0 : _a.closest("[data-theme]")) === null || _b === void 0 ? void 0 : _b.dataset.theme)
|
|
599
825
|
|| document.documentElement.dataset.theme;
|
|
600
|
-
|
|
601
|
-
|
|
826
|
+
const theme = configuredTheme === "dark" || configuredTheme === "light"
|
|
827
|
+
? configuredTheme
|
|
828
|
+
: inheritedTheme === "dark" || inheritedTheme === "light"
|
|
829
|
+
? inheritedTheme
|
|
830
|
+
: "light";
|
|
831
|
+
mount.dataset.theme = theme;
|
|
602
832
|
const root = shadowRootFor(mount);
|
|
603
833
|
const shell = document.createElement("div");
|
|
604
834
|
shell.className = "smeditor";
|
|
@@ -621,6 +851,7 @@ function bootMount(mount) {
|
|
|
621
851
|
extensions: kitExtensions(kit, uploadUrl),
|
|
622
852
|
content: input.value || "<p></p>",
|
|
623
853
|
placeholder,
|
|
854
|
+
theme,
|
|
624
855
|
deepSelection: true,
|
|
625
856
|
onUpdate: ({ editor: current }) => {
|
|
626
857
|
input.value = current.getHTML();
|
|
@@ -695,8 +926,8 @@ if (typeof window !== "undefined") {
|
|
|
695
926
|
* stay internal so we can evolve them without breaking consumers.
|
|
696
927
|
*/
|
|
697
928
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
698
|
-
exports.
|
|
699
|
-
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;
|
|
700
931
|
var editor_js_1 = require("./editor.js");
|
|
701
932
|
Object.defineProperty(exports, "createEditor", { enumerable: true, get: function () { return editor_js_1.createEditor; } });
|
|
702
933
|
var schema_js_1 = require("./schema.js");
|
|
@@ -708,6 +939,7 @@ Object.defineProperty(exports, "parseHTML", { enumerable: true, get: function ()
|
|
|
708
939
|
Object.defineProperty(exports, "escapeHTML", { enumerable: true, get: function () { return serializer_js_1.escapeHTML; } });
|
|
709
940
|
var css_js_1 = require("./css.js");
|
|
710
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; } });
|
|
711
943
|
Object.defineProperty(exports, "sanitizeCSSFontFamily", { enumerable: true, get: function () { return css_js_1.sanitizeCSSFontFamily; } });
|
|
712
944
|
Object.defineProperty(exports, "sanitizeCSSFontSize", { enumerable: true, get: function () { return css_js_1.sanitizeCSSFontSize; } });
|
|
713
945
|
Object.defineProperty(exports, "sanitizeCSSLineHeight", { enumerable: true, get: function () { return css_js_1.sanitizeCSSLineHeight; } });
|
|
@@ -725,6 +957,7 @@ Object.defineProperty(exports, "docsEqual", { enumerable: true, get: function ()
|
|
|
725
957
|
Object.defineProperty(exports, "emptyDocument", { enumerable: true, get: function () { return doc_utils_js_1.emptyDocument; } });
|
|
726
958
|
Object.defineProperty(exports, "hasMark", { enumerable: true, get: function () { return doc_utils_js_1.hasMark; } });
|
|
727
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; } });
|
|
728
961
|
Object.defineProperty(exports, "toggleMarkInDoc", { enumerable: true, get: function () { return doc_utils_js_1.toggleMarkInDoc; } });
|
|
729
962
|
Object.defineProperty(exports, "setMarkAcrossSelection", { enumerable: true, get: function () { return doc_utils_js_1.setMarkAcrossSelection; } });
|
|
730
963
|
Object.defineProperty(exports, "unsetMarkAcrossSelection", { enumerable: true, get: function () { return doc_utils_js_1.unsetMarkAcrossSelection; } });
|
|
@@ -800,7 +1033,7 @@ function createEditor(options) {
|
|
|
800
1033
|
}
|
|
801
1034
|
class Editor {
|
|
802
1035
|
constructor(options) {
|
|
803
|
-
var _a, _b, _c;
|
|
1036
|
+
var _a, _b, _c, _d;
|
|
804
1037
|
this.selection = null;
|
|
805
1038
|
this.history = new history_js_1.History();
|
|
806
1039
|
this.emitter = new events_js_1.EventEmitter();
|
|
@@ -830,10 +1063,13 @@ class Editor {
|
|
|
830
1063
|
// Capture the finished composed text in one shot.
|
|
831
1064
|
this.handleDOMInput();
|
|
832
1065
|
};
|
|
833
|
-
this.options =
|
|
834
|
-
|
|
1066
|
+
this.options = {
|
|
1067
|
+
...options,
|
|
1068
|
+
theme: (_a = options.theme) !== null && _a !== void 0 ? _a : "light",
|
|
1069
|
+
};
|
|
1070
|
+
this.element = (_b = options.element) !== null && _b !== void 0 ? _b : null;
|
|
835
1071
|
// 1. Resolve extensions
|
|
836
|
-
this.extensions = (0, schema_js_1.flattenExtensions)((
|
|
1072
|
+
this.extensions = (0, schema_js_1.flattenExtensions)((_c = options.extensions) !== null && _c !== void 0 ? _c : []);
|
|
837
1073
|
// 2. Compile schema
|
|
838
1074
|
this.schema = (0, schema_js_1.compileSchema)(this.extensions);
|
|
839
1075
|
// 3. Normalize initial content into JSON
|
|
@@ -842,7 +1078,7 @@ class Editor {
|
|
|
842
1078
|
this.commands = this.buildCommandMap();
|
|
843
1079
|
// 5. Lifecycle: onCreate for each extension
|
|
844
1080
|
for (const ext of this.extensions) {
|
|
845
|
-
(
|
|
1081
|
+
(_d = ext.onCreate) === null || _d === void 0 ? void 0 : _d.call(ext, this);
|
|
846
1082
|
}
|
|
847
1083
|
// 6. Mount to DOM if an element was given
|
|
848
1084
|
if (this.element) {
|
|
@@ -1070,6 +1306,11 @@ class Editor {
|
|
|
1070
1306
|
}
|
|
1071
1307
|
return map;
|
|
1072
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
|
+
}
|
|
1073
1314
|
isActive(name, attrs) {
|
|
1074
1315
|
var _a, _b;
|
|
1075
1316
|
// Mark active check
|
|
@@ -1129,9 +1370,10 @@ class Editor {
|
|
|
1129
1370
|
this.attachToElement();
|
|
1130
1371
|
}
|
|
1131
1372
|
attachToElement() {
|
|
1132
|
-
var _a;
|
|
1373
|
+
var _a, _b;
|
|
1133
1374
|
const el = this.element;
|
|
1134
1375
|
el.setAttribute("contenteditable", String(this.options.editable !== false));
|
|
1376
|
+
el.setAttribute("data-theme", (_a = this.options.theme) !== null && _a !== void 0 ? _a : "light");
|
|
1135
1377
|
el.classList.add("smeditor-editor");
|
|
1136
1378
|
el.addEventListener("input", this.onDomInput);
|
|
1137
1379
|
el.addEventListener("beforeinput", this.onDomBeforeInput);
|
|
@@ -1145,7 +1387,7 @@ class Editor {
|
|
|
1145
1387
|
// Keep the model selection in sync in both regular DOM and Shadow DOM.
|
|
1146
1388
|
// Chromium exposes the live shadow selection on ShadowRoot; listening to
|
|
1147
1389
|
// that root as well as Document avoids missing drag-selection events.
|
|
1148
|
-
const root = (
|
|
1390
|
+
const root = (_b = el.getRootNode) === null || _b === void 0 ? void 0 : _b.call(el);
|
|
1149
1391
|
const targets = [document];
|
|
1150
1392
|
if (root && root !== document && "addEventListener" in root)
|
|
1151
1393
|
targets.push(root);
|
|
@@ -1686,6 +1928,8 @@ function sanitizePastedStyleValue(name, value) {
|
|
|
1686
1928
|
if (name === "background-color" || name === "background") {
|
|
1687
1929
|
return (0, css_js_1.sanitizeCSSColor)(value);
|
|
1688
1930
|
}
|
|
1931
|
+
if (name === "text-decoration-color")
|
|
1932
|
+
return (0, css_js_1.sanitizeCSSColor)(value);
|
|
1689
1933
|
if (name === "font-family")
|
|
1690
1934
|
return (0, css_js_1.sanitizeCSSFontFamily)(value);
|
|
1691
1935
|
if (name === "font-size")
|
|
@@ -2067,33 +2311,66 @@ exports.parseHTML = parseHTML;
|
|
|
2067
2311
|
function serializeToHTML(doc, schema) {
|
|
2068
2312
|
if (!doc || doc.type !== "doc" || !doc.content)
|
|
2069
2313
|
return "";
|
|
2070
|
-
return doc.content
|
|
2314
|
+
return renderChildren(doc.content, schema);
|
|
2071
2315
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
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;
|
|
2084
2352
|
}
|
|
2085
|
-
|
|
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;
|
|
2086
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 : "");
|
|
2087
2368
|
const spec = schema.nodes[node.type];
|
|
2088
2369
|
if (!(spec === null || spec === void 0 ? void 0 : spec.toDOM)) {
|
|
2089
2370
|
// Unknown node — render children if any.
|
|
2090
|
-
return ((_b = node.content) !== null && _b !== void 0 ? _b : [])
|
|
2091
|
-
.map((c) => renderNode(c, schema))
|
|
2092
|
-
.join("");
|
|
2371
|
+
return renderChildren((_b = node.content) !== null && _b !== void 0 ? _b : [], schema);
|
|
2093
2372
|
}
|
|
2094
|
-
let inner = ((_c = node.content) !== null && _c !== void 0 ? _c : [])
|
|
2095
|
-
.map((c) => renderNode(c, schema))
|
|
2096
|
-
.join("");
|
|
2373
|
+
let inner = renderChildren((_c = node.content) !== null && _c !== void 0 ? _c : [], schema);
|
|
2097
2374
|
// An empty leaf block (a blank paragraph, an empty table cell) must
|
|
2098
2375
|
// still render with a <br> inside it — otherwise the element
|
|
2099
2376
|
// collapses to zero height in contenteditable and the caret can't
|
|
@@ -2843,6 +3120,7 @@ exports.stripBlockPrefix = stripBlockPrefix;
|
|
|
2843
3120
|
exports.forEachTextNode = forEachTextNode;
|
|
2844
3121
|
exports.hasMark = hasMark;
|
|
2845
3122
|
exports.selectionHasMark = selectionHasMark;
|
|
3123
|
+
exports.selectionMarkAttrs = selectionMarkAttrs;
|
|
2846
3124
|
exports.textsInSelection = textsInSelection;
|
|
2847
3125
|
exports.toggleMarkInDoc = toggleMarkInDoc;
|
|
2848
3126
|
exports.setMarkAcrossSelection = setMarkAcrossSelection;
|
|
@@ -2979,6 +3257,23 @@ function selectionHasMark(doc, selection, type, attrs) {
|
|
|
2979
3257
|
return false;
|
|
2980
3258
|
return texts.every((t) => hasMark(t, type, attrs));
|
|
2981
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
|
+
}
|
|
2982
3277
|
/** Collect every text node intersecting the selection. */
|
|
2983
3278
|
function textsInSelection(doc, selection) {
|
|
2984
3279
|
const result = [];
|
|
@@ -3091,9 +3386,8 @@ function toggleMarkInDoc(doc, selection, type, attrs) {
|
|
|
3091
3386
|
* Behaviour:
|
|
3092
3387
|
* - a real range → the mark is set on just that span, replacing any
|
|
3093
3388
|
* existing mark of the same type so colours/sizes swap cleanly;
|
|
3094
|
-
* - a collapsed caret → the mark is applied to the
|
|
3095
|
-
* caret
|
|
3096
|
-
* 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).
|
|
3097
3391
|
*
|
|
3098
3392
|
* Returns a new document, or null when nothing changed.
|
|
3099
3393
|
*/
|
|
@@ -3164,8 +3458,12 @@ function leafRangesInSelection(doc, selection) {
|
|
|
3164
3458
|
let from;
|
|
3165
3459
|
let to;
|
|
3166
3460
|
if (collapsed) {
|
|
3167
|
-
|
|
3168
|
-
|
|
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;
|
|
3169
3467
|
}
|
|
3170
3468
|
else {
|
|
3171
3469
|
const isStart = samePath(path, start.path);
|
|
@@ -3177,6 +3475,21 @@ function leafRangesInSelection(doc, selection) {
|
|
|
3177
3475
|
})
|
|
3178
3476
|
.filter(({ from, to }) => to > from);
|
|
3179
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
|
+
}
|
|
3180
3493
|
function inlineContentLength(content) {
|
|
3181
3494
|
// Use the same visible-width measure as splitInlineAt / setMarkOnInlineRange:
|
|
3182
3495
|
// an atomic inline node (a mention pill) contributes its full label
|
|
@@ -3714,6 +4027,7 @@ exports.sanitizeCSSTextStrokeWidth = sanitizeCSSTextStrokeWidth;
|
|
|
3714
4027
|
exports.sanitizeURL = sanitizeURL;
|
|
3715
4028
|
exports.sanitizeLinkTarget = sanitizeLinkTarget;
|
|
3716
4029
|
exports.hardenLinkAttrs = hardenLinkAttrs;
|
|
4030
|
+
exports.contrastTextColor = contrastTextColor;
|
|
3717
4031
|
const MAX_COLOR_LENGTH = 80;
|
|
3718
4032
|
const HEX_COLOR = /^#[0-9a-fA-F]{3,8}$/;
|
|
3719
4033
|
const CSS_IDENTIFIER = /^[a-zA-Z]+$/;
|
|
@@ -3909,6 +4223,33 @@ function normaliseProtocols(protocols) {
|
|
|
3909
4223
|
.map((protocol) => protocol.trim().replace(/:$/, "").toLowerCase())
|
|
3910
4224
|
.filter(Boolean));
|
|
3911
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
|
+
}
|
|
3912
4253
|
|
|
3913
4254
|
},{}],
|
|
3914
4255
|
11:[function(require,module,exports){
|
|
@@ -4570,21 +4911,51 @@ exports.default = exports.ItalicExtension;
|
|
|
4570
4911
|
/**
|
|
4571
4912
|
* @smeditor/extension-underline
|
|
4572
4913
|
*
|
|
4573
|
-
* 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
|
|
4574
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.
|
|
4575
4920
|
*/
|
|
4576
4921
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4577
4922
|
exports.UnderlineExtension = void 0;
|
|
4578
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
|
+
}
|
|
4579
4944
|
exports.UnderlineExtension = {
|
|
4580
4945
|
name: "underline",
|
|
4581
4946
|
marks: [
|
|
4582
4947
|
{
|
|
4583
4948
|
name: "underline",
|
|
4584
4949
|
inclusive: true,
|
|
4585
|
-
|
|
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
|
+
},
|
|
4586
4957
|
parseDOM: [
|
|
4587
|
-
{ tag: "u" },
|
|
4958
|
+
{ tag: "u", getAttrs: (el) => colorAttrs(el) },
|
|
4588
4959
|
{
|
|
4589
4960
|
// Catch <span style="text-decoration: underline"> from MS Word
|
|
4590
4961
|
// / Google Docs paste. We don't try to be exhaustive — common
|
|
@@ -4593,7 +4964,7 @@ exports.UnderlineExtension = {
|
|
|
4593
4964
|
getAttrs: (el) => {
|
|
4594
4965
|
var _a;
|
|
4595
4966
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
4596
|
-
return /text-decoration
|
|
4967
|
+
return /text-decoration(?:-line)?:\s*[^;]*underline/i.test(style) ? colorAttrs(el) : false;
|
|
4597
4968
|
},
|
|
4598
4969
|
},
|
|
4599
4970
|
],
|
|
@@ -4611,6 +4982,16 @@ exports.UnderlineExtension = {
|
|
|
4611
4982
|
});
|
|
4612
4983
|
return true;
|
|
4613
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,
|
|
4614
4995
|
},
|
|
4615
4996
|
keyboardShortcuts: {
|
|
4616
4997
|
"Mod-u": (editor) => {
|
|
@@ -4639,6 +5020,8 @@ exports.StrikeExtension = {
|
|
|
4639
5020
|
marks: [
|
|
4640
5021
|
{
|
|
4641
5022
|
name: "strike",
|
|
5023
|
+
// Innermost, so the line takes the colour of the text it crosses.
|
|
5024
|
+
rank: 2,
|
|
4642
5025
|
inclusive: true,
|
|
4643
5026
|
toDOM: () => ["s", 0],
|
|
4644
5027
|
parseDOM: [
|
|
@@ -8603,8 +8986,10 @@ exports.default = exports.SuperscriptExtension;
|
|
|
8603
8986
|
*
|
|
8604
8987
|
* Background-color highlighter mark. Renders as `<mark>` (semantic).
|
|
8605
8988
|
* Optional `color` attr — when set, becomes
|
|
8606
|
-
* `<mark style="background-color: COLOR"
|
|
8607
|
-
*
|
|
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.
|
|
8608
8993
|
*
|
|
8609
8994
|
* Security: only safe-looking CSS color tokens are accepted (hex,
|
|
8610
8995
|
* rgb/rgba, hsl/hsla, named keywords). Anything else is rejected at
|
|
@@ -8619,25 +9004,29 @@ exports.HighlightExtension = {
|
|
|
8619
9004
|
{
|
|
8620
9005
|
name: "highlight",
|
|
8621
9006
|
inclusive: true,
|
|
8622
|
-
attrs: { color: { default: null } },
|
|
9007
|
+
attrs: { color: { default: null }, textColor: { default: null } },
|
|
8623
9008
|
toDOM: (mark) => {
|
|
8624
|
-
var _a;
|
|
9009
|
+
var _a, _b, _c;
|
|
8625
9010
|
const color = (0, core_1.sanitizeCSSColor)((_a = mark.attrs) === null || _a === void 0 ? void 0 : _a.color);
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
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];
|
|
8629
9015
|
},
|
|
8630
9016
|
parseDOM: [
|
|
8631
9017
|
{
|
|
8632
9018
|
tag: "mark",
|
|
8633
9019
|
getAttrs: (el) => {
|
|
8634
|
-
var _a;
|
|
9020
|
+
var _a, _b, _c;
|
|
8635
9021
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
8636
9022
|
const m = /background(?:-color)?\s*:\s*([^;]+)/i.exec(style);
|
|
8637
9023
|
if (!m)
|
|
8638
9024
|
return null;
|
|
8639
9025
|
const color = (0, core_1.sanitizeCSSColor)(m[1]);
|
|
8640
|
-
|
|
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) };
|
|
8641
9030
|
},
|
|
8642
9031
|
},
|
|
8643
9032
|
],
|
|
@@ -8645,19 +9034,24 @@ exports.HighlightExtension = {
|
|
|
8645
9034
|
],
|
|
8646
9035
|
commands: {
|
|
8647
9036
|
/**
|
|
8648
|
-
* setHighlight({ color }) — highlights the selected text.
|
|
8649
|
-
* colour it applies (and replaces) a coloured highlight on just
|
|
8650
|
-
* 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>`.
|
|
8651
9042
|
*/
|
|
8652
9043
|
setHighlight: (opts = {}) => (editor) => {
|
|
9044
|
+
var _a;
|
|
8653
9045
|
const color = opts.color ? (0, core_1.sanitizeCSSColor)(opts.color) : null;
|
|
8654
9046
|
const selection = editor.getSelection();
|
|
8655
9047
|
if (!selection)
|
|
8656
9048
|
return false;
|
|
8657
9049
|
if (color) {
|
|
8658
|
-
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, {
|
|
8659
9053
|
type: "highlight",
|
|
8660
|
-
attrs: { color },
|
|
9054
|
+
attrs: { color, textColor },
|
|
8661
9055
|
});
|
|
8662
9056
|
if (!next)
|
|
8663
9057
|
return false;
|
|
@@ -8706,6 +9100,9 @@ exports.TextColorExtension = {
|
|
|
8706
9100
|
marks: [
|
|
8707
9101
|
{
|
|
8708
9102
|
name: "text_color",
|
|
9103
|
+
// Inside fills, so an explicit text colour wins over the fill's
|
|
9104
|
+
// automatic one.
|
|
9105
|
+
rank: 1,
|
|
8709
9106
|
inclusive: true,
|
|
8710
9107
|
attrs: { color: { default: null } },
|
|
8711
9108
|
toDOM: (mark) => {
|
|
@@ -8717,8 +9114,16 @@ exports.TextColorExtension = {
|
|
|
8717
9114
|
{
|
|
8718
9115
|
tag: "*",
|
|
8719
9116
|
getAttrs: (el) => {
|
|
8720
|
-
var _a;
|
|
8721
|
-
|
|
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 : "";
|
|
8722
9127
|
// Match `color:` only at the start of a declaration so a
|
|
8723
9128
|
// `background-color:` declaration is never mistaken for it.
|
|
8724
9129
|
const m = /(?:^|;)\s*color\s*:\s*([^;]+)/i.exec(style);
|
|
@@ -8872,27 +9277,50 @@ exports.default = exports.TextStrokeExtension;
|
|
|
8872
9277
|
/**
|
|
8873
9278
|
* @smeditor/extension-background-color
|
|
8874
9279
|
*
|
|
8875
|
-
*
|
|
8876
|
-
*
|
|
8877
|
-
*
|
|
8878
|
-
*
|
|
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.
|
|
8879
9292
|
*/
|
|
8880
9293
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8881
|
-
exports.BackgroundColorExtension = void 0;
|
|
9294
|
+
exports.BackgroundColorExtension = exports.FILL_CLASS = void 0;
|
|
9295
|
+
exports.isFillElement = isFillElement;
|
|
8882
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
|
+
}
|
|
8883
9310
|
exports.BackgroundColorExtension = {
|
|
8884
9311
|
name: "background_color",
|
|
8885
9312
|
marks: [
|
|
8886
9313
|
{
|
|
8887
9314
|
name: "background_color",
|
|
8888
9315
|
inclusive: true,
|
|
8889
|
-
attrs: { color: { default: null } },
|
|
9316
|
+
attrs: { color: { default: null }, textColor: { default: null } },
|
|
8890
9317
|
toDOM: (mark) => {
|
|
8891
|
-
var _a;
|
|
9318
|
+
var _a, _b, _c;
|
|
8892
9319
|
const color = (0, core_1.sanitizeCSSColor)((_a = mark.attrs) === null || _a === void 0 ? void 0 : _a.color);
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
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];
|
|
8896
9324
|
},
|
|
8897
9325
|
parseDOM: [
|
|
8898
9326
|
{
|
|
@@ -8902,26 +9330,34 @@ exports.BackgroundColorExtension = {
|
|
|
8902
9330
|
if (el.tagName.toLowerCase() === "mark")
|
|
8903
9331
|
return false;
|
|
8904
9332
|
const style = (_a = el.getAttribute("style")) !== null && _a !== void 0 ? _a : "";
|
|
8905
|
-
const
|
|
8906
|
-
if (!
|
|
9333
|
+
const color = (0, core_1.sanitizeCSSColor)(declaration(style, "background(?:-color)?"));
|
|
9334
|
+
if (!color)
|
|
8907
9335
|
return false;
|
|
8908
|
-
const
|
|
8909
|
-
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) };
|
|
8910
9338
|
},
|
|
8911
9339
|
},
|
|
8912
9340
|
],
|
|
8913
9341
|
},
|
|
8914
9342
|
],
|
|
8915
9343
|
commands: {
|
|
8916
|
-
/**
|
|
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
|
+
*/
|
|
8917
9350
|
setBackgroundColor: (opts) => (editor) => {
|
|
9351
|
+
var _a;
|
|
8918
9352
|
const color = (0, core_1.sanitizeCSSColor)(opts === null || opts === void 0 ? void 0 : opts.color);
|
|
8919
9353
|
if (!color)
|
|
8920
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);
|
|
8921
9356
|
const selection = editor.getSelection();
|
|
8922
|
-
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, {
|
|
8923
9359
|
type: "background_color",
|
|
8924
|
-
attrs: { color },
|
|
9360
|
+
attrs: { color, textColor },
|
|
8925
9361
|
});
|
|
8926
9362
|
if (!next)
|
|
8927
9363
|
return false;
|