├── .npmrc ├── .npmignore ├── .gitignore ├── .tern-project ├── src ├── README.md ├── inputrules.ts ├── index.ts ├── keymap.ts ├── prompt.ts └── menu.ts ├── CHANGELOG.md ├── LICENSE ├── package.json ├── style └── style.css ├── CONTRIBUTING.md └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-port 3 | /test 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-port 3 | /dist 4 | /notes.txt 5 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "libs": ["browser"], 3 | "plugins": { 4 | "node": {}, 5 | "complete_strings": {}, 6 | "es_modules": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | This module exports helper functions for deriving a set of basic menu 2 | items, input rules, or key bindings from a schema. These values need 3 | to know about the schema for two reasons—they need access to specific 4 | instances of node and mark types, and they need to know which of the 5 | node and mark types that they know about are actually present in the 6 | schema. 7 | 8 | @exampleSetup 9 | 10 | @buildMenuItems 11 | 12 | @buildKeymap 13 | 14 | @buildInputRules 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.3 (2024-08-16) 2 | 3 | ### Bug fixes 4 | 5 | Make the type of the `fullMenu` option `MenuElement[][]` instead of `MenuItem[][]`. 6 | 7 | ## 1.2.2 (2023-05-17) 8 | 9 | ### Bug fixes 10 | 11 | Include CommonJS type declarations in the package to please new TypeScript resolution settings. 12 | 13 | ## 1.2.1 (2022-06-22) 14 | 15 | ### Bug fixes 16 | 17 | Export CSS file from package.json. 18 | 19 | ## 1.2.0 (2022-05-30) 20 | 21 | ### New features 22 | 23 | Include TypeScript type declarations. 24 | 25 | ## 1.1.2 (2019-11-20) 26 | 27 | ### Bug fixes 28 | 29 | Rename ES module files to use a .js extension, since Webpack gets confused by .mjs 30 | 31 | ## 1.1.1 (2019-11-19) 32 | 33 | ### Bug fixes 34 | 35 | The file referred to in the package's `module` field now is compiled down to ES5. 36 | 37 | ## 1.1.0 (2019-11-08) 38 | 39 | ### New features 40 | 41 | Add a `module` field to package json file. 42 | 43 | ## 1.0.1 (2017-11-24) 44 | 45 | ### Bug fixes 46 | 47 | The example menu now allows you to enter relative links without automatically adding http:// in front of them. 48 | 49 | ## 1.0.0 (2017-10-13) 50 | 51 | First stable release. 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prosemirror-example-setup", 3 | "version": "1.2.3", 4 | "description": "An example for how to set up a ProseMirror editor", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | }, 14 | "./style/style.css": "./style/style.css" 15 | }, 16 | "sideEffects": ["./style/style.css"], 17 | "style": "style/style.css", 18 | "license": "MIT", 19 | "maintainers": [ 20 | { 21 | "name": "Marijn Haverbeke", 22 | "email": "marijn@haverbeke.berlin", 23 | "web": "http://marijnhaverbeke.nl" 24 | } 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/prosemirror/prosemirror-example-setup.git" 29 | }, 30 | "dependencies": { 31 | "prosemirror-inputrules": "^1.0.0", 32 | "prosemirror-schema-list": "^1.0.0", 33 | "prosemirror-keymap": "^1.0.0", 34 | "prosemirror-history": "^1.0.0", 35 | "prosemirror-commands": "^1.0.0", 36 | "prosemirror-state": "^1.0.0", 37 | "prosemirror-menu": "^1.0.0", 38 | "prosemirror-dropcursor": "^1.0.0", 39 | "prosemirror-gapcursor": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "@prosemirror/buildhelper": "^0.1.5" 43 | }, 44 | "scripts": { 45 | "prepare": "pm-buildhelper src/index.ts" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | /* Add space around the hr to make clicking it easier */ 2 | 3 | .ProseMirror-example-setup-style hr { 4 | padding: 2px 10px; 5 | border: none; 6 | margin: 1em 0; 7 | } 8 | 9 | .ProseMirror-example-setup-style hr:after { 10 | content: ""; 11 | display: block; 12 | height: 1px; 13 | background-color: silver; 14 | line-height: 2px; 15 | } 16 | 17 | .ProseMirror ul, .ProseMirror ol { 18 | padding-left: 30px; 19 | } 20 | 21 | .ProseMirror blockquote { 22 | padding-left: 1em; 23 | border-left: 3px solid #eee; 24 | margin-left: 0; margin-right: 0; 25 | } 26 | 27 | .ProseMirror-example-setup-style img { 28 | cursor: default; 29 | } 30 | 31 | .ProseMirror-prompt { 32 | background: white; 33 | padding: 5px 10px 5px 15px; 34 | border: 1px solid silver; 35 | position: fixed; 36 | border-radius: 3px; 37 | z-index: 11; 38 | box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); 39 | } 40 | 41 | .ProseMirror-prompt h5 { 42 | margin: 0; 43 | font-weight: normal; 44 | font-size: 100%; 45 | color: #444; 46 | } 47 | 48 | .ProseMirror-prompt input[type="text"], 49 | .ProseMirror-prompt textarea { 50 | background: #eee; 51 | border: none; 52 | outline: none; 53 | } 54 | 55 | .ProseMirror-prompt input[type="text"] { 56 | padding: 0 4px; 57 | } 58 | 59 | .ProseMirror-prompt-close { 60 | position: absolute; 61 | left: 2px; top: 1px; 62 | color: #666; 63 | border: none; background: transparent; padding: 0; 64 | } 65 | 66 | .ProseMirror-prompt-close:after { 67 | content: "✕"; 68 | font-size: 12px; 69 | } 70 | 71 | .ProseMirror-invalid { 72 | background: #ffc; 73 | border: 1px solid #cc7; 74 | border-radius: 4px; 75 | padding: 5px 10px; 76 | position: absolute; 77 | min-width: 10em; 78 | } 79 | 80 | .ProseMirror-prompt-buttons { 81 | margin-top: 5px; 82 | display: none; 83 | } 84 | -------------------------------------------------------------------------------- /src/inputrules.ts: -------------------------------------------------------------------------------- 1 | import {inputRules, wrappingInputRule, textblockTypeInputRule, 2 | smartQuotes, emDash, ellipsis} from "prosemirror-inputrules" 3 | import {NodeType, Schema} from "prosemirror-model" 4 | 5 | /// Given a blockquote node type, returns an input rule that turns `"> "` 6 | /// at the start of a textblock into a blockquote. 7 | export function blockQuoteRule(nodeType: NodeType) { 8 | return wrappingInputRule(/^\s*>\s$/, nodeType) 9 | } 10 | 11 | /// Given a list node type, returns an input rule that turns a number 12 | /// followed by a dot at the start of a textblock into an ordered list. 13 | export function orderedListRule(nodeType: NodeType) { 14 | return wrappingInputRule(/^(\d+)\.\s$/, nodeType, match => ({order: +match[1]}), 15 | (match, node) => node.childCount + node.attrs.order == +match[1]) 16 | } 17 | 18 | /// Given a list node type, returns an input rule that turns a bullet 19 | /// (dash, plush, or asterisk) at the start of a textblock into a 20 | /// bullet list. 21 | export function bulletListRule(nodeType: NodeType) { 22 | return wrappingInputRule(/^\s*([-+*])\s$/, nodeType) 23 | } 24 | 25 | /// Given a code block node type, returns an input rule that turns a 26 | /// textblock starting with three backticks into a code block. 27 | export function codeBlockRule(nodeType: NodeType) { 28 | return textblockTypeInputRule(/^```$/, nodeType) 29 | } 30 | 31 | /// Given a node type and a maximum level, creates an input rule that 32 | /// turns up to that number of `#` characters followed by a space at 33 | /// the start of a textblock into a heading whose level corresponds to 34 | /// the number of `#` signs. 35 | export function headingRule(nodeType: NodeType, maxLevel: number) { 36 | return textblockTypeInputRule(new RegExp("^(#{1," + maxLevel + "})\\s$"), 37 | nodeType, match => ({level: match[1].length})) 38 | } 39 | 40 | /// A set of input rules for creating the basic block quotes, lists, 41 | /// code blocks, and heading. 42 | export function buildInputRules(schema: Schema) { 43 | let rules = smartQuotes.concat(ellipsis, emDash), type 44 | if (type = schema.nodes.blockquote) rules.push(blockQuoteRule(type)) 45 | if (type = schema.nodes.ordered_list) rules.push(orderedListRule(type)) 46 | if (type = schema.nodes.bullet_list) rules.push(bulletListRule(type)) 47 | if (type = schema.nodes.code_block) rules.push(codeBlockRule(type)) 48 | if (type = schema.nodes.heading) rules.push(headingRule(type, 6)) 49 | return inputRules({rules}) 50 | } 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {keymap} from "prosemirror-keymap" 2 | import {history} from "prosemirror-history" 3 | import {baseKeymap} from "prosemirror-commands" 4 | import {Plugin} from "prosemirror-state" 5 | import {dropCursor} from "prosemirror-dropcursor" 6 | import {gapCursor} from "prosemirror-gapcursor" 7 | import {menuBar, MenuElement} from "prosemirror-menu" 8 | import {Schema} from "prosemirror-model" 9 | 10 | import {buildMenuItems} from "./menu" 11 | import {buildKeymap} from "./keymap" 12 | import {buildInputRules} from "./inputrules" 13 | 14 | export {buildMenuItems, buildKeymap, buildInputRules} 15 | 16 | /// Create an array of plugins pre-configured for the given schema. 17 | /// The resulting array will include the following plugins: 18 | /// 19 | /// * Input rules for smart quotes and creating the block types in the 20 | /// schema using markdown conventions (say `"> "` to create a 21 | /// blockquote) 22 | /// 23 | /// * A keymap that defines keys to create and manipulate the nodes in the 24 | /// schema 25 | /// 26 | /// * A keymap binding the default keys provided by the 27 | /// prosemirror-commands module 28 | /// 29 | /// * The undo history plugin 30 | /// 31 | /// * The drop cursor plugin 32 | /// 33 | /// * The gap cursor plugin 34 | /// 35 | /// * A custom plugin that adds a `menuContent` prop for the 36 | /// prosemirror-menu wrapper, and a CSS class that enables the 37 | /// additional styling defined in `style/style.css` in this package 38 | /// 39 | /// Probably only useful for quickly setting up a passable 40 | /// editor—you'll need more control over your settings in most 41 | /// real-world situations. 42 | export function exampleSetup(options: { 43 | /// The schema to generate key bindings and menu items for. 44 | schema: Schema 45 | 46 | /// Can be used to [adjust](#example-setup.buildKeymap) the key bindings created. 47 | mapKeys?: {[key: string]: string | false} 48 | 49 | /// Set to false to disable the menu bar. 50 | menuBar?: boolean 51 | 52 | /// Set to false to disable the history plugin. 53 | history?: boolean 54 | 55 | /// Set to false to make the menu bar non-floating. 56 | floatingMenu?: boolean 57 | 58 | /// Can be used to override the menu content. 59 | menuContent?: MenuElement[][] 60 | }) { 61 | let plugins = [ 62 | buildInputRules(options.schema), 63 | keymap(buildKeymap(options.schema, options.mapKeys)), 64 | keymap(baseKeymap), 65 | dropCursor(), 66 | gapCursor() 67 | ] 68 | if (options.menuBar !== false) 69 | plugins.push(menuBar({floating: options.floatingMenu !== false, 70 | content: options.menuContent || buildMenuItems(options.schema).fullMenu})) 71 | if (options.history !== false) 72 | plugins.push(history()) 73 | 74 | return plugins.concat(new Plugin({ 75 | props: { 76 | attributes: {class: "ProseMirror-example-setup-style"} 77 | } 78 | })) 79 | } 80 | -------------------------------------------------------------------------------- /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/keymap.ts: -------------------------------------------------------------------------------- 1 | import {wrapIn, setBlockType, chainCommands, toggleMark, exitCode, 2 | joinUp, joinDown, lift, selectParentNode} from "prosemirror-commands" 3 | import {wrapInList, splitListItem, liftListItem, sinkListItem} from "prosemirror-schema-list" 4 | import {undo, redo} from "prosemirror-history" 5 | import {undoInputRule} from "prosemirror-inputrules" 6 | import {Command} from "prosemirror-state" 7 | import {Schema} from "prosemirror-model" 8 | 9 | const mac = typeof navigator != "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) : false 10 | 11 | /// Inspect the given schema looking for marks and nodes from the 12 | /// basic schema, and if found, add key bindings related to them. 13 | /// This will add: 14 | /// 15 | /// * **Mod-b** for toggling [strong](#schema-basic.StrongMark) 16 | /// * **Mod-i** for toggling [emphasis](#schema-basic.EmMark) 17 | /// * **Mod-`** for toggling [code font](#schema-basic.CodeMark) 18 | /// * **Ctrl-Shift-0** for making the current textblock a paragraph 19 | /// * **Ctrl-Shift-1** to **Ctrl-Shift-Digit6** for making the current 20 | /// textblock a heading of the corresponding level 21 | /// * **Ctrl-Shift-Backslash** to make the current textblock a code block 22 | /// * **Ctrl-Shift-8** to wrap the selection in an ordered list 23 | /// * **Ctrl-Shift-9** to wrap the selection in a bullet list 24 | /// * **Ctrl->** to wrap the selection in a block quote 25 | /// * **Enter** to split a non-empty textblock in a list item while at 26 | /// the same time splitting the list item 27 | /// * **Mod-Enter** to insert a hard break 28 | /// * **Mod-_** to insert a horizontal rule 29 | /// * **Backspace** to undo an input rule 30 | /// * **Alt-ArrowUp** to `joinUp` 31 | /// * **Alt-ArrowDown** to `joinDown` 32 | /// * **Mod-BracketLeft** to `lift` 33 | /// * **Escape** to `selectParentNode` 34 | /// 35 | /// You can suppress or map these bindings by passing a `mapKeys` 36 | /// argument, which maps key names (say `"Mod-B"` to either `false`, to 37 | /// remove the binding, or a new key name string. 38 | export function buildKeymap(schema: Schema, mapKeys?: {[key: string]: false | string}) { 39 | let keys: {[key: string]: Command} = {}, type 40 | function bind(key: string, cmd: Command) { 41 | if (mapKeys) { 42 | let mapped = mapKeys[key] 43 | if (mapped === false) return 44 | if (mapped) key = mapped 45 | } 46 | keys[key] = cmd 47 | } 48 | 49 | bind("Mod-z", undo) 50 | bind("Shift-Mod-z", redo) 51 | bind("Backspace", undoInputRule) 52 | if (!mac) bind("Mod-y", redo) 53 | 54 | bind("Alt-ArrowUp", joinUp) 55 | bind("Alt-ArrowDown", joinDown) 56 | bind("Mod-BracketLeft", lift) 57 | bind("Escape", selectParentNode) 58 | 59 | if (type = schema.marks.strong) { 60 | bind("Mod-b", toggleMark(type)) 61 | bind("Mod-B", toggleMark(type)) 62 | } 63 | if (type = schema.marks.em) { 64 | bind("Mod-i", toggleMark(type)) 65 | bind("Mod-I", toggleMark(type)) 66 | } 67 | if (type = schema.marks.code) 68 | bind("Mod-`", toggleMark(type)) 69 | 70 | if (type = schema.nodes.bullet_list) 71 | bind("Shift-Ctrl-8", wrapInList(type)) 72 | if (type = schema.nodes.ordered_list) 73 | bind("Shift-Ctrl-9", wrapInList(type)) 74 | if (type = schema.nodes.blockquote) 75 | bind("Ctrl->", wrapIn(type)) 76 | if (type = schema.nodes.hard_break) { 77 | let br = type, cmd = chainCommands(exitCode, (state, dispatch) => { 78 | if (dispatch) dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()) 79 | return true 80 | }) 81 | bind("Mod-Enter", cmd) 82 | bind("Shift-Enter", cmd) 83 | if (mac) bind("Ctrl-Enter", cmd) 84 | } 85 | if (type = schema.nodes.list_item) { 86 | bind("Enter", splitListItem(type)) 87 | bind("Mod-[", liftListItem(type)) 88 | bind("Mod-]", sinkListItem(type)) 89 | } 90 | if (type = schema.nodes.paragraph) 91 | bind("Shift-Ctrl-0", setBlockType(type)) 92 | if (type = schema.nodes.code_block) 93 | bind("Shift-Ctrl-\\", setBlockType(type)) 94 | if (type = schema.nodes.heading) 95 | for (let i = 1; i <= 6; i++) bind("Shift-Ctrl-" + i, setBlockType(type, {level: i})) 96 | if (type = schema.nodes.horizontal_rule) { 97 | let hr = type 98 | bind("Mod-_", (state, dispatch) => { 99 | if (dispatch) dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()) 100 | return true 101 | }) 102 | } 103 | 104 | return keys 105 | } 106 | -------------------------------------------------------------------------------- /src/prompt.ts: -------------------------------------------------------------------------------- 1 | import {Attrs} from "prosemirror-model" 2 | 3 | const prefix = "ProseMirror-prompt" 4 | 5 | export function openPrompt(options: { 6 | title: string, 7 | fields: {[name: string]: Field}, 8 | callback: (attrs: Attrs) => void 9 | }) { 10 | let wrapper = document.body.appendChild(document.createElement("div")) 11 | wrapper.className = prefix 12 | 13 | let mouseOutside = (e: MouseEvent) => { if (!wrapper.contains(e.target as HTMLElement)) close() } 14 | setTimeout(() => window.addEventListener("mousedown", mouseOutside), 50) 15 | let close = () => { 16 | window.removeEventListener("mousedown", mouseOutside) 17 | if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper) 18 | } 19 | 20 | let domFields: HTMLElement[] = [] 21 | for (let name in options.fields) domFields.push(options.fields[name].render()) 22 | 23 | let submitButton = document.createElement("button") 24 | submitButton.type = "submit" 25 | submitButton.className = prefix + "-submit" 26 | submitButton.textContent = "OK" 27 | let cancelButton = document.createElement("button") 28 | cancelButton.type = "button" 29 | cancelButton.className = prefix + "-cancel" 30 | cancelButton.textContent = "Cancel" 31 | cancelButton.addEventListener("click", close) 32 | 33 | let form = wrapper.appendChild(document.createElement("form")) 34 | if (options.title) form.appendChild(document.createElement("h5")).textContent = options.title 35 | domFields.forEach(field => { 36 | form.appendChild(document.createElement("div")).appendChild(field) 37 | }) 38 | let buttons = form.appendChild(document.createElement("div")) 39 | buttons.className = prefix + "-buttons" 40 | buttons.appendChild(submitButton) 41 | buttons.appendChild(document.createTextNode(" ")) 42 | buttons.appendChild(cancelButton) 43 | 44 | let box = wrapper.getBoundingClientRect() 45 | wrapper.style.top = ((window.innerHeight - box.height) / 2) + "px" 46 | wrapper.style.left = ((window.innerWidth - box.width) / 2) + "px" 47 | 48 | let submit = () => { 49 | let params = getValues(options.fields, domFields) 50 | if (params) { 51 | close() 52 | options.callback(params) 53 | } 54 | } 55 | 56 | form.addEventListener("submit", e => { 57 | e.preventDefault() 58 | submit() 59 | }) 60 | 61 | form.addEventListener("keydown", e => { 62 | if (e.keyCode == 27) { 63 | e.preventDefault() 64 | close() 65 | } else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) { 66 | e.preventDefault() 67 | submit() 68 | } else if (e.keyCode == 9) { 69 | window.setTimeout(() => { 70 | if (!wrapper.contains(document.activeElement)) close() 71 | }, 500) 72 | } 73 | }) 74 | 75 | let input = form.elements[0] as HTMLElement 76 | if (input) input.focus() 77 | } 78 | 79 | function getValues(fields: {[name: string]: Field}, domFields: readonly HTMLElement[]) { 80 | let result = Object.create(null), i = 0 81 | for (let name in fields) { 82 | let field = fields[name], dom = domFields[i++] 83 | let value = field.read(dom), bad = field.validate(value) 84 | if (bad) { 85 | reportInvalid(dom, bad) 86 | return null 87 | } 88 | result[name] = field.clean(value) 89 | } 90 | return result 91 | } 92 | 93 | function reportInvalid(dom: HTMLElement, message: string) { 94 | // FIXME this is awful and needs a lot more work 95 | let parent = dom.parentNode! 96 | let msg = parent.appendChild(document.createElement("div")) 97 | msg.style.left = (dom.offsetLeft + dom.offsetWidth + 2) + "px" 98 | msg.style.top = (dom.offsetTop - 5) + "px" 99 | msg.className = "ProseMirror-invalid" 100 | msg.textContent = message 101 | setTimeout(() => parent.removeChild(msg), 1500) 102 | } 103 | 104 | /// The type of field that `openPrompt` expects to be passed to it. 105 | export abstract class Field { 106 | /// Create a field with the given options. Options support by all 107 | /// field types are: 108 | constructor( 109 | /// @internal 110 | readonly options: { 111 | /// The starting value for the field. 112 | value?: any 113 | 114 | /// The label for the field. 115 | label: string 116 | 117 | /// Whether the field is required. 118 | required?: boolean 119 | 120 | /// A function to validate the given value. Should return an 121 | /// error message if it is not valid. 122 | validate?: (value: any) => string | null 123 | 124 | /// A cleanup function for field values. 125 | clean?: (value: any) => any 126 | } 127 | ) {} 128 | 129 | /// Render the field to the DOM. Should be implemented by all subclasses. 130 | abstract render(): HTMLElement 131 | 132 | /// Read the field's value from its DOM node. 133 | read(dom: HTMLElement) { return (dom as any).value } 134 | 135 | /// A field-type-specific validation function. 136 | validateType(value: any): string | null { return null } 137 | 138 | /// @internal 139 | validate(value: any): string | null { 140 | if (!value && this.options.required) 141 | return "Required field" 142 | return this.validateType(value) || (this.options.validate ? this.options.validate(value) : null) 143 | } 144 | 145 | clean(value: any): any { 146 | return this.options.clean ? this.options.clean(value) : value 147 | } 148 | } 149 | 150 | /// A field class for single-line text fields. 151 | export class TextField extends Field { 152 | render() { 153 | let input = document.createElement("input") 154 | input.type = "text" 155 | input.placeholder = this.options.label 156 | input.value = this.options.value || "" 157 | input.autocomplete = "off" 158 | return input 159 | } 160 | } 161 | 162 | 163 | /// A field class for dropdown fields based on a plain `