@37signals/lexxy 0.9.33 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/lexxy.esm.js +344 -48
- package/dist/stylesheets/lexxy-editor.css +137 -2
- package/dist/stylesheets/lexxy-variables.css +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A modern rich text editor for Rails.
|
|
|
8
8
|
|
|
9
9
|
- Built on top of [Lexical](https://lexical.dev), the powerful text editor framework from Meta.
|
|
10
10
|
- Good HTML semantics. Paragraphs are real `<p>` tags, as they should be.
|
|
11
|
+
- Designed with [accessibility](https://lexxy.dev/docs/accessibility.html) in mind.
|
|
11
12
|
- Markdown support: shortcuts, auto-formatting on paste.
|
|
12
13
|
- Real-time code syntax highlighting.
|
|
13
14
|
- Create links by pasting URLs on selected text.
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { highlightCode, highlightElement } from './lexxy_helpers.esm.js';
|
|
2
2
|
import createDOMPurify from 'dompurify';
|
|
3
3
|
import { getStyleObjectFromCSS, getCSSFromStyleObject, $getSelectionStyleValueForProperty, $ensureForwardRangeSelection, $isAtNodeEnd, $patchStyleText, $setBlocksType, $forEachSelectedTextNode } from '@lexical/selection';
|
|
4
|
-
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $createTextNode, defineExtension, $createNodeSelection, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $isDecoratorNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $isNodeSelection, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, ElementNode, $splitNode, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, KEY_DOWN_COMMAND, flipDirection, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, KEY_BACKSPACE_COMMAND } from 'lexical';
|
|
4
|
+
import { SKIP_DOM_SELECTION_TAG, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, CAN_REDO_COMMAND, $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $createTextNode, defineExtension, $createNodeSelection, $getRoot, $caretFromPoint, $setSelectionFromCaretRange, $getCaretRange, $normalizeCaret, $getChildCaret, $getCaretInDirection, $isParagraphNode, $isLineBreakNode, $createParagraphNode, $isElementNode, $isRootOrShadowRoot, $isRootNode, $isDecoratorNode, $getSiblingCaret, $rewindSiblingCaret, $splitAtPointCaretNext, $normalizeSelection__EXPERIMENTAL, $isNodeSelection, $isChildCaret, $isTextPointCaret, $isExtendableTextPointCaret, $isSiblingCaret, $getCommonAncestor, $findMatchingParent, TextNode, createCommand, COMMAND_PRIORITY_EDITOR, $getEditor, $getNodeByKey, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, $cloneWithProperties, $getNearestRootOrShadowRoot, $createRangeSelection, $setSelection, ElementNode, $splitNode, createState, COMMAND_PRIORITY_NORMAL, $getState, $setState, $hasUpdateTag, PASTE_TAG, FORMAT_TEXT_COMMAND, UNDO_COMMAND, REDO_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_TAB_COMMAND, INSERT_LINE_BREAK_COMMAND, COMMAND_PRIORITY_HIGH, INSERT_PARAGRAPH_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_DOWN_COMMAND, DELETE_CHARACTER_COMMAND, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_CRITICAL, $addUpdateTag, CLICK_COMMAND, isDOMNode, $getNearestNodeFromDOMNode, $getChildCaretAtIndex, $createLineBreakNode, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, PASTE_COMMAND, $onUpdate, ParagraphNode, RootNode, DRAGSTART_COMMAND, DROP_COMMAND, KEY_DOWN_COMMAND, flipDirection, mergeRegister as mergeRegister$1, $createRangeSelectionFromDom, CLEAR_HISTORY_COMMAND, KEY_ENTER_COMMAND, KEY_SPACE_COMMAND, INPUT_COMMAND, HISTORY_PUSH_TAG, KEY_BACKSPACE_COMMAND } from 'lexical';
|
|
5
5
|
import * as lexical from 'lexical';
|
|
6
6
|
export { lexical as Lexical };
|
|
7
7
|
import { LinkNode, $createAutoLinkNode, $toggleLink, $createLinkNode, $isLinkNode, AutoLinkNode } from '@lexical/link';
|
|
@@ -589,6 +589,35 @@ function isKeyboardActivation(event) {
|
|
|
589
589
|
return event instanceof PointerEvent && event.pointerId === -1
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
+
function trapFocusAtTabBoundary(container, event) {
|
|
593
|
+
if (event.key !== "Tab") return
|
|
594
|
+
|
|
595
|
+
const tabStops = Array.from(container.querySelectorAll("a[href], area[href], button, input:not([type='hidden']), select, textarea, iframe, object, embed, [contenteditable]:not([contenteditable='false']), [tabindex], summary, audio[controls], video[controls]"))
|
|
596
|
+
.filter(element => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest("[inert]") && isActiveAndVisible(element))
|
|
597
|
+
.sort((first, second) => {
|
|
598
|
+
if (first.tabIndex > 0 && second.tabIndex > 0) {
|
|
599
|
+
return first.tabIndex - second.tabIndex
|
|
600
|
+
} else if (first.tabIndex > 0) {
|
|
601
|
+
return -1
|
|
602
|
+
} else if (second.tabIndex > 0) {
|
|
603
|
+
return 1
|
|
604
|
+
} else {
|
|
605
|
+
return 0
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
const firstTabStop = tabStops[0];
|
|
610
|
+
const lastTabStop = tabStops.at(-1);
|
|
611
|
+
|
|
612
|
+
if (event.shiftKey && document.activeElement === firstTabStop) {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
lastTabStop.focus();
|
|
615
|
+
} else if (!event.shiftKey && document.activeElement === lastTabStop) {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
firstTabStop.focus();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
592
621
|
function handleRollingTabIndex(elements, event, { orientation = "horizontal", wrap = false } = {}) {
|
|
593
622
|
const previousActiveElement = document.activeElement;
|
|
594
623
|
|
|
@@ -1443,10 +1472,12 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1443
1472
|
}
|
|
1444
1473
|
|
|
1445
1474
|
#handleKeyDown = (event) => {
|
|
1446
|
-
if (event.key === "Escape") {
|
|
1475
|
+
if (event.key === "Escape" && this.isOpen) {
|
|
1447
1476
|
event.stopPropagation();
|
|
1448
1477
|
this.close({ focusEditor: !this.#shouldReturnFocusToTrigger });
|
|
1449
1478
|
if (this.#shouldReturnFocusToTrigger) this.trigger?.focus();
|
|
1479
|
+
} else if (this.isOpen && this.panel.role === "dialog") {
|
|
1480
|
+
trapFocusAtTabBoundary(this.panel, event);
|
|
1450
1481
|
} else if (this.#isNavigatingMenu(event)) {
|
|
1451
1482
|
event.stopPropagation();
|
|
1452
1483
|
handleRollingTabIndex(this.#buttons, event, { orientation: "both", wrap: true });
|
|
@@ -1461,7 +1492,7 @@ class ToolbarDropdown extends HTMLElement {
|
|
|
1461
1492
|
// Ask for the ring explicitly: opening the menu with the mouse otherwise leaves the
|
|
1462
1493
|
// first item focused without a focus ring, since focus() inherits the mouse modality.
|
|
1463
1494
|
this.#interactiveElements[0]?.focus({ focusVisible: true });
|
|
1464
|
-
await this.#resetTabIndexValues();
|
|
1495
|
+
if (this.panel.role === "menu") await this.#resetTabIndexValues();
|
|
1465
1496
|
}
|
|
1466
1497
|
|
|
1467
1498
|
async #resetTabIndexValues() {
|
|
@@ -2233,12 +2264,33 @@ class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
|
2233
2264
|
const trimmedLabel = this.label.trim();
|
|
2234
2265
|
const matches = [ ...figure.querySelectorAll("span") ].filter((span) => span.textContent.trim() === trimmedLabel);
|
|
2235
2266
|
const deepest = matches.filter((span) => !matches.some((other) => other !== span && span.contains(other)));
|
|
2267
|
+
const labelImage = figure.querySelector("[data-lexxy-label-image]");
|
|
2268
|
+
|
|
2236
2269
|
for (const span of deepest) {
|
|
2237
2270
|
if (span.getAttribute("aria-hidden") !== "true") {
|
|
2238
|
-
span.
|
|
2271
|
+
if (span.contains(labelImage)) {
|
|
2272
|
+
this.#wrapLabelTextNodesIn(span);
|
|
2273
|
+
} else {
|
|
2274
|
+
span.setAttribute("data-lexxy-label-mirror", "");
|
|
2275
|
+
}
|
|
2239
2276
|
}
|
|
2240
2277
|
}
|
|
2241
2278
|
}
|
|
2279
|
+
|
|
2280
|
+
#wrapLabelTextNodesIn(element) {
|
|
2281
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
2282
|
+
const textNodes = [];
|
|
2283
|
+
|
|
2284
|
+
while (walker.nextNode()) {
|
|
2285
|
+
if (walker.currentNode.textContent.trim()) textNodes.push(walker.currentNode);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
for (const textNode of textNodes) {
|
|
2289
|
+
const mirror = createElement("span", { "data-lexxy-label-mirror": "" });
|
|
2290
|
+
textNode.replaceWith(mirror);
|
|
2291
|
+
mirror.append(textNode);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2242
2294
|
}
|
|
2243
2295
|
|
|
2244
2296
|
function $isCustomActionTextAttachmentNode(node) {
|
|
@@ -2422,14 +2474,14 @@ function extendTextNodeConversion(conversionName, ...callbacks) {
|
|
|
2422
2474
|
...conversionOutput,
|
|
2423
2475
|
forChild: (lexicalNode, parentNode) => {
|
|
2424
2476
|
const originalForChild = conversionOutput?.forChild ?? (x => x);
|
|
2425
|
-
|
|
2426
|
-
|
|
2477
|
+
const childNode = originalForChild(lexicalNode, parentNode);
|
|
2427
2478
|
|
|
2428
2479
|
if ($isTextNode(childNode)) {
|
|
2429
|
-
|
|
2480
|
+
return callbacks.reduce(
|
|
2430
2481
|
(childNode, callback) => callback(childNode, element) ?? childNode,
|
|
2431
2482
|
childNode
|
|
2432
|
-
)
|
|
2483
|
+
)
|
|
2484
|
+
} else {
|
|
2433
2485
|
return childNode
|
|
2434
2486
|
}
|
|
2435
2487
|
}
|
|
@@ -2975,7 +3027,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
2975
3027
|
node: new ActionTextAttachmentNode({
|
|
2976
3028
|
src: img.getAttribute("src"),
|
|
2977
3029
|
fileName: fileName,
|
|
2978
|
-
|
|
3030
|
+
altText: img.getAttribute("alt"),
|
|
2979
3031
|
contentType: "image/*",
|
|
2980
3032
|
width: img.getAttribute("width"),
|
|
2981
3033
|
height: img.getAttribute("height")
|
|
@@ -3300,8 +3352,9 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
3300
3352
|
}
|
|
3301
3353
|
|
|
3302
3354
|
#swapToPreviewDOM(figure, previewSrc) {
|
|
3355
|
+
const altText = this.editor.read(() => this.getLatest().altText);
|
|
3303
3356
|
this.#swapFigureContent(figure, "attachment--file", "attachment--preview", () => {
|
|
3304
|
-
const img = createElement("img", { src: previewSrc, draggable: false, alt:
|
|
3357
|
+
const img = createElement("img", { src: previewSrc, draggable: false, alt: altText });
|
|
3305
3358
|
img.onerror = () => this.#swapPreviewToFileDOM(img);
|
|
3306
3359
|
const container = createElement("div", { className: "attachment__container" });
|
|
3307
3360
|
container.appendChild(img);
|
|
@@ -6184,6 +6237,11 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
6184
6237
|
updateDOM(prevNode, dom) {
|
|
6185
6238
|
if (this.uploadError !== prevNode.uploadError) return true
|
|
6186
6239
|
|
|
6240
|
+
const image = dom.querySelector("img");
|
|
6241
|
+
if (image && prevNode.altText !== this.altText) {
|
|
6242
|
+
image.alt = this.altText;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6187
6245
|
if (prevNode.progress !== this.progress) {
|
|
6188
6246
|
const progress = dom.querySelector("progress");
|
|
6189
6247
|
progress.value = this.progress ?? 0;
|
|
@@ -6217,7 +6275,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
6217
6275
|
}
|
|
6218
6276
|
|
|
6219
6277
|
#createDOMForImage() {
|
|
6220
|
-
return createElement("img")
|
|
6278
|
+
return createElement("img", { alt: this.altText })
|
|
6221
6279
|
}
|
|
6222
6280
|
|
|
6223
6281
|
#createDOMForFile() {
|
|
@@ -6382,7 +6440,6 @@ class AttachmentNodeConversion {
|
|
|
6382
6440
|
const { blob } = this;
|
|
6383
6441
|
return {
|
|
6384
6442
|
sgid: blob.attachable_sgid,
|
|
6385
|
-
altText: blob.filename,
|
|
6386
6443
|
contentType: blob.content_type,
|
|
6387
6444
|
fileName: blob.filename,
|
|
6388
6445
|
fileSize: blob.byte_size,
|
|
@@ -6787,6 +6844,7 @@ class PastedContentFormatter {
|
|
|
6787
6844
|
format() {
|
|
6788
6845
|
this.#inlinePreservableStyles();
|
|
6789
6846
|
this.#stripStyleElements();
|
|
6847
|
+
this.#stripInterchangeNewlines();
|
|
6790
6848
|
this.#unwrapPlaceholderAnchors();
|
|
6791
6849
|
this.#stripTableCellColorStyles();
|
|
6792
6850
|
new OfficeFormatter(this.doc).format();
|
|
@@ -6822,6 +6880,35 @@ class PastedContentFormatter {
|
|
|
6822
6880
|
}
|
|
6823
6881
|
}
|
|
6824
6882
|
|
|
6883
|
+
// Chrome and Safari append <br class="Apple-interchange-newline"> when the
|
|
6884
|
+
// copied selection ends at a line end (Google Docs does this on every copy).
|
|
6885
|
+
// It marks the end of the selection, not a line the author wrote, so left in
|
|
6886
|
+
// place it pastes as an extra empty paragraph. Only the trailing marker goes:
|
|
6887
|
+
// one at the start of the fragment is a real line boundary.
|
|
6888
|
+
#stripInterchangeNewlines() {
|
|
6889
|
+
for (const br of this.doc.querySelectorAll("br.Apple-interchange-newline")) {
|
|
6890
|
+
if (this.#isAtEndOfBody(br)) {
|
|
6891
|
+
br.remove();
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
|
|
6896
|
+
#isAtEndOfBody(node) {
|
|
6897
|
+
for (let current = node; current && current !== this.doc.body; current = current.parentNode) {
|
|
6898
|
+
for (let sibling = current.nextSibling; sibling; sibling = sibling.nextSibling) {
|
|
6899
|
+
if (!this.#isIgnorableTrailingNode(sibling)) return false
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
return true
|
|
6903
|
+
}
|
|
6904
|
+
|
|
6905
|
+
#isIgnorableTrailingNode(node) {
|
|
6906
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
6907
|
+
return true
|
|
6908
|
+
}
|
|
6909
|
+
return node.nodeType === Node.TEXT_NODE && node.textContent.trim() === ""
|
|
6910
|
+
}
|
|
6911
|
+
|
|
6825
6912
|
// Anchors with non-meaningful hrefs (e.g. "#", "") appear in content copied
|
|
6826
6913
|
// from rendered views where mentions and interactive elements are wrapped in
|
|
6827
6914
|
// <a href="#"> tags. Unwrap them so their text content pastes as plain text
|
|
@@ -7260,7 +7347,7 @@ class Contents {
|
|
|
7260
7347
|
this.#performTextReplacement(anchorNode, lastIndex, stringToReplace, replacementNodes);
|
|
7261
7348
|
}
|
|
7262
7349
|
|
|
7263
|
-
uploadFiles(files, { selectLast } = {}) {
|
|
7350
|
+
uploadFiles(files, { selectLast, altText } = {}) {
|
|
7264
7351
|
if (!this.editorElement) return // Disposed (e.g. on turbo:before-cache); a late drop can still land here
|
|
7265
7352
|
|
|
7266
7353
|
if (!this.editorElement.supportsAttachments) {
|
|
@@ -7273,6 +7360,10 @@ class Contents {
|
|
|
7273
7360
|
const uploader = Uploader.for(this.editorElement, validFiles);
|
|
7274
7361
|
uploader.$uploadFiles();
|
|
7275
7362
|
|
|
7363
|
+
if (altText && files.length === 1 && uploader.nodes.length === 1) {
|
|
7364
|
+
uploader.nodes[0].getWritable().altText = altText;
|
|
7365
|
+
}
|
|
7366
|
+
|
|
7276
7367
|
if (selectLast && uploader.nodes?.length) {
|
|
7277
7368
|
const lastNode = uploader.nodes.at(-1);
|
|
7278
7369
|
lastNode.selectEnd();
|
|
@@ -7985,46 +8076,51 @@ class Clipboard {
|
|
|
7985
8076
|
}
|
|
7986
8077
|
|
|
7987
8078
|
#handlePastedFiles(clipboardData) {
|
|
7988
|
-
if (
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
if (files.length && this.#isCopiedImageHTML(html)) {
|
|
7994
|
-
this.#uploadFilesPreservingScroll(files);
|
|
7995
|
-
return true
|
|
7996
|
-
}
|
|
7997
|
-
|
|
7998
|
-
if (html && !this.#isLexicalClipboardData(clipboardData)) {
|
|
7999
|
-
this.contents.insertHtml(html, { tag: PASTE_TAG });
|
|
8000
|
-
return true
|
|
8001
|
-
}
|
|
8079
|
+
if (this.editorElement.supportsAttachments) {
|
|
8080
|
+
const html = clipboardData.getData("text/html");
|
|
8081
|
+
const files = clipboardData.files;
|
|
8082
|
+
const copiedImage = files.length && this.#copiedImage(html);
|
|
8002
8083
|
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8084
|
+
if (copiedImage) {
|
|
8085
|
+
this.#uploadFilesPreservingScroll(files, copiedImage.getAttribute("alt"));
|
|
8086
|
+
return true
|
|
8087
|
+
} else if (html && !this.#isLexicalClipboardData(clipboardData)) {
|
|
8088
|
+
this.contents.insertHtml(html, { tag: PASTE_TAG });
|
|
8089
|
+
return true
|
|
8090
|
+
} else if (files.length) {
|
|
8091
|
+
this.#uploadFilesPreservingScroll(files);
|
|
8092
|
+
return true
|
|
8093
|
+
} else {
|
|
8094
|
+
return false
|
|
8095
|
+
}
|
|
8096
|
+
} else {
|
|
8097
|
+
return false
|
|
8006
8098
|
}
|
|
8007
|
-
|
|
8008
|
-
return false
|
|
8009
8099
|
}
|
|
8010
8100
|
|
|
8011
8101
|
#isLexicalClipboardData(clipboardData) {
|
|
8012
8102
|
return Array.from(clipboardData.types).includes("application/x-lexical-editor")
|
|
8013
8103
|
}
|
|
8014
8104
|
|
|
8015
|
-
#
|
|
8016
|
-
if (
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
const elementChildren = Array.from(doc.body.children);
|
|
8105
|
+
#copiedImage(html) {
|
|
8106
|
+
if (html) {
|
|
8107
|
+
const doc = parseHtml(html);
|
|
8108
|
+
const elementChildren = Array.from(doc.body.children);
|
|
8020
8109
|
|
|
8021
|
-
|
|
8110
|
+
if (elementChildren.length === 1 && elementChildren[0].tagName === "IMG") {
|
|
8111
|
+
return elementChildren[0]
|
|
8112
|
+
} else {
|
|
8113
|
+
return null
|
|
8114
|
+
}
|
|
8115
|
+
} else {
|
|
8116
|
+
return null
|
|
8117
|
+
}
|
|
8022
8118
|
}
|
|
8023
8119
|
|
|
8024
|
-
#uploadFilesPreservingScroll(files) {
|
|
8120
|
+
#uploadFilesPreservingScroll(files, altText) {
|
|
8025
8121
|
this.#preservingScrollPosition(() => {
|
|
8026
8122
|
if (files.length) {
|
|
8027
|
-
this.contents.uploadFiles(files, { selectLast: true });
|
|
8123
|
+
this.contents.uploadFiles(files, { selectLast: true, altText });
|
|
8028
8124
|
}
|
|
8029
8125
|
});
|
|
8030
8126
|
}
|
|
@@ -8588,7 +8684,7 @@ class TrixContentExtension extends LexxyExtension {
|
|
|
8588
8684
|
priority: 1
|
|
8589
8685
|
}),
|
|
8590
8686
|
span: (element) => onlyStyledElements(element, {
|
|
8591
|
-
conversion: extendTextNodeConversion("
|
|
8687
|
+
conversion: extendTextNodeConversion("span", $applyHighlightStyle),
|
|
8592
8688
|
priority: 1
|
|
8593
8689
|
}),
|
|
8594
8690
|
strong: (element) => onlyStyledElements(element, {
|
|
@@ -10324,6 +10420,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10324
10420
|
#historyState = { undo: false, redo: false }
|
|
10325
10421
|
|
|
10326
10422
|
#validity = new Map()
|
|
10423
|
+
#validationAttempted = false
|
|
10327
10424
|
#validationTextArea = document.createElement("textarea")
|
|
10328
10425
|
#uploadRequests
|
|
10329
10426
|
#liveRegion
|
|
@@ -10408,10 +10505,13 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10408
10505
|
}
|
|
10409
10506
|
|
|
10410
10507
|
requiredChangedCallback() {
|
|
10508
|
+
if (this.editorContentElement) this.#synchronizeAriaAttribute("aria-required");
|
|
10411
10509
|
if (this.isConnected) this.#requestValidityRefresh();
|
|
10412
10510
|
}
|
|
10413
10511
|
|
|
10414
10512
|
formResetCallback() {
|
|
10513
|
+
this.#validationAttempted = false;
|
|
10514
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
10415
10515
|
this.value = this.#initialValue;
|
|
10416
10516
|
this.editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
|
|
10417
10517
|
}
|
|
@@ -10438,6 +10538,15 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10438
10538
|
return this.internals.validity
|
|
10439
10539
|
}
|
|
10440
10540
|
|
|
10541
|
+
get validationMessage() {
|
|
10542
|
+
return this.internals.validationMessage
|
|
10543
|
+
}
|
|
10544
|
+
|
|
10545
|
+
setCustomValidity(message) {
|
|
10546
|
+
this.#validationTextArea.setCustomValidity(message);
|
|
10547
|
+
this.#refreshValidity();
|
|
10548
|
+
}
|
|
10549
|
+
|
|
10441
10550
|
checkValidity() {
|
|
10442
10551
|
return this.internals.checkValidity()
|
|
10443
10552
|
}
|
|
@@ -10675,6 +10784,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10675
10784
|
this.#attachDebugHooks();
|
|
10676
10785
|
this.#attachToolbar();
|
|
10677
10786
|
this.#resetBeforeTurboCaches();
|
|
10787
|
+
this.#observeAriaAttributes();
|
|
10678
10788
|
|
|
10679
10789
|
this.#setInternalFormValue(this.value, { suppressEvent: true });
|
|
10680
10790
|
this.#synchronizeWithChanges();
|
|
@@ -10747,7 +10857,9 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10747
10857
|
classList: "lexxy-editor__content",
|
|
10748
10858
|
contenteditable: true,
|
|
10749
10859
|
role: "textbox",
|
|
10750
|
-
"aria-multiline":
|
|
10860
|
+
"aria-multiline": this.supportsMultiLine,
|
|
10861
|
+
"aria-required": this.required,
|
|
10862
|
+
"aria-invalid": false,
|
|
10751
10863
|
"aria-label": this.#labelText,
|
|
10752
10864
|
placeholder: this.getAttribute("placeholder")
|
|
10753
10865
|
});
|
|
@@ -10780,6 +10892,46 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10780
10892
|
return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
|
|
10781
10893
|
}
|
|
10782
10894
|
|
|
10895
|
+
#observeAriaAttributes() {
|
|
10896
|
+
const observer = new MutationObserver(mutations => {
|
|
10897
|
+
for (const { attributeName } of mutations) {
|
|
10898
|
+
if (attributeName.startsWith("aria-")) {
|
|
10899
|
+
this.#synchronizeAriaAttribute(attributeName);
|
|
10900
|
+
} else if (attributeName === "single-line") {
|
|
10901
|
+
this.#synchronizeAriaAttribute("aria-multiline");
|
|
10902
|
+
}
|
|
10903
|
+
}
|
|
10904
|
+
});
|
|
10905
|
+
observer.observe(this, { attributes: true });
|
|
10906
|
+
|
|
10907
|
+
this.#listeners.track(
|
|
10908
|
+
() => observer.disconnect(),
|
|
10909
|
+
registerEventListener(this, "invalid", () => {
|
|
10910
|
+
this.#validationAttempted = true;
|
|
10911
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
10912
|
+
})
|
|
10913
|
+
);
|
|
10914
|
+
}
|
|
10915
|
+
|
|
10916
|
+
#synchronizeAriaAttribute(name) {
|
|
10917
|
+
const value = this.getAttribute(name) ?? this.#defaultAriaValue(name);
|
|
10918
|
+
if (value === null) {
|
|
10919
|
+
this.editorContentElement.removeAttribute(name);
|
|
10920
|
+
} else if (this.editorContentElement.getAttribute(name) !== String(value)) {
|
|
10921
|
+
this.editorContentElement.setAttribute(name, value);
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10924
|
+
|
|
10925
|
+
#defaultAriaValue(name) {
|
|
10926
|
+
switch (name) {
|
|
10927
|
+
case "aria-label": return this.#labelText
|
|
10928
|
+
case "aria-required": return this.required
|
|
10929
|
+
case "aria-multiline": return this.supportsMultiLine
|
|
10930
|
+
case "aria-invalid": return this.#validationAttempted && !this.validity.valid
|
|
10931
|
+
default: return null
|
|
10932
|
+
}
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10783
10935
|
#setInternalFormValue(html, { suppressEvent = false } = {}) {
|
|
10784
10936
|
const changed = html !== this.#previousInternalFormValue;
|
|
10785
10937
|
|
|
@@ -10836,11 +10988,16 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10836
10988
|
#refreshValidity() {
|
|
10837
10989
|
this.#refreshInternalValidity();
|
|
10838
10990
|
const { validity, message } = this.#calculateValidity();
|
|
10839
|
-
this.internals.setValidity(validity, message, this.editorContentElement);
|
|
10991
|
+
this.internals.setValidity(validity, message, this.editorContentElement ?? undefined);
|
|
10992
|
+
if (this.editorContentElement) {
|
|
10993
|
+
this.#synchronizeAriaAttribute("aria-invalid");
|
|
10994
|
+
}
|
|
10840
10995
|
}
|
|
10841
10996
|
|
|
10842
10997
|
#refreshInternalValidity() {
|
|
10843
|
-
|
|
10998
|
+
if (this.editorContentElement) {
|
|
10999
|
+
this.#validationTextArea.required = this.required && this.isBlank;
|
|
11000
|
+
}
|
|
10844
11001
|
const flags = this.#validationTextArea.validity;
|
|
10845
11002
|
const message = this.#validationTextArea.validationMessage;
|
|
10846
11003
|
|
|
@@ -10855,12 +11012,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
10855
11012
|
// internal TextArea's ValidityState can contain `valid: true`
|
|
10856
11013
|
if (flags.valid === true) continue
|
|
10857
11014
|
|
|
11015
|
+
let hasError = false;
|
|
10858
11016
|
for (const flag in flags) {
|
|
10859
11017
|
if (flags[flag]) {
|
|
10860
11018
|
validity[flag] = true;
|
|
10861
|
-
|
|
11019
|
+
hasError = true;
|
|
10862
11020
|
}
|
|
10863
11021
|
}
|
|
11022
|
+
if (hasError) messages.push(message);
|
|
10864
11023
|
}
|
|
10865
11024
|
|
|
10866
11025
|
return { validity, message: messages.join("\n") }
|
|
@@ -11178,6 +11337,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
11178
11337
|
|
|
11179
11338
|
#resetValidity() {
|
|
11180
11339
|
this.#validity = new Map();
|
|
11340
|
+
this.#validationAttempted = false;
|
|
11181
11341
|
}
|
|
11182
11342
|
}
|
|
11183
11343
|
|
|
@@ -12123,6 +12283,111 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
12123
12283
|
}
|
|
12124
12284
|
}
|
|
12125
12285
|
|
|
12286
|
+
class AlternativeTextDialog {
|
|
12287
|
+
#editor
|
|
12288
|
+
#editorElement
|
|
12289
|
+
#dialog
|
|
12290
|
+
#input
|
|
12291
|
+
#nodeKey
|
|
12292
|
+
#elementToRefocus
|
|
12293
|
+
#listeners = new ListenerBin()
|
|
12294
|
+
|
|
12295
|
+
constructor(editorElement) {
|
|
12296
|
+
this.#editor = editorElement.editor;
|
|
12297
|
+
this.#editorElement = editorElement;
|
|
12298
|
+
this.#dialog = createElement("dialog", { className: "lexxy-alternative-text-dialog", ariaLabel: "Alternative text" });
|
|
12299
|
+
this.#input = createElement("textarea", { rows: 1, autofocus: true, ariaLabel: "Description", placeholder: "Keep it short, but enough to convey the context…" });
|
|
12300
|
+
|
|
12301
|
+
const title = createElement("h2", { textContent: "Describe the image for people who can't see it:" });
|
|
12302
|
+
|
|
12303
|
+
const actions = createElement("div", { className: "lexxy-alternative-text-dialog__actions" });
|
|
12304
|
+
const cancel = createElement("button", { type: "button", textContent: "Cancel" });
|
|
12305
|
+
const save = createElement("button", { type: "button", className: "lexxy-alternative-text-dialog__save", textContent: "Save" });
|
|
12306
|
+
actions.append(cancel, save);
|
|
12307
|
+
|
|
12308
|
+
const controls = createElement("div", { className: "lexxy-alternative-text-dialog__controls" });
|
|
12309
|
+
controls.append(this.#input, actions);
|
|
12310
|
+
this.#dialog.append(title, controls);
|
|
12311
|
+
editorElement.appendChild(this.#dialog);
|
|
12312
|
+
|
|
12313
|
+
this.#listeners.track(
|
|
12314
|
+
registerEventListener(cancel, "click", () => this.#close()),
|
|
12315
|
+
registerEventListener(save, "click", () => this.#save()),
|
|
12316
|
+
registerEventListener(this.#dialog, "keydown", this.#handleKeyDown),
|
|
12317
|
+
registerEventListener(document, "pointerdown", this.#closeOnClickOutside)
|
|
12318
|
+
);
|
|
12319
|
+
}
|
|
12320
|
+
|
|
12321
|
+
dispose() {
|
|
12322
|
+
this.#dialog.remove();
|
|
12323
|
+
this.#listeners.dispose();
|
|
12324
|
+
}
|
|
12325
|
+
|
|
12326
|
+
open(nodeKey) {
|
|
12327
|
+
const description = this.#editor.read(() => {
|
|
12328
|
+
const node = $getNodeByKey(nodeKey);
|
|
12329
|
+
if ($isActionTextAttachmentNode(node)) {
|
|
12330
|
+
return node.altText
|
|
12331
|
+
}
|
|
12332
|
+
});
|
|
12333
|
+
|
|
12334
|
+
if (description !== undefined) {
|
|
12335
|
+
this.#nodeKey = nodeKey;
|
|
12336
|
+
this.#input.value = description;
|
|
12337
|
+
this.#anchorBelowAttachment(nodeKey);
|
|
12338
|
+
this.#elementToRefocus = document.activeElement;
|
|
12339
|
+
this.#editorElement.querySelector("lexxy-toolbar")?.closeDropdowns();
|
|
12340
|
+
this.#dialog.show();
|
|
12341
|
+
this.#input.focus();
|
|
12342
|
+
}
|
|
12343
|
+
}
|
|
12344
|
+
|
|
12345
|
+
#anchorBelowAttachment(nodeKey) {
|
|
12346
|
+
const figureElement = this.#editor.getElementByKey(nodeKey);
|
|
12347
|
+
if (figureElement) {
|
|
12348
|
+
const rect = figureElement.getBoundingClientRect();
|
|
12349
|
+
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
12350
|
+
const bottom = rect.bottom - editorRect.top - this.#editorElement.clientTop + this.#editorElement.scrollTop;
|
|
12351
|
+
this.#dialog.style.setProperty("--lexxy-anchor-bottom", `${bottom}px`);
|
|
12352
|
+
}
|
|
12353
|
+
}
|
|
12354
|
+
|
|
12355
|
+
#save() {
|
|
12356
|
+
const description = this.#input.value.trim();
|
|
12357
|
+
this.#editor.update(() => {
|
|
12358
|
+
const node = $getNodeByKey(this.#nodeKey);
|
|
12359
|
+
if ($isActionTextAttachmentNode(node) && node.altText !== description) {
|
|
12360
|
+
node.getWritable().altText = description;
|
|
12361
|
+
}
|
|
12362
|
+
}, { tag: [ HISTORY_PUSH_TAG, SKIP_DOM_SELECTION_TAG ] });
|
|
12363
|
+
this.#close();
|
|
12364
|
+
}
|
|
12365
|
+
|
|
12366
|
+
#handleKeyDown = (event) => {
|
|
12367
|
+
event.stopPropagation();
|
|
12368
|
+
if (event.key === "Escape") {
|
|
12369
|
+
this.#close();
|
|
12370
|
+
} else {
|
|
12371
|
+
trapFocusAtTabBoundary(this.#dialog, event);
|
|
12372
|
+
}
|
|
12373
|
+
}
|
|
12374
|
+
|
|
12375
|
+
// Dismissing by clicking elsewhere must leave focus where the click landed.
|
|
12376
|
+
#closeOnClickOutside = (event) => {
|
|
12377
|
+
if (this.#dialog.open && !this.#dialog.contains(event.target)) {
|
|
12378
|
+
this.#close({ refocus: false });
|
|
12379
|
+
}
|
|
12380
|
+
}
|
|
12381
|
+
|
|
12382
|
+
#close({ refocus = true } = {}) {
|
|
12383
|
+
this.#dialog.close();
|
|
12384
|
+
if (refocus && this.#elementToRefocus?.isConnected) {
|
|
12385
|
+
this.#elementToRefocus.focus();
|
|
12386
|
+
}
|
|
12387
|
+
this.#elementToRefocus = null;
|
|
12388
|
+
}
|
|
12389
|
+
}
|
|
12390
|
+
|
|
12126
12391
|
const DELETE_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
|
|
12127
12392
|
<path d="M11.2041 1.01074C12.2128 1.113 13 1.96435 13 3V4H15L15.1025 4.00488C15.6067 4.05621 16 4.48232 16 5C16 5.55228 15.5523 6 15 6H14.8457L14.1416 15.1533C14.0614 16.1953 13.1925 17 12.1475 17H5.85254L5.6582 16.9902C4.76514 16.9041 4.03607 16.2296 3.88184 15.3457L3.8584 15.1533L3.1543 6H3C2.44772 6 2 5.55228 2 5C2 4.44772 2.44772 4 3 4H5V3C5 1.89543 5.89543 1 7 1H11L11.2041 1.01074ZM5.85254 15H12.1475L12.8398 6H5.16016L5.85254 15ZM7 4H11V3H7V4Z"/>
|
|
12128
12393
|
</svg>`;
|
|
@@ -12130,6 +12395,8 @@ const DELETE_ICON = `<svg viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"
|
|
|
12130
12395
|
class AttachmentToolbar extends HTMLElement {
|
|
12131
12396
|
#listeners = new ListenerBin()
|
|
12132
12397
|
#buttons = []
|
|
12398
|
+
#alternativeTextButton
|
|
12399
|
+
#alternativeTextDialog
|
|
12133
12400
|
#currentNodeKey = null
|
|
12134
12401
|
#reflowObserver = new ResizeObserver(() => this.#updatePosition())
|
|
12135
12402
|
#presentationObserver = new MutationObserver(() => this.#refresh())
|
|
@@ -12156,6 +12423,7 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12156
12423
|
this.#listeners.dispose();
|
|
12157
12424
|
this.#reflowObserver.disconnect();
|
|
12158
12425
|
this.#presentationObserver.disconnect();
|
|
12426
|
+
this.#alternativeTextDialog?.dispose();
|
|
12159
12427
|
}
|
|
12160
12428
|
|
|
12161
12429
|
get #editor() {
|
|
@@ -12170,7 +12438,9 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12170
12438
|
this.innerHTML = "";
|
|
12171
12439
|
|
|
12172
12440
|
const container = createElement("div", { className: "lexxy-floating-controls__group" });
|
|
12441
|
+
container.appendChild(this.#createAlternativeTextButton());
|
|
12173
12442
|
container.appendChild(this.#createRemoveButton());
|
|
12443
|
+
this.#alternativeTextDialog = new AlternativeTextDialog(this.#editorElement);
|
|
12174
12444
|
this.appendChild(container);
|
|
12175
12445
|
|
|
12176
12446
|
this.#buttons = Array.from(this.querySelectorAll("button"));
|
|
@@ -12203,6 +12473,22 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12203
12473
|
return this.#currentNodeKey !== null
|
|
12204
12474
|
}
|
|
12205
12475
|
|
|
12476
|
+
#createAlternativeTextButton() {
|
|
12477
|
+
this.#alternativeTextButton = createElement("button", {
|
|
12478
|
+
type: "button",
|
|
12479
|
+
className: "lexxy-attachment-toolbar__alternative-text",
|
|
12480
|
+
ariaLabel: "Alternative text",
|
|
12481
|
+
title: "Alternative text",
|
|
12482
|
+
ariaHasPopup: "dialog",
|
|
12483
|
+
tabIndex: -1
|
|
12484
|
+
});
|
|
12485
|
+
this.#alternativeTextButton.appendChild(createElement("span", null, "ALT"));
|
|
12486
|
+
this.#listeners.track(registerEventListener(this.#alternativeTextButton, "click", () => {
|
|
12487
|
+
this.#alternativeTextDialog.open(this.#currentNodeKey);
|
|
12488
|
+
}));
|
|
12489
|
+
return this.#alternativeTextButton
|
|
12490
|
+
}
|
|
12491
|
+
|
|
12206
12492
|
#navigateOrExit = (event) => {
|
|
12207
12493
|
if (event.key === "Escape") {
|
|
12208
12494
|
event.preventDefault();
|
|
@@ -12275,6 +12561,7 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12275
12561
|
|
|
12276
12562
|
this.#editor.getEditorState().read(() => {
|
|
12277
12563
|
const node = $getNodeByKey(nodeKey);
|
|
12564
|
+
this.#alternativeTextButton.hidden = !this.#canEditAlternativeText(node);
|
|
12278
12565
|
this.dataset.nodeType = node.getType();
|
|
12279
12566
|
this.dataset.presentation = this.#presentationOf(node, element);
|
|
12280
12567
|
|
|
@@ -12286,6 +12573,12 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12286
12573
|
});
|
|
12287
12574
|
}
|
|
12288
12575
|
|
|
12576
|
+
#canEditAlternativeText(node) {
|
|
12577
|
+
return this.#editorElement.dataset.actionTextSupportsAlt !== "false"
|
|
12578
|
+
&& node.getType() === "action_text_attachment"
|
|
12579
|
+
&& node.isPreviewableAttachment
|
|
12580
|
+
}
|
|
12581
|
+
|
|
12289
12582
|
#presentationOf(node, element) {
|
|
12290
12583
|
if ($isImageGalleryNode(node.getParent())) {
|
|
12291
12584
|
return "gallery"
|
|
@@ -12307,8 +12600,11 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12307
12600
|
if (figureElement) {
|
|
12308
12601
|
const rect = figureElement.getBoundingClientRect();
|
|
12309
12602
|
const editorRect = this.#editorElement.getBoundingClientRect();
|
|
12310
|
-
|
|
12311
|
-
|
|
12603
|
+
// We are positioned against the editor's padding box, but the rect measures its border box.
|
|
12604
|
+
const borderTop = this.#editorElement.clientTop;
|
|
12605
|
+
const borderLeft = this.#editorElement.clientLeft;
|
|
12606
|
+
this.style.setProperty("--lexxy-anchor-top", `${rect.top - editorRect.top - borderTop}px`);
|
|
12607
|
+
this.style.setProperty("--lexxy-anchor-left", `${rect.left - editorRect.left - borderLeft}px`);
|
|
12312
12608
|
this.style.setProperty("--lexxy-anchor-width", `${rect.width}px`);
|
|
12313
12609
|
this.style.setProperty("--lexxy-anchor-height", `${rect.height}px`);
|
|
12314
12610
|
this.#flipWhenOverflowing(editorRect);
|
|
@@ -12348,7 +12644,7 @@ class AttachmentToolbar extends HTMLElement {
|
|
|
12348
12644
|
#focusToolbarOnAltF10 = (event) => {
|
|
12349
12645
|
if (this.#hasSelectedNode && event.altKey && event.key === "F10") {
|
|
12350
12646
|
event.preventDefault();
|
|
12351
|
-
this.#buttons
|
|
12647
|
+
this.#buttons.find(isActiveAndVisible)?.focus();
|
|
12352
12648
|
return true
|
|
12353
12649
|
} else {
|
|
12354
12650
|
return false
|
|
@@ -855,7 +855,7 @@
|
|
|
855
855
|
|
|
856
856
|
.lexxy-table-control__more-menu-trigger {
|
|
857
857
|
aspect-ratio: auto;
|
|
858
|
-
box-shadow: 0 0 0 1px inset
|
|
858
|
+
box-shadow: 0 0 0 1px inset var(--lexxy-color-floating-controls-separator);
|
|
859
859
|
font-weight: normal;
|
|
860
860
|
padding: 0 1.25ch;
|
|
861
861
|
}
|
|
@@ -981,7 +981,7 @@
|
|
|
981
981
|
/* The anchor is measured in px from the editor's top-left, so these stay physical. */
|
|
982
982
|
left: calc(var(--lexxy-anchor-left) + var(--lexxy-anchor-width));
|
|
983
983
|
top: var(--lexxy-anchor-top);
|
|
984
|
-
transform: translate(-100
|
|
984
|
+
transform: translate(calc(-100% - 1.5ch), 1.5ch);
|
|
985
985
|
|
|
986
986
|
&[hidden] {
|
|
987
987
|
display: none;
|
|
@@ -1006,6 +1006,39 @@
|
|
|
1006
1006
|
}
|
|
1007
1007
|
}
|
|
1008
1008
|
|
|
1009
|
+
.lexxy-floating-controls__group {
|
|
1010
|
+
display: flex;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
button[hidden] {
|
|
1014
|
+
display: none;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.lexxy-attachment-toolbar__alternative-text {
|
|
1018
|
+
aspect-ratio: auto;
|
|
1019
|
+
margin-inline-end: calc(var(--lexxy-toolbar-spacing) + 1px);
|
|
1020
|
+
padding-inline: 1ch;
|
|
1021
|
+
position: relative;
|
|
1022
|
+
|
|
1023
|
+
span {
|
|
1024
|
+
display: inline-block;
|
|
1025
|
+
font-size: 0.9em;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
&:after {
|
|
1029
|
+
background-color: var(--lexxy-color-floating-controls-separator);
|
|
1030
|
+
block-size: 1.125em;
|
|
1031
|
+
content: "";
|
|
1032
|
+
display: block;
|
|
1033
|
+
inline-size: 1px;
|
|
1034
|
+
inset-block: 0;
|
|
1035
|
+
inset-inline-end: calc(-1 * var(--lexxy-toolbar-spacing));
|
|
1036
|
+
margin: auto;
|
|
1037
|
+
pointer-events: none;
|
|
1038
|
+
position: absolute;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1009
1042
|
.lexxy-attachment-toolbar__remove:hover,
|
|
1010
1043
|
.lexxy-attachment-toolbar__remove:focus-visible {
|
|
1011
1044
|
background-color: var(--lexxy-color-red);
|
|
@@ -1181,3 +1214,105 @@ action-text-attachment[content-type^="application/vnd.actiontext"] {
|
|
|
1181
1214
|
display: none;
|
|
1182
1215
|
}
|
|
1183
1216
|
}
|
|
1217
|
+
|
|
1218
|
+
.lexxy-alternative-text-dialog {
|
|
1219
|
+
background: var(--lexxy-color-canvas);
|
|
1220
|
+
border: 2px solid var(--lexxy-color-selected-hover);
|
|
1221
|
+
border-radius: calc(var(--lexxy-radius) + var(--lexxy-toolbar-gap));
|
|
1222
|
+
box-shadow: var(--lexxy-shadow);
|
|
1223
|
+
box-sizing: border-box;
|
|
1224
|
+
color: var(--lexxy-color-ink);
|
|
1225
|
+
font: inherit;
|
|
1226
|
+
inline-size: 60%;
|
|
1227
|
+
inset-block: calc(var(--lexxy-anchor-bottom) + var(--lexxy-toolbar-spacing)) auto;
|
|
1228
|
+
inset-inline: 0;
|
|
1229
|
+
margin-block: 0;
|
|
1230
|
+
margin-inline: auto;
|
|
1231
|
+
min-inline-size: min(20rem, 100%);
|
|
1232
|
+
overflow: auto;
|
|
1233
|
+
padding: var(--lexxy-toolbar-spacing);
|
|
1234
|
+
z-index: 3;
|
|
1235
|
+
|
|
1236
|
+
h2 {
|
|
1237
|
+
font-size: inherit;
|
|
1238
|
+
font-weight: bold;
|
|
1239
|
+
line-height: 1.5;
|
|
1240
|
+
margin: 0;
|
|
1241
|
+
padding-block-start: calc(0.25 * var(--lexxy-text-small));
|
|
1242
|
+
padding-inline: calc(0.5 * var(--lexxy-text-small));
|
|
1243
|
+
text-align: center;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.lexxy-alternative-text-dialog__controls {
|
|
1247
|
+
align-items: stretch;
|
|
1248
|
+
display: flex;
|
|
1249
|
+
flex-direction: column;
|
|
1250
|
+
gap: 0.5em;
|
|
1251
|
+
margin-block-start: 0.5em;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
textarea {
|
|
1255
|
+
background: var(--lexxy-color-canvas);
|
|
1256
|
+
border: 1px solid var(--lexxy-color-ink-lighter);
|
|
1257
|
+
border-radius: var(--lexxy-radius);
|
|
1258
|
+
box-sizing: border-box;
|
|
1259
|
+
color: inherit;
|
|
1260
|
+
display: block;
|
|
1261
|
+
font: inherit;
|
|
1262
|
+
font-size: var(--lexxy-text-small);
|
|
1263
|
+
font-weight: normal;
|
|
1264
|
+
min-block-size: calc(2lh + 1em + 2px);
|
|
1265
|
+
min-inline-size: 0;
|
|
1266
|
+
padding: 0.5em;
|
|
1267
|
+
|
|
1268
|
+
@supports (field-sizing: content) {
|
|
1269
|
+
field-sizing: content;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
@supports (resize: none) {
|
|
1273
|
+
resize: none;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
button {
|
|
1278
|
+
background-color: var(--lexxy-color-ink-lightest);
|
|
1279
|
+
block-size: var(--lexxy-toolbar-button-size);
|
|
1280
|
+
border: 0;
|
|
1281
|
+
border-radius: var(--lexxy-radius);
|
|
1282
|
+
color: currentColor;
|
|
1283
|
+
cursor: pointer;
|
|
1284
|
+
display: grid;
|
|
1285
|
+
font-size: var(--lexxy-text-small);
|
|
1286
|
+
inline-size: 100%;
|
|
1287
|
+
line-height: inherit;
|
|
1288
|
+
padding-inline: 2ch;
|
|
1289
|
+
place-items: center;
|
|
1290
|
+
|
|
1291
|
+
&:hover {
|
|
1292
|
+
opacity: 0.8;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
.lexxy-alternative-text-dialog__save {
|
|
1297
|
+
background-color: var(--lexxy-color-accent-dark);
|
|
1298
|
+
color: var(--lexxy-color-ink-inverted);
|
|
1299
|
+
|
|
1300
|
+
&:hover {
|
|
1301
|
+
background-color: var(--lexxy-color-accent-medium);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
textarea:focus-visible {
|
|
1306
|
+
outline: var(--lexxy-focus-ring-size) solid var(--lexxy-focus-ring-color);
|
|
1307
|
+
outline-offset: var(--lexxy-focus-ring-offset);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
.lexxy-alternative-text-dialog__actions {
|
|
1311
|
+
display: flex;
|
|
1312
|
+
gap: 0.5em;
|
|
1313
|
+
|
|
1314
|
+
button {
|
|
1315
|
+
flex: 1;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
--lexxy-color-selected-dark: var(--lexxy-color-blue);
|
|
36
36
|
--lexxy-color-selected-50: color-mix(in oklch, var(--lexxy-color-selected-dark) 50%, transparent);
|
|
37
37
|
--lexxy-color-code-bg: var(--lexxy-color-ink-lightest);
|
|
38
|
+
--lexxy-color-floating-controls-separator: rgba(128, 128, 128, 0.3);
|
|
38
39
|
|
|
39
40
|
/* Text color highlights */
|
|
40
41
|
--highlight-1: rgb(136, 118, 38);
|