snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
1932
packages/extension-text-style/CHANGELOG.md
Normal file
1932
packages/extension-text-style/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
18
packages/extension-text-style/README.md
Normal file
18
packages/extension-text-style/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# @tiptap/extension-text-style
|
||||
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-text-style)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-text-style)
|
||||
[](https://github.com/sponsors/ueberdosis)
|
||||
|
||||
## Introduction
|
||||
|
||||
Tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as _New York Times_, _The Guardian_ or _Atlassian_.
|
||||
|
||||
## Official Documentation
|
||||
|
||||
Documentation can be found on the [Tiptap website](https://tiptap.dev).
|
||||
|
||||
## License
|
||||
|
||||
Tiptap is open sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap/blob/main/LICENSE.md).
|
||||
106
packages/extension-text-style/package.json
Normal file
106
packages/extension-text-style/package.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "@tiptap/extension-text-style",
|
||||
"version": "3.30.3",
|
||||
"description": "text style extension for tiptap",
|
||||
"keywords": [
|
||||
"tiptap",
|
||||
"tiptap extension"
|
||||
],
|
||||
"homepage": "https://tiptap.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ueberdosis/tiptap/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ueberdosis/tiptap",
|
||||
"directory": "packages/extension-text-style"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": {
|
||||
"import": "./dist/index.d.ts",
|
||||
"require": "./dist/index.d.cts"
|
||||
},
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./background-color": {
|
||||
"types": {
|
||||
"import": "./dist/background-color/index.d.ts",
|
||||
"require": "./dist/background-color/index.d.cts"
|
||||
},
|
||||
"import": "./dist/background-color/index.js",
|
||||
"require": "./dist/background-color/index.cjs"
|
||||
},
|
||||
"./color": {
|
||||
"types": {
|
||||
"import": "./dist/color/index.d.ts",
|
||||
"require": "./dist/color/index.d.cts"
|
||||
},
|
||||
"import": "./dist/color/index.js",
|
||||
"require": "./dist/color/index.cjs"
|
||||
},
|
||||
"./font-family": {
|
||||
"types": {
|
||||
"import": "./dist/font-family/index.d.ts",
|
||||
"require": "./dist/font-family/index.d.cts"
|
||||
},
|
||||
"import": "./dist/font-family/index.js",
|
||||
"require": "./dist/font-family/index.cjs"
|
||||
},
|
||||
"./font-size": {
|
||||
"types": {
|
||||
"import": "./dist/font-size/index.d.ts",
|
||||
"require": "./dist/font-size/index.d.cts"
|
||||
},
|
||||
"import": "./dist/font-size/index.js",
|
||||
"require": "./dist/font-size/index.cjs"
|
||||
},
|
||||
"./line-height": {
|
||||
"types": {
|
||||
"import": "./dist/line-height/index.d.ts",
|
||||
"require": "./dist/line-height/index.d.cts"
|
||||
},
|
||||
"import": "./dist/line-height/index.js",
|
||||
"require": "./dist/line-height/index.cjs"
|
||||
},
|
||||
"./text-style": {
|
||||
"types": {
|
||||
"import": "./dist/text-style/index.d.ts",
|
||||
"require": "./dist/text-style/index.d.cts"
|
||||
},
|
||||
"import": "./dist/text-style/index.js",
|
||||
"require": "./dist/text-style/index.cjs"
|
||||
},
|
||||
"./text-style-kit": {
|
||||
"types": {
|
||||
"import": "./dist/text-style-kit/index.d.ts",
|
||||
"require": "./dist/text-style-kit/index.d.cts"
|
||||
},
|
||||
"import": "./dist/text-style-kit/index.js",
|
||||
"require": "./dist/text-style-kit/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vp pack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tiptap/core": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BackgroundColor } from './background-color.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const ext: any = (BackgroundColor as any).configure()
|
||||
const globalAttrs = ext.config.addGlobalAttributes && ext.config.addGlobalAttributes.call(ext)[0]
|
||||
const { parseHTML } = globalAttrs.attributes.backgroundColor
|
||||
|
||||
describe('background-color parseHTML', () => {
|
||||
it('parses rgb(...) inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'background-color: rgb(0, 255, 0)')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('rgb(0, 255, 0)')
|
||||
})
|
||||
|
||||
it('parses hex inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'background-color: #00ff00')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('#00ff00')
|
||||
})
|
||||
|
||||
it('parses hsla inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'background-color: hsla(120, 100%, 50%, 0.3)')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('hsla(120, 100%, 50%, 0.3)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import '../text-style/index.js'
|
||||
|
||||
import { Extension, getStyleProperty } from '@tiptap/core'
|
||||
|
||||
export type BackgroundColorOptions = {
|
||||
/**
|
||||
* The types where the color can be applied
|
||||
* @default ['textStyle']
|
||||
* @example ['heading', 'paragraph']
|
||||
*/
|
||||
types: string[]
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
backgroundColor: {
|
||||
/**
|
||||
* Set the text color
|
||||
* @param backgroundColor The color to set
|
||||
* @example editor.commands.setColor('red')
|
||||
*/
|
||||
setBackgroundColor: (backgroundColor: string) => ReturnType
|
||||
|
||||
/**
|
||||
* Unset the text backgroundColor
|
||||
* @example editor.commands.unsetBackgroundColor()
|
||||
*/
|
||||
unsetBackgroundColor: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore because the module is not found during dts build
|
||||
declare module '@tiptap/extension-text-style' {
|
||||
interface TextStyleAttributes {
|
||||
backgroundColor?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to color your text.
|
||||
* @see https://tiptap.dev/api/extensions/background-color
|
||||
*/
|
||||
export const BackgroundColor = Extension.create<BackgroundColorOptions>({
|
||||
name: 'backgroundColor',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
backgroundColor: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
// Prefer the raw inline `style` attribute so we preserve the
|
||||
// original format (e.g. `#rrggbb`) instead of the canonicalized
|
||||
// `rgb(...)` value returned by `element.style.backgroundColor`.
|
||||
const value =
|
||||
getStyleProperty(element, 'background-color') ?? element.style.backgroundColor
|
||||
return value?.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()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BackgroundColor } from './background-color.js'
|
||||
|
||||
export * from './background-color.js'
|
||||
|
||||
export default BackgroundColor
|
||||
35
packages/extension-text-style/src/color/color.test.ts
Normal file
35
packages/extension-text-style/src/color/color.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Color } from './color.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const ext: any = (Color as any).configure()
|
||||
const globalAttrs = ext.config.addGlobalAttributes && ext.config.addGlobalAttributes.call(ext)[0]
|
||||
const { parseHTML } = globalAttrs.attributes.color
|
||||
|
||||
describe('color parseHTML', () => {
|
||||
it('parses rgb(...) inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: rgb(149, 141, 241)')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('rgb(149, 141, 241)')
|
||||
})
|
||||
|
||||
it('parses hex inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: #958DF1')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('#958DF1')
|
||||
})
|
||||
|
||||
it('parses hsla inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: hsla(252, 100%, 80%, 0.5)')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('hsla(252, 100%, 80%, 0.5)')
|
||||
})
|
||||
})
|
||||
96
packages/extension-text-style/src/color/color.ts
Normal file
96
packages/extension-text-style/src/color/color.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import '../text-style/index.js'
|
||||
|
||||
import { Extension, getStyleProperty } from '@tiptap/core'
|
||||
|
||||
export type ColorOptions = {
|
||||
/**
|
||||
* The types where the color can be applied
|
||||
* @default ['textStyle']
|
||||
* @example ['heading', 'paragraph']
|
||||
*/
|
||||
types: string[]
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
color: {
|
||||
/**
|
||||
* Set the text color
|
||||
* @param color The color to set
|
||||
* @example editor.commands.setColor('red')
|
||||
*/
|
||||
setColor: (color: string) => ReturnType
|
||||
|
||||
/**
|
||||
* Unset the text color
|
||||
* @example editor.commands.unsetColor()
|
||||
*/
|
||||
unsetColor: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore because the module is not found during dts build
|
||||
declare module '@tiptap/extension-text-style' {
|
||||
interface TextStyleAttributes {
|
||||
color?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to color your text.
|
||||
* @see https://tiptap.dev/api/extensions/color
|
||||
*/
|
||||
export const Color = Extension.create<ColorOptions>({
|
||||
name: 'color',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
color: {
|
||||
default: null,
|
||||
parseHTML: element => {
|
||||
// Prefer the raw inline `style` attribute so we preserve the
|
||||
// original format (e.g. `#rrggbb`) instead of the canonicalized
|
||||
// `rgb(...)` value returned by `element.style.color`.
|
||||
const value = getStyleProperty(element, 'color') ?? element.style.color
|
||||
return value?.replace(/['"]+/g, '')
|
||||
},
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.color) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
style: `color: ${attributes.color}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setColor:
|
||||
color =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { color }).run()
|
||||
},
|
||||
unsetColor:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { color: null }).removeEmptyTextStyle().run()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
5
packages/extension-text-style/src/color/index.ts
Normal file
5
packages/extension-text-style/src/color/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Color } from './color.js'
|
||||
|
||||
export * from './color.js'
|
||||
|
||||
export default Color
|
||||
@@ -0,0 +1,89 @@
|
||||
import { FontFamily } from './font-family.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const ext: any = (FontFamily as any).configure()
|
||||
const globalAttrs = ext.config.addGlobalAttributes && ext.config.addGlobalAttributes.call(ext)[0]
|
||||
const { parseHTML } = globalAttrs.attributes.fontFamily
|
||||
|
||||
describe('fontFamily parseHTML', () => {
|
||||
it('preserves unquoted multi-word values without canonicalizing to quoted form', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: Comic Sans MS, Comic Sans')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('Comic Sans MS, Comic Sans')
|
||||
})
|
||||
|
||||
it('preserves single-quoted values without converting to double quotes', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', "font-family: 'Exo 2'")
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe("'Exo 2'")
|
||||
})
|
||||
|
||||
it('preserves double-quoted values as-is', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: "Open Sans"')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('"Open Sans"')
|
||||
})
|
||||
|
||||
it('parses simple single-word values', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: Inter')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('Inter')
|
||||
})
|
||||
|
||||
it('parses generic keyword values', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: serif')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('serif')
|
||||
})
|
||||
|
||||
it('preserves CSS variable values', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: var(--title-font-family)')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('var(--title-font-family)')
|
||||
})
|
||||
|
||||
it('returns the last declaration when font-family is declared multiple times', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-family: Inter; font-family: Arial')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('Arial')
|
||||
})
|
||||
|
||||
it('ignores other style declarations and only returns font-family', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: red; font-family: Inter; font-size: 16px')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('Inter')
|
||||
})
|
||||
|
||||
it('is case-insensitive on the property name', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'FONT-FAMILY: Inter')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('Inter')
|
||||
})
|
||||
})
|
||||
94
packages/extension-text-style/src/font-family/font-family.ts
Normal file
94
packages/extension-text-style/src/font-family/font-family.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import '../text-style/index.js'
|
||||
|
||||
import { Extension, getStyleProperty } from '@tiptap/core'
|
||||
|
||||
export type FontFamilyOptions = {
|
||||
/**
|
||||
* A list of node names where the font family can be applied.
|
||||
* @default ['textStyle']
|
||||
* @example ['heading', 'paragraph']
|
||||
*/
|
||||
types: string[]
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
fontFamily: {
|
||||
/**
|
||||
* Set the font family
|
||||
* @param fontFamily The font family
|
||||
* @example editor.commands.setFontFamily('Arial')
|
||||
*/
|
||||
setFontFamily: (fontFamily: string) => ReturnType
|
||||
/**
|
||||
* Unset the font family
|
||||
* @example editor.commands.unsetFontFamily()
|
||||
*/
|
||||
unsetFontFamily: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore because the module is not found during dts build
|
||||
declare module '@tiptap/extension-text-style' {
|
||||
interface TextStyleAttributes {
|
||||
fontFamily?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to set a font family for text.
|
||||
* @see https://www.tiptap.dev/api/extensions/font-family
|
||||
*/
|
||||
export const FontFamily = Extension.create<FontFamilyOptions>({
|
||||
name: 'fontFamily',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
fontFamily: {
|
||||
default: null,
|
||||
// Prefer the raw inline `style` attribute so unquoted or
|
||||
// single-quoted multi-word names are preserved instead of being
|
||||
// canonicalized by `element.style.fontFamily`, which forces double
|
||||
// quotes that then get HTML-encoded to `"` on serialization.
|
||||
parseHTML: element =>
|
||||
getStyleProperty(element, 'font-family') ?? element.style.fontFamily,
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.fontFamily) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
style: `font-family: ${attributes.fontFamily}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setFontFamily:
|
||||
fontFamily =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { fontFamily }).run()
|
||||
},
|
||||
unsetFontFamily:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { fontFamily: null }).removeEmptyTextStyle().run()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
5
packages/extension-text-style/src/font-family/index.ts
Normal file
5
packages/extension-text-style/src/font-family/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FontFamily } from './font-family.js'
|
||||
|
||||
export * from './font-family.js'
|
||||
|
||||
export default FontFamily
|
||||
@@ -0,0 +1,35 @@
|
||||
import { FontSize } from './font-size.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const ext: any = (FontSize as any).configure()
|
||||
const globalAttrs = ext.config.addGlobalAttributes && ext.config.addGlobalAttributes.call(ext)[0]
|
||||
const { parseHTML } = globalAttrs.attributes.fontSize
|
||||
|
||||
describe('fontSize parseHTML', () => {
|
||||
it('parses simple inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-size: 16px')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('16px')
|
||||
})
|
||||
|
||||
it('returns the last declaration when font-size is declared multiple times', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'font-size: 12px; font-size: 18px')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('18px')
|
||||
})
|
||||
|
||||
it('ignores other style declarations and only returns font-size', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: red; font-size: 1.5em; line-height: 1.2')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('1.5em')
|
||||
})
|
||||
})
|
||||
92
packages/extension-text-style/src/font-size/font-size.ts
Normal file
92
packages/extension-text-style/src/font-size/font-size.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import '../text-style/index.js'
|
||||
|
||||
import { Extension, getStyleProperty } from '@tiptap/core'
|
||||
|
||||
export type FontSizeOptions = {
|
||||
/**
|
||||
* A list of node names where the font size can be applied.
|
||||
* @default ['textStyle']
|
||||
* @example ['heading', 'paragraph']
|
||||
*/
|
||||
types: string[]
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
fontSize: {
|
||||
/**
|
||||
* Set the font size
|
||||
* @param fontSize The font size
|
||||
* @example editor.commands.setFontSize('16px')
|
||||
*/
|
||||
setFontSize: (fontSize: string) => ReturnType
|
||||
/**
|
||||
* Unset the font size
|
||||
* @example editor.commands.unsetFontSize()
|
||||
*/
|
||||
unsetFontSize: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore because the module is not found during dts build
|
||||
declare module '@tiptap/extension-text-style' {
|
||||
interface TextStyleAttributes {
|
||||
fontSize?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to set a font size for text.
|
||||
* @see https://www.tiptap.dev/api/extensions/font-size
|
||||
*/
|
||||
export const FontSize = Extension.create<FontSizeOptions>({
|
||||
name: 'fontSize',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
fontSize: {
|
||||
default: null,
|
||||
// Prefer the raw inline `style` attribute so the original format
|
||||
// is preserved instead of the canonicalized value returned by
|
||||
// `element.style.fontSize`.
|
||||
parseHTML: element => getStyleProperty(element, 'font-size') ?? element.style.fontSize,
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.fontSize) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
style: `font-size: ${attributes.fontSize}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setFontSize:
|
||||
fontSize =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { fontSize }).run()
|
||||
},
|
||||
unsetFontSize:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { fontSize: null }).removeEmptyTextStyle().run()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
5
packages/extension-text-style/src/font-size/index.ts
Normal file
5
packages/extension-text-style/src/font-size/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FontSize } from './font-size.js'
|
||||
|
||||
export * from './font-size.js'
|
||||
|
||||
export default FontSize
|
||||
14
packages/extension-text-style/src/index.ts
Normal file
14
packages/extension-text-style/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* oslint-disableno-empty-object-type */
|
||||
|
||||
export * from './background-color/index.js'
|
||||
export * from './color/index.js'
|
||||
export * from './font-family/index.js'
|
||||
export * from './font-size/index.js'
|
||||
export * from './line-height/index.js'
|
||||
export * from './text-style/index.js'
|
||||
export * from './text-style-kit/index.js'
|
||||
|
||||
/**
|
||||
* The available text style attributes.
|
||||
*/
|
||||
export interface TextStyleAttributes extends Record<string, any> {}
|
||||
5
packages/extension-text-style/src/line-height/index.ts
Normal file
5
packages/extension-text-style/src/line-height/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LineHeight } from './line-height.js'
|
||||
|
||||
export * from './line-height.js'
|
||||
|
||||
export default LineHeight
|
||||
@@ -0,0 +1,35 @@
|
||||
import { LineHeight } from './line-height.js'
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const ext: any = (LineHeight as any).configure()
|
||||
const globalAttrs = ext.config.addGlobalAttributes && ext.config.addGlobalAttributes.call(ext)[0]
|
||||
const { parseHTML } = globalAttrs.attributes.lineHeight
|
||||
|
||||
describe('lineHeight parseHTML', () => {
|
||||
it('parses simple inline style', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'line-height: 1.5')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('1.5')
|
||||
})
|
||||
|
||||
it('returns the last declaration when line-height is declared multiple times', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'line-height: 1.2; line-height: 2')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('2')
|
||||
})
|
||||
|
||||
it('ignores other style declarations and only returns line-height', () => {
|
||||
const el = document.createElement('span')
|
||||
el.setAttribute('style', 'color: red; line-height: 24px; font-size: 16px')
|
||||
|
||||
const parsed = parseHTML(el)
|
||||
|
||||
expect(parsed).toBe('24px')
|
||||
})
|
||||
})
|
||||
93
packages/extension-text-style/src/line-height/line-height.ts
Normal file
93
packages/extension-text-style/src/line-height/line-height.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import '../text-style/index.js'
|
||||
|
||||
import { Extension, getStyleProperty } from '@tiptap/core'
|
||||
|
||||
export type LineHeightOptions = {
|
||||
/**
|
||||
* A list of node names where the line height can be applied.
|
||||
* @default ['textStyle']
|
||||
* @example ['heading', 'paragraph']
|
||||
*/
|
||||
types: string[]
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
lineHeight: {
|
||||
/**
|
||||
* Set the line height
|
||||
* @param lineHeight The line height
|
||||
* @example editor.commands.setLineHeight('1.5')
|
||||
*/
|
||||
setLineHeight: (lineHeight: string) => ReturnType
|
||||
/**
|
||||
* Unset the line height
|
||||
* @example editor.commands.unsetLineHeight()
|
||||
*/
|
||||
unsetLineHeight: () => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore because the module is not found during dts build
|
||||
declare module '@tiptap/extension-text-style' {
|
||||
interface TextStyleAttributes {
|
||||
lineHeight?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to set the line-height for text.
|
||||
* @see https://www.tiptap.dev/api/extensions/line-height
|
||||
*/
|
||||
export const LineHeight = Extension.create<LineHeightOptions>({
|
||||
name: 'lineHeight',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
types: ['textStyle'],
|
||||
}
|
||||
},
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: this.options.types,
|
||||
attributes: {
|
||||
lineHeight: {
|
||||
default: null,
|
||||
// Prefer the raw inline `style` attribute so the original format
|
||||
// is preserved instead of the canonicalized value returned by
|
||||
// `element.style.lineHeight`.
|
||||
parseHTML: element =>
|
||||
getStyleProperty(element, 'line-height') ?? element.style.lineHeight,
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.lineHeight) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
style: `line-height: ${attributes.lineHeight}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setLineHeight:
|
||||
lineHeight =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { lineHeight }).run()
|
||||
},
|
||||
unsetLineHeight:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
return chain().setMark('textStyle', { lineHeight: null }).removeEmptyTextStyle().run()
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
86
packages/extension-text-style/src/text-style-kit/index.ts
Normal file
86
packages/extension-text-style/src/text-style-kit/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Extension } from '@tiptap/core'
|
||||
|
||||
import type { BackgroundColorOptions } from '../background-color/index.js'
|
||||
import { BackgroundColor } from '../background-color/index.js'
|
||||
import type { ColorOptions } from '../color/index.js'
|
||||
import { Color } from '../color/index.js'
|
||||
import type { FontFamilyOptions } from '../font-family/index.js'
|
||||
import { FontFamily } from '../font-family/index.js'
|
||||
import type { FontSizeOptions } from '../font-size/index.js'
|
||||
import { FontSize } from '../font-size/index.js'
|
||||
import type { LineHeightOptions } from '../line-height/index.js'
|
||||
import { LineHeight } from '../line-height/index.js'
|
||||
import type { TextStyleOptions } from '../text-style/index.js'
|
||||
import { TextStyle } from '../text-style/index.js'
|
||||
|
||||
export interface TextStyleKitOptions {
|
||||
/**
|
||||
* If set to false, the background color extension will not be registered
|
||||
* @example backgroundColor: false
|
||||
*/
|
||||
backgroundColor: Partial<BackgroundColorOptions> | false
|
||||
/**
|
||||
* If set to false, the color extension will not be registered
|
||||
* @example color: false
|
||||
*/
|
||||
color: Partial<ColorOptions> | false
|
||||
/**
|
||||
* If set to false, the font family extension will not be registered
|
||||
* @example fontFamily: false
|
||||
*/
|
||||
fontFamily: Partial<FontFamilyOptions> | false
|
||||
/**
|
||||
* If set to false, the font size extension will not be registered
|
||||
* @example fontSize: false
|
||||
*/
|
||||
fontSize: Partial<FontSizeOptions> | false
|
||||
/**
|
||||
* If set to false, the line height extension will not be registered
|
||||
* @example lineHeight: false
|
||||
*/
|
||||
lineHeight: Partial<LineHeightOptions> | false
|
||||
/**
|
||||
* If set to false, the text style extension will not be registered (required for other text style extensions)
|
||||
* @example textStyle: false
|
||||
*/
|
||||
textStyle: Partial<TextStyleOptions> | false
|
||||
}
|
||||
|
||||
/**
|
||||
* The table kit is a collection of table editor extensions.
|
||||
*
|
||||
* It’s a good starting point for building your own table in Tiptap.
|
||||
*/
|
||||
export const TextStyleKit = Extension.create<TextStyleKitOptions>({
|
||||
name: 'textStyleKit',
|
||||
|
||||
addExtensions() {
|
||||
const extensions = []
|
||||
|
||||
if (this.options.backgroundColor !== false) {
|
||||
extensions.push(BackgroundColor.configure(this.options.backgroundColor))
|
||||
}
|
||||
|
||||
if (this.options.color !== false) {
|
||||
extensions.push(Color.configure(this.options.color))
|
||||
}
|
||||
|
||||
if (this.options.fontFamily !== false) {
|
||||
extensions.push(FontFamily.configure(this.options.fontFamily))
|
||||
}
|
||||
|
||||
if (this.options.fontSize !== false) {
|
||||
extensions.push(FontSize.configure(this.options.fontSize))
|
||||
}
|
||||
|
||||
if (this.options.lineHeight !== false) {
|
||||
extensions.push(LineHeight.configure(this.options.lineHeight))
|
||||
}
|
||||
|
||||
if (this.options.textStyle !== false) {
|
||||
extensions.push(TextStyle.configure(this.options.textStyle))
|
||||
}
|
||||
|
||||
return extensions
|
||||
},
|
||||
})
|
||||
168
packages/extension-text-style/src/text-style/index.ts
Normal file
168
packages/extension-text-style/src/text-style/index.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Mark, mergeAttributes } from '@tiptap/core'
|
||||
|
||||
import type { TextStyleAttributes } from '../index.js'
|
||||
|
||||
export interface TextStyleOptions {
|
||||
/**
|
||||
* HTML attributes to add to the span element.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>
|
||||
/**
|
||||
* When enabled, merges the styles of nested spans into the child span during HTML parsing.
|
||||
* This prioritizes the style of the child span.
|
||||
* Used when parsing content created in other editors.
|
||||
* (Fix for ProseMirror's default behavior.)
|
||||
* @default true
|
||||
*/
|
||||
mergeNestedSpanStyles: boolean
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
textStyle: {
|
||||
/**
|
||||
* Remove spans without inline style attributes.
|
||||
* @example editor.commands.removeEmptyTextStyle()
|
||||
*/
|
||||
removeEmptyTextStyle: () => ReturnType
|
||||
/**
|
||||
* Toggle a text style
|
||||
* @param attributes The text style attributes
|
||||
* @example editor.commands.toggleTextStyle({ fontWeight: 'bold' })
|
||||
*/
|
||||
toggleTextStyle: (attributes?: TextStyleAttributes) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_FIND_CHILD_SPAN_DEPTH = 20
|
||||
|
||||
/**
|
||||
* Returns all next child spans, either direct children or nested deeper
|
||||
* but won't traverse deeper into child spans found, will only go MAX_FIND_CHILD_SPAN_DEPTH levels deep (default: 20)
|
||||
*/
|
||||
const findChildSpans = (element: HTMLElement, depth = 0): HTMLElement[] => {
|
||||
const childSpans: HTMLElement[] = []
|
||||
|
||||
if (!element.children.length || depth > MAX_FIND_CHILD_SPAN_DEPTH) {
|
||||
return childSpans
|
||||
}
|
||||
|
||||
Array.from(element.children).forEach(child => {
|
||||
if (child.tagName === 'SPAN') {
|
||||
childSpans.push(child as HTMLElement)
|
||||
} else if (child.children.length) {
|
||||
childSpans.push(...findChildSpans(child as HTMLElement, depth + 1))
|
||||
}
|
||||
})
|
||||
|
||||
return childSpans
|
||||
}
|
||||
|
||||
const mergeNestedSpanStyles = (element: HTMLElement) => {
|
||||
if (!element.children.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const childSpans = findChildSpans(element)
|
||||
|
||||
if (!childSpans) {
|
||||
return
|
||||
}
|
||||
|
||||
childSpans.forEach(childSpan => {
|
||||
const childStyle = childSpan.getAttribute('style')
|
||||
const closestParentSpanStyleOfChild = childSpan.parentElement
|
||||
?.closest('span')
|
||||
?.getAttribute('style')
|
||||
|
||||
childSpan.setAttribute('style', `${closestParentSpanStyleOfChild};${childStyle}`)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to create text styles. It is required by default
|
||||
* for the `text-color` and `font-family` extensions.
|
||||
* @see https://www.tiptap.dev/api/marks/text-style
|
||||
*/
|
||||
export const TextStyle = Mark.create<TextStyleOptions>({
|
||||
name: 'textStyle',
|
||||
|
||||
priority: 101,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
HTMLAttributes: {},
|
||||
mergeNestedSpanStyles: true,
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'span',
|
||||
consuming: false,
|
||||
getAttrs: element => {
|
||||
const hasStyles = (element as HTMLElement).hasAttribute('style')
|
||||
|
||||
if (!hasStyles) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.options.mergeNestedSpanStyles) {
|
||||
mergeNestedSpanStyles(element)
|
||||
}
|
||||
|
||||
return {}
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
toggleTextStyle:
|
||||
attributes =>
|
||||
({ commands }) => {
|
||||
return commands.toggleMark(this.name, attributes)
|
||||
},
|
||||
removeEmptyTextStyle:
|
||||
() =>
|
||||
({ tr }) => {
|
||||
const { selection } = tr
|
||||
|
||||
// Gather all of the nodes within the selection range.
|
||||
// We would need to go through each node individually
|
||||
// to check if it has any inline style attributes.
|
||||
// Otherwise, calling commands.unsetMark(this.name)
|
||||
// removes everything from all the nodes
|
||||
// within the selection range.
|
||||
tr.doc.nodesBetween(selection.from, selection.to, (node, pos) => {
|
||||
// Skip non-inline nodes, the text style only applies to spans
|
||||
if (!node.isInline) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if the node has no inline style attributes.
|
||||
// Filter out non-`textStyle` marks.
|
||||
if (
|
||||
!node.marks
|
||||
.filter(mark => mark.type === this.type)
|
||||
.some(mark => Object.values(mark.attrs).some(value => !!value))
|
||||
) {
|
||||
// Proceed with the removal of the `textStyle` mark for this node only
|
||||
tr.removeMark(pos, pos + node.nodeSize, this.type)
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { BackgroundColor, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('BackgroundColor commands', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyle, BackgroundColor],
|
||||
content: '<p>Example Text</p>',
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('sets the background color of the selected text', () => {
|
||||
expect(editor.isActive('textStyle', { backgroundColor: '#958DF1' })).toBe(false)
|
||||
editor.commands.setBackgroundColor('#958DF1')
|
||||
expect(editor.isActive('textStyle', { backgroundColor: '#958DF1' })).toBe(true)
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.textContent).toBe('Example Text')
|
||||
expect(span?.style.backgroundColor).toBe('#958DF1')
|
||||
})
|
||||
|
||||
it('removes the background color of the selected text', () => {
|
||||
editor.commands.setBackgroundColor('#958DF1')
|
||||
expect(editor.getHTML()).toContain('<span')
|
||||
|
||||
editor.commands.unsetBackgroundColor()
|
||||
expect(editor.getHTML()).not.toContain('<span')
|
||||
expect(editor.isActive('textStyle', { backgroundColor: '#958DF1' })).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes the active backgroundColor via getAttributes', () => {
|
||||
editor.commands.setBackgroundColor('#958DF1')
|
||||
expect(editor.getAttributes('textStyle').backgroundColor).toBe('#958DF1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { Color, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('Color commands', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyle, Color],
|
||||
content: '<p>Example Text</p>',
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('sets the color of the selected text', () => {
|
||||
expect(editor.isActive('textStyle', { color: '#958DF1' })).toBe(false)
|
||||
editor.commands.setColor('#958DF1')
|
||||
expect(editor.isActive('textStyle', { color: '#958DF1' })).toBe(true)
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.textContent).toBe('Example Text')
|
||||
expect(span?.style.color).toBe('#958DF1')
|
||||
})
|
||||
|
||||
it('removes the color of the selected text', () => {
|
||||
editor.commands.setColor('#958DF1')
|
||||
expect(editor.getHTML()).toContain('<span')
|
||||
|
||||
editor.commands.unsetColor()
|
||||
expect(editor.getHTML()).not.toContain('<span')
|
||||
})
|
||||
|
||||
it('exposes the active color via getAttributes', () => {
|
||||
editor.commands.setColor('#958DF1')
|
||||
expect(editor.getAttributes('textStyle').color).toBe('#958DF1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { FontFamily, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('FontFamily commands', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyle, FontFamily],
|
||||
content: '<p>Example Text</p>',
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('sets the font-family of the selected text', () => {
|
||||
expect(editor.isActive('textStyle', { fontFamily: 'monospace' })).toBe(false)
|
||||
editor.commands.setFontFamily('monospace')
|
||||
expect(editor.isActive('textStyle', { fontFamily: 'monospace' })).toBe(true)
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.textContent).toBe('Example Text')
|
||||
expect((span as HTMLSpanElement | null)?.style.fontFamily).toBe('monospace')
|
||||
})
|
||||
|
||||
it('removes the font-family of the selected text', () => {
|
||||
editor.commands.setFontFamily('monospace')
|
||||
expect(editor.getHTML()).toContain('<span')
|
||||
|
||||
editor.commands.unsetFontFamily()
|
||||
expect(editor.getHTML()).not.toContain('<span')
|
||||
})
|
||||
|
||||
it('allows CSS variables as a font-family', () => {
|
||||
editor.commands.setFontFamily('var(--title-font-family)')
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.style.fontFamily).toBe('var(--title-font-family)')
|
||||
})
|
||||
|
||||
it('allows fonts containing multiple font families', () => {
|
||||
editor.commands.setFontFamily('"Comic Sans MS", "Comic Sans"')
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.style.fontFamily).toBe('"Comic Sans MS", "Comic Sans"')
|
||||
})
|
||||
|
||||
it('allows fonts containing a space and number as a font-family', () => {
|
||||
editor.commands.setFontFamily('"Exo 2"')
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.style.fontFamily).toBe('"Exo 2"')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Blockquote from '@tiptap/extension-blockquote'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { Color, FontSize, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('FontSize commands', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyle, FontSize],
|
||||
content: '<p>Example Text</p>',
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('sets the font-size of the selected text', () => {
|
||||
expect(editor.isActive('textStyle', { fontSize: '28px' })).toBe(false)
|
||||
editor.commands.setFontSize('28px')
|
||||
expect(editor.isActive('textStyle', { fontSize: '28px' })).toBe(true)
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.textContent).toBe('Example Text')
|
||||
expect(span?.style.fontSize).toBe('28px')
|
||||
})
|
||||
|
||||
it('removes the font-size of the selected text', () => {
|
||||
editor.commands.setFontSize('28px')
|
||||
expect(editor.view.dom.querySelector('span')?.style.fontSize).toBe('28px')
|
||||
|
||||
editor.commands.unsetFontSize()
|
||||
expect(editor.getHTML()).not.toContain('<span')
|
||||
})
|
||||
})
|
||||
|
||||
describe('FontSize commands inside a wrapper node', () => {
|
||||
let editor: Editor
|
||||
|
||||
const editorWith = (content: string) => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, Blockquote, TextStyle, Color, FontSize],
|
||||
content,
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
return editor
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('keeps the other text styles when unsetting inside a blockquote', () => {
|
||||
editorWith(
|
||||
'<blockquote><p><span style="color: #ff0000; font-size: 28px">Example Text</span></p></blockquote>',
|
||||
)
|
||||
|
||||
editor.commands.unsetFontSize()
|
||||
|
||||
expect(editor.getHTML()).toContain('color: #ff0000')
|
||||
expect(editor.getHTML()).not.toContain('font-size')
|
||||
})
|
||||
|
||||
it('keeps the other text styles when unsetting inside a paragraph', () => {
|
||||
editorWith('<p><span style="color: #ff0000; font-size: 28px">Example Text</span></p>')
|
||||
|
||||
editor.commands.unsetFontSize()
|
||||
|
||||
expect(editor.getHTML()).toContain('color: #ff0000')
|
||||
expect(editor.getHTML()).not.toContain('font-size')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { LineHeight, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
describe('LineHeight commands', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyle, LineHeight],
|
||||
content: '<p>Example Text</p>',
|
||||
})
|
||||
editor.commands.selectAll()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const cases = [
|
||||
{ value: '1.5', style: '1.5' },
|
||||
{ value: '2.0', style: '2' },
|
||||
{ value: '4.0', style: '4' },
|
||||
]
|
||||
|
||||
cases.forEach(({ value, style }) => {
|
||||
it(`sets line-height ${value} for the selected text`, () => {
|
||||
expect(editor.isActive('textStyle', { lineHeight: value })).toBe(false)
|
||||
editor.commands.toggleTextStyle({ lineHeight: value })
|
||||
expect(editor.isActive('textStyle', { lineHeight: value })).toBe(true)
|
||||
const span = editor.view.dom.querySelector('span')
|
||||
expect(span?.textContent).toBe('Example Text')
|
||||
expect(span?.style.lineHeight).toBe(style)
|
||||
})
|
||||
})
|
||||
|
||||
it('removes the line-height of the selected text', () => {
|
||||
editor.commands.toggleTextStyle({ lineHeight: '1.5' })
|
||||
expect(editor.view.dom.querySelector('span')?.style.lineHeight).toBe('1.5')
|
||||
|
||||
editor.commands.unsetLineHeight()
|
||||
expect(editor.getHTML()).not.toContain('<span')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Bold from '@tiptap/extension-bold'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { TextStyleKit } from '@tiptap/extension-text-style'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
const demoContent = `
|
||||
<p><span>This has a <span> tag without a style attribute, so it's thrown away.</span></p>
|
||||
<p><span style="">But this one is wrapped in a <span> tag with an inline style attribute, so it's kept - even if it's empty for now.</span></p>
|
||||
<p>--- merge nested span styles option enabled ---</p>
|
||||
<p>
|
||||
<span style="color: #FF0000;">
|
||||
<span style="font-family: serif;">
|
||||
red serif
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span style="color: #FF0000;">
|
||||
<span style="font-family: serif;">
|
||||
<span style="color: #0000FF;">
|
||||
blue serif
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span style="color: #00FF00;">
|
||||
<span style="font-family: serif;">green serif </span>
|
||||
<span style="font-family: serif;color: #FF0000;">red serif</span>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
plain
|
||||
<span style="color: #0000FF;">blue</span>
|
||||
plain
|
||||
<span style="color: #00FF00;">
|
||||
green
|
||||
<span style="font-family: serif;">green serif</span>
|
||||
</span>
|
||||
plain
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span style="color: #0000FF;">
|
||||
blue
|
||||
<span style="color: #00FF00;">
|
||||
green
|
||||
<span style="font-family: serif;">
|
||||
green serif
|
||||
<span style="color: #0000FF;">blue serif</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
|
||||
describe('TextStyleKit nested span merge', () => {
|
||||
let editor: Editor
|
||||
|
||||
beforeEach(() => {
|
||||
const element = document.createElement('div')
|
||||
document.body.appendChild(element)
|
||||
editor = new Editor({
|
||||
element,
|
||||
extensions: [Document, Paragraph, Text, TextStyleKit, Bold],
|
||||
content: demoContent,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
it('merges styles of a span with one nested child span into a single span', () => {
|
||||
const spans = editor.view.dom.querySelectorAll('p:nth-child(4) > span')
|
||||
expect(spans.length).toBe(1)
|
||||
expect(spans[0].textContent?.trim()).toBe('red serif')
|
||||
expect(spans[0].getAttribute('style')).toBe('color: #FF0000; font-family: serif;')
|
||||
})
|
||||
|
||||
it('merges styles of multiple nested spans into the innermost descendant', () => {
|
||||
const spans = editor.view.dom.querySelectorAll('p:nth-child(5) > span')
|
||||
expect(spans.length).toBe(1)
|
||||
expect(spans[0].textContent?.trim()).toBe('blue serif')
|
||||
expect(spans[0].getAttribute('style')).toBe('color: #0000FF; font-family: serif;')
|
||||
})
|
||||
|
||||
it('merges parent styles into each sibling descendant span', () => {
|
||||
const spans = editor.view.dom.querySelectorAll('p:nth-child(6) > span')
|
||||
expect(spans.length).toBe(2)
|
||||
expect(spans[0].textContent?.trim()).toBe('green serif')
|
||||
expect(spans[0].getAttribute('style')).toBe('color: #00FF00; font-family: serif;')
|
||||
expect(spans[1].textContent?.trim()).toBe('red serif')
|
||||
expect(spans[1].getAttribute('style')).toBe('color: #FF0000; font-family: serif;')
|
||||
})
|
||||
|
||||
it('keeps descendant spans intact when the parent span has no style', () => {
|
||||
const spans = editor.view.dom.querySelectorAll('p:nth-child(7) > span')
|
||||
expect(spans.length).toBe(4)
|
||||
expect(spans[0].textContent?.trim()).toBe('blue')
|
||||
expect(spans[0].getAttribute('style')).toBe('color: #0000FF;')
|
||||
expect(spans[1].textContent?.trim()).toBe('green')
|
||||
expect(spans[1].getAttribute('style')).toBe('color: #00FF00;')
|
||||
expect(spans[2].textContent?.trim()).toBe('green serif')
|
||||
expect(spans[2].getAttribute('style')).toBe('color: #00FF00; font-family: serif;')
|
||||
})
|
||||
|
||||
it('merges parent styles into descendants while preserving root text spans', () => {
|
||||
const spans = editor.view.dom.querySelectorAll('p:nth-child(8) > span')
|
||||
expect(spans.length).toBe(4)
|
||||
expect(spans[0].textContent?.trim()).toBe('blue')
|
||||
expect(spans[0].getAttribute('style')).toBe('color: #0000FF;')
|
||||
expect(spans[1].textContent?.trim()).toBe('green')
|
||||
expect(spans[1].getAttribute('style')).toBe('color: #00FF00;')
|
||||
expect(spans[2].textContent?.trim()).toBe('green serif')
|
||||
expect(spans[2].getAttribute('style')).toBe('color: #00FF00; font-family: serif;')
|
||||
expect(spans[3].textContent?.trim()).toBe('blue serif')
|
||||
expect(spans[3].getAttribute('style')).toBe('color: #0000FF; font-family: serif;')
|
||||
})
|
||||
})
|
||||
27
packages/extension-text-style/vite.config.ts
Normal file
27
packages/extension-text-style/vite.config.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite-plus'
|
||||
import { tsupCompatibleExtensions } from '../../pack.config.mjs'
|
||||
|
||||
const entries = [
|
||||
'src/background-color/index.ts',
|
||||
'src/color/index.ts',
|
||||
'src/font-family/index.ts',
|
||||
'src/font-size/index.ts',
|
||||
'src/line-height/index.ts',
|
||||
'src/text-style/index.ts',
|
||||
'src/text-style-kit/index.ts',
|
||||
'src/index.ts',
|
||||
]
|
||||
|
||||
export default defineConfig({
|
||||
pack: entries.map(entry => ({
|
||||
entry: [entry],
|
||||
tsconfig: '../../tsconfig.build.json',
|
||||
outDir: `dist${entry.replace('src', '').split('/').slice(0, -1).join('/')}`,
|
||||
dts: { sourcemap: true },
|
||||
sourcemap: true,
|
||||
target: 'es2019',
|
||||
format: ['esm', 'cjs'],
|
||||
deps: { neverBundle: [/^[^./]/] },
|
||||
outExtensions: tsupCompatibleExtensions,
|
||||
})),
|
||||
})
|
||||
Reference in New Issue
Block a user