From 2d7d38e36e4c07ef7e41b4d1b48732494ade1852 Mon Sep 17 00:00:00 2001 From: sibiraj-s Date: Tue, 5 May 2020 22:28:58 +0530 Subject: [PATCH] feat: add support for headings --- src/lib/ngx-editor.component.scss | 89 ++++- src/lib/plugins/menu/MenuBarView.ts | 8 +- src/lib/plugins/menu/MenuItems.ts | 139 -------- src/lib/plugins/menu/i18n.ts | 8 + src/lib/plugins/menu/menu.ts | 355 +++++++++++++++++++ src/lib/plugins/menu/meta.ts | 42 +++ src/lib/pm-tools/commands/toggleBlockType.ts | 17 + src/lib/pm-tools/helpers/isNodeActive.ts | 16 +- src/lib/types.ts | 19 +- src/lib/utils/computeOptions.ts | 6 +- src/lib/utils/flatDeep.ts | 7 + src/lib/utils/icons/arrowDropDown.ts | 3 + src/lib/utils/icons/index.ts | 10 +- wiki/content/configuartion.md | 16 +- 14 files changed, 562 insertions(+), 173 deletions(-) delete mode 100644 src/lib/plugins/menu/MenuItems.ts create mode 100644 src/lib/plugins/menu/i18n.ts create mode 100644 src/lib/plugins/menu/menu.ts create mode 100644 src/lib/pm-tools/commands/toggleBlockType.ts create mode 100644 src/lib/utils/flatDeep.ts create mode 100644 src/lib/utils/icons/arrowDropDown.ts diff --git a/src/lib/ngx-editor.component.scss b/src/lib/ngx-editor.component.scss index 860d6d8a..f22cfdc5 100644 --- a/src/lib/ngx-editor.component.scss +++ b/src/lib/ngx-editor.component.scss @@ -14,27 +14,98 @@ $icon-size: 30px; .NgxEditor-MenuBar { display: flex; - padding: .2rem; + padding: 0.2rem; border-bottom: 1px solid #ddd; .NgxEditor-MenuItem { - height: $icon-size; - width: $icon-size; display: flex; align-items: center; justify-content: center; - transition: 0.3s ease-in-out; - border-radius: 4px; &:hover { cursor: pointer; background-color: #f1f1f1; } - &.NgxEditor-MenuItem__active { + &.NgxEditor-MenuItem__Icon { + height: $icon-size; + width: $icon-size; + transition: 0.3s ease-in-out; + border-radius: 2px; + margin-right: 2px; - svg { - fill: #06c; + &.NgxEditor-MenuItem__Active { + background-color: #e8f0fe; + + svg { + fill: #1a73e8; + } + } + } + + &.NgxEditor-MenuItem__Dropdown-Wrapper { + position: relative; + max-width: 100px; + width: 100%; + justify-content: start; + + .NgxEditor-MenuItem__Dropdown { + display: flex; + align-items: center; + width: 100%; + + .NgxEditor-MenuItem__Dropdown-Text { + margin-left: 5px; + } + + .NgxEditor-MenuItem__Dropdown-Icon { + margin-left: auto; + position: relative; + top: 2px; + } + } + + .NgxEditor-MenuItem__Dropdown-Menu { + position: absolute; + left: 0; + top: 30px; + box-shadow: rgba(60, 64, 67, 0.15) 0px 2px 6px 2px; + border-radius: 4px; + background-color: white; + display: none; + + .NgxEditor-MenuItem__Dropdown-Item { + cursor: pointer; + padding: 0.5rem 1rem; + white-space: nowrap; + + &:hover, + &.NgxEditor-MenuItem__active { + background-color: #f1f1f1; + } + } + } + + &.NgxEditor-MenuItem__Dropdown-Wrapper-Selected, + &.NgxEditor-MenuItem__Dropdown-Wrapper-Open { + background-color: #e8f0fe; + color: #1a73e8; + } + + &.NgxEditor-MenuItem__Dropdown-Wrapper-Open { + background-color: #e8f0fe; + color: #1a73e8; + + .NgxEditor-MenuItem__Dropdown-Icon { + svg { + fill: #1a73e8; + } + } + + .NgxEditor-MenuItem__Dropdown-Menu { + display: block; + color: initial; + } } } } @@ -46,7 +117,7 @@ $icon-size: 30px; } .NgxEditor-Content { - padding: .5rem; + padding: 0.5rem; white-space: pre-wrap; p { diff --git a/src/lib/plugins/menu/MenuBarView.ts b/src/lib/plugins/menu/MenuBarView.ts index efd62353..455d0c9b 100644 --- a/src/lib/plugins/menu/MenuBarView.ts +++ b/src/lib/plugins/menu/MenuBarView.ts @@ -3,7 +3,7 @@ import { EditorState } from 'prosemirror-state'; import { Toolbar } from '../../types'; -import MenuItems from './MenuItems'; +import { renderMenu } from './menu'; class MenuBarView { toolbar: Toolbar; @@ -26,10 +26,8 @@ class MenuBarView { const menuDom = document.createElement('div'); menuDom.className = 'NgxEditor-MenuBar'; - const menuItems = new MenuItems(this.toolbar, this.view, menuDom); - menuItems.render(); - - this.updateMenuItems = menuItems.update.bind(menuItems); + const { update } = renderMenu(this.toolbar, this.view, menuDom); + this.updateMenuItems = update; this.view.dom.parentNode.insertBefore(menuDom, this.view.dom); } diff --git a/src/lib/plugins/menu/MenuItems.ts b/src/lib/plugins/menu/MenuItems.ts deleted file mode 100644 index f83eac3d..00000000 --- a/src/lib/plugins/menu/MenuItems.ts +++ /dev/null @@ -1,139 +0,0 @@ -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/i18n.ts b/src/lib/plugins/menu/i18n.ts new file mode 100644 index 00000000..4dd1763f --- /dev/null +++ b/src/lib/plugins/menu/i18n.ts @@ -0,0 +1,8 @@ +export const labels = { + bold: 'Bold', + italics: 'Italics', + code: 'Code', + ordered_list: 'Ordered List', + bullet_list: 'Bullet List', + heading: 'Heading' +}; diff --git a/src/lib/plugins/menu/menu.ts b/src/lib/plugins/menu/menu.ts new file mode 100644 index 00000000..a41d6cc8 --- /dev/null +++ b/src/lib/plugins/menu/menu.ts @@ -0,0 +1,355 @@ +import { toggleMark } from 'prosemirror-commands'; +import { EditorView } from 'prosemirror-view'; +import { EditorState } from 'prosemirror-state'; +import { MarkType, NodeType } from 'prosemirror-model'; + +import { + Toolbar, + MenuItemMeta, + MenuItemViewSpec, + ToolbarItem, + ToolbarDropdownGroupKeys, + ToolbarDropdownGroupValues +} from '../../types'; + +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 toggleBlockType from '../../pm-tools/commands/toggleBlockType'; + +import { getIconSvg } from '../../utils/icons'; +import flatDeep from '../../utils/flatDeep'; + +import menuItemsMeta from './meta'; + +import { labels } from './i18n'; + +const MENU_ITEM_CLASSNAME = 'NgxEditor-MenuItem'; + +const DROPDOWN_ITEMS = new Map(); +DROPDOWN_ITEMS.set('heading', ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']); + +const isListItem = (type: NodeType) => { + return ( + type === schema.nodes.list_item || + type === schema.nodes.ordered_list || + type === schema.nodes.bullet_list + ); +}; + +class DropDownView { + private dropdownGroup: ToolbarDropdownGroupKeys; + private dropdownFields: ToolbarDropdownGroupValues; + private editorView: EditorView; + dom: HTMLElement; + + updates = []; + + constructor( + dropdownGroup: ToolbarDropdownGroupKeys, + dropdownFields: ToolbarDropdownGroupValues, + editorView: EditorView + ) { + this.dropdownGroup = dropdownGroup; + this.dropdownFields = dropdownFields; + this.editorView = editorView; + } + + getWrapperDom(): HTMLElement { + let isDropdownOpen = false; + const dropdownWrapper = document.createElement('div'); + + dropdownWrapper.classList.add(MENU_ITEM_CLASSNAME); + dropdownWrapper.classList.add(`${MENU_ITEM_CLASSNAME}__Dropdown-Wrapper`); + + // create dropdown content + const dropdown = document.createElement('div'); + dropdown.classList.add(`${MENU_ITEM_CLASSNAME}__Dropdown`); + + const dropdownText = document.createElement('div'); + dropdownText.classList.add(`${MENU_ITEM_CLASSNAME}__Dropdown-Text`); + dropdownText.textContent = labels[this.dropdownGroup]; + + const dropdownIcon = document.createElement('div'); + dropdownIcon.classList.add(`${MENU_ITEM_CLASSNAME}__Dropdown-Icon`); + dropdownIcon.innerHTML = getIconSvg('arrow_drop_down'); + + dropdown.appendChild(dropdownText); + dropdown.appendChild(dropdownIcon); + + const dropdownOpenClassName = `${MENU_ITEM_CLASSNAME}__Dropdown-Wrapper-Open`; + + const mouseDownHandler = (e: MouseEvent) => { + e.preventDefault(); + if (!dropdownWrapper.contains(e.target as Node)) { + closeDropdown(); + } + }; + + const openDropdown = () => { + dropdownWrapper.classList.add(dropdownOpenClassName); + isDropdownOpen = true; + window.addEventListener('mousedown', mouseDownHandler); + }; + + const closeDropdown = () => { + dropdownWrapper.classList.remove(dropdownOpenClassName); + isDropdownOpen = false; + window.removeEventListener('mousedown', mouseDownHandler); + }; + + dropdown.addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + if (!isDropdownOpen) { + openDropdown(); + } else { + closeDropdown(); + } + }); + + // create dropdown list + const dropdownList = document.createElement('div'); + dropdownList.classList.add(`${MENU_ITEM_CLASSNAME}__Dropdown-Menu`); + + this.dropdownFields.forEach(dropdownItem => { + const menuItem = menuItemsMeta[dropdownItem]; + + let text = labels[menuItem.key]; + + if (menuItem.key === 'heading') { + text += ` ${menuItem.attrs.level}`; + } + + const spec: MenuItemViewSpec = { + classNames: [ + `${MENU_ITEM_CLASSNAME}__Dropdown-Item` + ], + textContent: text, + attrs: { + title: text + } + }; + + const menuItemView = new MenuItemView(menuItem, this.editorView, spec); + const { update, dom } = menuItemView.render(); + + // remove open class once clicked on dropdown value + dom.addEventListener('click', (e: MouseEvent) => { + e.preventDefault(); + closeDropdown(); + }); + + // wrapper to execute when update is called + const dropUpdate = (state: EditorState) => { + update(state); + + const selectedClass = `${MENU_ITEM_CLASSNAME}__Dropdown-Wrapper-Selected`; + + // update the dropdown content heading when a class is selected + const activeEl = dropdownList.getElementsByClassName(`${MENU_ITEM_CLASSNAME}__Active`); + if (activeEl.length) { + const el = activeEl[0]; + dropdownText.textContent = el.textContent; + dropdownWrapper.classList.add(selectedClass); + } else { + // restore default value + dropdownText.textContent = labels[this.dropdownGroup]; + dropdownWrapper.classList.remove(selectedClass); + } + }; + + dropdownList.appendChild(dom); + this.updates.push(dropUpdate); + }); + + dropdownWrapper.appendChild(dropdown); + dropdownWrapper.appendChild(dropdownList); + + return dropdownWrapper; + } + + render() { + this.dom = this.getWrapperDom(); + + return { + dom: this.dom, + updates: this.updates + }; + } +} + +class MenuItemView { + private menuItem: MenuItemMeta; + private editorView: EditorView; + private spec: MenuItemViewSpec; + + dom: HTMLElement; + + constructor(menuItem: MenuItemMeta, editorView: EditorView, spec: MenuItemViewSpec) { + this.menuItem = menuItem; + this.editorView = editorView; + this.spec = spec; + } + + render() { + const dom = this.dom = this.getDom(); + this.setupCommandListeners(); + + const 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, menuItem.attrs); + } + + dom.classList.toggle(`${MENU_ITEM_CLASSNAME}__Active`, isActive); + }; + + return { + dom, + update + }; + } + + getDom(): HTMLElement { + const div = document.createElement('div'); + + if (this.spec.classNames) { + this.spec.classNames.forEach(className => { + div.classList.add(className); + }); + } + + if (this.spec.attrs) { + Object.entries(this.spec.attrs).forEach(obj => { + div.setAttribute(obj[0], obj[1]); + }); + } + + if (this.spec.innerHTML) { + div.innerHTML = this.spec.innerHTML; + } + + if (this.spec.textContent) { + div.innerHTML = this.spec.textContent; + } + + return div; + } + + private setupCommandListeners() { + this.dom.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + + // don't execute if not left click + if (e.buttons !== 1) { + return; + } + + if (this.menuItem.type === 'mark') { + const command = toggleMark(schema.marks[this.menuItem.key]); + command(this.editorView.state, this.editorView.dispatch); + return; + } + + 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); + return; + } + + if (type === schema.nodes.heading) { + const command = toggleBlockType(type, schema.nodes.paragraph, { level: this.menuItem.attrs.level }); + command(this.editorView.state, this.editorView.dispatch); + return; + } + } + }); + } +} + +const getSeperatorDom = (): HTMLElement => { + const div = document.createElement('div'); + div.className = `${MENU_ITEM_CLASSNAME}__Seperator`; + return div; +}; + +export const renderMenu = (toolbar: Toolbar, editorView: EditorView, menuDom: HTMLElement) => { + const updates: any[] = []; + + toolbar.forEach((group: ToolbarItem[], toolbarIndex: number): void => { + const isLastMenuGroup = toolbar.length - 1 === toolbarIndex; + + group.forEach((toolbarItem: ToolbarItem, menuIndex: number): void => { + const isLastMenuItem = group.length - 1 === menuIndex; + + // render dropdown + if (typeof toolbarItem === 'object') { + Object.keys(toolbarItem).forEach((dropdownGroup: ToolbarDropdownGroupKeys) => { + if (DROPDOWN_ITEMS.has(dropdownGroup)) { + const dropdown: ToolbarDropdownGroupValues = toolbarItem[dropdownGroup]; + + const dropdownView = new DropDownView(dropdownGroup, dropdown, editorView); + const rendered = dropdownView.render(); + updates.push(rendered.updates); + menuDom.appendChild(rendered.dom); + } else { + console.warn('Unkown dropdown group:', dropdownGroup); + } + }); + } + + // render Icons + if (typeof toolbarItem === 'string') { + const menuItem = menuItemsMeta[toolbarItem]; + + if (menuItem) { + const spec: MenuItemViewSpec = { + classNames: [ + MENU_ITEM_CLASSNAME, + `${MENU_ITEM_CLASSNAME}__Icon`, + `${MENU_ITEM_CLASSNAME}__${menuItem.key}` + ], + innerHTML: getIconSvg(menuItem.icon), + attrs: { + title: menuItem.key + } + }; + + const menuItemView = new MenuItemView(menuItem, editorView, spec); + const { update, dom } = menuItemView.render(); + + menuDom.appendChild(dom); + updates.push(update); + } + } + + if (isLastMenuItem && !isLastMenuGroup) { + const seperatorDom = getSeperatorDom(); + menuDom.appendChild(seperatorDom); + } + }); + }); + + const combinedUpdates = flatDeep(updates, Infinity); + + return { + update(state: EditorState) { + combinedUpdates.forEach((update: (state: EditorState) => void) => { + update(state); + }); + } + }; +}; diff --git a/src/lib/plugins/menu/meta.ts b/src/lib/plugins/menu/meta.ts index d8d3735f..ee60ea47 100644 --- a/src/lib/plugins/menu/meta.ts +++ b/src/lib/plugins/menu/meta.ts @@ -25,6 +25,48 @@ const menuItemsMeta: { [key: string]: MenuItemMeta } = { key: 'bullet_list', icon: 'bullet_list', type: 'node', + }, + h1: { + key: 'heading', + attrs: { + level: 1, + }, + type: 'node' + }, + h2: { + key: 'heading', + attrs: { + level: 2, + }, + type: 'node' + }, + h3: { + key: 'heading', + attrs: { + level: 3, + }, + type: 'node' + }, + h4: { + key: 'heading', + attrs: { + level: 4, + }, + type: 'node' + }, + h5: { + key: 'heading', + attrs: { + level: 5, + }, + type: 'node' + }, + h6: { + key: 'heading', + attrs: { + level: 6, + }, + type: 'node' } }; diff --git a/src/lib/pm-tools/commands/toggleBlockType.ts b/src/lib/pm-tools/commands/toggleBlockType.ts new file mode 100644 index 00000000..03f6eef4 --- /dev/null +++ b/src/lib/pm-tools/commands/toggleBlockType.ts @@ -0,0 +1,17 @@ +import { EditorState, Transaction } from 'prosemirror-state'; +import { NodeType } from 'prosemirror-model'; +import { setBlockType } from 'prosemirror-commands'; + +import isNodeActive from '../helpers/isNodeActive'; + +export default function toggleBlockType(type: NodeType, toggleType: NodeType, attrs = {}) { + return (state: EditorState, dispatch: (tr: Transaction) => void) => { + const isActive = isNodeActive(state, type, attrs); + + if (isActive) { + return setBlockType(toggleType)(state, dispatch); + } + + return setBlockType(type, attrs)(state, dispatch); + }; +} diff --git a/src/lib/pm-tools/helpers/isNodeActive.ts b/src/lib/pm-tools/helpers/isNodeActive.ts index bdb73d5d..1e7433d1 100644 --- a/src/lib/pm-tools/helpers/isNodeActive.ts +++ b/src/lib/pm-tools/helpers/isNodeActive.ts @@ -2,16 +2,18 @@ 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 isNodeActive = (state: EditorState, type: NodeType, attrs = {}): boolean => { + const { $from, to } = state.selection; const predicate = (n: ProsemirrorNode) => n.type === type; const node = findSelectedNodeOfType(type)(state.selection) || findParentNode(predicate)(state.selection); - return !!node; + + if (!Object.entries(attrs).length || !node) { + return !!node; + } + + // check if heading is active + return to <= $from.end() && $from.parent.hasMarkup(type, attrs); }; export default isNodeActive; diff --git a/src/lib/types.ts b/src/lib/types.ts index db08222e..e6c94dcb 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,8 @@ -export type Toolbar = string[][] | null; +export type ToolbarDropdown = { heading?: string[] }; +export type ToolbarDropdownGroupKeys = keyof ToolbarDropdown; +export type ToolbarDropdownGroupValues = ToolbarDropdown[ToolbarDropdownGroupKeys]; +export type ToolbarItem = string | ToolbarDropdown; +export type Toolbar = Array | null; export interface Config { toolbar: boolean | Toolbar; @@ -6,11 +10,22 @@ export interface Config { export interface MenuItemMeta { key: string; - icon: string; + icon?: string; type: 'mark' | 'node'; + label?: string; + attrs?: { + level?: number + }; command?: any; } +export interface MenuItemViewSpec { + classNames?: string[]; + innerHTML?: string; + textContent?: string; + attrs?: { [key: string]: string }; +} + export interface ComputedOptions extends Config { placeholder: string; toolbar: Toolbar; diff --git a/src/lib/utils/computeOptions.ts b/src/lib/utils/computeOptions.ts index 47a436c8..9763c7ab 100644 --- a/src/lib/utils/computeOptions.ts +++ b/src/lib/utils/computeOptions.ts @@ -6,8 +6,10 @@ interface Options { } const defaultToolbarOptions: Toolbar = [ - ['bold', 'italic', 'code'], - ['ordered_list', 'bullet_list'] + ['bold', 'italic'], + ['code'], + ['ordered_list', 'bullet_list'], + [{ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }] ]; const defaultConfig: Config = { diff --git a/src/lib/utils/flatDeep.ts b/src/lib/utils/flatDeep.ts new file mode 100644 index 00000000..b06dc312 --- /dev/null +++ b/src/lib/utils/flatDeep.ts @@ -0,0 +1,7 @@ +const flatDeep = (arr: any[], d = 1): any[] => { + return d > 0 + ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), []) + : arr.slice(); +}; + +export default flatDeep; diff --git a/src/lib/utils/icons/arrowDropDown.ts b/src/lib/utils/icons/arrowDropDown.ts new file mode 100644 index 00000000..3c9877e0 --- /dev/null +++ b/src/lib/utils/icons/arrowDropDown.ts @@ -0,0 +1,3 @@ +export default ` + +`; diff --git a/src/lib/utils/icons/index.ts b/src/lib/utils/icons/index.ts index 47f3f47d..70cb2a34 100644 --- a/src/lib/utils/icons/index.ts +++ b/src/lib/utils/icons/index.ts @@ -5,20 +5,22 @@ import italic from './italic'; import code from './code'; import orderedList from './ordered_list'; import bulletList from './bullet_list'; +import arrowDropDown from './arrowDropDown'; -const height = 20; -const width = 20; +const DEFAULT_ICON_HEIGHT = 20; +const DEFAULT_ICON_WIDTH = 20; const icons = { bold, italic, code, ordered_list: orderedList, - bullet_list: bulletList + bullet_list: bulletList, + arrow_drop_down: arrowDropDown }; // Helper function to create menu icons -export function getIconSvg(name: string) { +export function getIconSvg(name: string, width = DEFAULT_ICON_WIDTH, height = DEFAULT_ICON_HEIGHT) { const path = icons[name] || ''; return ` diff --git a/wiki/content/configuartion.md b/wiki/content/configuartion.md index 447e36e6..06da46f5 100644 --- a/wiki/content/configuartion.md +++ b/wiki/content/configuartion.md @@ -5,18 +5,24 @@ The Configuration can be provided using `config` property ## Usage ```html - + + ``` ### Default Configuration -The config property is a JSON object. +The config property is a JSON object. Each array is a group which is seperated by sperator. -```json +```jsonc { "toolbar": [ - ["bold", "italic", "code"], - ["ordered_list", "bullet_list"] + ["bold", "italic", "code"], // inline icons + ["ordered_list", "bullet_list"], + [{ "heading": ["h1", "h2", "h3", "h4", "h5", "h6"] }] // dropdown ] } ```