Skip to content
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

feat(lexical): add alt text on link node #3600

Merged
merged 13 commits into from
Oct 17, 2023
10 changes: 10 additions & 0 deletions packages/lexical-editor/src/commands/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createCommand, LexicalCommand } from "lexical";
import { LinkAttributes } from "@lexical/link";

export interface LinkNodePayload extends LinkAttributes {
url: string;
alt?: string;
}

export const TOGGLE_LINK_NODE_COMMAND: LexicalCommand<string | LinkNodePayload | null> =
createCommand("TOGGLE_LINK_NODE_COMMAND");
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useCallback, useEffect, useState } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
import { $isLinkNode as $baseLinkNode } from "@lexical/link";
import { $getSelection, $isRangeSelection } from "lexical";
import { getSelectedNode } from "~/utils/getSelectedNode";
import { useRichTextEditor } from "~/hooks/useRichTextEditor";
import { TOGGLE_LINK_NODE_COMMAND } from "~/commands/link";
import { $isLinkNode } from "~/nodes/LinkNode";

export const LinkAction = () => {
const [editor] = useLexicalComposerContext();
Expand All @@ -12,10 +14,10 @@ export const LinkAction = () => {

const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
editor.dispatchCommand(TOGGLE_LINK_NODE_COMMAND, "https://");
setNodeIsText(false);
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
editor.dispatchCommand(TOGGLE_LINK_NODE_COMMAND, null);
}
}, [editor, isLink]);

Expand All @@ -28,7 +30,7 @@ export const LinkAction = () => {
const node = getSelectedNode(selection);
// Update links
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
if ($baseLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
Expand Down
3 changes: 2 additions & 1 deletion packages/lexical-editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export { RichTextEditor } from "~/components/Editor/RichTextEditor";
export { HeadingEditor } from "~/components/Editor/HeadingEditor";
export { ParagraphEditor } from "~/components/Editor/ParagraphEditor";
// plugins
export { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
export { LinkPlugin } from "~/plugins/LinkNodePlugin/LinkNodePlugin";
export { FloatingLinkEditorPlugin } from "~/plugins/FloatingLinkEditorPlugin/FloatingLinkEditorPlugin";
export { CodeHighlightPlugin } from "~/plugins/CodeHighlightPlugin/CodeHighlightPlugin";
export { BlurEventPlugin } from "~/plugins/BlurEventPlugin/BlurEventPlugin";
Expand All @@ -41,6 +41,7 @@ export { TypographyPlugin } from "~/plugins/TypographyPlugin/TypographyPlugin";
export { QuotePlugin } from "~/plugins/WebinyQuoteNodePlugin/WebinyQuoteNodePlugin";
export { ListPlugin } from "~/plugins/ListPLugin/ListPlugin";
export { ImagesPlugin } from "~/plugins/ImagesPlugin/ImagesPlugin";

// utils
export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue";
export { isValidLexicalData } from "~/utils/isValidLexicalData";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import {
LinkAttributes,
LinkNode as BaseLinkNode,
SerializedAutoLinkNode,
SerializedLinkNode as BaseSerializedLinkNode
} from "@lexical/link";
import { DOMConversionMap, DOMConversionOutput, EditorConfig, NodeKey, Spread } from "lexical";
import { addClassNamesToElement, isHTMLAnchorElement } from "@lexical/utils";
import { sanitizeUrl } from "~/utils/sanitizeUrl";

export interface LinkNodeAttributes extends LinkAttributes {
alt?: string;
}

export type SerializedLinkNode = Spread<
{
alt?: string;
type: "link-node";
version: 1;
},
Spread<LinkAttributes, BaseSerializedLinkNode>
Spread<LinkNodeAttributes, BaseSerializedLinkNode>
>;

export class LinkNode extends BaseLinkNode {
constructor(url: string, attributes: LinkAttributes = {}, key?: NodeKey) {
__alt?: string;

constructor(url: string, attributes: LinkNodeAttributes, key?: NodeKey) {
super(url, attributes, key);
this.__alt = attributes.alt;
}

static override getType(): string {
Expand All @@ -28,11 +35,21 @@ export class LinkNode extends BaseLinkNode {
static override clone(node: LinkNode): LinkNode {
return new LinkNode(
node.__url,
{ rel: node.__rel, target: node.__target, title: node.__title },
{ rel: node.__rel, target: node.__target, title: node.__title, alt: node.__alt },
node.__key
);
}

getAlt(): string | undefined {
return this.__alt;
}

setAlt(text: string): this {
const self = super.getWritable();
self.__alt = text;
return self;
}

override createDOM(config: EditorConfig): HTMLAnchorElement {
const element = document.createElement("a");
element.href = sanitizeUrl(this.__url);
Expand All @@ -45,10 +62,23 @@ export class LinkNode extends BaseLinkNode {
if (this.__title !== null) {
element.title = this.__title;
}
if (this.__alt) {
element.setAttribute("alt", this.__alt);
}

addClassNamesToElement(element, config.theme.link);
return element;
}

override updateDOM(prevNode: LinkNode, dom: HTMLElement): boolean {
if (this.__alt) {
dom.setAttribute("alt", this.__alt);
}
// Returning false tells Lexical that this node does not need its
// DOM element replacing with a new copy from createDOM.
return false;
}

static override importDOM(): DOMConversionMap | null {
return {
a: () => ({
Expand All @@ -58,48 +88,53 @@ export class LinkNode extends BaseLinkNode {
};
}

static override importJSON(
serializedNode: BaseSerializedLinkNode | SerializedLinkNode | SerializedAutoLinkNode
): LinkNode {
static override importJSON(serializedNode: SerializedLinkNode): LinkNode {
const node = $createLinkNode(serializedNode.url, {
rel: serializedNode.rel,
target: serializedNode.target,
title: serializedNode.title
title: serializedNode.title,
alt: serializedNode.alt
});
node.setFormat(serializedNode.format);
node.setIndent(serializedNode.indent);
node.setDirection(serializedNode.direction);

if (serializedNode.alt) {
node.setAlt(serializedNode.alt);
}
return node;
}

override exportJSON(): BaseSerializedLinkNode | SerializedLinkNode | SerializedAutoLinkNode {
override exportJSON(): SerializedLinkNode {
return {
...super.exportJSON(),
alt: this.__alt,
type: "link-node",
version: 1
};
}
}

function convertAnchorElement(domNode: Node): DOMConversionOutput {
const convertAnchorElement = (domNode: Node): DOMConversionOutput => {
let node = null;
if (isHTMLAnchorElement(domNode)) {
const content = domNode.textContent;
if (content !== null && content !== "") {
node = $createLinkNode(domNode.getAttribute("href") || "", {
rel: domNode.getAttribute("rel"),
target: domNode.getAttribute("target"),
title: domNode.getAttribute("title")
title: domNode.getAttribute("title"),
alt: domNode.getAttribute("alt") ?? undefined
});
}
}
return { node };
}
};

export const $isLinkNode = (node: any): node is LinkNode => {
return node instanceof LinkNode;
};

export const $createLinkNode = (url: string, attributes: LinkAttributes = {}, key?: KeyType) => {
export const $createLinkNode = (url: string, attributes: LinkNodeAttributes, key?: KeyType) => {
return new LinkNode(url, attributes, key);
};
2 changes: 1 addition & 1 deletion packages/lexical-editor/src/nodes/webinyNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ParagraphNode } from "~/nodes/ParagraphNode";
import { HeadingNode as BaseHeadingNode, QuoteNode as BaseQuoteNode } from "@lexical/rich-text";
import { QuoteNode } from "~/nodes/QuoteNode";
import { ImageNode } from "~/nodes/ImageNode";
import { LinkNode } from "~/nodes/link-node";
import { LinkNode } from "~/nodes/LinkNode";

// This is a list of all the nodes that our Lexical implementation supports OOTB.
export const WebinyNodes: ReadonlyArray<
Expand Down
Loading
Loading