@bikdotai/bik-component-library 0.0.811-beta.2 → 0.0.811-beta.3
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/dist/cjs/editor/BikEditor.styles.js +0 -12
- package/dist/cjs/editor/BikEditor.styles.js.map +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
- package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
- package/dist/esm/editor/BikEditor.styles.js +2 -14
- package/dist/esm/editor/BikEditor.styles.js.map +1 -1
- package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
- package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
- package/package.json +1 -1
|
@@ -26,18 +26,6 @@
|
|
|
26
26
|
padding: 0;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/* ProseMirror appends an invisible trailing <br> after a paragraph that
|
|
30
|
-
ends in a hard break, so the cursor can sit on that final line. A plain
|
|
31
|
-
HTML viewer (Gmail, our email shadow DOM) doesn't add one, so the editor
|
|
32
|
-
would show one phantom empty line MORE than the sent HTML per such
|
|
33
|
-
paragraph. Hide it when the paragraph has other content (the break is
|
|
34
|
-
NOT the first child — e.g. a paragraph ending in <br>, even when the
|
|
35
|
-
<br> is nested inside a mark <span>); keep it for truly empty paragraphs
|
|
36
|
-
where it is the only child and provides the blank line. Editor == output. */
|
|
37
|
-
.ProseMirror-trailingBreak:not(:first-child) {
|
|
38
|
-
display: none;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
ul,
|
|
42
30
|
ol {
|
|
43
31
|
margin: 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BikEditor.styles.js","sources":["../../../src/editor/BikEditor.styles.ts"],"sourcesContent":["import styled from 'styled-components';\nimport { COLORS } from '../constants/Theme';\n\nexport const BikEditorShell = styled.div<{\n\tminHeight?: string;\n\tmaxHeight?: string;\n\ttightParagraphs?: boolean;\n}>`\n\tposition: relative;\n\twidth: 100%;\n\n\t.ProseMirror {\n\t\tmin-height: ${({ minHeight }) => minHeight ?? '80px'};\n\t\tmax-height: ${({ maxHeight }) => maxHeight ?? 'none'};\n\t\toverflow-y: auto;\n\t\toutline: none;\n\t\tpadding: 8px 12px;\n\t\tfont-size: 14px;\n\t\tline-height: 1.5;\n\t\tword-break: break-word;\n\n\t\t/* Paragraph spacing.\n\t\t Email/signature (rich): paragraphs carry the browser-default 1em block\n\t\t margin — the SAME spacing the sent HTML gets (we emit plain <p>, which\n\t\t renders against the default <p> margin in Gmail and our email viewer),\n\t\t so editor view == emitted HTML == Gmail. A single Enter = a new\n\t\t paragraph with a visible gap, like pasted Gmail/ChatGPT/Docs content.\n\t\t Other channels (WhatsApp/IG/FB/comments) are plain-text messaging, with\n\t\t no paragraph-gap concept — paragraphs are tight (margin:0) regardless of\n\t\t typed vs pasted. (A margin:0 attr still overrides to tight either way.) */\n\t\tp {\n\t\t\tmargin: ${({ tightParagraphs }) => (tightParagraphs ? '0' : '1em 0')};\n\t\t\tpadding: 0;\n\t\t}\n\n\t\
|
|
1
|
+
{"version":3,"file":"BikEditor.styles.js","sources":["../../../src/editor/BikEditor.styles.ts"],"sourcesContent":["import styled from 'styled-components';\nimport { COLORS } from '../constants/Theme';\n\nexport const BikEditorShell = styled.div<{\n\tminHeight?: string;\n\tmaxHeight?: string;\n\ttightParagraphs?: boolean;\n}>`\n\tposition: relative;\n\twidth: 100%;\n\n\t.ProseMirror {\n\t\tmin-height: ${({ minHeight }) => minHeight ?? '80px'};\n\t\tmax-height: ${({ maxHeight }) => maxHeight ?? 'none'};\n\t\toverflow-y: auto;\n\t\toutline: none;\n\t\tpadding: 8px 12px;\n\t\tfont-size: 14px;\n\t\tline-height: 1.5;\n\t\tword-break: break-word;\n\n\t\t/* Paragraph spacing.\n\t\t Email/signature (rich): paragraphs carry the browser-default 1em block\n\t\t margin — the SAME spacing the sent HTML gets (we emit plain <p>, which\n\t\t renders against the default <p> margin in Gmail and our email viewer),\n\t\t so editor view == emitted HTML == Gmail. A single Enter = a new\n\t\t paragraph with a visible gap, like pasted Gmail/ChatGPT/Docs content.\n\t\t Other channels (WhatsApp/IG/FB/comments) are plain-text messaging, with\n\t\t no paragraph-gap concept — paragraphs are tight (margin:0) regardless of\n\t\t typed vs pasted. (A margin:0 attr still overrides to tight either way.) */\n\t\tp {\n\t\t\tmargin: ${({ tightParagraphs }) => (tightParagraphs ? '0' : '1em 0')};\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tul,\n\t\tol {\n\t\t\tmargin: 0;\n\t\t\tpadding-left: 1.5em;\n\t\t}\n\n\t\tli + li {\n\t\t\tmargin-top: 2px;\n\t\t}\n\n\t\tp.is-editor-empty:first-child::before {\n\t\t\tcontent: attr(data-placeholder);\n\t\t\tfloat: left;\n\t\t\tcolor: #adb5bd;\n\t\t\tpointer-events: none;\n\t\t\theight: 0;\n\t\t}\n\t}\n\n\t.bik-mention {\n\t\tcolor: ${COLORS.content.brand};\n\t\tpadding: 1px 4px;\n\t}\n\t.bik-mention--team {\n\t\tcolor: ${COLORS.content.brand};\n\t}\n\t.bik-variable {\n\t}\n\n\ta,\n\t.bik-link {\n\t\tcolor: #4f46e5;\n\t\ttext-decoration: underline;\n\t\ttext-decoration-color: #a5b4fc;\n\t\ttext-underline-offset: 2px;\n\t\tcursor: pointer;\n\t\t&:hover {\n\t\t\tcolor: #3730a3;\n\t\t\ttext-decoration-color: #6366f1;\n\t\t}\n\t}\n`;\n"],"names":["BikEditorShell","div","_ref","minHeight","_ref2","maxHeight","_ref3","tightParagraphs","COLORS","content","brand"],"mappings":"kNAGaA,MAAAA,OAAuB,QAACC,GAInC;;;;;gBAKcC,IAAA,IAACC,UAAEA,GAAWD,EAAA,OAAKC,QAAAA,EAAa,MAAM;gBACtCC,IAAA,IAACC,UAAEA,GAAWD,EAAA,OAAKC,QAAAA,EAAa,MAAM;;;;;;;;;;;;;;;;;;aAkBzCC,IAAA,IAACC,gBAAEA,GAAiBD,EAAA,OAAMC,EAAkB,IAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;WAwB3DC,EAAMA,OAACC,QAAQC;;;;WAIfF,EAAMA,OAACC,QAAQC;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),t=require("@tiptap/pm/model"),r=require("@tiptap/pm/state"),n=require("./pasteUtils.js");const a=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1,plainOnly:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks,a=this.options.plainOnly;let s;const i=e=>{var t;const r=e.firstChild;s=r&&"paragraph"===r.type.name?null!==(t=r.attrs.margin)&&void 0!==t?t:null:void 0};return[new r.Plugin({appendTransaction(e,t,r){var n;if(!e.some((e=>e.docChanged)))return s=void 0,null;const a=r.tr;let i=!1;if(void 0!==s){const o=s;let l=t.selection.from;for(const t of e)l=t.mapping.map(l);const p=r.doc.resolve(Math.max(0,Math.min(l,r.doc.content.size)));for(let e=p.depth;e>=1;e--){const t=p.node(e);if("paragraph"===t.type.name){(null!==(n=t.attrs.margin)&&void 0!==n?n:null)!==o&&(a.setNodeMarkup(p.before(e),void 0,Object.assign(Object.assign({},t.attrs),{margin:o})),i=!0);break}}}s=void 0;const o=a.doc;let l=null;return o.forEach(((e,t)=>{var r;if("paragraph"!==e.type.name)return void(l=null);let n=null!==(r=e.attrs.margin)&&void 0!==r?r:null;0===e.childCount&&"0"!==n&&"0"===l&&(a.setNodeMarkup(t,void 0,Object.assign(Object.assign({},e.attrs),{margin:"0"})),n="0",i=!0),l=n})),i?a.setMeta("addToHistory",!1):null},props:Object.assign(Object.assign({},a?{handlePaste(e,t){var r,a;const s=null!==(a=null===(r=t.clipboardData)||void 0===r?void 0:r.getData("text/plain"))&&void 0!==a?a:"";if(!s)return!1;const o=n.parseClipboardTextAsBreaks(s,e.state.schema);return i(o.content),e.dispatch(e.state.tr.replaceSelection(o)),!0}}:{}),{clipboardTextParser(e,t,r,a){const s=n.parseClipboardTextAsBreaks(e,a.state.schema);return i(s.content),s},transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,n.wrapInlineContent(t),n.cleanBlockBrs(t),n.stripTrailingBrs(t),t.innerHTML},transformPasted(r){let a=n.normalizeHardBreaks(r.content);return e||(a=n.stripRichMarks(a)),a=n.mirrorParagraphMargins(a),i(a),new t.Slice(a,r.openStart,r.openEnd)}})})]}});exports.PasteNormalizationExtension=a;
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),t=require("@tiptap/pm/model"),r=require("@tiptap/pm/state"),n=require("./pasteUtils.js");const a=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1,plainOnly:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks,a=this.options.plainOnly;let s;const i=e=>{var t;const r=e.firstChild;s=r&&"paragraph"===r.type.name?null!==(t=r.attrs.margin)&&void 0!==t?t:null:void 0};return[new r.Plugin({appendTransaction(e,t,r){var n;if(!e.some((e=>e.docChanged)))return s=void 0,null;const a=r.tr;let i=!1;if(void 0!==s){const o=s;let l=t.selection.from;for(const t of e)l=t.mapping.map(l);const p=r.doc.resolve(Math.max(0,Math.min(l,r.doc.content.size)));for(let e=p.depth;e>=1;e--){const t=p.node(e);if("paragraph"===t.type.name){(null!==(n=t.attrs.margin)&&void 0!==n?n:null)!==o&&(a.setNodeMarkup(p.before(e),void 0,Object.assign(Object.assign({},t.attrs),{margin:o})),i=!0);break}}}s=void 0;const o=a.doc;let l=null;return o.forEach(((e,t)=>{var r;if("paragraph"!==e.type.name)return void(l=null);let n=null!==(r=e.attrs.margin)&&void 0!==r?r:null;0===e.childCount&&"0"!==n&&"0"===l&&(a.setNodeMarkup(t,void 0,Object.assign(Object.assign({},e.attrs),{margin:"0"})),n="0",i=!0),l=n})),i?a.setMeta("addToHistory",!1):null},props:Object.assign(Object.assign({},a?{handlePaste(e,t){var r,a;const s=null!==(a=null===(r=t.clipboardData)||void 0===r?void 0:r.getData("text/plain"))&&void 0!==a?a:"";if(!s)return!1;const o=n.parseClipboardTextAsBreaks(s,e.state.schema);return i(o.content),e.dispatch(e.state.tr.replaceSelection(o)),!0}}:{}),{clipboardTextParser(e,t,r,a){const s=n.parseClipboardTextAsBreaks(e,a.state.schema);return i(s.content),s},transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,t.querySelectorAll("br.Apple-interchange-newline").forEach((e=>e.remove())),n.wrapInlineContent(t),n.cleanBlockBrs(t),n.stripTrailingBrs(t),t.innerHTML},transformPasted(r){let a=n.normalizeHardBreaks(r.content);return e||(a=n.stripRichMarks(a)),a=n.mirrorParagraphMargins(a),i(a),new t.Slice(a,r.openStart,r.openEnd)}})})]}});exports.PasteNormalizationExtension=a;
|
|
2
2
|
//# sourceMappingURL=PasteNormalizationExtension.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\t// Strip Apple interchange <br> (WebKit clipboard artifact)\n\t\t\t\t\t\t// before cleanBlockBrs converts it to a phantom <p></p>.\n\t\t\t\t\t\tcontainer\n\t\t\t\t\t\t\t.querySelectorAll('br.Apple-interchange-newline')\n\t\t\t\t\t\t\t.forEach((br) => br.remove());\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","querySelectorAll","br","remove","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import e from"styled-components";import{COLORS as t}from"../constants/Theme.js";const
|
|
1
|
+
import e from"styled-components";import{COLORS as t}from"../constants/Theme.js";const r=e.div`
|
|
2
2
|
position: relative;
|
|
3
3
|
width: 100%;
|
|
4
4
|
|
|
@@ -26,18 +26,6 @@ import e from"styled-components";import{COLORS as t}from"../constants/Theme.js";
|
|
|
26
26
|
padding: 0;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/* ProseMirror appends an invisible trailing <br> after a paragraph that
|
|
30
|
-
ends in a hard break, so the cursor can sit on that final line. A plain
|
|
31
|
-
HTML viewer (Gmail, our email shadow DOM) doesn't add one, so the editor
|
|
32
|
-
would show one phantom empty line MORE than the sent HTML per such
|
|
33
|
-
paragraph. Hide it when the paragraph has other content (the break is
|
|
34
|
-
NOT the first child — e.g. a paragraph ending in <br>, even when the
|
|
35
|
-
<br> is nested inside a mark <span>); keep it for truly empty paragraphs
|
|
36
|
-
where it is the only child and provides the blank line. Editor == output. */
|
|
37
|
-
.ProseMirror-trailingBreak:not(:first-child) {
|
|
38
|
-
display: none;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
ul,
|
|
42
30
|
ol {
|
|
43
31
|
margin: 0;
|
|
@@ -79,5 +67,5 @@ import e from"styled-components";import{COLORS as t}from"../constants/Theme.js";
|
|
|
79
67
|
text-decoration-color: #6366f1;
|
|
80
68
|
}
|
|
81
69
|
}
|
|
82
|
-
`;export{
|
|
70
|
+
`;export{r as BikEditorShell};
|
|
83
71
|
//# sourceMappingURL=BikEditor.styles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BikEditor.styles.js","sources":["../../../src/editor/BikEditor.styles.ts"],"sourcesContent":["import styled from 'styled-components';\nimport { COLORS } from '../constants/Theme';\n\nexport const BikEditorShell = styled.div<{\n\tminHeight?: string;\n\tmaxHeight?: string;\n\ttightParagraphs?: boolean;\n}>`\n\tposition: relative;\n\twidth: 100%;\n\n\t.ProseMirror {\n\t\tmin-height: ${({ minHeight }) => minHeight ?? '80px'};\n\t\tmax-height: ${({ maxHeight }) => maxHeight ?? 'none'};\n\t\toverflow-y: auto;\n\t\toutline: none;\n\t\tpadding: 8px 12px;\n\t\tfont-size: 14px;\n\t\tline-height: 1.5;\n\t\tword-break: break-word;\n\n\t\t/* Paragraph spacing.\n\t\t Email/signature (rich): paragraphs carry the browser-default 1em block\n\t\t margin — the SAME spacing the sent HTML gets (we emit plain <p>, which\n\t\t renders against the default <p> margin in Gmail and our email viewer),\n\t\t so editor view == emitted HTML == Gmail. A single Enter = a new\n\t\t paragraph with a visible gap, like pasted Gmail/ChatGPT/Docs content.\n\t\t Other channels (WhatsApp/IG/FB/comments) are plain-text messaging, with\n\t\t no paragraph-gap concept — paragraphs are tight (margin:0) regardless of\n\t\t typed vs pasted. (A margin:0 attr still overrides to tight either way.) */\n\t\tp {\n\t\t\tmargin: ${({ tightParagraphs }) => (tightParagraphs ? '0' : '1em 0')};\n\t\t\tpadding: 0;\n\t\t}\n\n\t\
|
|
1
|
+
{"version":3,"file":"BikEditor.styles.js","sources":["../../../src/editor/BikEditor.styles.ts"],"sourcesContent":["import styled from 'styled-components';\nimport { COLORS } from '../constants/Theme';\n\nexport const BikEditorShell = styled.div<{\n\tminHeight?: string;\n\tmaxHeight?: string;\n\ttightParagraphs?: boolean;\n}>`\n\tposition: relative;\n\twidth: 100%;\n\n\t.ProseMirror {\n\t\tmin-height: ${({ minHeight }) => minHeight ?? '80px'};\n\t\tmax-height: ${({ maxHeight }) => maxHeight ?? 'none'};\n\t\toverflow-y: auto;\n\t\toutline: none;\n\t\tpadding: 8px 12px;\n\t\tfont-size: 14px;\n\t\tline-height: 1.5;\n\t\tword-break: break-word;\n\n\t\t/* Paragraph spacing.\n\t\t Email/signature (rich): paragraphs carry the browser-default 1em block\n\t\t margin — the SAME spacing the sent HTML gets (we emit plain <p>, which\n\t\t renders against the default <p> margin in Gmail and our email viewer),\n\t\t so editor view == emitted HTML == Gmail. A single Enter = a new\n\t\t paragraph with a visible gap, like pasted Gmail/ChatGPT/Docs content.\n\t\t Other channels (WhatsApp/IG/FB/comments) are plain-text messaging, with\n\t\t no paragraph-gap concept — paragraphs are tight (margin:0) regardless of\n\t\t typed vs pasted. (A margin:0 attr still overrides to tight either way.) */\n\t\tp {\n\t\t\tmargin: ${({ tightParagraphs }) => (tightParagraphs ? '0' : '1em 0')};\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tul,\n\t\tol {\n\t\t\tmargin: 0;\n\t\t\tpadding-left: 1.5em;\n\t\t}\n\n\t\tli + li {\n\t\t\tmargin-top: 2px;\n\t\t}\n\n\t\tp.is-editor-empty:first-child::before {\n\t\t\tcontent: attr(data-placeholder);\n\t\t\tfloat: left;\n\t\t\tcolor: #adb5bd;\n\t\t\tpointer-events: none;\n\t\t\theight: 0;\n\t\t}\n\t}\n\n\t.bik-mention {\n\t\tcolor: ${COLORS.content.brand};\n\t\tpadding: 1px 4px;\n\t}\n\t.bik-mention--team {\n\t\tcolor: ${COLORS.content.brand};\n\t}\n\t.bik-variable {\n\t}\n\n\ta,\n\t.bik-link {\n\t\tcolor: #4f46e5;\n\t\ttext-decoration: underline;\n\t\ttext-decoration-color: #a5b4fc;\n\t\ttext-underline-offset: 2px;\n\t\tcursor: pointer;\n\t\t&:hover {\n\t\t\tcolor: #3730a3;\n\t\t\ttext-decoration-color: #6366f1;\n\t\t}\n\t}\n`;\n"],"names":["BikEditorShell","styled","div","_ref","minHeight","_ref2","maxHeight","_ref3","tightParagraphs","COLORS","content","brand"],"mappings":"gFAGaA,MAAAA,EAAiBC,EAAOC,GAInC;;;;;gBAKcC,IAAA,IAACC,UAAEA,GAAWD,EAAA,OAAKC,QAAAA,EAAa,MAAM;gBACtCC,IAAA,IAACC,UAAEA,GAAWD,EAAA,OAAKC,QAAAA,EAAa,MAAM;;;;;;;;;;;;;;;;;;aAkBzCC,IAAA,IAACC,gBAAEA,GAAiBD,EAAA,OAAMC,EAAkB,IAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;WAwB3DC,EAAOC,QAAQC;;;;WAIfF,EAAOC,QAAQC;;;;;;;;;;;;;;;;;"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Extension as t}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as e}from"@tiptap/pm/model";import{Plugin as n}from"@tiptap/pm/state";import{parseClipboardTextAsBreaks as r,wrapInlineContent as a,cleanBlockBrs as o,stripTrailingBrs as s,normalizeHardBreaks as i,stripRichMarks as l,mirrorParagraphMargins as p}from"./pasteUtils.js";const
|
|
1
|
+
import{Extension as t}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as e}from"@tiptap/pm/model";import{Plugin as n}from"@tiptap/pm/state";import{parseClipboardTextAsBreaks as r,wrapInlineContent as a,cleanBlockBrs as o,stripTrailingBrs as s,normalizeHardBreaks as i,stripRichMarks as l,mirrorParagraphMargins as p}from"./pasteUtils.js";const c=t.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1,plainOnly:!1}),addProseMirrorPlugins(){const t=this.options.preserveMarks,c=this.options.plainOnly;let d;const m=t=>{var e;const n=t.firstChild;d=n&&"paragraph"===n.type.name?null!==(e=n.attrs.margin)&&void 0!==e?e:null:void 0};return[new n({appendTransaction(t,e,n){var r;if(!t.some((t=>t.docChanged)))return d=void 0,null;const a=n.tr;let o=!1;if(void 0!==d){const s=d;let i=e.selection.from;for(const e of t)i=e.mapping.map(i);const l=n.doc.resolve(Math.max(0,Math.min(i,n.doc.content.size)));for(let t=l.depth;t>=1;t--){const e=l.node(t);if("paragraph"===e.type.name){(null!==(r=e.attrs.margin)&&void 0!==r?r:null)!==s&&(a.setNodeMarkup(l.before(t),void 0,Object.assign(Object.assign({},e.attrs),{margin:s})),o=!0);break}}}d=void 0;const s=a.doc;let i=null;return s.forEach(((t,e)=>{var n;if("paragraph"!==t.type.name)return void(i=null);let r=null!==(n=t.attrs.margin)&&void 0!==n?n:null;0===t.childCount&&"0"!==r&&"0"===i&&(a.setNodeMarkup(e,void 0,Object.assign(Object.assign({},t.attrs),{margin:"0"})),r="0",o=!0),i=r})),o?a.setMeta("addToHistory",!1):null},props:Object.assign(Object.assign({},c?{handlePaste(t,e){var n,a;const o=null!==(a=null===(n=e.clipboardData)||void 0===n?void 0:n.getData("text/plain"))&&void 0!==a?a:"";if(!o)return!1;const s=r(o,t.state.schema);return m(s.content),t.dispatch(t.state.tr.replaceSelection(s)),!0}}:{}),{clipboardTextParser(t,e,n,a){const o=r(t,a.state.schema);return m(o.content),o},transformPastedHTML(t){if(t.includes("data-pm-slice"))return t;const e=document.createElement("div");return e.innerHTML=t,e.querySelectorAll("br.Apple-interchange-newline").forEach((t=>t.remove())),a(e),o(e),s(e),e.innerHTML},transformPasted(n){let r=i(n.content);return t||(r=l(r)),r=p(r),m(r),new e(r,n.openStart,n.openEnd)}})})]}});export{c as PasteNormalizationExtension};
|
|
2
2
|
//# sourceMappingURL=PasteNormalizationExtension.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"4WA0CaA,EAA8BC,EAAUC,OAAO,CAC3DC,KAAM,qBACNC,WAAUA,KACF,CACNC,eAAe,EACfC,WAAW,IAGbC,wBACC,MAAMF,EAAgBG,KAAKC,QAAQJ,cAC7BC,EAAYE,KAAKC,QAAQH,UAM/B,IAAII,EAEJ,MAAMC,EAAqBC,UAC1B,MAAMC,EAAQD,EAAQE,WACtBJ,EACCG,GAA6B,cAApBA,EAAME,KAAKZ,KACI,UAArBU,EAAMG,MAAc,cAAC,IAAAC,EAAAA,EAAI,UACzBC,CAAS,EAGd,MAAO,CACN,IAAIC,EAAO,CACVC,kBAAkBC,EAAcC,EAAUC,SACzC,IAAKF,EAAaG,MAAMC,GAAMA,EAAEC,aAE/B,OADAhB,OAAoBQ,EACb,KAGR,MAAMS,EAAKJ,EAASI,GACpB,IAAIC,GAAU,EAKd,QAA0BV,IAAtBR,EAAiC,CACpC,MAAMmB,EAASnB,EACf,IAAIoB,EAAMR,EAASS,UAAUC,KAC7B,IAAK,MAAMP,KAAKJ,EAAcS,EAAML,EAAEQ,QAAQC,IAAIJ,GAClD,MAAMK,EAAOZ,EAASa,IAAIC,QACzBC,KAAKC,IAAI,EAAGD,KAAKE,IAAIV,EAAKP,EAASa,IAAIxB,QAAQ6B,QAEhD,IAAK,IAAIC,EAAIP,EAAKQ,MAAOD,GAAK,EAAGA,IAAK,CACrC,MAAME,EAAOT,EAAKS,KAAKF,GACvB,GAAuB,cAAnBE,EAAK7B,KAAKZ,KAAsB,EACN,QAAxBc,EAAA2B,EAAK5B,MAAc,cAAK,IAAAC,EAAAA,EAAA,QAAUY,IACtCF,EAAGkB,cAAcV,EAAKW,OAAOJ,QAAIxB,iCAC7B0B,EAAK5B,OACR,CAAAa,YAEDD,GAAU,GAEX,KACA,CACD,CACD,CACDlB,OAAoBQ,EAUpB,MAAMkB,EAAMT,EAAGS,IACf,IAAIW,EAA4B,KAehC,OAdAX,EAAIY,SAAQ,CAACJ,EAAMd,WAClB,GAAuB,cAAnBc,EAAK7B,KAAKZ,KAEb,YADA4C,EAAa,MAGd,IAAIlB,EAAoD,QAA3CZ,EAAC2B,EAAK5B,MAAc,cAAuB,IAAAC,EAAAA,EAAA,KAChC,IAApB2B,EAAKK,YAA+B,MAAXpB,GAAiC,MAAfkB,IAC9CpB,EAAGkB,cAAcf,OAAKZ,EAASgC,OAAAC,OAAAD,OAAAC,OAAA,CAAA,EAAOP,EAAK5B,OAAO,CAAAa,OAAQ,OAC1DA,EAAS,IACTD,GAAU,GAEXmB,EAAalB,CAAM,IAGbD,EAAUD,EAAGyB,QAAQ,gBAAgB,GAAS,IACrD,EACDC,MAAKH,OAAAC,OAAAD,OAAAC,OAAA,CAAA,EAGA7C,EACD,CACAgD,YAAYC,EAAMC,WACjB,MAAMC,EAAiD,QAA1CC,EAAqB,QAArBzC,EAAAuC,EAAMG,qBAAe,IAAA1C,OAAA,EAAAA,EAAA2C,QAAQ,qBAAa,IAAAF,EAAAA,EAAI,GAC3D,IAAKD,EAAM,OAAO,EAClB,MAAMI,EAAQC,EACbL,EACAF,EAAKQ,MAAMC,QAIZ,OAFArD,EAAkBkD,EAAMjD,SACxB2C,EAAKU,SAASV,EAAKQ,MAAMpC,GAAGuC,iBAAiBL,KACtC,CACR,GAEA,CAAG,GAAA,CAKNM,oBAAoBV,EAAMW,EAAWC,EAAQd,GAC5C,MAAMM,EAAQC,EAA2BL,EAAMF,EAAKQ,MAAMC,QAE1D,OADArD,EAAkBkD,EAAMjD,SACjBiD,CACP,EAEDS,oBAAoBC,GACnB,GAAIA,EAAKC,SAAS,iBAAkB,OAAOD,EAC3C,MAAME,EAAYC,SAASC,cAAc,OAKzC,OAJAF,EAAUG,UAAYL,EACtBM,EAAkBJ,GAClBK,EAAcL,GACdM,EAAiBN,GACVA,EAAUG,SACjB,EAEDI,gBAAgBnB,GACf,IAAIjD,EAAUqE,EAAoBpB,EAAMjD,SAMxC,OALKP,IACJO,EAAUsE,EAAetE,IAE1BA,EAAUuE,EAAuBvE,GACjCD,EAAkBC,GACX,IAAIwE,EAAMxE,EAASiD,EAAMwB,UAAWxB,EAAMyB,QAClD,MAIJ"}
|
|
1
|
+
{"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\t// Strip Apple interchange <br> (WebKit clipboard artifact)\n\t\t\t\t\t\t// before cleanBlockBrs converts it to a phantom <p></p>.\n\t\t\t\t\t\tcontainer\n\t\t\t\t\t\t\t.querySelectorAll('br.Apple-interchange-newline')\n\t\t\t\t\t\t\t.forEach((br) => br.remove());\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","querySelectorAll","br","remove","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"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"}
|