Skip to content

Commit

Permalink
add debug logging
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonas-C committed Jan 17, 2025
1 parent a2f5c69 commit b27f826
Show file tree
Hide file tree
Showing 17 changed files with 209 additions and 58 deletions.
11 changes: 7 additions & 4 deletions packages/editor/src/core/createPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
import type { SlatePluginFn } from ".";

export const createPlugin: SlatePluginFn =
({ isInline: isInlineProp, shortcuts, isVoid: isVoidProp, type, normalize, transform }) =>
({ isInline: isInlineProp, name, shortcuts, isVoid: isVoidProp, type, normalize, transform }) =>
(editor) => {
const logger = editor.logger.getLogger(name);
const { isInline, isVoid } = editor;
editor.isInline = (element) => {
if (element.type === type) {
Expand All @@ -28,7 +29,7 @@ export const createPlugin: SlatePluginFn =
const { normalizeNode } = editor;
editor.normalizeNode = (entry, options) => {
const [node, path] = entry;
const res = normalize?.(editor, node, path, options);
const res = normalize?.(editor, node, path, logger);
if (res) {
return;
}
Expand All @@ -41,10 +42,12 @@ export const createPlugin: SlatePluginFn =
if (shortcutEntries.length) {
const { onKeyDown } = editor;
editor.onKeyDown = (event) => {
for (const [_key, { handler, keyCondition }] of shortcutEntries) {
for (const [key, { handler, keyCondition }] of shortcutEntries) {
const keyConditions = Array.isArray(keyCondition) ? keyCondition : [keyCondition];
if (keyConditions.some((condition) => condition(event))) {
if (handler(editor, event)) {
logger.log(`Shortcut "${key}" triggered.`);
if (handler(editor, event, logger)) {
logger.log(`Shortcut "${key}" consumed keyDown event. Ignoring further handlers.`);
return;
}
}
Expand Down
13 changes: 10 additions & 3 deletions packages/editor/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,32 @@
*
*/

import type { BaseEditor, Editor, Element, Node, Path } from "slate";
import type { Editor, Element, Node, Path } from "slate";
import type { KeyboardEventLike } from "is-hotkey";
import type { KeyboardEvent } from "react";

type KeyConditionFn = (event: KeyboardEventLike) => boolean;

export type ShortcutHandler = (editor: Editor, event: KeyboardEvent<HTMLDivElement>) => boolean;
export type ShortcutHandler = (editor: Editor, event: KeyboardEvent<HTMLDivElement>, logger: Logger) => boolean;

export type SlateExtensionFn = (editor: Editor) => Editor;

export interface Logger {
log: (...args: any[]) => void;
}

export interface Shortcut {
handler: ShortcutHandler;
keyCondition: KeyConditionFn | KeyConditionFn[];
}

interface SlateCreatePluginProps<TType extends Element["type"]> {
name: string;
isVoid?: boolean;
isInline?: boolean;
type?: TType;
shortcuts?: Record<string, Shortcut>;
normalize?: (editor: Editor, node: Node, path: Path, options?: Parameters<BaseEditor["normalizeNode"]>[1]) => boolean;
normalize?: (editor: Editor, node: Node, path: Path, logger: Logger) => boolean;
transform?: (editor: Editor) => Editor;
}

Expand Down
44 changes: 44 additions & 0 deletions packages/editor/src/editor/logger/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { Logger } from "../../core";

export interface LoggerOptions {
debug?: boolean;
}

const DEFAULT_LOGGER = "default";

export class LoggerManager {
#loggers: Map<string, Logger>;
#debug: boolean;
constructor(options?: LoggerOptions) {
this.#loggers = new Map();
this.#loggers.set(DEFAULT_LOGGER, this.createLogger(DEFAULT_LOGGER));
this.#debug = !!options?.debug;
}

getLogger(pluginName: string = DEFAULT_LOGGER) {
if (this.#loggers.has(pluginName)) {
return this.#loggers.get(pluginName)!;
}

const logger = this.createLogger(pluginName);
this.#loggers.set(pluginName, logger);
return logger;
}

private createLogger(pluginName: string): Logger {
return {
// eslint-disable-next-line no-console
log: (...args: any[]) => this.#debug && console.log(`[${pluginName}]:`, ...args),
};
}
}

export const loggerManager = new LoggerManager();
15 changes: 15 additions & 0 deletions packages/editor/src/editor/logger/withLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2025-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type { Editor } from "slate";
import { loggerManager, type LoggerManager } from "./Logger";

export const withLogger = (editor: Editor, logger: LoggerManager = loggerManager) => {
editor.logger = logger;
return editor;
};
75 changes: 40 additions & 35 deletions packages/editor/src/playground.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
*/

import { useState } from "react";
import { createEditor, type Descendant } from "slate";
import { withHistory } from "slate-history";
import { Editable, Slate, withReact } from "slate-react";
import { type Descendant } from "slate";
import { Editable, Slate } from "slate-react";
import type { Meta, StoryFn } from "@storybook/react";
import { OrderedList, UnOrderedList } from "@ndla/primitives";
import type { SlatePlugin } from "./core";
import { ListAlphabetical, ListOrdered, ListUnordered } from "@ndla/icons";
import { IconButton, OrderedList, UnOrderedList, type IconButtonProps } from "@ndla/primitives";
import { useListToolbarButton, useListToolbarButtonState } from "./plugins/list/hooks/useListToolbarButton";
import { listPlugin } from "./plugins/list/listPlugin";
import type { ListType } from "./plugins/list/listTypes";
import { markPlugin } from "./plugins/mark/markPlugin";
import { softBreakPlugin } from "./plugins/softBreak/softBreakPlugin";
import { withPlugins } from "./utils/createSlate";
import { createSlate } from "./utils/createSlate";

export default {
title: "Editor/Playground",
Expand All @@ -25,11 +26,6 @@ export default {
},
} as Meta;

const createSlate = ({ plugins }: { plugins: SlatePlugin[] }) => {
const editor = withPlugins(withHistory(withReact(createEditor())), plugins);
return editor;
};

const initialValue: Descendant[] = [
{
type: "section",
Expand All @@ -46,30 +42,6 @@ const initialValue: Descendant[] = [
type: "paragraph",
children: [{ text: "A line of text in a paragraph." }],
},
// {
// type: "list",
// listType: "numbered-list",
// data: {},
// children: [
// { type: "list-item", children: [{ type: "paragraph", children: [{ text: "Item 1" }] }] },
// {
// type: "list-item",
// children: [
// {
// type: "paragraph",
// children: [{ text: "Item 2" }],
// },
// {
// type: "list",
// data: {},
// listType: "numbered-list",
// children: [{ type: "list-item", children: [{ type: "paragraph", children: [{ text: "nested item" }] }] }],
// },
// ],
// },
// // { type: "list-item", children: [{ type: "paragraph", children: [{ text: "Item 3" }] }] },
// ],
// },
{
type: "paragraph",
children: [{ text: "A line of text in a paragraph." }],
Expand All @@ -78,15 +50,48 @@ const initialValue: Descendant[] = [
},
];

interface ListToolbarButtonProps extends IconButtonProps {
listType: ListType;
}

const ListToolbarButton = ({ listType, ...rest }: ListToolbarButtonProps) => {
const state = useListToolbarButtonState({ type: listType });
const toolbarButton = useListToolbarButton(state);
return <IconButton size="small" variant="secondary" {...toolbarButton.props} {...rest} />;
};

export const ToolbarButtons = () => {
return (
<>
<ListToolbarButton listType="bulleted-list">
<ListUnordered />
</ListToolbarButton>
<ListToolbarButton listType="numbered-list">
<ListOrdered />
</ListToolbarButton>
<ListToolbarButton listType="letter-list">
<ListAlphabetical />
</ListToolbarButton>
</>
);
};

export const EditorPlayground: StoryFn = () => {
const [editor] = useState(() => createSlate({ plugins: [markPlugin, listPlugin, softBreakPlugin] }));
return (
<Slate editor={editor} initialValue={initialValue}>
<ToolbarButtons></ToolbarButtons>
<Editable
onKeyDown={editor.onKeyDown}
renderElement={({ element, children, attributes }) => {
if (element.type === "list" && element.listType === "numbered-list") {
return <OrderedList {...attributes}>{children}</OrderedList>;
} else if (element.type === "list" && element.listType === "letter-list") {
return (
<OrderedList variant="letters" {...attributes}>
{children}
</OrderedList>
);
} else if (element.type === "list") {
return <UnOrderedList {...attributes}>{children}</UnOrderedList>;
} else if (element.type === "list-item") {
Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/plugins/link/linkPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const linkSerializer: SlateSerializer = {

export const linkPlugin = createPlugin({
type: LINK_ELEMENT_TYPE,
name: LINK_ELEMENT_TYPE,
isInline: true,
normalize: (editor, node, path) => {
if (!isElementOfType(node, LINK_ELEMENT_TYPE)) return false;
Expand Down
3 changes: 2 additions & 1 deletion packages/editor/src/plugins/list/handlers/listOnBackspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getCurrentBlock } from "../../../queries/getCurrentBlock";

// This function only has one purpose: to remove a list item when the user presses backspace at the start of a list item.
// Can probably be simplified further.
export const listOnBackspace: ShortcutHandler = (editor, event) => {
export const listOnBackspace: ShortcutHandler = (editor, event, logger) => {
if (!editor.selection || !Range.isCollapsed(editor.selection) || !hasNodeOfType(editor, LIST_ELEMENT_TYPE))
return false;

Expand All @@ -26,6 +26,7 @@ export const listOnBackspace: ShortcutHandler = (editor, event) => {
// If cursor is placed at start of first item child
if (Point.equals(Range.start(editor.selection), editor.start(currentItemPath.concat(0)))) {
event.preventDefault();
logger.log("Backspace at start of list item, lifting.");
Transforms.liftNodes(editor, { at: currentItemPath });
return true;
}
Expand Down
12 changes: 8 additions & 4 deletions packages/editor/src/plugins/list/handlers/listOnEnter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { defaultListItemBlock } from "../listBlocks";
import { isParagraphElement } from "../../paragraph/queries/paragraphElementQueries";
import { isListItemElement } from "../queries/listElementQueries";

export const listOnEnter: ShortcutHandler = (editor, event) => {
export const listOnEnter: ShortcutHandler = (editor, event, logger) => {
if (event.shiftKey || !editor.selection) return false;

const [firstEntry, secondEntry] = getEditorAncestors(editor, true);
Expand All @@ -32,14 +32,14 @@ export const listOnEnter: ShortcutHandler = (editor, event) => {
}
event.preventDefault();

// If selection is expanded, delete selected content first.
// Selection should now be collapsed
if (Range.isExpanded(editor.selection)) {
logger.log("Selection is expanded, deleting selected content.");
editor.deleteFragment();
// Selection should now be collapsed
}

// If list-item is empty, remove list item and jump out of list.
if (Node.string(selectedDefinitionItem) === "" && selectedDefinitionItem.children.length === 1) {
logger.log("List item is empty, removing list item and jumping out of list");
editor.withoutNormalizing(() => {
Transforms.unwrapNodes(editor, {
at: selectedDefinitionItemPath,
Expand All @@ -60,6 +60,7 @@ export const listOnEnter: ShortcutHandler = (editor, event) => {
const listItemEnd = editor.end(selectedDefinitionItemPath);
if (Point.equals(listItemEnd, editor.selection.anchor)) {
const nextPath = Path.next(selectedDefinitionItemPath);
logger.log("Enter at end of list item, inserting new list item.");
Transforms.insertNodes(
editor,
// TODO: Update children
Expand All @@ -73,6 +74,7 @@ export const listOnEnter: ShortcutHandler = (editor, event) => {
// If at the start of list-item, insert a new list item at current path
const listItemStart = editor.start(selectedDefinitionItemPath);
if (Point.equals(listItemStart, editor.selection.anchor)) {
logger.log("Enter at start of list item, inserting new list item.");
Transforms.insertNodes(
editor,
// TODO: Update children
Expand All @@ -81,6 +83,8 @@ export const listOnEnter: ShortcutHandler = (editor, event) => {
);
return true;
}

logger.log("Enter in the middle of list item, splitting list item.");
// Split current listItem at selection.
Transforms.splitNodes(editor, { match: isListItemElement, mode: "lowest" });
Transforms.select(editor, editor.start(Path.next(selectedDefinitionItemPath)));
Expand Down
5 changes: 4 additions & 1 deletion packages/editor/src/plugins/list/handlers/listOnTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { isElementOfType } from "../../../utils/isElementType";
import { defaultListBlock } from "../listBlocks";
import { isListElement, isListItemElement } from "../queries/listElementQueries";

export const listOnTab: ShortcutHandler = (editor, event) => {
export const listOnTab: ShortcutHandler = (editor, event, logger) => {
if (!editor.selection || !hasNodeOfType(editor, LIST_ELEMENT_TYPE)) return false;

const listEntry = getCurrentBlock(editor, LIST_ELEMENT_TYPE);
Expand Down Expand Up @@ -53,6 +53,7 @@ export const listOnTab: ShortcutHandler = (editor, event) => {
if (!isListItemElement(parentNode)) {
const childList = currentItemNode.children[currentItemNode.children.length - 1];
if (isListElement(childList) && childList.listType !== currentListNode.listType) {
logger.log("Lifting list element out of current list.");
Transforms.setNodes(
editor,
{ listType: currentListNode.listType },
Expand Down Expand Up @@ -135,9 +136,11 @@ export const listOnTab: ShortcutHandler = (editor, event) => {
const [lastNode, lastNodePath] = editor.node([...previousPath, previousNode.children.length - 1]);
// If previous list item has a sublist, move current item inside it.
if (isListElement(lastNode)) {
logger.log("Moving current list item into sublist of previous list item");
Transforms.moveNodes(editor, { at: currentItemPath, to: [...lastNodePath, lastNode.children.length] });
// Wrap current item inside a new list and move the new list to the previous list item.
} else {
logger.log("Wrapping current list item inside a new list and moving it to the previous list item");
Transforms.wrapNodes(editor, defaultListBlock(currentListNode.listType), { at: currentItemPath });
Transforms.moveNodes(editor, { at: currentItemPath, to: [...previousPath, previousNode.children.length] });
}
Expand Down
Loading

0 comments on commit b27f826

Please sign in to comment.