Skip to content

Commit

Permalink
feat: screenshot (#75)
Browse files Browse the repository at this point in the history
* chore: update html2canvas

* docs: screenshot doc init

* fix: fix hover icon color

* fix: options types

* feat: screenshot style

* feat: class module screenshot

* refactor: change screenshot class to function

* feat: add new screenshot option

* feat: replace screenshot insert image url

* docs: update screenshot

* fix: html2canvas get empty image

* docs: update options

* refactor: options extract

* docs: add screenshot upload server demo

* fix: the toolbar hover color at snow theme

* docs: update image replace logic
  • Loading branch information
zzxming authored Sep 21, 2024
1 parent c042e54 commit ec1dd2e
Show file tree
Hide file tree
Showing 14 changed files with 5,377 additions and 4,368 deletions.
1 change: 1 addition & 0 deletions packages/docs/fluent-editor/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function sidebar() {
{ text: '字符统计', link: '/docs/counter' },
{ text: '视频', link: '/docs/video' },
{ text: '获取 HTML', link: '/docs/get-html' },
{ text: '截屏', link: '/docs/screenshot' },
],
},
]
Expand Down
65 changes: 65 additions & 0 deletions packages/docs/fluent-editor/demos/screenshot/base.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Html2Canvas from 'html2canvas'
let editor
const editorRef = ref<HTMLElement>()
const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ list: 'ordered' }, { list: 'bullet' }],
['clean'],
['screenshot'],
]
// to solve html2canvas get image empty
const imgToBase64 = (imageUrl: string) => new Promise<string>((resolve, reject) => {
let canvas = document.createElement('canvas')
let img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageUrl
img.onload = function () {
const ctx = canvas.getContext('2d')
if (ctx) {
canvas.height = img.height
canvas.width = img.width
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0)
const dataURL = canvas.toDataURL('image/png', 1)
resolve(dataURL)
}
}
img.onerror = function () {
reject(new Error('Could not load image at ' + imageUrl))
}
})
onMounted(() => {
// ssr compat, reference: https://vitepress.dev/guide/ssr-compat#importing-in-mounted-hook
import('@opentiny/fluent-editor').then((module) => {
const FluentEditor = module.default
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
toolbar: TOOLBAR_CONFIG,
},
screenshot: {
Html2Canvas,
onclone: async (doc: Document) => {
const imgs = doc.querySelectorAll('img')
const promises = Array.from(imgs).map(async (img) => {
img.src = await imgToBase64(img.src)
})
await Promise.all(promises)
},
},
})
})
})
</script>

<template>
<div ref="editorRef" />
</template>
82 changes: 82 additions & 0 deletions packages/docs/fluent-editor/demos/screenshot/upload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Html2Canvas from 'html2canvas'
let editor
const editorRef = ref<HTMLElement>()
const TOOLBAR_CONFIG = [
[{ header: [] }],
['bold', 'italic', 'underline', 'link'],
[{ list: 'ordered' }, { list: 'bullet' }],
['clean'],
['screenshot'],
]
// to solve html2canvas get image empty
const imgToBase64 = (imageUrl: string) => new Promise<string>((resolve, reject) => {
let canvas = document.createElement('canvas')
let img = new Image()
img.crossOrigin = 'Anonymous'
img.src = imageUrl
img.onload = function () {
const ctx = canvas.getContext('2d')
if (ctx) {
canvas.height = img.height
canvas.width = img.width
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0)
const dataURL = canvas.toDataURL('image/png', 1)
resolve(dataURL)
}
}
img.onerror = function () {
reject(new Error('Could not load image at ' + imageUrl))
}
})
onMounted(() => {
// ssr compat, reference: https://vitepress.dev/guide/ssr-compat#importing-in-mounted-hook
import('@opentiny/fluent-editor').then((module) => {
const FluentEditor = module.default
if (!editorRef.value) return
editor = new FluentEditor(editorRef.value, {
theme: 'snow',
modules: {
toolbar: TOOLBAR_CONFIG,
},
screenshot: {
Html2Canvas,
onclone: async (doc: Document) => {
const imgs = doc.querySelectorAll('img')
const promises = Array.from(imgs).map(async (img) => {
img.src = await imgToBase64(img.src)
})
await Promise.all(promises)
},
beforeCreateImage(canvas) {
return new Promise((resolve) => {
canvas.toBlob(
(data: Blob | null) => {
if (!data) return
const file = new File([data], `screenshot.png`, { type: 'image/jpeg' })
// here can upload file to server. demo just use setTimeout to simulate
setTimeout(() => {
// return the final image url
resolve('https://res.hc-cdn.com/tiny-vue-web-doc/3.18.9.20240902190525/static/images/mountain.png')
}, 1000)
},
'image/png',
1,
)
})
},
},
})
})
})
</script>

<template>
<div ref="editorRef" />
</template>
31 changes: 31 additions & 0 deletions packages/docs/fluent-editor/docs/screenshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 截屏

截屏功能依赖 [`html2canvas`](https://github.com/niklasvh/html2canvas),请自行导入

初始化编辑器前请将变量 `Html2Canvas` 暴露在 `window` 上。模块化项目可将导入变量整体传入选项

## 基础使用

:::demo src=demos/screenshot/base.vue
:::

## 上传截图

可通过在 `beforeCreateImage` 中处理截屏图片,进行上传获取其他操作,最终返回显示的图片 url

:::demo src=demos/screenshot/upload.vue
:::

## options

可传递 html2canvas 支持的配置选项, 具体请查看[官方文档](https://html2canvas.hertzen.com/configuration)

除 html2canvas 支持的配置选项外,还支持以下配置

| 名称 | 类型 | 说明 | 默认值 |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------ |
| Html2Canvas | `html2canvas` | html2canvas 模块 | - |
| beforeCreateCanvas | `() => void` | canvas 绘制前执行函数, 可在此阶段对页面 dom 进行处理 | - |
| beforeCreateImage | `(canvas: HTMLCanvasElement) => HTMLCanvasElement \| string` | canvas 绘制完成后执行函数, 可通过返回字符串作为最终生成图片的 url 路径, 否则默认生成 base64 作为图片 url. 也可对 canvas 绘制进行调整 | - |
| useCORS | `boolean` | html2canvas 配置选项 | `true` |
| foreignObjectRendering | `boolean` | html2canvas 配置选项 | `true` |
2 changes: 1 addition & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"vue": "^3.4.38",
"katex": "^0.16.11",
"highlight.js": "^10.2.0",
"html2canvas": "^1.0.0-rc.7",
"html2canvas": "^1.4.1",
"quill-markdown-shortcuts": "^0.0.10"
}
}
2 changes: 1 addition & 1 deletion packages/fluent-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"commander": "^6.2.0",
"glob": "^11.0.0",
"highlight.js": "^10.2.0",
"html2canvas": "^1.0.0-rc.7",
"html2canvas": "^1.4.1",
"jest": "^26.6.3",
"prettier": "^2.3.0",
"sass": "^1.47.0",
Expand Down
80 changes: 80 additions & 0 deletions packages/fluent-editor/src/assets/screenshot.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@

.ql-screenshot{
&-wrapper {
--ql-screenshot-color-bg: rgba(0, 0, 0, 0.5);
--ql-screenshot-color-confirm: #2196F3;
--ql-screenshot-color-cancel: #ef1749;

.ql-screenshot-cancel,
.ql-screenshot-confirm {
position: relative;
height: 16px;
width: 16px;
cursor: pointer;
}
.ql-screenshot-confirm {
background-color: var(--ql-screenshot-color-confirm);
&::after {
content: '';
position: absolute;
left: 6px;
top: 3px;
width: 5px;
height: 10px;
border: solid #fff;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
}
.ql-screenshot-cancel {
background-color: var(--ql-screenshot-color-cancel);
&::after {
content: '';
position: absolute;
left: 7px;
top: 2px;
width: 2px;
height: 12px;
border: solid #fff;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
&::before {
content: '';
position: absolute;
left: 7px;
top: 2px;
width: 2px;
height: 12px;
border: solid #fff;
border-width: 0 3px 3px 0;
transform: rotate(-45deg);
}
}
}
&-mask {
position: fixed;
inset: 0;
background-color: var(--ql-screenshot-color-bg);
z-index: 50;

}
&-cutter {
position: fixed;
border: 1px solid #fff;
z-index: 50;
}
&-coordinate,
&-done {
position: absolute;
bottom: 0;
right: 0;
font-size: 14px;
white-space: nowrap;
}
&-done {
display: flex;
border-top: 1px solid #333;
border-left: 1px solid #333;
}
}
1 change: 1 addition & 0 deletions packages/fluent-editor/src/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
@import './fileBar';
@import './link';
@import './fullscreen';
@import './screenshot';

// 模块:字数统计 counter
@include counter;
Expand Down
24 changes: 13 additions & 11 deletions packages/fluent-editor/src/assets/toolbar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,20 @@ $arrowWidth: 5px; // 下拉框箭头宽度
&.toolbar-bottom {
border-top: 0;
}

&.ql-snow {
.ql-active,
.ql-picker-label.ql-active,
.ql-picker-label:hover,
.ql-picker-item.ql-selected,
.ql-picker-item:hover,
button.ql-active,
button:hover {
color: #5e7ce0;

.ql-active,
.ql-picker-label.ql-active,
.ql-picker-label:hover,
.ql-picker-item.ql-selected,
.ql-picker-item:hover,
button.ql-active,
button:hover {
color: #5e7ce0;

.icon-triangle-down {
border-top-color: #5e7ce0;
.icon-triangle-down {
border-top-color: #5e7ce0;
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions packages/fluent-editor/src/config/icons.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,10 @@ export const SCREENSHOT_ICON = `<svg width="16px" height="16px" viewBox="0 0 16
<g transform="translate(-1529.000000, -96.000000)">
<g transform="translate(1529.000000, 96.000000)">
<rect fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="16" height="16"></rect>
<polyline stroke="#293040" points="16 14 2 14 2 0 2 0"></polyline>
<polyline stroke="#293040" points="4 2 14 2 14 12"></polyline>
<rect fill="#293040" x="0" y="1.5" width="2" height="1"></rect>
<rect fill="#293040"
<polyline stroke="currentColor" points="16 14 2 14 2 0 2 0"></polyline>
<polyline stroke="currentColor" points="4 2 14 2 14 12"></polyline>
<rect fill="currentColor" x="0" y="1.5" width="2" height="1"></rect>
<rect fill="currentColor"
transform="translate(14.000000, 15.000000)
rotate(-90.000000)
translate(-14.000000, -15.000000) " x="13" y="14.5" width="2" height="1">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { QuillOptions } from 'quill'
import { IEditorModules } from './editor-modules.interface'
import { EditorFormat } from './type'
import { ScreenShotOptions } from '../../screenshot'

export interface IEditorConfig extends QuillOptions {
format?: EditorFormat
Expand All @@ -17,4 +18,5 @@ export interface IEditorConfig extends QuillOptions {
fileAccept?: Array<string>[] | string
isVideoPlay?: boolean
}
screenshot?: Partial<ScreenShotOptions>
}
4 changes: 2 additions & 2 deletions packages/fluent-editor/src/fluent-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import FileModule from './file' // 文件
import Link from './link' // 超链接0
import Mention from './mention/Mention' // @提醒
// import QuickMenu from './quick-menu' // 快捷菜单
// import Screenshot from './screenshot' // 截图
import { Screenshot } from './screenshot'// 截图
import SoftBreak from './soft-break' // 软回车
import Strike from './strike' // 删除线
import BetterTable from './table/better-table' // 表格
Expand Down Expand Up @@ -102,6 +102,7 @@ const registerModules = function () {
}
},
[FormatPainter.toolName]: FormatPainter,
[Screenshot.toolName]: Screenshot,
},
},
'better-table': {
Expand Down Expand Up @@ -142,7 +143,6 @@ const registerModules = function () {
'modules/emoji-shortname': Emoji.ShortNameEmoji,
// 'modules/global-link': GlobalLink,//暂未开发
'modules/link': Link, // 报错
// 'modules/screenshot': Screenshot,//暂未开发
// 'modules/quickmenu': QuickMenu,//暂未开发
'modules/syntax': CustomSyntax,

Expand Down
Loading

0 comments on commit ec1dd2e

Please sign in to comment.