@docs.plus/extension-placeholder 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@docs.plus/extension-placeholder` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/); the project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [2.0.0] — 2026-08-11
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- **First npm publish** — versioned `2.0.0` to align with the docs.plus extension-family shared major.
|
|
10
|
+
- **O(depth) cursor-based decoration** — decorates only the empty node at the cursor plus empty ancestor wrappers; no full-document `doc.descendants` scan on every keystroke.
|
|
11
|
+
- **Runtime editable toggle** — `editor.setEditable()` refreshes the placeholder immediately when `showOnlyWhenEditable` is on.
|
|
12
|
+
- **Clean-room Cypress suite** — twelve specs against built `dist/` on port 5177 (empty doc, cursor tracking, undo, full-doc paste, gap cursor).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `apply()` skips recomputation for meta-only transactions (neither doc nor selection changed).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- A non-text selection no longer puts `data-placeholder` on a wrapper node. `$anchor.parent` is a textblock only for a text selection. So a `NodeSelection` on an empty paragraph inside a blockquote or list item resolved to the wrapper and painted the placeholder on it. The guard now tests `node.type.isTextblock`.
|
|
21
|
+
- `editor.setEditable()` refreshes the placeholder at runtime. With `showOnlyWhenEditable: true`, toggling read-only left a stale placeholder (or failed to restore it on re-enable): the editability gate lived in the plugin's `apply`, which `setEditable` does not re-run. The gate moved to `props.decorations`, which `view.updateState` re-runs.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- Documented the differences from the built-in Placeholder, pointed the CSS example at `.ProseMirror [data-placeholder]::before`, and added a screen-reader labeling note.
|
|
26
|
+
|
|
27
|
+
### Internal
|
|
28
|
+
|
|
29
|
+
- The published manifest no longer declares `engines` — the monorepo's Node floor gated engine-strict consumer installs even though the shipped bundle is plain browser-targeted ESM/CJS.
|
|
30
|
+
- Added a clean-room Cypress E2E suite (`@docs.plus/playground`, port 5177) against the built `dist/`: empty-document lifecycle, cursor tracking, ancestor propagation, the editable toggle, option resolution, `isNodeEmpty` semantics, external edits, undo/redo, full-document paste, horizontal rule and gap cursor, per-node-type `placeholder` functions, and real-keystroke Backspace.
|
|
31
|
+
- Replaced a cast with `?? null` in `props.decorations`.
|
|
32
|
+
- Added a `typecheck` script; corrected "TipTap" to "Tiptap" in the package description.
|
|
33
|
+
|
|
34
|
+
## [0.1.0]
|
|
35
|
+
|
|
36
|
+
Pre-changelog baseline, never published to npm. O(depth) cursor-based placeholder decoration — decorates only the empty node at the cursor and its empty ancestor wrappers instead of scanning with Tiptap's built-in `doc.descendants`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2026 Hossein Marzban
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# @docs.plus/extension-placeholder
|
|
2
|
+
|
|
3
|
+
<a href="https://docs.plus"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/docs-plus/docs.plus/main/apps/webapp/public/badges/badge-docsplus-dark.svg"><img alt="docs.plus" height="20" src="https://raw.githubusercontent.com/docs-plus/docs.plus/main/apps/webapp/public/badges/badge-docsplus.svg"></picture></a>
|
|
4
|
+
[](https://www.npmjs.com/package/@docs.plus/extension-placeholder)
|
|
5
|
+
[](https://npmcharts.com/compare/@docs.plus/extension-placeholder)
|
|
6
|
+
[](https://www.npmjs.com/package/@docs.plus/extension-placeholder)
|
|
7
|
+
[](https://discord.gg/25JPG38J59)
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/docs-plus/docs.plus/main/extensions/extension-placeholder/assets/preview-dark.png">
|
|
12
|
+
<img alt="Empty editor showing placeholder hint text in the first paragraph" width="640" src="https://raw.githubusercontent.com/docs-plus/docs.plus/main/extensions/extension-placeholder/assets/preview-light.png">
|
|
13
|
+
</picture>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
Tiptap placeholder extension that shows hint text in the empty textblock at the cursor.
|
|
17
|
+
|
|
18
|
+
A textblock is a node that holds inline text, such as a paragraph or a heading. Tiptap's built-in Placeholder scans every top-level block with `doc.descendants` on every editor update, and the whole tree when `includeChildren` is on. This package starts at the cursor and walks up, so the cost tracks the cursor depth, not the document length. It writes the hint text into a `data-placeholder` attribute on the empty textblock at the cursor. It also adds `emptyNodeClass` to every empty ancestor wrapper above that textblock. It accepts four of the built-in's seven options, and its callback form gains `parentName` and `doc`.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bun add @docs.plus/extension-placeholder
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires **`@tiptap/core` ^3.22.3** and **`@tiptap/pm` ^3.22.3** (Tiptap 3.x).
|
|
27
|
+
|
|
28
|
+
Installs with no runtime dependencies.
|
|
29
|
+
|
|
30
|
+
To move from Tiptap's built-in Placeholder, see [Migrating from the built-in Placeholder](#migrating-from-the-built-in-placeholder).
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
Register `Placeholder` in the extensions array and set the hint text.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { Editor } from '@tiptap/core'
|
|
38
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
39
|
+
import { Placeholder } from '@docs.plus/extension-placeholder'
|
|
40
|
+
|
|
41
|
+
const editor = new Editor({
|
|
42
|
+
element: document.querySelector('#editor')!,
|
|
43
|
+
extensions: [
|
|
44
|
+
StarterKit,
|
|
45
|
+
// StarterKit does not include a placeholder extension.
|
|
46
|
+
// If your array already holds Tiptap's built-in Placeholder, remove it.
|
|
47
|
+
// Register one placeholder extension, never both.
|
|
48
|
+
Placeholder.configure({
|
|
49
|
+
placeholder: 'Write something …'
|
|
50
|
+
})
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The snippet mounts the editor into `#editor`, so that element must exist on the page. The editor renders no hint yet, because the package ships no CSS — add the rule in [Styling](#styling).
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
`.configure({ … })` accepts these four options and nothing else.
|
|
60
|
+
|
|
61
|
+
| Option | Type | Default | Description |
|
|
62
|
+
| ---------------------- | ------------------------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `placeholder` | `((props: PlaceholderRenderProps) => string) \| string` | `'Write something …'` | Hint text, or a callback that returns it per textblock. See [`placeholder`](#placeholder). |
|
|
64
|
+
| `emptyNodeClass` | `string` | `'is-empty'` | Class the extension adds to the empty textblock at the cursor, and to every empty ancestor wrapper above it. |
|
|
65
|
+
| `emptyEditorClass` | `string` | `'is-editor-empty'` | Extra class next to `emptyNodeClass` while the whole document is empty. The extension never adds it to the editor root. |
|
|
66
|
+
| `showOnlyWhenEditable` | `boolean` | `true` | Hides the hint and both classes while the editor is read-only. |
|
|
67
|
+
|
|
68
|
+
The extension reads `showOnlyWhenEditable` at render time. `editor.setEditable()` therefore updates the hint and both classes at once, without a transaction.
|
|
69
|
+
|
|
70
|
+
### `placeholder`
|
|
71
|
+
|
|
72
|
+
A string sets one hint for every empty textblock. A callback sets the hint per textblock, and receives one object:
|
|
73
|
+
|
|
74
|
+
- `node` — the empty textblock at the cursor.
|
|
75
|
+
- `parentName` — type name of the parent node, for example `doc`, `listItem` or `blockquote`.
|
|
76
|
+
- `pos` — position of `node` inside `doc`.
|
|
77
|
+
- `doc` — the document the extension builds the decoration from.
|
|
78
|
+
- `hasAnchor` — always `true`, because the extension decorates only the empty textblock at the cursor. See the `hasAnchor` note in [Migrating from the built-in Placeholder](#migrating-from-the-built-in-placeholder).
|
|
79
|
+
- `editor` — the editor instance.
|
|
80
|
+
|
|
81
|
+
The callback runs inside the plugin's `init()` and `apply()`. During `apply()` it runs before the editor commits the transaction, so resolve `pos` against the supplied `doc`, never `editor.state.doc`.
|
|
82
|
+
|
|
83
|
+
To change the hint after the editor exists, use the callback form and read the new text inside it. Then force a rebuild with the dispatch in [Caveats](#caveats).
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
Placeholder.configure({
|
|
87
|
+
placeholder: ({ node, parentName }) => {
|
|
88
|
+
if (node.type.name === 'heading') return "What's the title?"
|
|
89
|
+
if (parentName === 'listItem') return 'List item'
|
|
90
|
+
if (parentName === 'blockquote') return 'Quote'
|
|
91
|
+
return 'Can you add some further context?'
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`node` names the empty textblock and `parentName` names its parent, so the two answer different questions. An empty paragraph inside a list item reports `node.type.name === 'paragraph'` and `parentName === 'listItem'`.
|
|
97
|
+
|
|
98
|
+
A callback that returns `''` emits no decoration — see [Caveats](#caveats).
|
|
99
|
+
|
|
100
|
+
## Caveats
|
|
101
|
+
|
|
102
|
+
The extension adds at most one `data-placeholder` decoration per state, and it treats a node as empty only when `isNodeEmpty(node)` from `@tiptap/core` returns true.
|
|
103
|
+
|
|
104
|
+
- **Nothing renders without CSS.** The package ships no stylesheet, and the extension writes the hint text into a `data-placeholder` attribute. Add the rule in [Styling](#styling).
|
|
105
|
+
- **Two placeholder extensions collide.** This extension and Tiptap's built-in both register the name `placeholder`. Tiptap logs the warning `[tiptap warn]: Duplicate extension names found: ['placeholder']. This can lead to issues.`, and both plugins decorate the document.
|
|
106
|
+
- **A single space counts as content.** The check is `isNodeEmpty(node)` with the default `ignoreWhitespace: false`. A paragraph holding one space, or one hard break, shows no hint.
|
|
107
|
+
- **`showOnlyCurrent`, `includeChildren` and `dataAttribute` do not exist.** TypeScript rejects them in a `.configure({ … })` literal. A plain JavaScript host gets no error, and the extension ignores the key. The option map in [Migrating from the built-in Placeholder](#migrating-from-the-built-in-placeholder) gives the replacement for each one.
|
|
108
|
+
- **An empty hint removes the classes too.** A `placeholder` string or callback that resolves to `''` emits no decoration, so `emptyNodeClass` and `emptyEditorClass` disappear as well. Return a placeholder string of one space to keep the classes without visible text.
|
|
109
|
+
- **A selection at document depth hides the hint.** ⌘A or Ctrl+A produces an `AllSelection` whose anchor sits at that depth. A node selection on a top-level node does the same. The hint returns when the selection moves back into a textblock.
|
|
110
|
+
- **The hint stays after blur.** Nothing in the extension reads `editor.isFocused`, so the decoration survives a blur. Hide it with a CSS rule — see [Styling](#styling).
|
|
111
|
+
- **Do not rely on the hint for accessibility.** It is generated content on a `::before` pseudo-element, outside the document. Set `aria-label` or `aria-placeholder` on the editable element yourself.
|
|
112
|
+
- **The callback runs only on a document or selection change.** The extension rebuilds when a transaction changes the document or sets the selection. Hint text from outside the document, i18n text for example, does not refresh on its own. Force a rebuild with `editor.view.dispatch(editor.state.tr.setSelection(editor.state.selection))`. The same rule keeps the hint stable under [Collaborative editing](#collaborative-editing).
|
|
113
|
+
|
|
114
|
+
## Styling
|
|
115
|
+
|
|
116
|
+
The package ships no CSS.
|
|
117
|
+
|
|
118
|
+
The extension writes the resolved hint text into the `data-placeholder` attribute of the empty textblock at the cursor. Add this rule to render it:
|
|
119
|
+
|
|
120
|
+
```css
|
|
121
|
+
.ProseMirror [data-placeholder]::before {
|
|
122
|
+
content: attr(data-placeholder);
|
|
123
|
+
color: #9ca3af;
|
|
124
|
+
float: left;
|
|
125
|
+
height: 0;
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Class names
|
|
131
|
+
|
|
132
|
+
| Name | Where the extension adds it | When |
|
|
133
|
+
| -------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `data-placeholder` (attribute) | the empty textblock at the cursor only | the cursor sits in an empty textblock, the resolved hint text is not empty, and the editor is editable or `showOnlyWhenEditable` is `false` |
|
|
135
|
+
| `is-empty` (`emptyNodeClass`) | that same textblock, and every empty ancestor wrapper above it | the same three conditions as `data-placeholder` |
|
|
136
|
+
| `is-editor-empty` (`emptyEditorClass`) | the same nodes as `is-empty`, next to it | the same three conditions, and the whole document is empty |
|
|
137
|
+
|
|
138
|
+
Target `[data-placeholder]`, not `.is-empty`. The extension adds the empty class to an empty list item or blockquote, but not the attribute. A `.is-empty::before { content: attr(data-placeholder) }` rule therefore paints an empty pseudo-element on those wrappers as well.
|
|
139
|
+
|
|
140
|
+
The extension never adds `is-editor-empty` to the editor root, because the ancestor walk stops below the document node. A `.ProseMirror.is-editor-empty` selector matches nothing.
|
|
141
|
+
|
|
142
|
+
To show the hint only while the document is fully empty, swap the selector of the rule above for the selector below:
|
|
143
|
+
|
|
144
|
+
```css
|
|
145
|
+
.ProseMirror .is-editor-empty[data-placeholder]::before {
|
|
146
|
+
content: attr(data-placeholder);
|
|
147
|
+
color: #9ca3af;
|
|
148
|
+
float: left;
|
|
149
|
+
height: 0;
|
|
150
|
+
pointer-events: none;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Select the empty ancestor wrappers on their own. This rule dims an empty blockquote or list item. Replace the declaration as needed:
|
|
155
|
+
|
|
156
|
+
```css
|
|
157
|
+
.ProseMirror blockquote.is-empty,
|
|
158
|
+
.ProseMirror li.is-empty {
|
|
159
|
+
opacity: 0.6;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The extension has no focus option. To hide the hint while the editor has no focus, add this rule:
|
|
164
|
+
|
|
165
|
+
```css
|
|
166
|
+
.ProseMirror:not(:focus-within) [data-placeholder]::before {
|
|
167
|
+
content: none;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Collaborative editing
|
|
172
|
+
|
|
173
|
+
A remote edit that leaves the local selection alone keeps the hint, and moves it with the document.
|
|
174
|
+
|
|
175
|
+
The plugin rebuilds its decorations when a transaction changes the document or sets the selection. A Yjs update arrives as a document change, so the rebuild runs and the decoration follows the new position. A transaction that carries only metadata, such as an awareness ping, skips the rebuild and keeps the previous decoration set.
|
|
176
|
+
|
|
177
|
+
The decoration is local to one editor view. It never enters the document, so it never syncs to another client, and each client sees the hint at its own cursor.
|
|
178
|
+
|
|
179
|
+
The clean-room spec [cypress/e2e/external-edit.cy.ts](https://github.com/docs-plus/docs.plus/blob/main/extensions/extension-placeholder/cypress/e2e/external-edit.cy.ts) covers both shapes: text inserted elsewhere, and a paragraph inserted before the empty textblock.
|
|
180
|
+
|
|
181
|
+
## Migrating from the built-in Placeholder
|
|
182
|
+
|
|
183
|
+
Tiptap 3.x ships the built-in Placeholder from `@tiptap/extensions`, subpath `./placeholder`. `@tiptap/extension-placeholder` is the older name for the same extension.
|
|
184
|
+
|
|
185
|
+
Swap the import and keep the same configuration. The options this package accepts carry the built-in's names and defaults.
|
|
186
|
+
|
|
187
|
+
```diff
|
|
188
|
+
- import { Placeholder } from '@tiptap/extensions'
|
|
189
|
+
+ import { Placeholder } from '@docs.plus/extension-placeholder'
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Remove the built-in from the extensions array in the same change. Both register the name `placeholder`, so leaving both in place makes both decorate.
|
|
193
|
+
|
|
194
|
+
| Built-in option | This package |
|
|
195
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
196
|
+
| `placeholder` | Same name, same default `'Write something …'`. The callback also receives `parentName` and `doc`. |
|
|
197
|
+
| `emptyNodeClass` | Same name, same default `'is-empty'`. The extension also adds it to every empty ancestor wrapper. |
|
|
198
|
+
| `emptyEditorClass` | Same name, same default `'is-editor-empty'`. Unchanged: the built-in also adds it to the decorated nodes. |
|
|
199
|
+
| `showOnlyWhenEditable` | Same name, same default `true`. Read at render time, so `editor.setEditable()` updates the hint and both classes at once. |
|
|
200
|
+
| `showOnlyCurrent` | No option. The extension decorates only the empty textblock at the cursor. |
|
|
201
|
+
| `includeChildren` | No option needed. The extension adds `data-placeholder` to the empty textblock at the cursor inside a list item or blockquote too, and adds the empty class to its empty wrappers. |
|
|
202
|
+
| `dataAttribute` | No option. The attribute is always `data-placeholder`. |
|
|
203
|
+
|
|
204
|
+
Five behavior differences remain after the swap:
|
|
205
|
+
|
|
206
|
+
- **Cost.** The built-in scans every top-level block with `doc.descendants` on every editor update, and the whole tree when `includeChildren` is on. This package walks from the cursor up to the first non-empty ancestor.
|
|
207
|
+
- **Rebuild timing.** The built-in re-evaluates the `placeholder` callback on every editor update. This package rebuilds only when a transaction changes the document or sets the selection.
|
|
208
|
+
- **Empty hint.** A resolved hint of `''` removes the decoration and both classes here. The built-in still emits the classes.
|
|
209
|
+
- **Document-depth selection.** ⌘A or Ctrl+A hides the hint here. The built-in keeps it.
|
|
210
|
+
- **`hasAnchor`.** Always `true` here, because the extension decorates only the empty textblock at the cursor. The built-in passes `false` when `showOnlyCurrent: false` decorates a node away from the cursor.
|
|
211
|
+
|
|
212
|
+
Class-based CSS keeps working, with one change: the extension now adds the empty class to an empty list item or blockquote as well. Add `[data-placeholder]` to the selector to reach only the textblock that holds the hint. See [Styling](#styling).
|
|
213
|
+
|
|
214
|
+
Full breaking-change list: [CHANGELOG.md](https://github.com/docs-plus/docs.plus/blob/main/extensions/extension-placeholder/CHANGELOG.md).
|
|
215
|
+
|
|
216
|
+
## TypeScript
|
|
217
|
+
|
|
218
|
+
`dist/index.d.ts` exports three symbols, and the options in [Options](#options) are the whole API.
|
|
219
|
+
|
|
220
|
+
Extension:
|
|
221
|
+
|
|
222
|
+
- `Placeholder` — `Extension<PlaceholderOptions, any>`, registered under the name `placeholder`.
|
|
223
|
+
|
|
224
|
+
Types:
|
|
225
|
+
|
|
226
|
+
- `PlaceholderOptions` — the four option fields.
|
|
227
|
+
- `PlaceholderRenderProps` — the argument the callback form of `placeholder` receives.
|
|
228
|
+
|
|
229
|
+
Nothing else exists, so this README skips four sections:
|
|
230
|
+
|
|
231
|
+
- No `addCommands`. `editor.commands` gains nothing, so there is no Commands section.
|
|
232
|
+
- No `addKeyboardShortcuts` and no keydown handler. The extension binds no keys, so there is no Keyboard shortcuts section.
|
|
233
|
+
- No `addStorage`. `editor.storage.placeholder` holds the empty object Tiptap creates for every extension.
|
|
234
|
+
- No option and no attribute holds a URL, and the source calls no `window.open`, so there is no Security section.
|
|
235
|
+
|
|
236
|
+
## Part of docs.plus
|
|
237
|
+
|
|
238
|
+
This extension is built for and maintained by [docs.plus](https://docs.plus). docs.plus is a free, real-time collaboration tool that lets communities organize knowledge hierarchically, with a chat thread on every heading. docs.plus runs these packages from source in production, so every release is exercised there before it reaches npm.
|
|
239
|
+
|
|
240
|
+
- Website: [docs.plus](https://docs.plus)
|
|
241
|
+
- Project README: [docs-plus/docs.plus](https://github.com/docs-plus/docs.plus#readme)
|
|
242
|
+
- Sibling extensions and recommended pairings: [extensions/README.md](https://github.com/docs-plus/docs.plus/blob/main/extensions/README.md)
|
|
243
|
+
|
|
244
|
+
## Contributing
|
|
245
|
+
|
|
246
|
+
Bug reports and PRs welcome. Setup, test commands, and the playground harness live in [CONTRIBUTING.md](https://github.com/docs-plus/docs.plus/blob/main/extensions/extension-placeholder/CONTRIBUTING.md).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT — see [LICENSE](https://github.com/docs-plus/docs.plus/blob/main/LICENSE).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var p=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var g=Object.getOwnPropertyNames;var N=Object.prototype.hasOwnProperty;var C=(e,r)=>{for(var o in r)p(e,o,{get:r[o],enumerable:!0})},D=(e,r,o,t)=>{if(r&&typeof r=="object"||typeof r=="function")for(let n of g(r))!N.call(e,n)&&n!==o&&p(e,n,{get:()=>r[n],enumerable:!(t=b(r,n))||t.enumerable});return e};var O=e=>D(p({},"__esModule",{value:!0}),e);var w={};C(w,{Placeholder:()=>P});module.exports=O(w);var a=require("@tiptap/core"),l=require("@tiptap/pm/state"),i=require("@tiptap/pm/view"),x=new l.PluginKey("placeholder");function E(e,r,o){let{$anchor:t}=e.selection;if(t.depth===0)return i.DecorationSet.empty;let n=t.parent;if(!n.type.isTextblock||!(0,a.isNodeEmpty)(n))return i.DecorationSet.empty;let s=t.before(t.depth),S=t.node(t.depth-1).type.name,c=typeof o.placeholder=="function"?o.placeholder({editor:r,node:n,pos:s,hasAnchor:!0,parentName:S,doc:e.doc}):o.placeholder;if(!c)return i.DecorationSet.empty;let h=[o.emptyNodeClass];(0,a.isNodeEmpty)(e.doc)&&h.push(o.emptyEditorClass);let m=h.join(" "),u=[i.Decoration.node(s,s+n.nodeSize,{class:m,"data-placeholder":c})];for(let d=t.depth-1;d>=1;d--){let y=t.node(d);if(!(0,a.isNodeEmpty)(y))break;let f=t.before(d);u.push(i.Decoration.node(f,f+y.nodeSize,{class:m}))}return i.DecorationSet.create(e.doc,u)}var P=a.Extension.create({name:"placeholder",addOptions(){return{emptyEditorClass:"is-editor-empty",emptyNodeClass:"is-empty",placeholder:"Write something \u2026",showOnlyWhenEditable:!0}},addProseMirrorPlugins(){let e=this.editor,r=this.options;return[new l.Plugin({key:x,state:{init(o,t){return E(t,e,r)},apply(o,t,n,s){return!o.docChanged&&!o.selectionSet?t:E(s,e,r)}},props:{decorations(o){return r.showOnlyWhenEditable&&!e.isEditable?null:this.getState(o)??null}}})]}});0&&(module.exports={Placeholder});
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/placeholder.ts"],"sourcesContent":["export type { PlaceholderOptions, PlaceholderRenderProps } from './placeholder'\nexport { Placeholder } from './placeholder'\n","import type { Editor } from '@tiptap/core'\nimport { Extension, isNodeEmpty } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\n/** Arguments passed to the `placeholder` callback when it is a function. */\nexport interface PlaceholderRenderProps {\n editor: Editor\n node: ProseMirrorNode\n pos: number\n hasAnchor: boolean\n /** Parent node type name (e.g. 'doc', 'listItem', 'blockquote'). Safe during apply(). */\n parentName: string\n /** Doc that `pos` resolves against. Use this — `editor.state.doc` is stale during apply(). */\n doc: ProseMirrorNode\n}\n\nexport interface PlaceholderOptions {\n emptyEditorClass: string\n emptyNodeClass: string\n placeholder: ((props: PlaceholderRenderProps) => string) | string\n showOnlyWhenEditable: boolean\n}\n\nconst PLACEHOLDER_KEY = new PluginKey('placeholder')\n\n/**\n * At most one placeholder decoration at the cursor's empty parent, plus\n * empty-class decorations on empty ancestor wrappers. Cost: O(depth).\n */\nfunction buildFromCursor(\n state: EditorState,\n editor: Editor,\n options: PlaceholderOptions\n): DecorationSet {\n const { $anchor } = state.selection\n if ($anchor.depth === 0) return DecorationSet.empty\n\n const node = $anchor.parent\n if (!node.type.isTextblock || !isNodeEmpty(node)) return DecorationSet.empty\n\n const pos = $anchor.before($anchor.depth)\n const parentName = $anchor.node($anchor.depth - 1).type.name\n const placeholderText =\n typeof options.placeholder === 'function'\n ? options.placeholder({ editor, node, pos, hasAnchor: true, parentName, doc: state.doc })\n : options.placeholder\n\n if (!placeholderText) return DecorationSet.empty\n\n const classes = [options.emptyNodeClass]\n if (isNodeEmpty(state.doc)) {\n classes.push(options.emptyEditorClass)\n }\n\n const classStr = classes.join(' ')\n const decorations: Decoration[] = [\n Decoration.node(pos, pos + node.nodeSize, {\n class: classStr,\n 'data-placeholder': placeholderText\n })\n ]\n\n // Propagate empty class to ancestor wrappers (list items, blockquotes, etc.)\n for (let d = $anchor.depth - 1; d >= 1; d--) {\n const ancestor = $anchor.node(d)\n if (!isNodeEmpty(ancestor)) break\n const ancestorPos = $anchor.before(d)\n decorations.push(\n Decoration.node(ancestorPos, ancestorPos + ancestor.nodeSize, {\n class: classStr\n })\n )\n }\n\n return DecorationSet.create(state.doc, decorations)\n}\n\n/**\n * Drop-in replacement for the built-in Placeholder with O(depth) cursor-based\n * state init/apply instead of the built-in's O(N) doc.descendants scan.\n * Decorates the empty node at the cursor plus empty ancestor wrappers —\n * inherently showOnlyCurrent + includeChildren.\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n placeholder: 'Write something …',\n showOnlyWhenEditable: true\n }\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n const options = this.options\n\n return [\n new Plugin({\n key: PLACEHOLDER_KEY,\n\n state: {\n init(_: unknown, state: EditorState): DecorationSet {\n return buildFromCursor(state, editor, options)\n },\n\n apply(\n tr: Transaction,\n old: DecorationSet,\n _oldState: EditorState,\n newState: EditorState\n ): DecorationSet {\n // Decorations derive solely from doc + selection, so meta-only\n // transactions (collab pings, plugin meta) cannot change them.\n if (!tr.docChanged && !tr.selectionSet) return old\n\n return buildFromCursor(newState, editor, options)\n }\n },\n\n props: {\n // Gate editability here, not in apply. `setEditable()` refreshes props via\n // view.updateState without dispatching a transaction, so the placeholder\n // toggles immediately when the editor flips to/from read-only.\n decorations(state: EditorState): DecorationSet | null {\n if (options.showOnlyWhenEditable && !editor.isEditable) return null\n return this.getState(state) ?? null\n }\n }\n })\n ]\n }\n})\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,iBAAAE,IAAA,eAAAC,EAAAH,GCCA,IAAAI,EAAuC,wBAGvCC,EAAkC,4BAClCC,EAA0C,2BAqBpCC,EAAkB,IAAI,YAAU,aAAa,EAMnD,SAASC,EACPC,EACAC,EACAC,EACe,CACf,GAAM,CAAE,QAAAC,CAAQ,EAAIH,EAAM,UAC1B,GAAIG,EAAQ,QAAU,EAAG,OAAO,gBAAc,MAE9C,IAAMC,EAAOD,EAAQ,OACrB,GAAI,CAACC,EAAK,KAAK,aAAe,IAAC,eAAYA,CAAI,EAAG,OAAO,gBAAc,MAEvE,IAAMC,EAAMF,EAAQ,OAAOA,EAAQ,KAAK,EAClCG,EAAaH,EAAQ,KAAKA,EAAQ,MAAQ,CAAC,EAAE,KAAK,KAClDI,EACJ,OAAOL,EAAQ,aAAgB,WAC3BA,EAAQ,YAAY,CAAE,OAAAD,EAAQ,KAAAG,EAAM,IAAAC,EAAK,UAAW,GAAM,WAAAC,EAAY,IAAKN,EAAM,GAAI,CAAC,EACtFE,EAAQ,YAEd,GAAI,CAACK,EAAiB,OAAO,gBAAc,MAE3C,IAAMC,EAAU,CAACN,EAAQ,cAAc,KACnC,eAAYF,EAAM,GAAG,GACvBQ,EAAQ,KAAKN,EAAQ,gBAAgB,EAGvC,IAAMO,EAAWD,EAAQ,KAAK,GAAG,EAC3BE,EAA4B,CAChC,aAAW,KAAKL,EAAKA,EAAMD,EAAK,SAAU,CACxC,MAAOK,EACP,mBAAoBF,CACtB,CAAC,CACH,EAGA,QAAS,EAAIJ,EAAQ,MAAQ,EAAG,GAAK,EAAG,IAAK,CAC3C,IAAMQ,EAAWR,EAAQ,KAAK,CAAC,EAC/B,GAAI,IAAC,eAAYQ,CAAQ,EAAG,MAC5B,IAAMC,EAAcT,EAAQ,OAAO,CAAC,EACpCO,EAAY,KACV,aAAW,KAAKE,EAAaA,EAAcD,EAAS,SAAU,CAC5D,MAAOF,CACT,CAAC,CACH,CACF,CAEA,OAAO,gBAAc,OAAOT,EAAM,IAAKU,CAAW,CACpD,CAQO,IAAMG,EAAc,YAAU,OAA2B,CAC9D,KAAM,cAEN,YAAa,CACX,MAAO,CACL,iBAAkB,kBAClB,eAAgB,WAChB,YAAa,yBACb,qBAAsB,EACxB,CACF,EAEA,uBAAwB,CACtB,IAAMZ,EAAS,KAAK,OACdC,EAAU,KAAK,QAErB,MAAO,CACL,IAAI,SAAO,CACT,IAAKJ,EAEL,MAAO,CACL,KAAKgB,EAAYd,EAAmC,CAClD,OAAOD,EAAgBC,EAAOC,EAAQC,CAAO,CAC/C,EAEA,MACEa,EACAC,EACAC,EACAC,EACe,CAGf,MAAI,CAACH,EAAG,YAAc,CAACA,EAAG,aAAqBC,EAExCjB,EAAgBmB,EAAUjB,EAAQC,CAAO,CAClD,CACF,EAEA,MAAO,CAIL,YAAYF,EAA0C,CACpD,OAAIE,EAAQ,sBAAwB,CAACD,EAAO,WAAmB,KACxD,KAAK,SAASD,CAAK,GAAK,IACjC,CACF,CACF,CAAC,CACH,CACF,CACF,CAAC","names":["index_exports","__export","Placeholder","__toCommonJS","import_core","import_state","import_view","PLACEHOLDER_KEY","buildFromCursor","state","editor","options","$anchor","node","pos","parentName","placeholderText","classes","classStr","decorations","ancestor","ancestorPos","Placeholder","_","tr","old","_oldState","newState"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Extension, Editor } from '@tiptap/core';
|
|
2
|
+
import { Node } from '@tiptap/pm/model';
|
|
3
|
+
|
|
4
|
+
/** Arguments passed to the `placeholder` callback when it is a function. */
|
|
5
|
+
interface PlaceholderRenderProps {
|
|
6
|
+
editor: Editor;
|
|
7
|
+
node: Node;
|
|
8
|
+
pos: number;
|
|
9
|
+
hasAnchor: boolean;
|
|
10
|
+
/** Parent node type name (e.g. 'doc', 'listItem', 'blockquote'). Safe during apply(). */
|
|
11
|
+
parentName: string;
|
|
12
|
+
/** Doc that `pos` resolves against. Use this — `editor.state.doc` is stale during apply(). */
|
|
13
|
+
doc: Node;
|
|
14
|
+
}
|
|
15
|
+
interface PlaceholderOptions {
|
|
16
|
+
emptyEditorClass: string;
|
|
17
|
+
emptyNodeClass: string;
|
|
18
|
+
placeholder: ((props: PlaceholderRenderProps) => string) | string;
|
|
19
|
+
showOnlyWhenEditable: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drop-in replacement for the built-in Placeholder with O(depth) cursor-based
|
|
23
|
+
* state init/apply instead of the built-in's O(N) doc.descendants scan.
|
|
24
|
+
* Decorates the empty node at the cursor plus empty ancestor wrappers —
|
|
25
|
+
* inherently showOnlyCurrent + includeChildren.
|
|
26
|
+
*/
|
|
27
|
+
declare const Placeholder: Extension<PlaceholderOptions, any>;
|
|
28
|
+
|
|
29
|
+
export { Placeholder, type PlaceholderOptions, type PlaceholderRenderProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Extension, Editor } from '@tiptap/core';
|
|
2
|
+
import { Node } from '@tiptap/pm/model';
|
|
3
|
+
|
|
4
|
+
/** Arguments passed to the `placeholder` callback when it is a function. */
|
|
5
|
+
interface PlaceholderRenderProps {
|
|
6
|
+
editor: Editor;
|
|
7
|
+
node: Node;
|
|
8
|
+
pos: number;
|
|
9
|
+
hasAnchor: boolean;
|
|
10
|
+
/** Parent node type name (e.g. 'doc', 'listItem', 'blockquote'). Safe during apply(). */
|
|
11
|
+
parentName: string;
|
|
12
|
+
/** Doc that `pos` resolves against. Use this — `editor.state.doc` is stale during apply(). */
|
|
13
|
+
doc: Node;
|
|
14
|
+
}
|
|
15
|
+
interface PlaceholderOptions {
|
|
16
|
+
emptyEditorClass: string;
|
|
17
|
+
emptyNodeClass: string;
|
|
18
|
+
placeholder: ((props: PlaceholderRenderProps) => string) | string;
|
|
19
|
+
showOnlyWhenEditable: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Drop-in replacement for the built-in Placeholder with O(depth) cursor-based
|
|
23
|
+
* state init/apply instead of the built-in's O(N) doc.descendants scan.
|
|
24
|
+
* Decorates the empty node at the cursor plus empty ancestor wrappers —
|
|
25
|
+
* inherently showOnlyCurrent + includeChildren.
|
|
26
|
+
*/
|
|
27
|
+
declare const Placeholder: Extension<PlaceholderOptions, any>;
|
|
28
|
+
|
|
29
|
+
export { Placeholder, type PlaceholderOptions, type PlaceholderRenderProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{Extension as P,isNodeEmpty as d}from"@tiptap/core";import{Plugin as S,PluginKey as b}from"@tiptap/pm/state";import{Decoration as y,DecorationSet as s}from"@tiptap/pm/view";var g=new b("placeholder");function f(t,r,o){let{$anchor:e}=t.selection;if(e.depth===0)return s.empty;let n=e.parent;if(!n.type.isTextblock||!d(n))return s.empty;let i=e.before(e.depth),E=e.node(e.depth-1).type.name,l=typeof o.placeholder=="function"?o.placeholder({editor:r,node:n,pos:i,hasAnchor:!0,parentName:E,doc:t.doc}):o.placeholder;if(!l)return s.empty;let p=[o.emptyNodeClass];d(t.doc)&&p.push(o.emptyEditorClass);let c=p.join(" "),h=[y.node(i,i+n.nodeSize,{class:c,"data-placeholder":l})];for(let a=e.depth-1;a>=1;a--){let m=e.node(a);if(!d(m))break;let u=e.before(a);h.push(y.node(u,u+m.nodeSize,{class:c}))}return s.create(t.doc,h)}var N=P.create({name:"placeholder",addOptions(){return{emptyEditorClass:"is-editor-empty",emptyNodeClass:"is-empty",placeholder:"Write something \u2026",showOnlyWhenEditable:!0}},addProseMirrorPlugins(){let t=this.editor,r=this.options;return[new S({key:g,state:{init(o,e){return f(e,t,r)},apply(o,e,n,i){return!o.docChanged&&!o.selectionSet?e:f(i,t,r)}},props:{decorations(o){return r.showOnlyWhenEditable&&!t.isEditable?null:this.getState(o)??null}}})]}});export{N as Placeholder};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/placeholder.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Extension, isNodeEmpty } from '@tiptap/core'\nimport type { Node as ProseMirrorNode } from '@tiptap/pm/model'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\nimport { Decoration, DecorationSet } from '@tiptap/pm/view'\n\n/** Arguments passed to the `placeholder` callback when it is a function. */\nexport interface PlaceholderRenderProps {\n editor: Editor\n node: ProseMirrorNode\n pos: number\n hasAnchor: boolean\n /** Parent node type name (e.g. 'doc', 'listItem', 'blockquote'). Safe during apply(). */\n parentName: string\n /** Doc that `pos` resolves against. Use this — `editor.state.doc` is stale during apply(). */\n doc: ProseMirrorNode\n}\n\nexport interface PlaceholderOptions {\n emptyEditorClass: string\n emptyNodeClass: string\n placeholder: ((props: PlaceholderRenderProps) => string) | string\n showOnlyWhenEditable: boolean\n}\n\nconst PLACEHOLDER_KEY = new PluginKey('placeholder')\n\n/**\n * At most one placeholder decoration at the cursor's empty parent, plus\n * empty-class decorations on empty ancestor wrappers. Cost: O(depth).\n */\nfunction buildFromCursor(\n state: EditorState,\n editor: Editor,\n options: PlaceholderOptions\n): DecorationSet {\n const { $anchor } = state.selection\n if ($anchor.depth === 0) return DecorationSet.empty\n\n const node = $anchor.parent\n if (!node.type.isTextblock || !isNodeEmpty(node)) return DecorationSet.empty\n\n const pos = $anchor.before($anchor.depth)\n const parentName = $anchor.node($anchor.depth - 1).type.name\n const placeholderText =\n typeof options.placeholder === 'function'\n ? options.placeholder({ editor, node, pos, hasAnchor: true, parentName, doc: state.doc })\n : options.placeholder\n\n if (!placeholderText) return DecorationSet.empty\n\n const classes = [options.emptyNodeClass]\n if (isNodeEmpty(state.doc)) {\n classes.push(options.emptyEditorClass)\n }\n\n const classStr = classes.join(' ')\n const decorations: Decoration[] = [\n Decoration.node(pos, pos + node.nodeSize, {\n class: classStr,\n 'data-placeholder': placeholderText\n })\n ]\n\n // Propagate empty class to ancestor wrappers (list items, blockquotes, etc.)\n for (let d = $anchor.depth - 1; d >= 1; d--) {\n const ancestor = $anchor.node(d)\n if (!isNodeEmpty(ancestor)) break\n const ancestorPos = $anchor.before(d)\n decorations.push(\n Decoration.node(ancestorPos, ancestorPos + ancestor.nodeSize, {\n class: classStr\n })\n )\n }\n\n return DecorationSet.create(state.doc, decorations)\n}\n\n/**\n * Drop-in replacement for the built-in Placeholder with O(depth) cursor-based\n * state init/apply instead of the built-in's O(N) doc.descendants scan.\n * Decorates the empty node at the cursor plus empty ancestor wrappers —\n * inherently showOnlyCurrent + includeChildren.\n */\nexport const Placeholder = Extension.create<PlaceholderOptions>({\n name: 'placeholder',\n\n addOptions() {\n return {\n emptyEditorClass: 'is-editor-empty',\n emptyNodeClass: 'is-empty',\n placeholder: 'Write something …',\n showOnlyWhenEditable: true\n }\n },\n\n addProseMirrorPlugins() {\n const editor = this.editor\n const options = this.options\n\n return [\n new Plugin({\n key: PLACEHOLDER_KEY,\n\n state: {\n init(_: unknown, state: EditorState): DecorationSet {\n return buildFromCursor(state, editor, options)\n },\n\n apply(\n tr: Transaction,\n old: DecorationSet,\n _oldState: EditorState,\n newState: EditorState\n ): DecorationSet {\n // Decorations derive solely from doc + selection, so meta-only\n // transactions (collab pings, plugin meta) cannot change them.\n if (!tr.docChanged && !tr.selectionSet) return old\n\n return buildFromCursor(newState, editor, options)\n }\n },\n\n props: {\n // Gate editability here, not in apply. `setEditable()` refreshes props via\n // view.updateState without dispatching a transaction, so the placeholder\n // toggles immediately when the editor flips to/from read-only.\n decorations(state: EditorState): DecorationSet | null {\n if (options.showOnlyWhenEditable && !editor.isEditable) return null\n return this.getState(state) ?? null\n }\n }\n })\n ]\n }\n})\n"],"mappings":"AACA,OAAS,aAAAA,EAAW,eAAAC,MAAmB,eAGvC,OAAS,UAAAC,EAAQ,aAAAC,MAAiB,mBAClC,OAAS,cAAAC,EAAY,iBAAAC,MAAqB,kBAqB1C,IAAMC,EAAkB,IAAIH,EAAU,aAAa,EAMnD,SAASI,EACPC,EACAC,EACAC,EACe,CACf,GAAM,CAAE,QAAAC,CAAQ,EAAIH,EAAM,UAC1B,GAAIG,EAAQ,QAAU,EAAG,OAAON,EAAc,MAE9C,IAAMO,EAAOD,EAAQ,OACrB,GAAI,CAACC,EAAK,KAAK,aAAe,CAACX,EAAYW,CAAI,EAAG,OAAOP,EAAc,MAEvE,IAAMQ,EAAMF,EAAQ,OAAOA,EAAQ,KAAK,EAClCG,EAAaH,EAAQ,KAAKA,EAAQ,MAAQ,CAAC,EAAE,KAAK,KAClDI,EACJ,OAAOL,EAAQ,aAAgB,WAC3BA,EAAQ,YAAY,CAAE,OAAAD,EAAQ,KAAAG,EAAM,IAAAC,EAAK,UAAW,GAAM,WAAAC,EAAY,IAAKN,EAAM,GAAI,CAAC,EACtFE,EAAQ,YAEd,GAAI,CAACK,EAAiB,OAAOV,EAAc,MAE3C,IAAMW,EAAU,CAACN,EAAQ,cAAc,EACnCT,EAAYO,EAAM,GAAG,GACvBQ,EAAQ,KAAKN,EAAQ,gBAAgB,EAGvC,IAAMO,EAAWD,EAAQ,KAAK,GAAG,EAC3BE,EAA4B,CAChCd,EAAW,KAAKS,EAAKA,EAAMD,EAAK,SAAU,CACxC,MAAOK,EACP,mBAAoBF,CACtB,CAAC,CACH,EAGA,QAASI,EAAIR,EAAQ,MAAQ,EAAGQ,GAAK,EAAGA,IAAK,CAC3C,IAAMC,EAAWT,EAAQ,KAAKQ,CAAC,EAC/B,GAAI,CAAClB,EAAYmB,CAAQ,EAAG,MAC5B,IAAMC,EAAcV,EAAQ,OAAOQ,CAAC,EACpCD,EAAY,KACVd,EAAW,KAAKiB,EAAaA,EAAcD,EAAS,SAAU,CAC5D,MAAOH,CACT,CAAC,CACH,CACF,CAEA,OAAOZ,EAAc,OAAOG,EAAM,IAAKU,CAAW,CACpD,CAQO,IAAMI,EAActB,EAAU,OAA2B,CAC9D,KAAM,cAEN,YAAa,CACX,MAAO,CACL,iBAAkB,kBAClB,eAAgB,WAChB,YAAa,yBACb,qBAAsB,EACxB,CACF,EAEA,uBAAwB,CACtB,IAAMS,EAAS,KAAK,OACdC,EAAU,KAAK,QAErB,MAAO,CACL,IAAIR,EAAO,CACT,IAAKI,EAEL,MAAO,CACL,KAAKiB,EAAYf,EAAmC,CAClD,OAAOD,EAAgBC,EAAOC,EAAQC,CAAO,CAC/C,EAEA,MACEc,EACAC,EACAC,EACAC,EACe,CAGf,MAAI,CAACH,EAAG,YAAc,CAACA,EAAG,aAAqBC,EAExClB,EAAgBoB,EAAUlB,EAAQC,CAAO,CAClD,CACF,EAEA,MAAO,CAIL,YAAYF,EAA0C,CACpD,OAAIE,EAAQ,sBAAwB,CAACD,EAAO,WAAmB,KACxD,KAAK,SAASD,CAAK,GAAK,IACjC,CACF,CACF,CAAC,CACH,CACF,CACF,CAAC","names":["Extension","isNodeEmpty","Plugin","PluginKey","Decoration","DecorationSet","PLACEHOLDER_KEY","buildFromCursor","state","editor","options","$anchor","node","pos","parentName","placeholderText","classes","classStr","decorations","d","ancestor","ancestorPos","Placeholder","_","tr","old","_oldState","newState"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docs.plus/extension-placeholder",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A performance-optimized Tiptap placeholder extension using O(depth) cursor-based decoration",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"require": {
|
|
15
|
+
"types": "./dist/index.d.cts",
|
|
16
|
+
"default": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"author": "Hossein Marzban",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"homepage": "https://github.com/docs-plus/docs.plus/tree/main/extensions/extension-placeholder#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/docs-plus/docs.plus/issues"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/docs-plus/docs.plus",
|
|
31
|
+
"directory": "extensions/extension-placeholder"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "NODE_ENV=production bunx tsup",
|
|
35
|
+
"dev": "bunx tsup --watch",
|
|
36
|
+
"playground": "bun --hot docs-playground --entry ./test/playground/main.ts --port 5177",
|
|
37
|
+
"docs:screenshots": "bun run build && bunx start-server-and-test playground http://127.0.0.1:5177 'bunx cypress run --config specPattern=cypress/docs/**/*.cy.ts'",
|
|
38
|
+
"pretest": "bun run build",
|
|
39
|
+
"test": "bunx start-server-and-test playground http://127.0.0.1:5177 cypress:run",
|
|
40
|
+
"test:e2e": "bunx start-server-and-test playground http://127.0.0.1:5177 cypress:run",
|
|
41
|
+
"test:e2e:watch": "bunx start-server-and-test playground http://127.0.0.1:5177 cypress:open",
|
|
42
|
+
"cypress:run": "bunx cypress run",
|
|
43
|
+
"cypress:open": "bunx cypress open",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"prepack": "bunx release-prepack",
|
|
46
|
+
"prepublishOnly": "bunx release-preflight"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"keywords": [
|
|
57
|
+
"tiptap",
|
|
58
|
+
"tiptap extension",
|
|
59
|
+
"placeholder",
|
|
60
|
+
"rich-text-editor",
|
|
61
|
+
"prosemirror",
|
|
62
|
+
"docs.plus"
|
|
63
|
+
],
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@tiptap/core": "^3.22.3",
|
|
66
|
+
"@tiptap/pm": "^3.22.3"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@docs.plus/playground": "0.0.0",
|
|
70
|
+
"@docs.plus/release-tooling": "0.1.0",
|
|
71
|
+
"@tiptap/core": "^3.22.3",
|
|
72
|
+
"@tiptap/pm": "^3.22.3",
|
|
73
|
+
"@tiptap/starter-kit": "^3.22.3",
|
|
74
|
+
"cypress": "^15.13.1",
|
|
75
|
+
"cypress-real-events": "^1.15.0",
|
|
76
|
+
"eslint": "^9.39.4",
|
|
77
|
+
"start-server-and-test": "^2.1.0",
|
|
78
|
+
"tsup": "^8.5.1",
|
|
79
|
+
"typescript": "^5.9.3"
|
|
80
|
+
}
|
|
81
|
+
}
|