@seorii/tiptap 0.4.4 → 0.4.6

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.
@@ -10,5 +10,7 @@ declare module '@tiptap/core' {
10
10
  };
11
11
  }
12
12
  }
13
- declare const _default: Node<any, any>[];
13
+ declare const _default: Node<{
14
+ HTMLAttributes: {};
15
+ }, any>[];
14
16
  export default _default;
@@ -1,6 +1,42 @@
1
1
  import { Node, mergeAttributes } from '@tiptap/core';
2
+ import { NodeSelection, Plugin, PluginKey } from '@tiptap/pm/state';
2
3
  import './style.css';
3
4
  const normalizeColumnCount = (value) => (Number(value) === 3 ? 3 : 2);
5
+ const columnsSelectionPluginKey = new PluginKey('tiptap-columns-selection');
6
+ const isColumnsSelectHandleHit = (element, event) => {
7
+ const rect = element.getBoundingClientRect();
8
+ const handleInset = 4;
9
+ const handleSize = 32;
10
+ return (event.clientX >= rect.right - handleSize - handleInset &&
11
+ event.clientX <= rect.right - handleInset &&
12
+ event.clientY >= rect.top + handleInset &&
13
+ event.clientY <= rect.top + handleSize + handleInset);
14
+ };
15
+ const findNodePosByDOM = (view, element, nodeName) => {
16
+ let found = null;
17
+ view.state.doc.descendants((node, pos) => {
18
+ if (found !== null || node.type.name !== nodeName)
19
+ return;
20
+ if (view.nodeDOM(pos) === element) {
21
+ found = pos;
22
+ return false;
23
+ }
24
+ });
25
+ return found;
26
+ };
27
+ const tryCreateNodeSelection = (doc, pos) => {
28
+ if (pos < 0 || pos > doc.content.size)
29
+ return null;
30
+ const node = doc.nodeAt(pos);
31
+ if (!node || node.type.spec.selectable === false)
32
+ return null;
33
+ try {
34
+ return NodeSelection.create(doc, pos);
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ };
4
40
  const Column = Node.create({
5
41
  name: 'column',
6
42
  content: 'block+',
@@ -31,6 +67,7 @@ const Columns = Node.create({
31
67
  isolating: true,
32
68
  defining: true,
33
69
  draggable: true,
70
+ selectable: true,
34
71
  addOptions() {
35
72
  return {
36
73
  HTMLAttributes: {}
@@ -83,6 +120,40 @@ const Columns = Node.create({
83
120
  setTwoColumns: () => ({ commands }) => commands.setColumns(2),
84
121
  setThreeColumns: () => ({ commands }) => commands.setColumns(3)
85
122
  };
123
+ },
124
+ addProseMirrorPlugins() {
125
+ const editor = this.editor;
126
+ const nodeName = this.name;
127
+ return [
128
+ new Plugin({
129
+ key: columnsSelectionPluginKey,
130
+ props: {
131
+ handleDOMEvents: {
132
+ mousedown(view, event) {
133
+ if (!editor.isEditable)
134
+ return false;
135
+ if (!(event.target instanceof HTMLElement))
136
+ return false;
137
+ const columnsElement = event.target.closest('.tiptap-columns');
138
+ if (!columnsElement || !isColumnsSelectHandleHit(columnsElement, event)) {
139
+ return false;
140
+ }
141
+ const pos = findNodePosByDOM(view, columnsElement, nodeName);
142
+ if (pos === null)
143
+ return false;
144
+ const selection = tryCreateNodeSelection(view.state.doc, pos);
145
+ if (!selection)
146
+ return false;
147
+ event.preventDefault();
148
+ event.stopPropagation();
149
+ view.dispatch(view.state.tr.setSelection(selection).scrollIntoView());
150
+ view.focus();
151
+ return true;
152
+ }
153
+ }
154
+ }
155
+ })
156
+ ];
86
157
  }
87
158
  });
88
159
  export default [Columns, Column];
@@ -1,4 +1,5 @@
1
1
  .tiptap-columns {
2
+ position: relative;
2
3
  display: grid;
3
4
  gap: 12px;
4
5
  margin: 12px 0;
@@ -12,6 +13,13 @@
12
13
  grid-template-columns: repeat(3, minmax(0, 1fr));
13
14
  }
14
15
 
16
+ @media (max-width: 768px) {
17
+ .tiptap-columns.columns-2,
18
+ .tiptap-columns.columns-3 {
19
+ grid-template-columns: minmax(0, 1fr);
20
+ }
21
+ }
22
+
15
23
  .tiptap-column {
16
24
  min-width: 0;
17
25
  padding: 10px 12px;
@@ -35,3 +43,40 @@
35
43
  .editable .tiptap-column:hover {
36
44
  border-color: var(--primary-light7, rgba(112, 112, 112, 0.65));
37
45
  }
46
+
47
+ .editable .tiptap-columns::before {
48
+ position: absolute;
49
+ z-index: 1;
50
+ top: 4px;
51
+ right: 4px;
52
+ box-sizing: border-box;
53
+ width: 32px;
54
+ height: 32px;
55
+ border: 1px solid var(--primary-light4, rgba(112, 112, 112, 0.4));
56
+ border-radius: 6px;
57
+ background-color: var(--surface, Canvas);
58
+ background-image: radial-gradient(currentColor 1.2px, transparent 1.2px);
59
+ background-position: 7px 7px;
60
+ background-size: 8px 8px;
61
+ color: var(--primary-light8, rgba(112, 112, 112, 0.75));
62
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
63
+ content: '';
64
+ cursor: pointer;
65
+ opacity: 0;
66
+ transition:
67
+ opacity 0.15s ease,
68
+ border-color 0.15s ease,
69
+ color 0.15s ease;
70
+ }
71
+
72
+ .editable .tiptap-columns:hover::before,
73
+ .editable .tiptap-columns.ProseMirror-selectednode::before {
74
+ opacity: 1;
75
+ border-color: var(--primary-light7, rgba(112, 112, 112, 0.65));
76
+ color: var(--primary, currentColor);
77
+ }
78
+
79
+ .editable .tiptap-columns.ProseMirror-selectednode {
80
+ outline: 3px solid var(--primary);
81
+ outline-offset: 2px;
82
+ }
@@ -71,14 +71,13 @@ export const dropImagePlugin = () => {
71
71
  const upload = imageUploader || fallbackUpload;
72
72
  const items = Array.from(event.clipboardData?.items || []);
73
73
  const { schema } = view.state;
74
+ let handledImagePaste = false;
74
75
  items.forEach((item) => {
75
76
  const image = item.getAsFile();
76
77
  if (item.type.indexOf('image') === 0) {
78
+ handledImagePaste = true;
77
79
  event.preventDefault();
78
- const skeleton = insertUploadSkeleton({
79
- state: view.state,
80
- view
81
- }, {
80
+ const skeleton = insertUploadSkeleton({ view }, {
82
81
  kind: 'image',
83
82
  height: 220
84
83
  });
@@ -119,7 +118,7 @@ export const dropImagePlugin = () => {
119
118
  reader.readAsDataURL(image);
120
119
  }
121
120
  });
122
- return false;
121
+ return handledImagePaste;
123
122
  },
124
123
  drop: (view, event) => {
125
124
  const imageUploader = window.__image_uploader;
@@ -142,10 +141,7 @@ export const dropImagePlugin = () => {
142
141
  return false;
143
142
  images.forEach(async (image) => {
144
143
  const reader = new FileReader();
145
- const skeleton = insertUploadSkeleton({
146
- state: view.state,
147
- view
148
- }, {
144
+ const skeleton = insertUploadSkeleton({ view }, {
149
145
  kind: 'image',
150
146
  height: 220,
151
147
  at: coordinates.pos
@@ -1,2 +1,6 @@
1
- declare const _default: (crossorigin?: string) => import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>;
1
+ import { type ImageOptions } from '@tiptap/extension-image';
2
+ type ImageOptionsWithSizes = ImageOptions & {
3
+ sizes: string[];
4
+ };
5
+ declare const _default: (crossorigin?: string) => import("@tiptap/core").Node<ImageOptionsWithSizes, any>;
2
6
  export default _default;
@@ -1,4 +1,4 @@
1
- import Image from '@tiptap/extension-image';
1
+ import Image, {} from '@tiptap/extension-image';
2
2
  import { mergeAttributes } from '@tiptap/core';
3
3
  import { dropImagePlugin } from './dragdrop';
4
4
  export default (crossorigin = 'anonymous') => Image.extend({
@@ -453,7 +453,6 @@ function createResizeHandleDecoration(nodePos, widgetPos, resizeMeta, node) {
453
453
  optionButton.setAttribute('aria-pressed', isActive ? 'true' : 'false');
454
454
  toolbar.append(optionButton);
455
455
  }
456
- hasToolbarItems = true;
457
456
  if (supportsBottomWidthPreset) {
458
457
  const separator = document.createElement('span');
459
458
  separator.className = 'tiptap-media-toolbar-separator';
@@ -816,7 +815,7 @@ export default Extension.create({
816
815
  const startY = event.clientY;
817
816
  const startHeight = resolveStartHeight(resizeMeta.kind, node, target);
818
817
  const targetParent = target.parentElement;
819
- const shouldShowProxy = resizeMeta.kind !== 'image' && Boolean(targetParent);
818
+ const shouldShowProxy = Boolean(targetParent);
820
819
  const currentHorizontalAlign = normalizeHorizontalAlignAttr(node.attrs.horizontalAlign);
821
820
  const startWidth = Math.max(1, resolveElementWidth(node, target) ||
822
821
  targetParent?.getBoundingClientRect().width ||
@@ -47,7 +47,12 @@
47
47
  position: absolute;
48
48
  inset: 0;
49
49
  transform: translateX(-120%);
50
- background: linear-gradient(105deg, transparent 20%, rgba(255, 255, 255, 0.36), transparent 80%);
50
+ background: linear-gradient(
51
+ 105deg,
52
+ transparent 20%,
53
+ rgba(255, 255, 255, 0.36),
54
+ transparent 80%
55
+ );
51
56
  animation: shimmer 1.35s ease-in-out infinite;
52
57
  }
53
58
 
@@ -18,7 +18,7 @@ type InsertUploadSkeletonOptions = {
18
18
  type ReplaceOptions = {
19
19
  select?: boolean;
20
20
  };
21
- type EditorLike = Pick<Editor, 'state' | 'view'>;
21
+ type EditorLike = Pick<Editor, 'view'>;
22
22
  export type UploadSkeletonHandle = {
23
23
  id: string;
24
24
  exists: () => boolean;
@@ -11,20 +11,16 @@ const defaultHeight = {
11
11
  block: 180
12
12
  };
13
13
  function findUploadSkeleton(doc, id) {
14
- let foundPos = null;
15
- let foundNode = null;
14
+ let target = null;
16
15
  doc.descendants((node, pos) => {
17
16
  if (node.type.name !== UPLOAD_SKELETON_NODE)
18
17
  return;
19
18
  if (node.attrs.uploadId !== id)
20
19
  return;
21
- foundPos = pos;
22
- foundNode = node;
20
+ target = { pos, node };
23
21
  return false;
24
22
  });
25
- if (foundPos === null || foundNode === null)
26
- return null;
27
- return { pos: foundPos, node: foundNode };
23
+ return target;
28
24
  }
29
25
  function tryCreateNodeSelection(doc, pos) {
30
26
  if (pos < 0 || pos > doc.content.size)
@@ -40,15 +36,16 @@ function tryCreateNodeSelection(doc, pos) {
40
36
  }
41
37
  }
42
38
  export function insertUploadSkeleton(editor, { kind = 'block', height = defaultHeight[kind], at, select = true, insertParagraph = true } = {}) {
43
- const skeletonType = editor.state.schema.nodes[UPLOAD_SKELETON_NODE];
39
+ const getState = () => editor.view.state;
40
+ const skeletonType = getState().schema.nodes[UPLOAD_SKELETON_NODE];
44
41
  if (!skeletonType)
45
42
  return null;
46
43
  const clampedHeight = Math.max(44, Math.min(1200, Math.round(height)));
47
44
  const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
48
45
  const node = skeletonType.create({ uploadId, kind, height: clampedHeight });
49
- const paragraph = editor.state.schema.nodes.paragraph?.create();
50
- const safePos = Math.max(0, Math.min(at ?? editor.state.selection.from, editor.state.doc.content.size));
51
- const tr = editor.state.tr.insert(safePos, node);
46
+ const paragraph = getState().schema.nodes.paragraph?.create();
47
+ const safePos = Math.max(0, Math.min(at ?? getState().selection.from, getState().doc.content.size));
48
+ const tr = getState().tr.insert(safePos, node);
52
49
  if (insertParagraph && paragraph) {
53
50
  tr.insert(safePos + node.nodeSize, paragraph);
54
51
  }
@@ -60,21 +57,22 @@ export function insertUploadSkeleton(editor, { kind = 'block', height = defaultH
60
57
  editor.view.dispatch(tr);
61
58
  return {
62
59
  id: uploadId,
63
- exists: () => Boolean(findUploadSkeleton(editor.state.doc, uploadId)),
60
+ exists: () => Boolean(findUploadSkeleton(getState().doc, uploadId)),
64
61
  replaceWith: (content, options = {}) => {
65
- const target = findUploadSkeleton(editor.state.doc, uploadId);
62
+ const state = getState();
63
+ const target = findUploadSkeleton(state.doc, uploadId);
66
64
  if (!target)
67
65
  return false;
68
66
  if (!content?.type)
69
67
  return false;
70
68
  let nextNode;
71
69
  try {
72
- nextNode = editor.state.schema.nodeFromJSON(content);
70
+ nextNode = state.schema.nodeFromJSON(content);
73
71
  }
74
72
  catch {
75
73
  return false;
76
74
  }
77
- const tr = editor.state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
75
+ const tr = state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
78
76
  if (options.select ?? true) {
79
77
  const nodeSelection = tryCreateNodeSelection(tr.doc, target.pos);
80
78
  if (nodeSelection)
@@ -84,16 +82,17 @@ export function insertUploadSkeleton(editor, { kind = 'block', height = defaultH
84
82
  return true;
85
83
  },
86
84
  remove: () => {
87
- const target = findUploadSkeleton(editor.state.doc, uploadId);
85
+ const state = getState();
86
+ const target = findUploadSkeleton(state.doc, uploadId);
88
87
  if (!target)
89
88
  return false;
90
89
  const removeFrom = target.pos;
91
90
  let removeTo = target.pos + target.node.nodeSize;
92
- const nextNode = editor.state.doc.nodeAt(removeTo);
91
+ const nextNode = state.doc.nodeAt(removeTo);
93
92
  if (nextNode?.type.name === 'paragraph' && nextNode.content.size === 0) {
94
93
  removeTo += nextNode.nodeSize;
95
94
  }
96
- editor.view.dispatch(editor.state.tr.deleteRange(removeFrom, removeTo));
95
+ editor.view.dispatch(state.tr.deleteRange(removeFrom, removeTo));
97
96
  return true;
98
97
  }
99
98
  };
@@ -114,7 +113,7 @@ export default Node.create({
114
113
  kind: {
115
114
  default: 'block',
116
115
  parseHTML: (element) => element.getAttribute('data-upload-kind') || 'block',
117
- renderHTML: (attributes) => attributes.kind ? { 'data-upload-kind': attributes.kind } : {}
116
+ renderHTML: (attributes) => (attributes.kind ? { 'data-upload-kind': attributes.kind } : {})
118
117
  },
119
118
  height: {
120
119
  default: defaultHeight.block,
@@ -43,6 +43,7 @@
43
43
  resize?: boolean | ResizeOptions;
44
44
  };
45
45
 
46
+ /* eslint-disable no-useless-assignment -- Svelte bindable props are read by parent bindings. */
46
47
  let {
47
48
  body = $bindable<string>(),
48
49
  editable = false,
@@ -73,6 +74,7 @@
73
74
  codeBlockLanguageLabels = {},
74
75
  resize = true
75
76
  }: Props = $props();
77
+ /* eslint-enable no-useless-assignment */
76
78
 
77
79
  const scopedI18n: I18nTranslate = (...args) => translateWithLocale(locale, ...args);
78
80
  const resizeDataAttrs = [
@@ -154,6 +156,9 @@
154
156
  Promise.all([import('./tiptap'), import('@justinribeiro/lite-youtube')]).then(
155
157
  ([{ default: tt }]) => {
156
158
  if (!untrack(() => mounted)) return;
159
+ const initialBody = untrack(() => san(body));
160
+ body = initialBody;
161
+ last = initialBody;
157
162
  const editorPlaceholder = placeholder ?? scopedI18n('placeholder');
158
163
  const optionPlugins = Array.isArray(options.plugins)
159
164
  ? [...options.plugins]
@@ -164,7 +169,7 @@
164
169
  const resizeOptions = typeof resize === 'object' ? resize : {};
165
170
  optionPlugins.unshift(MediaResize.configure(resizeOptions));
166
171
  }
167
- tiptap.v = ref = tt(element, r, {
172
+ tiptap.v = ref = tt(element, initialBody, {
168
173
  placeholder: editorPlaceholder,
169
174
  editable,
170
175
  onTransaction: () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seorii/tiptap",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "svelte-kit sync && svelte-package",
@@ -14,30 +14,30 @@
14
14
  "page": "npm run build-page && node gh-pages.js"
15
15
  },
16
16
  "devDependencies": {
17
- "@eslint/js": "^9.39.3",
17
+ "@eslint/js": "^10.0.1",
18
18
  "@sveltejs/adapter-auto": "^7.0.1",
19
19
  "@sveltejs/adapter-static": "^3.0.10",
20
- "@sveltejs/kit": "^2.53.0",
20
+ "@sveltejs/kit": "^2.61.0",
21
21
  "@sveltejs/package": "^2.5.7",
22
- "@sveltejs/vite-plugin-svelte": "6.2.4",
23
- "@types/sanitize-html": "^2.16.0",
24
- "@typescript-eslint/eslint-plugin": "^8.56.0",
25
- "@typescript-eslint/parser": "^8.56.0",
26
- "eslint": "^9.39.3",
22
+ "@sveltejs/vite-plugin-svelte": "7.1.2",
23
+ "@types/sanitize-html": "^2.16.1",
24
+ "@typescript-eslint/eslint-plugin": "^8.59.4",
25
+ "@typescript-eslint/parser": "^8.59.4",
26
+ "eslint": "^10.4.0",
27
27
  "eslint-config-prettier": "^10.1.8",
28
- "eslint-plugin-svelte": "^3.15.0",
28
+ "eslint-plugin-svelte": "^3.17.1",
29
29
  "gh-pages": "^6.3.0",
30
- "globals": "^17.3.0",
30
+ "globals": "^17.6.0",
31
31
  "highlight.js": "^11.11.1",
32
- "prettier": "^3.8.1",
33
- "prettier-plugin-svelte": "^3.5.0",
34
- "publint": "^0.3.17",
35
- "svelte": "^5.53.2",
36
- "svelte-check": "^4.4.3",
37
- "svelte-eslint-parser": "^1.4.1",
32
+ "prettier": "^3.8.3",
33
+ "prettier-plugin-svelte": "^4.0.1",
34
+ "publint": "^0.3.21",
35
+ "svelte": "^5.55.9",
36
+ "svelte-check": "^4.4.8",
37
+ "svelte-eslint-parser": "^1.6.1",
38
38
  "tslib": "^2.8.1",
39
- "typescript": "^5.9.3",
40
- "vite": "^7.3.1"
39
+ "typescript": "^6.0.3",
40
+ "vite": "^8.0.14"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "svelte": "^5.0.0"
@@ -81,7 +81,7 @@
81
81
  "prosemirror-transform": "^1.11.0",
82
82
  "prosemirror-view": "^1.41.6",
83
83
  "sanitize-html": "^2.17.1",
84
- "svelte-awesome-color-picker": "^4.1.1",
84
+ "svelte-awesome-color-picker": "^4.1.2",
85
85
  "svelte-tiptap": "^2",
86
86
  "tippy.js": "^6.3.7"
87
87
  },