Skip to content

Commit

Permalink
feat: add support for indent/outdent
Browse files Browse the repository at this point in the history
  • Loading branch information
sibiraj-s committed May 13, 2024
1 parent 825791e commit 1dc5443
Show file tree
Hide file tree
Showing 14 changed files with 182 additions and 11 deletions.
2 changes: 1 addition & 1 deletion docs/src/content/docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class AppComponent implements OnInit, OnDestroy {
//[{ link: { showOpenInNewTab: false } }, 'image'],
['text_color', 'background_color'],
['align_left', 'align_center', 'align_right', 'align_justify'],
['horizontal_rule', 'format_clear'],
['horizontal_rule', 'format_clear', 'indent', 'outdent'],
];
colorPresets = ['red', '#FF0000', 'rgb(255, 0, 0)'];

Expand Down
74 changes: 66 additions & 8 deletions projects/ngx-editor/schema/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,40 @@ const paragraph: NodeSpec = {
align: {
default: null,
},
indent: {
default: null,
},
},
parseDOM: [
{
tag: 'p',
getAttrs(dom: HTMLElement): Record<string, any> {
getAttrs(dom: HTMLElement) {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
align,
indent,
};
},
},
],
toDOM(node): DOMOutputSpec {
const { align } = node.attrs;
const { align, indent } = node.attrs;

const styles: Partial<CSSStyleDeclaration> = {
textAlign: align !== 'left' ? align : null,
marginLeft: indent !== null ? `${indent * 40}px` : null,
};
const style = toStyleString(styles) || null;

return ['p', { style }, 0];
const attrs = {
style,
'data-indent': indent ?? null,
};

return ['p', attrs, 0];
},
};

Expand All @@ -52,9 +63,35 @@ const blockquote: NodeSpec = {
content: 'block+',
group: 'block',
defining: true,
parseDOM: [{ tag: 'blockquote' }],
toDOM(): DOMOutputSpec {
return ['blockquote', 0];
attrs: {
indent: {
default: null,
},
},
parseDOM: [
{
tag: 'blockquote',
getAttrs(dom: HTMLElement) {
const indent = dom.getAttribute('indent') || null;
return { indent };
},
},
],
toDOM(node): DOMOutputSpec {
const { indent } = node.attrs;

const styles: Partial<CSSStyleDeclaration> = {
marginLeft: indent !== null ? `${indent * 40}px` : null,
};

const style = toStyleString(styles) || null;

const attrs = {
style,
'data-indent': indent ?? null,
};

return ['blockquote', attrs, 0];
},
};

Expand All @@ -78,6 +115,9 @@ const heading: NodeSpec = {
align: {
default: null,
},
indent: {
default: null,
},
},
content: 'inline*',
group: 'block',
Expand All @@ -88,10 +128,12 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 1,
align,
indent,
};
},
},
Expand All @@ -100,10 +142,12 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 2,
align,
indent,
};
},
},
Expand All @@ -112,10 +156,12 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 3,
align,
indent,
};
},
},
Expand All @@ -124,10 +170,12 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 4,
align,
indent,
};
},
},
Expand All @@ -136,10 +184,12 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 5,
align,
indent,
};
},
},
Expand All @@ -148,23 +198,31 @@ const heading: NodeSpec = {
getAttrs(dom: HTMLElement): Record<string, any> {
const { textAlign } = dom.style;
const align = dom.getAttribute('align') || textAlign || null;
const indent = dom.getAttribute('indent') || null;

return {
level: 6,
align,
indent,
};
},
},
],
toDOM(node): DOMOutputSpec {
const { level, align } = node.attrs;
const { level, align, indent } = node.attrs;

const styles: Partial<CSSStyleDeclaration> = {
textAlign: align !== 'left' ? align : null,
marginLeft: indent !== null ? `${indent * 40}px` : null,
};
const style = toStyleString(styles) || null;

return [`h${level}`, { style }, 0];
const attrs = {
style,
'data-indent': indent ?? null,
};

return [`h${level}`, attrs, 0];
},
};

Expand Down
14 changes: 14 additions & 0 deletions projects/ngx-editor/src/lib/EditorCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import HeadingCommand, { HeadingLevels } from './commands/Heading';
import ImageCommand, { ImageAttrs } from './commands/Image';
import TextColorCommand from './commands/TextColor';
import TextAlignCommand, { Align } from './commands/TextAlign';
import IndentCommand from './commands/Indent';

import { HTML } from './trustedTypesUtil';
import { isString } from './stringUtil';

Expand Down Expand Up @@ -241,6 +243,18 @@ class EditorCommands {

return this;
}

indent(): this {
const command = new IndentCommand('increase');
command.insert()(this.state, this.dispatch);
return this;
}

outdent(): this {
const command = new IndentCommand('decrease');
command.insert()(this.state, this.dispatch);
return this;
}
}

export default EditorCommands;
2 changes: 2 additions & 0 deletions projects/ngx-editor/src/lib/Locals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const defaults: Record<string, string | Observable<string>> = {
insertLink: 'Insert Link',
removeLink: 'Remove Link',
insertImage: 'Insert Image',
indent: 'Increase Indent',
outdent: 'Decrease Indent',

// pupups, forms, others...
url: 'URL',
Expand Down
76 changes: 76 additions & 0 deletions projects/ngx-editor/src/lib/commands/Indent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { EditorState, Transaction, Command } from 'prosemirror-state';

import { clamp } from 'ngx-editor/utils';
import { InsertCommand } from './types';

const indentNodeTypes = ['paragraph', 'heading', 'blockquote'];

type IndentMethod = 'increase' | 'decrease';
const minIndent = 0;
const maxIndent = 10;

const udpateIndentLevel = (tr: Transaction, pos: number, method: IndentMethod): boolean => {
const node = tr.doc.nodeAt(pos);
if (!node) { return false; }

const nodeIndent = node.attrs['indent'] ?? 0;
const newIndent = clamp(nodeIndent + (method === 'increase' ? 1 : -1), minIndent, maxIndent);

if (newIndent === nodeIndent || newIndent < minIndent || newIndent > maxIndent) {
return false;
}

const attrs = {
...node.attrs,
indent: newIndent,
};

tr.setNodeMarkup(pos, node.type, attrs);
return true;
};

class Indent implements InsertCommand {
method: IndentMethod = 'increase';

constructor(method: IndentMethod) {
this.method = method;
}

insert(): Command {
return (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
const { tr, doc } = state;
const { from, to } = tr.selection;

let applicable = false;

doc.nodesBetween(from, to, (node, pos) => {
const nodeType = node.type;

if (indentNodeTypes.includes(nodeType.name)) {
applicable = udpateIndentLevel(tr, pos, this.method);
return false;
} else if (node.type.name.includes('list')) {
return false;
}

return true;
});

if (!applicable) {
return false;
}

if (tr.docChanged) {
dispatch?.(tr);
}

return true;
};
}

canExecute(state: EditorState): boolean {
return this.insert()(state);
}
}

export default Indent;
3 changes: 3 additions & 0 deletions projects/ngx-editor/src/lib/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Link from './Link';
import Image from './Image';
import TextColor from './TextColor';
import FormatClear from './FormatClear';
import Indent from './Indent';

export const STRONG = new Mark('strong');
export const EM = new Mark('em');
Expand All @@ -33,3 +34,5 @@ export const LINK = new Link();
export const IMAGE = new Image();
export const TEXT_COLOR = new TextColor('text_color', 'color');
export const TEXT_BACKGROUND_COLOR = new TextColor('text_background_color', 'backgroundColor');
export const INDENT = new Indent('increase');
export const OUTDENT = new Indent('decrease');
1 change: 1 addition & 0 deletions projects/ngx-editor/src/lib/icons/indent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default '<path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 21h18v-2H3v2zM3 8v8l4-4-4-4zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z"/>';
4 changes: 4 additions & 0 deletions projects/ngx-editor/src/lib/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import textColor from './text_color';
import colorFill from './color_fill';
import horizontalRule from './horizontal_rule';
import formatClear from './format_clear';
import indent from './indent';
import outdent from './outdent';

const DEFAULT_ICON_HEIGHT = 20;
const DEFAULT_ICON_WIDTH = 20;
Expand All @@ -45,6 +47,8 @@ export const icons: Record<string, any> = {
color_fill: colorFill,
horizontal_rule: horizontalRule,
format_clear: formatClear,
indent,
outdent,
path: '<path></path>',
};

Expand Down
1 change: 1 addition & 0 deletions projects/ngx-editor/src/lib/icons/outdent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default '<path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 17h10v-2H11v2zm-8-5l4 4V8l-4 4zm0 9h18v-2H3v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z"/>';
2 changes: 2 additions & 0 deletions projects/ngx-editor/src/lib/modules/menu/MenuCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const ToggleCommands: Record<string, ToggleCommand> = {
export const InsertCommands: Record<string, InsertCommand> = {
horizontal_rule: Commands.HORIZONTAL_RULE,
format_clear: Commands.FORMAT_CLEAR,
indent: Commands.INDENT,
outdent: Commands.OUTDENT,
};

export const Link = Commands.LINK;
Expand Down
4 changes: 3 additions & 1 deletion projects/ngx-editor/src/lib/modules/menu/menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const TOOLBAR_FULL: Toolbar = [
['link', 'image'],
['text_color', 'background_color'],
['align_left', 'align_center', 'align_right', 'align_justify'],
['horizontal_rule'],
['horizontal_rule', 'format_clear', 'indent', 'outdent'],
];

const DEFAULT_COLOR_PRESETS = [
Expand Down Expand Up @@ -91,6 +91,8 @@ export class MenuComponent implements OnInit {
insertCommands: ToolbarItem[] = [
'horizontal_rule',
'format_clear',
'indent',
'outdent',
];

iconContainerClass = ['NgxEditor__MenuItem', 'NgxEditor__MenuItem--Icon'];
Expand Down
4 changes: 3 additions & 1 deletion projects/ngx-editor/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export type TBItems = 'bold'
| 'align_right'
| 'align_justify'
| 'horizontal_rule'
| 'format_clear';
| 'format_clear'
| 'indent'
| 'outdent';

export type ToolbarDropdown = { heading?: TBHeadingItems[] };
export type ToolbarLinkOptions = Partial<LinkOptions>;
Expand Down
5 changes: 5 additions & 0 deletions projects/ngx-editor/utils/clamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const clamp = (value: number, min: number, max: number): number => {
return Math.min(Math.max(value, min), max);
};

export default clamp;
1 change: 1 addition & 0 deletions projects/ngx-editor/utils/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as isNil } from './isNil';
export { default as toStyleString } from './toStyleString';
export { default as NgxEditorError } from './error';
export { default as uniq } from './uniq';
export { default as clamp } from './clamp';

0 comments on commit 1dc5443

Please sign in to comment.