diff --git a/docs/src/content/docs/menu.md b/docs/src/content/docs/menu.md index 63e5932f..ca3c630d 100644 --- a/docs/src/content/docs/menu.md +++ b/docs/src/content/docs/menu.md @@ -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)']; diff --git a/projects/ngx-editor/schema/nodes.ts b/projects/ngx-editor/schema/nodes.ts index 6aaf61c9..27768674 100644 --- a/projects/ngx-editor/schema/nodes.ts +++ b/projects/ngx-editor/schema/nodes.ts @@ -21,29 +21,40 @@ const paragraph: NodeSpec = { align: { default: null, }, + indent: { + default: null, + }, }, parseDOM: [ { tag: 'p', - getAttrs(dom: HTMLElement): Record { + 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 = { 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]; }, }; @@ -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 = { + marginLeft: indent !== null ? `${indent * 40}px` : null, + }; + + const style = toStyleString(styles) || null; + + const attrs = { + style, + 'data-indent': indent ?? null, + }; + + return ['blockquote', attrs, 0]; }, }; @@ -78,6 +115,9 @@ const heading: NodeSpec = { align: { default: null, }, + indent: { + default: null, + }, }, content: 'inline*', group: 'block', @@ -88,10 +128,12 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { const { textAlign } = dom.style; const align = dom.getAttribute('align') || textAlign || null; + const indent = dom.getAttribute('indent') || null; return { level: 1, align, + indent, }; }, }, @@ -100,10 +142,12 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { const { textAlign } = dom.style; const align = dom.getAttribute('align') || textAlign || null; + const indent = dom.getAttribute('indent') || null; return { level: 2, align, + indent, }; }, }, @@ -112,10 +156,12 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { const { textAlign } = dom.style; const align = dom.getAttribute('align') || textAlign || null; + const indent = dom.getAttribute('indent') || null; return { level: 3, align, + indent, }; }, }, @@ -124,10 +170,12 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { const { textAlign } = dom.style; const align = dom.getAttribute('align') || textAlign || null; + const indent = dom.getAttribute('indent') || null; return { level: 4, align, + indent, }; }, }, @@ -136,10 +184,12 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { const { textAlign } = dom.style; const align = dom.getAttribute('align') || textAlign || null; + const indent = dom.getAttribute('indent') || null; return { level: 5, align, + indent, }; }, }, @@ -148,23 +198,31 @@ const heading: NodeSpec = { getAttrs(dom: HTMLElement): Record { 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 = { 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]; }, }; diff --git a/projects/ngx-editor/src/lib/EditorCommands.ts b/projects/ngx-editor/src/lib/EditorCommands.ts index 8c0640dd..351baf55 100644 --- a/projects/ngx-editor/src/lib/EditorCommands.ts +++ b/projects/ngx-editor/src/lib/EditorCommands.ts @@ -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'; @@ -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; diff --git a/projects/ngx-editor/src/lib/Locals.ts b/projects/ngx-editor/src/lib/Locals.ts index 8247a801..0fca6f07 100644 --- a/projects/ngx-editor/src/lib/Locals.ts +++ b/projects/ngx-editor/src/lib/Locals.ts @@ -28,6 +28,8 @@ export const defaults: Record> = { insertLink: 'Insert Link', removeLink: 'Remove Link', insertImage: 'Insert Image', + indent: 'Increase Indent', + outdent: 'Decrease Indent', // pupups, forms, others... url: 'URL', diff --git a/projects/ngx-editor/src/lib/commands/Indent.ts b/projects/ngx-editor/src/lib/commands/Indent.ts new file mode 100644 index 00000000..f0079492 --- /dev/null +++ b/projects/ngx-editor/src/lib/commands/Indent.ts @@ -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; diff --git a/projects/ngx-editor/src/lib/commands/index.ts b/projects/ngx-editor/src/lib/commands/index.ts index 7361ac7e..666b515c 100644 --- a/projects/ngx-editor/src/lib/commands/index.ts +++ b/projects/ngx-editor/src/lib/commands/index.ts @@ -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'); @@ -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'); diff --git a/projects/ngx-editor/src/lib/icons/indent.ts b/projects/ngx-editor/src/lib/icons/indent.ts new file mode 100644 index 00000000..99138a46 --- /dev/null +++ b/projects/ngx-editor/src/lib/icons/indent.ts @@ -0,0 +1 @@ +export default ''; diff --git a/projects/ngx-editor/src/lib/icons/index.ts b/projects/ngx-editor/src/lib/icons/index.ts index 7c0e1989..ddf8aba5 100644 --- a/projects/ngx-editor/src/lib/icons/index.ts +++ b/projects/ngx-editor/src/lib/icons/index.ts @@ -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; @@ -45,6 +47,8 @@ export const icons: Record = { color_fill: colorFill, horizontal_rule: horizontalRule, format_clear: formatClear, + indent, + outdent, path: '', }; diff --git a/projects/ngx-editor/src/lib/icons/outdent.ts b/projects/ngx-editor/src/lib/icons/outdent.ts new file mode 100644 index 00000000..fcb02d02 --- /dev/null +++ b/projects/ngx-editor/src/lib/icons/outdent.ts @@ -0,0 +1 @@ +export default ''; diff --git a/projects/ngx-editor/src/lib/modules/menu/MenuCommands.ts b/projects/ngx-editor/src/lib/modules/menu/MenuCommands.ts index 6c79b081..e4aa5ee4 100644 --- a/projects/ngx-editor/src/lib/modules/menu/MenuCommands.ts +++ b/projects/ngx-editor/src/lib/modules/menu/MenuCommands.ts @@ -26,6 +26,8 @@ export const ToggleCommands: Record = { export const InsertCommands: Record = { horizontal_rule: Commands.HORIZONTAL_RULE, format_clear: Commands.FORMAT_CLEAR, + indent: Commands.INDENT, + outdent: Commands.OUTDENT, }; export const Link = Commands.LINK; diff --git a/projects/ngx-editor/src/lib/modules/menu/menu.component.ts b/projects/ngx-editor/src/lib/modules/menu/menu.component.ts index b252d4fb..9d62b796 100644 --- a/projects/ngx-editor/src/lib/modules/menu/menu.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/menu.component.ts @@ -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 = [ @@ -91,6 +91,8 @@ export class MenuComponent implements OnInit { insertCommands: ToolbarItem[] = [ 'horizontal_rule', 'format_clear', + 'indent', + 'outdent', ]; iconContainerClass = ['NgxEditor__MenuItem', 'NgxEditor__MenuItem--Icon']; diff --git a/projects/ngx-editor/src/lib/types.ts b/projects/ngx-editor/src/lib/types.ts index 398d14d7..5f859ac4 100644 --- a/projects/ngx-editor/src/lib/types.ts +++ b/projects/ngx-editor/src/lib/types.ts @@ -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; diff --git a/projects/ngx-editor/utils/clamp.ts b/projects/ngx-editor/utils/clamp.ts new file mode 100644 index 00000000..ea1b5f25 --- /dev/null +++ b/projects/ngx-editor/utils/clamp.ts @@ -0,0 +1,5 @@ +const clamp = (value: number, min: number, max: number): number => { + return Math.min(Math.max(value, min), max); +}; + +export default clamp; diff --git a/projects/ngx-editor/utils/public_api.ts b/projects/ngx-editor/utils/public_api.ts index f595509f..5b031831 100644 --- a/projects/ngx-editor/utils/public_api.ts +++ b/projects/ngx-editor/utils/public_api.ts @@ -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';