diff --git a/docs/src/demos/Examples/Focus/index.spec.js b/docs/src/demos/Examples/Focus/index.spec.js index c5ebe63e..e98d7d12 100644 --- a/docs/src/demos/Examples/Focus/index.spec.js +++ b/docs/src/demos/Examples/Focus/index.spec.js @@ -6,6 +6,9 @@ context('focus', () => { describe('focus class', () => { it('should have class', () => { cy.get('.ProseMirror').window().then(window => { + const { editor } = window + editor.focus('start') + cy.get('.ProseMirror p:first').should('have.class', 'has-focus') }) }) diff --git a/docs/src/demos/Examples/Focus/index.vue b/docs/src/demos/Examples/Focus/index.vue index 4bd6400e..c7c08a5f 100644 --- a/docs/src/demos/Examples/Focus/index.vue +++ b/docs/src/demos/Examples/Focus/index.vue @@ -15,7 +15,7 @@ import Italic from '@tiptap/extension-italic' import Code from '@tiptap/extension-code' import CodeBlock from '@tiptap/extension-codeblock' import Heading from '@tiptap/extension-heading' -// import Focus from '@tiptap/extension-focus' +import Focus from '@tiptap/extension-focus' export default { components: { @@ -40,12 +40,12 @@ export default { new Code(), new CodeBlock(), new Heading(), - // new Focus({ - // className: 'has-focus', - // nested: true, - // }), + new Focus({ + className: 'has-focus', + nested: true, + }), ], - // autoFocus: true, + autoFocus: true, content: `

With the focus extension you can add custom classes to focused nodes. Default options: @@ -65,14 +65,6 @@ export default { window.editor = this.editor }, - watch: { - editable() { - this.editor.setOptions({ - editable: this.editable, - }) - }, - }, - beforeDestroy() { this.editor.destroy() }, diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index 887fb581..8e469281 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -22,6 +22,7 @@ import Mark from './Mark' import EventEmitter from './EventEmitter' import ComponentRenderer from './ComponentRenderer' +// commands import clearContent from './commands/clearContent' import deleteSelection from './commands/deleteSelection' import focus from './commands/focus' @@ -37,6 +38,9 @@ import toggleMark from './commands/toggleMark' import toggleNode from './commands/toggleNode' import updateMark from './commands/updateMark' +// plugins +import focusPlugin from './plugins/focus' + export type Command = (next: Function, editor: Editor) => (...args: any) => any export interface CommandSpec { @@ -70,6 +74,8 @@ export class Editor extends EventEmitter { injectCSS: true, extensions: [], } + public isFocused = false + public isEditable = true constructor(options: Partial = {}) { super() @@ -175,6 +181,7 @@ export class Editor extends EventEmitter { keymap(baseKeymap), dropCursor(), gapCursor(), + focusPlugin(this.proxy), ] } diff --git a/packages/core/src/plugins/focus.ts b/packages/core/src/plugins/focus.ts new file mode 100644 index 00000000..07fb7243 --- /dev/null +++ b/packages/core/src/plugins/focus.ts @@ -0,0 +1,28 @@ +import { Plugin } from 'prosemirror-state' +import Editor from '../..' + +export default (editor: Editor) => new Plugin({ + props: { + attributes: { + tabindex: '0', + }, + handleDOMEvents: { + focus: () => { + editor.isFocused = true + + const transaction = editor.state.tr.setMeta('focused', true) + editor.view.dispatch(transaction) + + return true + }, + blur: () => { + editor.isFocused = false + + const transaction = editor.state.tr.setMeta('focused', false) + editor.view.dispatch(transaction) + + return true + }, + }, + }, +}) \ No newline at end of file diff --git a/packages/extension-focus/index.ts b/packages/extension-focus/index.ts new file mode 100644 index 00000000..a0a5ac2a --- /dev/null +++ b/packages/extension-focus/index.ts @@ -0,0 +1,50 @@ +import { Extension } from '@tiptap/core' +import { Plugin } from 'prosemirror-state' +import { DecorationSet, Decoration } from 'prosemirror-view' + +export default class Focus extends Extension { + + name = 'focus' + + defaultOptions() { + return { + className: 'has-focus', + nested: false, + } + } + + plugins() { + return [ + new Plugin({ + props: { + decorations: ({ doc, plugins, selection }) => { + const { isFocused, isEditable } = this.editor + const isActive = isEditable && this.options.className + const { anchor } = selection + const decorations: Decoration[] = [] + + if (!isActive || !isFocused) { + return + } + + doc.descendants((node, pos) => { + const hasAnchor = anchor >= pos && anchor <= (pos + node.nodeSize) + + if (hasAnchor && !node.isText) { + const decoration = Decoration.node(pos, pos + node.nodeSize, { + class: this.options.className, + }) + decorations.push(decoration) + } + + return this.options.nested + }) + + return DecorationSet.create(doc, decorations) + }, + }, + }), + ] + } + +} diff --git a/packages/extension-focus/package.json b/packages/extension-focus/package.json new file mode 100644 index 00000000..dbcb9e4b --- /dev/null +++ b/packages/extension-focus/package.json @@ -0,0 +1,18 @@ +{ + "name": "@tiptap/extension-focus", + "version": "1.0.0", + "source": "index.ts", + "main": "dist/tiptap-extension-focus.js", + "umd:main": "dist/tiptap-extension-focus.umd.js", + "module": "dist/tiptap-extension-focus.mjs", + "unpkg": "dist/tiptap-extension-focus.js", + "jsdelivr": "dist/tiptap-extension-focus.js", + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@tiptap/core": "2.x", + "prosemirror-state": "^1.3.3" + } +}