├── .npmrc ├── .gitignore ├── .npmignore ├── package.json ├── LICENSE ├── README.md ├── CHANGELOG.md ├── CONTRIBUTING.md └── src └── dropcursor.ts /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-port 3 | /dist 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-port 3 | /test 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prosemirror-dropcursor", 3 | "version": "1.8.2", 4 | "description": "Drop cursor plugin for ProseMirror", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "sideEffects": false, 14 | "license": "MIT", 15 | "maintainers": [ 16 | { 17 | "name": "Marijn Haverbeke", 18 | "email": "marijn@haverbeke.berlin", 19 | "web": "http://marijnhaverbeke.nl" 20 | } 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "git://github.com/prosemirror/prosemirror-dropcursor.git" 25 | }, 26 | "dependencies": { 27 | "prosemirror-state": "^1.0.0", 28 | "prosemirror-view": "^1.1.0", 29 | "prosemirror-transform": "^1.1.0" 30 | }, 31 | "devDependencies": { 32 | "@prosemirror/buildhelper": "^0.1.5" 33 | }, 34 | "scripts": { 35 | "test": "pm-runtests", 36 | "prepare": "pm-buildhelper src/dropcursor.ts" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2017 by Marijn Haverbeke and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prosemirror-dropcursor 2 | 3 | [ [**WEBSITE**](https://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror-dropcursor/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**CHANGELOG**](https://github.com/ProseMirror/prosemirror-dropcursor/blob/master/CHANGELOG.md) ] 4 | 5 | This is a non-core example module for [ProseMirror](https://prosemirror.net). 6 | ProseMirror is a well-behaved rich semantic content editor based on 7 | contentEditable, with support for collaborative editing and custom 8 | document schemas. 9 | 10 | This module implements a plugin that shows a drop cursor for 11 | ProseMirror. 12 | 13 | The [project page](https://prosemirror.net) has more information, a 14 | number of [examples](https://prosemirror.net/examples/) and the 15 | [documentation](https://prosemirror.net/docs/). 16 | 17 | This code is released under an 18 | [MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). 19 | There's a [forum](http://discuss.prosemirror.net) for general 20 | discussion and support requests, and the 21 | [Github bug tracker](https://github.com/prosemirror/prosemirror/issues) 22 | is the place to report issues. 23 | 24 | We aim to be an inclusive, welcoming community. To make that explicit, 25 | we have a [code of 26 | conduct](http://contributor-covenant.org/version/1/1/0/) that applies 27 | to communication around the project. 28 | 29 | ## Documentation 30 | 31 | * **`dropCursor`**`(options?: interface = {}) → Plugin`\ 32 | Create a plugin that, when added to a ProseMirror instance, 33 | causes a decoration to show up at the drop position when something 34 | is dragged over the editor. 35 | 36 | Nodes may add a `disableDropCursor` property to their spec to 37 | control the showing of a drop cursor inside them. This may be a 38 | boolean or a function, which will be called with a view, a 39 | position, and the DragEvent, and should return a boolean. 40 | 41 | * **`options`** 42 | 43 | * **`color`**`?: string`\ 44 | The color of the cursor. Defaults to `black`. 45 | 46 | * **`width`**`?: number`\ 47 | The precise width of the cursor in pixels. Defaults to 1. 48 | 49 | * **`class`**`?: string`\ 50 | A CSS class name to add to the cursor element. 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.8.2 (2025-04-22) 2 | 3 | ### Bug fixes 4 | 5 | Make sure the drop cursor is positioned in the right place when the editor is scaled with a CSS transform. 6 | 7 | Fix the width of the cursor when in a transformed element. Fix incorrect check in dragleave handler 8 | 9 | Fix an issue where the dropcursor would hide when drag moved over the top editor element. 10 | 11 | ## 1.8.1 (2023-05-17) 12 | 13 | ### Bug fixes 14 | 15 | Include CommonJS type declarations in the package to please new TypeScript resolution settings. 16 | 17 | ## 1.8.0 (2023-03-27) 18 | 19 | ### New features 20 | 21 | If the `color` option is set to `false`, the library will not assign an explicit color to the cursor. 22 | 23 | ## 1.7.1 (2023-03-02) 24 | 25 | ### Bug fixes 26 | 27 | Don't hide the drop cursor when no valid drop point can be found below the pointer. 28 | 29 | ## 1.7.0 (2023-02-07) 30 | 31 | ### New features 32 | 33 | The drop cursor element now has a `prosemirror-blockcursor-block`/`inline` CSS class depending on whether it is in a block or inline position. 34 | 35 | ## 1.6.1 (2022-10-25) 36 | 37 | ### Bug fixes 38 | 39 | Fix a crash when there's no DOM for the node next to the drag position. 40 | 41 | ## 1.6.0 (2022-08-07) 42 | 43 | ### New features 44 | 45 | `disableDropCursor` is now passed the dragover event as 3rd argument. 46 | 47 | ## 1.5.0 (2022-05-30) 48 | 49 | ### New features 50 | 51 | Include TypeScript type declarations. 52 | 53 | ## 1.4.0 (2021-11-11) 54 | 55 | ### New features 56 | 57 | Add support for a `disableDropCursor` property on node specs, which can be used to turn off the drop cursor inside such nodes. 58 | 59 | ## 1.3.5 (2021-05-25) 60 | 61 | ### Bug fixes 62 | 63 | Avoid a crash that happened when the document shrank during dragging. 64 | 65 | ## 1.3.4 (2021-04-01) 66 | 67 | ### Bug fixes 68 | 69 | Hide the drop cursor when `dropPoint` doesn't return a position. 70 | 71 | ## 1.3.3 (2021-02-04) 72 | 73 | ### Bug fixes 74 | 75 | Fix drop cursor positioning when the editor's `offsetParent` has been scrolled. 76 | 77 | ## 1.3.2 (2019-11-20) 78 | 79 | ### Bug fixes 80 | 81 | Rename ES module files to use a .js extension, since Webpack gets confused by .mjs 82 | 83 | ## 1.3.1 (2019-11-19) 84 | 85 | ### Bug fixes 86 | 87 | The file referred to in the package's `module` field now is compiled down to ES5. 88 | 89 | ## 1.3.0 (2019-11-08) 90 | 91 | ### New features 92 | 93 | Add a `module` field to package json file. 94 | 95 | ## 1.2.0 (2019-10-08) 96 | 97 | ### New features 98 | 99 | `dropCursor` now takes a new option, `class`, to set the CSS class name of the cursor element. Add class option to in-code docs 100 | 101 | ## 1.1.2 (2019-09-05) 102 | 103 | ### Bug fixes 104 | 105 | Fix crash on IE11 due to using a method that platform doesn't support. Don't show a drop cursor when the view isn't editable 106 | 107 | The drop cursor will no longer show up when the view isn't editable. 108 | 109 | ## 1.1.1 (2018-10-23) 110 | 111 | ### Bug fixes 112 | 113 | Fix crash when destroying the plugin, due to a misspelled method name. 114 | 115 | ## 1.1.0 (2018-10-22) 116 | 117 | ### Bug fixes 118 | 119 | Fixes an issue where drop cursors changed line breaking, causing the content to jump around during dragging. 120 | 121 | ### New features 122 | 123 | Between-blocks drop cursors are now shown as a horizontal line. 124 | 125 | ## 1.0.1 (2018-06-20) 126 | 127 | ### Bug fixes 128 | 129 | Dragging from a content node directly to the outside of the editor will now properly hide the drop cursor. 130 | 131 | Make removal of drop cursor part of the drop transaction when possible. 132 | 133 | Use `dropPoint` from prosemirror-transform, rather than a local parallel implementation. 134 | 135 | ## 1.0.0 (2017-10-13) 136 | 137 | First stable release. 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | - [Getting help](#getting-help) 4 | - [Submitting bug reports](#submitting-bug-reports) 5 | - [Contributing code](#contributing-code) 6 | 7 | ## Getting help 8 | 9 | Community discussion, questions, and informal bug reporting is done on the 10 | [discuss.ProseMirror forum](http://discuss.prosemirror.net). 11 | 12 | ## Submitting bug reports 13 | 14 | Report bugs on the 15 | [GitHub issue tracker](http://github.com/prosemirror/prosemirror/issues). 16 | Before reporting a bug, please read these pointers. 17 | 18 | - The issue tracker is for *bugs*, not requests for help. Questions 19 | should be asked on the [forum](http://discuss.prosemirror.net). 20 | 21 | - Include information about the version of the code that exhibits the 22 | problem. For browser-related issues, include the browser and browser 23 | version on which the problem occurred. 24 | 25 | - Mention very precisely what went wrong. "X is broken" is not a good 26 | bug report. What did you expect to happen? What happened instead? 27 | Describe the exact steps a maintainer has to take to make the 28 | problem occur. A screencast can be useful, but is no substitute for 29 | a textual description. 30 | 31 | - A great way to make it easy to reproduce your problem, if it can not 32 | be trivially reproduced on the website demos, is to submit a script 33 | that triggers the issue. 34 | 35 | ## Contributing code 36 | 37 | - Make sure you have a [GitHub Account](https://github.com/signup/free) 38 | 39 | - Fork the relevant repository 40 | ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) 41 | 42 | - Create a local checkout of the code. You can use the 43 | [main repository](https://github.com/prosemirror/prosemirror) to 44 | easily check out all core modules. 45 | 46 | - Make your changes, and commit them 47 | 48 | - Follow the code style of the rest of the project (see below). Run 49 | `npm run lint` (in the main repository checkout) to make sure that 50 | the linter is happy. 51 | 52 | - If your changes are easy to test or likely to regress, add tests in 53 | the relevant `test/` directory. Either put them in an existing 54 | `test-*.js` file, if they fit there, or add a new file. 55 | 56 | - Make sure all tests pass. Run `npm run test` to verify tests pass 57 | (you will need Node.js v6+). 58 | 59 | - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)). 60 | Don't put more than one feature/fix in a single pull request. 61 | 62 | By contributing code to ProseMirror you 63 | 64 | - Agree to license the contributed code under the project's [MIT 65 | license](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE). 66 | 67 | - Confirm that you have the right to contribute and license the code 68 | in question. (Either you hold all rights on the code, or the rights 69 | holder has explicitly granted the right to use it like this, 70 | through a compatible open source license or through a direct 71 | agreement with you.) 72 | 73 | ### Coding standards 74 | 75 | - ES6 syntax, targeting an ES5 runtime (i.e. don't use library 76 | elements added by ES6, don't use ES7/ES.next syntax). 77 | 78 | - 2 spaces per indentation level, no tabs. 79 | 80 | - No semicolons except when necessary. 81 | 82 | - Follow the surrounding code when it comes to spacing, brace 83 | placement, etc. 84 | 85 | - Brace-less single-statement bodies are encouraged (whenever they 86 | don't impact readability). 87 | 88 | - [getdocs](https://github.com/marijnh/getdocs)-style doc comments 89 | above items that are part of the public API. 90 | 91 | - When documenting non-public items, you can put the type after a 92 | single colon, so that getdocs doesn't pick it up and add it to the 93 | API reference. 94 | 95 | - The linter (`npm run lint`) complains about unused variables and 96 | functions. Prefix their names with an underscore to muffle it. 97 | 98 | - ProseMirror does *not* follow JSHint or JSLint prescribed style. 99 | Patches that try to 'fix' code to pass one of these linters will not 100 | be accepted. 101 | -------------------------------------------------------------------------------- /src/dropcursor.ts: -------------------------------------------------------------------------------- 1 | import {Plugin, EditorState} from "prosemirror-state" 2 | import {EditorView} from "prosemirror-view" 3 | import {dropPoint} from "prosemirror-transform" 4 | 5 | interface DropCursorOptions { 6 | /// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class. 7 | color?: string | false 8 | 9 | /// The precise width of the cursor in pixels. Defaults to 1. 10 | width?: number 11 | 12 | /// A CSS class name to add to the cursor element. 13 | class?: string 14 | } 15 | 16 | /// Create a plugin that, when added to a ProseMirror instance, 17 | /// causes a decoration to show up at the drop position when something 18 | /// is dragged over the editor. 19 | /// 20 | /// Nodes may add a `disableDropCursor` property to their spec to 21 | /// control the showing of a drop cursor inside them. This may be a 22 | /// boolean or a function, which will be called with a view and a 23 | /// position, and should return a boolean. 24 | export function dropCursor(options: DropCursorOptions = {}): Plugin { 25 | return new Plugin({ 26 | view(editorView) { return new DropCursorView(editorView, options) } 27 | }) 28 | } 29 | 30 | // Add disableDropCursor to NodeSpec 31 | declare module "prosemirror-model" { 32 | interface NodeSpec { 33 | disableDropCursor?: boolean | ((view: EditorView, pos: {pos: number, inside: number}, event: DragEvent) => boolean) 34 | } 35 | } 36 | 37 | class DropCursorView { 38 | width: number 39 | color: string | undefined 40 | class: string | undefined 41 | cursorPos: number | null = null 42 | element: HTMLElement | null = null 43 | timeout: number = -1 44 | handlers: {name: string, handler: (event: Event) => void}[] 45 | 46 | constructor(readonly editorView: EditorView, options: DropCursorOptions) { 47 | this.width = options.width ?? 1 48 | this.color = options.color === false ? undefined : (options.color || "black") 49 | this.class = options.class 50 | 51 | this.handlers = ["dragover", "dragend", "drop", "dragleave"].map(name => { 52 | let handler = (e: Event) => { (this as any)[name](e) } 53 | editorView.dom.addEventListener(name, handler) 54 | return {name, handler} 55 | }) 56 | } 57 | 58 | destroy() { 59 | this.handlers.forEach(({name, handler}) => this.editorView.dom.removeEventListener(name, handler)) 60 | } 61 | 62 | update(editorView: EditorView, prevState: EditorState) { 63 | if (this.cursorPos != null && prevState.doc != editorView.state.doc) { 64 | if (this.cursorPos > editorView.state.doc.content.size) this.setCursor(null) 65 | else this.updateOverlay() 66 | } 67 | } 68 | 69 | setCursor(pos: number | null) { 70 | if (pos == this.cursorPos) return 71 | this.cursorPos = pos 72 | if (pos == null) { 73 | this.element!.parentNode!.removeChild(this.element!) 74 | this.element = null 75 | } else { 76 | this.updateOverlay() 77 | } 78 | } 79 | 80 | updateOverlay() { 81 | let $pos = this.editorView.state.doc.resolve(this.cursorPos!) 82 | let isBlock = !$pos.parent.inlineContent, rect 83 | let editorDOM = this.editorView.dom, editorRect = editorDOM.getBoundingClientRect() 84 | let scaleX = editorRect.width / editorDOM.offsetWidth, scaleY = editorRect.height / editorDOM.offsetHeight 85 | if (isBlock) { 86 | let before = $pos.nodeBefore, after = $pos.nodeAfter 87 | if (before || after) { 88 | let node = this.editorView.nodeDOM(this.cursorPos! - (before ? before.nodeSize : 0)) 89 | if (node) { 90 | let nodeRect = (node as HTMLElement).getBoundingClientRect() 91 | let top = before ? nodeRect.bottom : nodeRect.top 92 | if (before && after) 93 | top = (top + (this.editorView.nodeDOM(this.cursorPos!) as HTMLElement).getBoundingClientRect().top) / 2 94 | let halfWidth = (this.width / 2) * scaleY 95 | rect = {left: nodeRect.left, right: nodeRect.right, top: top - halfWidth, bottom: top + halfWidth} 96 | } 97 | } 98 | } 99 | if (!rect) { 100 | let coords = this.editorView.coordsAtPos(this.cursorPos!) 101 | let halfWidth = (this.width / 2) * scaleX 102 | rect = {left: coords.left - halfWidth, right: coords.left + halfWidth, top: coords.top, bottom: coords.bottom} 103 | } 104 | 105 | let parent = this.editorView.dom.offsetParent as HTMLElement 106 | if (!this.element) { 107 | this.element = parent.appendChild(document.createElement("div")) 108 | if (this.class) this.element.className = this.class 109 | this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;" 110 | if (this.color) { 111 | this.element.style.backgroundColor = this.color 112 | } 113 | } 114 | this.element.classList.toggle("prosemirror-dropcursor-block", isBlock) 115 | this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock) 116 | let parentLeft, parentTop 117 | if (!parent || parent == document.body && getComputedStyle(parent).position == "static") { 118 | parentLeft = -pageXOffset 119 | parentTop = -pageYOffset 120 | } else { 121 | let rect = parent.getBoundingClientRect() 122 | let parentScaleX = rect.width / parent.offsetWidth, parentScaleY = rect.height / parent.offsetHeight 123 | parentLeft = rect.left - parent.scrollLeft * parentScaleX 124 | parentTop = rect.top - parent.scrollTop * parentScaleY 125 | } 126 | this.element.style.left = (rect.left - parentLeft) / scaleX + "px" 127 | this.element.style.top = (rect.top - parentTop) / scaleY + "px" 128 | this.element.style.width = (rect.right - rect.left) / scaleX + "px" 129 | this.element.style.height = (rect.bottom - rect.top) / scaleY + "px" 130 | } 131 | 132 | scheduleRemoval(timeout: number) { 133 | clearTimeout(this.timeout) 134 | this.timeout = setTimeout(() => this.setCursor(null), timeout) 135 | } 136 | 137 | dragover(event: DragEvent) { 138 | if (!this.editorView.editable) return 139 | let pos = this.editorView.posAtCoords({left: event.clientX, top: event.clientY}) 140 | 141 | let node = pos && pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside) 142 | let disableDropCursor = node && node.type.spec.disableDropCursor 143 | let disabled = typeof disableDropCursor == "function" 144 | ? disableDropCursor(this.editorView, pos!, event) 145 | : disableDropCursor 146 | 147 | if (pos && !disabled) { 148 | let target = pos.pos 149 | if (this.editorView.dragging && this.editorView.dragging.slice) { 150 | let point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice) 151 | if (point != null) target = point 152 | } 153 | this.setCursor(target) 154 | this.scheduleRemoval(5000) 155 | } 156 | } 157 | 158 | dragend() { 159 | this.scheduleRemoval(20) 160 | } 161 | 162 | drop() { 163 | this.scheduleRemoval(20) 164 | } 165 | 166 | dragleave(event: DragEvent) { 167 | if (!this.editorView.dom.contains((event as any).relatedTarget)) 168 | this.setCursor(null) 169 | } 170 | } 171 | --------------------------------------------------------------------------------