feat: Migrate ESLint to flat config and resolve initial linting errors
Migrated the ESLint configuration to the new flat config format () and addressed several initial linting errors. Key changes include: - Updated ESLint configuration to treat as warnings instead of errors. - Fixed errors in by declaring and . - Corrected error in by using instead of an out-of-scope . - Resolved error in by replacing the undefined directive with the component. - Addressed errors in by replacing with and with . - Fixed errors in by importing necessary modules (, , ) and adding missing props (, , , , ).
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { mode } from 'mode-watcher';
|
||||
let { hex = $bindable() }: { hex: string } = $props();
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
</script>
|
||||
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex
|
||||
sliderDirection="vertical"
|
||||
label="Pick a color"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { mode } from 'mode-watcher';
|
||||
let { hex = $bindable() }: { hex: string } = $props();
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
</script>
|
||||
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex
|
||||
sliderDirection="vertical"
|
||||
label="Pick a color"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin } from '@tiptap/pm/state';
|
||||
|
||||
import findColors from '../utils.js';
|
||||
|
||||
export const ColorHighlighter = Extension.create({
|
||||
name: 'colorHighlighter',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return findColors(doc);
|
||||
},
|
||||
apply(transaction, oldState) {
|
||||
return transaction.docChanged ? findColors(transaction.doc) : oldState;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { Plugin } from '@tiptap/pm/state';
|
||||
|
||||
import findColors from '../utils.js';
|
||||
|
||||
export const ColorHighlighter = Extension.create({
|
||||
name: 'colorHighlighter',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init(_, { doc }) {
|
||||
return findColors(doc);
|
||||
},
|
||||
apply(transaction, oldState) {
|
||||
return transaction.docChanged ? findColors(transaction.doc) : oldState;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
import ImageExtendedComponent from '../image-extended-component.svelte';
|
||||
import Image from '@tiptap/extension-image';
|
||||
|
||||
export const ImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
default: null
|
||||
},
|
||||
align: {
|
||||
default: 'left'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView: () => {
|
||||
return SvelteNodeViewRenderer(ImageExtendedComponent);
|
||||
}
|
||||
});
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
import ImageExtendedComponent from '../image-extended-component.svelte';
|
||||
import Image from '@tiptap/extension-image';
|
||||
|
||||
export const ImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
alt: {
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
default: '100%'
|
||||
},
|
||||
height: {
|
||||
default: null
|
||||
},
|
||||
align: {
|
||||
default: 'left'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addNodeView: () => {
|
||||
return SvelteNodeViewRenderer(ImageExtendedComponent);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,406 +1,406 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade)
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Extension, type Range, type Dispatch } from '@tiptap/core';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state';
|
||||
import { Node as PMNode } from '@tiptap/pm/model';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
search: {
|
||||
/**
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set case sensitivity in extension.
|
||||
*/
|
||||
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Reset current search result to first instance.
|
||||
*/
|
||||
resetIndex: () => ReturnType;
|
||||
/**
|
||||
* @description Find next instance of search result.
|
||||
*/
|
||||
nextSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Find previous instance of search result.
|
||||
*/
|
||||
previousSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Replace first instance of search result with given replace term.
|
||||
*/
|
||||
replace: () => ReturnType;
|
||||
/**
|
||||
* @description Replace all instances of search result with given replace term.
|
||||
*/
|
||||
replaceAll: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface TextNodesWithPosition {
|
||||
text: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s,
|
||||
caseSensitive ? 'gu' : 'gui'
|
||||
);
|
||||
};
|
||||
|
||||
interface ProcessedSearches {
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
}
|
||||
|
||||
function processSearches(
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
): ProcessedSearches {
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
|
||||
if (!searchTerm) {
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.empty,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
doc?.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (textNodesWithPosition[index]) {
|
||||
textNodesWithPosition[index] = {
|
||||
text: textNodesWithPosition[index].text + node.text,
|
||||
pos: textNodesWithPosition[index].pos
|
||||
};
|
||||
} else {
|
||||
textNodesWithPosition[index] = {
|
||||
text: `${node.text}`,
|
||||
pos
|
||||
};
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
|
||||
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||
|
||||
for (const element of textNodesWithPosition) {
|
||||
const { text, pos } = element;
|
||||
const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim());
|
||||
|
||||
for (const m of matches) {
|
||||
if (m[0] === '') break;
|
||||
|
||||
if (m.index !== undefined) {
|
||||
results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const r = results[i];
|
||||
const className =
|
||||
i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass;
|
||||
const decoration: Decoration = Decoration.inline(r.from, r.to, {
|
||||
class: className
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
const replace = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
) => {
|
||||
const firstResult = results[0];
|
||||
|
||||
if (!firstResult) return;
|
||||
|
||||
const { from, to } = results[0];
|
||||
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
};
|
||||
|
||||
const rebaseNextResult = (
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
): [number, Range[]] | null => {
|
||||
const nextIndex = index + 1;
|
||||
|
||||
if (!results[nextIndex]) return null;
|
||||
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
|
||||
const { from, to } = results[nextIndex];
|
||||
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
|
||||
return [offset, results];
|
||||
};
|
||||
|
||||
const replaceAll = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
) => {
|
||||
let offset = 0;
|
||||
|
||||
let resultsCopy = results.slice();
|
||||
|
||||
if (!resultsCopy.length) return;
|
||||
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
|
||||
dispatch(tr);
|
||||
};
|
||||
|
||||
export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin');
|
||||
|
||||
export interface SearchAndReplaceOptions {
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceStorage {
|
||||
searchTerm: string;
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
}
|
||||
|
||||
export const SearchAndReplace = Extension.create<SearchAndReplaceOptions, SearchAndReplaceStorage>({
|
||||
name: 'searchAndReplace',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
replaceTerm: '',
|
||||
results: [],
|
||||
lastSearchTerm: '',
|
||||
caseSensitive: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const nextIndex = resultIndex + 1;
|
||||
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const prevIndex = resultIndex - 1;
|
||||
|
||||
if (results[prevIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = results.length - 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
replace:
|
||||
() =>
|
||||
({ editor, state, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
|
||||
const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastCaseSensitive = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t);
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: searchAndReplacePluginKey,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply({ doc, docChanged }, oldState) {
|
||||
const {
|
||||
searchTerm,
|
||||
lastSearchTerm,
|
||||
caseSensitive,
|
||||
lastCaseSensitive,
|
||||
resultIndex,
|
||||
lastResultIndex
|
||||
} = editor.storage.searchAndReplace;
|
||||
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchAndReplace;
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade)
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Extension, type Range, type Dispatch } from '@tiptap/core';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state';
|
||||
import { Node as PMNode } from '@tiptap/pm/model';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
search: {
|
||||
/**
|
||||
* @description Set search term in extension.
|
||||
*/
|
||||
setSearchTerm: (searchTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set replace term in extension.
|
||||
*/
|
||||
setReplaceTerm: (replaceTerm: string) => ReturnType;
|
||||
/**
|
||||
* @description Set case sensitivity in extension.
|
||||
*/
|
||||
setCaseSensitive: (caseSensitive: boolean) => ReturnType;
|
||||
/**
|
||||
* @description Reset current search result to first instance.
|
||||
*/
|
||||
resetIndex: () => ReturnType;
|
||||
/**
|
||||
* @description Find next instance of search result.
|
||||
*/
|
||||
nextSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Find previous instance of search result.
|
||||
*/
|
||||
previousSearchResult: () => ReturnType;
|
||||
/**
|
||||
* @description Replace first instance of search result with given replace term.
|
||||
*/
|
||||
replace: () => ReturnType;
|
||||
/**
|
||||
* @description Replace all instances of search result with given replace term.
|
||||
*/
|
||||
replaceAll: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface TextNodesWithPosition {
|
||||
text: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s,
|
||||
caseSensitive ? 'gu' : 'gui'
|
||||
);
|
||||
};
|
||||
|
||||
interface ProcessedSearches {
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
}
|
||||
|
||||
function processSearches(
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
): ProcessedSearches {
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
|
||||
if (!searchTerm) {
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.empty,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
|
||||
doc?.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (textNodesWithPosition[index]) {
|
||||
textNodesWithPosition[index] = {
|
||||
text: textNodesWithPosition[index].text + node.text,
|
||||
pos: textNodesWithPosition[index].pos
|
||||
};
|
||||
} else {
|
||||
textNodesWithPosition[index] = {
|
||||
text: `${node.text}`,
|
||||
pos
|
||||
};
|
||||
}
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
});
|
||||
|
||||
textNodesWithPosition = textNodesWithPosition.filter(Boolean);
|
||||
|
||||
for (const element of textNodesWithPosition) {
|
||||
const { text, pos } = element;
|
||||
const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim());
|
||||
|
||||
for (const m of matches) {
|
||||
if (m[0] === '') break;
|
||||
|
||||
if (m.index !== undefined) {
|
||||
results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i += 1) {
|
||||
const r = results[i];
|
||||
const className =
|
||||
i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass;
|
||||
const decoration: Decoration = Decoration.inline(r.from, r.to, {
|
||||
class: className
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
const replace = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
) => {
|
||||
const firstResult = results[0];
|
||||
|
||||
if (!firstResult) return;
|
||||
|
||||
const { from, to } = results[0];
|
||||
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
};
|
||||
|
||||
const rebaseNextResult = (
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
): [number, Range[]] | null => {
|
||||
const nextIndex = index + 1;
|
||||
|
||||
if (!results[nextIndex]) return null;
|
||||
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
|
||||
const { from, to } = results[nextIndex];
|
||||
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
|
||||
return [offset, results];
|
||||
};
|
||||
|
||||
const replaceAll = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
) => {
|
||||
let offset = 0;
|
||||
|
||||
let resultsCopy = results.slice();
|
||||
|
||||
if (!resultsCopy.length) return;
|
||||
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
|
||||
dispatch(tr);
|
||||
};
|
||||
|
||||
export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin');
|
||||
|
||||
export interface SearchAndReplaceOptions {
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceStorage {
|
||||
searchTerm: string;
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
}
|
||||
|
||||
export const SearchAndReplace = Extension.create<SearchAndReplaceOptions, SearchAndReplaceStorage>({
|
||||
name: 'searchAndReplace',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
replaceTerm: '',
|
||||
results: [],
|
||||
lastSearchTerm: '',
|
||||
caseSensitive: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const nextIndex = resultIndex + 1;
|
||||
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const prevIndex = resultIndex - 1;
|
||||
|
||||
if (results[prevIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = results.length - 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
replace:
|
||||
() =>
|
||||
({ editor, state, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
|
||||
const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t);
|
||||
const setLastCaseSensitive = (t: boolean) =>
|
||||
(editor.storage.searchAndReplace.lastCaseSensitive = t);
|
||||
const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t);
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: searchAndReplacePluginKey,
|
||||
state: {
|
||||
init: () => DecorationSet.empty,
|
||||
apply({ doc, docChanged }, oldState) {
|
||||
const {
|
||||
searchTerm,
|
||||
lastSearchTerm,
|
||||
caseSensitive,
|
||||
lastCaseSensitive,
|
||||
resultIndex,
|
||||
lastResultIndex
|
||||
} = editor.storage.searchAndReplace;
|
||||
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchAndReplace;
|
||||
|
||||
@@ -1,133 +1,133 @@
|
||||
import { Extension, textInputRule } from '@tiptap/core';
|
||||
|
||||
export const SmilieReplacer = Extension.create({
|
||||
name: 'smilieReplacer',
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textInputRule({ find: /-___- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:'-\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /':-\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':-D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:-\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /-__- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /':-\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /:'-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: />:-\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /O:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;\^\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /:'\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /:-D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /':\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />;\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />=\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /;-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;-\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\^\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /B-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:-\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\^\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:-\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /-_- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:-X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: />.< $/, replace: '😣 ' }),
|
||||
textInputRule({ find: /:-O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /O_O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: />:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: />:P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /X-P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /x-p $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /':\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /'=\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: />:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-. $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:'\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /;-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /#-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: />:\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /0:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O=\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /<\/3 $/, replace: '💔 ' }),
|
||||
textInputRule({ find: /:D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /=D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /;\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;D $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /=\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /d: $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /=\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /;\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /D: $/, replace: '😨 ' }),
|
||||
textInputRule({ find: /:\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /=\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /#\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /:@ $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /<3 $/, replace: '❤️ ' }),
|
||||
textInputRule({ find: /\/shrug $/, replace: '¯\\_(ツ)_/¯' })
|
||||
];
|
||||
}
|
||||
});
|
||||
import { Extension, textInputRule } from '@tiptap/core';
|
||||
|
||||
export const SmilieReplacer = Extension.create({
|
||||
name: 'smilieReplacer',
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
textInputRule({ find: /-___- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:'-\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /':-\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':-D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:-\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /-__- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /':-\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /:'-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: />:-\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /O:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;\^\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0;-\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:-3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /:'\) $/, replace: '😂 ' }),
|
||||
textInputRule({ find: /:-D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /':\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=\) $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /':D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: /'=D $/, replace: '😅 ' }),
|
||||
textInputRule({ find: />:\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />;\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: />=\) $/, replace: '😆 ' }),
|
||||
textInputRule({ find: /;-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*-\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;-\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\^\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /B-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8-D $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:-\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\^\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:-\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /-_- $/, replace: '😑 ' }),
|
||||
textInputRule({ find: /:-X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:-x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: />.< $/, replace: '😣 ' }),
|
||||
textInputRule({ find: /:-O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /O_O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: />:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:-P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:-b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: />:P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /X-P $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /x-p $/, replace: '😜 ' }),
|
||||
textInputRule({ find: /':\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: /'=\( $/, replace: '😓 ' }),
|
||||
textInputRule({ find: />:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:-. $/, replace: '😕 ' }),
|
||||
textInputRule({ find: />:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:-\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:'\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /;-\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /#-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X-\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: />:\( $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /0:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /0:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O=\) $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /O:3 $/, replace: '😇 ' }),
|
||||
textInputRule({ find: /<\/3 $/, replace: '💔 ' }),
|
||||
textInputRule({ find: /:D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /=D $/, replace: '😃 ' }),
|
||||
textInputRule({ find: /;\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /\*\) $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;\] $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /;D $/, replace: '😉 ' }),
|
||||
textInputRule({ find: /B\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /8\) $/, replace: '😎 ' }),
|
||||
textInputRule({ find: /:\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /=\* $/, replace: '😘 ' }),
|
||||
textInputRule({ find: /:\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /=\) $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:\] $/, replace: '🙂 ' }),
|
||||
textInputRule({ find: /:X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=X $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:x $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /=# $/, replace: '😶 ' }),
|
||||
textInputRule({ find: /:O $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:o $/, replace: '😮 ' }),
|
||||
textInputRule({ find: /:P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=P $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /=p $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:Þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:þ $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:b $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /d: $/, replace: '😛 ' }),
|
||||
textInputRule({ find: /:\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\/ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=\\ $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /=L $/, replace: '😕 ' }),
|
||||
textInputRule({ find: /:\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /:\[ $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /=\( $/, replace: '😞 ' }),
|
||||
textInputRule({ find: /;\( $/, replace: '😢 ' }),
|
||||
textInputRule({ find: /D: $/, replace: '😨 ' }),
|
||||
textInputRule({ find: /:\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /=\$ $/, replace: '😳 ' }),
|
||||
textInputRule({ find: /#\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /%\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /X\) $/, replace: '😵 ' }),
|
||||
textInputRule({ find: /:@ $/, replace: '😠 ' }),
|
||||
textInputRule({ find: /<3 $/, replace: '❤️ ' }),
|
||||
textInputRule({ find: /\/shrug $/, replace: '¯\\_(ツ)_/¯' })
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { NodeViewWrapper, NodeViewContent } from 'svelte-tiptap';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import { onMount } from 'svelte';
|
||||
const { node, editor, selected, deleteNode, updateAttributes, extension }: NodeViewProps =
|
||||
$props();
|
||||
import { Copy, Check, ChevronDown } from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
|
||||
let preRef: HTMLPreElement;
|
||||
|
||||
let isCopying = $state(false);
|
||||
|
||||
const languages = extension.options.lowlight.listLanguages().sort();
|
||||
|
||||
let defaultLanguage = $state(node.attrs.language);
|
||||
|
||||
onMount(() => {
|
||||
console.log(node);
|
||||
});
|
||||
|
||||
function copyCode() {
|
||||
isCopying = true;
|
||||
navigator.clipboard.writeText(preRef.innerText);
|
||||
setTimeout(() => {
|
||||
isCopying = false;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper
|
||||
class="code-wrapper group relative rounded bg-muted p-6 dark:bg-muted/20"
|
||||
draggable
|
||||
>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
contenteditable="false"
|
||||
class={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
class:
|
||||
'absolute left-2 top-2 h-4 rounded px-1 py-2 text-xs capitalize text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100'
|
||||
})}
|
||||
>{defaultLanguage}
|
||||
<ChevronDown class="size-3!" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="h-60 w-40 overflow-auto" contenteditable="false">
|
||||
{#each languages as language}
|
||||
<DropdownMenu.Item
|
||||
contenteditable="false"
|
||||
data-current={defaultLanguage === language}
|
||||
class="capitalize data-[current=true]:bg-muted"
|
||||
onclick={() => {
|
||||
defaultLanguage = language;
|
||||
updateAttributes({ language: defaultLanguage });
|
||||
}}
|
||||
>
|
||||
<span>{language}</span>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="absolute right-2 top-2 size-4 p-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onclick={copyCode}
|
||||
>
|
||||
{#if isCopying}
|
||||
<Check class="size-3 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="size-3" />
|
||||
{/if}
|
||||
</Button>
|
||||
<pre bind:this={preRef}>
|
||||
<NodeViewContent as="code" class={`language-${defaultLanguage}`} {...node.attrs} />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
<script lang="ts">
|
||||
import { NodeViewWrapper, NodeViewContent } from 'svelte-tiptap';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import { onMount } from 'svelte';
|
||||
const { node, editor, selected, deleteNode, updateAttributes, extension }: NodeViewProps =
|
||||
$props();
|
||||
import { Copy, Check, ChevronDown } from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
|
||||
let preRef: HTMLPreElement;
|
||||
|
||||
let isCopying = $state(false);
|
||||
|
||||
const languages = extension.options.lowlight.listLanguages().sort();
|
||||
|
||||
let defaultLanguage = $state(node.attrs.language);
|
||||
|
||||
onMount(() => {
|
||||
console.log(node);
|
||||
});
|
||||
|
||||
function copyCode() {
|
||||
isCopying = true;
|
||||
navigator.clipboard.writeText(preRef.innerText);
|
||||
setTimeout(() => {
|
||||
isCopying = false;
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper
|
||||
class="code-wrapper group relative rounded bg-muted p-6 dark:bg-muted/20"
|
||||
draggable
|
||||
>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
contenteditable="false"
|
||||
class={buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
class:
|
||||
'absolute left-2 top-2 h-4 rounded px-1 py-2 text-xs capitalize text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100'
|
||||
})}
|
||||
>{defaultLanguage}
|
||||
<ChevronDown class="size-3!" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="h-60 w-40 overflow-auto" contenteditable="false">
|
||||
{#each languages as language}
|
||||
<DropdownMenu.Item
|
||||
contenteditable="false"
|
||||
data-current={defaultLanguage === language}
|
||||
class="capitalize data-[current=true]:bg-muted"
|
||||
onclick={() => {
|
||||
defaultLanguage = language;
|
||||
updateAttributes({ language: defaultLanguage });
|
||||
}}
|
||||
>
|
||||
<span>{language}</span>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="absolute right-2 top-2 size-4 p-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onclick={copyCode}
|
||||
>
|
||||
{#if isCopying}
|
||||
<Check class="size-3 text-green-500" />
|
||||
{:else}
|
||||
<Copy class="size-3" />
|
||||
{/if}
|
||||
</Button>
|
||||
<pre bind:this={preRef}>
|
||||
<NodeViewContent as="code" class={`language-${defaultLanguage}`} {...node.attrs} />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
|
||||
@@ -1,251 +1,251 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { NodeViewWrapper } from 'svelte-tiptap';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
EllipsisVertical,
|
||||
CopyIcon,
|
||||
Fullscreen,
|
||||
Trash,
|
||||
Captions
|
||||
} from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { duplicateContent } from './utils.js';
|
||||
|
||||
const { node, editor, selected, deleteNode, updateAttributes }: NodeViewProps = $props();
|
||||
|
||||
const minWidth = 150;
|
||||
|
||||
let imgRef: HTMLImageElement;
|
||||
let nodeRef: HTMLDivElement;
|
||||
|
||||
let resizing = $state(false);
|
||||
let resizingInitialWidth = $state(0);
|
||||
let resizingInitialMouseX = $state(0);
|
||||
let resizingPosition = $state<'left' | 'right'>('left');
|
||||
let openedMore = $state(false);
|
||||
|
||||
function handleResizingPosition(e: MouseEvent, position: 'left' | 'right') {
|
||||
startResize(e);
|
||||
resizingPosition = position;
|
||||
}
|
||||
|
||||
function startResize(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
resizing = true;
|
||||
resizingInitialMouseX = e.clientX;
|
||||
if (imgRef) resizingInitialWidth = imgRef.offsetWidth;
|
||||
}
|
||||
|
||||
function resize(e: MouseEvent) {
|
||||
if (!resizing) return;
|
||||
let dx = e.clientX - resizingInitialMouseX;
|
||||
if (resizingPosition === 'left') {
|
||||
dx = resizingInitialMouseX - e.clientX;
|
||||
}
|
||||
const newWidth = Math.max(resizingInitialWidth + dx, minWidth);
|
||||
const parentWidth = nodeRef?.parentElement?.offsetWidth || 0;
|
||||
if (newWidth < parentWidth) {
|
||||
updateAttributes({ width: newWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function endResize() {
|
||||
resizing = false;
|
||||
resizingInitialMouseX = 0;
|
||||
resizingInitialWidth = 0;
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent, position: 'left' | 'right') {
|
||||
e.preventDefault();
|
||||
resizing = true;
|
||||
resizingPosition = position;
|
||||
resizingInitialMouseX = e.touches[0].clientX;
|
||||
if (imgRef) resizingInitialWidth = imgRef.offsetWidth;
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent) {
|
||||
if (!resizing) return;
|
||||
let dx = e.touches[0].clientX - resizingInitialMouseX;
|
||||
if (resizingPosition === 'left') {
|
||||
dx = resizingInitialMouseX - e.touches[0].clientX;
|
||||
}
|
||||
const newWidth = Math.max(resizingInitialWidth + dx, minWidth);
|
||||
const parentWidth = nodeRef?.parentElement?.offsetWidth || 0;
|
||||
if (newWidth < parentWidth) {
|
||||
updateAttributes({ width: newWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
resizing = false;
|
||||
resizingInitialMouseX = 0;
|
||||
resizingInitialWidth = 0;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Attach id to nodeRef
|
||||
nodeRef = document.getElementById('resizable-container') as HTMLDivElement;
|
||||
|
||||
// Mouse events
|
||||
window.addEventListener('mousemove', resize);
|
||||
window.addEventListener('mouseup', endResize);
|
||||
// Touch events
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
window.addEventListener('touchend', handleTouchEnd);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('mousemove', resize);
|
||||
window.removeEventListener('mouseup', endResize);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('touchend', handleTouchEnd);
|
||||
});
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper
|
||||
id="resizable-container"
|
||||
class={cn(
|
||||
'relative flex flex-col rounded-md border-2 border-transparent',
|
||||
selected ? 'border-muted-foreground' : '',
|
||||
node.attrs.align === 'left' && 'left-0 -translate-x-0',
|
||||
node.attrs.align === 'center' && 'left-1/2 -translate-x-1/2',
|
||||
node.attrs.align === 'right' && 'left-full -translate-x-full'
|
||||
)}
|
||||
style={`width: ${node.attrs.width}px`}
|
||||
>
|
||||
<div class={cn('group relative flex flex-col rounded-md', resizing && '')}>
|
||||
<img
|
||||
bind:this={imgRef}
|
||||
src={node.attrs.src}
|
||||
alt={node.attrs.alt}
|
||||
title={node.attrs.title}
|
||||
class="m-0 object-cover"
|
||||
/>
|
||||
{#if node.attrs.title !== null && node.attrs.title.trim() !== ''}
|
||||
<input
|
||||
value={node.attrs.title}
|
||||
type="text"
|
||||
class="my-1 w-full bg-transparent text-center text-sm text-muted-foreground outline-hidden"
|
||||
onchange={(e) => {
|
||||
if (e.target === null) return;
|
||||
//@ts-ignore
|
||||
updateAttributes({ title: e.target.value });
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if editor?.isEditable}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-start p-2"
|
||||
style="left: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'left');
|
||||
}}
|
||||
ontouchstart={(event: TouchEvent) => {
|
||||
handleTouchStart(event, 'left');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="z-20 h-[70px] w-1 rounded-xl border bg-muted opacity-0 transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-end p-2"
|
||||
style="right: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'right');
|
||||
}}
|
||||
ontouchstart={(event: TouchEvent) => {
|
||||
handleTouchStart(event, 'right');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="z-20 h-[70px] w-1 rounded-xl border bg-muted opacity-0 transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class={cn(
|
||||
'absolute right-4 top-2 flex items-center gap-1 rounded border bg-background p-1 opacity-0 transition-opacity',
|
||||
!resizing && 'group-hover:opacity-100',
|
||||
openedMore && 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'left' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'left' })}
|
||||
>
|
||||
<AlignLeft class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'center' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'center' })}
|
||||
>
|
||||
<AlignCenter class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'right' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'right' })}
|
||||
>
|
||||
<AlignRight class="size-4" />
|
||||
</Button>
|
||||
<DropdownMenu.Root bind:open={openedMore} onOpenChange={(value) => (openedMore = value)}>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', class: 'size-6 p-0' })}>
|
||||
<EllipsisVertical class="size-4" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="start" alignOffset={-90} class="mt-1 overflow-auto text-sm">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (node.attrs.title === null || node.attrs.title.trim() === '')
|
||||
updateAttributes({
|
||||
title: 'Image Caption'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Captions class="mr-1 size-4" /> Caption
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
duplicateContent(editor);
|
||||
}}
|
||||
>
|
||||
<CopyIcon class="mr-1 size-4" /> Duplicate
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
updateAttributes({
|
||||
width: 'fit-content'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Fullscreen class="mr-1 size-4" /> Full Screen
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
deleteNode();
|
||||
}}
|
||||
class="text-destructive"
|
||||
>
|
||||
<Trash class="mr-1 size-4" /> Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { NodeViewWrapper } from 'svelte-tiptap';
|
||||
import type { NodeViewProps } from '@tiptap/core';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button/index.js';
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
EllipsisVertical,
|
||||
CopyIcon,
|
||||
Fullscreen,
|
||||
Trash,
|
||||
Captions
|
||||
} from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { duplicateContent } from './utils.js';
|
||||
|
||||
const { node, editor, selected, deleteNode, updateAttributes }: NodeViewProps = $props();
|
||||
|
||||
const minWidth = 150;
|
||||
|
||||
let imgRef: HTMLImageElement;
|
||||
let nodeRef: HTMLDivElement;
|
||||
|
||||
let resizing = $state(false);
|
||||
let resizingInitialWidth = $state(0);
|
||||
let resizingInitialMouseX = $state(0);
|
||||
let resizingPosition = $state<'left' | 'right'>('left');
|
||||
let openedMore = $state(false);
|
||||
|
||||
function handleResizingPosition(e: MouseEvent, position: 'left' | 'right') {
|
||||
startResize(e);
|
||||
resizingPosition = position;
|
||||
}
|
||||
|
||||
function startResize(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
resizing = true;
|
||||
resizingInitialMouseX = e.clientX;
|
||||
if (imgRef) resizingInitialWidth = imgRef.offsetWidth;
|
||||
}
|
||||
|
||||
function resize(e: MouseEvent) {
|
||||
if (!resizing) return;
|
||||
let dx = e.clientX - resizingInitialMouseX;
|
||||
if (resizingPosition === 'left') {
|
||||
dx = resizingInitialMouseX - e.clientX;
|
||||
}
|
||||
const newWidth = Math.max(resizingInitialWidth + dx, minWidth);
|
||||
const parentWidth = nodeRef?.parentElement?.offsetWidth || 0;
|
||||
if (newWidth < parentWidth) {
|
||||
updateAttributes({ width: newWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function endResize() {
|
||||
resizing = false;
|
||||
resizingInitialMouseX = 0;
|
||||
resizingInitialWidth = 0;
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent, position: 'left' | 'right') {
|
||||
e.preventDefault();
|
||||
resizing = true;
|
||||
resizingPosition = position;
|
||||
resizingInitialMouseX = e.touches[0].clientX;
|
||||
if (imgRef) resizingInitialWidth = imgRef.offsetWidth;
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent) {
|
||||
if (!resizing) return;
|
||||
let dx = e.touches[0].clientX - resizingInitialMouseX;
|
||||
if (resizingPosition === 'left') {
|
||||
dx = resizingInitialMouseX - e.touches[0].clientX;
|
||||
}
|
||||
const newWidth = Math.max(resizingInitialWidth + dx, minWidth);
|
||||
const parentWidth = nodeRef?.parentElement?.offsetWidth || 0;
|
||||
if (newWidth < parentWidth) {
|
||||
updateAttributes({ width: newWidth });
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
resizing = false;
|
||||
resizingInitialMouseX = 0;
|
||||
resizingInitialWidth = 0;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Attach id to nodeRef
|
||||
nodeRef = document.getElementById('resizable-container') as HTMLDivElement;
|
||||
|
||||
// Mouse events
|
||||
window.addEventListener('mousemove', resize);
|
||||
window.addEventListener('mouseup', endResize);
|
||||
// Touch events
|
||||
window.addEventListener('touchmove', handleTouchMove);
|
||||
window.addEventListener('touchend', handleTouchEnd);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('mousemove', resize);
|
||||
window.removeEventListener('mouseup', endResize);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('touchend', handleTouchEnd);
|
||||
});
|
||||
</script>
|
||||
|
||||
<NodeViewWrapper
|
||||
id="resizable-container"
|
||||
class={cn(
|
||||
'relative flex flex-col rounded-md border-2 border-transparent',
|
||||
selected ? 'border-muted-foreground' : '',
|
||||
node.attrs.align === 'left' && 'left-0 -translate-x-0',
|
||||
node.attrs.align === 'center' && 'left-1/2 -translate-x-1/2',
|
||||
node.attrs.align === 'right' && 'left-full -translate-x-full'
|
||||
)}
|
||||
style={`width: ${node.attrs.width}px`}
|
||||
>
|
||||
<div class={cn('group relative flex flex-col rounded-md', resizing && '')}>
|
||||
<img
|
||||
bind:this={imgRef}
|
||||
src={node.attrs.src}
|
||||
alt={node.attrs.alt}
|
||||
title={node.attrs.title}
|
||||
class="m-0 object-cover"
|
||||
/>
|
||||
{#if node.attrs.title !== null && node.attrs.title.trim() !== ''}
|
||||
<input
|
||||
value={node.attrs.title}
|
||||
type="text"
|
||||
class="my-1 w-full bg-transparent text-center text-sm text-muted-foreground outline-hidden"
|
||||
onchange={(e) => {
|
||||
if (e.target === null) return;
|
||||
//@ts-ignore
|
||||
updateAttributes({ title: e.target.value });
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if editor?.isEditable}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-start p-2"
|
||||
style="left: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'left');
|
||||
}}
|
||||
ontouchstart={(event: TouchEvent) => {
|
||||
handleTouchStart(event, 'left');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="z-20 h-[70px] w-1 rounded-xl border bg-muted opacity-0 transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Back"
|
||||
class="absolute inset-y-0 z-20 flex w-[25px] cursor-col-resize items-center justify-end p-2"
|
||||
style="right: 0px"
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
handleResizingPosition(event, 'right');
|
||||
}}
|
||||
ontouchstart={(event: TouchEvent) => {
|
||||
handleTouchStart(event, 'right');
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="z-20 h-[70px] w-1 rounded-xl border bg-muted opacity-0 transition-all group-hover:opacity-100"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class={cn(
|
||||
'absolute right-4 top-2 flex items-center gap-1 rounded border bg-background p-1 opacity-0 transition-opacity',
|
||||
!resizing && 'group-hover:opacity-100',
|
||||
openedMore && 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'left' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'left' })}
|
||||
>
|
||||
<AlignLeft class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'center' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'center' })}
|
||||
>
|
||||
<AlignCenter class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-6 p-0', node.attrs.align === 'right' && 'bg-muted')}
|
||||
onclick={() => updateAttributes({ align: 'right' })}
|
||||
>
|
||||
<AlignRight class="size-4" />
|
||||
</Button>
|
||||
<DropdownMenu.Root bind:open={openedMore} onOpenChange={(value) => (openedMore = value)}>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', class: 'size-6 p-0' })}>
|
||||
<EllipsisVertical class="size-4" />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="start" alignOffset={-90} class="mt-1 overflow-auto text-sm">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (node.attrs.title === null || node.attrs.title.trim() === '')
|
||||
updateAttributes({
|
||||
title: 'Image Caption'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Captions class="mr-1 size-4" /> Caption
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
duplicateContent(editor);
|
||||
}}
|
||||
>
|
||||
<CopyIcon class="mr-1 size-4" /> Duplicate
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
updateAttributes({
|
||||
width: 'fit-content'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Fullscreen class="mr-1 size-4" /> Full Screen
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
deleteNode();
|
||||
}}
|
||||
class="text-destructive"
|
||||
>
|
||||
<Trash class="mr-1 size-4" /> Delete
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { Node } from '@tiptap/pm/model';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
|
||||
export default function (doc: Node): DecorationSet {
|
||||
const hexColor = /(#[0-9a-f]{3,6})\b/gi;
|
||||
const decorations: Decoration[] = [];
|
||||
|
||||
doc.descendants((node, position) => {
|
||||
if (!node.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(node.text.matchAll(hexColor)).forEach((match) => {
|
||||
const color = match[0];
|
||||
const index = match.index || 0;
|
||||
const from = position + index;
|
||||
const to = from + color.length;
|
||||
const decoration = Decoration.inline(from, to, {
|
||||
class: 'color',
|
||||
style: `--color: ${color}`
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
});
|
||||
});
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
}
|
||||
|
||||
export const duplicateContent = (editor: Editor) => {
|
||||
const { view } = editor;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(selection.to, selection.content().content.firstChild?.toJSON(), {
|
||||
updateSelection: true
|
||||
})
|
||||
.focus(selection.to)
|
||||
.run();
|
||||
};
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import { Node } from '@tiptap/pm/model';
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||
|
||||
export default function (doc: Node): DecorationSet {
|
||||
const hexColor = /(#[0-9a-f]{3,6})\b/gi;
|
||||
const decorations: Decoration[] = [];
|
||||
|
||||
doc.descendants((node, position) => {
|
||||
if (!node.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(node.text.matchAll(hexColor)).forEach((match) => {
|
||||
const color = match[0];
|
||||
const index = match.index || 0;
|
||||
const from = position + index;
|
||||
const to = from + color.length;
|
||||
const decoration = Decoration.inline(from, to, {
|
||||
class: 'color',
|
||||
style: `--color: ${color}`
|
||||
});
|
||||
|
||||
decorations.push(decoration);
|
||||
});
|
||||
});
|
||||
|
||||
return DecorationSet.create(doc, decorations);
|
||||
}
|
||||
|
||||
export const duplicateContent = (editor: Editor) => {
|
||||
const { view } = editor;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(selection.to, selection.content().content.firstChild?.toJSON(), {
|
||||
updateSelection: true
|
||||
})
|
||||
.focus(selection.to)
|
||||
.run();
|
||||
};
|
||||
|
||||
@@ -1,172 +1,181 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import Undo from './icons/undo.svelte';
|
||||
import Redo from './icons/redo.svelte';
|
||||
// import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import Bold from './icons/bold.svelte';
|
||||
import Italic from './icons/italic.svelte';
|
||||
import Underline from './icons/underline.svelte';
|
||||
import Strikethrough from './icons/strikethrough.svelte';
|
||||
import Link from './icons/link.svelte';
|
||||
import Code from './icons/code.svelte';
|
||||
import BlockQuote from './icons/block-quote.svelte';
|
||||
import Subscript from './icons/subscript.svelte';
|
||||
import BulletList from './icons/buttle-list.svelte'; // Typo in the icon name
|
||||
import OrderedList from './icons/ordered-list.svelte';
|
||||
import TaskList from './icons/task-list.svelte';
|
||||
import Highlighter from './icons/highlighter.svelte';
|
||||
import Superscript from './icons/superscript.svelte';
|
||||
import Textcolor from './icons/textcolor.svelte';
|
||||
import Align from './icons/textalign.svelte';
|
||||
import Quickcolor from './icons/quickcolor.svelte';
|
||||
import Table from './icons/table.svelte';
|
||||
// import Image from './icons/image.svelte';
|
||||
import Text from './icons/text.svelte';
|
||||
import SearchReplace from './icons/search-replace.svelte';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
show_button_kv?: any;
|
||||
}
|
||||
|
||||
let { editor, show_button_kv }: Props = $props();
|
||||
|
||||
let show_button_kv_defaults: any = {
|
||||
undo: true,
|
||||
redo: true,
|
||||
|
||||
text: false,
|
||||
|
||||
bold: true,
|
||||
italic: true,
|
||||
underline: true,
|
||||
strikethrough: false,
|
||||
align: false,
|
||||
link: false,
|
||||
code: false,
|
||||
blockquote: false,
|
||||
subscript: false,
|
||||
superscript: false,
|
||||
bullet_list: true,
|
||||
ordered_list: true,
|
||||
task_list: false,
|
||||
image: false,
|
||||
table: false,
|
||||
text_color: false,
|
||||
highlighter: false,
|
||||
quick_color: false,
|
||||
search_replace: false,
|
||||
};
|
||||
console.log('show_button_kv', show_button_kv);
|
||||
if (show_button_kv) {
|
||||
show_button_kv = {...show_button_kv_defaults, ...show_button_kv};
|
||||
} else {
|
||||
show_button_kv = show_button_kv_defaults;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
transition:fade={{delay: 250, duration: 750, easing: cubicOut}}
|
||||
class="
|
||||
flex flex-row flex-wrap gap-0.5
|
||||
w-full items-center justify-between
|
||||
overflow-auto border-b p-1
|
||||
transition-all duration-1000
|
||||
"
|
||||
>
|
||||
<span
|
||||
class:hidden={!show_button_kv.undo && !show_button_kv.redo}
|
||||
>
|
||||
{#if show_button_kv.undo}
|
||||
<Undo {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.redo}
|
||||
<Redo {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<!-- <Separator orientation="vertical" class="h-fit" /> -->
|
||||
<span
|
||||
class:hidden={!show_button_kv.text}
|
||||
>
|
||||
{#if show_button_kv.text}
|
||||
<Text {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.bold && !show_button_kv.italic && !show_button_kv.underline && !show_button_kv.strikethrough}
|
||||
>
|
||||
{#if show_button_kv.bold}
|
||||
<Bold {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.italic}
|
||||
<Italic {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.underline}
|
||||
<Underline {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.strikethrough}
|
||||
<Strikethrough {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.align && !show_button_kv.link && !show_button_kv.code && !show_button_kv.blockquote && !show_button_kv.subscript && !show_button_kv.superscript && !show_button_kv.bullet_list && !show_button_kv.ordered_list && !show_button_kv.task_list && !show_button_kv.image && !show_button_kv.table}
|
||||
>
|
||||
{#if show_button_kv.align}
|
||||
<Align {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.link}
|
||||
<Link {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.code}
|
||||
<Code {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.blockquote}
|
||||
<BlockQuote {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.subscript}
|
||||
<Subscript {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.superscript}
|
||||
<Superscript {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.bullet_list}
|
||||
<BulletList {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.ordered_list}
|
||||
<OrderedList {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.task_list}
|
||||
<TaskList {editor} />
|
||||
{/if}
|
||||
<!-- {#if show_button_kv.image}
|
||||
<Image {editor} />
|
||||
{/if} -->
|
||||
{#if show_button_kv.table}
|
||||
<Table {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.text_color && !show_button_kv.highlighter && !show_button_kv.quick_color}
|
||||
>
|
||||
cxx
|
||||
{#if show_button_kv.text_color}
|
||||
<Textcolor {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.highlighter}
|
||||
<Highlighter {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.quick_color}
|
||||
<Quickcolor {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.search_replace}
|
||||
>
|
||||
{#if show_button_kv.search_replace}
|
||||
<SearchReplace {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import Undo from './icons/undo.svelte';
|
||||
import Redo from './icons/redo.svelte';
|
||||
// import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import Bold from './icons/bold.svelte';
|
||||
import Italic from './icons/italic.svelte';
|
||||
import Underline from './icons/underline.svelte';
|
||||
import Strikethrough from './icons/strikethrough.svelte';
|
||||
import Link from './icons/link.svelte';
|
||||
import Code from './icons/code.svelte';
|
||||
import BlockQuote from './icons/block-quote.svelte';
|
||||
import Subscript from './icons/subscript.svelte';
|
||||
import BulletList from './icons/buttle-list.svelte'; // Typo in the icon name
|
||||
import OrderedList from './icons/ordered-list.svelte';
|
||||
import TaskList from './icons/task-list.svelte';
|
||||
import Highlighter from './icons/highlighter.svelte';
|
||||
import Superscript from './icons/superscript.svelte';
|
||||
import Textcolor from './icons/textcolor.svelte';
|
||||
import Align from './icons/textalign.svelte';
|
||||
import Quickcolor from './icons/quickcolor.svelte';
|
||||
import Table from './icons/table.svelte';
|
||||
// import Image from './icons/image.svelte';
|
||||
import Text from './icons/text.svelte';
|
||||
import SearchReplace from './icons/search-replace.svelte';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
show_button_kv?: any;
|
||||
}
|
||||
|
||||
let { editor, show_button_kv }: Props = $props();
|
||||
|
||||
let show_button_kv_defaults: any = {
|
||||
undo: true,
|
||||
redo: true,
|
||||
|
||||
text: false,
|
||||
|
||||
bold: true,
|
||||
italic: true,
|
||||
underline: true,
|
||||
strikethrough: false,
|
||||
align: false,
|
||||
link: false,
|
||||
code: false,
|
||||
blockquote: false,
|
||||
subscript: false,
|
||||
superscript: false,
|
||||
bullet_list: true,
|
||||
ordered_list: true,
|
||||
task_list: false,
|
||||
image: false,
|
||||
table: false,
|
||||
text_color: false,
|
||||
highlighter: false,
|
||||
quick_color: false,
|
||||
search_replace: false
|
||||
};
|
||||
console.log('show_button_kv', show_button_kv);
|
||||
if (show_button_kv) {
|
||||
show_button_kv = { ...show_button_kv_defaults, ...show_button_kv };
|
||||
} else {
|
||||
show_button_kv = show_button_kv_defaults;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
transition:fade={{ delay: 250, duration: 750, easing: cubicOut }}
|
||||
class="
|
||||
flex flex-row flex-wrap gap-0.5
|
||||
w-full items-center justify-between
|
||||
overflow-auto border-b p-1
|
||||
transition-all duration-1000
|
||||
"
|
||||
>
|
||||
<span class:hidden={!show_button_kv.undo && !show_button_kv.redo}>
|
||||
{#if show_button_kv.undo}
|
||||
<Undo {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.redo}
|
||||
<Redo {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<!-- <Separator orientation="vertical" class="h-fit" /> -->
|
||||
<span class:hidden={!show_button_kv.text}>
|
||||
{#if show_button_kv.text}
|
||||
<Text {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.bold &&
|
||||
!show_button_kv.italic &&
|
||||
!show_button_kv.underline &&
|
||||
!show_button_kv.strikethrough}
|
||||
>
|
||||
{#if show_button_kv.bold}
|
||||
<Bold {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.italic}
|
||||
<Italic {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.underline}
|
||||
<Underline {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.strikethrough}
|
||||
<Strikethrough {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.align &&
|
||||
!show_button_kv.link &&
|
||||
!show_button_kv.code &&
|
||||
!show_button_kv.blockquote &&
|
||||
!show_button_kv.subscript &&
|
||||
!show_button_kv.superscript &&
|
||||
!show_button_kv.bullet_list &&
|
||||
!show_button_kv.ordered_list &&
|
||||
!show_button_kv.task_list &&
|
||||
!show_button_kv.image &&
|
||||
!show_button_kv.table}
|
||||
>
|
||||
{#if show_button_kv.align}
|
||||
<Align {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.link}
|
||||
<Link {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.code}
|
||||
<Code {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.blockquote}
|
||||
<BlockQuote {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.subscript}
|
||||
<Subscript {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.superscript}
|
||||
<Superscript {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.bullet_list}
|
||||
<BulletList {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.ordered_list}
|
||||
<OrderedList {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.task_list}
|
||||
<TaskList {editor} />
|
||||
{/if}
|
||||
<!-- {#if show_button_kv.image}
|
||||
<Image {editor} />
|
||||
{/if} -->
|
||||
{#if show_button_kv.table}
|
||||
<Table {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span
|
||||
class:hidden={!show_button_kv.text_color &&
|
||||
!show_button_kv.highlighter &&
|
||||
!show_button_kv.quick_color}
|
||||
>
|
||||
cxx
|
||||
{#if show_button_kv.text_color}
|
||||
<Textcolor {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.highlighter}
|
||||
<Highlighter {editor} />
|
||||
{/if}
|
||||
{#if show_button_kv.quick_color}
|
||||
<Quickcolor {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
<span class:hidden={!show_button_kv.search_replace}>
|
||||
{#if show_button_kv.search_replace}
|
||||
<SearchReplace {editor} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,159 +1,157 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import "@skeletonlabs/skeleton";
|
||||
@import "@skeletonlabs/skeleton/optional/presets";
|
||||
|
||||
/* @import "tailwindcss/preflight"; */
|
||||
/* @tailwind utilities; */
|
||||
|
||||
@import '@skeletonlabs/skeleton/themes/cerberus';
|
||||
@import '@skeletonlabs/skeleton/themes/modern';
|
||||
@import '@skeletonlabs/skeleton/themes/wintry';
|
||||
|
||||
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
||||
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
|
||||
|
||||
.tiptap :where(p):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.tiptap code:not(pre code) {
|
||||
/* Remove the before and after pseudo elements */
|
||||
@apply rounded-sm border bg-black/50 p-1 before:content-[''] after:content-[''];
|
||||
}
|
||||
|
||||
.tiptap blockquote p {
|
||||
@apply before:content-[''] after:content-[''];
|
||||
}
|
||||
|
||||
.tiptap a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul li,
|
||||
ol li {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li[data-checked='true'] div {
|
||||
@apply text-black/50 line-through;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
@apply size-4 cursor-pointer rounded-sm;
|
||||
/* This kills compiling !important */
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Color swatches */
|
||||
.color {
|
||||
@apply whitespace-nowrap;
|
||||
}
|
||||
|
||||
.color::before {
|
||||
@apply mb-[0.15rem] mr-[0.1rem] inline-block size-[1rem] rounded-sm border border-black/50 align-middle;
|
||||
background-color: var(--color);
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
/* Table-specific styling */
|
||||
.tiptap table {
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tiptap table td,
|
||||
.tiptap table th {
|
||||
@apply border border-black/50;
|
||||
box-sizing: border-box;
|
||||
min-width: 1em;
|
||||
padding: 6px 8px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tiptap table td > *,
|
||||
.tiptap table th > * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tiptap table th {
|
||||
@apply bg-black/50 text-left;
|
||||
/* Kills compile: font-bold */
|
||||
font-weight: bold;
|
||||
|
||||
}
|
||||
|
||||
.tiptap table .selectedCell:after {
|
||||
@apply pointer-events-none absolute bottom-0 left-0 right-0 top-0 border-[2px] border-black/50;
|
||||
content: '';
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tiptap table .column-resize-handle {
|
||||
@apply pointer-events-none absolute -bottom-[2px] -right-[2px] top-0 w-1 bg-black/50;
|
||||
}
|
||||
|
||||
.tiptap .tableWrapper {
|
||||
margin: 1.5rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tiptap.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
/* Tiptap code block */
|
||||
.tiptap pre {
|
||||
@apply m-0 flex h-fit overflow-auto !rounded-none bg-transparent p-0;
|
||||
}
|
||||
|
||||
.tiptap pre code {
|
||||
@apply flex-1 !rounded-none bg-transparent p-0 text-inherit;
|
||||
}
|
||||
|
||||
.tiptap .search-result {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.tiptap .search-result-current {
|
||||
background-color: orange;
|
||||
}
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import '@skeletonlabs/skeleton';
|
||||
@import '@skeletonlabs/skeleton/optional/presets';
|
||||
|
||||
/* @import "tailwindcss/preflight"; */
|
||||
/* @tailwind utilities; */
|
||||
|
||||
@import '@skeletonlabs/skeleton/themes/cerberus';
|
||||
@import '@skeletonlabs/skeleton/themes/modern';
|
||||
@import '@skeletonlabs/skeleton/themes/wintry';
|
||||
|
||||
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';
|
||||
|
||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||
|
||||
.tiptap :where(p):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.tiptap code:not(pre code) {
|
||||
/* Remove the before and after pseudo elements */
|
||||
@apply rounded-sm border bg-black/50 p-1 before:content-[''] after:content-[''];
|
||||
}
|
||||
|
||||
.tiptap blockquote p {
|
||||
@apply before:content-[''] after:content-[''];
|
||||
}
|
||||
|
||||
.tiptap a {
|
||||
@apply text-blue-600 underline;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ul li,
|
||||
ol li {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] li[data-checked='true'] div {
|
||||
@apply text-black/50 line-through;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
@apply size-4 cursor-pointer rounded-sm;
|
||||
/* This kills compiling !important */
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Color swatches */
|
||||
.color {
|
||||
@apply whitespace-nowrap;
|
||||
}
|
||||
|
||||
.color::before {
|
||||
@apply mb-[0.15rem] mr-[0.1rem] inline-block size-[1rem] rounded-sm border border-black/50 align-middle;
|
||||
background-color: var(--color);
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
/* Table-specific styling */
|
||||
.tiptap table {
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tiptap table td,
|
||||
.tiptap table th {
|
||||
@apply border border-black/50;
|
||||
box-sizing: border-box;
|
||||
min-width: 1em;
|
||||
padding: 6px 8px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.tiptap table td > *,
|
||||
.tiptap table th > * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tiptap table th {
|
||||
@apply bg-black/50 text-left;
|
||||
/* Kills compile: font-bold */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tiptap table .selectedCell:after {
|
||||
@apply pointer-events-none absolute bottom-0 left-0 right-0 top-0 border-[2px] border-black/50;
|
||||
content: '';
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tiptap table .column-resize-handle {
|
||||
@apply pointer-events-none absolute -bottom-[2px] -right-[2px] top-0 w-1 bg-black/50;
|
||||
}
|
||||
|
||||
.tiptap .tableWrapper {
|
||||
margin: 1.5rem 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tiptap.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
/* Tiptap code block */
|
||||
.tiptap pre {
|
||||
@apply m-0 flex h-fit overflow-auto !rounded-none bg-transparent p-0;
|
||||
}
|
||||
|
||||
.tiptap pre code {
|
||||
@apply flex-1 !rounded-none bg-transparent p-0 text-inherit;
|
||||
}
|
||||
|
||||
.tiptap .search-result {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.tiptap .search-result-current {
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Quote } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('blockquote') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
>
|
||||
<Quote />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Block Quote (⌘⇧B)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Quote } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('blockquote') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
>
|
||||
<Quote />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Block Quote (⌘⇧B)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Bold } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('bold') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Bold />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Bold (⌘B)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Bold } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('bold') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBold().run()}
|
||||
>
|
||||
<Bold />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Bold (⌘B)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { List } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('size-8', editor.isActive('bulletList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
>
|
||||
<List />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Bullet List (⌘⇧8)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { List } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('size-8', editor.isActive('bulletList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
>
|
||||
<List />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Bullet List (⌘⇧8)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Code } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('size-8', editor.isActive('code') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleCode().run()}
|
||||
>
|
||||
<Code />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Code (⌘E)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Code } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class={cn('size-8', editor.isActive('code') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleCode().run()}
|
||||
>
|
||||
<Code />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Code (⌘E)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { Highlighter } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { mode } from 'mode-watcher';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { editor, color = $bindable('') }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('highlight') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus()}
|
||||
>
|
||||
<Highlighter />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-[1.2rem] font-bold">Pick a highlight color</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex={color}
|
||||
sliderDirection="vertical"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
on:input={(event) => {
|
||||
if (event.detail.hex === undefined) return;
|
||||
color = event.detail.hex;
|
||||
editor.chain().focus().setHighlight({ color }).run();
|
||||
}}
|
||||
isDialog={false}
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="border-destructive text-destructive hover:bg-destructive hover:text-foreground"
|
||||
onclick={() => editor.chain().focus().unsetHighlight().run()}
|
||||
>Remove Highlight
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Highlighter (⌘⇧H)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Highlighter } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { mode } from 'mode-watcher';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { editor, color = $bindable('') }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('highlight') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus()}
|
||||
>
|
||||
<Highlighter />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-[1.2rem] font-bold">Pick a highlight color</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex={color}
|
||||
sliderDirection="vertical"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
on:input={(event) => {
|
||||
if (event.detail.hex === undefined) return;
|
||||
color = event.detail.hex;
|
||||
editor.chain().focus().setHighlight({ color }).run();
|
||||
}}
|
||||
isDialog={false}
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="border-destructive text-destructive hover:bg-destructive hover:text-foreground"
|
||||
onclick={() => editor.chain().focus().unsetHighlight().run()}
|
||||
>Remove Highlight
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Highlighter (⌘⇧H)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { Image } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<Image />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold">Image</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<p>Insert image url</p>
|
||||
<Input
|
||||
placeholder="Enter image url..."
|
||||
type="url"
|
||||
onchange={(e) => {
|
||||
if (e !== null && e.target !== null) {
|
||||
//@ts-ignore
|
||||
editor.chain().focus().setImage({ src: e.target.value }).run();
|
||||
}
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
<p>OR Pick an Image</p>
|
||||
<Input
|
||||
id="picture"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
onchange={(e: Event) => {
|
||||
//@ts-ignore
|
||||
if (e.target && e.target.files) {
|
||||
//@ts-ignore
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.map((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const src = reader.result as string;
|
||||
editor.chain().focus().setImage({ src }).run();
|
||||
};
|
||||
//@ts-ignore
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Add Image</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Image } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<Image />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold">Image</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<p>Insert image url</p>
|
||||
<Input
|
||||
placeholder="Enter image url..."
|
||||
type="url"
|
||||
onchange={(e) => {
|
||||
if (e !== null && e.target !== null) {
|
||||
//@ts-ignore
|
||||
editor.chain().focus().setImage({ src: e.target.value }).run();
|
||||
}
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
<p>OR Pick an Image</p>
|
||||
<Input
|
||||
id="picture"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
onchange={(e: Event) => {
|
||||
//@ts-ignore
|
||||
if (e.target && e.target.files) {
|
||||
//@ts-ignore
|
||||
const files = Array.from(e.target.files || []);
|
||||
files.map((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const src = reader.result as string;
|
||||
editor.chain().focus().setImage({ src }).run();
|
||||
};
|
||||
//@ts-ignore
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Add Image</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Italic } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('italic') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Italic />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Italic (⌘I)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Italic } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('italic') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleItalic().run()}
|
||||
>
|
||||
<Italic />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Italic (⌘I)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { Link } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
function setLink(url: string) {
|
||||
if (url.trim() === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('link') && 'bg-muted')}
|
||||
>
|
||||
<Link />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold">Link</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<p>Insert or remove link from selected text.</p>
|
||||
<Input
|
||||
placeholder="Enter link to attach.."
|
||||
value={editor?.getAttributes('link').href}
|
||||
onchange={(e) => {
|
||||
//@ts-ignore
|
||||
if (e !== null && e.target !== null) setLink(e.target.value);
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button size="sm" onclick={() => {}}>
|
||||
<Popover.Close>Insert</Popover.Close>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={() => {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
}}>Remove</Button
|
||||
>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Add Or Remove Link</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Link } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
function setLink(url: string) {
|
||||
if (url.trim() === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('link') && 'bg-muted')}
|
||||
>
|
||||
<Link />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-bold">Link</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<p>Insert or remove link from selected text.</p>
|
||||
<Input
|
||||
placeholder="Enter link to attach.."
|
||||
value={editor?.getAttributes('link').href}
|
||||
onchange={(e) => {
|
||||
//@ts-ignore
|
||||
if (e !== null && e.target !== null) setLink(e.target.value);
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button size="sm" onclick={() => {}}>
|
||||
<Popover.Close>Insert</Popover.Close>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onclick={() => {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
}}>Remove</Button
|
||||
>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Add Or Remove Link</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { ListOrdered } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('orderedList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
>
|
||||
<ListOrdered />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Ordered List</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { ListOrdered } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('orderedList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
>
|
||||
<ListOrdered />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Ordered List</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import { Check, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
const colors = [
|
||||
{ label: 'Default', value: '' },
|
||||
{ label: 'Blue', value: '#0000FF' },
|
||||
{ label: 'Brown', value: '#A52A2A' },
|
||||
{ label: 'Green', value: '#008000' },
|
||||
{ label: 'Grey', value: '#808080' },
|
||||
{ label: 'Orange', value: '#FFA500' },
|
||||
{ label: 'Pink', value: '#FFC0CB' },
|
||||
{ label: 'Purple', value: '#800080' },
|
||||
{ label: 'Red', value: '#FF0000' },
|
||||
{ label: 'Yellow', value: '#FFFF00' }
|
||||
];
|
||||
|
||||
const currentColor = $derived(editor.getAttributes('textStyle').color);
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8" style={`color: ${currentColor}`}>
|
||||
A
|
||||
<ChevronDown class="size-3!" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-h-100 w-40 overflow-auto">
|
||||
<DropdownMenu.Group>
|
||||
<span class="text-[0.75rem] font-medium text-muted-foreground">Text Color</span>
|
||||
{#each colors as color}
|
||||
<DropdownMenu.Item
|
||||
class="flex items-center"
|
||||
onclick={() => {
|
||||
if (color.value === '' || color.label === 'Default')
|
||||
editor.chain().focus().unsetColor().run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setColor(currentColor === color.value ? '' : color.value)
|
||||
.run();
|
||||
}}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<span class="rounded border px-1 py-px font-medium" style={`color: ${color.value}`}
|
||||
>A</span
|
||||
>
|
||||
<span>{color.label}</span>
|
||||
{#if editor.isActive('textStyle', { color: color.value })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<span class="text-[0.75rem] font-medium text-muted-foreground">Background Colors</span>
|
||||
{#each colors as color}
|
||||
<DropdownMenu.Item
|
||||
class="flex items-center"
|
||||
onclick={() => {
|
||||
if (color.value === '' || color.label === 'Default')
|
||||
editor.chain().focus().unsetHighlight().run();
|
||||
else editor.chain().focus().toggleHighlight({ color: color.value }).run();
|
||||
}}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<span
|
||||
class="rounded px-1 py-px font-medium"
|
||||
style={`background-color: ${color.value};`}>A</span
|
||||
>
|
||||
<span>{color.label}</span>
|
||||
{#if editor.isActive('highlight', { color: color.value })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Quick Colors</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import { Check, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
const colors = [
|
||||
{ label: 'Default', value: '' },
|
||||
{ label: 'Blue', value: '#0000FF' },
|
||||
{ label: 'Brown', value: '#A52A2A' },
|
||||
{ label: 'Green', value: '#008000' },
|
||||
{ label: 'Grey', value: '#808080' },
|
||||
{ label: 'Orange', value: '#FFA500' },
|
||||
{ label: 'Pink', value: '#FFC0CB' },
|
||||
{ label: 'Purple', value: '#800080' },
|
||||
{ label: 'Red', value: '#FF0000' },
|
||||
{ label: 'Yellow', value: '#FFFF00' }
|
||||
];
|
||||
|
||||
const currentColor = $derived(editor.getAttributes('textStyle').color);
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8" style={`color: ${currentColor}`}>
|
||||
A
|
||||
<ChevronDown class="size-3!" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-h-100 w-40 overflow-auto">
|
||||
<DropdownMenu.Group>
|
||||
<span class="text-[0.75rem] font-medium text-muted-foreground">Text Color</span>
|
||||
{#each colors as color}
|
||||
<DropdownMenu.Item
|
||||
class="flex items-center"
|
||||
onclick={() => {
|
||||
if (color.value === '' || color.label === 'Default')
|
||||
editor.chain().focus().unsetColor().run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setColor(currentColor === color.value ? '' : color.value)
|
||||
.run();
|
||||
}}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<span class="rounded border px-1 py-px font-medium" style={`color: ${color.value}`}
|
||||
>A</span
|
||||
>
|
||||
<span>{color.label}</span>
|
||||
{#if editor.isActive('textStyle', { color: color.value })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<span class="text-[0.75rem] font-medium text-muted-foreground">Background Colors</span>
|
||||
{#each colors as color}
|
||||
<DropdownMenu.Item
|
||||
class="flex items-center"
|
||||
onclick={() => {
|
||||
if (color.value === '' || color.label === 'Default')
|
||||
editor.chain().focus().unsetHighlight().run();
|
||||
else editor.chain().focus().toggleHighlight({ color: color.value }).run();
|
||||
}}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<span
|
||||
class="rounded px-1 py-px font-medium"
|
||||
style={`background-color: ${color.value};`}>A</span
|
||||
>
|
||||
<span>{color.label}</span>
|
||||
{#if editor.isActive('highlight', { color: color.value })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Quick Colors</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Redo } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
onclick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().chain().focus().redo().run()}
|
||||
>
|
||||
<Redo />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Redo (⌘R)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Redo } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="size-8"
|
||||
onclick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editor.can().chain().focus().redo().run()}
|
||||
>
|
||||
<Redo />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Redo (⌘R)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
TextSearch,
|
||||
X,
|
||||
Replace,
|
||||
ReplaceAll,
|
||||
ChevronDown
|
||||
} from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
let searchText = $state('');
|
||||
let replaceText = $state('');
|
||||
let caseSensitive = $state(false);
|
||||
|
||||
let searchIndex = $derived(editor.storage?.searchAndReplace?.resultIndex);
|
||||
let searchCount = $derived(editor.storage?.searchAndReplace?.results.length);
|
||||
|
||||
function updateSearchTerm(clearIndex: boolean = false) {
|
||||
if (clearIndex) editor.commands.resetIndex();
|
||||
|
||||
editor.commands.setSearchTerm(searchText);
|
||||
editor.commands.setReplaceTerm(replaceText);
|
||||
editor.commands.setCaseSensitive(caseSensitive);
|
||||
}
|
||||
|
||||
function goToSelection() {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
const position: Range = results[resultIndex];
|
||||
if (!position) return;
|
||||
//@ts-ignore
|
||||
editor.commands.setTextSelection(position);
|
||||
const { node } = editor.view.domAtPos(editor.state.selection.anchor);
|
||||
node instanceof HTMLElement && node.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function replace() {
|
||||
editor.commands.replace();
|
||||
goToSelection();
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
editor.commands.nextSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
editor.commands.previousSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
searchText = '';
|
||||
replaceText = '';
|
||||
editor.commands.resetIndex();
|
||||
};
|
||||
|
||||
const replaceAll = () => editor.commands.replaceAll();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root
|
||||
onOpenChange={(open) => {
|
||||
if (open) updateSearchTerm();
|
||||
else {
|
||||
clear();
|
||||
updateSearchTerm(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popover.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<TextSearch />
|
||||
<ChevronDown class="size-3 text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<Popover.Close
|
||||
class="absolute right-2 top-0 text-muted-foreground"
|
||||
onclick={() => {
|
||||
clear();
|
||||
updateSearchTerm(true);
|
||||
}}
|
||||
>
|
||||
<X class="size-4" />
|
||||
</Popover.Close>
|
||||
<div class="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Enter Text to search.."
|
||||
bind:value={searchText}
|
||||
oninput={() => updateSearchTerm()}
|
||||
class="mr-1 "
|
||||
/>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={previous}>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={next}>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Enter Text to Replace.."
|
||||
bind:value={replaceText}
|
||||
oninput={() => updateSearchTerm()}
|
||||
class="mr-1 "
|
||||
/>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={replace}>
|
||||
<Replace />
|
||||
</Button>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={replaceAll}>
|
||||
<ReplaceAll />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={caseSensitive}
|
||||
onchange={() => updateSearchTerm()}
|
||||
/>
|
||||
<p>Case Sensitive</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{searchCount > 0 ? searchIndex + 1 : 0} / {searchCount}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Search And Replace Text</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
TextSearch,
|
||||
X,
|
||||
Replace,
|
||||
ReplaceAll,
|
||||
ChevronDown
|
||||
} from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
|
||||
let searchText = $state('');
|
||||
let replaceText = $state('');
|
||||
let caseSensitive = $state(false);
|
||||
|
||||
let searchIndex = $derived(editor.storage?.searchAndReplace?.resultIndex);
|
||||
let searchCount = $derived(editor.storage?.searchAndReplace?.results.length);
|
||||
|
||||
function updateSearchTerm(clearIndex: boolean = false) {
|
||||
if (clearIndex) editor.commands.resetIndex();
|
||||
|
||||
editor.commands.setSearchTerm(searchText);
|
||||
editor.commands.setReplaceTerm(replaceText);
|
||||
editor.commands.setCaseSensitive(caseSensitive);
|
||||
}
|
||||
|
||||
function goToSelection() {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
const position: Range = results[resultIndex];
|
||||
if (!position) return;
|
||||
//@ts-ignore
|
||||
editor.commands.setTextSelection(position);
|
||||
const { node } = editor.view.domAtPos(editor.state.selection.anchor);
|
||||
node instanceof HTMLElement && node.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function replace() {
|
||||
editor.commands.replace();
|
||||
goToSelection();
|
||||
}
|
||||
|
||||
const next = () => {
|
||||
editor.commands.nextSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
editor.commands.previousSearchResult();
|
||||
goToSelection();
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
searchText = '';
|
||||
replaceText = '';
|
||||
editor.commands.resetIndex();
|
||||
};
|
||||
|
||||
const replaceAll = () => editor.commands.replaceAll();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root
|
||||
onOpenChange={(open) => {
|
||||
if (open) updateSearchTerm();
|
||||
else {
|
||||
clear();
|
||||
updateSearchTerm(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popover.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<TextSearch />
|
||||
<ChevronDown class="size-3 text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<Popover.Close
|
||||
class="absolute right-2 top-0 text-muted-foreground"
|
||||
onclick={() => {
|
||||
clear();
|
||||
updateSearchTerm(true);
|
||||
}}
|
||||
>
|
||||
<X class="size-4" />
|
||||
</Popover.Close>
|
||||
<div class="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Enter Text to search.."
|
||||
bind:value={searchText}
|
||||
oninput={() => updateSearchTerm()}
|
||||
class="mr-1 "
|
||||
/>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={previous}>
|
||||
<ArrowLeft />
|
||||
</Button>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={next}>
|
||||
<ArrowRight />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Enter Text to Replace.."
|
||||
bind:value={replaceText}
|
||||
oninput={() => updateSearchTerm()}
|
||||
class="mr-1 "
|
||||
/>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={replace}>
|
||||
<Replace />
|
||||
</Button>
|
||||
<Button variant="ghost" class="ml-1 size-8" onclick={replaceAll}>
|
||||
<ReplaceAll />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={caseSensitive}
|
||||
onchange={() => updateSearchTerm()}
|
||||
/>
|
||||
<p>Case Sensitive</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{searchCount > 0 ? searchIndex + 1 : 0} / {searchCount}
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Search And Replace Text</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Strikethrough } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('strike') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleStrike().run()}
|
||||
>
|
||||
<Strikethrough />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Strike (⌘⇧S)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Strikethrough } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('strike') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleStrike().run()}
|
||||
>
|
||||
<Strikethrough />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Strike (⌘⇧S)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Subscript } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('subscript') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleSubscript().run()}
|
||||
>
|
||||
<Subscript />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Subscript (⌘,)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Subscript } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('subscript') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleSubscript().run()}
|
||||
>
|
||||
<Subscript />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Subscript (⌘,)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Superscript } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('superscript') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleSuperscript().run()}
|
||||
>
|
||||
<Superscript />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Superscript (⌘.)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Superscript } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('superscript') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleSuperscript().run()}
|
||||
>
|
||||
<Superscript />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Superscript (⌘.)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,111 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import { Table, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<Table />
|
||||
<ChevronDown class="size-3!" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-h-100 w-40 overflow-auto">
|
||||
<DropdownMenu.Item
|
||||
onclick={() =>
|
||||
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
|
||||
>
|
||||
<span>Insert Table</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Header</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderColumn().run()}>
|
||||
<span>Toggle Column</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderRow().run()}>
|
||||
<span>Toggle Row</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderCell().run()}>
|
||||
<span>Toggle Cell</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Cells</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().mergeCells().run()}>
|
||||
<span>Merge</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().splitCell().run()}>
|
||||
<span>Split</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().mergeOrSplit().run()}>
|
||||
<span>Merge or Split</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Row</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addRowBefore().run()}>
|
||||
<span>Insert Above</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addRowAfter().run()}>
|
||||
<span>Insert Below</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteRow().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete Row</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Column</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addColumnBefore().run()}>
|
||||
<span>Insert Before</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addColumnAfter().run()}>
|
||||
<span>Insert After</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteColumn().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteTable().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Table</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import { Table, ChevronDown } from 'lucide-svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
<Table />
|
||||
<ChevronDown class="size-3!" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="max-h-100 w-40 overflow-auto">
|
||||
<DropdownMenu.Item
|
||||
onclick={() =>
|
||||
editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}
|
||||
>
|
||||
<span>Insert Table</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Header</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderColumn().run()}>
|
||||
<span>Toggle Column</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderRow().run()}>
|
||||
<span>Toggle Row</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().toggleHeaderCell().run()}>
|
||||
<span>Toggle Cell</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Cells</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().mergeCells().run()}>
|
||||
<span>Merge</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().splitCell().run()}>
|
||||
<span>Split</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().mergeOrSplit().run()}>
|
||||
<span>Merge or Split</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Row</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addRowBefore().run()}>
|
||||
<span>Insert Above</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addRowAfter().run()}>
|
||||
<span>Insert Below</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteRow().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete Row</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger>
|
||||
<span>Column</span>
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.SubContent>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addColumnBefore().run()}>
|
||||
<span>Insert Before</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => editor.chain().focus().addColumnAfter().run()}>
|
||||
<span>Insert After</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteColumn().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Sub>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().deleteTable().run()}
|
||||
class="text-destructive hover:text-foreground data-[highlighted]:bg-destructive"
|
||||
>
|
||||
<span>Delete</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Table</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { CheckSquare } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('taskList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleTaskList().run()}
|
||||
>
|
||||
<CheckSquare />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Tasks List (⌘⇧9)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { CheckSquare } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('taskList') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleTaskList().run()}
|
||||
>
|
||||
<CheckSquare />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Tasks List (⌘⇧9)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Pilcrow,
|
||||
FileJson,
|
||||
ChevronDown,
|
||||
Check,
|
||||
Minus
|
||||
} from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
{#if editor.isActive('heading', { level: 1 })}
|
||||
<Heading1 />
|
||||
{:else if editor.isActive('heading', { level: 2 })}
|
||||
<Heading2 />
|
||||
{:else if editor.isActive('heading', { level: 3 })}
|
||||
<Heading3 />
|
||||
{:else if editor.isActive('paragraph')}
|
||||
<Pilcrow />
|
||||
{:else if editor.isActive('codeBlock')}
|
||||
<FileJson />
|
||||
{:else}
|
||||
<Minus />
|
||||
{/if}
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading1 /> Heading 1
|
||||
{#if editor.isActive('heading', { level: 1 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading2 /> Heading 2
|
||||
{#if editor.isActive('heading', { level: 2 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading3 /> Heading 3
|
||||
{#if editor.isActive('heading', { level: 3 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setParagraph().run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Pilcrow /> Paragraph
|
||||
{#if editor.isActive('paragraph')}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FileJson /> Code Block
|
||||
{#if editor.isActive('codeBlock')}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Formatting</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import {
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Pilcrow,
|
||||
FileJson,
|
||||
ChevronDown,
|
||||
Check,
|
||||
Minus
|
||||
} from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
{#if editor.isActive('heading', { level: 1 })}
|
||||
<Heading1 />
|
||||
{:else if editor.isActive('heading', { level: 2 })}
|
||||
<Heading2 />
|
||||
{:else if editor.isActive('heading', { level: 3 })}
|
||||
<Heading3 />
|
||||
{:else if editor.isActive('paragraph')}
|
||||
<Pilcrow />
|
||||
{:else if editor.isActive('codeBlock')}
|
||||
<FileJson />
|
||||
{:else}
|
||||
<Minus />
|
||||
{/if}
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading1 /> Heading 1
|
||||
{#if editor.isActive('heading', { level: 1 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading2 /> Heading 2
|
||||
{#if editor.isActive('heading', { level: 2 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Heading3 /> Heading 3
|
||||
{#if editor.isActive('heading', { level: 3 })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setParagraph().run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<Pilcrow /> Paragraph
|
||||
{#if editor.isActive('paragraph')}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FileJson /> Code Block
|
||||
{#if editor.isActive('codeBlock')}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Formatting</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
AlignJustify,
|
||||
ChevronDown,
|
||||
Check
|
||||
} from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
{#if editor.isActive({ textAlign: 'left' })}
|
||||
<AlignLeft />
|
||||
{:else if editor.isActive({ textAlign: 'center' })}
|
||||
<AlignCenter />
|
||||
{:else if editor.isActive({ textAlign: 'right' })}
|
||||
<AlignRight />
|
||||
{:else if editor.isActive({ textAlign: 'justify' })}
|
||||
<AlignJustify />
|
||||
{:else}
|
||||
<AlignLeft />
|
||||
{/if}
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignLeft /> Align Left
|
||||
{#if editor.isActive({ textAlign: 'left' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignCenter /> Align Center
|
||||
{#if editor.isActive({ textAlign: 'center' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignRight /> Align Right
|
||||
{#if editor.isActive({ textAlign: 'right' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignJustify /> Align Justify
|
||||
{#if editor.isActive({ textAlign: 'justify' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Alignment</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
AlignJustify,
|
||||
ChevronDown,
|
||||
Check
|
||||
} from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="ghost" size="sm" class="h-8">
|
||||
{#if editor.isActive({ textAlign: 'left' })}
|
||||
<AlignLeft />
|
||||
{:else if editor.isActive({ textAlign: 'center' })}
|
||||
<AlignCenter />
|
||||
{:else if editor.isActive({ textAlign: 'right' })}
|
||||
<AlignRight />
|
||||
{:else if editor.isActive({ textAlign: 'justify' })}
|
||||
<AlignJustify />
|
||||
{:else}
|
||||
<AlignLeft />
|
||||
{/if}
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content class="w-56">
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignLeft /> Align Left
|
||||
{#if editor.isActive({ textAlign: 'left' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignCenter /> Align Center
|
||||
{#if editor.isActive({ textAlign: 'center' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignRight /> Align Right
|
||||
{#if editor.isActive({ textAlign: 'right' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<AlignJustify /> Align Justify
|
||||
{#if editor.isActive({ textAlign: 'justify' })}
|
||||
<Check class="absolute right-2 size-3! text-muted-foreground" />
|
||||
{/if}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Alignment</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { Baseline } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { mode } from 'mode-watcher';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { editor, color = $bindable('') }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('textStyle') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus()}
|
||||
>
|
||||
<Baseline />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-[1.2rem] font-bold">Pick a text color</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex={color}
|
||||
sliderDirection="vertical"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
on:input={(event) => {
|
||||
if (event.detail.hex === undefined) return;
|
||||
color = event.detail.hex;
|
||||
editor.chain().focus().setColor(color).run();
|
||||
}}
|
||||
isDialog={false}
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="border-destructive text-destructive hover:bg-destructive hover:text-foreground"
|
||||
onclick={() => editor.chain().focus().unsetColor().run()}
|
||||
>Remove Color
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Color</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Baseline } from 'lucide-svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { ChevronDown } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import * as Popover from '$lib/components/ui/popover/index.js';
|
||||
import { mode } from 'mode-watcher';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
interface Props {
|
||||
editor: Editor;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { editor, color = $bindable('') }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class={cn('h-8', editor.isActive('textStyle') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus()}
|
||||
>
|
||||
<Baseline />
|
||||
<ChevronDown class="size-3! text-muted-foreground" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="bg-popover shadow-lg *:my-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-[1.2rem] font-bold">Pick a text color</h1>
|
||||
<Popover.Close>
|
||||
<X class="size-4 text-muted-foreground" />
|
||||
</Popover.Close>
|
||||
</div>
|
||||
<div class:dark={$mode === 'dark'}>
|
||||
<ColorPicker
|
||||
bind:hex={color}
|
||||
sliderDirection="vertical"
|
||||
isTextInput={false}
|
||||
isAlpha
|
||||
on:input={(event) => {
|
||||
if (event.detail.hex === undefined) return;
|
||||
color = event.detail.hex;
|
||||
editor.chain().focus().setColor(color).run();
|
||||
}}
|
||||
isDialog={false}
|
||||
--picker-indicator-size="1rem"
|
||||
--input-size="1rem"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="border-destructive text-destructive hover:bg-destructive hover:text-foreground"
|
||||
onclick={() => editor.chain().focus().unsetColor().run()}
|
||||
>Remove Color
|
||||
</Button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Text Color</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Underline } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('underline') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<Underline />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Underline (⌘U)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Underline } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class={cn('size-8', editor.isActive('underline') && 'bg-muted')}
|
||||
onclick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
>
|
||||
<Underline />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Underline (⌘U)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Undo } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="size-8 p-0"
|
||||
onclick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().chain().focus().undo().run()}
|
||||
>
|
||||
<Undo />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Undo (⌘Z)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
<script lang="ts">
|
||||
import { Undo } from 'lucide-svelte';
|
||||
import { type Editor } from '@tiptap/core';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
|
||||
let { editor }: { editor: Editor } = $props();
|
||||
</script>
|
||||
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="size-8 p-0"
|
||||
onclick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editor.can().chain().focus().undo().run()}
|
||||
>
|
||||
<Undo />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<p>Undo (⌘Z)</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
/* One Dark and Light Theme for Highlight.js using Tailwind CSS */
|
||||
.tiptap pre code {
|
||||
@apply text-[#383a42] dark:text-[#abb2bf];
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
@apply italic text-[#a0a1a7] dark:text-[#5c6370];
|
||||
}
|
||||
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
@apply text-[#e45649] dark:text-[#e06c75];
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
@apply text-[#986801] dark:text-[#d19a66];
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
@apply text-[#c18401] dark:text-[#e5c07b];
|
||||
}
|
||||
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
@apply text-[#50a14f] dark:text-[#98c379];
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
@apply text-[#4078f2] dark:text-[#61afef];
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
@apply text-[#a626a4] dark:text-[#c678dd];
|
||||
}
|
||||
|
||||
/* Cyan */
|
||||
.hljs-emphasis {
|
||||
@apply italic text-[#0184bc] dark:text-[#56b6c2];
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
/* @apply font-bold; */
|
||||
/* Kills compile: font-bold */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
@apply text-[#a626a4] dark:text-[#c678dd];
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-subst {
|
||||
@apply text-[#383a42] dark:text-[#abb2bf];
|
||||
}
|
||||
|
||||
/* Line highlights */
|
||||
.hljs-addition {
|
||||
@apply bg-[#e6ffed] dark:bg-[#283428];
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
@apply bg-[#ffeef0] dark:bg-[#342828];
|
||||
}
|
||||
/* One Dark and Light Theme for Highlight.js using Tailwind CSS */
|
||||
.tiptap pre code {
|
||||
@apply text-[#383a42] dark:text-[#abb2bf];
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
@apply italic text-[#a0a1a7] dark:text-[#5c6370];
|
||||
}
|
||||
|
||||
/* Red */
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-regexp,
|
||||
.hljs-deletion {
|
||||
@apply text-[#e45649] dark:text-[#e06c75];
|
||||
}
|
||||
|
||||
/* Orange */
|
||||
.hljs-number,
|
||||
.hljs-built_in,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params,
|
||||
.hljs-meta,
|
||||
.hljs-link {
|
||||
@apply text-[#986801] dark:text-[#d19a66];
|
||||
}
|
||||
|
||||
/* Yellow */
|
||||
.hljs-attribute {
|
||||
@apply text-[#c18401] dark:text-[#e5c07b];
|
||||
}
|
||||
|
||||
/* Green */
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-addition {
|
||||
@apply text-[#50a14f] dark:text-[#98c379];
|
||||
}
|
||||
|
||||
/* Blue */
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
@apply text-[#4078f2] dark:text-[#61afef];
|
||||
}
|
||||
|
||||
/* Purple */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
@apply text-[#a626a4] dark:text-[#c678dd];
|
||||
}
|
||||
|
||||
/* Cyan */
|
||||
.hljs-emphasis {
|
||||
@apply italic text-[#0184bc] dark:text-[#56b6c2];
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
/* @apply font-bold; */
|
||||
/* Kills compile: font-bold */
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.hljs-doctag,
|
||||
.hljs-formula {
|
||||
@apply text-[#a626a4] dark:text-[#c678dd];
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-subst {
|
||||
@apply text-[#383a42] dark:text-[#abb2bf];
|
||||
}
|
||||
|
||||
/* Line highlights */
|
||||
.hljs-addition {
|
||||
@apply bg-[#e6ffed] dark:bg-[#283428];
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
@apply bg-[#ffeef0] dark:bg-[#342828];
|
||||
}
|
||||
|
||||
@@ -1,180 +1,175 @@
|
||||
<script lang="ts">
|
||||
import './editor.css';
|
||||
|
||||
import { Editor, type Content } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import EditorToolbar from './editor-toolbar.svelte';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
import { Subscript } from '@tiptap/extension-subscript';
|
||||
import { Superscript } from '@tiptap/extension-superscript';
|
||||
import { Underline } from '@tiptap/extension-underline';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import TaskList from '@tiptap/extension-task-list';
|
||||
import TaskItem from '@tiptap/extension-task-item';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import Color from '@tiptap/extension-color';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import Text from '@tiptap/extension-text';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
|
||||
import { SmilieReplacer } from './custom/Extentions/SmilieReplacer.js';
|
||||
import { ColorHighlighter } from './custom/Extentions/ColorHighlighter.js';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import TableRow from '@tiptap/extension-table-row';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import { ImageExtension } from './custom/Extentions/ImageExtention.js';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
import CodeExtended from './custom/code-extended.svelte';
|
||||
|
||||
// Lowlight
|
||||
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
import './onedark.css';
|
||||
import SearchAndReplace from './custom/Extentions/SearchAndReplace.js';
|
||||
|
||||
const lowlight = createLowlight(all);
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
content?: Content;
|
||||
show_toolbar?: boolean;
|
||||
// html_text?: string;
|
||||
new_html?: string;
|
||||
placeholder?: string;
|
||||
show_button_kv?: any;
|
||||
}
|
||||
|
||||
let { class:
|
||||
className = '',
|
||||
content = $bindable(''),
|
||||
show_toolbar = true,
|
||||
// html_text = '',
|
||||
new_html = $bindable(''),
|
||||
placeholder = $bindable('Start typing...'),
|
||||
show_button_kv = $bindable({})
|
||||
}: Props = $props();
|
||||
|
||||
let editor = $state<Editor>();
|
||||
let element = $state<HTMLElement>();
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
'm-auto p-2 focus:outline-hidden flex-1 prose text-foreground min-w-full max-h-full overflow-auto dark:prose-invert *:my-2'
|
||||
}
|
||||
},
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: 'list-decimal'
|
||||
}
|
||||
},
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: 'list-disc'
|
||||
}
|
||||
},
|
||||
heading: {
|
||||
levels: [1, 2, 3, 4],
|
||||
HTMLAttributes: {
|
||||
class: 'tiptap-heading'
|
||||
}
|
||||
}
|
||||
}),
|
||||
Typography,
|
||||
Text,
|
||||
TextStyle,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph']
|
||||
}),
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true }),
|
||||
Underline,
|
||||
Superscript,
|
||||
Subscript,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
HTMLAttributes: {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true
|
||||
}),
|
||||
SearchAndReplace,
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight
|
||||
}).extend({
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(CodeExtended);
|
||||
}
|
||||
}),
|
||||
SmilieReplacer,
|
||||
ColorHighlighter,
|
||||
Table.configure({
|
||||
allowTableNodeSelection: true,
|
||||
resizable: true
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
ImageExtension
|
||||
],
|
||||
autofocus: false, // This should be changed to a prop in the future
|
||||
onTransaction: (transaction) => {
|
||||
/**
|
||||
* Weird behavior of editor.
|
||||
* If we do not make it undefined, then it looses it's reactivity
|
||||
* this is because assigning editor directly to `transaction.editor`
|
||||
* the original object is not mutated.
|
||||
*/
|
||||
editor = undefined;
|
||||
editor = transaction.editor;
|
||||
// console.log(editor.isActive('bold'));
|
||||
content = editor.getHTML();
|
||||
// console.log(content);
|
||||
let html = editor.getHTML();
|
||||
if (html == '<p></p>') {
|
||||
new_html = '';
|
||||
} else {
|
||||
new_html = html ?? '';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) editor.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cn('flex flex-col rounded border textarea editor', className)}
|
||||
>
|
||||
{#if editor && show_toolbar}
|
||||
<EditorToolbar {editor} {show_button_kv} />
|
||||
{/if}
|
||||
<div
|
||||
bind:this={element}
|
||||
spellcheck="false"
|
||||
class="tiptap overflow-auto relative">
|
||||
<span
|
||||
class="placeholder text-sm text-gray-400 italic absolute p-3 w-full"
|
||||
contenteditable="false"
|
||||
hidden={editor?.getHTML() !== '<p></p>'}
|
||||
>{placeholder}</span>
|
||||
</div>
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import './editor.css';
|
||||
|
||||
import { Editor, type Content } from '@tiptap/core';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import EditorToolbar from './editor-toolbar.svelte';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
import { Subscript } from '@tiptap/extension-subscript';
|
||||
import { Superscript } from '@tiptap/extension-superscript';
|
||||
import { Underline } from '@tiptap/extension-underline';
|
||||
import { Link } from '@tiptap/extension-link';
|
||||
import TaskList from '@tiptap/extension-task-list';
|
||||
import TaskItem from '@tiptap/extension-task-item';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import Color from '@tiptap/extension-color';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import Text from '@tiptap/extension-text';
|
||||
import Typography from '@tiptap/extension-typography';
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
|
||||
import { SmilieReplacer } from './custom/Extentions/SmilieReplacer.js';
|
||||
import { ColorHighlighter } from './custom/Extentions/ColorHighlighter.js';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import TableRow from '@tiptap/extension-table-row';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import { ImageExtension } from './custom/Extentions/ImageExtention.js';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
import CodeExtended from './custom/code-extended.svelte';
|
||||
|
||||
// Lowlight
|
||||
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
||||
import { all, createLowlight } from 'lowlight';
|
||||
import './onedark.css';
|
||||
import SearchAndReplace from './custom/Extentions/SearchAndReplace.js';
|
||||
|
||||
const lowlight = createLowlight(all);
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
content?: Content;
|
||||
show_toolbar?: boolean;
|
||||
// html_text?: string;
|
||||
new_html?: string;
|
||||
placeholder?: string;
|
||||
show_button_kv?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
content = $bindable(''),
|
||||
show_toolbar = true,
|
||||
// html_text = '',
|
||||
new_html = $bindable(''),
|
||||
placeholder = $bindable('Start typing...'),
|
||||
show_button_kv = $bindable({})
|
||||
}: Props = $props();
|
||||
|
||||
let editor = $state<Editor>();
|
||||
let element = $state<HTMLElement>();
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
'm-auto p-2 focus:outline-hidden flex-1 prose text-foreground min-w-full max-h-full overflow-auto dark:prose-invert *:my-2'
|
||||
}
|
||||
},
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: 'list-decimal'
|
||||
}
|
||||
},
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: 'list-disc'
|
||||
}
|
||||
},
|
||||
heading: {
|
||||
levels: [1, 2, 3, 4],
|
||||
HTMLAttributes: {
|
||||
class: 'tiptap-heading'
|
||||
}
|
||||
}
|
||||
}),
|
||||
Typography,
|
||||
Text,
|
||||
TextStyle,
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph']
|
||||
}),
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true }),
|
||||
Underline,
|
||||
Superscript,
|
||||
Subscript,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: 'https',
|
||||
HTMLAttributes: {
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}
|
||||
}),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true
|
||||
}),
|
||||
SearchAndReplace,
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight
|
||||
}).extend({
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(CodeExtended);
|
||||
}
|
||||
}),
|
||||
SmilieReplacer,
|
||||
ColorHighlighter,
|
||||
Table.configure({
|
||||
allowTableNodeSelection: true,
|
||||
resizable: true
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
ImageExtension
|
||||
],
|
||||
autofocus: false, // This should be changed to a prop in the future
|
||||
onTransaction: (transaction) => {
|
||||
/**
|
||||
* Weird behavior of editor.
|
||||
* If we do not make it undefined, then it looses it's reactivity
|
||||
* this is because assigning editor directly to `transaction.editor`
|
||||
* the original object is not mutated.
|
||||
*/
|
||||
editor = undefined;
|
||||
editor = transaction.editor;
|
||||
// console.log(editor.isActive('bold'));
|
||||
content = editor.getHTML();
|
||||
// console.log(content);
|
||||
let html = editor.getHTML();
|
||||
if (html == '<p></p>') {
|
||||
new_html = '';
|
||||
} else {
|
||||
new_html = html ?? '';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) editor.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class={cn('flex flex-col rounded border textarea editor', className)}>
|
||||
{#if editor && show_toolbar}
|
||||
<EditorToolbar {editor} {show_button_kv} />
|
||||
{/if}
|
||||
<div bind:this={element} spellcheck="false" class="tiptap overflow-auto relative">
|
||||
<span
|
||||
class="placeholder text-sm text-gray-400 italic absolute p-3 w-full"
|
||||
contenteditable="false"
|
||||
hidden={editor?.getHTML() !== '<p></p>'}>{placeholder}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
<script lang="ts" module>
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
base: 'ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline: 'border-input bg-background hover:bg-accent hover:text-accent-foreground border',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
});
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
|
||||
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];
|
||||
|
||||
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||
WithElementRef<HTMLAnchorAttributes> & {
|
||||
@@ -39,27 +38,22 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
ref = $bindable(null),
|
||||
href = undefined,
|
||||
type = "button",
|
||||
type = 'button',
|
||||
children,
|
||||
...restProps
|
||||
}: ButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
<a bind:this={ref} class={cn(buttonVariants({ variant, size, className }))} {href} {...restProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{:else}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Root, { buttonVariants } from "./button.svelte";
|
||||
import Root, { buttonVariants } from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
|
||||
//
|
||||
Root as Button,
|
||||
buttonVariants,
|
||||
buttonVariants
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import Root, {
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
buttonVariants,
|
||||
} from "./button.svelte";
|
||||
buttonVariants
|
||||
} from './button.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -13,5 +13,5 @@ export {
|
||||
buttonVariants,
|
||||
type ButtonProps,
|
||||
type ButtonSize,
|
||||
type ButtonVariant,
|
||||
type ButtonVariant
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import Minus from "lucide-svelte/icons/minus";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import type { Snippet } from "svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||
import Check from 'lucide-svelte/icons/check';
|
||||
import Minus from 'lucide-svelte/icons/minus';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -22,7 +22,7 @@
|
||||
bind:checked
|
||||
bind:indeterminate
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
@@ -32,7 +32,7 @@
|
||||
{#if indeterminate}
|
||||
<Minus class="size-4" />
|
||||
{:else}
|
||||
<Check class={cn("size-4", !checked && "text-transparent")} />
|
||||
<Check class={cn('size-4', !checked && 'text-transparent')} />
|
||||
{/if}
|
||||
</span>
|
||||
{@render childrenProp?.()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -18,7 +18,7 @@
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md outline-hidden",
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 overflow-hidden rounded-md border p-1 shadow-md outline-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,6 +14,6 @@
|
||||
|
||||
<DropdownMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -15,8 +15,8 @@
|
||||
<DropdownMenuPrimitive.Item
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { type WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { type WithElementRef } from 'bits-ui';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from "bits-ui";
|
||||
import Circle from "lucide-svelte/icons/circle";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive, type WithoutChild } from 'bits-ui';
|
||||
import Circle from 'lucide-svelte/icons/circle';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,7 +14,7 @@
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
<DropdownMenuPrimitive.Separator
|
||||
bind:ref
|
||||
class={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
class={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { type WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { type WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
class={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
class={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -12,7 +12,7 @@
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground z-50 min-w-32 rounded-md border p-1 shadow-lg focus:outline-hidden",
|
||||
'bg-popover text-popover-foreground z-50 min-w-32 rounded-md border p-1 shadow-lg focus:outline-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import ChevronRight from 'lucide-svelte/icons/chevron-right';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -17,8 +17,8 @@
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
'data-[highlighted]:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||
import Content from './dropdown-menu-content.svelte';
|
||||
import GroupHeading from './dropdown-menu-group-heading.svelte';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
import Label from './dropdown-menu-label.svelte';
|
||||
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||
import Separator from './dropdown-menu-separator.svelte';
|
||||
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
@@ -46,5 +46,5 @@ export {
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
Trigger
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
|
||||
import Content from "./dropdown-menu-content.svelte";
|
||||
import GroupHeading from "./dropdown-menu-group-heading.svelte";
|
||||
import Item from "./dropdown-menu-item.svelte";
|
||||
import Label from "./dropdown-menu-label.svelte";
|
||||
import RadioItem from "./dropdown-menu-radio-item.svelte";
|
||||
import Separator from "./dropdown-menu-separator.svelte";
|
||||
import Shortcut from "./dropdown-menu-shortcut.svelte";
|
||||
import SubContent from "./dropdown-menu-sub-content.svelte";
|
||||
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import CheckboxItem from './dropdown-menu-checkbox-item.svelte';
|
||||
import Content from './dropdown-menu-content.svelte';
|
||||
import GroupHeading from './dropdown-menu-group-heading.svelte';
|
||||
import Item from './dropdown-menu-item.svelte';
|
||||
import Label from './dropdown-menu-label.svelte';
|
||||
import RadioItem from './dropdown-menu-radio-item.svelte';
|
||||
import Separator from './dropdown-menu-separator.svelte';
|
||||
import Shortcut from './dropdown-menu-shortcut.svelte';
|
||||
import SubContent from './dropdown-menu-sub-content.svelte';
|
||||
import SubTrigger from './dropdown-menu-sub-trigger.svelte';
|
||||
|
||||
const Sub = DropdownMenuPrimitive.Sub;
|
||||
const Root = DropdownMenuPrimitive.Root;
|
||||
@@ -46,5 +46,5 @@ export {
|
||||
Sub,
|
||||
SubContent,
|
||||
SubTrigger,
|
||||
Trigger,
|
||||
Trigger
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
Root as Input
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
Root as Input
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
import type { WithElementRef } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,7 +14,7 @@
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import Content from './popover-content.svelte';
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Trigger = PopoverPrimitive.Trigger;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
@@ -13,5 +13,5 @@ export {
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Close as PopoverClose
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import Content from "./popover-content.svelte";
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import Content from './popover-content.svelte';
|
||||
const Root = PopoverPrimitive.Root;
|
||||
const Trigger = PopoverPrimitive.Trigger;
|
||||
const Close = PopoverPrimitive.Close;
|
||||
@@ -13,5 +13,5 @@ export {
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Close as PopoverClose
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
align = 'center',
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
@@ -20,7 +20,7 @@
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden",
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-hidden',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
Root as Separator
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
Root as Separator
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Separator as SeparatorPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
orientation = "horizontal",
|
||||
orientation = 'horizontal',
|
||||
...restProps
|
||||
}: SeparatorPrimitive.RootProps = $props();
|
||||
</script>
|
||||
@@ -13,8 +13,8 @@
|
||||
<SeparatorPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
"bg-border shrink-0",
|
||||
orientation === "horizontal" ? "h-px w-full" : "min-h-full w-px",
|
||||
'bg-border shrink-0',
|
||||
orientation === 'horizontal' ? 'h-px w-full' : 'min-h-full w-px',
|
||||
className
|
||||
)}
|
||||
{orientation}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import Content from "./tooltip-content.svelte";
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import Content from './tooltip-content.svelte';
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Trigger = TooltipPrimitive.Trigger;
|
||||
@@ -14,5 +14,5 @@ export {
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider,
|
||||
Provider as TooltipProvider
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import Content from "./tooltip-content.svelte";
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import Content from './tooltip-content.svelte';
|
||||
|
||||
const Root = TooltipPrimitive.Root;
|
||||
const Trigger = TooltipPrimitive.Trigger;
|
||||
@@ -14,5 +14,5 @@ export {
|
||||
Root as Tooltip,
|
||||
Content as TooltipContent,
|
||||
Trigger as TooltipTrigger,
|
||||
Provider as TooltipProvider,
|
||||
Provider as TooltipProvider
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Tooltip as TooltipPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils/utils.js";
|
||||
import { Tooltip as TooltipPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -14,7 +14,7 @@
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
|
||||
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
Reference in New Issue
Block a user