feat: add hyperlink
This commit is contained in:
@@ -111,6 +111,9 @@
|
||||
<i></i>
|
||||
<input type="file" id="image" accept=".png, .jpg, .jpeg">
|
||||
</div>
|
||||
<div class="menu-item__hyperlink">
|
||||
<i></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-divider"></div>
|
||||
<div class="menu-item">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8.9 8.192l4.242 4.243-.707.707L8.192 8.9 3.95 13.142l-.707-.707 4.242-4.243L3.243 3.95l.707-.707 4.242 4.242 4.243-4.242.707.707L8.9 8.192z" fill="#6A6A6A" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><g transform="rotate(45 5.5 13.328)" fill="#3D4757" fill-rule="evenodd"><path d="M7 6H6V3.515a2.5 2.5 0 10-5 0V6H0V3.515a3.5 3.5 0 117 0V6zm0 4v2.5a3.5 3.5 0 01-7 0V10h1v2.5a2.5 2.5 0 105 0V10h1z" fill-rule="nonzero"/><rect x="3.062" y="5.209" width="1" height="5.5" rx=".5"/></g></svg>
|
||||
|
After Width: | Height: | Size: 349 B |
@@ -0,0 +1,136 @@
|
||||
import { EditorComponent, EDITOR_COMPONENT } from '../../editor'
|
||||
import './dialog.css'
|
||||
|
||||
export interface IDialogData {
|
||||
type: string;
|
||||
label: string;
|
||||
name: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface IDialogConfirm {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IDialogOptions {
|
||||
onClose?: () => void;
|
||||
onCancel?: () => void;
|
||||
onConfirm?: (payload: IDialogConfirm[]) => void;
|
||||
title: string;
|
||||
data: IDialogData[];
|
||||
}
|
||||
|
||||
export class Dialog {
|
||||
|
||||
private options: IDialogOptions
|
||||
private mask: HTMLDivElement | null
|
||||
private container: HTMLDivElement | null
|
||||
private inputList: HTMLInputElement[]
|
||||
|
||||
constructor(options: IDialogOptions) {
|
||||
this.options = options
|
||||
this.mask = null
|
||||
this.container = null
|
||||
this.inputList = []
|
||||
this._render()
|
||||
}
|
||||
|
||||
private _render() {
|
||||
const { title, data, onClose, onCancel, onConfirm } = this.options
|
||||
// 渲染遮罩层
|
||||
const mask = document.createElement('div')
|
||||
mask.classList.add('dialog-mask')
|
||||
mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
document.body.append(mask)
|
||||
// 渲染容器
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('dialog-container')
|
||||
container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
// 弹窗
|
||||
const dialogContainer = document.createElement('div')
|
||||
dialogContainer.classList.add('dialog')
|
||||
container.append(dialogContainer)
|
||||
// 标题容器
|
||||
const titleContainer = document.createElement('div')
|
||||
titleContainer.classList.add('dialog-title')
|
||||
// 标题&关闭按钮
|
||||
const titleSpan = document.createElement('span')
|
||||
titleSpan.append(document.createTextNode(title))
|
||||
const titleClose = document.createElement('i')
|
||||
titleClose.onclick = () => {
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
titleContainer.append(titleSpan)
|
||||
titleContainer.append(titleClose)
|
||||
dialogContainer.append(titleContainer)
|
||||
// 选项容器
|
||||
const optionContainer = document.createElement('div')
|
||||
optionContainer.classList.add('dialog-option')
|
||||
// 选项
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const option = data[i]
|
||||
const optionItemContainer = document.createElement('div')
|
||||
optionItemContainer.classList.add('dialog-option__item')
|
||||
// 选项名称
|
||||
const optionName = document.createElement('span')
|
||||
optionName.append(document.createTextNode(option.label))
|
||||
optionItemContainer.append(optionName)
|
||||
// 选项输入框
|
||||
const optionInput = document.createElement('input')
|
||||
optionInput.type = option.type
|
||||
optionInput.name = option.name
|
||||
optionInput.value = option.value || ''
|
||||
optionInput.placeholder = option.placeholder || ''
|
||||
optionItemContainer.append(optionInput)
|
||||
optionContainer.append(optionItemContainer)
|
||||
this.inputList.push(optionInput)
|
||||
}
|
||||
dialogContainer.append(optionContainer)
|
||||
// 按钮容器
|
||||
const menuContainer = document.createElement('div')
|
||||
menuContainer.classList.add('dialog-menu')
|
||||
// 取消按钮
|
||||
const cancelBtn = document.createElement('button')
|
||||
cancelBtn.classList.add('dialog-menu__cancel')
|
||||
cancelBtn.append(document.createTextNode('取消'))
|
||||
cancelBtn.type = 'default'
|
||||
cancelBtn.onclick = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(cancelBtn)
|
||||
// 确认按钮
|
||||
const confirmBtn = document.createElement('button')
|
||||
confirmBtn.append(document.createTextNode('确定'))
|
||||
confirmBtn.type = 'primary'
|
||||
confirmBtn.onclick = () => {
|
||||
if (onConfirm) {
|
||||
const payload = this.inputList.map<IDialogConfirm>(input => ({
|
||||
name: input.name,
|
||||
value: input.value
|
||||
}))
|
||||
onConfirm(payload)
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(confirmBtn)
|
||||
dialogContainer.append(menuContainer)
|
||||
// 渲染
|
||||
document.body.append(container)
|
||||
this.container = container
|
||||
this.mask = mask
|
||||
}
|
||||
|
||||
private _dispose() {
|
||||
this.mask?.remove()
|
||||
this.container?.remove()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .5;
|
||||
background: #000000;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
z-index: 999;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: absolute;
|
||||
padding: 0 30px 30px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
position: relative;
|
||||
border-bottom: 1px solid #e2e6ed;
|
||||
margin-bottom: 30px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dialog-title i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background: url(../../assets/images/close.svg);
|
||||
}
|
||||
|
||||
.dialog-option__item {
|
||||
margin-bottom: 18px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog-option__item span {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
color: #3d4757;
|
||||
}
|
||||
|
||||
.dialog-option__item input {
|
||||
width: 276px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #d3d3d3;
|
||||
min-height: 30px;
|
||||
line-height: 17px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dialog-option__item input:focus {
|
||||
border-color: #4991f2;
|
||||
}
|
||||
|
||||
.dialog-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-menu button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
line-height: 22px;
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-menu button:hover {
|
||||
background: rgba(25, 55, 88, .04);
|
||||
}
|
||||
|
||||
.dialog-menu__cancel {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dialog-menu button[type='primary'] {
|
||||
color: #ffffff;
|
||||
background: #4991f2;
|
||||
border-color: #4991f2;
|
||||
}
|
||||
|
||||
.dialog-menu button[type='primary']:hover {
|
||||
background: #5b9cf3;
|
||||
border-color: #5b9cf3;
|
||||
}
|
||||
@@ -307,3 +307,27 @@
|
||||
.contextmenu-delete-table {
|
||||
background-image: url(../../assets/images/delete-table.svg);
|
||||
}
|
||||
|
||||
.hyperlink-popup {
|
||||
min-width: 100px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 12px 0 rgb(98 107 132 / 20%);
|
||||
border-radius: 2px;
|
||||
color: #3d4757;
|
||||
padding: 16px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hyperlink-popup a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
padding-bottom: 2px;
|
||||
color: #0000ff;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IElement } from "../.."
|
||||
import { RowFlex } from "../../dataset/enum/Row"
|
||||
import { IDrawImagePayload } from "../../interface/Draw"
|
||||
import { CommandAdapt } from "./CommandAdapt"
|
||||
@@ -34,6 +35,7 @@ export class Command {
|
||||
private static deleteTableCol: Function
|
||||
private static deleteTable: Function
|
||||
private static image: Function
|
||||
private static hyperlink: Function
|
||||
private static search: Function
|
||||
private static print: Function
|
||||
private static pageScaleRecovery: Function
|
||||
@@ -71,6 +73,7 @@ export class Command {
|
||||
Command.deleteTableCol = adapt.deleteTableCol.bind(adapt)
|
||||
Command.deleteTable = adapt.deleteTable.bind(adapt)
|
||||
Command.image = adapt.image.bind(adapt)
|
||||
Command.hyperlink = adapt.hyperlink.bind(adapt)
|
||||
Command.search = adapt.search.bind(adapt)
|
||||
Command.print = adapt.print.bind(adapt)
|
||||
Command.pageScaleRecovery = adapt.pageScaleRecovery.bind(adapt)
|
||||
@@ -165,7 +168,7 @@ export class Command {
|
||||
return Command.rowMargin(payload)
|
||||
}
|
||||
|
||||
// 表格、图片上传、搜索、打印
|
||||
// 表格、图片上传、超链接、搜索、打印
|
||||
public executeInsertTable(row: number, col: number) {
|
||||
return Command.insertTable(row, col)
|
||||
}
|
||||
@@ -198,6 +201,10 @@ export class Command {
|
||||
return Command.deleteTable()
|
||||
}
|
||||
|
||||
public executeHyperlink(payload: IElement) {
|
||||
return Command.hyperlink(payload)
|
||||
}
|
||||
|
||||
public executeImage(payload: IDrawImagePayload) {
|
||||
return Command.image(payload)
|
||||
}
|
||||
|
||||
@@ -654,6 +654,30 @@ export class CommandAdapt {
|
||||
}
|
||||
}
|
||||
|
||||
public hyperlink(payload: IElement) {
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (startIndex === 0 && endIndex === 0) return
|
||||
const elementList = this.draw.getElementList()
|
||||
const { valueList, url } = payload
|
||||
const hyperlinkId = getUUID()
|
||||
const newElementList = valueList?.map<IElement>(v => ({
|
||||
url,
|
||||
hyperlinkId,
|
||||
value: v.value,
|
||||
type: ElementType.HYPERLINK
|
||||
}))
|
||||
if (!newElementList) return
|
||||
const start = startIndex + 1
|
||||
if (startIndex === endIndex) {
|
||||
elementList.splice(start, 0, ...newElementList)
|
||||
} else {
|
||||
elementList.splice(start, endIndex - startIndex, ...newElementList)
|
||||
}
|
||||
const curIndex = start + newElementList.length - 1
|
||||
this.range.setRange(curIndex, curIndex)
|
||||
this.draw.render({ curIndex })
|
||||
}
|
||||
|
||||
public image(payload: IDrawImagePayload) {
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (startIndex === 0 && endIndex === 0) return
|
||||
@@ -706,7 +730,8 @@ export class CommandAdapt {
|
||||
// 搜索文本
|
||||
function searchClosure(payload: string | null, type: EditorContext, elementList: IElement[], restArgs?: ISearchResultRestArgs) {
|
||||
if (!payload) return
|
||||
const text = elementList.map(e => !e.type || e.type === ElementType.TEXT ? e.value : ZERO)
|
||||
const { TEXT, HYPERLINK } = ElementType
|
||||
const text = elementList.map(e => !e.type || e.type === TEXT || e.type === HYPERLINK ? e.value : ZERO)
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
const matchStartIndexList = []
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GlobalObserver } from "../observer/GlobalObserver"
|
||||
import { TableParticle } from "./particle/table/TableParticle"
|
||||
import { ISearchResult } from "../../interface/Search"
|
||||
import { TableTool } from "./particle/table/TableTool"
|
||||
import { HyperlinkParticle } from "./particle/HyperlinkParticle"
|
||||
|
||||
export class Draw {
|
||||
|
||||
@@ -54,6 +55,7 @@ export class Draw {
|
||||
private tableParticle: TableParticle
|
||||
private tableTool: TableTool
|
||||
private pageNumber: PageNumber
|
||||
private hyperlinkParticle: HyperlinkParticle
|
||||
|
||||
private rowList: IRow[]
|
||||
private painterStyle: IElementStyle | null
|
||||
@@ -92,6 +94,7 @@ export class Draw {
|
||||
this.tableParticle = new TableParticle(this)
|
||||
this.tableTool = new TableTool(this)
|
||||
this.pageNumber = new PageNumber(this)
|
||||
this.hyperlinkParticle = new HyperlinkParticle(this)
|
||||
new GlobalObserver(this)
|
||||
|
||||
this.canvasEvent = new CanvasEvent(this)
|
||||
@@ -251,6 +254,10 @@ export class Draw {
|
||||
return this.tableTool
|
||||
}
|
||||
|
||||
public getHyperlinkParticle(): HyperlinkParticle {
|
||||
return this.hyperlinkParticle
|
||||
}
|
||||
|
||||
public getRowCount(): number {
|
||||
return this.rowList.length
|
||||
}
|
||||
@@ -431,11 +438,10 @@ export class Draw {
|
||||
const ascent = metrics.boundingBoxAscent + rowMargin
|
||||
const descent = metrics.boundingBoxDescent + rowMargin
|
||||
const height = ascent + descent
|
||||
const rowElement: IRowElement = {
|
||||
...element,
|
||||
const rowElement: IRowElement = Object.assign(element, {
|
||||
metrics,
|
||||
style: this._getFont(element, scale)
|
||||
}
|
||||
})
|
||||
// 超过限定宽度
|
||||
const preElement = elementList[i - 1]
|
||||
if (
|
||||
@@ -509,9 +515,21 @@ export class Draw {
|
||||
}
|
||||
}
|
||||
positionList.push(positionItem)
|
||||
// 元素绘制
|
||||
if (element.type === ElementType.IMAGE) {
|
||||
this.textParticle.complete()
|
||||
this.imageParticle.render(ctx, element, x, y + offsetY)
|
||||
} else if (element.type === ElementType.TABLE) {
|
||||
this.tableParticle.render(ctx, element, x, y)
|
||||
} else if (element.type === ElementType.HYPERLINK) {
|
||||
this.textParticle.complete()
|
||||
this.hyperlinkParticle.render(ctx, element, x, y + offsetY)
|
||||
} else {
|
||||
this.textParticle.record(ctx, element, x, y + offsetY)
|
||||
}
|
||||
// 下划线绘制
|
||||
if (element.underline) {
|
||||
this.underline.render(ctx, x, y + curRow.height, metrics.width)
|
||||
this.underline.render(ctx, element.color!, x, y + curRow.height, metrics.width)
|
||||
}
|
||||
// 删除线绘制
|
||||
if (element.strikeout) {
|
||||
@@ -521,15 +539,6 @@ export class Draw {
|
||||
if (element.highlight) {
|
||||
this.highlight.render(ctx, element.highlight, x, y, metrics.width, curRow.height)
|
||||
}
|
||||
// 元素绘制
|
||||
if (element.type === ElementType.IMAGE) {
|
||||
this.textParticle.complete()
|
||||
this.imageParticle.render(ctx, element, x, y + offsetY)
|
||||
} else if (element.type === ElementType.TABLE) {
|
||||
this.tableParticle.render(ctx, element, x, y)
|
||||
} else {
|
||||
this.textParticle.record(ctx, element, x, y + offsetY)
|
||||
}
|
||||
// 选区绘制
|
||||
const { startIndex, endIndex } = this.range.getRange()
|
||||
if (startIndex !== endIndex && startIndex < index && index <= endIndex) {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { IElement } from "../../.."
|
||||
import { IEditorOption } from "../../../interface/Editor"
|
||||
import { IElementPosition } from "../../../interface/Element"
|
||||
import { IRowElement } from "../../../interface/Row"
|
||||
import { Draw } from "../Draw"
|
||||
|
||||
export class HyperlinkParticle {
|
||||
|
||||
private draw: Draw
|
||||
private options: Required<IEditorOption>
|
||||
private container: HTMLDivElement
|
||||
private hyperlinkPopupContainer: HTMLDivElement
|
||||
private hyperlinkDom: HTMLAnchorElement
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.draw = draw
|
||||
this.options = draw.getOptions()
|
||||
this.container = draw.getContainer()
|
||||
const { hyperlinkPopupContainer, hyperlinkDom } = this._createHyperlinkPopupDom()
|
||||
this.hyperlinkDom = hyperlinkDom
|
||||
this.hyperlinkPopupContainer = hyperlinkPopupContainer
|
||||
}
|
||||
|
||||
private _createHyperlinkPopupDom() {
|
||||
const hyperlinkPopupContainer = document.createElement('div')
|
||||
hyperlinkPopupContainer.classList.add('hyperlink-popup')
|
||||
const hyperlinkDom = document.createElement('a')
|
||||
hyperlinkDom.target = '_blank'
|
||||
hyperlinkPopupContainer.append(hyperlinkDom)
|
||||
this.container.append(hyperlinkPopupContainer)
|
||||
return { hyperlinkPopupContainer, hyperlinkDom }
|
||||
}
|
||||
|
||||
public drawHyperlinkPopup(element: IElement, position: IElementPosition) {
|
||||
const { coordinate: { leftTop: [left, top] }, lineHeight } = position
|
||||
const height = this.draw.getHeight()
|
||||
const pageGap = this.draw.getPageGap()
|
||||
const preY = this.draw.getPageNo() * (height + pageGap)
|
||||
// 位置
|
||||
this.hyperlinkPopupContainer.style.display = 'block'
|
||||
this.hyperlinkPopupContainer.style.left = `${left}px`
|
||||
this.hyperlinkPopupContainer.style.top = `${top + preY + lineHeight}px`
|
||||
// 标签
|
||||
const url = element.url || '#'
|
||||
this.hyperlinkDom.href = url
|
||||
this.hyperlinkDom.innerText = url
|
||||
}
|
||||
|
||||
public clearHyperlinkPopup() {
|
||||
this.hyperlinkPopupContainer.style.display = 'none'
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, element: IRowElement, x: number, y: number) {
|
||||
ctx.save()
|
||||
ctx.font = element.style
|
||||
if (!element.color) {
|
||||
element.color = this.options.defaultHyperlinkColor
|
||||
}
|
||||
ctx.fillStyle = element.color
|
||||
if (element.underline === undefined) {
|
||||
element.underline = true
|
||||
}
|
||||
ctx.fillText(element.value, x, y)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,10 +9,10 @@ export class Underline {
|
||||
this.options = draw.getOptions()
|
||||
}
|
||||
|
||||
public render(ctx: CanvasRenderingContext2D, x: number, y: number, width: number) {
|
||||
public render(ctx: CanvasRenderingContext2D, color: string, x: number, y: number, width: number) {
|
||||
const { underlineColor } = this.options
|
||||
ctx.save()
|
||||
ctx.strokeStyle = underlineColor
|
||||
ctx.strokeStyle = color || underlineColor
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.lineTo(x + width, y)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ElementType } from "../.."
|
||||
import { ZERO } from "../../dataset/constant/Common"
|
||||
import { ElementStyleKey } from "../../dataset/enum/ElementStyle"
|
||||
import { MouseEventButton } from "../../dataset/enum/Event"
|
||||
@@ -6,6 +7,7 @@ import { IElement } from "../../interface/Element"
|
||||
import { writeTextByElementList } from "../../utils/clipboard"
|
||||
import { Cursor } from "../cursor/Cursor"
|
||||
import { Draw } from "../draw/Draw"
|
||||
import { HyperlinkParticle } from "../draw/particle/HyperlinkParticle"
|
||||
import { ImageParticle } from "../draw/particle/ImageParticle"
|
||||
import { TableTool } from "../draw/particle/table/TableTool"
|
||||
import { HistoryManager } from "../history/HistoryManager"
|
||||
@@ -27,6 +29,7 @@ export class CanvasEvent {
|
||||
private historyManager: HistoryManager
|
||||
private imageParticle: ImageParticle
|
||||
private tableTool: TableTool
|
||||
private hyperlinkParticle: HyperlinkParticle
|
||||
|
||||
constructor(draw: Draw) {
|
||||
this.isAllowDrag = false
|
||||
@@ -42,6 +45,7 @@ export class CanvasEvent {
|
||||
this.historyManager = this.draw.getHistoryManager()
|
||||
this.imageParticle = this.draw.getImageParticle()
|
||||
this.tableTool = this.draw.getTableTool()
|
||||
this.hyperlinkParticle = this.draw.getHyperlinkParticle()
|
||||
}
|
||||
|
||||
public register() {
|
||||
@@ -160,20 +164,26 @@ export class CanvasEvent {
|
||||
isComputeRowList: false
|
||||
})
|
||||
}
|
||||
// 图片尺寸拖拽组件
|
||||
this.imageParticle.clearResizer()
|
||||
if (isDirectHitImage) {
|
||||
const elementList = this.draw.getElementList()
|
||||
const positionList = this.position.getPositionList()
|
||||
const curIndex = isTable ? tdValueIndex! : index
|
||||
this.imageParticle.drawResizer(elementList[curIndex], positionList[curIndex])
|
||||
const curElement = elementList[curIndex]
|
||||
// 图片尺寸拖拽组件
|
||||
this.imageParticle.clearResizer()
|
||||
if (isDirectHitImage) {
|
||||
this.imageParticle.drawResizer(curElement, positionList[curIndex])
|
||||
}
|
||||
// 表格工具组件
|
||||
this.tableTool.dispose()
|
||||
if (isTable) {
|
||||
const elementList = this.draw.getOriginalElementList()
|
||||
const positionList = this.position.getOriginalPositionList()
|
||||
this.tableTool.render(elementList[index], positionList[index])
|
||||
const originalElementList = this.draw.getOriginalElementList()
|
||||
const originalPositionList = this.position.getOriginalPositionList()
|
||||
this.tableTool.render(originalElementList[index], originalPositionList[index])
|
||||
}
|
||||
// 超链接
|
||||
this.hyperlinkParticle.clearHyperlinkPopup()
|
||||
if (curElement.type === ElementType.HYPERLINK) {
|
||||
this.hyperlinkParticle.drawHyperlinkPopup(curElement, positionList[curIndex])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +325,7 @@ export class CanvasEvent {
|
||||
if (!this.cursor) return
|
||||
const cursorPosition = this.position.getCursorPosition()
|
||||
if (!data || !cursorPosition || this.isCompositing) return
|
||||
const { TEXT, HYPERLINK } = ElementType
|
||||
const text = data.replaceAll(`\n`, ZERO)
|
||||
const elementList = this.draw.getElementList()
|
||||
const agentDom = this.cursor.getAgentDom()
|
||||
@@ -330,18 +341,32 @@ export class CanvasEvent {
|
||||
restArg = { tdId, trId, tableId }
|
||||
}
|
||||
const element = elementList[endIndex]
|
||||
const inputData: IElement[] = text.split('').map(value => ({
|
||||
const inputData: IElement[] = text.split('').map(value => {
|
||||
const newElement: IElement = {
|
||||
value,
|
||||
font: element.font,
|
||||
size: element.size,
|
||||
bold: element.bold,
|
||||
color: element.color,
|
||||
highlight: element.highlight,
|
||||
italic: element.italic,
|
||||
underline: element.underline,
|
||||
strikeout: element.strikeout,
|
||||
...restArg
|
||||
}))
|
||||
}
|
||||
if (
|
||||
!element.type
|
||||
|| element.type === TEXT
|
||||
|| (element.type === HYPERLINK && elementList[endIndex + 1]?.type === HYPERLINK)
|
||||
) {
|
||||
if (element.type) {
|
||||
newElement.type = element.type
|
||||
}
|
||||
newElement.font = element.font
|
||||
newElement.size = element.size
|
||||
newElement.bold = element.bold
|
||||
newElement.color = element.color
|
||||
newElement.highlight = element.highlight
|
||||
newElement.italic = element.italic
|
||||
newElement.underline = element.underline
|
||||
newElement.strikeout = element.strikeout
|
||||
newElement.url = element.url
|
||||
newElement.hyperlinkId = element.hyperlinkId
|
||||
}
|
||||
return newElement
|
||||
})
|
||||
let start = 0
|
||||
if (isCollspace) {
|
||||
start = index + 1
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IEditorOption } from "../../interface/Editor"
|
||||
import { findParent } from "../../utils"
|
||||
import { Cursor } from "../cursor/Cursor"
|
||||
import { Draw } from "../draw/Draw"
|
||||
import { HyperlinkParticle } from "../draw/particle/HyperlinkParticle"
|
||||
import { ImageParticle } from "../draw/particle/ImageParticle"
|
||||
import { TableTool } from "../draw/particle/table/TableTool"
|
||||
import { RangeManager } from "../range/RangeManager"
|
||||
@@ -18,6 +19,7 @@ export class GlobalEvent {
|
||||
private range: RangeManager
|
||||
private imageParticle: ImageParticle
|
||||
private tableTool: TableTool
|
||||
private hyperlinkParticle: HyperlinkParticle
|
||||
|
||||
constructor(draw: Draw, canvasEvent: CanvasEvent) {
|
||||
this.draw = draw
|
||||
@@ -28,6 +30,7 @@ export class GlobalEvent {
|
||||
this.range = draw.getRange()
|
||||
this.imageParticle = draw.getImageParticle()
|
||||
this.tableTool = draw.getTableTool()
|
||||
this.hyperlinkParticle = draw.getHyperlinkParticle()
|
||||
}
|
||||
|
||||
public register() {
|
||||
@@ -70,6 +73,7 @@ export class GlobalEvent {
|
||||
this.range.setRange(0, 0)
|
||||
this.imageParticle.clearResizer()
|
||||
this.tableTool.dispose()
|
||||
this.hyperlinkParticle.clearHyperlinkPopup()
|
||||
}
|
||||
|
||||
public setDragState() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum EditorComponent {
|
||||
COMPONENT = 'component',
|
||||
MENU = 'menu',
|
||||
MAIN = 'main',
|
||||
FOOTER = 'footer',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export enum ElementType {
|
||||
TEXT = 'text',
|
||||
IMAGE = 'image',
|
||||
TABLE = 'table'
|
||||
TABLE = 'table',
|
||||
HYPERLINK = 'hyperlink'
|
||||
}
|
||||
+6
-1
@@ -13,6 +13,8 @@ import { globalMenus } from './core/contextmenu/menus/GlobalMenus'
|
||||
import { ContextMenu } from './core/contextmenu/ContextMenu'
|
||||
import { tableMenus } from './core/contextmenu/menus/tableMenus'
|
||||
import { IRegisterContextMenu } from './interface/contextmenu/ContextMenu'
|
||||
import { EditorComponent } from './dataset/enum/Editor'
|
||||
import { EDITOR_COMPONENT } from './dataset/constant/Editor'
|
||||
|
||||
export default class Editor {
|
||||
|
||||
@@ -49,6 +51,7 @@ export default class Editor {
|
||||
margins: [100, 120, 100, 120],
|
||||
tdPadding: 5,
|
||||
defaultTdHeight: 40,
|
||||
defaultHyperlinkColor: '#0000FF',
|
||||
...options
|
||||
}
|
||||
formatElementList(elementList)
|
||||
@@ -74,7 +77,9 @@ export default class Editor {
|
||||
export {
|
||||
Editor,
|
||||
RowFlex,
|
||||
ElementType
|
||||
ElementType,
|
||||
EditorComponent,
|
||||
EDITOR_COMPONENT
|
||||
}
|
||||
|
||||
// 对外类型
|
||||
|
||||
@@ -26,4 +26,5 @@ export interface IEditorOption {
|
||||
margins?: [top: number, right: number, bootom: number, left: number],
|
||||
tdPadding?: number;
|
||||
defaultTdHeight?: number;
|
||||
defaultHyperlinkColor?: string;
|
||||
}
|
||||
@@ -35,9 +35,15 @@ export interface ITableElement {
|
||||
tableId?: string;
|
||||
}
|
||||
|
||||
export interface IHyperlinkElement {
|
||||
valueList?: IElement[];
|
||||
url?: string;
|
||||
hyperlinkId?: string;
|
||||
}
|
||||
|
||||
export type ITable = ITableAttr & ITableElement
|
||||
|
||||
export type IElement = IElementBasic & IElementStyle & ITable
|
||||
export type IElement = IElementBasic & IElementStyle & ITable & IHyperlinkElement
|
||||
|
||||
export interface IElementMetrics {
|
||||
width: number;
|
||||
|
||||
@@ -7,8 +7,9 @@ export function writeText(text: string) {
|
||||
}
|
||||
|
||||
export function writeTextByElementList(elementList: IElement[]) {
|
||||
const { TEXT, HYPERLINK } = ElementType
|
||||
const text = elementList
|
||||
.map(p => !p.type || p.type === ElementType.TEXT ? p.value : '')
|
||||
.map(p => !p.type || p.type === TEXT || p.type === HYPERLINK ? p.value : '')
|
||||
.join('')
|
||||
writeText(text)
|
||||
}
|
||||
@@ -8,7 +8,8 @@ export function formatElementList(elementList: IElement[]) {
|
||||
value: ZERO
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < elementList.length; i++) {
|
||||
let i = 0
|
||||
while (i < elementList.length) {
|
||||
const el = elementList[i]
|
||||
if (el.type === ElementType.TABLE) {
|
||||
const tableId = getUUID()
|
||||
@@ -32,6 +33,23 @@ export function formatElementList(elementList: IElement[]) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (el.type === ElementType.HYPERLINK) {
|
||||
const valueList = el.valueList || []
|
||||
// 移除父节点
|
||||
elementList.splice(i, 1)
|
||||
// 追加字节点
|
||||
if (valueList.length) {
|
||||
const hyperlinkId = getUUID()
|
||||
for (let v = 0; v < valueList.length; v++) {
|
||||
const value = valueList[v]
|
||||
value.type = el.type
|
||||
value.url = el.url
|
||||
value.hyperlinkId = hyperlinkId
|
||||
elementList.splice(i, 0, value)
|
||||
i++
|
||||
}
|
||||
}
|
||||
i--
|
||||
}
|
||||
if (el.value === '\n') {
|
||||
el.value = ZERO
|
||||
@@ -39,5 +57,6 @@ export function formatElementList(elementList: IElement[]) {
|
||||
if (el.type === ElementType.IMAGE) {
|
||||
el.id = getUUID()
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
+54
-2
@@ -1,9 +1,10 @@
|
||||
import './style.css'
|
||||
import Editor, { ElementType, IElement, RowFlex } from './editor'
|
||||
import { Dialog } from './components/dialog/Dialog'
|
||||
|
||||
window.onload = function () {
|
||||
|
||||
const text = `人民医院门诊病历\n主诉:\n发热三天,咳嗽五天。\n现病史:\n患者于三天前无明显诱因,感冒后发现面部水肿,无皮疹,尿量减少,出现乏力,在外治疗无好转,现来我院就诊。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。没有报告其他既往疾病。\n流行病史:\n否认14天内接触过新冠肺炎确诊患者、疑似患者、无症状感染者及其密切接触者;否认14天内去过以下场所:水产、肉类批发市场,农贸市场,集市,大型超市,夜市;否认14天内与以下场所工作人员密切接触:水产、肉类批发市场,农贸市场,集市,大型超市;否认14天内周围(如家庭、办公室)有2例以上聚集性发病;否认14天内接触过有发热或呼吸道症状的人员;否认14天内自身有发热或呼吸道症状;否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形;陪同家属{有无选择代码}有以上情况。\n体格检查:\nT:36.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10^9/L(偏高)参考值:0.1~0.6;\n门诊诊断:\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗:\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名:【】\n其他记录:`
|
||||
const text = `人民医院门诊病历\n主诉:\n发热三天,咳嗽五天。\n现病史:\n患者于三天前无明显诱因,感冒后发现面部水肿,无皮疹,尿量减少,出现乏力,在外治疗无好转,现来我院就诊。\n既往史:\n有糖尿病10年,有高血压2年,有传染性疾病1年。没有报告其他既往疾病。\n流行病史:\n否认14天内接触过确诊患者、疑似患者、无症状感染者及其密切接触者;否认14天内去过以下场所:水产、肉类批发市场,农贸市场,集市,大型超市,夜市;否认14天内与以下场所工作人员密切接触:水产、肉类批发市场,农贸市场,集市,大型超市;否认14天内周围(如家庭、办公室)有2例以上聚集性发病;否认14天内接触过有发热或呼吸道症状的人员;否认14天内自身有发热或呼吸道症状;否认14天内接触过纳入隔离观察的人员及其他可能与新冠肺炎关联的情形;陪同家属{有无选择代码}有以上情况。\n体格检查:\nT:36.5℃,P:80bpm,R:20次/分,BP:120/80mmHg;\n辅助检查:\n2020年6月10日,普放:血细胞比容36.50%(偏低)40~50;单核细胞绝对值0.75*10^9/L(偏高)参考值:0.1~0.6;\n门诊诊断:\n1.高血压\n2.糖尿病\n3.病毒性感冒\n4.过敏性鼻炎\n5.过敏性鼻息肉\n处置治疗:\n1.超声引导下甲状腺细针穿刺术;\n2.乙型肝炎表面抗体测定;\n3.膜式病变细胞采集术、后颈皮下肤层;\n电子签名:【】\n其他记录:`
|
||||
// 模拟行居中
|
||||
const centerText = ['人民医院门诊病历']
|
||||
const centerIndex: number[] = centerText.map(c => {
|
||||
@@ -62,7 +63,25 @@ window.onload = function () {
|
||||
size: 16
|
||||
}
|
||||
})
|
||||
data.splice(595, 0, {
|
||||
data.splice(137, 0, {
|
||||
type: ElementType.HYPERLINK,
|
||||
value: '',
|
||||
valueList: [{
|
||||
value: '新',
|
||||
size: 16
|
||||
}, {
|
||||
value: '冠',
|
||||
size: 16
|
||||
}, {
|
||||
value: '肺',
|
||||
size: 16
|
||||
}, {
|
||||
value: '炎',
|
||||
size: 16
|
||||
}],
|
||||
url: 'https://hufe.club/canvas-editor'
|
||||
})
|
||||
data.splice(592, 0, {
|
||||
value: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAAAgCAYAAAB5JtSmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAQ0SURBVGhD7dhrUSNBFAVgvKACEVjAAhJQgAIUYAABGEAABvgfAdn6UnWou01PppOZhIXNj1P9vo9zH5PK1Waz2V5wWlxIPgMuJJ8Bi0h+fn7eXl9fb29ubrYPDw/dO/8DHh8fu/vB4kym4Orqaofb29vund8OSSbhemewSrugBMnG3vlvw9vb265yn56edmtz/t/f33+5C8MkixQSZSsl9UzLOHUmcwTYAN/Rpl5eXnY+pnIB0Xd3d7s5m3rvDsrkCGszNiQ7r/tr4v39fSc/uipOqRcqufTHBiO78GGdzG5xcLtIFmVde7L9NsvXRo9s84+Pj+79pUAwn5GcD1wIz5r+fYGeJdnjGiF9hwL7iWAcfX19/evtKVHJXrtN8Rf4A3TVczqhrut5i1mSZQgnIriSWtdzP2N+EvIhi3/GWqHWtWXuy2IYbheiKarJZIZknkxyrryc2Utrgal+9S8iScUXIx/3kcxfe/jotcuDezLFlIbARDrzHpytXdKnQr4xyc74Vu9YV5Ih2Q/tT7mDSEYw5ZU4wu3nJx64k/1z9umlUG0hah/JSbC6Jzi5exDJWoTHERoBxu8uf/pT1j3HDkUIJitjbRfRA/iwVzlgy1RCfSF5ili9xj7BUWKs9wJZ3MpditYu+lsc+/PRx53cVF9Pdg/syE9Hb6cS75PkmhUEUFofmTvLGEXKimHueJP9Y3swWQwGLUiA9xEbHKuvgs4pPe1+1myTAKlw81buJ8kigjAXKauXPLQPhEYgJSEYsgdTUR0BmTVgc6C359wcvKGnBrGO8dO5VlD1ZZ519nrBHvrwKVMCas9hgL0YUI2wV98fC4FqCWizzXyqF44A0ZKLHkilgvPs1zbiTuZIdZ414KvqGCKZYx4zple+MSrrJVncAyL02/TOqncJwVMglx5zI4QDZ5WPvBGEcNP+7TlEcqJIAQFGsIdQjmZt7MlYA5yiI3pOQTCQXUm2TuVmXgmewxDJQDgl6deJJoU5y7p9uwZagmu1mCvbNoOOBfkhOf6lRZjzPb8qRjBMMiUhM9GNMZQq5/oRXBP7Mlj/i12A7EMIaJGqDcl8I79+/N1xTvdINQ2TDAQSvI9Md479vdqCHKSFQKAfEmgBqCTDkjaSgOZXQkg2jy1ti0xApnBQJo/0obQRipeQXbN3CmxKGQch5xgki4Efghl/kFqzPD//2DnXIodIRpaoETaXxcmwGNO7N4I2Oyuc6b+xK/tL9IH3kY/E+r1JdST4yM+7VUiuJbuPZHBeHZcNvXtziMMV9mRuvUOX8Vg9IFjRx9dUYM3s2oJyNx9ahFfSWwyRHKHG3nmL2q/mojyFVAWnEdi2Hg7OBXwUCCKr1QEtoe0+/9jI3xqIiuF2QRD0zqcwpfQnge9TVSI4tWrNe79shj98F0xDC0N4bTUVF5LPgAvJJ8dm+wcP2iJuZNdC5QAAAABJRU5ErkJggg==`,
|
||||
width: 89,
|
||||
height: 32,
|
||||
@@ -386,6 +405,39 @@ window.onload = function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
const hyperlinkDom = document.querySelector<HTMLDivElement>('.menu-item__hyperlink')!
|
||||
hyperlinkDom.onclick = function () {
|
||||
console.log('hyperlink')
|
||||
new Dialog({
|
||||
title: '超链接',
|
||||
data: [{
|
||||
type: 'text',
|
||||
label: '文本',
|
||||
name: 'name',
|
||||
placeholder: '请输入文本'
|
||||
}, {
|
||||
type: 'text',
|
||||
label: '链接',
|
||||
name: 'url',
|
||||
placeholder: '请输入链接'
|
||||
}],
|
||||
onConfirm: (payload) => {
|
||||
const name = payload.find(p => p.name === 'name')?.value
|
||||
if (!name) return
|
||||
const url = payload.find(p => p.name === 'url')?.value
|
||||
if (!url) return
|
||||
instance.command.executeHyperlink({
|
||||
type: ElementType.HYPERLINK,
|
||||
value: '',
|
||||
url,
|
||||
valueList: name.split('').map(n => ({
|
||||
value: n,
|
||||
size: 16
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
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 () {
|
||||
|
||||
@@ -357,6 +357,10 @@ ul {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.menu-item__hyperlink i {
|
||||
background-image: url('./assets/images/hyperlink.svg');
|
||||
}
|
||||
|
||||
.menu-item__search {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user