@seorii/tiptap 0.0.7 → 0.0.9
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/dist/plugin/command/index.d.ts +4 -0
- package/dist/plugin/command/index.js +27 -0
- package/dist/plugin/command/stores.d.ts +12 -0
- package/dist/plugin/command/stores.js +5 -0
- package/dist/plugin/command/suggest.d.ts +23 -0
- package/dist/plugin/command/suggest.js +109 -0
- package/dist/plugin/table/deleteTable.js +2 -2
- package/dist/plugin/table/util.d.ts +2 -2
- package/dist/plugin/table/util.js +8 -0
- package/dist/tiptap/Bubble.svelte +69 -0
- package/dist/tiptap/Bubble.svelte.d.ts +17 -0
- package/dist/tiptap/Command.svelte +82 -0
- package/dist/tiptap/Command.svelte.d.ts +16 -0
- package/dist/tiptap/Floating.svelte +54 -0
- package/dist/tiptap/Floating.svelte.d.ts +14 -0
- package/dist/tiptap/TipTap.svelte +78 -30
- package/dist/tiptap/TipTap.svelte.d.ts +3 -1
- package/dist/tiptap/ToolbarButton.svelte +26 -0
- package/dist/tiptap/ToolbarButton.svelte.d.ts +25 -0
- package/dist/tiptap/setMath.d.ts +1 -0
- package/dist/tiptap/setMath.js +14 -0
- package/dist/tiptap/tiptap.d.ts +1 -1
- package/dist/tiptap/tiptap.js +6 -5
- package/package.json +17 -7
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import Suggestion from '@tiptap/suggestion';
|
|
3
|
+
import suggestion from "./suggest";
|
|
4
|
+
export const Command = Extension.create({
|
|
5
|
+
name: 'slash',
|
|
6
|
+
addOptions() {
|
|
7
|
+
return {
|
|
8
|
+
suggestion: {
|
|
9
|
+
char: '/',
|
|
10
|
+
command: ({ editor, range, props }) => {
|
|
11
|
+
props.command({ editor, range });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
addProseMirrorPlugins() {
|
|
17
|
+
return [
|
|
18
|
+
Suggestion({
|
|
19
|
+
editor: this.editor,
|
|
20
|
+
...this.options.suggestion
|
|
21
|
+
})
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
export default Command.configure({
|
|
26
|
+
suggestion
|
|
27
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
export declare const slashVisible: import("svelte/store").Writable<boolean>;
|
|
3
|
+
export declare const slashItems: import("svelte/store").Writable<never[]>;
|
|
4
|
+
export declare const slashLocaltion: import("svelte/store").Writable<{
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const slashProps: import("svelte/store").Writable<{
|
|
10
|
+
editor: null;
|
|
11
|
+
range: null;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
items: ({ query }: {
|
|
3
|
+
query: any;
|
|
4
|
+
}) => {
|
|
5
|
+
section: string;
|
|
6
|
+
list: {
|
|
7
|
+
icon: string;
|
|
8
|
+
title: string;
|
|
9
|
+
subtitle: string;
|
|
10
|
+
command: ({ editor, range }: {
|
|
11
|
+
editor: any;
|
|
12
|
+
range: any;
|
|
13
|
+
}) => void;
|
|
14
|
+
}[];
|
|
15
|
+
}[];
|
|
16
|
+
render: () => {
|
|
17
|
+
onStart: (props: any) => void;
|
|
18
|
+
onUpdate(props: any): void;
|
|
19
|
+
onKeyDown(props: any): true | undefined;
|
|
20
|
+
onExit(): void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export default _default;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps } from './stores';
|
|
2
|
+
export default {
|
|
3
|
+
items: ({ query }) => {
|
|
4
|
+
const raw = [
|
|
5
|
+
{
|
|
6
|
+
section: '텍스트', list: [
|
|
7
|
+
{
|
|
8
|
+
icon: 'title',
|
|
9
|
+
title: '제목 1',
|
|
10
|
+
subtitle: '큰 제목',
|
|
11
|
+
command: ({ editor, range }) => {
|
|
12
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
icon: 'title',
|
|
17
|
+
title: '제목 2',
|
|
18
|
+
subtitle: '좀 더 작은 제목',
|
|
19
|
+
command: ({ editor, range }) => {
|
|
20
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
icon: 'title',
|
|
25
|
+
title: '제목 3',
|
|
26
|
+
subtitle: '적당히 큰 제목',
|
|
27
|
+
command: ({ editor, range }) => {
|
|
28
|
+
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
icon: 'format_list_bulleted',
|
|
33
|
+
title: '리스트',
|
|
34
|
+
subtitle: '순서 없는 리스트',
|
|
35
|
+
command: ({ editor, range }) => {
|
|
36
|
+
editor.commands.deleteRange(range);
|
|
37
|
+
editor.commands.toggleBulletList();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
icon: 'format_list_numbered',
|
|
42
|
+
title: '숫자 리스트',
|
|
43
|
+
subtitle: '1, 2, 3, 4',
|
|
44
|
+
command: ({ editor, range }) => {
|
|
45
|
+
editor.commands.deleteRange(range);
|
|
46
|
+
editor.commands.toggleOrderedList();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
section: '블록', list: [
|
|
53
|
+
{
|
|
54
|
+
icon: 'code',
|
|
55
|
+
title: '코드 블록',
|
|
56
|
+
subtitle: '하이라이팅되는 코드 블록',
|
|
57
|
+
command: ({ editor, range }) => {
|
|
58
|
+
editor.chain().focus().deleteRange(range).setNode('codeBlock').run();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
icon: 'functions',
|
|
63
|
+
title: '수식 블록',
|
|
64
|
+
subtitle: '가운데로 정렬된 큰 수식',
|
|
65
|
+
command: ({ editor, range }) => {
|
|
66
|
+
const { to } = range;
|
|
67
|
+
editor.chain().focus().deleteRange(range).setNode('math_display').focus().run();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
icon: 'table_chart',
|
|
72
|
+
title: '테이블',
|
|
73
|
+
subtitle: '표',
|
|
74
|
+
command: ({ editor, range }) => {
|
|
75
|
+
editor.chain().focus().insertTable({ rows: 2, cols: 3 }).run();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
const filtered = raw.map(({ section, list }) => ({ section, list: list.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())) })).filter(({ list }) => list.length > 0);
|
|
82
|
+
return filtered;
|
|
83
|
+
},
|
|
84
|
+
render: () => {
|
|
85
|
+
return {
|
|
86
|
+
onStart: (props) => {
|
|
87
|
+
let editor = props.editor;
|
|
88
|
+
let range = props.range;
|
|
89
|
+
let location = props.clientRect();
|
|
90
|
+
slashProps.set({ editor, range });
|
|
91
|
+
slashVisible.set(true);
|
|
92
|
+
slashLocaltion.set({ x: location.x, y: location.y, height: location.height });
|
|
93
|
+
slashItems.set(props.items);
|
|
94
|
+
},
|
|
95
|
+
onUpdate(props) {
|
|
96
|
+
slashItems.set(props.items);
|
|
97
|
+
},
|
|
98
|
+
onKeyDown(props) {
|
|
99
|
+
if (props.event.key === 'Escape') {
|
|
100
|
+
slashVisible.set(false);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onExit() {
|
|
105
|
+
slashVisible.set(false);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isColumnSelected, isTableSelected } from "./util";
|
|
1
|
+
import { isColumnSelected, isRowSelected, isTableSelected } from "./util";
|
|
2
2
|
import { TableMap } from "prosemirror-tables";
|
|
3
3
|
export const deleteTable = ({ editor }) => {
|
|
4
4
|
const { selection } = editor.state;
|
|
@@ -14,7 +14,7 @@ export const deleteTable = ({ editor }) => {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
for (let i = height - 1; i >= 0; i--) {
|
|
17
|
-
if (
|
|
17
|
+
if (isRowSelected(i)(selection)) {
|
|
18
18
|
editor.commands.deleteRow();
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Node, ResolvedPos } from 'prosemirror-model';
|
|
2
|
-
import type { Selection, Transaction } from 'prosemirror-state';
|
|
2
|
+
import type { EditorState, Selection, Transaction } from 'prosemirror-state';
|
|
3
3
|
import { CellSelection, type TableRect } from 'prosemirror-tables';
|
|
4
|
-
import type { EditorState } from "prosemirror-state";
|
|
5
4
|
export declare const isRectSelected: (rect: any) => (selection: CellSelection) => boolean;
|
|
6
5
|
export declare const findTable: (selection: Selection) => {
|
|
7
6
|
pos: number;
|
|
@@ -10,6 +9,7 @@ export declare const findTable: (selection: Selection) => {
|
|
|
10
9
|
node: Node;
|
|
11
10
|
} | undefined;
|
|
12
11
|
export declare const isCellSelection: (selection: any) => boolean;
|
|
12
|
+
export declare const isTableAnySelected: (selection: any) => false | number[];
|
|
13
13
|
export declare const isColumnSelected: (columnIndex: number) => (selection: any) => boolean;
|
|
14
14
|
export declare const isRowSelected: (rowIndex: number) => (selection: any) => boolean;
|
|
15
15
|
export declare const isTableSelected: (selection: any) => boolean;
|
|
@@ -16,6 +16,14 @@ export const findTable = (selection) => findParentNode((node) => node.type.spec.
|
|
|
16
16
|
export const isCellSelection = (selection) => {
|
|
17
17
|
return selection instanceof CellSelection;
|
|
18
18
|
};
|
|
19
|
+
export const isTableAnySelected = (selection) => {
|
|
20
|
+
if (isCellSelection(selection)) {
|
|
21
|
+
const map = TableMap.get(selection.$anchorCell.node(-1));
|
|
22
|
+
const start = selection.$anchorCell.start(-1);
|
|
23
|
+
return map.cellsInRect(map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start));
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
19
27
|
export const isColumnSelected = (columnIndex) => (selection) => {
|
|
20
28
|
if (isCellSelection(selection)) {
|
|
21
29
|
const map = TableMap.get(selection.$anchorCell.node(-1));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script>import { BubbleMenu } from "svelte-tiptap";
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import 'tippy.js/animations/shift-away-subtle.css';
|
|
4
|
+
import ToolbarButton from "./ToolbarButton.svelte";
|
|
5
|
+
import { isTableAnySelected } from "../plugin/table/util";
|
|
6
|
+
import deleteTable from "../plugin/table/deleteTable";
|
|
7
|
+
//@ts-ignore
|
|
8
|
+
import { MathInline } from "@seorii/prosemirror-math/tiptap";
|
|
9
|
+
import setMath from "./setMath";
|
|
10
|
+
const tiptap = getContext('editor');
|
|
11
|
+
$: selection = $tiptap?.state?.selection;
|
|
12
|
+
$: table = isTableAnySelected(selection);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
{#if $tiptap}
|
|
17
|
+
<BubbleMenu editor={$tiptap} updateDelay={50}
|
|
18
|
+
tippyOptions={{moveTransition: 'transform 0.2s cubic-bezier(1,.5,0,.85)', animation:'shift-away-subtle', duration: [200, 50]}}>
|
|
19
|
+
{#if $$slots.default}
|
|
20
|
+
<slot/>
|
|
21
|
+
{:else}
|
|
22
|
+
<main>
|
|
23
|
+
{#if table}
|
|
24
|
+
{#if table.length > 1}
|
|
25
|
+
<ToolbarButton icon="cell_merge" handler={() => $tiptap.commands.mergeCells()}/>
|
|
26
|
+
{:else}
|
|
27
|
+
<ToolbarButton icon="splitscreen_left" handler={() => $tiptap.commands.splitCell()}/>
|
|
28
|
+
{/if}
|
|
29
|
+
<ToolbarButton icon="keyboard_double_arrow_left"
|
|
30
|
+
handler={() => $tiptap.chain().focus().addColumnBefore().run()}/>
|
|
31
|
+
<ToolbarButton icon="keyboard_double_arrow_right"
|
|
32
|
+
handler={() => $tiptap.chain().focus().addColumnAfter().run()}/>
|
|
33
|
+
<ToolbarButton icon="keyboard_double_arrow_up"
|
|
34
|
+
handler={() => $tiptap.chain().focus().addRowBefore().run()}/>
|
|
35
|
+
<ToolbarButton icon="keyboard_double_arrow_down"
|
|
36
|
+
handler={() => $tiptap.chain().focus().addRowAfter().run()}/>
|
|
37
|
+
<ToolbarButton icon="close" handler={() => deleteTable({editor: $tiptap})}/>
|
|
38
|
+
{:else}
|
|
39
|
+
<ToolbarButton icon="format_bold" prop="bold"/>
|
|
40
|
+
<ToolbarButton icon="format_italic" prop="italic"/>
|
|
41
|
+
<ToolbarButton icon="format_strikethrough" prop="strike"/>
|
|
42
|
+
<ToolbarButton icon="format_underlined" prop="underline"/>
|
|
43
|
+
<ToolbarButton icon="functions" handler={() => setMath($tiptap)}/>
|
|
44
|
+
<ToolbarButton icon="code" prop="code"/>
|
|
45
|
+
{/if}
|
|
46
|
+
</main>
|
|
47
|
+
{/if}
|
|
48
|
+
</BubbleMenu>
|
|
49
|
+
{/if}
|
|
50
|
+
|
|
51
|
+
<style>main {
|
|
52
|
+
box-shadow: var(--shadow);
|
|
53
|
+
background: var(--surface);
|
|
54
|
+
padding: 8px;
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
font-size: 1.2em;
|
|
60
|
+
}
|
|
61
|
+
main > :global(*) {
|
|
62
|
+
margin: 0 2px;
|
|
63
|
+
}
|
|
64
|
+
main > :global(*):first-child {
|
|
65
|
+
margin-left: 0;
|
|
66
|
+
}
|
|
67
|
+
main > :global(*):last-child {
|
|
68
|
+
margin-right: 0;
|
|
69
|
+
}</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
import 'tippy.js/animations/shift-away-subtle.css';
|
|
3
|
+
declare const __propDef: {
|
|
4
|
+
props: Record<string, never>;
|
|
5
|
+
events: {
|
|
6
|
+
[evt: string]: CustomEvent<any>;
|
|
7
|
+
};
|
|
8
|
+
slots: {
|
|
9
|
+
default: {};
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type BubbleProps = typeof __propDef.props;
|
|
13
|
+
export type BubbleEvents = typeof __propDef.events;
|
|
14
|
+
export type BubbleSlots = typeof __propDef.slots;
|
|
15
|
+
export default class Bubble extends SvelteComponentTyped<BubbleProps, BubbleEvents, BubbleSlots> {
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script>import { List, TwoLine } from "nunui";
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import { slashVisible, slashItems, slashLocaltion, slashProps } from '../plugin/command/stores';
|
|
4
|
+
import { fly, slide } from "svelte/transition";
|
|
5
|
+
import { quartOut } from "svelte/easing";
|
|
6
|
+
const tiptap = getContext('editor');
|
|
7
|
+
export let selectedIndex = 0;
|
|
8
|
+
let height = 0, elements = [];
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<svelte:window bind:innerHeight={height}/>
|
|
12
|
+
|
|
13
|
+
{#if $slashVisible}
|
|
14
|
+
<div class="scrim" on:click={() => $slashVisible = false}/>
|
|
15
|
+
<main style="left: {$slashLocaltion.x}px; top: {$slashLocaltion.y + $slashLocaltion.height + 384 > height
|
|
16
|
+
? $slashLocaltion.y - $slashLocaltion.height - 384
|
|
17
|
+
: $slashLocaltion.y + $slashLocaltion.height}px;" transition:fly={{y: 10, duration: 200, easing: quartOut}}>
|
|
18
|
+
<div class="list">
|
|
19
|
+
<List>
|
|
20
|
+
{#each $slashItems as {section, list}(section)}
|
|
21
|
+
<div class="section" transition:slide={{duration: 400, easing: quartOut}}>{section}</div>
|
|
22
|
+
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
23
|
+
{#each list || [] as {title, subtitle, icon, command, section}, i(title)}
|
|
24
|
+
<div transition:slide={{duration: 400, easing: quartOut}}>
|
|
25
|
+
<TwoLine on:mouseenter={() => (selectedIndex = i)} on:click={() => {
|
|
26
|
+
$slashVisible = false;
|
|
27
|
+
command?.($slashProps);
|
|
28
|
+
setTimeout(() => $tiptap.commands.focus());
|
|
29
|
+
}} bind:this={elements[i]} {icon} {title} subtitle={subtitle || ''}/>
|
|
30
|
+
</div>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
{/each}
|
|
34
|
+
{#if !$slashItems.length}
|
|
35
|
+
<div class="section" transition:slide={{duration: 400, easing: quartOut}}>결과 없음</div>
|
|
36
|
+
{/if}
|
|
37
|
+
</List>
|
|
38
|
+
</div>
|
|
39
|
+
</main>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<style>.scrim {
|
|
43
|
+
position: fixed;
|
|
44
|
+
top: 0;
|
|
45
|
+
left: 0;
|
|
46
|
+
width: 100%;
|
|
47
|
+
height: 100%;
|
|
48
|
+
cursor: default;
|
|
49
|
+
z-index: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main {
|
|
53
|
+
position: absolute;
|
|
54
|
+
background: var(--surface, #fff);
|
|
55
|
+
width: 220px;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
overflow-y: scroll;
|
|
58
|
+
z-index: 10;
|
|
59
|
+
box-shadow: var(--shadow);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.section {
|
|
63
|
+
padding: 8px 16px;
|
|
64
|
+
font-size: 0.8em;
|
|
65
|
+
font-weight: 300;
|
|
66
|
+
color: var(--on-surface, #000);
|
|
67
|
+
opacity: 0.8;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.list {
|
|
71
|
+
color: var(--primary-dark7);
|
|
72
|
+
}
|
|
73
|
+
.list :global(.title) {
|
|
74
|
+
font-size: 0.7em !important;
|
|
75
|
+
font-weight: 300 !important;
|
|
76
|
+
margin-bottom: 4px;
|
|
77
|
+
}
|
|
78
|
+
.list :global(.subtitle) {
|
|
79
|
+
font-size: 0.8em !important;
|
|
80
|
+
font-weight: 300 !important;
|
|
81
|
+
color: var(--primary-dark1);
|
|
82
|
+
}</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
selectedIndex?: number | undefined;
|
|
5
|
+
};
|
|
6
|
+
events: {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
};
|
|
9
|
+
slots: {};
|
|
10
|
+
};
|
|
11
|
+
export type CommandProps = typeof __propDef.props;
|
|
12
|
+
export type CommandEvents = typeof __propDef.events;
|
|
13
|
+
export type CommandSlots = typeof __propDef.slots;
|
|
14
|
+
export default class Command extends SvelteComponentTyped<CommandProps, CommandEvents, CommandSlots> {
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script>import { BubbleMenu, FloatingMenu } from "svelte-tiptap";
|
|
2
|
+
import { getContext } from "svelte";
|
|
3
|
+
import { Button, IconButton, List, OneLine, Tooltip } from "nunui";
|
|
4
|
+
import ToolbarButton from "./ToolbarButton.svelte";
|
|
5
|
+
import setMath from "./setMath";
|
|
6
|
+
const tiptap = getContext('editor');
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
{#if $tiptap}
|
|
10
|
+
<FloatingMenu editor={$tiptap}
|
|
11
|
+
tippyOptions={{animation:'fade', duration: [200, 50]}}>
|
|
12
|
+
<main on:mousedown={() => setTimeout(() => $tiptap.commands.focus())}>
|
|
13
|
+
<span>
|
|
14
|
+
/로 명령어 입력. 또는
|
|
15
|
+
</span>
|
|
16
|
+
<Tooltip bottom left xstack width="160px">
|
|
17
|
+
<IconButton size="1.2em" icon="text_fields" slot="target"/>
|
|
18
|
+
<div style="margin: -6px">
|
|
19
|
+
<List>
|
|
20
|
+
<OneLine title="제목 1" on:click={() => $tiptap.commands.setHeading({level: 1})}/>
|
|
21
|
+
<OneLine title="제목 2" on:click={() => $tiptap.commands.setHeading({level: 2})}/>
|
|
22
|
+
<OneLine title="제목 3" on:click={() => $tiptap.commands.setHeading({level: 3})}/>
|
|
23
|
+
<OneLine title="본문" on:click={() => $tiptap.commands.setParagraph()}/>
|
|
24
|
+
</List>
|
|
25
|
+
</div>
|
|
26
|
+
</Tooltip>
|
|
27
|
+
<ToolbarButton icon="format_bold" prop="bold"/>
|
|
28
|
+
<ToolbarButton icon="format_italic" prop="italic"/>
|
|
29
|
+
<ToolbarButton icon="format_strikethrough" prop="strike"/>
|
|
30
|
+
<ToolbarButton icon="format_underlined" prop="underline"/>
|
|
31
|
+
<ToolbarButton icon="functions" handler={() => {
|
|
32
|
+
const end = $tiptap.state.selection.$to.pos;
|
|
33
|
+
$tiptap.chain().focus().insertContent({
|
|
34
|
+
type: 'math_inline',
|
|
35
|
+
}).insertContent(' ').run();
|
|
36
|
+
}}/>
|
|
37
|
+
<ToolbarButton icon="code" prop="code"/>
|
|
38
|
+
</main>
|
|
39
|
+
</FloatingMenu>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
<style>span {
|
|
43
|
+
opacity: 0.6;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
main {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
width: max-content;
|
|
50
|
+
align-items: center;
|
|
51
|
+
}
|
|
52
|
+
main > :global(*) {
|
|
53
|
+
margin-right: 4px;
|
|
54
|
+
}</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type FloatingProps = typeof __propDef.props;
|
|
10
|
+
export type FloatingEvents = typeof __propDef.events;
|
|
11
|
+
export type FloatingSlots = typeof __propDef.slots;
|
|
12
|
+
export default class Floating extends SvelteComponentTyped<FloatingProps, FloatingEvents, FloatingSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
<script>import { browser } from "$app/environment";
|
|
2
|
-
import { onMount, setContext } from 'svelte';
|
|
3
|
-
import { Card, Input } from "nunui";
|
|
2
|
+
import { beforeUpdate, onMount, setContext } from 'svelte';
|
|
4
3
|
import { writable } from "svelte/store";
|
|
5
4
|
import sanitizeHtml from 'sanitize-html';
|
|
6
5
|
import "@seorii/prosemirror-math/style.css";
|
|
7
|
-
|
|
6
|
+
import Bubble from "./Bubble.svelte";
|
|
7
|
+
import Floating from "./Floating.svelte";
|
|
8
|
+
import Command from "./Command.svelte";
|
|
9
|
+
import { slashItems, slashProps, slashVisible } from "../plugin/command/stores";
|
|
10
|
+
const san = (body) => sanitizeHtml(body, {
|
|
8
11
|
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'math-inline', 'math-node', 'iframe', 'tiptap-file']),
|
|
9
12
|
allowedStyles: '*', allowedAttributes: {
|
|
10
13
|
'*': ['style', 'class'],
|
|
11
14
|
a: ['href', 'name', 'target'],
|
|
12
15
|
img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],
|
|
13
16
|
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
|
17
|
+
th: ['colwidth', 'colspan', 'rowspan'],
|
|
18
|
+
td: ['colwidth', 'colspan', 'rowspan'],
|
|
14
19
|
'tiptap-file': ['id']
|
|
15
20
|
},
|
|
16
21
|
});
|
|
17
22
|
export let body = '', editor = false, style = '', ref = null, options = {};
|
|
18
23
|
const tiptap = setContext('editor', writable(null));
|
|
19
|
-
let element,
|
|
24
|
+
let element, fullscreen = false, mounted = false, last = '';
|
|
20
25
|
$: ref = $tiptap;
|
|
21
|
-
$: _san = san(body);
|
|
22
|
-
$: if (_san !== _body && $tiptap)
|
|
23
|
-
$tiptap?.commands.setContent(_body = _san);
|
|
24
26
|
$: $tiptap && $tiptap.setEditable(editor);
|
|
25
|
-
if (browser)
|
|
27
|
+
if (browser) {
|
|
26
28
|
onMount(() => {
|
|
29
|
+
body = last = san(body);
|
|
27
30
|
mounted = true;
|
|
28
31
|
import('./tiptap').then(({ default: tt }) => {
|
|
29
32
|
if (!mounted)
|
|
30
33
|
return;
|
|
31
|
-
$tiptap = tt(element,
|
|
34
|
+
$tiptap = tt(element, body, {
|
|
32
35
|
editable: editor,
|
|
33
36
|
onTransaction: () => $tiptap = $tiptap,
|
|
34
37
|
...options,
|
|
@@ -37,7 +40,7 @@ if (browser)
|
|
|
37
40
|
let content = tiptap.getHTML(), json = tiptap.getJSON().content;
|
|
38
41
|
if (Array.isArray(json) && json.length === 1 && !json[0].hasOwnProperty("content"))
|
|
39
42
|
content = null;
|
|
40
|
-
|
|
43
|
+
body = last = content;
|
|
41
44
|
});
|
|
42
45
|
});
|
|
43
46
|
return () => {
|
|
@@ -45,43 +48,88 @@ if (browser)
|
|
|
45
48
|
$tiptap?.destroy?.();
|
|
46
49
|
};
|
|
47
50
|
});
|
|
51
|
+
beforeUpdate(() => {
|
|
52
|
+
if (last === body)
|
|
53
|
+
return;
|
|
54
|
+
body = san(body);
|
|
55
|
+
$tiptap?.commands?.setContent?.(body);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
let selectedIndex = 0;
|
|
59
|
+
$: selectedIndex = $slashVisible ? selectedIndex : 0;
|
|
60
|
+
function handleKeydown(event) {
|
|
61
|
+
if (!$slashVisible)
|
|
62
|
+
return;
|
|
63
|
+
if (event.key === 'ArrowUp') {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
selectedIndex = (selectedIndex + $slashItems.length - 1) % $slashItems.length;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (event.key === 'ArrowDown') {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
selectedIndex = (selectedIndex + 1) % $slashItems.length;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (event.key === 'Enter') {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
selectItem(selectedIndex);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
function selectItem(index) {
|
|
81
|
+
const item = $slashItems[index];
|
|
82
|
+
if (item) {
|
|
83
|
+
let range = $slashProps.range;
|
|
84
|
+
item.command({ editor, range });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
48
87
|
</script>
|
|
49
88
|
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
</
|
|
89
|
+
<main class:fullscreen class:editor>
|
|
90
|
+
<div class="wrapper">
|
|
91
|
+
<div bind:this={element} class="target" on:keydown|capture={handleKeydown}></div>
|
|
92
|
+
{#if !$tiptap}
|
|
93
|
+
로드 중...
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
{#if editor}
|
|
97
|
+
<Command {selectedIndex}/>
|
|
98
|
+
<Floating/>
|
|
99
|
+
{#if $$slots.bubble}
|
|
100
|
+
<Bubble>
|
|
101
|
+
<slot name="bubble"/>
|
|
102
|
+
</Bubble>
|
|
103
|
+
{:else}
|
|
104
|
+
<Bubble/>
|
|
105
|
+
{/if}
|
|
106
|
+
{/if}
|
|
107
|
+
</main>
|
|
108
|
+
|
|
64
109
|
|
|
65
110
|
<style>main {
|
|
66
111
|
position: relative;
|
|
67
112
|
overscroll-behavior: none;
|
|
113
|
+
--shadow: 0 1px 2px rgba(127, 127, 127, 0.07),
|
|
114
|
+
0 2px 4px rgba(127, 127, 127, 0.07),
|
|
115
|
+
0 4px 8px rgba(127, 127, 127, 0.07),
|
|
116
|
+
0 8px 16px rgba(127, 127, 127, 0.07),
|
|
117
|
+
0 16px 32px rgba(127, 127, 127, 0.07),
|
|
118
|
+
0 32px 64px rgba(127, 127, 127, 0.07);
|
|
68
119
|
}
|
|
69
120
|
main.fullscreen {
|
|
70
|
-
z-index:
|
|
121
|
+
z-index: 999999999;
|
|
71
122
|
position: fixed;
|
|
72
123
|
top: 0;
|
|
73
124
|
left: 0;
|
|
125
|
+
right: 0;
|
|
126
|
+
bottom: 0;
|
|
74
127
|
background: var(--surface);
|
|
75
128
|
padding: 82px 12px 12px 12px;
|
|
76
|
-
width: calc(100% - 24px);
|
|
77
|
-
height: calc(100% - 94px);
|
|
78
129
|
}
|
|
79
130
|
main .wrapper {
|
|
80
131
|
position: relative;
|
|
81
132
|
}
|
|
82
|
-
main .wrapper .hide {
|
|
83
|
-
display: none;
|
|
84
|
-
}
|
|
85
133
|
|
|
86
134
|
.target > :global(div) > :global(*:first-child) {
|
|
87
135
|
margin-top: 0 !important;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script>import { getContext } from "svelte";
|
|
2
|
+
import { Button, IconButton } from "nunui";
|
|
3
|
+
const editor = getContext("editor");
|
|
4
|
+
export let prop = '', attrs = '', label = '', icon = '', methodName = 'toggle' + prop.charAt(0).toUpperCase() + prop.slice(1), tooltip, handler;
|
|
5
|
+
$: isActive = () => {
|
|
6
|
+
return editor && prop && $editor.isActive(prop, attrs);
|
|
7
|
+
};
|
|
8
|
+
function toggle() {
|
|
9
|
+
if (!$editor)
|
|
10
|
+
return;
|
|
11
|
+
//$editor.chain().focus().clearNodes().run()
|
|
12
|
+
if (handler)
|
|
13
|
+
return handler();
|
|
14
|
+
setTimeout(() => $editor.chain().focus()[methodName](attrs)?.run(), 0);
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#if icon}
|
|
19
|
+
<IconButton size="1.2em" {icon} active={isActive()} on:click={toggle} tooltip={tooltip} tabindex="0"/>
|
|
20
|
+
{:else}
|
|
21
|
+
<Button outlined={!isActive()} on:click={handler || toggle} small {...$$restProps}>
|
|
22
|
+
{label}
|
|
23
|
+
<slot/>
|
|
24
|
+
</Button>
|
|
25
|
+
{/if}
|
|
26
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: {
|
|
4
|
+
[x: string]: any;
|
|
5
|
+
prop?: string | undefined;
|
|
6
|
+
attrs?: string | undefined;
|
|
7
|
+
label?: string | undefined;
|
|
8
|
+
icon?: string | undefined;
|
|
9
|
+
methodName?: string | undefined;
|
|
10
|
+
tooltip: any;
|
|
11
|
+
handler: any;
|
|
12
|
+
};
|
|
13
|
+
events: {
|
|
14
|
+
[evt: string]: CustomEvent<any>;
|
|
15
|
+
};
|
|
16
|
+
slots: {
|
|
17
|
+
default: {};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type ToolbarButtonProps = typeof __propDef.props;
|
|
21
|
+
export type ToolbarButtonEvents = typeof __propDef.events;
|
|
22
|
+
export type ToolbarButtonSlots = typeof __propDef.slots;
|
|
23
|
+
export default class ToolbarButton extends SvelteComponentTyped<ToolbarButtonProps, ToolbarButtonEvents, ToolbarButtonSlots> {
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function setMath(tiptap: any): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default function setMath(tiptap) {
|
|
2
|
+
const { selection } = tiptap.state;
|
|
3
|
+
tiptap.chain().command(({ state, tr }) => state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
|
|
4
|
+
if (!node.isTextblock || selection.from === selection.to)
|
|
5
|
+
return;
|
|
6
|
+
const startPosition = Math.max(position + 1, selection.from);
|
|
7
|
+
const endPosition = Math.min(position + node.nodeSize, selection.to);
|
|
8
|
+
const substringFrom = Math.max(0, selection.from - position - 1);
|
|
9
|
+
const substringTo = Math.max(0, selection.to - position - 1);
|
|
10
|
+
const updatedText = node.textContent.substring(substringFrom, substringTo);
|
|
11
|
+
const newNode = state.schema.nodes.math_inline.create(null, state.schema.text(updatedText));
|
|
12
|
+
tr = tr.replaceWith(startPosition, endPosition, newNode);
|
|
13
|
+
})).run();
|
|
14
|
+
}
|
package/dist/tiptap/tiptap.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Editor } from "@tiptap/core";
|
|
2
|
-
declare const _default: (element: Element, content: string, {
|
|
2
|
+
declare const _default: (element: Element, content: string, { plugins, ...props }?: any) => Editor;
|
|
3
3
|
export default _default;
|
package/dist/tiptap/tiptap.js
CHANGED
|
@@ -16,14 +16,14 @@ import tableRow from "../plugin/table/tableRow";
|
|
|
16
16
|
import tableCell from "../plugin/table/tableCell";
|
|
17
17
|
import Superscript from '@tiptap/extension-superscript';
|
|
18
18
|
import Subscript from "@tiptap/extension-subscript";
|
|
19
|
-
import Placeholder from "@tiptap/extension-placeholder";
|
|
20
19
|
import { Indent } from "../plugin/indent";
|
|
21
20
|
import { Color } from '@tiptap/extension-color';
|
|
22
21
|
import TextStyle from '@tiptap/extension-text-style';
|
|
23
22
|
import Iframe from "../plugin/iframe";
|
|
24
23
|
// @ts-ignore
|
|
25
|
-
import {
|
|
26
|
-
|
|
24
|
+
import { MathInline, MathBlock } from "@seorii/prosemirror-math/tiptap";
|
|
25
|
+
import command from "../plugin/command";
|
|
26
|
+
export default (element, content, { plugins = [], ...props } = {}) => new Editor({
|
|
27
27
|
element, content, ...props,
|
|
28
28
|
extensions: [
|
|
29
29
|
CodeBlockLowlight.configure({ lowlight }),
|
|
@@ -35,23 +35,24 @@ export default (element, content, { placeholder = '내용을 입력하세요...'
|
|
|
35
35
|
TextAlign.configure({ types: ['heading', 'paragraph', 'image'] }),
|
|
36
36
|
DropCursor,
|
|
37
37
|
orderedlist,
|
|
38
|
+
MathInline,
|
|
39
|
+
MathBlock,
|
|
38
40
|
table,
|
|
39
41
|
tableHeader,
|
|
40
42
|
tableRow,
|
|
41
43
|
tableCell,
|
|
42
44
|
Superscript,
|
|
43
45
|
Subscript,
|
|
44
|
-
Placeholder.configure({ placeholder }),
|
|
45
46
|
Indent,
|
|
46
47
|
Color,
|
|
47
48
|
TextStyle,
|
|
48
|
-
Katex,
|
|
49
49
|
Iframe,
|
|
50
50
|
Code.extend({
|
|
51
51
|
renderHTML({ HTMLAttributes }) {
|
|
52
52
|
return ['code', mergeAttributes(HTMLAttributes, { class: 'inline' })];
|
|
53
53
|
}
|
|
54
54
|
}),
|
|
55
|
+
command,
|
|
55
56
|
...plugins,
|
|
56
57
|
],
|
|
57
58
|
});
|
package/package.json
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seorii/tiptap",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "svelte-kit sync && svelte-package",
|
|
7
|
+
"build-page": "vite build",
|
|
7
8
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
8
9
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
9
10
|
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
|
10
11
|
"format": "prettier --plugin-search-dir . --write .",
|
|
11
|
-
"deploy": "svelte-kit sync && svelte-package && npm publish --access public"
|
|
12
|
+
"deploy": "svelte-kit sync && svelte-package && npm publish --access public",
|
|
13
|
+
"page": "npm run build-page && node gh-pages.js"
|
|
12
14
|
},
|
|
13
15
|
"devDependencies": {
|
|
14
16
|
"@sveltejs/adapter-auto": "^2.1.0",
|
|
15
|
-
"@sveltejs/
|
|
17
|
+
"@sveltejs/adapter-static": "^2.0.3",
|
|
18
|
+
"@sveltejs/kit": "^1.22.4",
|
|
16
19
|
"@sveltejs/package": "^2.2.0",
|
|
20
|
+
"@types/sanitize-html": "^2.9.0",
|
|
17
21
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
|
18
22
|
"@typescript-eslint/parser": "^6.2.0",
|
|
19
23
|
"eslint": "^8.46.0",
|
|
20
24
|
"eslint-config-prettier": "^8.9.0",
|
|
21
25
|
"eslint-plugin-svelte3": "^4.0.0",
|
|
26
|
+
"gh-pages": "^5.0.0",
|
|
22
27
|
"prettier": "^3.0.0",
|
|
23
28
|
"prettier-plugin-svelte": "^3.0.3",
|
|
24
29
|
"sass": "^1.64.1",
|
|
25
|
-
"svelte": "^4.1.
|
|
30
|
+
"svelte": "^4.1.2",
|
|
26
31
|
"svelte-check": "^3.4.6",
|
|
27
32
|
"svelte-preprocess": "^5.0.4",
|
|
28
33
|
"tslib": "^2.6.1",
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
},
|
|
32
37
|
"type": "module",
|
|
33
38
|
"dependencies": {
|
|
34
|
-
"@seorii/prosemirror-math": "^0.
|
|
39
|
+
"@seorii/prosemirror-math": "^0.4.2",
|
|
35
40
|
"@tiptap/core": "^2.0.4",
|
|
36
41
|
"@tiptap/extension-code": "^2.0.4",
|
|
37
42
|
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
|
@@ -51,15 +56,20 @@
|
|
|
51
56
|
"@tiptap/extension-text-align": "^2.0.4",
|
|
52
57
|
"@tiptap/extension-text-style": "^2.0.4",
|
|
53
58
|
"@tiptap/extension-underline": "^2.0.4",
|
|
59
|
+
"@tiptap/pm": "^2.0.4",
|
|
54
60
|
"@tiptap/starter-kit": "^2.0.4",
|
|
61
|
+
"@tiptap/suggestion": "^2.0.4",
|
|
55
62
|
"lowlight": "^2.9.0",
|
|
56
|
-
"nunui": "^0.0.
|
|
63
|
+
"nunui": "^0.0.99",
|
|
64
|
+
"prosemirror-commands": "^1.5.2",
|
|
57
65
|
"prosemirror-model": "^1.19.3",
|
|
58
66
|
"prosemirror-state": "^1.4.3",
|
|
59
67
|
"prosemirror-tables": "^1.3.4",
|
|
60
68
|
"prosemirror-transform": "^1.7.4",
|
|
61
69
|
"prosemirror-view": "^1.31.7",
|
|
62
|
-
"sanitize-html": "^2.11.0"
|
|
70
|
+
"sanitize-html": "^2.11.0",
|
|
71
|
+
"svelte-tiptap": "^1.1.2",
|
|
72
|
+
"tippy.js": "^6.3.7"
|
|
63
73
|
},
|
|
64
74
|
"exports": {
|
|
65
75
|
".": {
|