import { data, options } from './mock' import './style.css' import prism from 'prismjs' import Editor, { ControlType, EditorMode, ElementType, IElement } from './editor' import { Dialog } from './components/dialog/Dialog' import { formatPrismToken } from './utils/prism' window.onload = function () { // 1. 初始化编辑器 const container = document.querySelector('.editor')! const instance = new Editor(container, data, options) console.log('实例: ', instance) // cypress使用 Reflect.set(window, 'editor', instance) // 2. | 撤销 | 重做 | 格式刷 | 清除格式 | const undoDom = document.querySelector('.menu-item__undo')! undoDom.onclick = function () { console.log('undo') instance.command.executeUndo() } const redoDom = document.querySelector('.menu-item__redo')! redoDom.onclick = function () { console.log('redo') instance.command.executeRedo() } const painterDom = document.querySelector('.menu-item__painter')! painterDom.onclick = function () { console.log('painter') instance.command.executePainter({ isDblclick: false }) } painterDom.ondblclick = function () { console.log('painter') instance.command.executePainter({ isDblclick: true }) } document.querySelector('.menu-item__format')!.onclick = function () { console.log('format') instance.command.executeFormat() } // 3. | 字体 | 字体变大 | 字体变小 | 加粗 | 斜体 | 下划线 | 删除线 | 上标 | 下标 | 字体颜色 | 背景色 | const fontDom = document.querySelector('.menu-item__font')! const fontSelectDom = fontDom.querySelector('.select')! const fontOptionDom = fontDom.querySelector('.options')! fontDom.onclick = function () { console.log('font') fontOptionDom.classList.toggle('visible') } fontOptionDom.onclick = function (evt) { const li = evt.target as HTMLLIElement instance.command.executeFont(li.dataset.family!) } document.querySelector('.menu-item__size-add')!.onclick = function () { console.log('size-add') instance.command.executeSizeAdd() } document.querySelector('.menu-item__size-minus')!.onclick = function () { console.log('size-minus') instance.command.executeSizeMinus() } const boldDom = document.querySelector('.menu-item__bold')! boldDom.onclick = function () { console.log('bold') instance.command.executeBold() } const italicDom = document.querySelector('.menu-item__italic')! italicDom.onclick = function () { console.log('italic') instance.command.executeItalic() } const underlineDom = document.querySelector('.menu-item__underline')! underlineDom.onclick = function () { console.log('underline') instance.command.executeUnderline() } const strikeoutDom = document.querySelector('.menu-item__strikeout')! strikeoutDom.onclick = function () { console.log('strikeout') instance.command.executeStrikeout() } const superscriptDom = document.querySelector('.menu-item__superscript')! superscriptDom.onclick = function () { console.log('superscript') instance.command.executeSuperscript() } const subscriptDom = document.querySelector('.menu-item__subscript')! subscriptDom.onclick = function () { console.log('subscript') instance.command.executeSubscript() } const colorControlDom = document.querySelector('#color')! colorControlDom.onchange = function () { instance.command.executeColor(colorControlDom!.value) } const colorDom = document.querySelector('.menu-item__color')! const colorSpanDom = colorDom.querySelector('span')! colorDom.onclick = function () { console.log('color') colorControlDom.click() } const highlightControlDom = document.querySelector('#highlight')! highlightControlDom.onchange = function () { instance.command.executeHighlight(highlightControlDom.value) } const highlightDom = document.querySelector('.menu-item__highlight')! const highlightSpanDom = highlightDom.querySelector('span')! highlightDom.onclick = function () { console.log('highlight') highlightControlDom?.click() } const leftDom = document.querySelector('.menu-item__left')! leftDom.onclick = function () { console.log('left') instance.command.executeLeft() } const centerDom = document.querySelector('.menu-item__center')! centerDom.onclick = function () { console.log('center') instance.command.executeCenter() } const rightDom = document.querySelector('.menu-item__right')! rightDom.onclick = function () { console.log('right') instance.command.executeRight() } const rowMarginDom = document.querySelector('.menu-item__row-margin')! const rowOptionDom = rowMarginDom.querySelector('.options')! rowMarginDom.onclick = function () { console.log('row-margin') rowOptionDom.classList.toggle('visible') } rowOptionDom.onclick = function (evt) { const li = evt.target as HTMLLIElement instance.command.executeRowMargin(Number(li.dataset.rowmargin!)) } // 4. | 表格 | 图片 | 超链接 | 分割线 | 水印 | 代码块 | 分隔符 | const tableDom = document.querySelector('.menu-item__table')! const tablePanelContainer = document.querySelector('.menu-item__table__collapse')! const tableClose = document.querySelector('.table-close')! const tableTitle = document.querySelector('.table-select')! const tablePanel = document.querySelector('.table-panel')! // 绘制行列 const tableCellList: HTMLDivElement[][] = [] for (let i = 0; i < 10; i++) { const tr = document.createElement('tr') tr.classList.add('table-row') const trCellList: HTMLDivElement[] = [] for (let j = 0; j < 10; j++) { const td = document.createElement('td') td.classList.add('table-cel') tr.append(td) trCellList.push(td) } tablePanel.append(tr) tableCellList.push(trCellList) } let colIndex = 0 let rowIndex = 0 // 移除所有格选择 function removeAllTableCellSelect() { tableCellList.forEach(tr => { tr.forEach(td => td.classList.remove('active')) }) } // 设置标题内容 function setTableTitle(payload: string) { tableTitle.innerText = payload } // 恢复初始状态 function recoveryTable() { // 还原选择样式、标题、选择行列 removeAllTableCellSelect() setTableTitle('插入') colIndex = 0 rowIndex = 0 // 隐藏panel tablePanelContainer.style.display = 'none' } tableDom.onclick = function () { console.log('table') tablePanelContainer!.style.display = 'block' } tablePanel.onmousemove = function (evt) { const celSize = 16 const rowMarginTop = 10 const celMarginRight = 6 const { offsetX, offsetY } = evt // 移除所有选择 removeAllTableCellSelect() colIndex = Math.ceil(offsetX / (celSize + celMarginRight)) || 1 rowIndex = Math.ceil(offsetY / (celSize + rowMarginTop)) || 1 // 改变选择样式 tableCellList.forEach((tr, trIndex) => { tr.forEach((td, tdIndex) => { if (tdIndex < colIndex && trIndex < rowIndex) { td.classList.add('active') } }) }) // 改变表格标题 setTableTitle(`${rowIndex}×${colIndex}`) } tableClose.onclick = function () { recoveryTable() } tablePanel.onclick = function () { // 应用选择 instance.command.executeInsertTable(rowIndex, colIndex) recoveryTable() } const imageDom = document.querySelector('.menu-item__image')! const imageFileDom = document.querySelector('#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, }) imageFileDom.value = '' } } } const hyperlinkDom = document.querySelector('.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 separatorDom = document.querySelector('.menu-item__separator')! const separatorOptionDom = separatorDom.querySelector('.options')! separatorDom.onclick = function () { console.log('separator') separatorOptionDom.classList.toggle('visible') } separatorOptionDom.onmousedown = function (evt) { let payload: number[] = [] const li = evt.target as HTMLLIElement const separatorDash = li.dataset.separator?.split(',').map(Number) if (separatorDash) { const isSingleLine = separatorDash.every(d => d === 0) if (!isSingleLine) { payload = separatorDash } } instance.command.executeSeparator(payload) } const pageBreakDom = document.querySelector('.menu-item__page-break')! pageBreakDom.onclick = function () { console.log('pageBreak') instance.command.executePageBreak() } const watermarkDom = document.querySelector('.menu-item__watermark')! const watermarkOptionDom = watermarkDom.querySelector('.options')! watermarkDom.onclick = function () { console.log('watermark') watermarkOptionDom.classList.toggle('visible') } watermarkOptionDom.onmousedown = function (evt) { const li = evt.target as HTMLLIElement const menu = li.dataset.menu! watermarkOptionDom.classList.toggle('visible') if (menu === 'add') { new Dialog({ title: '水印', data: [{ type: 'text', label: '内容', name: 'data', placeholder: '请输入内容' }, { type: 'color', label: '颜色', name: 'color', value: '#AEB5C0' }, { type: 'number', label: '字体大小', name: 'size', value: '120' }], onConfirm: (payload) => { const nullableIndex = payload.findIndex(p => !p.value) if (~nullableIndex) return const watermark = payload.reduce((pre, cur) => { pre[cur.name] = cur.value return pre }, {}) instance.command.executeAddWatermark({ data: watermark.data, color: watermark.color, size: Number(watermark.size) }) } }) } else { instance.command.executeDeleteWatermark() } } const codeblockDom = document.querySelector('.menu-item__codeblock')! codeblockDom.onclick = function () { console.log('codeblock') new Dialog({ title: '代码块', data: [{ type: 'textarea', name: 'codeblock', placeholder: '请输入代码', width: 500, height: 300 }], onConfirm: (payload) => { const codeblock = payload.find(p => p.name === 'codeblock')?.value if (!codeblock) return const tokenList = prism.tokenize(codeblock, prism.languages.javascript) const formatTokenList = formatPrismToken(tokenList) const elementList: IElement[] = [] for (let i = 0; i < formatTokenList.length; i++) { const formatToken = formatTokenList[i] const tokenStringList = formatToken.content.split('') for (let j = 0; j < tokenStringList.length; j++) { const value = tokenStringList[j] const element: IElement = { value } if (formatToken.color) { element.color = formatToken.color } if (formatToken.bold) { element.bold = true } if (formatToken.italic) { element.italic = true } elementList.push(element) } } elementList.unshift({ value: '\n' }) instance.command.executeInsertElementList(elementList) } }) } const controlDom = document.querySelector('.menu-item__control')! const controlOptionDom = controlDom.querySelector('.options')! controlDom.onclick = function () { console.log('control') controlOptionDom.classList.toggle('visible') } controlOptionDom.onmousedown = function (evt) { controlOptionDom.classList.toggle('visible') const li = evt.target as HTMLLIElement const type = li.dataset.control switch (type) { case ControlType.TEXT: new Dialog({ title: '文本控件', data: [{ type: 'text', label: '占位符', name: 'placeholder', placeholder: '请输入占位符' }, { type: 'text', label: '默认值', name: 'value', placeholder: '请输入默认值' }], onConfirm: (payload) => { const placeholder = payload.find(p => p.name === 'placeholder')?.value if (!placeholder) return const value = payload.find(p => p.name === 'value')?.value || '' instance.command.executeInsertElementList([{ type: ElementType.CONTROL, value: '', control: { type, value: value ? [{ value }] : null, placeholder } }]) } }) break case ControlType.SELECT: new Dialog({ title: '列举控件', data: [{ type: 'text', label: '占位符', name: 'placeholder', placeholder: '请输入占位符' }, { type: 'text', label: '默认值', name: 'code', placeholder: '请输入默认值' }, { type: 'textarea', label: '值集', name: 'valueSets', height: 100, placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` }], onConfirm: (payload) => { const placeholder = payload.find(p => p.name === 'placeholder')?.value if (!placeholder) return const valueSets = payload.find(p => p.name === 'valueSets')?.value if (!valueSets) return const code = payload.find(p => p.name === 'code')?.value instance.command.executeInsertElementList([{ type: ElementType.CONTROL, value: '', control: { type, code, value: null, placeholder, valueSets: JSON.parse(valueSets) } }]) } }) break case ControlType.CHECKBOX: new Dialog({ title: '复选框控件', data: [{ type: 'text', label: '默认值', name: 'code', placeholder: '请输入默认值,多个值以英文逗号分割' }, { type: 'textarea', label: '值集', name: 'valueSets', height: 100, placeholder: `请输入值集JSON,例:\n[{\n"value":"有",\n"code":"98175"\n}]` }], onConfirm: (payload) => { const valueSets = payload.find(p => p.name === 'valueSets')?.value if (!valueSets) return const code = payload.find(p => p.name === 'code')?.value instance.command.executeInsertElementList([{ type: ElementType.CONTROL, value: '', control: { type, code, value: null, valueSets: JSON.parse(valueSets) } }]) } }) break default: break } } const checkboxDom = document.querySelector('.menu-item__checkbox')! checkboxDom.onclick = function () { console.log('checkbox') instance.command.executeInsertElementList([{ type: ElementType.CHECKBOX, value: '' }]) } // 5. | 搜索&替换 | 打印 | const searchCollapseDom = document.querySelector('.menu-item__search__collapse')! const searchInputDom = document.querySelector('.menu-item__search__collapse__search input')! const replaceInputDom = document.querySelector('.menu-item__search__collapse__replace input')! const searchDom = document.querySelector('.menu-item__search')! searchDom.onclick = function () { console.log('search') searchCollapseDom.style.display = 'block' const bodyRect = document.body.getBoundingClientRect() const searchRect = searchDom.getBoundingClientRect() const searchCollapseRect = searchCollapseDom.getBoundingClientRect() if (searchRect.left + searchCollapseRect.width > bodyRect.width) { searchCollapseDom.style.right = '0px' searchCollapseDom.style.left = 'unset' } else { searchCollapseDom.style.right = 'unset' } } searchCollapseDom.querySelector('span')!.onclick = function () { searchCollapseDom.style.display = 'none' searchInputDom.value = '' replaceInputDom.value = '' instance.command.executeSearch(null) } searchInputDom.oninput = function () { instance.command.executeSearch(searchInputDom.value || null) } searchInputDom.onkeydown = function (evt) { if (evt.key === 'Enter') { instance.command.executeSearch(searchInputDom.value || null) } } searchCollapseDom.querySelector('button')!.onclick = function () { const searchValue = searchInputDom.value const replaceValue = replaceInputDom.value if (searchValue && replaceValue && searchValue !== replaceValue) { instance.command.executeReplace(replaceValue) } } document.querySelector('.menu-item__print')!.onclick = function () { console.log('print') instance.command.executePrint() } // 6. 纸张缩放 document.querySelector('.page-scale-percentage')!.onclick = function () { console.log('page-scale-recovery') instance.command.executePageScaleRecovery() } document.querySelector('.page-scale-minus')!.onclick = function () { console.log('page-scale-minus') instance.command.executePageScaleMinus() } document.querySelector('.page-scale-add')!.onclick = function () { console.log('page-scale-add') instance.command.executePageScaleAdd() } // 7. 编辑器使用模式 let modeIndex = 0 const modeList = [{ mode: EditorMode.EDIT, name: '编辑模式' }, { mode: EditorMode.CLEAN, name: '清洁模式' }, { mode: EditorMode.READONLY, name: '只读模式' }] const modeElement = document.querySelector('.editor-mode')! modeElement.onclick = function () { // 模式选择循环 modeIndex === 2 ? modeIndex = 0 : modeIndex++ // 设置模式 const { name, mode } = modeList[modeIndex] modeElement.innerText = name instance.command.executeMode(mode) // 设置菜单栏权限视觉反馈 const isReadonly = mode === EditorMode.READONLY const enableMenuList = ['search', 'print'] document.querySelectorAll('.menu-item>div').forEach(dom => { const menu = dom.dataset.menu isReadonly && (!menu || !enableMenuList.includes(menu)) ? dom.classList.add('disable') : dom.classList.remove('disable') }) } // 8. 内部事件监听 instance.listener.rangeStyleChange = function (payload) { // 控件类型 payload.type === ElementType.SUBSCRIPT ? subscriptDom.classList.add('active') : subscriptDom.classList.remove('active') payload.type === ElementType.SUPERSCRIPT ? superscriptDom.classList.add('active') : superscriptDom.classList.remove('active') payload.type === ElementType.SEPARATOR ? separatorDom.classList.add('active') : separatorDom.classList.remove('active') separatorOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active')) if (payload.type === ElementType.SEPARATOR) { const separator = payload.dashArray.join(',') || '0,0' const curSeparatorDom = separatorOptionDom.querySelector(`[data-separator='${separator}']`)! if (curSeparatorDom) { curSeparatorDom.classList.add('active') } } // 富文本 fontOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active')) const curFontDom = fontOptionDom.querySelector(`[data-family=${payload.font}]`) if (curFontDom) { fontSelectDom.innerText = curFontDom.innerText curFontDom.classList.add('active') } payload.bold ? boldDom.classList.add('active') : boldDom.classList.remove('active') payload.italic ? italicDom.classList.add('active') : italicDom.classList.remove('active') payload.underline ? underlineDom.classList.add('active') : underlineDom.classList.remove('active') payload.strikeout ? strikeoutDom.classList.add('active') : strikeoutDom.classList.remove('active') if (payload.color) { colorDom.classList.add('active') colorControlDom.value = payload.color colorSpanDom.style.backgroundColor = payload.color } else { colorDom.classList.remove('active') colorControlDom.value = '#000000' colorSpanDom.style.backgroundColor = '#000000' } if (payload.highlight) { highlightDom.classList.add('active') highlightControlDom.value = payload.highlight highlightSpanDom.style.backgroundColor = payload.highlight } else { highlightDom.classList.remove('active') highlightControlDom.value = '#ffff00' highlightSpanDom.style.backgroundColor = '#ffff00' } // 行布局 leftDom.classList.remove('active') centerDom.classList.remove('active') rightDom.classList.remove('active') if (payload.rowFlex && payload.rowFlex === 'right') { rightDom.classList.add('active') } else if (payload.rowFlex && payload.rowFlex === 'center') { centerDom.classList.add('active') } else { leftDom.classList.add('active') } // 行间距 rowOptionDom.querySelectorAll('li').forEach(li => li.classList.remove('active')) const curRowMarginDom = rowOptionDom.querySelector(`[data-rowmargin='${payload.rowMargin}']`)! curRowMarginDom.classList.add('active') // 功能 payload.undo ? undoDom.classList.remove('no-allow') : undoDom.classList.add('no-allow') payload.redo ? redoDom.classList.remove('no-allow') : redoDom.classList.add('no-allow') payload.painter ? painterDom.classList.add('active') : painterDom.classList.remove('active') } instance.listener.visiblePageNoListChange = function (payload) { const text = payload.map(i => i + 1).join('、') document.querySelector('.page-no-list')!.innerText = text } instance.listener.pageSizeChange = function (payload) { document.querySelector('.page-size')!.innerText = `${payload}` } instance.listener.intersectionPageNoChange = function (payload) { document.querySelector('.page-no')!.innerText = `${payload + 1}` } instance.listener.pageScaleChange = function (payload) { document.querySelector('.page-scale-percentage')!.innerText = `${Math.floor(payload * 10 * 10)}%` } instance.listener.controlChange = function (payload) { const disableMenusInControlContext = [ 'superscript', 'subscript', 'table', 'image', 'hyperlink', 'separator', 'codeblock', 'page-break', 'control', 'checkbox' ] // 菜单操作权限 disableMenusInControlContext.forEach(menu => { const menuDom = document.querySelector(`.menu-item__${menu}`)! payload ? menuDom.classList.add('disable') : menuDom.classList.remove('disable') }) } instance.listener.saved = function (payload) { console.log('elementList: ', payload) } }