UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

362 lines (305 loc) • 10.2 kB
import { Mark, MarkType, Plugin, PluginKey, EditorState, EditorView, Schema, } from '../../prosemirror'; import * as commands from '../../commands'; import keymapHandler from './keymap'; import inputRulePlugin from './input-rule'; import { transformToCodeAction } from './transform-to-code'; export type StateChangeHandler = (state: TextFormattingState) => any; export type BlockTypeStateSubscriber = (state: TextFormattingState) => void; export class TextFormattingState { private changeHandlers: StateChangeHandler[] = []; private state: EditorState<any>; // public state emActive = false; emDisabled = false; emHidden = false; codeActive = false; codeDisabled = false; codeHidden = false; underlineActive = false; underlineDisabled = false; underlineHidden = false; strikeActive = false; strikeDisabled = false; strikeHidden = false; strongActive = false; strongDisabled = false; strongHidden = false; superscriptActive = false; superscriptDisabled = false; superscriptHidden = false; subscriptActive = false; subscriptDisabled = false; subscriptHidden = false; marksToRemove; keymapHandler; constructor(state: EditorState<any>) { this.state = state; this.emHidden = !state.schema.marks.em; this.strongHidden = !state.schema.marks.strong; this.underlineHidden = !state.schema.marks.underline; this.codeHidden = !state.schema.marks.code; this.superscriptHidden = !state.schema.marks.subsup; this.subscriptHidden = !state.schema.marks.subsup; this.strikeHidden = !state.schema.marks.strike; this.update(state); } toggleEm(view: EditorView): boolean { const { em } = this.state.schema.marks; if (em) { return this.toggleMark(view, em); } return false; } toggleCode(view: EditorView): boolean { const { code } = this.state.schema.marks; const { from, to } = this.state.selection; if (code) { if (!this.codeActive) { view.dispatch(transformToCodeAction(view.state, from, to)); return true; } return commands.toggleMark(code)(view.state, view.dispatch); } return false; } toggleStrike(view: EditorView) { const { strike } = this.state.schema.marks; if (strike) { return this.toggleMark(view, strike); } return false; } toggleStrong(view: EditorView) { const { strong } = this.state.schema.marks; if (strong) { return this.toggleMark(view, strong); } return false; } toggleSuperscript(view: EditorView) { const { subsup } = this.state.schema.marks; if (subsup) { if (this.subscriptActive) { // If subscript is enabled, turn it off first. return this.toggleMark(view, subsup); } return this.toggleMark(view, subsup, { type: 'sup' }); } return false; } toggleSubscript(view: EditorView): boolean { const { subsup } = this.state.schema.marks; if (subsup) { if (this.superscriptActive) { // If superscript is enabled, turn it off first. return this.toggleMark(view, subsup); } return this.toggleMark(view, subsup, { type: 'sub' }); } return false; } toggleUnderline(view: EditorView): boolean { const { underline } = this.state.schema.marks; if (underline) { return this.toggleMark(view, underline); } return false; } subscribe(cb: StateChangeHandler) { this.changeHandlers.push(cb); cb(this); } unsubscribe(cb: StateChangeHandler) { this.changeHandlers = this.changeHandlers.filter(ch => ch !== cb); } update(newEditorState: EditorState<any>) { this.state = newEditorState; const { state } = this; const { em, code, strike, strong, subsup, underline } = state.schema.marks; let dirty = false; if (code) { const newCodeActive = this.markActive(code.create()); if (newCodeActive !== this.codeActive) { this.codeActive = newCodeActive; dirty = true; } const newCodeDisabled = !commands.toggleMark(code)(this.state); if (newCodeDisabled !== this.codeDisabled) { this.codeDisabled = newCodeDisabled; dirty = true; } } if (em) { const newEmActive = this.anyMarkActive(em); if (newEmActive !== this.emActive) { this.emActive = newEmActive; dirty = true; } const newEmDisabled = !commands.toggleMark(em)(this.state); if (this.codeActive || newEmDisabled !== this.emDisabled) { this.emDisabled = this.codeActive ? true : newEmDisabled; dirty = true; } } if (strike) { const newStrikeActive = this.anyMarkActive(strike); if (newStrikeActive !== this.strikeActive) { this.strikeActive = newStrikeActive; dirty = true; } const newStrikeDisabled = !commands.toggleMark(strike)(this.state); if (this.codeActive || newStrikeDisabled !== this.strikeDisabled) { this.strikeDisabled = this.codeActive ? true : newStrikeDisabled; dirty = true; } } if (strong) { const newStrongActive = this.anyMarkActive(strong); if (newStrongActive !== this.strongActive) { this.strongActive = newStrongActive; dirty = true; } const newStrongDisabled = !commands.toggleMark(strong)(this.state); if (this.codeActive || newStrongDisabled !== this.strongDisabled) { this.strongDisabled = this.codeActive ? true : newStrongDisabled; dirty = true; } } if (subsup) { const subMark = subsup.create({ type: 'sub' }); const supMark = subsup.create({ type: 'sup' }); const newSubscriptActive = this.markActive(subMark); if (newSubscriptActive !== this.subscriptActive) { this.subscriptActive = newSubscriptActive; dirty = true; } const newSubscriptDisabled = !commands.toggleMark(subsup, { type: 'sub' })(this.state); if (this.codeActive || newSubscriptDisabled !== this.subscriptDisabled) { this.subscriptDisabled = this.codeActive ? true : newSubscriptDisabled; dirty = true; } const newSuperscriptActive = this.markActive(supMark); if (newSuperscriptActive !== this.superscriptActive) { this.superscriptActive = newSuperscriptActive; dirty = true; } const newSuperscriptDisabled = !commands.toggleMark(subsup, { type: 'sup' })(this.state); if (this.codeActive || newSuperscriptDisabled !== this.superscriptDisabled) { this.superscriptDisabled = this.codeActive ? true : newSuperscriptDisabled; dirty = true; } } if (underline) { const newUnderlineActive = this.anyMarkActive(underline); if (newUnderlineActive !== this.underlineActive) { this.underlineActive = newUnderlineActive; dirty = true; } const newUnderlineDisabled = !commands.toggleMark(underline)(this.state); if (this.codeActive || newUnderlineDisabled !== this.underlineDisabled) { this.underlineDisabled = this.codeActive ? true : newUnderlineDisabled; dirty = true; } } if (dirty) { this.triggerOnChange(); } } /** * Determine if a mark (with specific attribute values) exists anywhere in the selection. */ markActive(mark: Mark): boolean { const { state } = this; const { from, to, empty } = state.selection; let foundMark = false; if (this.marksToRemove) { this.marksToRemove.forEach(markToRemove => { if (markToRemove.type.name === mark.type.name) { foundMark = true; } }); } const currentMarkBefore = state.doc.rangeHasMark(from - 1, to, mark.type); const currentMarkAfter = state.doc.rangeHasMark(from, to, mark.type); if (foundMark && (!currentMarkBefore || ( !mark.type.spec.inclusive && !currentMarkAfter ))) { return false; } // When the selection is empty, only the active marks apply. if (empty) { return !!mark.isInSet(state.tr.storedMarks || state.selection.$from.marks()); } // For a non-collapsed selection, the marks on the nodes matter. let found = false; state.doc.nodesBetween(from, to, node => { found = found || mark.isInSet(node.marks); }); return found; } private triggerOnChange() { this.changeHandlers.forEach(cb => cb(this)); } /** * Determine if a mark of a specific type exists anywhere in the selection. */ private anyMarkActive(markType: MarkType): boolean { const { state } = this; const { from, to, empty } = state.selection; let found = false; if (this.marksToRemove) { this.marksToRemove.forEach(mark => { if (mark.type.name === markType.name) { found = true; } }); } if (found && !state.doc.rangeHasMark(from - 1, to, markType)) { return false; } if (empty) { return !!markType.isInSet(state.tr.storedMarks || state.selection.$from.marks()); } return state.doc.rangeHasMark(from, to, markType); } private toggleMark(view: EditorView, markType: MarkType, attrs?: any): boolean { // Disable text-formatting inside code if (this.codeActive ? this.codeDisabled : true) { return commands.toggleMark(markType, attrs)(view.state, view.dispatch); } return false; } } export const stateKey = new PluginKey('textFormatting'); export const plugin = new Plugin({ state: { init(config, state: EditorState<any>) { return new TextFormattingState(state); }, apply(tr, pluginState: TextFormattingState, oldState, newState) { pluginState.update(newState); return pluginState; } }, key: stateKey, view: (view: EditorView) => { const pluginState = stateKey.getState(view.state); pluginState.keymapHandler = keymapHandler(view, pluginState); return {}; }, props: { handleKeyDown(view, event) { return stateKey.getState(view.state).keymapHandler(view, event); } } }); const plugins = (schema: Schema<any, any>) => { return [plugin, inputRulePlugin(schema)].filter((plugin) => !!plugin) as Plugin[]; }; export default plugins;