feat:增加图片插入

pr675
黄云飞 4 years ago
parent b4553560af
commit 4160abfac8

@ -96,6 +96,10 @@
</div>
<div class="menu-divider"></div>
<div class="menu-item">
<div class="menu-item__image">
<i></i>
<input type="file" id="image" accept=".png, .jpg, .jpeg">
</div>
<div class="menu-item__search">
<i></i>
</div>

@ -0,0 +1 @@
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 16 16" xml:space="preserve"><style>.st0{fill:#3d4757}</style><g id="_x30_0-公共_x2F_02工具栏_x2F_插入图片-16px-"><g id="Group-19" transform="translate(1 1)"><path id="Combined-Shape" class="st0" d="M1 0h12c.6 0 1 .4 1 1v11c0 .6-.4 1-1 1H1c-.6 0-1-.4-1-1V1c0-.6.4-1 1-1zm0 1v11h12V1H1z"/><circle id="椭圆形" class="st0" cx="10" cy="4" r="1"/><path id="Path" class="st0" d="M8.5 11.2l-4-4.1L1 10.7V9.2c1.7-1.6 2.7-2.5 3-2.8.4-.5.7-.4 1 0L8.5 10 11 7.3c.4-.5.6-.5 1-.1l2 2.8v1.5l-2.5-3.4-3 3.1z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 613 B

@ -1,4 +1,5 @@
import { RowFlex } from "../../dataset/enum/Row"
import { IDrawImagePayload } from "../../interface/Draw"
import { CommandAdapt } from "./CommandAdapt"
export class Command {
@ -20,6 +21,7 @@ export class Command {
private static center: Function
private static right: Function
private static rowMargin: Function
private static image: Function
private static search: Function
private static print: Function
@ -41,6 +43,7 @@ export class Command {
Command.center = adapt.rowFlex.bind(adapt)
Command.right = adapt.rowFlex.bind(adapt)
Command.rowMargin = adapt.rowMargin.bind(adapt)
Command.image = adapt.image.bind(adapt)
Command.search = adapt.search.bind(adapt)
Command.print = adapt.print.bind(adapt)
}
@ -115,7 +118,11 @@ export class Command {
return Command.rowMargin(payload)
}
// 搜索、打印
// 图片上传、搜索、打印
public executeImage(payload: IDrawImagePayload) {
return Command.image(payload)
}
public executeSearch(payload: string | null) {
return Command.search(payload)
}

@ -1,7 +1,11 @@
import { ZERO } from "../../dataset/constant/Common"
import { ElementType } from "../../dataset/enum/Element"
import { ElementStyleKey } from "../../dataset/enum/ElementStyle"
import { RowFlex } from "../../dataset/enum/Row"
import { IDrawImagePayload } from "../../interface/Draw"
import { IEditorOption } from "../../interface/Editor"
import { IElementStyle } from "../../interface/Element"
import { IElement, IElementStyle } from "../../interface/Element"
import { getUUID } from "../../utils"
import { printImageBase64 } from "../../utils/print"
import { Draw } from "../draw/Draw"
import { HistoryManager } from "../history/HistoryManager"
@ -205,10 +209,32 @@ export class CommandAdapt {
this.draw.render({ curIndex, isSetCursor })
}
public image(payload: IDrawImagePayload) {
const { startIndex, endIndex } = this.range.getRange()
if (startIndex === 0 && endIndex === 0) return
const elementList = this.draw.getElementList()
const { value, width, height } = payload
const element: IElement = {
value,
width,
height,
id: getUUID(),
type: ElementType.IMAGE
}
const curIndex = startIndex + 1
if (startIndex === endIndex) {
elementList.splice(curIndex, 0, element)
} else {
elementList.splice(curIndex, endIndex - startIndex, element)
this.range.setRange(curIndex, curIndex)
}
this.draw.render({ curIndex })
}
public search(payload: string | null) {
if (payload) {
const elementList = this.draw.getElementList()
const text = elementList.map(e => !e.type || e.type === 'TEXT' ? e.value : null)
const text = elementList.map(e => !e.type || e.type === ElementType.TEXT ? e.value : ZERO)
.filter(Boolean)
.join('')
const matchStartIndexList = []

@ -1,3 +1,4 @@
import { CURSOR_AGENT_HEIGHT } from "../../dataset/constant/Cursor"
import { Draw } from "../draw/Draw"
import { CanvasEvent } from "../event/CanvasEvent"
import { Position } from "../position/Position"
@ -48,18 +49,19 @@ export class Cursor {
if (!cursorPosition) return
// 设置光标代理
const { metrics, coordinate: { leftTop, rightTop }, ascent } = cursorPosition
const height = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
const height = metrics.boundingBoxAscent + metrics.boundingBoxDescent
const agentCursorDom = this.cursorAgent.getAgentCursorDom()
setTimeout(() => {
agentCursorDom.focus()
agentCursorDom.setSelectionRange(0, 0)
})
const curosrleft = `${rightTop[0]}px`
agentCursorDom.style.left = curosrleft
agentCursorDom.style.top = `${leftTop[1] + ascent - 12}px`
const cursorTop = leftTop[1] + ascent - metrics.boundingBoxAscent
const curosrleft = rightTop[0]
agentCursorDom.style.left = `${curosrleft}px`
agentCursorDom.style.top = `${cursorTop + height - CURSOR_AGENT_HEIGHT}px`
// 模拟光标显示
this.cursorDom.style.left = curosrleft
this.cursorDom.style.top = `${leftTop[1] + ascent - metrics.fontBoundingBoxAscent}px`
this.cursorDom.style.left = `${curosrleft}px`
this.cursorDom.style.top = `${cursorTop}px`
this.cursorDom.style.display = 'block'
this.cursorDom.style.height = `${height}px`
setTimeout(() => {

@ -2,8 +2,8 @@ import { ZERO } from "../../dataset/constant/Common"
import { RowFlex } from "../../dataset/enum/Row"
import { IDrawOption } from "../../interface/Draw"
import { IEditorOption } from "../../interface/Editor"
import { IElement, IElementPosition, IElementStyle } from "../../interface/Element"
import { IRow } from "../../interface/Row"
import { IElement, IElementMetrics, IElementPosition, IElementStyle } from "../../interface/Element"
import { IRow, IRowElement } from "../../interface/Row"
import { deepClone } from "../../utils"
import { Cursor } from "../cursor/Cursor"
import { CanvasEvent } from "../event/CanvasEvent"
@ -12,12 +12,14 @@ import { HistoryManager } from "../history/HistoryManager"
import { Listener } from "../listener/Listener"
import { Position } from "../position/Position"
import { RangeManager } from "../range/RangeManager"
import { Background } from "./Background"
import { Highlight } from "./Highlight"
import { Margin } from "./Margin"
import { Search } from "./Search"
import { Strikeout } from "./Strikeout"
import { Underline } from "./Underline"
import { Background } from "./frame/Background"
import { Highlight } from "./richtext/Highlight"
import { Margin } from "./frame/Margin"
import { Search } from "./interactive/Search"
import { Strikeout } from "./richtext/Strikeout"
import { Underline } from "./richtext/Underline"
import { ElementType } from "../../dataset/enum/Element"
import { ImageParticle } from "./particle/ImageParticle"
export class Draw {
@ -37,6 +39,7 @@ export class Draw {
private strikeout: Strikeout
private highlight: Highlight
private historyManager: HistoryManager
private imageParticle: ImageParticle
private rowCount: number
private painterStyle: IElementStyle | null
@ -64,6 +67,7 @@ export class Draw {
this.underline = new Underline(ctx, options)
this.strikeout = new Strikeout(ctx, options)
this.highlight = new Highlight(ctx, options)
this.imageParticle = new ImageParticle(ctx)
const canvasEvent = new CanvasEvent(canvas, this)
this.cursor = new Cursor(canvas, this, canvasEvent)
@ -179,29 +183,47 @@ export class Draw {
this.ctx.save()
const curRow: IRow = rowList[rowList.length - 1]
const element = this.elementList[i]
this.ctx.font = this.getFont(element)
const metrics = this.ctx.measureText(element.value)
const width = metrics.width
const rowMargin = defaultBasicRowMarginHeight * (element.rowMargin || defaultRowMargin)
const fontBoundingBoxAscent = metrics.fontBoundingBoxAscent + rowMargin
const fontBoundingBoxDescent = metrics.fontBoundingBoxDescent + rowMargin
const height = fontBoundingBoxAscent + fontBoundingBoxDescent
const lineText = { ...element, metrics }
if (curRow.width + width > rightTopPoint[0] - leftTopPoint[0] || (i !== 0 && element.value === ZERO)) {
let metrics: IElementMetrics = {
width: 0,
boundingBoxAscent: 0,
boundingBoxDescent: 0
}
if (element.type === ElementType.IMAGE) {
metrics.width = element.width!
metrics.boundingBoxAscent = 0
metrics.boundingBoxDescent = element.height!
} else {
this.ctx.font = this.getFont(element)
const fontMetrics = this.ctx.measureText(element.value)
metrics.width = fontMetrics.width
metrics.boundingBoxAscent = fontMetrics.fontBoundingBoxAscent
metrics.boundingBoxDescent = fontMetrics.fontBoundingBoxDescent
}
const ascent = metrics.boundingBoxAscent + rowMargin
const descent = metrics.boundingBoxDescent + rowMargin
const height = ascent + descent
const rowElement: IRowElement = { ...element, metrics }
// 超过限定宽度
if (curRow.width + metrics.width > rightTopPoint[0] - leftTopPoint[0] || (i !== 0 && element.value === ZERO)) {
rowList.push({
width,
width: metrics.width,
height: this.options.defaultSize,
elementList: [lineText],
ascent: fontBoundingBoxAscent,
rowFlex: lineText.rowFlex
elementList: [rowElement],
ascent,
rowFlex: rowElement.rowFlex
})
} else {
curRow.width += width
curRow.width += metrics.width
if (curRow.height < height) {
curRow.height = height
curRow.ascent = fontBoundingBoxAscent
if (element.type === ElementType.IMAGE) {
curRow.ascent = element.height!
} else {
curRow.ascent = ascent
}
}
curRow.elementList.push(lineText)
curRow.elementList.push(rowElement)
}
this.ctx.restore()
}
@ -224,16 +246,21 @@ export class Draw {
this.ctx.save()
const element = curRow.elementList[j]
const metrics = element.metrics
this.ctx.font = this.getFont(element)
if (element.color) {
this.ctx.fillStyle = element.color
if (!element.type || element.type === ElementType.TEXT) {
this.ctx.font = this.getFont(element)
if (element.color) {
this.ctx.fillStyle = element.color
}
}
const offsetY = element.type === ElementType.IMAGE
? curRow.ascent - element.height!
: curRow.ascent
const positionItem: IElementPosition = {
index,
value: element.value,
rowNo: i,
metrics,
ascent: curRow.ascent,
ascent: offsetY,
lineHeight: curRow.height,
isLastLetter: j === curRow.elementList.length - 1,
coordinate: {
@ -252,12 +279,16 @@ export class Draw {
if (element.strikeout) {
this.strikeout.render(x, y + curRow.height / 2, metrics.width)
}
// 文本高亮
// 元素高亮
if (element.highlight) {
this.highlight.render(element.highlight, x, y, metrics.width, curRow.height)
}
// 文本
this.ctx.fillText(element.value, x, y + curRow.ascent)
// 元素绘制
if (element.type === ElementType.IMAGE) {
this.imageParticle.render(element, x, y + offsetY)
} else {
this.ctx.fillText(element.value, x, y + offsetY)
}
// 选区绘制
const { startIndex, endIndex } = this.range.getRange()
if (startIndex !== endIndex && startIndex < index && index <= endIndex) {

@ -1,4 +1,4 @@
import { IEditorOption } from "../../interface/Editor"
import { IEditorOption } from "../../../interface/Editor"
export class Margin {

@ -1,6 +1,6 @@
import { IEditorOption } from "../../interface/Editor"
import { Position } from "../position/Position"
import { Draw } from "./Draw"
import { IEditorOption } from "../../../interface/Editor"
import { Position } from "../../position/Position"
import { Draw } from "../Draw"
export class Search {

@ -0,0 +1,31 @@
import { IElement } from "../../../interface/Element";
export class ImageParticle {
private ctx: CanvasRenderingContext2D
private imageCache: Map<string, HTMLImageElement>;
constructor(ctx: CanvasRenderingContext2D) {
this.ctx = ctx
this.imageCache = new Map()
}
public getImageCache(): Map<string, HTMLImageElement> {
return this.imageCache
}
render(element: IElement, x: number, y: number) {
if (this.imageCache.has(element.id!)) {
const img = this.imageCache.get(element.id!)!
this.ctx.drawImage(img, x, y, element.width!, element.height!)
} else {
const img = new Image()
img.src = element.value
img.onload = () => {
this.ctx.drawImage(img, x, y, img.width, img.height)
this.imageCache.set(element.id!, img)
}
}
}
}

@ -1,4 +1,4 @@
import { IEditorOption } from "../../interface/Editor"
import { IEditorOption } from "../../../interface/Editor"
export class Highlight {

@ -1,4 +1,4 @@
import { IEditorOption } from "../../interface/Editor"
import { IEditorOption } from "../../../interface/Editor"
export class Strikeout {

@ -1,4 +1,4 @@
import { IEditorOption } from "../../interface/Editor"
import { IEditorOption } from "../../../interface/Editor"
export class Underline {

@ -2,7 +2,7 @@ import { ZERO } from "../../dataset/constant/Common"
import { ElementStyleKey } from "../../dataset/enum/ElementStyle"
import { KeyMap } from "../../dataset/enum/Keymap"
import { IElement } from "../../interface/Element"
import { writeText } from "../../utils/clipboard"
import { writeTextByElementList } from "../../utils/clipboard"
import { Cursor } from "../cursor/Cursor"
import { Draw } from "../draw/Draw"
import { HistoryManager } from "../history/HistoryManager"
@ -187,11 +187,11 @@ export class CanvasEvent {
evt.preventDefault()
} else if (evt.ctrlKey && evt.key === KeyMap.C) {
if (!isCollspace) {
writeText(elementList.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
writeTextByElementList(elementList.slice(startIndex + 1, endIndex + 1))
}
} else if (evt.ctrlKey && evt.key === KeyMap.X) {
if (!isCollspace) {
writeText(position.slice(startIndex + 1, endIndex + 1).map(p => p.value).join(''))
writeTextByElementList(elementList.slice(startIndex + 1, endIndex + 1))
elementList.splice(startIndex + 1, endIndex - startIndex)
const curIndex = startIndex
this.range.setRange(curIndex, curIndex)

@ -0,0 +1 @@
export const CURSOR_AGENT_HEIGHT = 12

@ -0,0 +1,4 @@
export enum ElementType {
TEXT = 'text',
IMAGE = 'image'
}

@ -7,6 +7,8 @@ import { Command } from './core/command/Command'
import { CommandAdapt } from './core/command/CommandAdapt'
import { Listener } from './core/listener/Listener'
import { RowFlex } from './dataset/enum/Row'
import { getUUID } from './utils'
import { ElementType } from './dataset/enum/Element'
export default class Editor {
@ -43,10 +45,11 @@ export default class Editor {
value: ZERO
})
}
elementList.forEach(text => {
if (text.value === '\n') {
text.value = ZERO
elementList.forEach(el => {
if (el.value === '\n') {
el.value = ZERO
}
el.id = getUUID()
})
// 监听
this.listener = new Listener()
@ -59,8 +62,14 @@ export default class Editor {
}
// 对外属性
// 对外对象
export {
Editor,
RowFlex
RowFlex,
ElementType
}
// 对外类型
export type {
IElement
}

@ -1,5 +1,11 @@
export interface IDrawOption {
curIndex?: number;
isSetCursor?: boolean
isSetCursor?: boolean;
isSubmitHistory?: boolean;
}
export interface IDrawImagePayload {
width: number;
height: number;
value: string;
}

@ -1,5 +1,12 @@
import { ElementType } from "../dataset/enum/Element"
import { RowFlex } from "../dataset/enum/Row"
export interface IElementMetrics {
width: number;
boundingBoxAscent: number;
boundingBoxDescent: number;
}
export interface IElementStyle {
font?: string;
size?: number;
@ -16,7 +23,8 @@ export interface IElementStyle {
}
export interface IElementBasic {
type?: 'TEXT' | 'IMAGE';
id?: string;
type?: ElementType;
value: string;
}
@ -28,7 +36,7 @@ export interface IElementPosition {
rowNo: number;
ascent: number;
lineHeight: number;
metrics: TextMetrics;
metrics: IElementMetrics;
isLastLetter: boolean,
coordinate: {
leftTop: number[];

@ -1,8 +1,8 @@
import { RowFlex } from "../dataset/enum/Row"
import { IElement } from "./Element"
import { IElement, IElementMetrics } from "./Element"
export type IRowElement = IElement & {
metrics: TextMetrics
metrics: IElementMetrics
}
export interface IRow {

@ -1,6 +1,14 @@
import { ElementType, IElement } from ".."
import { ZERO } from "../dataset/constant/Common"
export function writeText(text: string) {
if (!text) return
window.navigator.clipboard.writeText(text.replaceAll(ZERO, `\n`))
}
export function writeTextByElementList(elementList: IElement[]) {
const text = elementList
.map(p => !p.type || p.type === ElementType.TEXT ? p.value : '')
.join('')
writeText(text)
}

@ -41,4 +41,11 @@ export function findParent(node: Element, filterFn: Function, includeSelf: boole
}
}
return null
}
export function getUUID(): string {
function S4(): string {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
}
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4())
}

@ -1,5 +1,5 @@
import './style.css'
import Editor, { RowFlex } from './editor'
import Editor, { IElement, RowFlex } from './editor'
window.onload = function () {
@ -31,7 +31,7 @@ window.onload = function () {
return ~i ? Array(b.length).fill(i).map((_, j) => i + j) : []
}).flat()
// 组合数据
const data = text.split('').map((value, index) => {
const data: IElement[] = text.split('').map((value, index) => {
if (centerIndex.includes(index)) {
return {
value,
@ -177,7 +177,30 @@ window.onload = function () {
const li = evt.target as HTMLLIElement
instance.command.executeRowMargin(Number(li.dataset.rowmargin!))
}
// 搜索、打印
// 图片上传、搜索、打印
const imageDom = document.querySelector<HTMLDivElement>('.menu-item__image')!
const imageFileDom = document.querySelector<HTMLInputElement>('#image')!
imageDom.onclick = function () {
imageFileDom.click()
}
imageFileDom.onchange = function () {
const file = imageFileDom.files?.[0]!
const fileReader = new FileReader()
fileReader.readAsDataURL(file)
fileReader.onload = function () {
// 计算宽高
const image = new Image()
const value = fileReader.result as string
image.src = value
image.onload = function () {
instance.command.executeImage({
value,
width: image.width,
height: image.height,
})
}
}
}
const collspanDom = document.querySelector<HTMLDivElement>('.menu-item__search__collapse')
const searchInputDom = document.querySelector<HTMLInputElement>('.menu-item__search__collapse__search input')
document.querySelector<HTMLDivElement>('.menu-item__search')!.onclick = function () {

@ -232,6 +232,14 @@ ul {
background-image: url('./assets/images/row-margin.svg');
}
.menu-item__image i {
background-image: url('./assets/images/image.svg');
}
.menu-item__image input {
display: none;
}
.menu-item__search {
position: relative;
}

Loading…
Cancel
Save