Skip to content

Commit

Permalink
Merge pull request #341 from MovieReviewComment/feature/issue-302/pla…
Browse files Browse the repository at this point in the history
…ceholder

[#302] Add Placeholder Plugin
  • Loading branch information
2wheeh authored Apr 24, 2024
2 parents 7608cf7 + e66d355 commit ba52dd9
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 3 deletions.
4 changes: 2 additions & 2 deletions ui/src/components/review/server/viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import dynamic from 'next/dynamic';

import { getHtml } from '@/lib/utils/review/get-html';
import { parseReviewContent } from '@/lib/utils/review/parse-review-content';
import { MAX_CONTENT_LENGTH } from '@/lib/constants/review';
import { getHtml } from '@/lib/utils/editor/get-html';
import { parseReviewContent } from '@/lib/utils/review/parse-review-content';

export async function Viewer({ content }: { content: string }) {
const { serializedEditorState } = parseReviewContent(content);
Expand Down
6 changes: 5 additions & 1 deletion ui/src/editor/plugins/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import { HistoryPlugin } from '@/editor/plugins/history';
import { ListMaxIndentLevelPlugin } from '@/editor/plugins/list-max-indent-level';
import { MarkdownShortcutPlugin } from '@/editor/plugins/markdown-shorcut';
import { MaxLengthPlugin } from '@/editor/plugins/max-length';
import { PlaceholderPlugin } from '@/editor/plugins/placeholder';

import { DEFAULT_PLACEHOLDER } from '@/lib/constants/editor';

function Placeholder() {
return <div className="placeholder">Begin writing your review...</div>;
return <div className="placeholder">{DEFAULT_PLACEHOLDER}</div>;
}

export function Plugins({ maxLength }: { maxLength?: number }) {
Expand All @@ -39,6 +42,7 @@ export function Plugins({ maxLength }: { maxLength?: number }) {
<TabIndentationPlugin />
<FixIOSKoreanIssuePlugin />
<ListMaxIndentLevelPlugin maxDepth={3} />
<PlaceholderPlugin />
{onRef !== undefined && <EditorRefPlugin editorRef={onRef} />}
{maxLength && <MaxLengthPlugin maxLength={maxLength} />}
</>
Expand Down
35 changes: 35 additions & 0 deletions ui/src/editor/plugins/placeholder/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { HeadingNode } from '@lexical/rich-text';
import { mergeRegister } from '@lexical/utils';
import { useEffect } from 'react';

import '@/editor/plugins/placeholder/styles.css';

import {
$removePlaceholderFromParagraphNodes,
$setPlaceholderOnCreate,
$setPlaceholderOnSelectedParagraphNode,
} from '@/lib/utils/editor/placeholder';

export function PlaceholderPlugin() {
const [editor] = useLexicalComposerContext();

useEffect(() => {
return mergeRegister(
editor.registerMutationListener(HeadingNode, (nodes) => {
// Set placeholder to heading nodes on create
$setPlaceholderOnCreate(nodes, editor);
}),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
// Remove placeholder from all paragraph nodes
$removePlaceholderFromParagraphNodes(editor);
// Set placeholder for the currently selected paragraph node
$setPlaceholderOnSelectedParagraphNode(editor);
});
})
);
}, [editor]);

return null;
}
6 changes: 6 additions & 0 deletions ui/src/editor/plugins/placeholder/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.node-placeholder:has(br):not(:has(span))::before {
position: absolute;
content: attr(data-placeholder);
opacity: 0.5;
cursor: text;
}
6 changes: 6 additions & 0 deletions ui/src/lib/constants/editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const DATA_PLACEHOLDER = 'data-placeholder';
export const PLACEHOLDER_CLASS_NAME = 'node-placeholder';

export const PARAGRAPH_PLACEHOLDER = 'Type something...';
export const HEADING_PLACEHOLDER = 'Heading';
export const DEFAULT_PLACEHOLDER = 'Begin writing your review...';
File renamed without changes.
28 changes: 28 additions & 0 deletions ui/src/lib/utils/editor/nodes-of-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { EditorState, Klass, LexicalEditor, LexicalNode } from 'lexical';

// based on the implementation of $nodesOfType from '@lexical/utils'
function $forEachNodesOfType<T extends LexicalNode>(
editorState: EditorState,
klass: Klass<T>,
callback: (node: T) => void
) {
const readOnly = editorState._readOnly;
const klassType = klass.getType();
const nodes = editorState._nodeMap;
for (const node of nodes.values()) {
if (node instanceof klass && node.__type === klassType && (readOnly || node.isAttached())) {
callback(node as T);
}
}
}

export function $removeClassesFromNodesOfType<T extends LexicalNode>(
editor: LexicalEditor,
klass: Klass<T>,
...classNames: string[]
) {
$forEachNodesOfType(editor.getEditorState(), klass, (node) => {
const element = editor.getElementByKey(node.getKey());
element?.classList.remove(...classNames);
});
}
78 changes: 78 additions & 0 deletions ui/src/lib/utils/editor/placeholder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
$getRoot,
$getSelection,
$isParagraphNode,
$isRangeSelection,
ParagraphNode,
} from 'lexical';
import type { LexicalEditor, LexicalNode, NodeKey, NodeMutation } from 'lexical';

import {
DATA_PLACEHOLDER,
HEADING_PLACEHOLDER,
PARAGRAPH_PLACEHOLDER,
PLACEHOLDER_CLASS_NAME,
} from '@/lib/constants/editor';
import { $removeClassesFromNodesOfType } from '@/lib/utils/editor/nodes-of-type';

export function $removePlaceholderFromParagraphNodes(editor: LexicalEditor) {
return $removeClassesFromNodesOfType(editor, ParagraphNode, PLACEHOLDER_CLASS_NAME);
}

function $getPlaceholderText(element: HTMLElement) {
const tag = element.tagName;

if (tag.startsWith('H')) {
const level = tag.charAt(tag.length - 1);
return `${HEADING_PLACEHOLDER}${level}`;
}

if (tag === 'P') {
return PARAGRAPH_PLACEHOLDER;
}

return null;
}

function $setPlaceholder(nodeKey: string, editor: LexicalEditor) {
const element = editor.getElementByKey(nodeKey);

if (element === null) {
return;
}

const placeholder = $getPlaceholderText(element);

if (placeholder === null) {
return;
}

element.classList.add(PLACEHOLDER_CLASS_NAME);
element.setAttribute(DATA_PLACEHOLDER, placeholder);
}

export function $setPlaceholderOnCreate(nodes: Map<NodeKey, NodeMutation>, editor: LexicalEditor) {
for (const [key, mutation] of nodes) {
if (mutation === 'created') {
$setPlaceholder(key, editor);
}
}
}

// Set placeholder when the selected node is paragraph node and not the first child of the root node,
// Otherwise we use the default placeholder from the <RichTextPlugin />
function $shouldSetPlaceholder(node: LexicalNode) {
return $isParagraphNode(node) && node !== $getRoot().getFirstChild();
}

export function $setPlaceholderOnSelectedParagraphNode(editor: LexicalEditor) {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}

const targetNode = selection.anchor.getNode();
if ($shouldSetPlaceholder(targetNode)) {
$setPlaceholder(targetNode.getKey(), editor);
}
}

0 comments on commit ba52dd9

Please sign in to comment.