67
common/resources/client/text-editor/extensions/background-color.ts
Executable file
67
common/resources/client/text-editor/extensions/background-color.ts
Executable file
@@ -0,0 +1,67 @@
|
||||
import {Extension} from '@tiptap/core';
|
||||
import '@tiptap/extension-text-style';
|
||||
|
||||
export type ColorOptions = {
|
||||
types: string[];
|
||||
};
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
bgColor: {
|
||||
setBackgroundColor: (color: string) => ReturnType;
|
||||
unsetBackgroundColor: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const BackgroundColor = Extension.create<ColorOptions>({
|
||||
name: 'backgroundColor',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
backgroundColor: {
|
||||
default: null,
|
||||
parseHTML: element =>
|
||||
element.style.backgroundColor.replace(/['"]+/g, ''),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.backgroundColor) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
style: `background-color: ${attributes.backgroundColor}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setBackgroundColor:
|
||||
backgroundColor =>
|
||||
({chain}) => {
|
||||
return chain().setMark('textStyle', {backgroundColor}).run();
|
||||
},
|
||||
unsetBackgroundColor:
|
||||
() =>
|
||||
({chain}) => {
|
||||
return chain()
|
||||
.setMark('textStyle', {backgroundColor: null})
|
||||
.removeEmptyTextStyle()
|
||||
.run();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
57
common/resources/client/text-editor/extensions/embed.ts
Executable file
57
common/resources/client/text-editor/extensions/embed.ts
Executable file
@@ -0,0 +1,57 @@
|
||||
import {mergeAttributes, Node} from '@tiptap/react';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
embed: {
|
||||
setEmbed: (options: {src: string}) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Embed = Node.create({
|
||||
name: 'embed',
|
||||
group: 'block',
|
||||
atom: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
allowfullscreen: {
|
||||
default: null,
|
||||
},
|
||||
allow: {
|
||||
default: 'autoplay; fullscreen; picture-in-picture',
|
||||
},
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'iframe',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({HTMLAttributes}) {
|
||||
return [
|
||||
'iframe',
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setEmbed:
|
||||
options =>
|
||||
({commands}) => {
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: options,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
134
common/resources/client/text-editor/extensions/indent.ts
Executable file
134
common/resources/client/text-editor/extensions/indent.ts
Executable file
@@ -0,0 +1,134 @@
|
||||
import {Command, Extension} from '@tiptap/core';
|
||||
import {AllSelection, TextSelection, Transaction} from 'prosemirror-state';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
indent: {
|
||||
indent: () => ReturnType;
|
||||
outdent: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const Indent = Extension.create({
|
||||
name: 'indent',
|
||||
|
||||
addOptions: () => {
|
||||
return {
|
||||
types: ['listItem', 'paragraph'],
|
||||
minLevel: 0,
|
||||
maxLevel: 6,
|
||||
};
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
indent: {
|
||||
renderHTML: attributes => {
|
||||
return attributes?.indent > this.options.minLevel
|
||||
? {'data-indent': attributes.indent}
|
||||
: null;
|
||||
},
|
||||
parseHTML: element => {
|
||||
const level = Number(element.getAttribute('data-indent'));
|
||||
return level && level > this.options.minLevel ? level : null;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
const setNodeIndentMarkup = (
|
||||
tr: Transaction,
|
||||
pos: number,
|
||||
delta: number
|
||||
): Transaction => {
|
||||
const node = tr?.doc?.nodeAt(pos);
|
||||
|
||||
if (node) {
|
||||
const nextLevel = (node.attrs.indent || 0) + delta;
|
||||
const {minLevel, maxLevel} = this.options;
|
||||
const indent =
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
nextLevel < minLevel
|
||||
? minLevel
|
||||
: nextLevel > maxLevel
|
||||
? maxLevel
|
||||
: nextLevel;
|
||||
|
||||
if (indent !== node.attrs.indent) {
|
||||
const {indent: oldIndent, ...currentAttrs} = node.attrs;
|
||||
const nodeAttrs =
|
||||
indent > minLevel ? {...currentAttrs, indent} : currentAttrs;
|
||||
return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks);
|
||||
}
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
const updateIndentLevel = (tr: Transaction, delta: number): Transaction => {
|
||||
const {doc, selection} = tr;
|
||||
|
||||
if (
|
||||
doc &&
|
||||
selection &&
|
||||
(selection instanceof TextSelection ||
|
||||
selection instanceof AllSelection)
|
||||
) {
|
||||
const {from, to} = selection;
|
||||
doc.nodesBetween(from, to, (node, pos) => {
|
||||
if (this.options.types.includes(node.type.name)) {
|
||||
tr = setNodeIndentMarkup(tr, pos, delta);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return tr;
|
||||
};
|
||||
const applyIndent: (direction: number) => () => Command =
|
||||
direction =>
|
||||
() =>
|
||||
({tr, state, dispatch}) => {
|
||||
const {selection} = state;
|
||||
tr = tr.setSelection(selection);
|
||||
tr = updateIndentLevel(tr, direction);
|
||||
|
||||
if (tr.docChanged) {
|
||||
dispatch?.(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
indent: applyIndent(1),
|
||||
outdent: applyIndent(-1),
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: ({editor}) => {
|
||||
if (editor.state.selection.to > editor.state.selection.from) {
|
||||
return this.editor.commands.indent();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
'Shift-Tab': ({editor}) => {
|
||||
if (editor.state.selection.to > editor.state.selection.from) {
|
||||
return this.editor.commands.outdent();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
76
common/resources/client/text-editor/extensions/info-block.ts
Executable file
76
common/resources/client/text-editor/extensions/info-block.ts
Executable file
@@ -0,0 +1,76 @@
|
||||
import {mergeAttributes, Node} from '@tiptap/react';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
important: {
|
||||
addInfo: (attributes: {
|
||||
type: 'important' | 'warning' | 'success';
|
||||
}) => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const InfoBlock = Node.create({
|
||||
name: 'be-info-block',
|
||||
group: 'block',
|
||||
content: 'inline*',
|
||||
defining: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['important', 'warning', 'success'],
|
||||
defaultType: 'success',
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
type: {
|
||||
default: this.options.defaultType,
|
||||
parseHTML: element => {
|
||||
for (const type of this.options.types) {
|
||||
if (element.classList.contains(type)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return this.options.defaultType;
|
||||
},
|
||||
renderHTML: attrs => {
|
||||
return {class: attrs.type};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div',
|
||||
contentElement: 'p',
|
||||
getAttrs: node =>
|
||||
(node as HTMLElement).classList.contains('info-block') && null,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({HTMLAttributes}) {
|
||||
return [
|
||||
'div',
|
||||
mergeAttributes(HTMLAttributes, {
|
||||
class: 'info-block',
|
||||
}),
|
||||
['div', {class: 'title'}, 'Important:'],
|
||||
['p', 0],
|
||||
];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
addInfo:
|
||||
attributes =>
|
||||
({commands}) => {
|
||||
return commands.setNode(this.name, attributes);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
51
common/resources/client/text-editor/highlight/editor-styles.css
vendored
Executable file
51
common/resources/client/text-editor/highlight/editor-styles.css
vendored
Executable file
@@ -0,0 +1,51 @@
|
||||
.ProseMirror {
|
||||
@apply outline-none min-h-inherit;
|
||||
}
|
||||
|
||||
.ProseMirror img.ProseMirror-selectednode,
|
||||
.ProseMirror iframe.ProseMirror-selectednode {
|
||||
@apply ring-primary-light rounded;
|
||||
}
|
||||
|
||||
.prose [data-indent='1'] {
|
||||
@apply pl-20;
|
||||
}
|
||||
.prose [data-indent='2'] {
|
||||
@apply pl-40;
|
||||
}
|
||||
.prose [data-indent='3'] {
|
||||
@apply pl-60;
|
||||
}
|
||||
.prose [data-indent='4'] {
|
||||
@apply pl-80;
|
||||
}
|
||||
.prose [data-indent='5'] {
|
||||
@apply pl-100;
|
||||
}
|
||||
.prose [data-indent='6'] {
|
||||
@apply pl-128;
|
||||
}
|
||||
|
||||
.prose iframe {
|
||||
@apply w-full aspect-video;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
@apply text-base bg-positive/hover border-l border-l-4 border-l-positive/20 break-words rounded my-32 p-14 max-w-full border-l-4 border-l-positive;
|
||||
}
|
||||
|
||||
.info-block .title {
|
||||
@apply font-medium text-base text-[#43484d];
|
||||
}
|
||||
|
||||
.info-block.important {
|
||||
@apply bg-[#f3a432]/hover border-l-[#f3a432];
|
||||
}
|
||||
|
||||
.info-block.warning {
|
||||
@apply bg-danger/hover border-l-danger;
|
||||
}
|
||||
|
||||
.info-block p {
|
||||
@apply mt-6 mb-0;
|
||||
}
|
||||
127
common/resources/client/text-editor/highlight/github-dark-theme.css
vendored
Executable file
127
common/resources/client/text-editor/highlight/github-dark-theme.css
vendored
Executable file
@@ -0,0 +1,127 @@
|
||||
/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/
|
||||
|
||||
.hljs-dark {
|
||||
.hljs {
|
||||
color: #c9d1d9;
|
||||
background: #0d1117;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
/* prettylights-syntax-keyword */
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
/* prettylights-syntax-entity */
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-variable,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
/* prettylights-syntax-constant */
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
/* prettylights-syntax-string */
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
/* prettylights-syntax-variable */
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-code,
|
||||
.hljs-formula {
|
||||
/* prettylights-syntax-comment */
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-tag,
|
||||
.hljs-selector-pseudo {
|
||||
/* prettylights-syntax-entity-tag */
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
/* prettylights-syntax-storage-modifier-import */
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.hljs-section {
|
||||
/* prettylights-syntax-markup-heading */
|
||||
color: #1f6feb;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-bullet {
|
||||
/* prettylights-syntax-markup-list */
|
||||
color: #f2cc60;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
/* prettylights-syntax-markup-italic */
|
||||
color: #c9d1d9;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
/* prettylights-syntax-markup-bold */
|
||||
color: #c9d1d9;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
/* prettylights-syntax-markup-inserted */
|
||||
color: #aff5b4;
|
||||
background-color: #033a16;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
/* prettylights-syntax-markup-deleted */
|
||||
color: #ffdcd7;
|
||||
background-color: #67060c;
|
||||
}
|
||||
|
||||
.hljs-char.escape_,
|
||||
.hljs-link,
|
||||
.hljs-params,
|
||||
.hljs-property,
|
||||
.hljs-punctuation,
|
||||
.hljs-tag {
|
||||
/* purposely ignored */
|
||||
}
|
||||
}
|
||||
127
common/resources/client/text-editor/highlight/github-theme.css
vendored
Executable file
127
common/resources/client/text-editor/highlight/github-theme.css
vendored
Executable file
@@ -0,0 +1,127 @@
|
||||
/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/
|
||||
|
||||
.hljs-light {
|
||||
.hljs {
|
||||
color: #24292e;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-variable.language_ {
|
||||
/* prettylights-syntax-keyword */
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-title.class_,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-title.function_ {
|
||||
/* prettylights-syntax-entity */
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-attribute,
|
||||
.hljs-literal,
|
||||
.hljs-meta,
|
||||
.hljs-number,
|
||||
.hljs-operator,
|
||||
.hljs-variable,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-id {
|
||||
/* prettylights-syntax-constant */
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.hljs-regexp,
|
||||
.hljs-string,
|
||||
.hljs-meta .hljs-string {
|
||||
/* prettylights-syntax-string */
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-symbol {
|
||||
/* prettylights-syntax-variable */
|
||||
color: #e36209;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-code,
|
||||
.hljs-formula {
|
||||
/* prettylights-syntax-comment */
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.hljs-name,
|
||||
.hljs-quote,
|
||||
.hljs-selector-tag,
|
||||
.hljs-selector-pseudo {
|
||||
/* prettylights-syntax-entity-tag */
|
||||
color: #22863a;
|
||||
}
|
||||
|
||||
.hljs-subst {
|
||||
/* prettylights-syntax-storage-modifier-import */
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.hljs-section {
|
||||
/* prettylights-syntax-markup-heading */
|
||||
color: #005cc5;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-bullet {
|
||||
/* prettylights-syntax-markup-list */
|
||||
color: #735c0f;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
/* prettylights-syntax-markup-italic */
|
||||
color: #24292e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
/* prettylights-syntax-markup-bold */
|
||||
color: #24292e;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
/* prettylights-syntax-markup-inserted */
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
/* prettylights-syntax-markup-deleted */
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
.hljs-char.escape_,
|
||||
.hljs-link,
|
||||
.hljs-params,
|
||||
.hljs-property,
|
||||
.hljs-punctuation,
|
||||
.hljs-tag {
|
||||
/* purposely ignored */
|
||||
}
|
||||
}
|
||||
19
common/resources/client/text-editor/highlight/highlight-code.ts
Executable file
19
common/resources/client/text-editor/highlight/highlight-code.ts
Executable file
@@ -0,0 +1,19 @@
|
||||
export function highlightAllCode(
|
||||
el: HTMLElement,
|
||||
themeMode: 'light' | 'dark' = 'dark',
|
||||
) {
|
||||
el.querySelectorAll('pre code').forEach(e => {
|
||||
highlightCode(e as HTMLElement, themeMode);
|
||||
});
|
||||
}
|
||||
|
||||
export async function highlightCode(
|
||||
el: HTMLElement,
|
||||
themeMode: 'light' | 'dark' = 'dark',
|
||||
) {
|
||||
const {hljs} = await import('@common/text-editor/highlight/highlight');
|
||||
if (!el.dataset.highlighted) {
|
||||
el.classList.add(themeMode === 'dark' ? 'hljs-dark' : 'hljs-light');
|
||||
hljs.highlightElement(el);
|
||||
}
|
||||
}
|
||||
110
common/resources/client/text-editor/highlight/highlight-material-palenight.css
vendored
Executable file
110
common/resources/client/text-editor/highlight/highlight-material-palenight.css
vendored
Executable file
@@ -0,0 +1,110 @@
|
||||
.hljs::selection,
|
||||
.hljs ::selection {
|
||||
background-color: #32374D;
|
||||
color: #959DCB
|
||||
}
|
||||
/* purposely do not highlight these things */
|
||||
.hljs-formula,
|
||||
.hljs-params,
|
||||
.hljs-property {
|
||||
|
||||
}
|
||||
/* base03 - #676E95 - Comments, Invisibles, Line Highlighting */
|
||||
.hljs-comment {
|
||||
color: #676E95
|
||||
}
|
||||
/* base04 - #8796B0 - Dark Foreground (Used for status bars) */
|
||||
.hljs-tag {
|
||||
color: #8796B0
|
||||
}
|
||||
/* base05 - #959DCB - Default Foreground, Caret, Delimiters, Operators */
|
||||
.hljs-subst,
|
||||
.hljs-punctuation,
|
||||
.hljs-operator {
|
||||
color: #959DCB
|
||||
}
|
||||
.hljs-operator {
|
||||
opacity: 0.7
|
||||
}
|
||||
/* base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted */
|
||||
.hljs-bullet,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-selector-tag,
|
||||
.hljs-name,
|
||||
.hljs-deletion {
|
||||
color: #F07178
|
||||
}
|
||||
/* base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url */
|
||||
.hljs-symbol,
|
||||
.hljs-number,
|
||||
.hljs-link,
|
||||
.hljs-attr,
|
||||
.hljs-variable.constant_,
|
||||
.hljs-literal {
|
||||
color: #F78C6C
|
||||
}
|
||||
/* base0A - Classes, Markup Bold, Search Text Background */
|
||||
.hljs-title,
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-title.class_ {
|
||||
color: #FFCB6B
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
color: #FFCB6B
|
||||
}
|
||||
/* base0B - Strings, Inherited Class, Markup Code, Diff Inserted */
|
||||
.hljs-code,
|
||||
.hljs-addition,
|
||||
.hljs-title.class_.inherited__,
|
||||
.hljs-string {
|
||||
color: #C3E88D
|
||||
}
|
||||
/* base0C - Support, Regular Expressions, Escape Characters, Markup Quotes */
|
||||
/* guessing */
|
||||
.hljs-built_in,
|
||||
.hljs-doctag,
|
||||
.hljs-quote,
|
||||
.hljs-keyword.hljs-atrule,
|
||||
.hljs-regexp {
|
||||
color: #89DDFF
|
||||
}
|
||||
/* base0D - Functions, Methods, Attribute IDs, Headings */
|
||||
.hljs-function .hljs-title,
|
||||
.hljs-attribute,
|
||||
.ruby .hljs-property,
|
||||
.hljs-title.function_,
|
||||
.hljs-section {
|
||||
color: #82AAFF
|
||||
}
|
||||
/* base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed */
|
||||
/* .hljs-selector-id, */
|
||||
/* .hljs-selector-class, */
|
||||
/* .hljs-selector-attr, */
|
||||
/* .hljs-selector-pseudo, */
|
||||
.hljs-type,
|
||||
.hljs-template-tag,
|
||||
.diff .hljs-meta,
|
||||
.hljs-keyword {
|
||||
color: #C792EA
|
||||
}
|
||||
.hljs-emphasis {
|
||||
color: #C792EA;
|
||||
font-style: italic
|
||||
}
|
||||
/* base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?> */
|
||||
/*
|
||||
prevent top level .keyword and .string scopes
|
||||
from leaking into meta by accident
|
||||
*/
|
||||
.hljs-meta,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #FF5370
|
||||
}
|
||||
/* for v10 compatible themes */
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta-keyword {
|
||||
font-weight: bold
|
||||
}
|
||||
32
common/resources/client/text-editor/highlight/highlight.ts
Executable file
32
common/resources/client/text-editor/highlight/highlight.ts
Executable file
@@ -0,0 +1,32 @@
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
|
||||
// load specific languages only
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import html from 'highlight.js/lib/languages/xml';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import php from 'highlight.js/lib/languages/php';
|
||||
import shell from 'highlight.js/lib/languages/shell';
|
||||
import bash from 'highlight.js/lib/languages/bash';
|
||||
import ruby from 'highlight.js/lib/languages/ruby';
|
||||
import python from 'highlight.js/lib/languages/python';
|
||||
import java from 'highlight.js/lib/languages/java';
|
||||
import c from 'highlight.js/lib/languages/c';
|
||||
|
||||
// load css
|
||||
import './github-theme.css';
|
||||
import './github-dark-theme.css';
|
||||
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
hljs.registerLanguage('html', html);
|
||||
hljs.registerLanguage('css', css);
|
||||
hljs.registerLanguage('php', php);
|
||||
hljs.registerLanguage('shell', shell);
|
||||
hljs.registerLanguage('bash', bash);
|
||||
hljs.registerLanguage('ruby', ruby);
|
||||
hljs.registerLanguage('python', python);
|
||||
hljs.registerLanguage('java', java);
|
||||
hljs.registerLanguage('c', c);
|
||||
|
||||
export {hljs};
|
||||
32
common/resources/client/text-editor/highlight/lowlight.ts
Executable file
32
common/resources/client/text-editor/highlight/lowlight.ts
Executable file
@@ -0,0 +1,32 @@
|
||||
import {createLowlight} from 'lowlight';
|
||||
|
||||
// load specific languages only
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import html from 'highlight.js/lib/languages/xml';
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import php from 'highlight.js/lib/languages/php';
|
||||
import shell from 'highlight.js/lib/languages/shell';
|
||||
import bash from 'highlight.js/lib/languages/bash';
|
||||
import ruby from 'highlight.js/lib/languages/ruby';
|
||||
import python from 'highlight.js/lib/languages/python';
|
||||
import java from 'highlight.js/lib/languages/java';
|
||||
import c from 'highlight.js/lib/languages/c';
|
||||
|
||||
// load css
|
||||
import './highlight-material-palenight.css';
|
||||
|
||||
const lowlight = createLowlight();
|
||||
lowlight.register('javascript', javascript);
|
||||
lowlight.register('typescript', typescript);
|
||||
lowlight.register('html', html);
|
||||
lowlight.register('css', css);
|
||||
lowlight.register('php', php);
|
||||
lowlight.register('shell', shell);
|
||||
lowlight.register('bash', bash);
|
||||
lowlight.register('ruby', ruby);
|
||||
lowlight.register('python', python);
|
||||
lowlight.register('java', java);
|
||||
lowlight.register('c', c);
|
||||
|
||||
export {lowlight};
|
||||
32
common/resources/client/text-editor/insert-link-into-text-editor.ts
Executable file
32
common/resources/client/text-editor/insert-link-into-text-editor.ts
Executable file
@@ -0,0 +1,32 @@
|
||||
import {Editor} from '@tiptap/react';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
target?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function insertLinkIntoTextEditor(
|
||||
editor: Editor,
|
||||
{text, target, href}: Props
|
||||
) {
|
||||
// no selection, insert new link with specified text
|
||||
if (editor.state.selection.empty && text) {
|
||||
editor.commands.insertContent(
|
||||
`<a href="${href}" target="${target}">${text}</a>`
|
||||
);
|
||||
} else if (!editor.state.selection.empty) {
|
||||
// no href provided, remove link from selection
|
||||
if (!href) {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
} else {
|
||||
// add link to selection
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange('link')
|
||||
.setLink({href: href, target})
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
common/resources/client/text-editor/menubar/align-buttons.tsx
Executable file
78
common/resources/client/text-editor/menubar/align-buttons.tsx
Executable file
@@ -0,0 +1,78 @@
|
||||
import clsx from 'clsx';
|
||||
import {ComponentType} from 'react';
|
||||
import {FormatAlignLeftIcon} from '../../icons/material/FormatAlignLeft';
|
||||
import {FormatAlignCenterIcon} from '../../icons/material/FormatAlignCenter';
|
||||
import {FormatAlignRightIcon} from '../../icons/material/FormatAlignRight';
|
||||
import {FormatAlignJustifyIcon} from '../../icons/material/FormatAlignJustify';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {message} from '../../i18n/message';
|
||||
|
||||
const iconMap = {
|
||||
left: {
|
||||
icon: FormatAlignLeftIcon,
|
||||
label: message('Align left'),
|
||||
},
|
||||
center: {
|
||||
icon: FormatAlignCenterIcon,
|
||||
label: message('Align center'),
|
||||
},
|
||||
right: {
|
||||
icon: FormatAlignRightIcon,
|
||||
label: message('Align right'),
|
||||
},
|
||||
justify: {
|
||||
icon: FormatAlignJustifyIcon,
|
||||
label: message('Justify'),
|
||||
},
|
||||
};
|
||||
|
||||
export function AlignButtons({editor, size}: MenubarButtonProps) {
|
||||
const activeKey = (Object.keys(iconMap).find(key => {
|
||||
return editor.isActive({textAlign: key});
|
||||
}) || 'left') as keyof typeof iconMap;
|
||||
const ActiveIcon: ComponentType = activeKey
|
||||
? iconMap[activeKey].icon
|
||||
: iconMap.left.icon;
|
||||
|
||||
return (
|
||||
<MenuTrigger
|
||||
floatingWidth="auto"
|
||||
selectionMode="single"
|
||||
selectedValue={activeKey}
|
||||
onSelectionChange={key => {
|
||||
editor.commands.focus();
|
||||
editor.commands.setTextAlign(key as string);
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={activeKey ? 'primary' : null}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<ActiveIcon />
|
||||
</IconButton>
|
||||
<Menu>
|
||||
{Object.entries(iconMap).map(([name, config]) => {
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<MenuItem
|
||||
key={name}
|
||||
value={name}
|
||||
startIcon={<Icon size="md" />}
|
||||
capitalizeFirst
|
||||
>
|
||||
<Trans message={config.label.message} />
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
23
common/resources/client/text-editor/menubar/clear-format-button.tsx
Executable file
23
common/resources/client/text-editor/menubar/clear-format-button.tsx
Executable file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {FormatClearIcon} from '../../icons/material/FormatClear';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function ClearFormatButton({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<Tooltip label={<Trans message="Clear formatting" />}>
|
||||
<IconButton
|
||||
className={clsx('flex-shrink-0')}
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.chain().focus().clearNodes().unsetAllMarks().run();
|
||||
}}
|
||||
>
|
||||
<FormatClearIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
47
common/resources/client/text-editor/menubar/code-block-menu-trigger.tsx
Executable file
47
common/resources/client/text-editor/menubar/code-block-menu-trigger.tsx
Executable file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {CodeIcon} from '../../icons/material/Code';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function CodeBlockMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
const language = editor.getAttributes('codeBlock').language || '';
|
||||
return (
|
||||
<MenuTrigger
|
||||
selectionMode="single"
|
||||
selectedValue={language}
|
||||
onSelectionChange={key => {
|
||||
editor.commands.toggleCodeBlock({language: key as string});
|
||||
}}
|
||||
>
|
||||
<Tooltip label={<Trans message="Codeblock" />}>
|
||||
<IconButton
|
||||
className={clsx('flex-shrink-0')}
|
||||
size={size}
|
||||
color={language ? 'primary' : null}
|
||||
>
|
||||
<CodeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu>
|
||||
<MenuItem value="html">HTML</MenuItem>
|
||||
<MenuItem value="javascript">JavaScript</MenuItem>
|
||||
<MenuItem value="css">CSS</MenuItem>
|
||||
<MenuItem value="php">PHP</MenuItem>
|
||||
<MenuItem value="shell">Shell</MenuItem>
|
||||
<MenuItem value="bash">Bash</MenuItem>
|
||||
<MenuItem value="ruby">Ruby</MenuItem>
|
||||
<MenuItem value="python">Python</MenuItem>
|
||||
<MenuItem value="java">Java</MenuItem>
|
||||
<MenuItem value="c++">C++</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
55
common/resources/client/text-editor/menubar/color-buttons.tsx
Executable file
55
common/resources/client/text-editor/menubar/color-buttons.tsx
Executable file
@@ -0,0 +1,55 @@
|
||||
import React, {Fragment, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {FormatColorTextIcon} from '../../icons/material/FormatColorText';
|
||||
import {ColorPickerDialog} from '../../ui/color-picker/color-picker-dialog';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatColorFillIcon} from '../../icons/material/FormatColorFill';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
|
||||
export function ColorButtons({editor, size}: MenubarButtonProps) {
|
||||
const [dialog, setDialog] = useState<'text' | 'bg' | false>(false);
|
||||
const textActive = editor.getAttributes('textStyle').color;
|
||||
const backgroundActive = editor.getAttributes('textStyle').backgroundColor;
|
||||
return (
|
||||
<Fragment>
|
||||
<span className={clsx('flex-shrink-0 whitespace-nowrap')}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={textActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
setDialog('text');
|
||||
}}
|
||||
>
|
||||
<FormatColorTextIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={backgroundActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
setDialog('bg');
|
||||
}}
|
||||
>
|
||||
<FormatColorFillIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
<DialogTrigger
|
||||
defaultValue={dialog === 'text' ? '#000000' : '#FFFFFF'}
|
||||
type="modal"
|
||||
isOpen={!!dialog}
|
||||
onClose={newValue => {
|
||||
if (newValue) {
|
||||
if (dialog === 'text') {
|
||||
editor.commands.setColor(newValue);
|
||||
} else {
|
||||
editor.commands.setBackgroundColor(newValue);
|
||||
}
|
||||
}
|
||||
setDialog(false);
|
||||
}}
|
||||
>
|
||||
<ColorPickerDialog />
|
||||
</DialogTrigger>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
3
common/resources/client/text-editor/menubar/divider.tsx
Executable file
3
common/resources/client/text-editor/menubar/divider.tsx
Executable file
@@ -0,0 +1,3 @@
|
||||
export function Divider() {
|
||||
return <div className="self-stretch mx-4 w-1 bg-divider flex-shrink-0" />;
|
||||
}
|
||||
52
common/resources/client/text-editor/menubar/font-style-buttons.tsx
Executable file
52
common/resources/client/text-editor/menubar/font-style-buttons.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatBoldIcon} from '../../icons/material/FormatBold';
|
||||
import {FormatItalicIcon} from '../../icons/material/FormatItalic';
|
||||
import {FormatUnderlinedIcon} from '../../icons/material/FormatUnderlined';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function FontStyleButtons({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0 whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Bold" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('bold') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleBold();
|
||||
}}
|
||||
>
|
||||
<FormatBoldIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Italic" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('italic') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleItalic();
|
||||
}}
|
||||
>
|
||||
<FormatItalicIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Underline" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={editor.isActive('underline') ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleUnderline();
|
||||
}}
|
||||
>
|
||||
<FormatUnderlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
106
common/resources/client/text-editor/menubar/format-menu-trigger.tsx
Executable file
106
common/resources/client/text-editor/menubar/format-menu-trigger.tsx
Executable file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {KeyboardArrowDownIcon} from '../../icons/material/KeyboardArrowDown';
|
||||
import {Keyboard} from '../../ui/keyboard/keyboard';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
|
||||
type Level = 1 | 2 | 3 | 4;
|
||||
|
||||
export function FormatMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<MenuTrigger
|
||||
floatingMinWidth="w-256"
|
||||
onItemSelected={key => {
|
||||
editor.commands.focus();
|
||||
if (typeof key === 'string' && key.startsWith('h')) {
|
||||
editor.commands.toggleHeading({
|
||||
level: parseInt(key.replace('h', '')) as Level,
|
||||
});
|
||||
} else if (key === 'code') {
|
||||
editor.commands.toggleCode();
|
||||
} else if (key === 'strike') {
|
||||
editor.commands.toggleStrike();
|
||||
} else if (key === 'super') {
|
||||
editor.commands.toggleSuperscript();
|
||||
} else if (key === 'sub') {
|
||||
editor.commands.toggleSubscript();
|
||||
} else if (key === 'blockquote') {
|
||||
editor.commands.toggleBlockquote();
|
||||
} else if (key === 'paragraph') {
|
||||
editor.commands.setParagraph();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
className={clsx('flex-shrink-0')}
|
||||
variant="text"
|
||||
size={size}
|
||||
endIcon={<KeyboardArrowDownIcon />}
|
||||
>
|
||||
<Trans message="Format" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuItem value="h1" endSection={<Keyboard modifier>Alt+1</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 1}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h2" endSection={<Keyboard modifier>Alt+2</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 2}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h3" endSection={<Keyboard modifier>Alt+3</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 3}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="h4" endSection={<Keyboard modifier>Alt+4</Keyboard>}>
|
||||
<Trans message="Heading :number" values={{number: 4}} />
|
||||
</MenuItem>
|
||||
<MenuItem value="code" endSection={<Keyboard modifier>E</Keyboard>}>
|
||||
<Trans message="Code" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="strike"
|
||||
endSection={<Keyboard modifier>Shift+X</Keyboard>}
|
||||
>
|
||||
<Trans message="Strikethrough" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="super"
|
||||
endSection={
|
||||
<Keyboard modifier separator=" ">
|
||||
.
|
||||
</Keyboard>
|
||||
}
|
||||
>
|
||||
<Trans message="Superscript" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="sub"
|
||||
endSection={
|
||||
<Keyboard modifier separator=" ">
|
||||
,
|
||||
</Keyboard>
|
||||
}
|
||||
>
|
||||
<Trans message="Subscript" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="blockquote"
|
||||
endSection={<Keyboard modifier>Shift+B</Keyboard>}
|
||||
>
|
||||
<Trans message="Blockquote" />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="paragraph"
|
||||
endSection={<Keyboard modifier>Alt+0</Keyboard>}
|
||||
>
|
||||
<Trans message="Paragraph" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
);
|
||||
}
|
||||
32
common/resources/client/text-editor/menubar/history-buttons.tsx
Executable file
32
common/resources/client/text-editor/menubar/history-buttons.tsx
Executable file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {UndoIcon} from '../../icons/material/Undo';
|
||||
import {RedoIcon} from '../../icons/material/Redo';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
|
||||
export function HistoryButtons({editor}: MenubarButtonProps) {
|
||||
return (
|
||||
<span>
|
||||
<IconButton
|
||||
size="md"
|
||||
disabled={!editor.can().undo()}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.undo();
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="md"
|
||||
disabled={!editor.can().redo()}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.redo();
|
||||
}}
|
||||
>
|
||||
<RedoIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
52
common/resources/client/text-editor/menubar/image-button.tsx
Executable file
52
common/resources/client/text-editor/menubar/image-button.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {ImageIcon} from '../../icons/material/Image';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {useActiveUpload} from '../../uploads/uploader/use-active-upload';
|
||||
import {UploadInputType} from '../../uploads/types/upload-input-config';
|
||||
import {Disk} from '../../uploads/types/backend-metadata';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
const TwoMB = 2097152;
|
||||
|
||||
interface Props extends MenubarButtonProps {
|
||||
diskPrefix?: string;
|
||||
}
|
||||
|
||||
export function ImageButton({editor, size, diskPrefix = 'page_media'}: Props) {
|
||||
const {selectAndUploadFile} = useActiveUpload();
|
||||
|
||||
const handleUpload = () => {
|
||||
selectAndUploadFile({
|
||||
showToastOnRestrictionFail: true,
|
||||
restrictions: {
|
||||
allowedFileTypes: [UploadInputType.image],
|
||||
maxFileSize: TwoMB,
|
||||
},
|
||||
metadata: {
|
||||
diskPrefix: diskPrefix,
|
||||
disk: Disk.public,
|
||||
},
|
||||
onSuccess: entry => {
|
||||
editor.commands.focus();
|
||||
editor.commands.setImage({
|
||||
src: entry.url,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={<Trans message="Insert image" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={handleUpload}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
37
common/resources/client/text-editor/menubar/indent-buttons.tsx
Executable file
37
common/resources/client/text-editor/menubar/indent-buttons.tsx
Executable file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatIndentDecreaseIcon} from '../../icons/material/FormatIndentDecrease';
|
||||
import {FormatIndentIncreaseIcon} from '../../icons/material/FormatIndentIncrease';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function IndentButtons({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0', 'whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Decrease indent" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.outdent();
|
||||
}}
|
||||
>
|
||||
<FormatIndentDecreaseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Increase indent" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.indent();
|
||||
}}
|
||||
>
|
||||
<FormatIndentIncreaseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
128
common/resources/client/text-editor/menubar/insert-menu-trigger.tsx
Executable file
128
common/resources/client/text-editor/menubar/insert-menu-trigger.tsx
Executable file
@@ -0,0 +1,128 @@
|
||||
import React, {useState} from 'react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import clsx from 'clsx';
|
||||
import {HorizontalRuleIcon} from '../../icons/material/HorizontalRule';
|
||||
import {PriorityHighIcon} from '../../icons/material/PriorityHigh';
|
||||
import {WarningIcon} from '../../icons/material/Warning';
|
||||
import {NoteIcon} from '../../icons/material/Note';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {MoreVertIcon} from '../../icons/material/MoreVert';
|
||||
import {SmartDisplayIcon} from '../../icons/material/SmartDisplay';
|
||||
import {Form} from '../../ui/forms/form';
|
||||
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
|
||||
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
} from '../../ui/navigation/menu/menu-trigger';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
|
||||
export function InsertMenuTrigger({editor, size}: MenubarButtonProps) {
|
||||
const [dialog, setDialog] = useState<'embed' | false>(false);
|
||||
return (
|
||||
<>
|
||||
<MenuTrigger
|
||||
onItemSelected={key => {
|
||||
if (key === 'hr') {
|
||||
editor.commands.focus();
|
||||
editor.commands.setHorizontalRule();
|
||||
} else if (key === 'embed') {
|
||||
setDialog('embed');
|
||||
} else {
|
||||
editor.commands.focus();
|
||||
editor.commands.addInfo({type: key as any});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
variant="text"
|
||||
size={size}
|
||||
className={clsx('flex-shrink-0')}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu>
|
||||
<MenuItem value="hr" startIcon={<HorizontalRuleIcon />}>
|
||||
<Trans message="Horizontal rule" />
|
||||
</MenuItem>
|
||||
<MenuItem value="embed" startIcon={<SmartDisplayIcon />}>
|
||||
<Trans message="Embed" />
|
||||
</MenuItem>
|
||||
<MenuItem value="important" startIcon={<PriorityHighIcon />}>
|
||||
<Trans message="Important" />
|
||||
</MenuItem>
|
||||
<MenuItem value="warning" startIcon={<WarningIcon />}>
|
||||
<Trans message="Warning" />
|
||||
</MenuItem>
|
||||
<MenuItem value="success" startIcon={<NoteIcon />}>
|
||||
<Trans message="Note" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
isOpen={!!dialog}
|
||||
onClose={() => {
|
||||
setDialog(false);
|
||||
}}
|
||||
>
|
||||
<EmbedDialog editor={editor} />
|
||||
</DialogTrigger>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EmbedDialog({editor}: MenubarButtonProps) {
|
||||
const previousSrc = editor.getAttributes('embed').src;
|
||||
const form = useForm<{src: string}>({
|
||||
defaultValues: {src: previousSrc},
|
||||
});
|
||||
const {formId, close} = useDialogContext();
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Insert link" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={form}
|
||||
id={formId}
|
||||
onSubmit={value => {
|
||||
editor.commands.setEmbed(value);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<FormTextField
|
||||
name="src"
|
||||
label={<Trans message="Embed URL" />}
|
||||
autoFocus
|
||||
type="url"
|
||||
required
|
||||
/>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button onClick={close} variant="text">
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form={formId}
|
||||
disabled={!form.formState.isValid}
|
||||
variant="flat"
|
||||
color="primary"
|
||||
>
|
||||
<Trans message="Add" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
102
common/resources/client/text-editor/menubar/link-button.tsx
Executable file
102
common/resources/client/text-editor/menubar/link-button.tsx
Executable file
@@ -0,0 +1,102 @@
|
||||
import {useForm} from 'react-hook-form';
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {Form} from '../../ui/forms/form';
|
||||
import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';
|
||||
import {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {LinkIcon} from '../../icons/material/Link';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {FormSelect, Option} from '../../ui/forms/select/select';
|
||||
import {useDialogContext} from '../../ui/overlays/dialog/dialog-context';
|
||||
import {Dialog} from '../../ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '../../ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '../../ui/overlays/dialog/dialog-body';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {insertLinkIntoTextEditor} from '@common/text-editor/insert-link-into-text-editor';
|
||||
|
||||
interface FormValue {
|
||||
href: string;
|
||||
target?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export function LinkButton({editor, size}: MenubarButtonProps) {
|
||||
return (
|
||||
<DialogTrigger type="modal">
|
||||
<Tooltip label={<Trans message="Insert link" />}>
|
||||
<IconButton size={size} className={clsx('flex-shrink-0')}>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<LinkDialog editor={editor} />
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function LinkDialog({editor}: MenubarButtonProps) {
|
||||
const previousUrl = editor.getAttributes('link').href;
|
||||
const previousText = editor.state.doc.textBetween(
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to,
|
||||
'',
|
||||
);
|
||||
|
||||
const form = useForm<FormValue>({
|
||||
defaultValues: {href: previousUrl, text: previousText, target: '_blank'},
|
||||
});
|
||||
const {formId, close} = useDialogContext();
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Insert link" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={form}
|
||||
id={formId}
|
||||
onSubmit={value => {
|
||||
insertLinkIntoTextEditor(editor, value);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<FormTextField
|
||||
name="href"
|
||||
label={<Trans message="URL" />}
|
||||
autoFocus
|
||||
type="url"
|
||||
className="mb-20"
|
||||
/>
|
||||
<FormTextField
|
||||
name="text"
|
||||
label={<Trans message="Text to display" />}
|
||||
className="mb-20"
|
||||
/>
|
||||
<FormSelect
|
||||
selectionMode="single"
|
||||
name="target"
|
||||
label={<Trans message="Open link in..." />}
|
||||
>
|
||||
<Option value="_self">
|
||||
<Trans message="Current window" />
|
||||
</Option>
|
||||
<Option value="_blank">
|
||||
<Trans message="New window" />
|
||||
</Option>
|
||||
</FormSelect>
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button onClick={close} variant="text">
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
<Button type="submit" form={formId} variant="flat" color="primary">
|
||||
<Trans message="Save" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
41
common/resources/client/text-editor/menubar/list-buttons.tsx
Executable file
41
common/resources/client/text-editor/menubar/list-buttons.tsx
Executable file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {IconButton} from '../../ui/buttons/icon-button';
|
||||
import {FormatListBulletedIcon} from '../../icons/material/FormatListBulleted';
|
||||
import {FormatListNumberedIcon} from '../../icons/material/FormatListNumbered';
|
||||
import {MenubarButtonProps} from './menubar-button-props';
|
||||
import {Tooltip} from '@common/ui/tooltip/tooltip';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
|
||||
export function ListButtons({editor, size}: MenubarButtonProps) {
|
||||
const bulletActive = editor.isActive('bulletList');
|
||||
const orderedActive = editor.isActive('orderedList');
|
||||
return (
|
||||
<span className={clsx('flex-shrink-0', 'whitespace-nowrap')}>
|
||||
<Tooltip label={<Trans message="Bulleted list" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={bulletActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleBulletList();
|
||||
}}
|
||||
>
|
||||
<FormatListBulletedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip label={<Trans message="Numbered list" />}>
|
||||
<IconButton
|
||||
size={size}
|
||||
color={orderedActive ? 'primary' : null}
|
||||
onClick={() => {
|
||||
editor.commands.focus();
|
||||
editor.commands.toggleOrderedList();
|
||||
}}
|
||||
>
|
||||
<FormatListNumberedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
7
common/resources/client/text-editor/menubar/menubar-button-props.ts
Executable file
7
common/resources/client/text-editor/menubar/menubar-button-props.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
import type {Editor} from '@tiptap/react';
|
||||
import {ButtonSize} from '../../ui/buttons/button-size';
|
||||
|
||||
export interface MenubarButtonProps {
|
||||
editor: Editor;
|
||||
size?: ButtonSize;
|
||||
}
|
||||
31
common/resources/client/text-editor/menubar/mode-button.tsx
Executable file
31
common/resources/client/text-editor/menubar/mode-button.tsx
Executable file
@@ -0,0 +1,31 @@
|
||||
import {Button} from '../../ui/buttons/button';
|
||||
import {CodeIcon} from '../../icons/material/Code';
|
||||
import {Trans} from '../../i18n/trans';
|
||||
import {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';
|
||||
import {AceDialog} from '../../ace-editor/ace-dialog';
|
||||
import {Editor} from '@tiptap/react';
|
||||
import React from 'react';
|
||||
|
||||
interface ModeButtonProps {
|
||||
editor: Editor;
|
||||
}
|
||||
export function ModeButton({editor}: ModeButtonProps) {
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
onClose={newValue => {
|
||||
if (newValue != null) {
|
||||
editor?.commands.setContent(newValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button variant="text" startIcon={<CodeIcon />}>
|
||||
<Trans message="Source" />
|
||||
</Button>
|
||||
<AceDialog
|
||||
title={<Trans message="Source code" />}
|
||||
defaultValue={editor.getHTML()}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user