UNPKG

@zodiac-ui/editor

Version:

A rich text editor for Angular based on `@atlaskit/editor-core`.

1,348 lines (1,310 loc) 193 kB
import { ReplaySubject, Subject } from 'rxjs'; import { findWrapping } from 'prosemirror-transform'; import { history, redo, undo } from 'prosemirror-history'; import * as BaseCodeMirrorNs from 'codemirror'; import 'codemirror/addon/mode/overlay'; import 'codemirror/addon/mode/multiplex'; import 'codemirror/addon/mode/simple'; import 'codemirror/mode/meta'; import { Schema, Node, Fragment, Slice } from 'prosemirror-model'; import * as LinkifyItNs from 'linkify-it'; import { EditorView, DecorationSet, Decoration } from 'prosemirror-view'; import { keydownHandler, keymap } from 'prosemirror-keymap'; import { EditorState, Selection, Plugin, PluginKey, TextSelection, NodeSelection } from 'prosemirror-state'; import { InputRule, textblockTypeInputRule, wrappingInputRule, inputRules, undoInputRule } from 'prosemirror-inputrules'; import { baseKeymap, chainCommands, exitCode, toggleMark } from 'prosemirror-commands'; import { __assign, __spread, __awaiter, __generator, __read, __extends } from 'tslib'; import { CommonModule } from '@angular/common'; import { InjectionToken, EventEmitter, Inject, Injectable, Optional, ChangeDetectionStrategy, Component, ElementRef, Input, Output, ViewEncapsulation, NgModule, Directive, ComponentFactoryResolver, ChangeDetectorRef, HostBinding, HostListener, Injector } from '@angular/core'; import { ComponentPortal } from '@angular/cdk/portal'; import { findParentNodeOfType, hasParentNodeOfType, safeInsert, findParentDomRefOfType, removeParentNodeOfType, setParentNodeMarkup } from 'prosemirror-utils'; import { ConnectionPositionPair, OverlayRef, Overlay } from '@angular/cdk/overlay'; import { MatButtonModule, MatIconModule, MatSelectModule, MatMenuModule, MatTooltipModule, MatFormFieldModule, MatInputModule } from '@angular/material'; var findChangedNodesFromTransaction = function (tr) { var nodes = []; var steps = (tr.steps || []); steps.forEach(function (step) { var to = step.to, from = step.from, slice = step.slice; var size = slice && slice.content ? slice.content.size : 0; var _loop_1 = function (i) { if (i <= tr.doc.content.size) { var topLevelNode_1 = tr.doc.resolve(i).node(1); if (topLevelNode_1 && !nodes.find(function (n) { return n === topLevelNode_1; })) { nodes.push(topLevelNode_1); } } }; for (var i = from; i <= to + size; i++) { _loop_1(i); } }); return nodes; }; /** Validates prosemirror nodes, and returns true only if all nodes are valid */ var validateNodes = function (nodes) { return nodes.every(function (node) { try { node.check(); // this will throw an error if the node is invalid } catch (error) { return false; } return true; }); }; var EventDispatcher = /** @class */ (function () { function EventDispatcher() { this.listeners = {}; } EventDispatcher.prototype.on = function (event, cb) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(cb); }; EventDispatcher.prototype.off = function (event, cb) { if (!this.listeners[event]) { return; } this.listeners[event] = this.listeners[event].filter(function (callback) { return callback !== cb; }); }; EventDispatcher.prototype.emit = function (event, data) { if (!this.listeners[event]) { return; } this.listeners[event].forEach(function (cb) { return cb(data); }); }; EventDispatcher.prototype.destroy = function () { this.listeners = {}; }; return EventDispatcher; }()); function createPMPlugins(_a) { var editorConfig = _a.editorConfig, schema = _a.schema, dispatch = _a.dispatch, eventDispatcher = _a.eventDispatcher; return editorConfig.pmPlugins // .sort(sortByOrder("plugins")) .map(function (_a) { var plugin = _a.plugin; return plugin({ schema: schema, dispatch: dispatch, // providerFactory, // errorReporter, eventDispatcher: eventDispatcher, }); }) .filter(function (plugin) { return !!plugin; }); } var docNode = { content: '(block|layoutSection)+', marks: 'alignment breakout indentation link', }; // import { Inline, MarksObject, NoMark } from './doc'; // import { AlignmentMarkDefinition, IndentationMarkDefinition } from '../marks'; // /** // * @name paragraph_node // */ // export interface ParagraphBaseDefinition { // type: 'paragraph'; // /** // * @allowUnsupportedInline true // */ // content?: Array<Inline>; // marks?: Array<any>; // } // // /** // * @name paragraph_with_no_marks_node // */ // export type ParagraphDefinition = ParagraphBaseDefinition & NoMark; // // /** // * NOTE: Need this because TS is too smart and inline everything. // * So we need to give them separate identity. // * Probably there's a way to solve it but that will need time and exploration. // * // http://bit.ly/2raXFX5 // * type T1 = X | Y // * type T2 = A | T1 | B // T2 = A | X | Y | B // */ // // /** // * @name paragraph_with_alignment_node // * @stage 0 // */ // export type ParagraphWithAlignmentDefinition = ParagraphBaseDefinition & // MarksObject<AlignmentMarkDefinition>; // // /** // * @name paragraph_with_indentation_node // * @stage 0 // */ // export type ParagraphWithIndentationDefinition = ParagraphBaseDefinition & // MarksObject<IndentationMarkDefinition>; // // export type ParagraphWithMarksDefinition = // | ParagraphWithAlignmentDefinition // | ParagraphWithIndentationDefinition; var pDOM = ['p', 0]; var paragraphNode = { content: 'inline*', group: 'block', marks: 'strong code em link strike subsup textColor typeAheadQuery underline mentionQuery emojiQuery confluenceInlineComment action annotation', parseDOM: [{ tag: 'p' }], toDOM: function () { return pDOM; }, }; var ALIGNMENT = "alignment"; var INDENTATION = "indentation"; var alignment = { excludes: ALIGNMENT + " " + INDENTATION, group: ALIGNMENT, attrs: { align: {}, }, parseDOM: [ { tag: 'div.fabric-editor-block-mark', getAttrs: function (dom) { var align = dom.getAttribute('data-align'); return align ? { align: align } : false; }, }, ], toDOM: function (mark) { return [ 'div', { class: "fabric-editor-block-mark fabric-editor-align-" + mark.attrs.align, 'data-align': mark.attrs.align, }, 0, ]; }, }; function isContentSupported(nodes, contentKey) { var nodeKeys = Object.keys(nodes); // content is with valid node if (nodeKeys.indexOf(contentKey) > -1) { return true; } // content is with valid group // tslint:disable-next-line for (var supportedKey in nodes) { var nodeSpec = nodes[supportedKey]; if (nodeSpec && nodeSpec.group === contentKey) { return true; } } return false; } function sanitizedContent(content, invalidContent) { if (!invalidContent.length) { return content || ''; } if (!content || !content.match(/\w/)) { return ''; } var newContent = content .replace(new RegExp("(" + invalidContent + "((\\s)*\\|)+)|((\\|(\\s)*)+" + invalidContent + ")|(" + invalidContent + "$)", 'g'), '') .replace(' ', ' ') .trim(); return newContent; } function sanitizeNodes(nodes, supportedMarks) { var nodeNames = Object.keys(nodes); nodeNames.forEach(function (nodeKey) { var nodeSpec = __assign({}, nodes[nodeKey]); if (nodeSpec.marks && nodeSpec.marks !== '_') { nodeSpec.marks = nodeSpec.marks .split(' ') .filter(function (mark) { return !!supportedMarks[mark]; }) .join(' '); } if (nodeSpec.content) { var content = nodeSpec.content.replace(/\W/g, ' '); var contentKeys = content.split(' '); var unsupportedContentKeys = contentKeys.filter(function (contentKey) { return !isContentSupported(nodes, contentKey); }); nodeSpec.content = unsupportedContentKeys.reduce(function (newContent, nodeName) { return sanitizedContent(newContent, nodeName); }, nodeSpec.content); } nodes[nodeKey] = nodeSpec; }); return nodes; } var textNode = { group: 'inline', }; var markGroupDeclarations = [ // groupDeclaration(COLOR), // groupDeclaration(FONT_STYLE), // groupDeclaration(SEARCH_QUERY), // groupDeclaration(LINK), ]; var ɵ0 = function (groupMark) { return groupMark.name; }; var markGroupDeclarationsNames = markGroupDeclarations.map(ɵ0); function sortByOrder(item) { // return function(a: { name: string }, b: { name: string }): number { // return Ranks[item].indexOf(a.name) - Ranks[item].indexOf(b.name); // }; return function () { return 0; }; } function fixExcludes(marks) { var markKeys = Object.keys(marks); var markGroups = new Set(markKeys.map(function (mark) { return marks[mark].group; })); markKeys.map(function (markKey) { var mark = marks[markKey]; if (mark.excludes) { mark.excludes = mark.excludes .split(' ') .filter(function (group) { return markGroups.has(group); }) .join(' '); } }); return marks; } function createSchema(editorConfig) { var marks = fixExcludes(editorConfig.marks.sort(sortByOrder('marks')).reduce(function (acc, mark) { acc[mark.name] = mark.mark; return acc; }, {})); var nodes = sanitizeNodes(editorConfig.nodes.sort(sortByOrder('nodes')).reduce(function (acc, node) { acc[node.name] = node.node; return acc; }, {}), marks); return new Schema({ nodes: nodes, marks: marks }); } /** * Creates a dispatch function that can be called inside ProseMirror Plugin * to notify listeners about that plugin's state change. */ function createDispatch(eventDispatcher) { return function (eventName, data) { if (!eventName) { throw new Error('event name is required!'); } var event = typeof eventName === 'string' ? eventName : eventName.key; eventDispatcher.emit(event, data); }; } function createConfig(plugins, editorProps) { var config = { nodes: [], marks: [], pmPlugins: [], }; var pluginsOptions = plugins.reduce(function (acc, plugin) { if (plugin.pluginsOptions) { Object.keys(plugin.pluginsOptions).forEach(function (pluginName) { if (!acc[pluginName]) { acc[pluginName] = []; } acc[pluginName].push(plugin.pluginsOptions[pluginName]); }); } return acc; }, {}); return plugins.reduce(function (acc, plugin) { var _a, _b, _c; if (plugin.pmPlugins) { (_a = acc.pmPlugins).push.apply(_a, __spread(plugin.pmPlugins(plugin.name ? pluginsOptions[plugin.name] : undefined))); } if (plugin.nodes) { (_b = acc.nodes).push.apply(_b, __spread(plugin.nodes(editorProps))); } if (plugin.marks) { (_c = acc.marks).push.apply(_c, __spread(plugin.marks(editorProps))); } // // if (plugin.contentComponent) { // acc.contentComponents.push(plugin.contentComponent); // } // // if (plugin.primaryToolbarComponent) { // acc.primaryToolbarComponents.push(plugin.primaryToolbarComponent); // } // // if (plugin.secondaryToolbarComponent) { // acc.secondaryToolbarComponents.push(plugin.secondaryToolbarComponent); // } return acc; }, config); } var EDITOR_PLUGIN = new InjectionToken("EDITOR_PLUGIN"); var STATE_HANDLER = new InjectionToken("STATE_HANDLER"); var defaultState = { content: [], type: "doc", }; var EditorService = /** @class */ (function () { function EditorService(plugins, handlers) { this.eventDispatcher = new EventDispatcher(); this.viewChange = new EventEmitter(); this.stateChange = new ReplaySubject(1); this.plugins = plugins; this.handlers = handlers; } EditorService.prototype.runTool = function (tool) { tool.run(this.state, this.view.dispatch, this.view); }; EditorService.prototype.updateState = function (state) { if (this.view) { this.createEditorState(state); this.view.updateState(this.state); } }; EditorService.prototype.createEditorState = function (state) { this.config = createConfig(this.plugins, {}); var schema = createSchema(this.config); var dispatch = createDispatch(this.eventDispatcher); var doc = Node.fromJSON(schema, state ? state.doc : defaultState); var selection = state ? Selection.fromJSON(doc, state.selection) : undefined; // const errorReporter = createErrorReporter(errorReporterHandler); var plugins = createPMPlugins({ schema: schema, dispatch: dispatch, editorConfig: this.config, eventDispatcher: this.eventDispatcher, }); // let doc; // if (options.replaceDoc) { // doc = // this.contentTransformer && typeof defaultValue === 'string' // ? this.contentTransformer.parse(defaultValue) // : processRawValue(schema, defaultValue); // } // let selection: Selection | undefined; // if (doc) { // // ED-4759: Don't set selection at end for full-page editor - should be at start // selection = // options.props.editorProps.appearance === 'full-page' // ? Selection.atStart(doc) // : Selection.atEnd(doc); // } // // Workaround for ED-3507: When media node is the last element, scrollIntoView throws an error // const patchedSelection = selection // ? Selection.findFrom(selection.$head, -1, true) || undefined // : undefined; this.state = EditorState.create({ schema: schema, plugins: plugins, doc: doc, selection: selection }); }; EditorService.prototype.createEditorView = function (node) { var _this = this; // Creates the editor-view from this.editorState. If an editor has been mounted // previously, this will contain the previous state of the editor. this.view = new EditorView({ mount: node }, { state: this.state, dispatchTransaction: function (transaction) { if (!_this.view) { return; } var nodes = findChangedNodesFromTransaction(transaction); if (validateNodes(nodes)) { // go ahead and update the state now we know the transaction is good var editorState = _this.view.state.apply(transaction); _this.view.updateState(editorState); if (transaction.docChanged) { _this.viewChange.emit(_this); } _this.state = editorState; _this.stateChange.next(_this); } // else { // const documents = { // new: getDocStructure(transaction.doc), // prev: getDocStructure(transaction.docs[0]), // }; // } }, }); this.viewChange.emit(this); this.stateChange.next(this); if (this.handlers) { this.stateChange.subscribe(function (editor) { _this.handlers.forEach(function (handler) { return handler(editor); }); }); } }; EditorService.decorators = [ { type: Injectable } ]; /** @nocollapse */ EditorService.ctorParameters = function () { return [ { type: Array, decorators: [{ type: Inject, args: [EDITOR_PLUGIN,] }] }, { type: Array, decorators: [{ type: Optional }, { type: Inject, args: [STATE_HANDLER,] }] } ]; }; return EditorService; }()); function hasChanges(change) { return change && change.previousValue !== change.currentValue; } var EditorComponent = /** @class */ (function () { function EditorComponent(editor, editorRef) { this.editor = editor; this.viewChange = editor.viewChange; this.stateChange = editor.stateChange; this.editorRef = editorRef; } EditorComponent.prototype.ngOnInit = function () { this.editor.createEditorState(this.state); this.editor.createEditorView(this.editorRef.nativeElement); }; EditorComponent.prototype.ngOnChanges = function (changes) { if (hasChanges(changes.state)) { this.editor.updateState(this.state); } }; EditorComponent.prototype.runTool = function (command) { this.editor.runTool(command); }; EditorComponent.decorators = [ { type: Component, args: [{ selector: "z-editor", template: "<!-- use host -->", viewProviders: [EditorService], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: [".ProseMirror{display:block;overflow-wrap:break-word;white-space:pre-wrap}.ProseMirror:focus{outline:0}.fabric-editor-align-center{text-align:center}.fabric-editor-align-end{text-align:right}.CodeMirror{border:1px solid #eee;height:auto}"] }] } ]; /** @nocollapse */ EditorComponent.ctorParameters = function () { return [ { type: EditorService }, { type: ElementRef } ]; }; EditorComponent.propDecorators = { state: [{ type: Input }], viewChange: [{ type: Output }], stateChange: [{ type: Output }] }; return EditorComponent; }()); var EditorModule = /** @class */ (function () { function EditorModule() { } EditorModule.decorators = [ { type: NgModule, args: [{ imports: [ CommonModule, ], declarations: [EditorComponent], exports: [EditorComponent], },] } ]; return EditorModule; }()); var EditorToolbar = /** @class */ (function () { function EditorToolbar() { } return EditorToolbar; }()); var EditorToolbarComponent = /** @class */ (function () { function EditorToolbarComponent() { this.viewChange = new Subject(); this.stateChange = new Subject(); } EditorToolbarComponent.prototype.ngOnInit = function () { this.editor.viewChange.subscribe(this.viewChange); this.editor.stateChange.subscribe(this.stateChange); }; EditorToolbarComponent.prototype.runTool = function (tool) { if (this.editor) { this.editor.runTool(tool); } }; EditorToolbarComponent.decorators = [ { type: Component, args: [{ selector: "z-editor-toolbar", template: "\n <ng-content></ng-content>\n ", changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: EditorToolbar, useExisting: EditorToolbarComponent }], styles: [""] }] } ]; /** @nocollapse */ EditorToolbarComponent.ctorParameters = function () { return []; }; EditorToolbarComponent.propDecorators = { editor: [{ type: Input }] }; return EditorToolbarComponent; }()); var EditorToolbarButtonDirective = /** @class */ (function () { function EditorToolbarButtonDirective() { } EditorToolbarButtonDirective.decorators = [ { type: Directive, args: [{ selector: "[zEditorToolbarButton]", },] } ]; /** @nocollapse */ EditorToolbarButtonDirective.ctorParameters = function () { return []; }; EditorToolbarButtonDirective.propDecorators = { zEditorToolbarButton: [{ type: Input }] }; return EditorToolbarButtonDirective; }()); var EditorToolbarModule = /** @class */ (function () { function EditorToolbarModule() { } EditorToolbarModule.decorators = [ { type: NgModule, args: [{ declarations: [EditorToolbarComponent, EditorToolbarButtonDirective], exports: [EditorToolbarComponent], imports: [ CommonModule, MatButtonModule, MatSelectModule, MatIconModule ], },] } ]; return EditorToolbarModule; }()); var getActiveAlignment = function (state) { var node = findParentNodeOfType([ state.schema.nodes.paragraph, state.schema.nodes.heading, ])(state.selection); var getMark = node && node.node.marks.filter(function (mark) { return mark.type === state.schema.marks.alignment; })[0]; return (getMark && getMark.attrs.align) || 'start'; }; var toggleBlockMark = function (markType, getAttrs, allowedBlocks) { return function (state, dispatch) { var _a = state.selection, from = _a.from, to = _a.to; var markApplied = false; var tr = state.tr; state.doc.nodesBetween(from, to, function (node, pos, parent) { if (!node.type.isBlock) { return false; } if ((!allowedBlocks || (Array.isArray(allowedBlocks) ? allowedBlocks.indexOf(node.type) > -1 : allowedBlocks(state.schema, node, parent))) && parent.type.allowsMarkType(markType)) { var oldMarks = node.marks.filter(function (mark) { return mark.type === markType; }); var prevAttrs = oldMarks.length ? oldMarks[0].attrs : undefined; var newAttrs = getAttrs(prevAttrs, node); if (newAttrs !== undefined) { tr.setNodeMarkup(pos, node.type, node.attrs, node.marks .filter(function (mark) { return !markType.excludes(mark.type); }) .concat(newAttrs === false ? [] : markType.create(newAttrs))); markApplied = true; } } }); if (markApplied && tr.docChanged) { if (dispatch) { dispatch(tr.scrollIntoView()); } return true; } return false; }; }; var cascadeCommands = function (cmds) { return function (state, dispatch) { var baseTr = state.tr; var shouldDispatch = false; var onDispatchAction = function (tr) { tr.steps.forEach(function (st) { baseTr.step(st); }); shouldDispatch = true; }; cmds.forEach(function (cmd) { cmd(state, onDispatchAction); }); if (dispatch && shouldDispatch) { dispatch(baseTr); return true; } return false; }; }; var isAlignable = function (align) { return function (state, dispatch) { var _a = state.schema, _b = _a.nodes, paragraph = _b.paragraph, heading = _b.heading, alignment = _a.marks.alignment; return toggleBlockMark(alignment, function () { return (!align ? undefined : align === "start" ? false : { align: align }); }, [paragraph, heading])(state, dispatch); }; }; var changeAlignment = function (align) { return function (state, dispatch) { var _a = state.schema, _b = _a.nodes, paragraph = _b.paragraph, heading = _b.heading, alignment = _a.marks.alignment; return cascadeCommands([ changeImageAlignment(align), toggleBlockMark(alignment, function () { return (!align ? undefined : align === 'start' ? false : { align: align }); }, [paragraph, heading]), ])(state, dispatch); }; }; var changeImageAlignment = function (align) { return function (state, dispatch) { var _a = state.selection, from = _a.from, to = _a.to; var tr = state.tr; state.doc.nodesBetween(from, to, function (node, pos, parent) { if (node.type === state.schema.nodes.mediaSingle) { tr.setNodeMarkup(pos, undefined, __assign({}, node.attrs, { layout: align === 'center' ? 'center' : "align-" + align })); } }); if (tr.docChanged && dispatch) { dispatch(tr.scrollIntoView()); return true; } return false; }; }; var pluginKey = new PluginKey("alignmentPlugin"); var defaultConfig = { align: 'left', }; function createInitialPluginState(editorState, pluginConfig) { return { align: getActiveAlignment(editorState) || pluginConfig.align, isEnabled: true, }; } function createPlugin(dispatch, pluginConfig) { return new Plugin({ key: pluginKey, state: { init: function (config, editorState) { return createInitialPluginState(editorState, pluginConfig); }, apply: function (tr, state, prevState, nextState) { var nextPluginState = getActiveAlignment(nextState); var isEnabled = isAlignable(nextPluginState)(nextState, /** * NOTE: Stan is already making dispatch optional in another PR. * We can remove this once it's merged. */ undefined); var newState = __assign({}, state, { align: nextPluginState, isEnabled: isEnabled }); if (nextPluginState !== state.align || isEnabled !== state.isEnabled) { dispatch(pluginKey, newState); } return newState; }, }, }); } var alignmentPlugin = { name: "alignmentPlugin", marks: function () { return [{ name: "alignment", mark: alignment }]; }, pmPlugins: function () { return [ { name: "alignmentPlugin", plugin: function (_a) { var dispatch = _a.dispatch; return createPlugin(dispatch, defaultConfig); }, } ]; } }; var ɵ0$1 = alignmentPlugin; var AlignmentModule = /** @class */ (function () { function AlignmentModule() { } AlignmentModule.decorators = [ { type: NgModule, args: [{ providers: [{ provide: EDITOR_PLUGIN, useValue: ɵ0$1, multi: true }], },] } ]; return AlignmentModule; }()); var hasInvalidSteps = function (tr) { return (tr.steps || []).some(function (step) { return step.from > step.to; }); }; var filterStepsPlugin = function () { return new Plugin({ filterTransaction: function (tr) { if (hasInvalidSteps(tr)) { // tslint:disable-next-line:no-console console.warn('The transaction was blocked because it contains invalid steps', tr.steps); return false; } return true; }, }); }; var focusStateKey = new PluginKey('focusStatePlugin'); var focusHandlerPlugin = function (dispatch) { return new Plugin({ key: focusStateKey, state: { init: function () { return true; }, apply: function (tr, wasEditorFocused) { var meta = tr.getMeta(focusStateKey); if (typeof meta === 'boolean') { if (meta !== wasEditorFocused) { dispatch(focusStateKey, meta); return meta; } } return wasEditorFocused; }, }, props: { handleDOMEvents: { click: function (view) { var isEditorFocused = focusStateKey.getState(view.state); if (!isEditorFocused) { view.dispatch(view.state.tr.setMeta(focusStateKey, view.hasFocus())); } return false; }, focus: function (view) { var isEditorFocused = focusStateKey.getState(view.state); if (!isEditorFocused) { view.dispatch(view.state.tr.setMeta(focusStateKey, true)); } return false; }, blur: function (view) { var isEditorFocused = focusStateKey.getState(view.state); if (isEditorFocused) { view.dispatch(view.state.tr.setMeta(focusStateKey, false)); } return false; }, }, }, }); }; var filterCommands = function (predicates, cmd) { return function (state, dispatch, view) { if (!Array.isArray(predicates)) { predicates = [predicates]; } if (predicates.some(function (pred) { return !pred(state, view); })) { return false; } return cmd(state, dispatch, view) || false; }; }; // import { typeAheadPluginKey } from '../../../plugins/type-ahead'; // import { emojiPluginKey } from '../../../plugins/emoji/pm-plugins/main'; var newlinePreserveMarksKey = new PluginKey('newlinePreserveMarksPlugin'); var isSelectionEndOfParagraph = function (state) { return state.selection.$to.parent.type === state.schema.nodes.paragraph && state.selection.$to.pos === state.doc.resolve(state.selection.$to.pos).end(); }; var isSelectionAligned = function (state) { return !!state.selection.$to.parent.marks.find(function (m) { return m.type === state.schema.marks.alignment; }); }; var isTypeaheadNotDisplaying = function (state) { return true; }; // !typeAheadPluginKey.getState(state).active && // !emojiPluginKey.getState(state).queryActive; var splitBlockPreservingMarks = function (state, dispatch) { dispatch(state.tr.split(state.tr.mapping.map(state.selection.$from.pos), 1)); return true; }; var newlinePreserveMarksPlugin = function () { return new Plugin({ key: newlinePreserveMarksKey, props: { handleKeyDown: keydownHandler({ Enter: filterCommands([ isSelectionEndOfParagraph, isSelectionAligned, isTypeaheadNotDisplaying, ], splitBlockPreservingMarks), }), }, }); }; var ZWSP = '\u200b'; var inlineCursorTargetStateKey = new PluginKey('inlineCursorTargetPlugin'); var SPECIAL_NODES = ['mention', 'emoji']; var isSpecial = function (node) { return node && SPECIAL_NODES.indexOf(node.type.name) !== -1; }; var findSpecialNodeAfter = function ($pos, tr) { if (isSpecial($pos.nodeAfter)) { return $pos.pos + 1; } var parentOffset = $pos.parentOffset, parent = $pos.parent; var docSize = tr.doc.nodeSize - 2; if (parentOffset === parent.content.size && $pos.pos + 1 < docSize - 2) { var nodeAfter = tr.doc.resolve($pos.pos + 1).nodeAfter; if (nodeAfter && isSpecial(nodeAfter.firstChild)) { return $pos.pos + 2; } } }; var findSpecialNodeBefore = function ($pos, tr) { if (isSpecial($pos.nodeBefore)) { return $pos.pos - 1; } if ($pos.pos === 0) { return; } var parentOffset = $pos.parentOffset; if (parentOffset === 0) { var nodeBefore = tr.doc.resolve($pos.pos - 1).nodeBefore; if (nodeBefore && isSpecial(nodeBefore.firstChild)) { return $pos.pos - 2; } } }; var inlineCursorTargetPlugin = function () { return new Plugin({ key: inlineCursorTargetStateKey, state: { init: function () { return ({ positions: [] }); }, apply: function (tr) { var selection = tr.selection; var $from = selection.$from; var positions = []; var posAfter = findSpecialNodeAfter($from, tr); var posBefore = findSpecialNodeBefore($from, tr); if (posAfter !== undefined) { positions.push(posAfter); } if (posBefore !== undefined) { positions.push(posBefore); } return { positions: positions }; }, }, props: { decorations: function (state) { var doc = state.doc; var positions = inlineCursorTargetStateKey.getState(state).positions; if (positions && positions.length) { var decorations = positions.map(function (position) { var node = document.createElement('span'); node.appendChild(document.createTextNode(ZWSP)); return Decoration.widget(position, node, { raw: true, side: -1, }); }); return DecorationSet.create(doc, decorations); } return null; }, }, }); }; var basePlugin = { pmPlugins: function () { return [ { name: 'filterStepsPlugin', plugin: function () { return filterStepsPlugin(); }, }, { name: 'inlineCursorTargetPlugin', plugin: function () { return inlineCursorTargetPlugin(); }, }, { name: 'focusHandlerPlugin', plugin: function (_a) { var dispatch = _a.dispatch; return focusHandlerPlugin(dispatch); }, }, { name: 'newlinePreserveMarksPlugin', plugin: newlinePreserveMarksPlugin, }, { name: 'history', plugin: function () { return history(); } }, // should be last :( { name: 'codeBlockIndent', plugin: function () { return keymap(__assign({}, baseKeymap, { 'Mod-[': function () { return true; }, 'Mod-]': function () { return true; } })); }, }, ]; }, }; var ɵ0$4 = basePlugin; var BaseModule = /** @class */ (function () { function BaseModule() { } BaseModule.decorators = [ { type: NgModule, args: [{ providers: [{ provide: EDITOR_PLUGIN, useValue: ɵ0$4, multi: true }], },] } ]; return BaseModule; }()); // The names of the blocks don't map precisely to schema nodes, because // of concepts like "paragraph" <-> "Normal text" and "Unknown". // // (there are also different blocks for different types of panel, when // they're really all just a panel node) // // Rather than half-match half-not, this plugin introduces its own // nomenclature for what 'block type' is active. var messages = ({ normal: { id: 'fabric.editor.normal', defaultMessage: 'Normal text', description: 'This is the default text style', }, heading1: { id: 'fabric.editor.heading1', defaultMessage: 'Heading 1', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, heading2: { id: 'fabric.editor.heading2', defaultMessage: 'Heading 2', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, heading3: { id: 'fabric.editor.heading3', defaultMessage: 'Heading 3', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, heading4: { id: 'fabric.editor.heading4', defaultMessage: 'Heading 4', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, heading5: { id: 'fabric.editor.heading5', defaultMessage: 'Heading 5', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, heading6: { id: 'fabric.editor.heading6', defaultMessage: 'Heading 6', description: 'Used for the title of a section of your document, headings run from 1 (largest size) to 6 (smallest size)', }, blockquote: { id: 'fabric.editor.blockquote', defaultMessage: 'Quote', description: 'Quote some text', }, codeblock: { id: 'fabric.editor.codeblock', defaultMessage: 'Code snippet', description: 'Insert a snippet/segment of code (code block)', }, panel: { id: 'fabric.editor.panel', defaultMessage: 'Panel', description: 'Visually distinguishes your text by adding a background colour (blue, purple, yellow, green, red)', }, notePanel: { id: 'fabric.editor.notePanel', defaultMessage: 'Note panel', description: 'Visually distinguishes your text by adding a note panel', }, successPanel: { id: 'fabric.editor.successPanel', defaultMessage: 'Success panel', description: 'Visually distinguishes your text by adding a success panel', }, warningPanel: { id: 'fabric.editor.warningPanel', defaultMessage: 'Warning panel', description: 'Visually distinguishes your text by adding a warning panel', }, errorPanel: { id: 'fabric.editor.errorPanel', defaultMessage: 'Error panel', description: 'Visually distinguishes your text by adding a error panel', }, other: { id: 'fabric.editor.other', defaultMessage: 'Others...', description: 'Other text formatting', }, }); var NORMAL_TEXT = { name: 'normal', title: messages.normal, nodeName: 'paragraph', tagName: 'p', }; var HEADING_1 = { name: 'heading1', title: messages.heading1, nodeName: 'heading', tagName: 'h1', level: 1, }; var HEADING_2 = { name: 'heading2', title: messages.heading2, nodeName: 'heading', tagName: 'h2', level: 2, }; var HEADING_3 = { name: 'heading3', title: messages.heading3, nodeName: 'heading', tagName: 'h3', level: 3, }; var HEADING_4 = { name: 'heading4', title: messages.heading4, nodeName: 'heading', tagName: 'h4', level: 4, }; var HEADING_5 = { name: 'heading5', title: messages.heading5, nodeName: 'heading', tagName: 'h5', level: 5, }; var HEADING_6 = { name: 'heading6', title: messages.heading6, nodeName: 'heading', tagName: 'h6', level: 6, }; var BLOCK_QUOTE = { name: 'blockquote', title: messages.blockquote, nodeName: 'blockquote', }; var CODE_BLOCK = { name: 'codeblock', title: messages.codeblock, nodeName: 'codeBlock', }; var PANEL = { name: 'panel', title: messages.panel, nodeName: 'panel', }; var OTHER = { name: 'other', title: messages.other, nodeName: '', }; var TEXT_BLOCK_TYPES = [ NORMAL_TEXT, HEADING_1, HEADING_2, HEADING_3, HEADING_4, HEADING_5, HEADING_6, ]; var WRAPPER_BLOCK_TYPES = [BLOCK_QUOTE, CODE_BLOCK, PANEL]; var HEADINGS_BY_LEVEL = TEXT_BLOCK_TYPES.reduce(function (acc, blockType) { if (blockType.level && blockType.nodeName === 'heading') { acc[blockType.level] = blockType; } return acc; }, {}); var HEADINGS_BY_NAME = TEXT_BLOCK_TYPES.reduce(function (acc, blockType) { if (blockType.level && blockType.nodeName === 'heading') { acc[blockType.name] = blockType; } return acc; }, {}); function getSelectedWrapperNodes(state) { var nodes = []; if (state.selection) { var _a = state.selection, $from = _a.$from, $to = _a.$to; var _b = state.schema.nodes, blockquote_1 = _b.blockquote, panel_1 = _b.panel, orderedList_1 = _b.orderedList, bulletList_1 = _b.bulletList, listItem_1 = _b.listItem, codeBlock_1 = _b.codeBlock; state.doc.nodesBetween($from.pos, $to.pos, function (node, pos) { if ((node.isBlock && [blockquote_1, panel_1, orderedList_1, bulletList_1, listItem_1].indexOf(node.type) >= 0) || node.type === codeBlock_1) { nodes.push(node.type); } }); } return nodes; } function defaultInputRuleHandler(inputRule, isBlockNodeRule) { if (isBlockNodeRule === void 0) { isBlockNodeRule = false; } var originalHandler = inputRule.handler; inputRule.handler = function (state, match, start, end) { // Skip any input rule inside code // https://product-fabric.atlassian.net/wiki/spaces/E/pages/37945345/Editor+content+feature+rules#Editorcontent/featurerules-Rawtextblocks var unsupportedMarks = isBlockNodeRule ? hasUnsupportedMarkForBlockInputRule(state, start, end) : hasUnsupportedMarkForInputRule(state, start, end); if (state.selection.$from.parent.type.spec.code || unsupportedMarks) { return; } return originalHandler(state, match, start, end); }; return inputRule; } function createInputRule(match, handler, isBlockNodeRule) { if (isBlockNodeRule === void 0) { isBlockNodeRule = false; } return defaultInputRuleHandler(new InputRule(match, handler), isBlockNodeRule); } // ProseMirror uses the Unicode Character 'OBJECT REPLACEMENT CHARACTER' (U+FFFC) as text representation for // leaf nodes, i.e. nodes that don't have any content or text property (e.g. hardBreak, emoji, mention, rule) // It was introduced because of https://github.com/ProseMirror/prosemirror/issues/262 // This can be used in an input rule regex to be able to include or exclude such nodes. var leafNodeReplacementCharacter = '\ufffc'; var hasUnsupportedMarkForBlockInputRule = function (state, start, end) { var doc = state.doc, marks = state.schema.marks; var unsupportedMarksPresent = false; var isUnsupportedMark = function (node) { return node.type === marks.code || node.type === marks.link || node.type === marks.typeAheadQuery; }; doc.nodesBetween(start, end, function (node) { unsupportedMarksPresent = unsupportedMarksPresent || node.marks.filter(isUnsupportedMark).length > 0; }); return unsupportedMarksPresent; }; var hasUnsupportedMarkForInputRule = function (state, start, end) { var doc = state.doc, marks = state.schema.marks; var unsupportedMarksPresent = false; var isCodemark = function (node) { return node.type === marks.code || node.type === marks.typeAheadQuery; }; doc.nodesBetween(start, end, function (node) { unsupportedMarksPresent = unsupportedMarksPresent || node.marks.filter(isCodemark).length > 0; }); return unsupportedMarksPresent; }; /** * A helper to get the underlying array of a fragment. */ function getFragmentBackingArray(fragment) { return fragment.content; } function mapFragment(content, callback, parent) { if (parent === void 0) { parent = null; } var children = []; for (var i = 0, size = content.childCount; i < size; i++) { var node = content.child(i); var transformed = node.isLeaf ? callback(node, parent, i) : callback(node.copy(mapFragment(node.content, callback, node)), parent, i); if (transformed) { if (transformed instanceof Fragment) { children.push.apply(children, __spread(getFragmentBackingArray(transformed))); } else if (Array.isArray(transformed)) { children.push.apply(children, __spread(transformed)); } else { children.push(transformed); } } } return Fragment.fromArray(children); } function mapSlice(slice, callback) { var fragment = mapFragment(slice.content, callback); return new Slice(fragment, slice.openStart, slice.openEnd); } function setNormalText() { return function (state, dispatch) { var tr = state.tr, _a = state.selection, $from = _a.$from, $to = _a.$to, schema = state.schema; if (dispatch) { dispatch(tr.setBlockType($from.pos, $to.pos, schema.nodes.paragraph)); } return true; }; } function setHeading(level) { return function (state, dispatch) { var tr = state.tr, _a = state.selection, $from = _a.$from, $to = _a.$to, schema = state.schema; if (dispatch) { dispatch(tr.setBlockType($from.pos, $to.pos, schema.nodes.heading, { level: level })); } return true; }; } function setBlockType(name) { return function (state, dispatch) { var nodes = state.schema.nodes; if (name === NORMAL_TEXT.name && nodes.paragraph) { return setNormalText()(state, dispatch); } var headingBlockType = HEADINGS_BY_NAME[name]; if (headingBlockType && nodes.heading && headingBlockType.level) { return setHeading(headingBlockType.level)(state, dispatch); } return false; }; } function transformToCodeBlockAction(state, attrs) { var codeBlock = state.schema.nodes.codeBlock; if (state.selection.empty) { var startOfCodeBlockText = state.selection.$from; var parentPos = startOfCodeBlockText.before(); var end = startOfCodeBlockText.end(); var slice = mapSlice(state.doc.slice(startOfCodeBlockText.pos, end), function (node) { if (node.type === state.schema.nodes.hardBreak) { return state.schema.text('\n'); } if (node.isText) { return node.mark([]); } else if (node.isInline) { return node.attrs.text ? state.schema.text(node.attrs.text) : null; } else { return node.content.childCount ? node.content : null; } }); var tr = state.tr.replaceRange(startOfCodeBlockText.pos, end, slice); // If our offset isnt at 3 (backticks) at the start of line, cater for content. if (startOfCodeBlockText.parentOffset >= 3) { return tr.split(startOfCodeBlockText.pos, undefined, [ { type: codeBlock, attrs: attrs }, ]); } return tr.setNodeMarkup(parentPos, codeBlock, attrs); } return state.tr; } function isConvertableToCodeBlock(state) { // Before a document is loaded, there is no selection. if (!state.selection) { return false; } var $from = state.selection.$from; var node = $from.parent; if (!node.isTextblock || node.type === state.schema.nodes.codeBlock) { return false; } var parentDepth = $from.depth - 1; var parentNode = $from.node(parentDepth); var index = $from.index(parentDepth); return parentNode.canReplaceWith(index, index + 1, state.schema.nodes.codeBlock); } /** * Function will insert code block at current selection if block is empty or below current selection and set focus on it. */ function insertCodeBlock() { return function (state, dispatch) { var tr = state.tr; var $to = state.selection.$to; var codeBlock = state.schema.nodes.codeBlock; var getNextNode = state.doc.nodeAt($to.pos + 1); var addPos = getNextNode && getNextNode.isText ? 0 : 1; /** We always want to append a block type */ tr.replaceRangeWith($to.pos + addPos, $to.pos + addPos, codeBlock.createAndFill()); tr.setSelection(Selection.near(tr.doc.resolve(state.selection.to + addPos))); if (dispatch) {