Skip to content

Commit

Permalink
feat: printer
Browse files Browse the repository at this point in the history
  • Loading branch information
Seedsa committed Sep 26, 2024
1 parent 4e69dd0 commit e0eb661
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 22 deletions.
2 changes: 2 additions & 0 deletions examples/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import {
Code,
AI,
Preview,
Printer,
} from 'echo-editor'
import { ExportWord } from './extensions/ExportWord'
import OpenAI from 'openai'
Expand Down Expand Up @@ -324,6 +325,7 @@ const extensions = [
upload: handleFileUpload,
}),
FindAndReplace.configure({ spacer: true }),
Printer,
Preview,
]
async function handleFileUpload(files: File[]) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"echo-drag-handle-plugin": "^0.0.2",
"hotkeys-js": "^3.13.7",
"tippy.js": "^6.3.7"
},
"devDependencies": {
Expand Down
9 changes: 8 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/components/EchoEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import AIMenu from './menus/AIMenu.vue'
import Menubars from './Menubars.vue'
import Toolbar from './Toolbar.vue'
import Preview from './Preview.vue'
import Printer from './Printer.vue'
import FindAndReplace from './FindAndReplace.vue'
import { EchoEditorOnChange } from '@/type'
import { useDark, useToggle } from '@vueuse/core'
Expand Down Expand Up @@ -176,6 +177,7 @@ defineExpose({ editor })
<AIMenu :editor="editor" :disabled="disabled" />
<BasicBubbleMenu v-if="!hideBubble" :editor="editor" :disabled="disableBubble" />
<Preview :editor="editor" />
<Printer :editor="editor" />
<FindAndReplace :container-ref="contentRef" :editor="editor" />
<div
class="relative"
Expand Down
21 changes: 1 addition & 20 deletions src/components/Menubars.vue
Original file line number Diff line number Diff line change
Expand Up @@ -124,26 +124,7 @@ const menubarMenus = ref<MenuGroup[]>([
shortcut: ['mod', 'P'],
action: () => {
// 实现打印
const content = props.editor.getHTML()
if (content) {
const printWindow = window.open('', '', 'width=600,height=600') // 打开新窗口
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title></title>
</head>
<body>
${content}
</body>
</html>
`)
printWindow.document.close() // 关闭文档流
printWindow.focus() // 聚焦到打印窗口
printWindow.print() // 调用打印
printWindow.close() // 打印后关闭窗口
}
}
store.state.printer = true
},
},
],
Expand Down
73 changes: 73 additions & 0 deletions src/components/Printer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { useTiptapStore } from '@/hooks'
import type { Editor } from '@tiptap/core'
import ProseMirrorStyle from '@/styles/ProseMirror.scss?inline'
import { useHotkeys } from '@/hooks'
interface Props {
editor: Editor
disabled?: boolean
containerRef: Object
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
containerRef: undefined,
})
const { state } = useTiptapStore()
const srcdoc = ref('')
const iframeRef = ref<HTMLIFrameElement | null>(null)
async function handlePrint() {
props.editor.commands.blur()
const content = props.editor.getHTML()
srcdoc.value = `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Echo Editor</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>${ProseMirrorStyle}</style>
</head>
<body class="is-print">
<div class="tiptap ProseMirror" translate="no" aria-expanded="false">
${content}
</div>
</body>
</html>`
await nextTick()
setupPrintListener(iframeRef.value!)
iframeRef?.value?.contentWindow?.print()
}
function setupPrintListener(iframe: HTMLIFrameElement) {
if (!iframe.contentWindow) {
console.error('无法访问 iframe 的 contentWindow')
return
}
const iframeWindow = iframe.contentWindow
// 监听 afterprint 事件(用于处理用户取消打印的情况)
iframeWindow.addEventListener('afterprint', () => {
state.printer = false
})
}
watch(
() => state.printer,
val => {
if (val) {
handlePrint()
}
}
)
// 创建快捷键的绑定和解绑控制
const { bind, unbind } = useHotkeys('ctrl+p,command+p', () => {
state.printer = true
})
// 当编辑器获得焦点时绑定快捷键,失去焦点时解绑快捷键
props.editor.on('focus', bind)
props.editor.on('blur', unbind)
</script>
<template>
<iframe ref="iframeRef" v-if="state.printer" class="absolute w-0 h-0 border-none overflow-auto" :srcdoc="srcdoc" />
</template>
4 changes: 3 additions & 1 deletion src/extensions/FindAndReplace/FindAndReplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/p
import { Node as PMNode } from '@tiptap/pm/model'
import ActionButton from '@/components/ActionButton.vue'
import { useTiptapStore } from '@/hooks'
import type { GeneralOptions } from '@/type'

const store = useTiptapStore()
declare module '@tiptap/core' {
Expand Down Expand Up @@ -198,7 +199,7 @@ const replaceAll = (

export const findAndReplacePluginKey = new PluginKey('findAndReplacePlugin')

export interface FindAndReplaceOptions {
export interface FindAndReplaceOptions extends GeneralOptions<FindAndReplaceOptions> {
searchResultClass: string
disableRegex: boolean
}
Expand All @@ -218,6 +219,7 @@ export const FindAndReplace = Extension.create<FindAndReplaceOptions, FindAndRep
name: 'findAndReplace',
addOptions() {
return {
...this.parent?.(),
searchResultClass: 'echo-editor-search-result',
disableRegex: true,
button: ({ editor, extension, t }) => ({
Expand Down
25 changes: 25 additions & 0 deletions src/extensions/Printer/Printer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ActionButton from '@/components/ActionButton.vue'
import { useTiptapStore } from '@/hooks'
import type { GeneralOptions } from '@/type'
import { Extension } from '@tiptap/core'
export interface PrinterOptions extends GeneralOptions<PrinterOptions> {}

const { togglePrinter, state } = useTiptapStore()
export const Printer = Extension.create<PrinterOptions>({
name: 'printer',
addOptions() {
return {
...this.parent?.(),
button: ({ editor, extension, t }) => ({
component: ActionButton,
componentProps: {
tooltip: t('editor.printer.tooltip'),
action: () => togglePrinter(),
icon: 'Printer',
shortcutKeys: ['mod', 'P'],
isActive: () => state.printer,
},
}),
}
},
})
1 change: 1 addition & 0 deletions src/extensions/Printer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Printer'
3 changes: 3 additions & 0 deletions src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,6 @@ export type { PreviewOptions } from './Preview'
export { Preview } from './Preview'

export { FindAndReplace } from './FindAndReplace'

export { Printer } from './Printer'
export type { PrinterOptions } from './Printer'
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { createContext, useContext } from './useContext'
export { useTiptapStore } from './useStore'
export { useHotkeys } from './useHotkeys'
28 changes: 28 additions & 0 deletions src/hooks/useHotkeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import hotkeys from 'hotkeys-js'
import { onBeforeUnmount } from 'vue'

export const useHotkeys = (keys: string, callback: CallableFunction) => {
hotkeys.filter = () => true

// 绑定快捷键
const bind = () => {
hotkeys(keys, (e: Event) => {
e.preventDefault()
callback()
return false
})
}

// 解绑快捷键
const unbind = () => {
hotkeys.unbind(keys)
}

// 在组件卸载时自动解绑
onBeforeUnmount(() => {
unbind()
})

// 返回用于手动控制的绑定和解绑函数
return { bind, unbind }
}
7 changes: 7 additions & 0 deletions src/hooks/useStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ interface Instance {

/** FindAndReplace */
findAndReplace: boolean
/** Printer */
printer: boolean
}

export const useTiptapStore = createGlobalState(() => {
Expand All @@ -62,6 +64,7 @@ export const useTiptapStore = createGlobalState(() => {
showPreview: false,
spellCheck: false,
findAndReplace: false,
printer: false,
})

const isFullscreen = computed(() => state.isFullscreen)
Expand All @@ -79,6 +82,9 @@ export const useTiptapStore = createGlobalState(() => {
function toggleFindAndReplace() {
state.findAndReplace = !state.findAndReplace
}
function togglePrinter() {
state.printer = !state.printer
}

watchEffect(() => {
state.extensions = _state.extensions
Expand All @@ -92,5 +98,6 @@ export const useTiptapStore = createGlobalState(() => {
togglePreview,
toggleSpellCheck,
toggleFindAndReplace,
togglePrinter,
}
})
1 change: 1 addition & 0 deletions src/locales/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ const locale: Record<string, string> = {
'editor.findAndReplace.replace': 'Replace',
'editor.findAndReplace.replaceAll': 'Replace All',
'editor.findAndReplace.caseSensitive': 'Case Sensitive',
'editor.printer.tooltip': 'Print',
}

export default locale
1 change: 1 addition & 0 deletions src/locales/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const locale: Record<string, string> = {
'editor.findAndReplace.replace': '替换',
'editor.findAndReplace.replaceAll': '全部替换',
'editor.findAndReplace.caseSensitive': '区分大小写',
'editor.printer.tooltip': '打印',
}

export default locale

0 comments on commit e0eb661

Please sign in to comment.