Skip to content

Commit

Permalink
feat(lexical-editor): add alt text on link node (#3600)
Browse files Browse the repository at this point in the history
  • Loading branch information
mihajlovco authored Oct 17, 2023
1 parent cd33bf0 commit eedeb37
Show file tree
Hide file tree
Showing 12 changed files with 755 additions and 192 deletions.
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,40 +1,60 @@
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<
{
type: "link-node";
alt?: string;
type: "link";
version: 1;
},
Spread<LinkAttributes, BaseSerializedLinkNode>
Spread<LinkNodeAttributes, BaseSerializedLinkNode>
>;

/**
* NOTES: This class is extended to support custom URLs patterns.
* - We use custom 'sanitizeUrl' method to control what kind of ULRs we will support or prevent to be added.
*/
export class LinkNode extends BaseLinkNode {
constructor(url: string, attributes: LinkAttributes = {}, key?: NodeKey) {
super(url, attributes, key);
}
__alt?: string;

static override getType(): string {
return "link-node";
constructor(url: string, attributes: LinkNodeAttributes, key?: NodeKey) {
super(url, attributes, key);
this.__alt = attributes.alt;
}

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");
/**
* Use custom sanitization function for the URL.
*/
element.href = sanitizeUrl(this.__url);
if (this.__target !== null) {
element.target = this.__target;
Expand All @@ -45,10 +65,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 +91,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(),
type: "link-node",
alt: this.__alt,
type: "link",
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

0 comments on commit eedeb37

Please sign in to comment.