Variable Plugin #2256
Replies: 4 comments 3 replies
-
|
Got the part of editing work by change mode to 'token' , for anyone else looking for this export function $createTextVariableNode(payload: TextVariablePayload): TextVariableNode {
const textVariableNode = new TextVariableNode(payload);
textVariableNode.setMode('token').toggleDirectionless();
return textVariableNode;
} |
Beta Was this translation helpful? Give feedback.
-
|
@fameoflight Any news on your variable plugin? kind of curious on how it works since I'm currently implementing my own variable plugin |
Beta Was this translation helpful? Give feedback.
-
|
Any chance you are willing to share the plugin? I am looking for a template variable plugin and would love to try yours out |
Beta Was this translation helpful? Give feedback.
-
|
@nickbugati please find it below, it may not work in current version (as I haven't updated it in two years, but general logic should work) index.tsx import TextVariableCorePlugin from './TextVariableCorePlugin';
import TextVariableDropdown from './TextVariableDropdown';
import {
$createTextVariableNode,
$isTextVariableNode,
TextVariableNode,
} from './TextVariableNode';
import { TextVariableInputType as InputType } from './types';
export type TextVariableInputType = InputType;
const TextVariablePlugin = {
Plugin: TextVariableCorePlugin,
Node: TextVariableNode,
Dropdown: TextVariableDropdown,
$createTextVariableNode,
$isTextVariableNode,
};
export default TextVariablePlugin;TextVariableCorePlugin.tsx import { useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getNearestNodeOfType } from '@lexical/utils';
import { $createTextVariableNode, TextVariableNode } from './TextVariableNode';
import {
$getSelection,
$isRangeSelection,
$isTextNode,
COMMAND_PRIORITY_EDITOR,
ElementNode,
LexicalCommand,
LexicalNode,
createCommand,
} from 'lexical';
import { TextVariableInputType } from './types';
export const INSERT_VARIABLE_COMMAND: LexicalCommand<TextVariableInputType> =
createCommand();
export default function TextVariableCorePlugin(): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([TextVariableNode])) {
throw new Error(
'TextVariablePlugin: TextVariableNode not registered on editor'
);
}
return editor.registerCommand<TextVariableInputType>(
INSERT_VARIABLE_COMMAND,
(payload) => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const focusNode = selection.focus.getNode();
const variableNode = $createTextVariableNode(payload);
if ($isTextNode(focusNode)) {
selection.insertNodes([variableNode]);
} else {
const elementNode: LexicalNode | null = $getNearestNodeOfType(
focusNode,
ElementNode
);
if (elementNode) {
elementNode.append(variableNode);
}
}
}
return true;
},
COMMAND_PRIORITY_EDITOR
);
}, [editor]);
return null;
}TextVariableDropdown.tsx import React from 'react';
import _ from 'lodash';
import { Dropdown } from 'antd';
import LexicalButton from '../LexicalButton';
import TextVariableMenu from './TextVariableMenu';
import { TextVariableInputType } from './types';
interface ITextVariableDropdownProps {
className?: string;
variables?: readonly TextVariableInputType[];
}
function TextVariableDropdown({
variables,
className,
}: ITextVariableDropdownProps) {
if (!variables || _.isEmpty(variables)) {
return null;
}
const menu = (
<TextVariableMenu
variables={variables}
inlineIndent={2}
getPopupContainer={(node) => node.parentElement as any}
/>
);
return (
<Dropdown
className={className}
overlay={menu}
placement="bottom"
trigger={['click']}
>
<LexicalButton
type="button"
className="toolbar-item block-controls"
aria-label="Formatting Options"
>
<i className="icon variables" />
<span className="text">Variables</span>
<i className="chevron-down" />
</LexicalButton>
</Dropdown>
);
}
export default TextVariableDropdown;TextVariableMenu.tsx import React, { useCallback, useState } from 'react';
import _ from 'lodash';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { Menu, MenuProps } from 'antd';
import { INSERT_VARIABLE_COMMAND } from './TextVariableCorePlugin';
import { titlize, trimPrefix } from '../../../utils';
import { TextVariableInputType } from './types';
interface ITextVariableMenuProps extends MenuProps {
variables?: readonly TextVariableInputType[];
}
function TextVariableMenu(props: ITextVariableMenuProps) {
const { variables, onClick, ...restProps } = props;
if (!variables || _.isEmpty(variables)) {
return null;
}
const [openKeys, setOpenKeys] = useState<string[]>([]);
const variableGroups = _.groupBy(
variables,
(variable) => _.split(variable.key, '__')[0]
);
const menuItems = _.map(variableGroups, (values, key) => {
const prefix = titlize(key);
const newValues = _.map(values, (value) => ({
...value,
className: 'min-w-max',
label: trimPrefix(value.label, prefix),
}));
// const newValues = _.map(values, (valueItem) => {valueItem, label: trimPrefix(valueItem.label, prefix)})
return {
key,
label: prefix,
children: newValues,
};
});
const [editor] = useLexicalComposerContext();
const onVariableAdd = useCallback(
(variableKey: string) => {
const variable = _.keyBy(variables, 'key')[variableKey];
editor.dispatchCommand(INSERT_VARIABLE_COMMAND, variable);
},
[editor, variables]
);
const rootSubmenuKeys = _.map(menuItems, (menuItem) => menuItem.key);
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
return (
<Menu
{...restProps}
onClick={(info) => {
onVariableAdd(info.key);
onClick?.(info);
}}
openKeys={openKeys}
items={menuItems}
onOpenChange={onOpenChange}
/>
);
}
export default TextVariableMenu;TextVariableNode.tsx /**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
/* eslint-disable */
import _ from 'lodash';
import type { EditorConfig, LexicalNode, NodeKey } from 'lexical';
import { TextNode } from 'lexical';
import { SerializedTextVariableNode, TextVariablePayloadType } from './types';
export class TextVariableNode extends TextNode {
__textVariableLabel: string;
__textVariableKey: string;
__textVariableValue: string | null = null;
static getType(): string {
return 'textVariable';
}
static clone(node: TextVariableNode): TextVariableNode {
const payload: TextVariablePayloadType = {
label: node.__textVariableLabel,
key: node.__textVariableKey,
value: node.__textVariableValue,
};
return new TextVariableNode(payload, node.__text, node.__key);
}
static importJSON(
serializedNode: SerializedTextVariableNode
): TextVariableNode {
const payload: TextVariablePayloadType = {
label: serializedNode.textVariableLabel,
key: serializedNode.textVariableKey,
value: serializedNode.textVariableValue,
};
const node = $createTextVariableNode(payload);
node.setTextContent(serializedNode.text);
node.setFormat(serializedNode.format);
// @ts-ignore
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
constructor(payload: TextVariablePayloadType, text?: string, key?: NodeKey) {
const label = payload.value || payload.label;
super(label, key);
this.__textVariableLabel = payload.label;
this.__textVariableKey = payload.key;
this.__textVariableValue = payload.value || null;
}
exportJSON(): SerializedTextVariableNode {
return {
...super.exportJSON(),
textVariableLabel: this.__textVariableLabel,
textVariableKey: this.__textVariableKey,
textVariableValue: undefined,
type: 'textVariable',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
let className = config.theme.textVariable;
if (this.__textVariableValue) {
className = '';
}
const dom = super.createDOM(config);
dom.className = className;
return dom;
}
}
export function $createTextVariableNode(
payload: TextVariablePayloadType
): TextVariableNode {
const textVariableNode = new TextVariableNode(payload);
textVariableNode.setMode('token').toggleDirectionless();
return textVariableNode;
}
export function $isTextVariableNode(
node: LexicalNode | null | undefined
): node is TextVariableNode {
return node instanceof TextVariableNode;
}types import { SpreadTextNode } from '../../shared/types';
export type SerializedTextVariableNode = SpreadTextNode<{
textVariableLabel: string;
textVariableKey: string;
textVariableValue?: string | null;
type: 'textVariable';
version: 1;
}>;
export type TextVariablePayloadType = {
label: string;
key: string;
value?: string | null;
};
export type TextVariableInputType = TextVariablePayloadType & {
value?: string | null;
}; |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi All,
I am building a variable plugin, I have got the basics right but running into few problems. FIrst I want to make sure when someone backspace the editor deletes the whole variable.
I have tried to make dom contenteditable=false, here is my createDom Function
Also very weird stuff happen sometime inserting it, any ideas what I might be doing wrong here
Any architecture tips of how to replace these variables with actual values when previewing, for now I was just thinking of iterating the json and replace textVariableNode with textNode with value. Let me know if there is better way to do this.
Beta Was this translation helpful? Give feedback.
All reactions