From 274dcc7a6facb72fef721fccc5e6ddb2e8493e25 Mon Sep 17 00:00:00 2001 From: sibiraj-s Date: Mon, 4 May 2020 11:57:08 +0530 Subject: [PATCH] feat: add support for list --- demo/src/app/app.component.scss | 5 +- package-lock.json | 40 +++++++ package.json | 5 + src/lib/ngx-editor.component.scss | 1 + src/lib/ngx-editor.component.ts | 3 +- src/lib/plugins/menu/MenuBarView.ts | 69 ++++------- src/lib/plugins/menu/MenuItems.ts | 139 +++++++++++++++++++++++ src/lib/plugins/menu/constants.ts | 2 - src/lib/plugins/menu/getMenu.ts | 58 ---------- src/lib/plugins/menu/index.ts | 19 +++- src/lib/plugins/menu/menu.ts | 22 ---- src/lib/plugins/menu/meta.ts | 31 +++++ src/lib/pm-tools/commands/toggleList.ts | 17 +++ src/lib/pm-tools/helpers/isMarkActive.ts | 14 +++ src/lib/pm-tools/helpers/isNodeActive.ts | 17 +++ src/lib/schema.ts | 26 +++++ src/lib/types.ts | 7 +- src/lib/utils/computeOptions.ts | 1 + src/lib/utils/icons/bullet_list.ts | 4 + src/lib/utils/icons/index.ts | 6 +- src/lib/utils/icons/ordered_list.ts | 4 + src/lib/utils/plugins.ts | 31 +++-- src/ng-package.json | 19 +++- src/package.json | 12 +- src/public-api.ts | 1 + wiki/content/configuartion.md | 5 +- wiki/content/ng-model.md | 2 +- wiki/content/shortcuts.md | 12 ++ wiki/summary.json | 4 + 29 files changed, 425 insertions(+), 151 deletions(-) create mode 100644 src/lib/plugins/menu/MenuItems.ts delete mode 100644 src/lib/plugins/menu/constants.ts delete mode 100644 src/lib/plugins/menu/getMenu.ts delete mode 100644 src/lib/plugins/menu/menu.ts create mode 100644 src/lib/plugins/menu/meta.ts create mode 100644 src/lib/pm-tools/commands/toggleList.ts create mode 100644 src/lib/pm-tools/helpers/isMarkActive.ts create mode 100644 src/lib/pm-tools/helpers/isNodeActive.ts create mode 100644 src/lib/schema.ts create mode 100644 src/lib/utils/icons/bullet_list.ts create mode 100644 src/lib/utils/icons/ordered_list.ts create mode 100644 wiki/content/shortcuts.md diff --git a/demo/src/app/app.component.scss b/demo/src/app/app.component.scss index 08b50ee5..f9465321 100644 --- a/demo/src/app/app.component.scss +++ b/demo/src/app/app.component.scss @@ -1,5 +1,7 @@ .container { - width: 900px; + width: 100%; + max-width: 900px; + min-width: 320px; margin: auto; display: flex; align-items: center; @@ -18,6 +20,7 @@ display: flex; flex-direction: column; align-items: center; + text-align: center; } .badges { diff --git a/package-lock.json b/package-lock.json index 0f3cd675..114548c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1928,6 +1928,17 @@ "@types/prosemirror-model": "*" } }, + "@types/prosemirror-schema-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz", + "integrity": "sha512-+iUYq+pj2wVHSThj0MjNDzkkGwq8aDQ6j0UJK8a0cNCL8v44Ftcx1noGPtBIEUJgitH960VnfBNoTWfQoQZfRA==", + "dev": true, + "requires": { + "@types/orderedmap": "*", + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*" + } + }, "@types/prosemirror-state": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/prosemirror-state/-/prosemirror-state-1.2.3.tgz", @@ -13196,6 +13207,16 @@ "prosemirror-model": "^1.2.0" } }, + "prosemirror-schema-list": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.2.tgz", + "integrity": "sha512-dgM9PwtM4twa5WsgSYMB+J8bwjnR43DAD3L9MsR9rKm/nZR5Y85xcjB7gusVMSsbQ2NomMZF03RE6No6mTnclQ==", + "dev": true, + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, "prosemirror-state": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.3.tgz", @@ -13206,6 +13227,19 @@ "prosemirror-transform": "^1.0.0" } }, + "prosemirror-tables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.0.tgz", + "integrity": "sha512-E00+KSbDw65966GdiLBpqTNxIextw0RavlGmvdv/dyYbN9OTD0gzaoCU1S8MAbz4GLKmY9Y/g4nSiC1IL1ThQg==", + "dev": true, + "requires": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, "prosemirror-transform": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.2.5.tgz", @@ -13215,6 +13249,12 @@ "prosemirror-model": "^1.0.0" } }, + "prosemirror-utils": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz", + "integrity": "sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA==", + "dev": true + }, "prosemirror-view": { "version": "1.14.7", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.14.7.tgz", diff --git a/package.json b/package.json index fcc36260..3e11e896 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/prosemirror-keymap": "^1.0.1", "@types/prosemirror-model": "^1.7.2", "@types/prosemirror-schema-basic": "^1.0.1", + "@types/prosemirror-schema-list": "^1.0.1", "@types/prosemirror-state": "^1.2.3", "@types/prosemirror-view": "^1.11.4", "chalk": "^4.0.0", @@ -77,8 +78,12 @@ "prosemirror-commands": "^1.1.4", "prosemirror-history": "^1.1.3", "prosemirror-keymap": "^1.1.3", + "prosemirror-model": "^1.9.1", "prosemirror-schema-basic": "^1.1.2", + "prosemirror-schema-list": "^1.1.2", "prosemirror-state": "^1.3.3", + "prosemirror-tables": "^1.1.0", + "prosemirror-utils": "^0.9.6", "prosemirror-view": "^1.14.7", "protractor": "~5.4.3", "ts-node": "~8.3.0", diff --git a/src/lib/ngx-editor.component.scss b/src/lib/ngx-editor.component.scss index 9f682180..860d6d8a 100644 --- a/src/lib/ngx-editor.component.scss +++ b/src/lib/ngx-editor.component.scss @@ -47,6 +47,7 @@ $icon-size: 30px; .NgxEditor-Content { padding: .5rem; + white-space: pre-wrap; p { margin: 0; diff --git a/src/lib/ngx-editor.component.ts b/src/lib/ngx-editor.component.ts index a17d6736..7223772c 100644 --- a/src/lib/ngx-editor.component.ts +++ b/src/lib/ngx-editor.component.ts @@ -7,7 +7,8 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; import { EditorState, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { Node as ProsemirrorNode } from 'prosemirror-model'; -import { schema } from 'prosemirror-schema-basic'; + +import schema from './schema'; import { Config, ComputedOptions } from './types'; diff --git a/src/lib/plugins/menu/MenuBarView.ts b/src/lib/plugins/menu/MenuBarView.ts index 0df1343d..efd62353 100644 --- a/src/lib/plugins/menu/MenuBarView.ts +++ b/src/lib/plugins/menu/MenuBarView.ts @@ -1,70 +1,41 @@ import { EditorView } from 'prosemirror-view'; import { EditorState } from 'prosemirror-state'; -import { MarkType, Mark } from 'prosemirror-model'; -import { MenuItem, Toolbar } from '../../types'; -import { MENU_WRAPPER_CLASSNAME, MENU_ITEM_CLASSNAME } from './constants'; -import getMenu from './getMenu'; +import { Toolbar } from '../../types'; + +import MenuItems from './MenuItems'; class MenuBarView { - menu: MenuItem[]; - editorView: EditorView; + toolbar: Toolbar; + view: EditorView; dom: HTMLElement; - constructor(toolbar: Toolbar, editorView: EditorView) { - const menu = getMenu(toolbar); - this.editorView = editorView; - - // remove elements without commands - this.menu = menu.filter(menuItem => menuItem.command); - - this.dom = document.createElement('div'); - this.dom.className = MENU_WRAPPER_CLASSNAME; + updateMenuItems: (state: EditorState) => void; - menu.forEach(({ dom }) => this.dom.appendChild(dom)); + constructor(toolbar: Toolbar, editorView: EditorView) { + // const menu = getMenu(toolbar); + this.view = editorView; + this.toolbar = toolbar; + this.render(); this.update(); - - this.dom.addEventListener('mousedown', (e: MouseEvent) => { - e.preventDefault(); - editorView.focus(); - - this.menu.forEach(({ command, dom }) => { - if (dom.contains(e.target as HTMLElement)) { - command(editorView.state, editorView.dispatch, editorView); - } - }); - }); } - getIsElementActive(state: EditorState, type: MarkType): boolean | Mark { - const { from, $from, to, empty } = state.selection; + render() { + const menuDom = document.createElement('div'); + menuDom.className = 'NgxEditor-MenuBar'; - if (empty) { - return type.isInSet(state.storedMarks || $from.marks()); - } else { - return state.doc.rangeHasMark(from, to, type); - } - } - - update() { - this.menu.forEach(({ command, dom, key }) => { - const canShowItem = command(this.editorView.state, null, this.editorView); - const state = this.editorView.state; + const menuItems = new MenuItems(this.toolbar, this.view, menuDom); + menuItems.render(); - const markType = state.schema.marks[key]; + this.updateMenuItems = menuItems.update.bind(menuItems); - const isActive = !!this.getIsElementActive(state, markType); - - dom.style.display = canShowItem ? '' : 'none'; - - dom.classList.toggle(`${MENU_ITEM_CLASSNAME}__active`, isActive); - }); + this.view.dom.parentNode.insertBefore(menuDom, this.view.dom); } - destroy() { - this.dom.remove(); + update() { + this.updateMenuItems(this.view.state); } } diff --git a/src/lib/plugins/menu/MenuItems.ts b/src/lib/plugins/menu/MenuItems.ts new file mode 100644 index 00000000..f83eac3d --- /dev/null +++ b/src/lib/plugins/menu/MenuItems.ts @@ -0,0 +1,139 @@ +import { toggleMark } from 'prosemirror-commands'; +import { EditorView } from 'prosemirror-view'; +import { EditorState } from 'prosemirror-state'; +import { MarkType, NodeType } from 'prosemirror-model'; + +import schema from '../../schema'; + +import isNodeActive from '../../pm-tools/helpers/isNodeActive'; +import isMarkActive from '../../pm-tools/helpers/isMarkActive'; +import toggleList from '../../pm-tools/commands/toggleList'; + +import { getIconSvg } from '../../utils/icons'; +import { Toolbar, MenuItemMeta } from '../../types'; +import menuItemsMeta from './meta'; + +const MENU_ITEM_CLASSNAME = 'NgxEditor-MenuItem'; + +const isListItem = (type: NodeType) => { + return ( + type === schema.nodes.list_item || + type === schema.nodes.ordered_list || + type === schema.nodes.bullet_list + ); +}; + +class MenuItemView { + menuItem: MenuItemMeta; + dom: HTMLElement; + editorView: EditorView; + + constructor(menuItem: MenuItemMeta, editorView: EditorView) { + this.menuItem = menuItem; + this.editorView = editorView; + this.dom = this.getDom(); + this.setupEventListeners(); + } + + getDom(): HTMLElement { + const div = document.createElement('div'); + + div.classList.add(MENU_ITEM_CLASSNAME); + div.classList.add(`${MENU_ITEM_CLASSNAME}__${name}`); + div.title = name; + div.innerHTML = getIconSvg(this.menuItem.icon); + + return div; + } + + private setupEventListeners() { + this.dom.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + + if (this.menuItem.type === 'mark') { + const command = toggleMark(schema.marks[this.menuItem.key]); + command(this.editorView.state, this.editorView.dispatch); + } + + if (this.menuItem.type === 'node') { + const type = schema.nodes[this.menuItem.key]; + + if (isListItem(type)) { + const command = toggleList(type, schema.nodes.list_item); + command(this.editorView.state, this.editorView.dispatch); + } + } + }); + } + + update(state: EditorState): void { + const menuItem = this.menuItem; + let isActive = false; + + if (menuItem.type === 'mark') { + const type: MarkType = schema.marks[menuItem.key]; + isActive = isMarkActive(state, type); + } + + if (menuItem.type === 'node') { + const type: NodeType = schema.nodes[menuItem.key]; + isActive = isNodeActive(state, type); + } + + this.dom.classList.toggle(`${MENU_ITEM_CLASSNAME}__active`, isActive); + } +} + +function getSeperatorDom(): HTMLElement { + const div = document.createElement('div'); + div.className = `${MENU_ITEM_CLASSNAME}__Seperator`; + return div; +} + +class MenuItems { + private toolbar: Toolbar; + private menuDom: HTMLElement; + private editorView: EditorView; + + updates = []; + + constructor(toolbar: Toolbar, editorView: EditorView, menuDom: HTMLElement) { + this.toolbar = toolbar; + this.editorView = editorView; + this.menuDom = menuDom; + } + + render() { + this.toolbar.forEach((group, toolbarIndex) => { + const isLastMenuGroup = this.toolbar.length - 1 === toolbarIndex; + + group.forEach((menuName, menuIndex) => { + const isLastMenuItem = group.length - 1 === menuIndex; + + const menuItem = menuItemsMeta[menuName]; + + if (menuItem) { + const menuItemView = new MenuItemView(menuItem, this.editorView); + const update = menuItemView.update.bind(menuItemView); + + this.menuDom.appendChild(menuItemView.dom); + this.updates.push(update); + + if (isLastMenuItem && !isLastMenuGroup) { + const seperatorDom = getSeperatorDom(); + this.menuDom.appendChild(seperatorDom); + } + } + }); + }); + } + + update(state: EditorState) { + this.updates.forEach(update => { + update(state); + }); + } +} + + +export default MenuItems; diff --git a/src/lib/plugins/menu/constants.ts b/src/lib/plugins/menu/constants.ts deleted file mode 100644 index ddab7731..00000000 --- a/src/lib/plugins/menu/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MENU_ITEM_CLASSNAME = 'NgxEditor-MenuItem'; -export const MENU_WRAPPER_CLASSNAME = 'NgxEditor-MenuBar'; diff --git a/src/lib/plugins/menu/getMenu.ts b/src/lib/plugins/menu/getMenu.ts deleted file mode 100644 index 94ae1417..00000000 --- a/src/lib/plugins/menu/getMenu.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { toggleMark } from 'prosemirror-commands'; -import { schema } from 'prosemirror-schema-basic'; - -import { getIconSvg } from '../../utils/icons'; -import { MENU_ITEM_CLASSNAME } from './constants'; -import { MenuItem, Toolbar } from '../../types'; - -function getMenuDom(name: string): HTMLElement { - const div = document.createElement('div'); - - div.classList.add(MENU_ITEM_CLASSNAME); - div.classList.add(`${MENU_ITEM_CLASSNAME}__${name}`); - - div.title = name; - - div.innerHTML = getIconSvg(name); - - return div; -} - -function getSeperatorDom(): HTMLElement { - const div = document.createElement('div'); - div.className = `${MENU_ITEM_CLASSNAME}__Seperator`; - return div; -} - -const menuItems: { [key: string]: MenuItem } = { - bold: { key: 'strong', command: toggleMark(schema.marks.strong), dom: getMenuDom('bold') }, - italic: { key: 'em', command: toggleMark(schema.marks.em), dom: getMenuDom('italic') }, - code: { key: 'code', command: toggleMark(schema.marks.code), dom: getMenuDom('code') } -}; - -const seperator: MenuItem = { key: 'seperator', dom: getSeperatorDom() }; - -const getMenu = (toolbar: Toolbar): MenuItem[] => { - const menu = []; - - toolbar.forEach((group, toolbarIndex) => { - const isLastMenuGroup = toolbar.length - 1 === toolbarIndex; - - group.forEach((menuName, menuIndex) => { - const isLastMenuItem = group.length - 1 === menuIndex; - - const menuItem = menuItems[menuName]; - if (menuItem) { - menu.push(menuItem); - - if (isLastMenuItem && !isLastMenuGroup) { - menu.push(seperator); - } - } - }); - }); - - return menu; -}; - -export default getMenu; diff --git a/src/lib/plugins/menu/index.ts b/src/lib/plugins/menu/index.ts index a85aad25..a19b9b4d 100644 --- a/src/lib/plugins/menu/index.ts +++ b/src/lib/plugins/menu/index.ts @@ -1,3 +1,20 @@ -import menu from './menu'; +import { EditorView } from 'prosemirror-view'; +import { Plugin, PluginKey } from 'prosemirror-state'; + +import { Toolbar } from '../../types'; +import MenuBarView from './MenuBarView'; + +function menuPlugin(toolbar: Toolbar): Plugin { + return new Plugin({ + key: new PluginKey('menu'), + view(editorView: EditorView): MenuBarView { + return new MenuBarView(toolbar, editorView); + }, + }); +} + +const menu = (toolbarOptions: Toolbar) => { + return menuPlugin(toolbarOptions); +}; export default menu; diff --git a/src/lib/plugins/menu/menu.ts b/src/lib/plugins/menu/menu.ts deleted file mode 100644 index ddd57087..00000000 --- a/src/lib/plugins/menu/menu.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { EditorView } from 'prosemirror-view'; -import { Plugin, PluginKey } from 'prosemirror-state'; - -import { Toolbar } from '../../types'; -import MenuBarView from './MenuBarView'; - -function menuPlugin(toolbar: Toolbar): Plugin { - return new Plugin({ - key: new PluginKey('menu'), - view(editorView: EditorView): MenuBarView { - const menuView = new MenuBarView(toolbar, editorView); - editorView.dom.parentNode.insertBefore(menuView.dom, editorView.dom); - return menuView; - }, - }); -} - -const menu = (toolbarOptions: Toolbar) => { - return menuPlugin(toolbarOptions); -}; - -export default menu; diff --git a/src/lib/plugins/menu/meta.ts b/src/lib/plugins/menu/meta.ts new file mode 100644 index 00000000..d8d3735f --- /dev/null +++ b/src/lib/plugins/menu/meta.ts @@ -0,0 +1,31 @@ +import { MenuItemMeta } from '../../types'; + +const menuItemsMeta: { [key: string]: MenuItemMeta } = { + bold: { + key: 'strong', + icon: 'bold', + type: 'mark', + }, + italic: { + key: 'em', + icon: 'italic', + type: 'mark', + }, + code: { + key: 'code', + icon: 'code', + type: 'mark', + }, + ordered_list: { + key: 'ordered_list', + icon: 'ordered_list', + type: 'node', + }, + bullet_list: { + key: 'bullet_list', + icon: 'bullet_list', + type: 'node', + } +}; + +export default menuItemsMeta; diff --git a/src/lib/pm-tools/commands/toggleList.ts b/src/lib/pm-tools/commands/toggleList.ts new file mode 100644 index 00000000..4db4aeda --- /dev/null +++ b/src/lib/pm-tools/commands/toggleList.ts @@ -0,0 +1,17 @@ +import { EditorState, Transaction } from 'prosemirror-state'; +import { NodeType } from 'prosemirror-model'; +import { wrapInList, liftListItem } from 'prosemirror-schema-list'; + +import isNodeActive from '../helpers/isNodeActive'; + +export default function toggleList(type: NodeType, itemType: NodeType) { + return (state: EditorState, dispatch: (tr: Transaction) => void) => { + const isActive = isNodeActive(state, type); + + if (isActive) { + return liftListItem(itemType)(state, dispatch); + } + + return wrapInList(type)(state, dispatch); + }; +} diff --git a/src/lib/pm-tools/helpers/isMarkActive.ts b/src/lib/pm-tools/helpers/isMarkActive.ts new file mode 100644 index 00000000..237952aa --- /dev/null +++ b/src/lib/pm-tools/helpers/isMarkActive.ts @@ -0,0 +1,14 @@ +import { EditorState } from 'prosemirror-state'; +import { MarkType } from 'prosemirror-model'; + +const isMarkActive = (state: EditorState, type: MarkType): boolean => { + const { from, $from, to, empty } = state.selection; + + if (empty) { + return !!type.isInSet(state.storedMarks || $from.marks()); + } else { + return state.doc.rangeHasMark(from, to, type); + } +}; + +export default isMarkActive; diff --git a/src/lib/pm-tools/helpers/isNodeActive.ts b/src/lib/pm-tools/helpers/isNodeActive.ts new file mode 100644 index 00000000..bdb73d5d --- /dev/null +++ b/src/lib/pm-tools/helpers/isNodeActive.ts @@ -0,0 +1,17 @@ +import { EditorState } from 'prosemirror-state'; +import { NodeType, Node as ProsemirrorNode } from 'prosemirror-model'; +import { findSelectedNodeOfType, findParentNode } from 'prosemirror-utils'; + +const isNodeActive = (state: EditorState, type: NodeType): boolean => { + // const { from } = state.selection; + + // if (from === 0) { // all selection + // return contains(state.doc, type); + // } + + const predicate = (n: ProsemirrorNode) => n.type === type; + const node = findSelectedNodeOfType(type)(state.selection) || findParentNode(predicate)(state.selection); + return !!node; +}; + +export default isNodeActive; diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 00000000..22d63427 --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,26 @@ +import { Schema } from 'prosemirror-model'; +import * as schemaBasic from 'prosemirror-schema-basic'; +import * as schemaList from 'prosemirror-schema-list'; + +const listGroup = 'block'; + +const listItem = Object.assign({}, schemaList.listItem, { content: 'paragraph block*' }); +const orderedList = Object.assign({}, schemaList.orderedList, { content: 'list_item+', group: listGroup }); +const bulletList = Object.assign({}, schemaList.bulletList, { content: 'list_item+', group: listGroup }); + +const nodes = Object.assign( + {}, + schemaBasic.nodes, + { + list_item: listItem, + ordered_list: orderedList, + bullet_list: bulletList + } +); + +const schema = new Schema({ + marks: schemaBasic.marks, + nodes +}); + +export default schema; diff --git a/src/lib/types.ts b/src/lib/types.ts index d1dc66d2..db08222e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -4,13 +4,16 @@ export interface Config { toolbar: boolean | Toolbar; } -export interface MenuItem { +export interface MenuItemMeta { key: string; + icon: string; + type: 'mark' | 'node'; command?: any; - dom: HTMLElement; } export interface ComputedOptions extends Config { placeholder: string; toolbar: Toolbar; } + +export type KeyMap = { [key: string]: any }; diff --git a/src/lib/utils/computeOptions.ts b/src/lib/utils/computeOptions.ts index 70a74c92..47a436c8 100644 --- a/src/lib/utils/computeOptions.ts +++ b/src/lib/utils/computeOptions.ts @@ -7,6 +7,7 @@ interface Options { const defaultToolbarOptions: Toolbar = [ ['bold', 'italic', 'code'], + ['ordered_list', 'bullet_list'] ]; const defaultConfig: Config = { diff --git a/src/lib/utils/icons/bullet_list.ts b/src/lib/utils/icons/bullet_list.ts new file mode 100644 index 00000000..611a95cf --- /dev/null +++ b/src/lib/utils/icons/bullet_list.ts @@ -0,0 +1,4 @@ +export default ` + + +`; diff --git a/src/lib/utils/icons/index.ts b/src/lib/utils/icons/index.ts index 729bae60..47f3f47d 100644 --- a/src/lib/utils/icons/index.ts +++ b/src/lib/utils/icons/index.ts @@ -3,6 +3,8 @@ import bold from './bold'; import italic from './italic'; import code from './code'; +import orderedList from './ordered_list'; +import bulletList from './bullet_list'; const height = 20; const width = 20; @@ -10,7 +12,9 @@ const width = 20; const icons = { bold, italic, - code + code, + ordered_list: orderedList, + bullet_list: bulletList }; // Helper function to create menu icons diff --git a/src/lib/utils/icons/ordered_list.ts b/src/lib/utils/icons/ordered_list.ts new file mode 100644 index 00000000..2d9b9b86 --- /dev/null +++ b/src/lib/utils/icons/ordered_list.ts @@ -0,0 +1,4 @@ +export default ` + + +`; diff --git a/src/lib/utils/plugins.ts b/src/lib/utils/plugins.ts index b68d6179..01014d07 100644 --- a/src/lib/utils/plugins.ts +++ b/src/lib/utils/plugins.ts @@ -2,17 +2,19 @@ import { Plugin } from 'prosemirror-state'; import { history, undo, redo } from 'prosemirror-history'; import { keymap } from 'prosemirror-keymap'; import { baseKeymap, toggleMark } from 'prosemirror-commands'; -import { schema } from 'prosemirror-schema-basic'; -import { ComputedOptions } from '../types'; +import schema from '../schema'; + +import { ComputedOptions, KeyMap } from '../types'; import menu from '../plugins/menu'; import placeholder from '../plugins/placeholder'; +import { splitListItem, liftListItem, sinkListItem } from 'prosemirror-schema-list'; const isMacOs = /Mac/.test(navigator.platform); -function getHistoryKeyMap() { - const historyMap = {}; +const getHistoryKeyMap = (): KeyMap => { + const historyMap: KeyMap = {}; historyMap['Mod-z'] = undo; @@ -23,20 +25,33 @@ function getHistoryKeyMap() { } return historyMap; -} +}; + +const getListKeyMap = (): KeyMap => { + const listMap: KeyMap = {}; + + listMap.Enter = splitListItem(schema.nodes.list_item); + listMap['Mod-['] = liftListItem(schema.nodes.list_item); + listMap['Mod-]'] = sinkListItem(schema.nodes.list_item); + listMap.Tab = sinkListItem(schema.nodes.list_item); + + return listMap; +}; export const getPlugins = (options: ComputedOptions): Plugin[] => { const historyKeyMap = getHistoryKeyMap(); + const listKeyMap = getListKeyMap(); const plugins = [ history(), - keymap(baseKeymap), - keymap(historyKeyMap), keymap({ 'Mod-b': toggleMark(schema.marks.strong), 'Mod-i': toggleMark(schema.marks.em), - 'Mod-`': toggleMark(schema.marks.code) + 'Mod-`': toggleMark(schema.marks.code), }), + keymap(historyKeyMap), + keymap(listKeyMap), + keymap(baseKeymap), placeholder(options.placeholder), ]; diff --git a/src/ng-package.json b/src/ng-package.json index 958457e5..d9259b48 100644 --- a/src/ng-package.json +++ b/src/ng-package.json @@ -6,10 +6,25 @@ "umdModuleIds": { "prosemirror-state": "prosemirrorState", "prosemirror-view": "prosemirrorView", + "prosemirror-model": "prosemirrorModel", "prosemirror-schema-basic": "prosemirrorSchemaBasic", + "prosemirror-schema-list": "prosemirrorSchemaList", "prosemirror-history": "prosemirrorHistory", "prosemirror-keymap": "prosemirrorKeymap", - "prosemirror-commands": "prosemirrorCommands" + "prosemirror-commands": "prosemirrorCommands", + "prosemirror-utils": "prosemirrorUtils" } - } + }, + "whitelistedNonPeerDependencies": [ + "prosemirror-state", + "prosemirror-view", + "prosemirror-model", + "prosemirror-schema-basic", + "prosemirror-schema-list", + "prosemirror-history", + "prosemirror-keymap", + "prosemirror-commands", + "prosemirror-utils", + "prosemirror-tables" + ] } diff --git a/src/package.json b/src/package.json index 18211361..c46df407 100644 --- a/src/package.json +++ b/src/package.json @@ -17,12 +17,20 @@ "@angular/common": "^9.0.0", "@angular/core": "^9.0.0", "@angular/forms": "^9.0.0", + "tslib": "^1.10.0" + }, + "dependencies": { "prosemirror-commands": "^1.1.0", "prosemirror-history": "^1.1.0", "prosemirror-keymap": "^1.1.0", + "prosemirror-model": "^1.1.0", "prosemirror-schema-basic": "^1.1.0", + "prosemirror-schema-list": "^1.1.0", "prosemirror-state": "^1.3.0", - "prosemirror-view": "^1.14.0", - "tslib": "^1.10.0" + "prosemirror-tables": "^1.1.0", + + "prosemirror-utils": "^0.9.6", + + "prosemirror-view": "^1.14.0" } } diff --git a/src/public-api.ts b/src/public-api.ts index 46b6038c..3f7b6e74 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -4,3 +4,4 @@ export * from './lib/ngx-editor.component'; export * from './lib/ngx-editor.module'; +export { default as schema } from './lib/schema'; diff --git a/wiki/content/configuartion.md b/wiki/content/configuartion.md index 0eb06836..447e36e6 100644 --- a/wiki/content/configuartion.md +++ b/wiki/content/configuartion.md @@ -14,7 +14,10 @@ The config property is a JSON object. ```json { - "toolbar": [["bold", "italic", "code"]] + "toolbar": [ + ["bold", "italic", "code"], + ["ordered_list", "bullet_list"] + ] } ``` diff --git a/wiki/content/ng-model.md b/wiki/content/ng-model.md index 6286bad5..5de7791d 100644 --- a/wiki/content/ng-model.md +++ b/wiki/content/ng-model.md @@ -19,8 +19,8 @@ class AppComponent { ## Generate HTML from JSON ```ts -import { schema } from "prosemirror-schema-basic"; import { DOMSerializer } from "prosemirror-model"; +import { schema } from "ngx-editor"; const contentNode = schema.nodeFromJSON(jsonDoc); diff --git a/wiki/content/shortcuts.md b/wiki/content/shortcuts.md new file mode 100644 index 00000000..c00c410e --- /dev/null +++ b/wiki/content/shortcuts.md @@ -0,0 +1,12 @@ +# Shortcuts + +- **Mod+b** - Toggle Strong +- **Mod+i** - Toggle Emphasis +- **Mod+`** - Toggle Code +- **Mod+Z** - Undo +- **Shift-Mod-z or Mod-y** - Redo +- **Mod-[** - Lift the list item around the selection up into a wrapping list +- **Mod-] or Tab** - Sink the list item around the selection down into an inner list + + +Note: **Mod-** is a shorthand for **Cmd-** on Mac and **Ctrl-** on other platforms. diff --git a/wiki/summary.json b/wiki/summary.json index 1e1145bd..549698be 100644 --- a/wiki/summary.json +++ b/wiki/summary.json @@ -10,5 +10,9 @@ { "title": "NgModel Binding", "file": "content/ng-model.md" + }, + { + "title": "Shortcuts", + "file": "content/shortcuts.md" } ]