Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions EDITOR_SHORTCUTS.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Hotkeys and Shortcuts Guide

This guide provides an overview of the available hotkeys and shortcuts for the custom text editor. These hotkeys and shortcuts can help you speed up your content creation and editing process.

## Hotkeys

| Hotkey | Description |
|-----------------|------------------------------|
| `Tab` | Trigger shortcuts (see below)|
| `Backspace` | Select Previous word |
| `Meta+(1-6)` | Heading 1 - 6 |
| `Meta+b` | Bold |
| `Meta+i` | Italic |
| `Meta+shift+b` | Bold & Italic |
| `Meta+s` | Code Snippet |
| `Meta+shift+c` | Code Block |
| `Meta+Shift+.` | Block Quote |
| `Meta+u` | URL |
| `Meta+l` | Link |
| `Meta+Shift+i` | Image |

## How to Use Hotkeys

1. Press the meta key (windows key or mac cmd key) with the desired hotkey eg, cmd+1 to render #
2. You can also highlight the word and then use the hotkey combination and for this you can double click the word or phrase or press meta+backspace a few times to highlight the required selection of text.
3. For Links and images, select the text and then use the hotkey combination. You will be prompted for the url.


## Markdown Shortcuts

| Shortcut | Description | Example |
|----------|----------------------------------------|-------------------------------|
| `/link` | Create a link with text and URL | `[text](url)` |
| `/image` | Insert an image with alt text and URL | `![text](url)` |

## Custom Tag Shortcuts

| Shortcut | Description | Example |
|---------------|------------------------------------|-------------------------------|
| `/media` | Embed a media file with src | `{% media src="url" /%}` |
| `/youtube` | Embed a YouTube video with src | `{% youtube src="url" /%}` |
| `/codepen` | Embed a CodePen project with src | `{% codepen src="url" /%}` |
| `/codesandbox`| Embed a CodeSandbox project with src | `{% codesandbox src="url" /%}`|

## How to Use Shortcuts

1. Place the cursor where you want to insert the content.
2. Type the shortcut (e.g., `/link`).
3. Press the `Tab` key.
4. For Markdown shortcuts `/link` and `/image`, you'll be prompted to enter the text and URL.
5. For custom tag shortcuts, you'll be prompted to enter the URL for the `src` attribute.

The editor will automatically replace the shortcut with the corresponding content.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

### Editor Doc

To learn about the editor shortcuts and hotkeys you can check out this document

- [Markdoc Editor Hotkeys and Shortcus](/EDITOR_SHORTCUTS.MD)

## πŸ’₯ Issues

You are welcome to [open issues](https://github.com/codu-code/codu/issues/new/choose) to discuss ideas about improving our CodΓΊ. Enhancements are encouraged and appreciated.
35 changes: 35 additions & 0 deletions components/CustomTextareAutosize/CustomTextareaAutosize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { forwardRef, Ref, ForwardRefRenderFunction } from 'react';
import TextareaAutosize, { TextareaAutosizeProps } from 'react-textarea-autosize';

interface TextareaAutosizeWrapperProps extends TextareaAutosizeProps {
inputRef?: Ref<HTMLTextAreaElement>;
}

const TextareaAutosizeWrapper: ForwardRefRenderFunction<HTMLTextAreaElement, TextareaAutosizeWrapperProps> = (
props,
ref
) => {
const { inputRef, ...rest } = props;

const combinedRef = (node: HTMLTextAreaElement | null) => {
if (ref) {
if (typeof ref === 'function') {
ref(node);
} else {
(ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
}
}

if (inputRef) {
if (typeof inputRef === 'function') {
inputRef(node);
} else {
(inputRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
}
}
};

return <TextareaAutosize ref={combinedRef} {...rest} />;
};

export default forwardRef(TextareaAutosizeWrapper);
125 changes: 125 additions & 0 deletions markdoc/editor/hotkeys/hotkeys.markdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useCallback, useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';

export interface Hotkey {
key: string;
useShift: boolean;
markup: string;
type: string;
}

type Hotkeys = {
[name: string]: Hotkey;
};

// Define hotkeys, markup, and it is +metaKey or
export const hotkeys: Record<string, Hotkey> = {
'selectPrevious': {key: 'backspace', useShift: false, markup: '', type: "select"},
'heading 1': { key: '1', useShift: false, markup: '# ', type: "pre" },
'heading 2': { key: '2', useShift: false, markup: '## ', type: "pre" },
'heading 3': { key: '3', useShift: false, markup: '### ', type: "pre" },
'heading 4': { key: '4', useShift: false, markup: '#### ', type: "pre" },
'heading 5': { key: '5', useShift: false, markup: '##### ', type: "pre" },
'heading 6': { key: '6', useShift: false, markup: '###### ', type: "pre" },
'bold': { key: 'b', useShift: false, markup: '**', type: "wrap" },
'italic': { key: 'i', useShift: false, markup: '_', type: "wrap" },
'boldItalic': { key: 'b', useShift: true, markup: '***', type: "wrap" },
'codeSnippet': {key: 's', useShift: false, markup: '`', type: "wrap"},
'codeBlock': {key: 'c', useShift: true, markup: '```', type: "wrap"},
'blockQuote': {key: '.', useShift: true, markup: '>', type: "blockQuote"},
'link': { key: 'l', useShift: false, markup: '[text](url)', type: "linkOrImage" },
'image': { key: 'i', useShift: true, markup: '![text](url)', type: "linkOrImage" },
'url': { key: 'u', useShift: false, markup: '<>', type: "wrap" },
};



export const useMarkdownHotkeys = (textareaRef: React.RefObject<HTMLTextAreaElement>) => {

// Create a single callback for all hotkeys
const handleHotkey = useCallback((hotkey: Hotkey) => (e: KeyboardEvent) => {
e.preventDefault();
if (textareaRef.current) {
const textarea = textareaRef.current;
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const currentValue = textarea.value;
const { markup, type } = hotkey;
let newText;

switch (type) {

case "pre":
newText = `${markup}${currentValue.slice(startPos, endPos)}`;
break;

case "wrap":
// check for codeBlock, url then default wrap
if (hotkey.key === 'c' && hotkey.useShift) {
newText = `${markup}\n\n${markup}`;
} else if(hotkey.key === 'u'){
newText = `${markup[0]}${currentValue.slice(startPos, endPos)}${markup[1]}`;
}else {
newText = `${markup}${currentValue.slice(startPos, endPos)}${markup}`;
}
break;

case "blockQuote":
const lines = currentValue.slice(startPos, endPos).split('\n');
const quotedLines = lines.map(line => `${markup} ${line}`);
newText = quotedLines.join('\n');
break;

case "linkOrImage":
const selectedText = currentValue.slice(startPos, endPos);
if (!selectedText) return; // Do nothing if no text is selected

const url = prompt('Enter the URL:');
if (!url) return;

const tag = markup.replace('text', selectedText).replace('url', url);
textarea.value = `${currentValue.slice(0, startPos)}${tag}${currentValue.slice(endPos)}`;
const cursorPos = startPos + tag.length;
textarea.setSelectionRange(cursorPos, cursorPos);
return;


case "select":
let start = startPos - 1;

// Move left while the cursor is on whitespace
while (start >= 0 && /\s/.test(currentValue[start])) {
start--;
}

// Move left while the cursor is on non-whitespace
while (start >= 0 && /\S/.test(currentValue[start])) {
start--;
}

start++; // Move to the beginning of the word

// Trim right whitespace
let trimmedEnd = endPos;
while (/\s/.test(currentValue[trimmedEnd - 1])) {
trimmedEnd--;
}
textarea.setSelectionRange(start, trimmedEnd);
return;

default:
setSelectCount(0);
return;
}

textarea.value = `${currentValue.slice(0, startPos)}${newText}${currentValue.slice(endPos)}`;
const cursorPos = (type === "wrap" && hotkey.key === 'c' && hotkey.useShift) ? startPos + markup.length + 1 : startPos + newText.length;
textarea.setSelectionRange(cursorPos, cursorPos);
}
}, []);

// Map each hotkey to its corresponding callback
Object.values(hotkeys).forEach((hotkey) => {
useHotkeys(`${hotkey.key}${hotkey.useShift ? '+meta+shift' : '+meta'}`, handleHotkey(hotkey), { enableOnFormTags: true });
});
};
103 changes: 103 additions & 0 deletions markdoc/editor/shortcuts/shortcuts.markdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// useCustomShortcuts.tsx
import { useCallback } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { RefObject } from 'react';

type Shortcuts = {
[name: string]: string;
};

export const markdownShortcuts: Shortcuts = {
'/link': '[text](url)',
'/image': '![text](url)',
};

export const customTagsShortcuts: Shortcuts = {
'/media': '{% media src="" /%}',
'/youtube': '{% youtube src="" /%}',
'/codepen': '{% codepen src="" /%}',
'/codesandbox': '{% codesandbox src="" /%}',
};

const insertCustomTag = (
shortcut: string,
textarea: HTMLTextAreaElement
) => {
const tag = customTagsShortcuts[shortcut];
const currentValue = textarea.value;
const startPos = textarea.selectionStart;

textarea.value = `${currentValue.slice(
0,
startPos - shortcut.length
)}${tag}${currentValue.slice(startPos)}`;

const cursorPos = startPos + tag.length - shortcut.length;
textarea.setSelectionRange(cursorPos, cursorPos);
};

export const useMarkdownShortcuts = (
textareaRef: RefObject<HTMLTextAreaElement>
) => {
const handleShortcut = useCallback(
(e: KeyboardEvent) => {
const textarea = textareaRef.current;
const value = textarea.value;
const startPos = textarea.selectionStart;

for (const shortcut in customTagsShortcuts) {
if (value.slice(startPos - shortcut.length, startPos) === shortcut) {
e.preventDefault();

// Prompt for URL
const url = prompt('Enter the URL:');
if (!url) return;

const tag = customTagsShortcuts[shortcut].replace('src=""', `src="${url}"`);
const currentValue = textarea.value;

textarea.value = `${currentValue.slice(
0,
startPos - shortcut.length
)}${tag}${currentValue.slice(startPos)}`;
const cursorPos = startPos + tag.length - shortcut.length;
textarea.setSelectionRange(cursorPos, cursorPos);
break;
}
}

for (const shortcut in markdownShortcuts) {
if (value.slice(startPos - shortcut.length, startPos) === shortcut) {
e.preventDefault();
const tagTemplate = markdownShortcuts[shortcut];

if (shortcut === '/link' || shortcut === '/image') {
// Prompt for text
const text = prompt('Enter the text:');
if (!text) return;

// Prompt for URL
const url = prompt('Enter the URL:');
if (!url) return;

const tag = tagTemplate.replace('text', text).replace('url', url);
const currentValue = textarea.value;

textarea.value = `${currentValue.slice(
0,
startPos - shortcut.length
)}${tag}${currentValue.slice(startPos)}`;
const cursorPos = startPos + tag.length - shortcut.length;
textarea.setSelectionRange(cursorPos, cursorPos);
}
break;
}
}
},
[textareaRef]
);

useHotkeys('tab', handleShortcut, { enableOnFormTags: true });
};


18 changes: 17 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.34.0",
"react-hot-toast": "^2.3.0",
"react-hotkeys-hook": "^4.3.8",
"react-intersection-observer": "^9.4.1",
"react-query": "^3.39.2",
"react-textarea-autosize": "^8.3.4",
Expand Down
Loading