From e05091ecff54f4c93edf7f9e89103cc425046ea9 Mon Sep 17 00:00:00 2001 From: Saraph1nes Date: Thu, 29 Aug 2024 15:54:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20#876=20=E7=AA=97=E5=8F=A3=E6=B5=AE?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/scripts/index-demo.js | 1 + src/Cherry.config.js | 1 + src/Cherry.js | 85 ++++++++++++++++++++++++++++- src/Previewer.js | 28 ++++++++++ src/sass/cherry.scss | 45 +++++++++++++++ src/toolbars/hooks/TogglePreview.js | 12 ++++ types/cherry.d.ts | 1 + types/previewer.d.ts | 2 + 8 files changed, 174 insertions(+), 1 deletion(-) diff --git a/examples/scripts/index-demo.js b/examples/scripts/index-demo.js index 7add26d1..9bac39a8 100644 --- a/examples/scripts/index-demo.js +++ b/examples/scripts/index-demo.js @@ -235,6 +235,7 @@ var basicConfig = { previewer: { // 自定义markdown预览区域class // className: 'markdown' + floatWhenClosePreviewer: true, }, keydown: [], //extensions: [], diff --git a/src/Cherry.config.js b/src/Cherry.config.js index f410ce01..32f00ef1 100644 --- a/src/Cherry.config.js +++ b/src/Cherry.config.js @@ -481,6 +481,7 @@ const defaultConfig = { className: 'cherry-markdown', // 是否启用预览区域编辑能力(目前支持编辑图片尺寸、编辑表格内容) enablePreviewerBubble: true, + floatWhenClosePreviewer: false, /** * 配置图片懒加载的逻辑 * - 如果不希望图片懒加载,可配置成 lazyLoadImg = {noLoadImgNum: -1} diff --git a/src/Cherry.js b/src/Cherry.js index 4a97eae1..b53f44ba 100644 --- a/src/Cherry.js +++ b/src/Cherry.js @@ -67,6 +67,13 @@ export default class Cherry extends CherryStatic { */ this.options = mergeWith({}, defaultConfigCopy, options, customizer); + this.storageFloatPreviewerWrapData = { + x: 50, + y: 58, + width: 800, + height: 500, + }; + this.locales = locales; if (this.options.locales) { this.locales = { @@ -737,7 +744,7 @@ export default class Cherry extends CherryStatic { const anchorStyle = (this.options.engine.syntax.header && this.options.engine.syntax.header.anchorStyle) || 'default'; const autonumberClass = anchorStyle === 'autonumber' ? ' head-num' : ''; - const { className, dom, enablePreviewerBubble } = this.options.previewer; + const { className, dom, enablePreviewerBubble, floatWhenClosePreviewer } = this.options.previewer; const previewerClassName = [ 'cherry-previewer cherry-markdown', className || '', @@ -763,12 +770,88 @@ export default class Cherry extends CherryStatic { value: this.options.value, isPreviewOnly: this.options.isPreviewOnly, enablePreviewerBubble, + floatWhenClosePreviewer, lazyLoadImg: this.options.previewer.lazyLoadImg, }); return this.previewer; } + clearFloatPreviewer() { + this.wrapperDom.appendChild(this.previewer.getDom()); + this.storageFloatPreviewerWrapData = { + x: this.floatPreviewerWrapDom.offsetLeft, + y: this.floatPreviewerWrapDom.offsetTop, + height: this.floatPreviewerWrapDom.offsetHeight, + width: this.floatPreviewerWrapDom.offsetWidth, + }; + this.floatPreviewerWrapDom.remove(); + } + + /** + * @private + * @returns {import('@/Previewer').default} + */ + createFloatPreviewer() { + const floatPreviewerWrap = createElement('div', 'float-previewer-wrap'); + const floatPreviewerHeader = createElement('div', 'float-previewer-header'); + const floatPreviewerClose = createElement('div', 'float-previewer-close'); + const floatPreviewerTitle = createElement('div', 'float-previewer-title'); + floatPreviewerTitle.innerHTML = '预览'; + floatPreviewerWrap.style.left = `${this.storageFloatPreviewerWrapData.x}px`; + floatPreviewerWrap.style.top = `${this.storageFloatPreviewerWrapData.y}px`; + floatPreviewerWrap.style.height = `${this.storageFloatPreviewerWrapData.height}px`; + floatPreviewerWrap.style.width = `${this.storageFloatPreviewerWrapData.width}px`; + floatPreviewerHeader.appendChild(floatPreviewerTitle); + floatPreviewerHeader.appendChild(floatPreviewerClose); + floatPreviewerWrap.appendChild(floatPreviewerHeader); + floatPreviewerWrap.appendChild(this.previewer.getDom()); + this.floatPreviewerWrapDom = floatPreviewerWrap; + this.wrapperDom.appendChild(floatPreviewerWrap); + + const pageWidth = document.body.clientWidth; + const pageHeight = document.body.clientHeight; + + let initOffsetX = 0; + let initOffsetY = 0; + + document.addEventListener('mousedown', (evt) => { + if (evt.target !== floatPreviewerHeader) return; + evt.preventDefault(); + initOffsetX = evt.offsetX; + initOffsetY = evt.offsetY; + floatPreviewerWrap.classList.add('float-previewer-dragging'); + }); + + document.addEventListener('mouseup', (evt) => { + floatPreviewerWrap.classList.remove('float-previewer-dragging'); + }); + + document.addEventListener('mousemove', (evt) => { + if (!floatPreviewerWrap.classList.contains('float-previewer-dragging')) return; + evt.preventDefault(); + const { clientX, clientY } = evt; + let newRight = clientX - initOffsetX; + let newTop = clientY - initOffsetY; + if (newRight < 0) { + newRight = 0; + } + if (newTop < 0) { + newTop = 0; + } + if (newRight + floatPreviewerWrap.offsetWidth > pageWidth) { + newRight = pageWidth - floatPreviewerWrap.offsetWidth; + } + if (newTop + floatPreviewerWrap.offsetHeight > pageHeight) { + newTop = pageHeight - floatPreviewerWrap.offsetHeight; + } + requestAnimationFrame(() => { + floatPreviewerWrap.style.left = `${newRight}px`; + floatPreviewerWrap.style.top = `${newTop}px`; + }); + }); + } + /** * @private * @param {import('codemirror').Editor} codemirror diff --git a/src/Previewer.js b/src/Previewer.js index 348c0c85..5c18e35a 100644 --- a/src/Previewer.js +++ b/src/Previewer.js @@ -72,6 +72,7 @@ export default class Previewer { minBlockPercentage: 0.2, // editor或previewer所占宽度比例的最小值 value: '', enablePreviewerBubble: true, + floatWhenClosePreviewer: false, // 是否在关闭预览区时,将预览区浮动 afterUpdateCallBack: [], isPreviewOnly: false, previewerCache: { @@ -190,6 +191,15 @@ export default class Previewer { return this.options.previewerDom.classList.contains('cherry-previewer--hidden'); } + isPreviewerFloat() { + const floatDom = this.$cherry.cherryDom.querySelector('.float-previewer-wrap'); + return this.$cherry.cherryDom.contains(floatDom); + } + + isPreviewerNeedFloat() { + return this.options.floatWhenClosePreviewer; + } + calculateRealLayout(editorWidth) { // 根据editor的绝对宽度计算editor和previewer的百分比宽度 const editorDomWidth = this.editor.options.editorDom.getBoundingClientRect().width; @@ -775,6 +785,24 @@ export default class Previewer { this.$cherry.$event.emit('editorOpen'); } + floatPreviewer() { + const fullEditorLayout = { + editorPercentage: '100%', + previewerPercentage: '100%', + }; + const editorWidth = this.editor.options.editorDom.getBoundingClientRect().width; + const layout = this.calculateRealLayout(editorWidth); + this.options.previewerCache.layout = layout; + this.setRealLayout(fullEditorLayout.editorPercentage, fullEditorLayout.previewerPercentage); + this.options.virtualDragLineDom.classList.add('cherry-drag--hidden'); + this.$cherry.createFloatPreviewer(); + } + + recoverFloatPreviewer() { + this.recoverPreviewer(true); + this.$cherry.clearFloatPreviewer(); + } + recoverPreviewer(dealToolbar = false) { this.options.previewerDom.classList.remove('cherry-previewer--hidden'); this.options.virtualDragLineDom.classList.remove('cherry-drag--hidden'); diff --git a/src/sass/cherry.scss b/src/sass/cherry.scss index f7799dfd..3d9b0369 100644 --- a/src/sass/cherry.scss +++ b/src/sass/cherry.scss @@ -695,6 +695,51 @@ } } +.float-previewer-wrap { + position: fixed; + right: 0; + top: 0; + z-index: 100; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 0 60px rgba(0, 0, 0, 0.1); + resize: both; + min-width: 430px; + min-height: 300px; + + &.float-previewer-dragging{ + box-shadow: 0 0 60px rgba(0, 0, 0, 0.3); + + .float-previewer-header{ + cursor: grabbing; + background: #ace4ff; + } + } + + .float-previewer-header{ + z-index: 999999; + height: 40px; + border-bottom: 1px solid #ebedee; + background: #caecfd; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + cursor: grab; + + .float-previewer-title{ + user-select: none; + font-size: 16px; + color: #333; + font-weight: bold; + } + } + + .cherry-previewer{ + border-left: none; + } +} + .cherry-previewer { padding: 20px 45px 20px 20px; border-left: 2px solid #ebedee; diff --git a/src/toolbars/hooks/TogglePreview.js b/src/toolbars/hooks/TogglePreview.js index b99c36a8..b90caf21 100644 --- a/src/toolbars/hooks/TogglePreview.js +++ b/src/toolbars/hooks/TogglePreview.js @@ -68,6 +68,18 @@ export default class TogglePreview extends MenuBase { * 响应点击事件 */ onClick() { + // 需要浮动预览 + if (this.editor.previewer.isPreviewerNeedFloat()) { + // 正在浮动预览 + if (this.editor.previewer.isPreviewerFloat()) { + this.editor.previewer.recoverFloatPreviewer(true); + this.isHidden = false; + } else { + this.editor.previewer.floatPreviewer(); + this.isHidden = true; + } + return; + } if (this.editor.previewer.isPreviewerHidden()) { this.editor.previewer.recoverPreviewer(true); this.isHidden = false; diff --git a/types/cherry.d.ts b/types/cherry.d.ts index 36aa5c4d..5e552859 100644 --- a/types/cherry.d.ts +++ b/types/cherry.d.ts @@ -200,6 +200,7 @@ export interface CherryPreviewerOptions { /** 预览区域的DOM className */ className: string; enablePreviewerBubble: boolean; + floatWhenClosePreviewer: boolean; // 配置图片懒加载的逻辑 lazyLoadImg: { // 加载图片时如果需要展示loaing图,则配置loading图的地址 diff --git a/types/previewer.d.ts b/types/previewer.d.ts index c15f683e..5f6b8353 100644 --- a/types/previewer.d.ts +++ b/types/previewer.d.ts @@ -26,6 +26,8 @@ export interface PreviewerOptions { previewerMaskDom: HTMLDivElement; /** 是否开启预览区域菜单 */ enablePreviewerBubble?: boolean; + // 是否在关闭预览区时,将预览区浮动 + floatWhenClosePreviewer?: boolean; $cherry?: Cherry; // 配置图片懒加载的逻辑 lazyLoadImg: {