feat: migrate auth from supabase to better auth (#596)
* feat: add README for @cio/db package and remove existingAccounts.json * load session, load profile and organization, add create profile hook * chore: cache session request The trick was passing all the cookies to the backend. Previously I was only passing the better-auth.session_token and not better-auth.session_data. Read about the difference here https://www.better-auth.com/docs/concepts/cookies#custom-cookies * feat: login, logout, onboarding, verify email, reset password. * feat: complete change email and email verification * feat: standardise how we make rpc calls and create org endpoints * fix ui errors * fix: handle potential undefined customerId and standardize avatar URL property * refactor: standardize custom domain handling and update telegram chat ID references * chore: add tailwind prefix to @cio/ui this fixed the media query issues because @cio/ui classes were overriding this dashboard tailwind build * chore: clean up org sidebar * chore: persist mode state * feat: implement order breadcrumbs * chore: upgrade to better auth 1.4 * chore: address common auth flow bugs and setup app for others to setup locally.
This commit is contained in:
@@ -26,383 +26,372 @@ import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/p
|
||||
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 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;
|
||||
text: string;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => {
|
||||
return RegExp(
|
||||
disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s,
|
||||
caseSensitive ? 'gu' : 'gui'
|
||||
);
|
||||
return RegExp(disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s, caseSensitive ? 'gu' : 'gui');
|
||||
};
|
||||
|
||||
interface ProcessedSearches {
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
decorationsToReturn: DecorationSet;
|
||||
results: Range[];
|
||||
}
|
||||
|
||||
function processSearches(
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
doc: PMNode,
|
||||
searchTerm: RegExp,
|
||||
searchResultClass: string,
|
||||
resultIndex: number
|
||||
): ProcessedSearches {
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
const decorations: Decoration[] = [];
|
||||
const results: Range[] = [];
|
||||
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
let textNodesWithPosition: TextNodesWithPosition[] = [];
|
||||
let index = 0;
|
||||
|
||||
if (!searchTerm) {
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.empty,
|
||||
results: []
|
||||
};
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
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);
|
||||
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 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;
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
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);
|
||||
}
|
||||
decorations.push(decoration);
|
||||
}
|
||||
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
return {
|
||||
decorationsToReturn: DecorationSet.create(doc, decorations),
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
const replace = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ state, dispatch }: { state: EditorState; dispatch: Dispatch }
|
||||
) => {
|
||||
const firstResult = results[0];
|
||||
const firstResult = results[0];
|
||||
|
||||
if (!firstResult) return;
|
||||
if (!firstResult) return;
|
||||
|
||||
const { from, to } = results[0];
|
||||
const { from, to } = results[0];
|
||||
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
|
||||
};
|
||||
|
||||
const rebaseNextResult = (
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
replaceTerm: string,
|
||||
index: number,
|
||||
lastOffset: number,
|
||||
results: Range[]
|
||||
): [number, Range[]] | null => {
|
||||
const nextIndex = index + 1;
|
||||
const nextIndex = index + 1;
|
||||
|
||||
if (!results[nextIndex]) return null;
|
||||
if (!results[nextIndex]) return null;
|
||||
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
const { from: currentFrom, to: currentTo } = results[index];
|
||||
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
const offset = currentTo - currentFrom - replaceTerm.length + lastOffset;
|
||||
|
||||
const { from, to } = results[nextIndex];
|
||||
const { from, to } = results[nextIndex];
|
||||
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset
|
||||
};
|
||||
|
||||
return [offset, results];
|
||||
return [offset, results];
|
||||
};
|
||||
|
||||
const replaceAll = (
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
replaceTerm: string,
|
||||
results: Range[],
|
||||
{ tr, dispatch }: { tr: Transaction; dispatch: Dispatch }
|
||||
) => {
|
||||
let offset = 0;
|
||||
let offset = 0;
|
||||
|
||||
let resultsCopy = results.slice();
|
||||
let resultsCopy = results.slice();
|
||||
|
||||
if (!resultsCopy.length) return;
|
||||
if (!resultsCopy.length) return;
|
||||
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
for (let i = 0; i < resultsCopy.length; i += 1) {
|
||||
const { from, to } = resultsCopy[i];
|
||||
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
tr.insertText(replaceTerm, from, to);
|
||||
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy);
|
||||
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
if (!rebaseNextResultResponse) continue;
|
||||
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
offset = rebaseNextResultResponse[0];
|
||||
resultsCopy = rebaseNextResultResponse[1];
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(tr);
|
||||
}
|
||||
if (dispatch) {
|
||||
dispatch(tr);
|
||||
}
|
||||
};
|
||||
|
||||
export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin');
|
||||
|
||||
export interface SearchAndReplaceOptions {
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
searchResultClass: string;
|
||||
disableRegex: boolean;
|
||||
}
|
||||
|
||||
export interface SearchAndReplaceStorage {
|
||||
searchTerm: string;
|
||||
replaceTerm: string;
|
||||
results: Range[];
|
||||
lastSearchTerm: string;
|
||||
caseSensitive: boolean;
|
||||
lastCaseSensitive: boolean;
|
||||
resultIndex: number;
|
||||
lastResultIndex: number;
|
||||
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',
|
||||
name: 'searchAndReplace',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
searchResultClass: 'search-result',
|
||||
disableRegex: true
|
||||
};
|
||||
},
|
||||
|
||||
addStorage() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
replaceTerm: '',
|
||||
results: [],
|
||||
lastSearchTerm: '',
|
||||
caseSensitive: false,
|
||||
lastCaseSensitive: false,
|
||||
resultIndex: 0,
|
||||
lastResultIndex: 0
|
||||
};
|
||||
},
|
||||
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;
|
||||
addCommands() {
|
||||
return {
|
||||
setSearchTerm:
|
||||
(searchTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.searchTerm = searchTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
return false;
|
||||
},
|
||||
setReplaceTerm:
|
||||
(replaceTerm: string) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
|
||||
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
return false;
|
||||
},
|
||||
setCaseSensitive:
|
||||
(caseSensitive: boolean) =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
|
||||
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
return false;
|
||||
},
|
||||
resetIndex:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
nextSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const nextIndex = resultIndex + 1;
|
||||
const nextIndex = resultIndex + 1;
|
||||
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
if (results[nextIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = nextIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
previousSearchResult:
|
||||
() =>
|
||||
({ editor }) => {
|
||||
const { results, resultIndex } = editor.storage.searchAndReplace;
|
||||
|
||||
const prevIndex = resultIndex - 1;
|
||||
const prevIndex = resultIndex - 1;
|
||||
|
||||
if (results[prevIndex]) {
|
||||
editor.storage.searchAndReplace.resultIndex = prevIndex;
|
||||
} else {
|
||||
editor.storage.searchAndReplace.resultIndex = results.length - 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;
|
||||
return false;
|
||||
},
|
||||
replace:
|
||||
() =>
|
||||
({ editor, state, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
replace(replaceTerm, results, { state, dispatch });
|
||||
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
return false;
|
||||
},
|
||||
replaceAll:
|
||||
() =>
|
||||
({ editor, tr, dispatch }) => {
|
||||
const { replaceTerm, results } = editor.storage.searchAndReplace;
|
||||
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
replaceAll(replaceTerm, results, { tr, dispatch });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const editor = this.editor;
|
||||
const { searchResultClass, disableRegex } = this.options;
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
if (
|
||||
!docChanged &&
|
||||
lastSearchTerm === searchTerm &&
|
||||
lastCaseSensitive === caseSensitive &&
|
||||
lastResultIndex === resultIndex
|
||||
)
|
||||
return oldState;
|
||||
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
setLastSearchTerm(searchTerm);
|
||||
setLastCaseSensitive(caseSensitive);
|
||||
setLastResultIndex(resultIndex);
|
||||
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
if (!searchTerm) {
|
||||
editor.storage.searchAndReplace.results = [];
|
||||
return DecorationSet.empty;
|
||||
}
|
||||
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
const { decorationsToReturn, results } = processSearches(
|
||||
doc,
|
||||
getRegex(searchTerm, disableRegex, caseSensitive),
|
||||
searchResultClass,
|
||||
resultIndex
|
||||
);
|
||||
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
editor.storage.searchAndReplace.results = results;
|
||||
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return decorationsToReturn;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchAndReplace;
|
||||
|
||||
@@ -2,146 +2,137 @@ import { Node, nodeInputRule } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
export interface AudioOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
audio: {
|
||||
/**
|
||||
* Set a audio node
|
||||
*/
|
||||
setAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a audio
|
||||
*/
|
||||
toggleAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a audio
|
||||
*/
|
||||
removeAudio: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
audio: {
|
||||
/**
|
||||
* Set a audio node
|
||||
*/
|
||||
setAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a audio
|
||||
*/
|
||||
toggleAudio: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a audio
|
||||
*/
|
||||
removeAudio: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const AUDIO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
||||
|
||||
export const Audio = Node.create<AudioOptions>({
|
||||
name: 'audio',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
atom: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'audio',
|
||||
getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
name: 'audio',
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
atom: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'audio',
|
||||
getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'audio',
|
||||
{ controls: 'true', style: 'width: 100%;', ...HTMLAttributes },
|
||||
['source', HTMLAttributes]
|
||||
];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setAudio:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(
|
||||
`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`
|
||||
),
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['audio', { controls: 'true', style: 'width: 100%;', ...HTMLAttributes }, ['source', HTMLAttributes]];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setAudio:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(`<audio controls autoplay="false" style="width: 100%;" src="${src}"></audio>`),
|
||||
|
||||
toggleAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: AUDIO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
toggleAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeAudio:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: AUDIO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('audioDropPlugin'),
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('audioDropPlugin'),
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles =
|
||||
event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
|
||||
if (!hasFiles) return false;
|
||||
if (!hasFiles) return false;
|
||||
|
||||
const audios = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
/audio/i.test(file.type)
|
||||
);
|
||||
const audios = Array.from(event.dataTransfer.files).filter((file) => /audio/i.test(file.type));
|
||||
|
||||
if (audios.length === 0) return false;
|
||||
if (audios.length === 0) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
|
||||
audios.forEach((audio) => {
|
||||
const reader = new FileReader();
|
||||
audios.forEach((audio) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.audio.create({ src: readerEvent.target?.result });
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.audio.create({ src: readerEvent.target?.result });
|
||||
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(audio);
|
||||
});
|
||||
reader.readAsDataURL(audio);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,62 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface AudioPlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
audioPlaceholder: {
|
||||
/**
|
||||
* Inserts an audio placeholder
|
||||
*/
|
||||
insertAudioPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
audioPlaceholder: {
|
||||
/**
|
||||
* Inserts an audio placeholder
|
||||
*/
|
||||
insertAudioPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const AudioPlaceholder = (
|
||||
component: Component<NodeViewProps>
|
||||
): Node<AudioPlaceholderOptions> =>
|
||||
Node.create<AudioPlaceholderOptions>({
|
||||
name: 'audio-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
export const AudioPlaceholder = (component: Component<NodeViewProps>): Node<AudioPlaceholderOptions> =>
|
||||
Node.create<AudioPlaceholderOptions>({
|
||||
name: 'audio-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertAudioPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'audio-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertAudioPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'audio-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,26 +3,26 @@ import { EditorView } from '@tiptap/pm/view';
|
||||
import * as pmView from '@tiptap/pm/view';
|
||||
|
||||
function getPmView() {
|
||||
try {
|
||||
return pmView;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return pmView;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeForClipboard(view: EditorView, slice: Slice) {
|
||||
// Newer Tiptap/ProseMirror
|
||||
if (view && typeof view.serializeForClipboard === 'function') {
|
||||
return view.serializeForClipboard(slice);
|
||||
}
|
||||
// Newer Tiptap/ProseMirror
|
||||
if (view && typeof view.serializeForClipboard === 'function') {
|
||||
return view.serializeForClipboard(slice);
|
||||
}
|
||||
|
||||
// Older version fallback
|
||||
const proseMirrorView = getPmView();
|
||||
// Older version fallback
|
||||
const proseMirrorView = getPmView();
|
||||
|
||||
if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === 'function') {
|
||||
return proseMirrorView.__serializeForClipboard(view, slice);
|
||||
}
|
||||
if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === 'function') {
|
||||
return proseMirrorView.__serializeForClipboard(view, slice);
|
||||
}
|
||||
|
||||
throw new Error('No supported clipboard serialization method found.');
|
||||
throw new Error('No supported clipboard serialization method found.');
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { Node } from '@tiptap/core';
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean;
|
||||
HTMLAttributes: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
allowFullscreen: boolean;
|
||||
HTMLAttributes: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
/**
|
||||
* Add an iframe with src
|
||||
*/
|
||||
setIframe: (options: { src: string }) => ReturnType;
|
||||
removeIframe: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
/**
|
||||
* Add an iframe with src
|
||||
*/
|
||||
setIframe: (options: { src: string }) => ReturnType;
|
||||
removeIframe: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper'
|
||||
}
|
||||
};
|
||||
},
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
frameborder: {
|
||||
default: 0
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen
|
||||
}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null
|
||||
},
|
||||
frameborder: {
|
||||
default: 0
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe'
|
||||
}
|
||||
];
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe'
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]];
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create(options);
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr;
|
||||
const node = this.type.create(options);
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
}
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
removeIframe:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
}
|
||||
return true;
|
||||
},
|
||||
removeIframe:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,54 +3,54 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface IFramePlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframePlaceholder: {
|
||||
/**
|
||||
* Inserts a IFrame placeholder
|
||||
*/
|
||||
insertIFramePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
iframePlaceholder: {
|
||||
/**
|
||||
* Inserts a IFrame placeholder
|
||||
*/
|
||||
insertIFramePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const IFramePlaceholder = (content: Component<NodeViewProps>) =>
|
||||
Node.create<IFramePlaceholderOptions>({
|
||||
name: 'iframe-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
Node.create<IFramePlaceholderOptions>({
|
||||
name: 'iframe-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertIFramePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'iframe-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertIFramePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'iframe-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,33 +4,33 @@ import type { Component } from 'svelte';
|
||||
import type { NodeViewProps, Node } from '@tiptap/core';
|
||||
|
||||
export const ImageExtended = (component: Component<NodeViewProps>): Node<ImageOptions, unknown> => {
|
||||
return 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(component);
|
||||
}
|
||||
}).configure({
|
||||
allowBase64: true
|
||||
});
|
||||
return 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(component);
|
||||
}
|
||||
}).configure({
|
||||
allowBase64: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,62 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface ImagePlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
imagePlaceholder: {
|
||||
/**
|
||||
* Inserts an image placeholder
|
||||
*/
|
||||
insertImagePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
imagePlaceholder: {
|
||||
/**
|
||||
* Inserts an image placeholder
|
||||
*/
|
||||
insertImagePlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ImagePlaceholder = (
|
||||
component: Component<NodeViewProps>
|
||||
): Node<ImagePlaceholderOptions> =>
|
||||
Node.create<ImagePlaceholderOptions>({
|
||||
name: 'image-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
export const ImagePlaceholder = (component: Component<NodeViewProps>): Node<ImagePlaceholderOptions> =>
|
||||
Node.create<ImagePlaceholderOptions>({
|
||||
name: 'image-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertImagePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'image-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(component);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertImagePlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'image-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TableRow as TiptapTableRow } from '@tiptap/extension-table';
|
||||
|
||||
export const TableRow = TiptapTableRow.extend({
|
||||
allowGapCursor: false,
|
||||
content: '(tableCell | tableHeader)*'
|
||||
allowGapCursor: false,
|
||||
content: '(tableCell | tableHeader)*'
|
||||
});
|
||||
|
||||
export default TableRow;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Table as TiptapTable } from '@tiptap/extension-table';
|
||||
|
||||
export const Table = TiptapTable.configure({
|
||||
resizable: true,
|
||||
lastColumnResizable: true,
|
||||
allowTableNodeSelection: true
|
||||
resizable: true,
|
||||
lastColumnResizable: true,
|
||||
allowTableNodeSelection: true
|
||||
});
|
||||
|
||||
export default Table;
|
||||
|
||||
@@ -2,146 +2,137 @@ import { Node, nodeInputRule } from '@tiptap/core';
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
export interface VideoOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
/**
|
||||
* Set a video node
|
||||
*/
|
||||
setVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a video
|
||||
*/
|
||||
toggleVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a video
|
||||
*/
|
||||
removeVideo: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
video: {
|
||||
/**
|
||||
* Set a video node
|
||||
*/
|
||||
setVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Toggle a video
|
||||
*/
|
||||
toggleVideo: (src: string) => ReturnType;
|
||||
/**
|
||||
* Remove a video
|
||||
*/
|
||||
removeVideo: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/;
|
||||
|
||||
export const Video = Node.create<VideoOptions>({
|
||||
name: 'video',
|
||||
group: 'block',
|
||||
content: 'inline*',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'video',
|
||||
getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
name: 'video',
|
||||
group: 'block',
|
||||
content: 'inline*',
|
||||
draggable: true,
|
||||
isolating: true,
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
};
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'),
|
||||
renderHTML: (attrs) => ({ src: attrs.src })
|
||||
}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'video',
|
||||
getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') })
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'video',
|
||||
{ controls: 'true', style: 'width: fit-content;', ...HTMLAttributes },
|
||||
['source', HTMLAttributes]
|
||||
];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(
|
||||
`<video controls="true" autoplay="false" style="width: fit-content" src="${src}" />`
|
||||
),
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['video', { controls: 'true', style: 'width: fit-content;', ...HTMLAttributes }, ['source', HTMLAttributes]];
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setVideo:
|
||||
(src: string) =>
|
||||
({ commands }) =>
|
||||
commands.insertContent(`<video controls="true" autoplay="false" style="width: fit-content" src="${src}" />`),
|
||||
|
||||
toggleVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: VIDEO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
toggleVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.toggleNode(this.name, 'paragraph'),
|
||||
removeVideo:
|
||||
() =>
|
||||
({ commands }) =>
|
||||
commands.deleteNode(this.name)
|
||||
};
|
||||
},
|
||||
addInputRules() {
|
||||
return [
|
||||
nodeInputRule({
|
||||
find: VIDEO_INPUT_REGEX,
|
||||
type: this.type,
|
||||
getAttributes: (match) => {
|
||||
const [, , src] = match;
|
||||
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('videoDropPlugin'),
|
||||
return { src };
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey('videoDropPlugin'),
|
||||
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles =
|
||||
event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
drop(view, event) {
|
||||
const {
|
||||
state: { schema, tr },
|
||||
dispatch
|
||||
} = view;
|
||||
const hasFiles = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length;
|
||||
|
||||
if (!hasFiles) return false;
|
||||
if (!hasFiles) return false;
|
||||
|
||||
const videos = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
/video/i.test(file.type)
|
||||
);
|
||||
const videos = Array.from(event.dataTransfer.files).filter((file) => /video/i.test(file.type));
|
||||
|
||||
if (videos.length === 0) return false;
|
||||
if (videos.length === 0) return false;
|
||||
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
||||
|
||||
videos.forEach((video) => {
|
||||
const reader = new FileReader();
|
||||
videos.forEach((video) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.video.create({ src: readerEvent.target?.result });
|
||||
reader.onload = (readerEvent) => {
|
||||
const node = schema.nodes.video.create({ src: readerEvent.target?.result });
|
||||
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
if (coordinates && typeof coordinates.pos === 'number') {
|
||||
const transaction = tr.insert(coordinates?.pos, node);
|
||||
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
dispatch(transaction);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsDataURL(video);
|
||||
});
|
||||
reader.readAsDataURL(video);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,60 +3,60 @@ import type { Component } from 'svelte';
|
||||
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
||||
|
||||
export interface VideoPlaceholderOptions {
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
HTMLAttributes: Record<string, object>;
|
||||
onDrop: (files: File[], editor: Editor) => void;
|
||||
onDropRejected?: (files: File[], editor: Editor) => void;
|
||||
onEmbed: (url: string, editor: Editor) => void;
|
||||
allowedMimeTypes?: Record<string, string[]>;
|
||||
maxFiles?: number;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
videoPlaceholder: {
|
||||
/**
|
||||
* Inserts a video placeholder
|
||||
*/
|
||||
insertVideoPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
interface Commands<ReturnType> {
|
||||
videoPlaceholder: {
|
||||
/**
|
||||
* Inserts a video placeholder
|
||||
*/
|
||||
insertVideoPlaceholder: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const VideoPlaceholder = (content: Component<NodeViewProps>) =>
|
||||
Node.create<VideoPlaceholderOptions>({
|
||||
name: 'video-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
Node.create<VideoPlaceholderOptions>({
|
||||
name: 'video-placeholder',
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
onDrop: () => {},
|
||||
onDropRejected: () => {},
|
||||
onEmbed: () => {}
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: `div[data-type="${this.name}"]` }];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['div', mergeAttributes(HTMLAttributes)];
|
||||
},
|
||||
group: 'block',
|
||||
draggable: true,
|
||||
atom: true,
|
||||
content: 'inline*',
|
||||
isolating: true,
|
||||
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertVideoPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'video-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
addNodeView() {
|
||||
return SvelteNodeViewRenderer(content);
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
insertVideoPlaceholder: () => (props: CommandProps) => {
|
||||
return props.commands.insertContent({
|
||||
type: 'video-placeholder'
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user