Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# TinyEditor

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/tiny-editor)
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# TinyEditor 富文本编辑器

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/opentiny/tiny-editor)
Expand Down
2 changes: 2 additions & 0 deletions packages/docs/fluent-editor/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function sidebar() {
{ text: '模拟语雀文档', link: 'https://opentiny.github.io/tiny-editor/projects' },
{ text: '图片工具栏', link: '/docs/demo/image-tool' },
{ text: 'AI', link: '/docs/demo/ai' },
{ text: '思维导图', link: '/docs/demo/mind-map' },
{ text: '流程图', link: '/docs/demo/flow-chart' },
],
},
{
Expand Down
43 changes: 43 additions & 0 deletions packages/docs/fluent-editor/demos/flow-chart-background.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['flow-chart'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'flow-chart': {
background: {
color: '#98FB98',
// image: 'url(path/to/image.png)',
repeat: 'repeat',
position: 'center',
size: 'auto',
opacity: 0.1,
},
},
},
})
const ops = [{ insert: '\n' }, { insert: { 'flow-chart': { nodes: [{ id: 'node1', type: 'rect', x: 100, y: 150, text: '开始' }, { id: 'node2', type: 'rect', x: 300, y: 150, text: '结束' }], edges: [{ id: 'edge1', sourceNodeId: 'node1', targetNodeId: 'node2', type: 'polyline' }] } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
42 changes: 42 additions & 0 deletions packages/docs/fluent-editor/demos/flow-chart-grid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['flow-chart'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'flow-chart': { grid: {
size: 15,
visible: true,
type: 'mesh',
config: {
color: '#98FB98',
thickness: 5,
},
} },
},
})
const ops = [{ insert: '\n' }, { insert: { 'flow-chart': { nodes: [{ id: 'node1', type: 'rect', x: 100, y: 150, text: '开始' }, { id: 'node2', type: 'rect', x: 300, y: 150, text: '结束' }], edges: [{ id: 'edge1', sourceNodeId: 'node1', targetNodeId: 'node2', type: 'polyline' }] } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
36 changes: 36 additions & 0 deletions packages/docs/fluent-editor/demos/flow-chart-resize.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['flow-chart'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'flow-chart': {
resize: true,
},
},
})
const ops = [{ insert: '\n' }, { insert: { 'flow-chart': { nodes: [{ id: 'node1', type: 'rect', x: 100, y: 150, text: '开始' }, { id: 'node2', type: 'rect', x: 300, y: 150, text: '结束' }], edges: [{ id: 'edge1', sourceNodeId: 'node1', targetNodeId: 'node2', type: 'polyline' }] } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
55 changes: 55 additions & 0 deletions packages/docs/fluent-editor/demos/flow-chart.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, test } from '@playwright/test'

test.describe('FlowChart.vue', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:5173/tiny-editor/docs/demo/flow-chart')
})

test('should render the editor', async ({ page }) => {
const editor = page.locator('.ql-editor')
await expect(editor).toBeVisible()
})
Comment on lines +8 to +11
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix strict-mode locator conflicts; scope to one editor container and remove arbitrary waits.

Multiple editors/toolbars exist on the docs page, so generic class selectors break in Playwright strict mode. Scope all queries to a single .ql-container (or a dedicated test id), and replace waitForTimeout with expectations.

Apply this diff:

@@
-  test('should render the editor', async ({ page }) => {
-    const editor = page.locator('.ql-editor')
-    await expect(editor).toBeVisible()
-  })
+  test('should render the editor', async ({ page }) => {
+    const container = page.locator('.ql-container').first()
+    const editor = container.locator('.ql-editor')
+    await expect(editor).toBeVisible()
+  })
@@
-  test('should have flow-chart button in toolbar', async ({ page }) => {
-    const toolbar = page.locator('.ql-toolbar')
-    await expect(toolbar).toBeVisible()
-
-    const flowChartButton = toolbar.locator('.ql-flow-chart')
-    await expect(flowChartButton).toBeVisible()
-  })
+  test('should have flow-chart button in toolbar', async ({ page }) => {
+    const container = page.locator('.ql-container').first()
+    const toolbar = container.locator('.ql-toolbar')
+    await expect(toolbar).toBeVisible()
+    await expect(toolbar.locator('.ql-flow-chart')).toBeVisible()
+  })
@@
-  test('should initialize editor with flow chart content', async ({ page }) => {
-    const editor = page.locator('.ql-editor')
-    await expect(editor).toBeVisible()
-
-    await page.waitForTimeout(1000)
-
-    const flowChartElement = editor.locator('.ql-flow-chart-item')
-    await expect(flowChartElement).toBeVisible()
-  })
+  test('should initialize editor with flow chart content', async ({ page }) => {
+    const container = page.locator('.ql-container').first()
+    const editor = container.locator('.ql-editor')
+    await expect(editor).toBeVisible()
+    await expect(container.locator('.ql-flow-chart-item').first()).toBeVisible()
+  })
@@
-  test('should contain initial flow chart nodes and edges', async ({ page }) => {
-    await page.waitForTimeout(2000)
-
-    const flowChartContainer = page.locator('.ql-flow-chart-item')
-    await expect(flowChartContainer).toBeVisible()
-
-    const nodes = page.locator('.lf-node')
-    await expect(nodes).toHaveCount(2)
-
-    const edges = page.locator('.lf-edge')
-    await expect(edges).toHaveCount(1)
-  })
+  test('should contain initial flow chart nodes and edges', async ({ page }) => {
+    const container = page.locator('.ql-container').first()
+    const flowChart = container.locator('.ql-flow-chart-item').first()
+    await expect(flowChart).toBeVisible()
+    await expect(flowChart.locator('.lf-node')).toHaveCount(2, { timeout: 5000 })
+    await expect(flowChart.locator('.lf-edge')).toHaveCount(1, { timeout: 5000 })
+  })
@@
-  test('should activate flow-chart when button is clicked', async ({ page }) => {
-    const flowChartButton = page.locator('.ql-toolbar .ql-flow-chart')
-    await expect(flowChartButton).toBeVisible()
-
-    await flowChartButton.click()
-
-    await page.waitForTimeout(500)
-
-    const editor = page.locator('.ql-editor')
-    await expect(editor).toBeVisible()
-  })
+  test('should activate flow-chart when button is clicked', async ({ page }) => {
+    const container = page.locator('.ql-container').first()
+    const flowChartButton = container.locator('.ql-toolbar .ql-flow-chart')
+    await expect(flowChartButton).toBeVisible()
+    await flowChartButton.click()
+    await expect(container.locator('.ql-editor')).toBeVisible()
+  })

Optionally, add data-testid hooks to the demo page and scope to page.getByTestId('flow-chart-demo') for maximum stability.

Also applies to: 13-19, 21-29, 31-42, 44-54

🧰 Tools
🪛 GitHub Actions: Playwright Tests

[error] 8-10: Playwright test failure: strict mode violation. locator('.ql-editor') resolved to multiple elements; expected a single element for visibility check.

🤖 Prompt for AI Agents
In packages/docs/fluent-editor/demos/flow-chart.spec.ts around lines 8-11 (and
similarly for ranges 13-19, 21-29, 31-42, 44-54), the tests use generic
selectors that conflict in Playwright strict mode and may rely on arbitrary
waits; scope all locators to a single editor container (e.g.,
page.locator('.ql-container').first() or better
page.getByTestId('flow-chart-demo') if you add a data-testid to the demo),
replace any waitForTimeout calls with explicit expectations (e.g., await
expect(scopedLocator.locator('.ql-editor')).toBeVisible()) and update all other
queries in the file to use the same scoped container so the test targets only
one editor instance and becomes stable.


test('should have flow-chart button in toolbar', async ({ page }) => {
const toolbar = page.locator('.ql-toolbar')
await expect(toolbar).toBeVisible()

const flowChartButton = toolbar.locator('.ql-flow-chart')
await expect(flowChartButton).toBeVisible()
})

test('should initialize editor with flow chart content', async ({ page }) => {
const editor = page.locator('.ql-editor')
await expect(editor).toBeVisible()

await page.waitForTimeout(1000)

const flowChartElement = editor.locator('.ql-flow-chart-item')
await expect(flowChartElement).toBeVisible()
})

test('should contain initial flow chart nodes and edges', async ({ page }) => {
await page.waitForTimeout(2000)

const flowChartContainer = page.locator('.ql-flow-chart-item')
await expect(flowChartContainer).toBeVisible()

const nodes = page.locator('.lf-node')
await expect(nodes).toHaveCount(2)

const edges = page.locator('.lf-edge')
await expect(edges).toHaveCount(1)
})

test('should activate flow-chart when button is clicked', async ({ page }) => {
const flowChartButton = page.locator('.ql-toolbar .ql-flow-chart')
await expect(flowChartButton).toBeVisible()

await flowChartButton.click()

await page.waitForTimeout(500)

const editor = page.locator('.ql-editor')
await expect(editor).toBeVisible()
})
})
34 changes: 34 additions & 0 deletions packages/docs/fluent-editor/demos/flow-chart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['flow-chart'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'flow-chart': true,
},
})
const ops = [{ insert: '\n' }, { insert: { 'flow-chart': { nodes: [{ id: 'node1', type: 'rect', x: 100, y: 150, text: '开始' }, { id: 'node2', type: 'rect', x: 300, y: 150, text: '结束' }], edges: [{ id: 'edge1', sourceNodeId: 'node1', targetNodeId: 'node2', type: 'polyline' }] } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
42 changes: 42 additions & 0 deletions packages/docs/fluent-editor/demos/mind-map-background.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['mind-map'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'mind-map': {
background: {
color: '#A4DD00',
// image: 'url(path/to/image.png)',
repeat: 'repeat',
position: 'center',
size: 'auto',
},
},
},
})
const ops = [{ insert: '\n' }, { insert: { 'mind-map': { layout: 'logicalStructure', root: { data: { text: '根节点', expand: true, uid: '36bae545-da0b-4c08-be14-ff05f7f05d0a', isActive: false }, children: [{ data: { text: '二级节点', uid: 'ef0895d2-b5cc-4214-b0ee-e29f8f02420d', expand: true, richText: false, isActive: false }, children: [] }], smmVersion: '0.14.0-fix.1' }, theme: { template: 'default', config: { backgroundColor: '#A4DD00', backgroundRepeat: 'repeat', backgroundPosition: 'center', backgroundSize: 'auto' } }, view: { transform: { scaleX: 1, scaleY: 1, shear: 0, rotate: 0, translateX: 0, translateY: 0, originX: 0, originY: 0, a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }, state: { scale: 1, x: 0, y: 0, sx: 0, sy: 0 } } } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
41 changes: 41 additions & 0 deletions packages/docs/fluent-editor/demos/mind-map-line.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['mind-map'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'mind-map': {
line: {
color: '#009CE0',
width: 3,
dasharray: '15, 10, 5, 10, 15',
style: 'curve',
},
},
},
})
const ops = [{ insert: '\n' }, { insert: { 'mind-map': { layout: 'logicalStructure', root: { data: { text: '根节点', expand: true, uid: '36bae545-da0b-4c08-be14-ff05f7f05d0a', isActive: false }, children: [{ data: { text: '二级节点', uid: 'ef0895d2-b5cc-4214-b0ee-e29f8f02420d', expand: true, richText: false, isActive: false }, children: [] }], smmVersion: '0.14.0-fix.1' }, theme: { template: 'default', config: { lineColor: '#009CE0', lineWidth: 3, lineDasharray: '15, 10, 5, 10, 15', lineStyle: 'curve' } }, view: { transform: { scaleX: 1, scaleY: 1, shear: 0, rotate: 0, translateX: 0, translateY: 0, originX: 0, originY: 0, a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }, state: { scale: 1, x: 0, y: 0, sx: 0, sy: 0 } } } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
36 changes: 36 additions & 0 deletions packages/docs/fluent-editor/demos/mind-map-resize.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type FluentEditor from '@opentiny/fluent-editor'
import { onMounted, ref } from 'vue'

let editor: FluentEditor
const editorRef = ref<HTMLElement>()

const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ align: [] }, { list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
['clean'],
['mind-map'],
]

onMounted(() => {
import('@opentiny/fluent-editor').then(({ default: FluentEditor }) => {
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
'toolbar': TOOLBAR_CONFIG,
'mind-map': {
resize: true,
},
},
})
const ops = [{ insert: '\n' }, { insert: { 'mind-map': { layout: 'logicalStructure', root: { data: { text: '根节点', expand: true, uid: '36bae545-da0b-4c08-be14-ff05f7f05d0a', isActive: false }, children: [{ data: { text: '二级节点', uid: 'ef0895d2-b5cc-4214-b0ee-e29f8f02420d', expand: true, richText: false, isActive: false }, children: [] }], smmVersion: '0.14.0-fix.1' }, theme: { template: 'default', config: {} }, view: { transform: { scaleX: 1, scaleY: 1, shear: 0, rotate: 0, translateX: 0, translateY: 0, originX: 0, originY: 0, a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }, state: { scale: 1, x: 0, y: 0, sx: 0, sy: 0 } } } } }, { insert: '\n\n' }]
editor.setContents(ops)
})
})
</script>

<template>
<div ref="editorRef" />
</template>
Loading
Loading