-
Notifications
You must be signed in to change notification settings - Fork 134
Fix/unset link clear transient hyperlink styleid #3190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { Attribute } from '@core/Attribute.js'; | |
| import { getMarkRange } from '@core/helpers/getMarkRange.js'; | ||
| import { findOrCreateRelationship } from '@core/parts/adapters/relationships-mutation.js'; | ||
| import { sanitizeHref, encodeTooltip, UrlValidationConstants } from '@superdoc/url-validation'; | ||
| import { TRANSIENT_HYPERLINK_STYLE_IDS } from '@extensions/run/calculateInlineRunPropertiesPlugin.js'; | ||
|
|
||
| /** | ||
| * Target frame options | ||
|
|
@@ -36,6 +37,7 @@ import { sanitizeHref, encodeTooltip, UrlValidationConstants } from '@superdoc/u | |
| * @property {string} [docLocation] - Location in target hyperlink | ||
| * @property {string} [tooltip] - Tooltip for the link | ||
| * @property {string} [rId] @internal Word relationship ID for internal links | ||
| * @property {boolean} [hadUnderline] @internal Whether selection already had underline before setLink | ||
| */ | ||
|
|
||
| /** | ||
|
|
@@ -119,6 +121,12 @@ export const Link = Mark.create({ | |
| * @param {string} [rId] - Word relationship ID for internal links | ||
| */ | ||
| rId: { default: this.options.htmlAttributes.rId || null }, | ||
| /** | ||
| * @private | ||
| * @category Attribute | ||
| * @param {boolean} [hadUnderline] - keep if underline existed before link creation | ||
| */ | ||
| hadUnderline: { default: false, rendered: false }, | ||
| /** | ||
| * @category Attribute | ||
| * @param {string} [text] - Display text for the link | ||
|
|
@@ -226,6 +234,16 @@ export const Link = Mark.create({ | |
| to = from + finalText.length; | ||
| } | ||
|
|
||
| let hadUnderline = false; | ||
| if (underlineMarkType) { | ||
| state.doc.nodesBetween(from, to, (node) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. repro:
the editor crashes: fix: do the |
||
| if (!node.isText) return; | ||
| if (node.marks.some((mark) => mark.type === underlineMarkType)) { | ||
| hadUnderline = true; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| if (linkMarkType) tr = tr.removeMark(from, to, linkMarkType); | ||
| if (underlineMarkType) tr = tr.removeMark(from, to, underlineMarkType); | ||
|
|
||
|
|
@@ -237,7 +255,7 @@ export const Link = Mark.create({ | |
| if (id) rId = id; | ||
| } | ||
|
|
||
| const linkAttrs = { text: finalText, rId }; | ||
| const linkAttrs = { text: finalText, rId, hadUnderline }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a selection mixes underlined and plain text, this stores one Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @christos8333 valid - i reproduced this through the toolbar flow. flagged it in the review at line 237 with a fix sketch.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. repro:
result: "hi" is still underlined, even though it was plain to start with. editing a link re-runs |
||
| if (sanitizedHref?.href) { | ||
| linkAttrs.href = sanitizedHref.href; | ||
| } | ||
|
|
@@ -258,11 +276,80 @@ export const Link = Mark.create({ | |
| */ | ||
| unsetLink: | ||
| () => | ||
| ({ chain }) => { | ||
| return chain() | ||
| .unsetMark('underline', { extendEmptyMarkRange: true }) | ||
| ({ chain, state, editor }) => { | ||
| const { selection } = state; | ||
| const linkMarkType = editor.schema.marks.link; | ||
|
|
||
| let { from, to } = selection; | ||
|
|
||
| if (selection.empty && linkMarkType) { | ||
| const range = getMarkRange(selection.$from, linkMarkType); | ||
| if (range) { | ||
| from = range.from; | ||
| to = range.to; | ||
| } | ||
| } | ||
|
|
||
| let hadUnderline = false; | ||
| if (linkMarkType) { | ||
| state.doc.nodesBetween(from, to, (node) => { | ||
| if (!node.isText || hadUnderline) return; | ||
| const linkMark = node.marks.find((mark) => mark.type === linkMarkType); | ||
| if (linkMark?.attrs?.hadUnderline) { | ||
| hadUnderline = true; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const commandChain = chain(); | ||
| if (!hadUnderline) { | ||
| commandChain.unsetMark('underline', { extendEmptyMarkRange: true }); | ||
| } | ||
|
|
||
| return commandChain | ||
| .unsetColor() | ||
| .unsetMark('link', { extendEmptyMarkRange: true }) | ||
| .command(({ tr }) => { | ||
| const textStyleMarkType = tr.doc.type.schema.marks.textStyle; | ||
| if (textStyleMarkType) { | ||
| tr.doc.nodesBetween(from, to, (node, pos) => { | ||
| if (!node.isText) return; | ||
|
|
||
| node.marks.forEach((mark) => { | ||
| if (mark.type !== textStyleMarkType) return; | ||
| if (!TRANSIENT_HYPERLINK_STYLE_IDS.has(mark.attrs?.styleId)) return; | ||
|
|
||
| const clearedAttrs = { ...mark.attrs, styleId: null }; | ||
| tr.removeMark(pos, pos + node.nodeSize, mark); | ||
| tr.addMark(pos, pos + node.nodeSize, textStyleMarkType.create(clearedAttrs)); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| const runNodesToUpdate = []; | ||
| tr.doc.nodesBetween(from, to, (node, pos) => { | ||
| if (node.type.name !== 'run') return; | ||
| if (!TRANSIENT_HYPERLINK_STYLE_IDS.has(node.attrs?.runProperties?.styleId)) return; | ||
| runNodesToUpdate.push({ node, pos }); | ||
| }); | ||
|
|
||
| runNodesToUpdate | ||
| .sort((a, b) => b.pos - a.pos) | ||
| .forEach(({ node, pos }) => { | ||
| const mappedPos = tr.mapping.map(pos); | ||
| tr.setNodeMarkup( | ||
| mappedPos, | ||
| node.type, | ||
| { | ||
| ...node.attrs, | ||
| runProperties: { ...node.attrs.runProperties, styleId: null }, | ||
| }, | ||
| node.marks, | ||
| ); | ||
| }); | ||
|
|
||
| return true; | ||
| }) | ||
| .run(); | ||
| }, | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repro:
result: "abcd" stays underlined. "d" was plain to start with, and there's no easy way to clear it.
the flag is one true/false for the whole selection, so any partial underline turns it on. fix: track it per character, or drop the underline-removal step and let the styleId cleanup handle the visual.