lexxy 0.9.28 → 0.9.29
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/app/assets/javascript/lexxy.js +150 -32
- data/app/assets/javascript/lexxy.js.br +0 -0
- data/app/assets/javascript/lexxy.js.gz +0 -0
- data/app/assets/javascript/lexxy.js.map +1 -1
- data/app/assets/javascript/lexxy.min.js +1 -1
- data/app/assets/javascript/lexxy.min.js.br +0 -0
- data/app/assets/javascript/lexxy.min.js.gz +0 -0
- data/lib/lexxy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3c6392cdd1a3fd5cc9535778cb84e544b5864e6d75378af4e19b6d7c6c0a5628
|
|
4
|
+
data.tar.gz: 88bccc783d7e3de2f7650e2d80ad7899369053182de3fbe4b51c50f7566421fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0aa7bf890d287539720cb79cd58cee9e32833c26ed4bd0d6418ab7f94c0bed4fee739a14299093a3333e8337d23a0d4cd00fe1524defefa8f14842ceb0b278a9
|
|
7
|
+
data.tar.gz: c0dfc34fee283b34b76281b77b9dd24a57fcd7ef8d488e70628107fb1aa7a537c8811986e1a5f77d5df2573e6d040214a62f037c5f3845de136f44b813f2e71c
|
|
@@ -8029,7 +8029,7 @@ class CustomActionTextAttachmentNode extends Ji {
|
|
|
8029
8029
|
return Lexxy.global.get("attachmentTagName")
|
|
8030
8030
|
}
|
|
8031
8031
|
|
|
8032
|
-
constructor({ tagName, sgid, contentType, innerHtml, plainText }, key) {
|
|
8032
|
+
constructor({ tagName, sgid, contentType, innerHtml, plainText } = {}, key) {
|
|
8033
8033
|
super(key);
|
|
8034
8034
|
|
|
8035
8035
|
const contentTypeNamespace = Lexxy.global.get("attachmentContentTypeNamespace");
|
|
@@ -8840,7 +8840,7 @@ class ActionTextAttachmentNode extends Ji {
|
|
|
8840
8840
|
return Lexxy.global.get("attachmentTagName")
|
|
8841
8841
|
}
|
|
8842
8842
|
|
|
8843
|
-
constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError }, key) {
|
|
8843
|
+
constructor({ tagName, sgid, src, previewSrc, previewable, previewStatusUrl, pendingPreview, altText, caption, contentType, fileName, fileSize, width, height, uploadError } = {}, key) {
|
|
8844
8844
|
super(key);
|
|
8845
8845
|
|
|
8846
8846
|
this.tagName = tagName || ActionTextAttachmentNode.TAG_NAME;
|
|
@@ -12034,7 +12034,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
12034
12034
|
return null
|
|
12035
12035
|
}
|
|
12036
12036
|
|
|
12037
|
-
constructor(node, key) {
|
|
12037
|
+
constructor(node = {}, key) {
|
|
12038
12038
|
const { file, uploadUrl, blobUrlTemplate, progress, width, height, uploadError, fileName, contentType } = node;
|
|
12039
12039
|
super({ ...node, contentType: file?.type ?? contentType }, key);
|
|
12040
12040
|
this.file = file ?? null;
|
|
@@ -12055,9 +12055,10 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
12055
12055
|
// node is reloaded from saved state such as from history.
|
|
12056
12056
|
this.#startUploadIfNeeded();
|
|
12057
12057
|
|
|
12058
|
-
// Bridge-managed uploads (uploadUrl is null)
|
|
12059
|
-
//
|
|
12060
|
-
|
|
12058
|
+
// Bridge-managed uploads (uploadUrl is null) and restored or remote
|
|
12059
|
+
// instances (no local File) don't have file data to show an image
|
|
12060
|
+
// preview, so always show the file icon during upload.
|
|
12061
|
+
const canPreviewFile = this.isPreviewableAttachment && this.uploadUrl != null && this.file != null;
|
|
12061
12062
|
const figure = this.createAttachmentFigure(canPreviewFile);
|
|
12062
12063
|
|
|
12063
12064
|
if (canPreviewFile) {
|
|
@@ -12128,9 +12129,12 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
12128
12129
|
const figcaption = createElement("figcaption", { className: "attachment__caption" });
|
|
12129
12130
|
|
|
12130
12131
|
const nameSpan = createElement("span", { className: "attachment__name", textContent: this.caption || this.fileName || "" });
|
|
12131
|
-
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file?.size) });
|
|
12132
12132
|
figcaption.appendChild(nameSpan);
|
|
12133
|
-
|
|
12133
|
+
|
|
12134
|
+
if (this.file) {
|
|
12135
|
+
const sizeSpan = createElement("span", { className: "attachment__size", textContent: bytesToHumanSize(this.file.size) });
|
|
12136
|
+
figcaption.appendChild(sizeSpan);
|
|
12137
|
+
}
|
|
12134
12138
|
|
|
12135
12139
|
return figcaption
|
|
12136
12140
|
}
|
|
@@ -12152,6 +12156,7 @@ class ActionTextAttachmentUploadNode extends ActionTextAttachmentNode {
|
|
|
12152
12156
|
async #startUploadIfNeeded() {
|
|
12153
12157
|
if (this.#uploadStarted) return
|
|
12154
12158
|
if (!this.uploadUrl) return // Bridge-managed upload — skip DirectUpload
|
|
12159
|
+
if (!this.file) return // Restored or remote instance — no local File to upload
|
|
12155
12160
|
|
|
12156
12161
|
this.#setUploadStarted();
|
|
12157
12162
|
|
|
@@ -12643,12 +12648,24 @@ class OfficeFormatter {
|
|
|
12643
12648
|
}
|
|
12644
12649
|
}
|
|
12645
12650
|
|
|
12651
|
+
// Properties worth rescuing from a foreign style sheet before #stripStyleElements
|
|
12652
|
+
// drops it, keyed by the elements that carry them. Word and Outlook declare
|
|
12653
|
+
// paragraph spacing in the sheet (p.MsoNormal { margin: 0cm }) and never inline,
|
|
12654
|
+
// so once the sheet goes there is nothing left to say how tightly the author's
|
|
12655
|
+
// paragraphs sat, and hosts that turn pasted paragraph margins into explicit
|
|
12656
|
+
// blank lines read tight Office paragraphs as spaced ones and multiply every
|
|
12657
|
+
// line break.
|
|
12658
|
+
const PRESERVABLE_STYLES = {
|
|
12659
|
+
p: [ "margin-top", "margin-bottom" ]
|
|
12660
|
+
};
|
|
12661
|
+
|
|
12646
12662
|
class PastedContentFormatter {
|
|
12647
12663
|
constructor(doc) {
|
|
12648
12664
|
this.doc = doc;
|
|
12649
12665
|
}
|
|
12650
12666
|
|
|
12651
12667
|
format() {
|
|
12668
|
+
this.#inlinePreservableStyles();
|
|
12652
12669
|
this.#stripStyleElements();
|
|
12653
12670
|
this.#unwrapPlaceholderAnchors();
|
|
12654
12671
|
this.#stripTableCellColorStyles();
|
|
@@ -12656,9 +12673,23 @@ class PastedContentFormatter {
|
|
|
12656
12673
|
this.#unwrapWrappedListChildren();
|
|
12657
12674
|
this.#nestStrayListChildren();
|
|
12658
12675
|
this.#stripStrayListChildren();
|
|
12676
|
+
this.#replaceGmailEmojiImgTags();
|
|
12659
12677
|
return this.doc
|
|
12660
12678
|
}
|
|
12661
12679
|
|
|
12680
|
+
// Runs before #stripStyleElements, to inline anything we still need
|
|
12681
|
+
// from an external style before we remove it
|
|
12682
|
+
#inlinePreservableStyles() {
|
|
12683
|
+
const rules = this.#styleRules();
|
|
12684
|
+
if (rules.length === 0) return
|
|
12685
|
+
|
|
12686
|
+
for (const [ selector, properties ] of Object.entries(PRESERVABLE_STYLES)) {
|
|
12687
|
+
for (const element of this.doc.querySelectorAll(selector)) {
|
|
12688
|
+
this.#inlineDeclaredStyles(element, properties, rules);
|
|
12689
|
+
}
|
|
12690
|
+
}
|
|
12691
|
+
}
|
|
12692
|
+
|
|
12662
12693
|
// Spreadsheets (e.g. Excel) copy a <style> block whose rules (td { color:
|
|
12663
12694
|
// black }, .xlNN { ... }) cascade onto the imported text. That color rides
|
|
12664
12695
|
// in through the cascade rather than an inline style, so it slips past both
|
|
@@ -12753,6 +12784,72 @@ class PastedContentFormatter {
|
|
|
12753
12784
|
}
|
|
12754
12785
|
}
|
|
12755
12786
|
|
|
12787
|
+
// Read the rules from a sheet we build rather than from this.doc.styleSheets: the
|
|
12788
|
+
// document came from DOMParser, so it has no browsing context, and WebKit leaves
|
|
12789
|
+
// its styleSheets empty. Parsing the text ourselves still expands the shorthands
|
|
12790
|
+
// Word writes (margin: 0cm) into the longhands we ask for.
|
|
12791
|
+
#styleRules() {
|
|
12792
|
+
const css = Array.from(this.doc.querySelectorAll("style"), (style) => style.textContent).join("\n");
|
|
12793
|
+
if (css.trim() === "") return []
|
|
12794
|
+
|
|
12795
|
+
const sheet = new CSSStyleSheet();
|
|
12796
|
+
sheet.replaceSync(css);
|
|
12797
|
+
|
|
12798
|
+
const rules = [];
|
|
12799
|
+
for (const rule of sheet.cssRules) {
|
|
12800
|
+
if (rule instanceof CSSStyleRule) {
|
|
12801
|
+
rules.push(rule);
|
|
12802
|
+
}
|
|
12803
|
+
}
|
|
12804
|
+
return rules
|
|
12805
|
+
}
|
|
12806
|
+
|
|
12807
|
+
// A declaration the element already carries inline is the author's own and wins.
|
|
12808
|
+
#inlineDeclaredStyles(element, properties, rules) {
|
|
12809
|
+
for (const property of properties) {
|
|
12810
|
+
if (element.style.getPropertyValue(property) === "") {
|
|
12811
|
+
const value = this.#declaredValue(rules, element, property);
|
|
12812
|
+
if (value) {
|
|
12813
|
+
this.#appendStyleProperty(element, property, value);
|
|
12814
|
+
}
|
|
12815
|
+
}
|
|
12816
|
+
}
|
|
12817
|
+
}
|
|
12818
|
+
|
|
12819
|
+
// Source order alone doesn't decide the cascade, so when the matching rules
|
|
12820
|
+
// disagree we can't say which value wins without resolving specificity. Skip
|
|
12821
|
+
// the property rather than guess: guessing is permanent, since the sheet is
|
|
12822
|
+
// about to go, and skipping only leaves the host on its own default.
|
|
12823
|
+
#declaredValue(rules, element, property) {
|
|
12824
|
+
const declared = new Set();
|
|
12825
|
+
for (const rule of rules) {
|
|
12826
|
+
const value = rule.style.getPropertyValue(property);
|
|
12827
|
+
if (value !== "" && element.matches(rule.selectorText)) {
|
|
12828
|
+
declared.add(value);
|
|
12829
|
+
}
|
|
12830
|
+
}
|
|
12831
|
+
|
|
12832
|
+
if (declared.size === 1) {
|
|
12833
|
+
return declared.values().next().value
|
|
12834
|
+
} else {
|
|
12835
|
+
return null
|
|
12836
|
+
}
|
|
12837
|
+
}
|
|
12838
|
+
|
|
12839
|
+
// Assigning through element.style would reserialize the style attribute and
|
|
12840
|
+
// drop the mso-* declarations Word leaves inline (they aren't valid CSS),
|
|
12841
|
+
// and OfficeFormatter still needs to read mso-list from them. Append to the
|
|
12842
|
+
// attribute instead so the original declarations survive untouched.
|
|
12843
|
+
#appendStyleProperty(element, property, value) {
|
|
12844
|
+
const style = element.getAttribute("style");
|
|
12845
|
+
const declaration = `${property}:${value}`;
|
|
12846
|
+
if (style === null || style.trim() === "") {
|
|
12847
|
+
element.setAttribute("style", declaration);
|
|
12848
|
+
} else {
|
|
12849
|
+
element.setAttribute("style", `${style.replace(/;\s*$/, "")};${declaration}`);
|
|
12850
|
+
}
|
|
12851
|
+
}
|
|
12852
|
+
|
|
12756
12853
|
#firstStrayListChild(list) {
|
|
12757
12854
|
for (const child of list.childNodes) {
|
|
12758
12855
|
if (child.nodeType !== Node.ELEMENT_NODE || child.tagName !== "LI") {
|
|
@@ -12769,6 +12866,16 @@ class PastedContentFormatter {
|
|
|
12769
12866
|
#containsListItems(node) {
|
|
12770
12867
|
return node.nodeType === Node.ELEMENT_NODE && node.querySelector("li") !== null
|
|
12771
12868
|
}
|
|
12869
|
+
|
|
12870
|
+
// Emoji in gmail: <img data-emoji="😂" class="an1" alt="😂" aria-label="😂" draggable="false" src="https://fonts.gstatic.com/s/e/notoemoji/17.0/1f602/32.png" loading="lazy" style="height: 1.2em; width: 1.2em; vertical-align: middle;">
|
|
12871
|
+
#replaceGmailEmojiImgTags() {
|
|
12872
|
+
for (const emojiImg of this.doc.querySelectorAll("img[data-emoji]")) {
|
|
12873
|
+
const emoji = emojiImg.dataset.emoji;
|
|
12874
|
+
if (emoji === emojiImg.alt) {
|
|
12875
|
+
emojiImg.replaceWith(emoji);
|
|
12876
|
+
}
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12772
12879
|
}
|
|
12773
12880
|
|
|
12774
12881
|
class Contents {
|
|
@@ -12811,20 +12918,6 @@ class Contents {
|
|
|
12811
12918
|
}, { tag });
|
|
12812
12919
|
}
|
|
12813
12920
|
|
|
12814
|
-
// A literal "\n" stays visible in the editor, which Lexical renders with white-space: pre-wrap,
|
|
12815
|
-
// but collapses to a space once serialized to HTML, so it has to become line break nodes.
|
|
12816
|
-
insertTextWithLineBreaks(text) {
|
|
12817
|
-
const normalizedText = text?.replace(/\r\n?/g, "\n");
|
|
12818
|
-
const selection = Qr();
|
|
12819
|
-
|
|
12820
|
-
if (normalizedText?.includes("\n") && Fr(selection)) {
|
|
12821
|
-
selection.insertRawText(normalizedText);
|
|
12822
|
-
return true
|
|
12823
|
-
} else {
|
|
12824
|
-
return false
|
|
12825
|
-
}
|
|
12826
|
-
}
|
|
12827
|
-
|
|
12828
12921
|
insertAtCursor(...nodes) {
|
|
12829
12922
|
const selection = this.#insertableSelection();
|
|
12830
12923
|
const inserter = BaseNodeInserter.for(selection);
|
|
@@ -15542,6 +15635,39 @@ class CustomAttachmentDragAndDropExtension extends LexxyExtension {
|
|
|
15542
15635
|
}
|
|
15543
15636
|
}
|
|
15544
15637
|
|
|
15638
|
+
const LINE_SEPARATORS = /\r\n|[\n\r\u2028\u2029]/;
|
|
15639
|
+
|
|
15640
|
+
// A literal line separator inside a text node renders as a break in the editor, or not at all,
|
|
15641
|
+
// but never survives serialization to HTML. macOS text replacements deliver U+2028 LINE SEPARATOR,
|
|
15642
|
+
// and other insertions can carry "\n", so however the text arrives, a transform converts whatever
|
|
15643
|
+
// lands in a text node into line break nodes.
|
|
15644
|
+
class LineSeparatorsExtension extends LexxyExtension {
|
|
15645
|
+
get lexicalExtension() {
|
|
15646
|
+
return this.defineExtension({
|
|
15647
|
+
name: "lexxy/line-separators",
|
|
15648
|
+
register(editor) {
|
|
15649
|
+
return editor.registerNodeTransform(gr, $replaceLineSeparatorWithLineBreak)
|
|
15650
|
+
}
|
|
15651
|
+
})
|
|
15652
|
+
}
|
|
15653
|
+
}
|
|
15654
|
+
|
|
15655
|
+
// Replaces the first separator only: the split leaves dirty text nodes behind, so Lexical re-runs
|
|
15656
|
+
// the transform until none remain.
|
|
15657
|
+
function $replaceLineSeparatorWithLineBreak(textNode) {
|
|
15658
|
+
const match = textNode.getTextContent().match(LINE_SEPARATORS);
|
|
15659
|
+
|
|
15660
|
+
if (match) {
|
|
15661
|
+
let separatorNode;
|
|
15662
|
+
if (match.index === 0) {
|
|
15663
|
+
[ separatorNode ] = textNode.splitText(match[0].length);
|
|
15664
|
+
} else {
|
|
15665
|
+
[ , separatorNode ] = textNode.splitText(match.index, match.index + match[0].length);
|
|
15666
|
+
}
|
|
15667
|
+
separatorNode.replace(or());
|
|
15668
|
+
}
|
|
15669
|
+
}
|
|
15670
|
+
|
|
15545
15671
|
class LexicalEditorElement extends HTMLElement {
|
|
15546
15672
|
static formAssociated = true
|
|
15547
15673
|
static debug = false
|
|
@@ -15700,7 +15826,8 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
15700
15826
|
FormatEscapeExtension,
|
|
15701
15827
|
LinkOpenerExtension,
|
|
15702
15828
|
PreventLexicalTripleClickExtension,
|
|
15703
|
-
CustomAttachmentDragAndDropExtension
|
|
15829
|
+
CustomAttachmentDragAndDropExtension,
|
|
15830
|
+
LineSeparatorsExtension
|
|
15704
15831
|
]
|
|
15705
15832
|
}
|
|
15706
15833
|
|
|
@@ -15888,7 +16015,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
15888
16015
|
#initialize() {
|
|
15889
16016
|
this.#registerComponents();
|
|
15890
16017
|
this.#handleEnter();
|
|
15891
|
-
this.#handleReplacementText();
|
|
15892
16018
|
this.#registerFocusEvents();
|
|
15893
16019
|
this.#registerHistoryEvents();
|
|
15894
16020
|
this.#registerFileAcceptFilter();
|
|
@@ -16152,14 +16278,6 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
16152
16278
|
));
|
|
16153
16279
|
}
|
|
16154
16280
|
|
|
16155
|
-
#handleReplacementText() {
|
|
16156
|
-
this.#listeners.track(this.editor.registerCommand(
|
|
16157
|
-
ge$3,
|
|
16158
|
-
(event) => event instanceof InputEvent && this.contents.insertTextWithLineBreaks(event.data),
|
|
16159
|
-
oo
|
|
16160
|
-
));
|
|
16161
|
-
}
|
|
16162
|
-
|
|
16163
16281
|
#registerFocusEvents() {
|
|
16164
16282
|
this.#listeners.track(
|
|
16165
16283
|
registerEventListener(this, "focusin", this.#handleFocusIn),
|
|
Binary file
|
|
Binary file
|