Skip to content

Commit

Permalink
Exporting/importing Yoopta content in different formats html, md, text (
Browse files Browse the repository at this point in the history
#187)

* Published @yoopta/exports with import/export in the next formats: plain text, html, markdown
* Added copy/cut/pasting functionality
* Added docs for some plugins
  • Loading branch information
Darginec05 authored Jun 25, 2024
1 parent cd8c25f commit 403bc2c
Show file tree
Hide file tree
Showing 100 changed files with 3,655 additions and 1,320 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

[![RepoRater](https://repo-rater.eddiehub.io/api/badge?owner=Darginec05&name=Yoopta-Editor)](https://repo-rater.eddiehub.io/rate?owner=Darginec05&name=Yoopta-Editor)
![npm](https://img.shields.io/npm/v/@yoopta/editor)
![license](https://img.shields.io/npm/l/@yoopta/editor)
![downloads](https://img.shields.io/npm/dm/@yoopta/editor)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/Darginec05)

Expand Down Expand Up @@ -35,7 +34,7 @@ All of this is customizable, extensible, and easy to set up!
- Indent and outdent for every plugin by tabs and shift+tabs
- Editor instance to programmatically control your content
- Editor events for saving to DB in real-time
- Exports in markdown, plain text, html - [in progress. Currently available only HTML exports]
- Exports in markdown, html, plain text
- Shortcuts, hotkeys. And customization for this!
- Super AI tools not for HYPE, but for real useful work with editor content - [in progress]
- The soul invested in the development of this editor 💙
Expand All @@ -46,6 +45,7 @@ All of this is customizable, extensible, and easy to set up!
- Core

- [**@yoopta/editor**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/core/editor/README.md)
- [**@yoopta/exports**](https://github.com/Darginec05/Yoopta-Editor/blob/master/packages/core/exports/README.md)

- Plugins

Expand Down
8 changes: 1 addition & 7 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"packages": [
"packages/plugins/*",
"packages/tools/*",
"packages/marks/*",
"packages/core/*",
"packages/development"
],
"packages": ["packages/plugins/*", "packages/tools/*", "packages/marks", "packages/core/*", "packages/development"],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"workspaces": [
"packages/plugins/*",
"packages/tools/*",
"packages/marks/*",
"packages/marks",
"packages/core/*",
"packages/development"
],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/editor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@yoopta/editor",
"version": "4.5.1",
"version": "4.5.2-rc.3",
"license": "MIT",
"private": false,
"main": "dist/index.js",
Expand Down
91 changes: 54 additions & 37 deletions packages/core/editor/src/components/Editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CSSProperties, ReactNode, useEffect, useRef } from 'react';
import { ClipboardEvent, CSSProperties, ReactNode, useEffect, useRef } from 'react';
import { useYooptaEditor, useYooptaReadOnly } from '../../contexts/YooptaContext/YooptaContext';
import { RenderBlocks } from './RenderBlocks';
import { YooptaMark } from '../../marks';
Expand All @@ -12,6 +12,7 @@ import { ReactEditor } from 'slate-react';
import { YooptaBlockPath } from '../../editor/types';
import { useRectangeSelectionBox } from '../SelectionBox/hooks';
import { SelectionBox } from '../SelectionBox/SelectionBox';
import { serializeHTML } from '../../parsers/serializeHTML';

type Props = {
marks?: YooptaMark<any>[];
Expand All @@ -27,7 +28,7 @@ type Props = {
const getEditorStyles = (styles: CSSProperties) => ({
...styles,
width: styles.width || 400,
paddingBottom: styles.paddingBottom || 100,
paddingBottom: typeof styles.paddingBottom === 'number' ? styles.paddingBottom : 100,
});

type State = {
Expand Down Expand Up @@ -120,12 +121,29 @@ const Editor = ({
state.selectionStarted = false;
};

const onClick = (event: React.MouseEvent) => {
const onMouseDown = (event: React.MouseEvent) => {
if (isReadOnly) return;

// [TODO] - handle shift+click
// if (event.shiftKey) {
// }
if (event.shiftKey) {
const currentSelectionIndex = editor.selection;
if (!currentSelectionIndex) return;

const targetBlock = (event.target as HTMLElement).closest('div[data-yoopta-block]');
const targetBlockId = targetBlock?.getAttribute('data-yoopta-block-id') || '';
const targetBlockIndex = editor.children[targetBlockId]?.meta.order;
if (typeof targetBlockIndex !== 'number') return;

const indexesBetween = Array.from({ length: Math.abs(targetBlockIndex - currentSelectionIndex[0]) }).map(
(_, index) =>
targetBlockIndex > currentSelectionIndex[0]
? currentSelectionIndex[0] + index + 1
: currentSelectionIndex[0] - index - 1,
);

editor.blur();
editor.setBlockSelected([currentSelectionIndex[0], ...indexesBetween], { only: true });
return;
}

resetSelectionState();
handleEmptyZoneClick(event);
Expand All @@ -142,39 +160,9 @@ const Editor = ({
resetSelectedBlocks();
};

// [TODO] - implement with @yoopta/exports
const onCopy = (event: React.ClipboardEvent) => {
// function escapeHtml(text) {
// const map = {
// '&': '&amp;',
// '<': '&lt;',
// '>': '&gt;',
// '"': '&quot;',
// "'": '&#039;',
// };
// return text.replace(/[&<>"']/g, (m) => map[m]);
// }
// function serializeNode(node, plugins) {
// if (Text.isText(node)) {
// return escapeHtml(node.text);
// }
// const children = node.children.map((node) => serializeNode(node, plugins)).join('');
// const plugin = plugins[node.type];
// if (typeof plugin.exports?.html?.serialize === 'function') {
// return plugin.exports.html.serialize(node, children);
// }
// return children;
// }
// export function serializeHtml(data: Descendant[], pluginsMap) {
// const html = data.map((node) => serializeNode(node, pluginsMap)).join('');
// return html;
// }
};

const onKeyDown = (event) => {
if (isReadOnly) return;

// [TODO] - handle shift+click?
if (HOTKEYS.isSelect(event)) {
const isAllBlocksSelected = editor.selectedBlocks?.length === Object.keys(editor.children).length;

Expand All @@ -190,6 +178,35 @@ const Editor = ({
}
}

if (HOTKEYS.isCopy(event) || HOTKEYS.isCut(event)) {
if (Array.isArray(editor.selectedBlocks) && editor.selectedBlocks.length > 0) {
event.preventDefault();

const htmlString = serializeHTML(editor, editor.getEditorValue());
const blob = new Blob([htmlString], { type: 'text/html' });

const item = new ClipboardItem({ 'text/html': blob });

navigator.clipboard.write([item]).then(() => {
const html = new DOMParser().parseFromString(htmlString, 'text/html');
console.log('HTML copied\n', html.body);
});

if (HOTKEYS.isCut(event)) {
const isAllBlocksSelected = editor.selectedBlocks.length === Object.keys(editor.children).length;

editor.deleteBlocks({ paths: editor.selectedBlocks, focus: false });
editor.setBlockSelected(null);
resetSelectionState();

if (isAllBlocksSelected) {
editor.insertBlock(buildBlockData({ id: generateId() }), { at: [0], focus: true });
}
}
return;
}
}

if (HOTKEYS.isBackspace(event)) {
event.stopPropagation();

Expand Down Expand Up @@ -348,7 +365,7 @@ const Editor = ({
className={className ? `yoopta-editor ${className}` : 'yoopta-editor'}
style={editorStyles}
ref={yooptaEditorRef}
onClick={onClick}
onMouseDown={onMouseDown}
onBlur={onBlur}
>
<RenderBlocks editor={editor} marks={marks} placeholder={placeholder} />
Expand Down
6 changes: 3 additions & 3 deletions packages/core/editor/src/editor/blocks/increaseBlockDepth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { findPluginBlockBySelectionPath } from '../../utils/findPluginBlockBySel
import { YooEditor, YooptaEditorTransformOptions } from '../types';

export function increaseBlockDepth(editor: YooEditor, options: YooptaEditorTransformOptions = {}) {
const { at = editor.selection } = options;
const { at = editor.selection, blockId = '' } = options;

if (!at) return;
if (!blockId && !at) return;
editor.children = createDraft(editor.children);

const block = findPluginBlockBySelectionPath(editor);
const block = editor.children[blockId] || findPluginBlockBySelectionPath(editor);
if (!block) return;

block.meta.depth = block.meta.depth + 1;
Expand Down
1 change: 1 addition & 0 deletions packages/core/editor/src/editor/blocks/insertBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function insertBlocks(editor: YooEditor, blocks: YooptaBlockData[], optio
}

editor.children = finishDraft(editor.children);

editor.applyChanges();
editor.emit('change', editor.children);

Expand Down
2 changes: 0 additions & 2 deletions packages/core/editor/src/editor/blocks/updateBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ export function updateBlock<TElementKeys extends string, TProps>(
const block = editor.children[blockId];

if (!block) {
// [TODO] - some weird behaviour when copy/paste
console.log(`Block with id ${blockId} not found`);
return;
}

Expand Down
8 changes: 5 additions & 3 deletions packages/core/editor/src/editor/core/blur.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export type EditorBlurOptions = Pick<YooptaEditorTransformOptions, 'slate'> & {
};

function blurFn(editor: YooEditor, slate: SlateEditor) {
ReactEditor.blur(slate);
ReactEditor.deselect(slate);
Transforms.deselect(slate);
try {
ReactEditor.blur(slate);
ReactEditor.deselect(slate);
Transforms.deselect(slate);
} catch (error) {}

editor.setBlockSelected(null);
editor.setSelection(null);
Expand Down
1 change: 1 addition & 0 deletions packages/core/editor/src/editor/core/setEditorValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export function setEditorValue(editor: YooEditor, value: YooptaContentValue) {
editor.blockEditorsMap = buildBlockSlateEditors(editor);

editor.applyChanges();
editor.emit('change', editor.children);
}
4 changes: 2 additions & 2 deletions packages/core/editor/src/editor/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ export type YooptaBlockData<T = Descendant | SlateElement> = {
meta: YooptaBlockBaseMeta;
};

export type YooptaContentValue = Record<string, YooptaBlockData>;

export type YooptaBlockBaseMeta = {
order: number;
depth: number;
};

export type YooptaContentValue = Record<string, YooptaBlockData>;

export type SlateEditor = Editor;

// add 'end' | 'start'
Expand Down
10 changes: 5 additions & 5 deletions packages/core/editor/src/handlers/onKeyDown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { isKeyHotkey } from 'is-hotkey';
import { Editor, Path, Point, Range, Text, Transforms } from 'slate';
import { ReactEditor } from 'slate-react';
import { buildBlockData } from '../components/Editor/utils';
import { Elements } from '../editor/elements';
import { SlateEditor, YooEditor, YooptaBlockPath } from '../editor/types';
import { findPluginBlockBySelectionPath } from '../utils/findPluginBlockBySelectionPath';
import { findSlateBySelectionPath } from '../utils/findSlateBySelectionPath';
Expand Down Expand Up @@ -81,8 +80,6 @@ export function onKeyDown(editor: YooEditor) {
if (HOTKEYS.isBackspace(event)) {
if (event.isDefaultPrevented()) return;

const blockData = findPluginBlockBySelectionPath(editor, { at: editor.selection });
const block = editor.blocks[blockData?.type || ''];
const parentPath = Path.parent(slate.selection.anchor.path);
const isStart = Editor.isStart(slate, slate.selection.anchor, parentPath);

Expand Down Expand Up @@ -160,8 +157,11 @@ export function onKeyDown(editor: YooEditor) {
const fullRange = Editor.range(slate, firstElementPath, lastElementPath);
const isAllBlockElementsSelected = Range.equals(slate.selection, fullRange);

// [TODO] - handle cases for void node elements and when string is empty
if (Range.isExpanded(slate.selection) && isAllBlockElementsSelected) {
const string = Editor.string(slate, fullRange);
const isElementEmpty = string.trim().length === 0;

// [TODO] - handle cases for void node elements
if ((Range.isExpanded(slate.selection) && isAllBlockElementsSelected) || isElementEmpty) {
event.preventDefault();

ReactEditor.blur(slate);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export {
PluginElementRenderProps,
PluginEventHandlerOptions,
PluginCustomEditorRenderProps,
PluginDeserializeParser,
PluginserializeParser,
YooptaMarkProps,
} from './plugins/types';

Expand Down
Loading

0 comments on commit 403bc2c

Please sign in to comment.