# editor

富文本编辑器,可以对图片、文字进行编辑。

# 兼容性

Web 微信小程序 Android iOS HarmonyOS HarmonyOS(Vapor)
4.0 4.41 5.08 5.08 5.08 5.08

# 属性

名称 类型 默认值 兼容性 描述
read-only boolean -
设置编辑器为只读
placeholder string -
提示信息
show-img-size boolean -
点击图片时显示图片大小控件
show-img-toolbar boolean -
点击图片时显示工具栏控件
show-img-resize string -
点击图片时显示修改尺寸控件
enable-formats Array.<string> -
(Array.<string>)
编辑器允许的名单内的格式
type "none" "null"
输入类型,暂只支持 none(不弹出键盘)
@ready (event: UniEvent) => void -
编辑器初始化完成时触发
@focus (event: UniEvent) => void -
编辑器聚焦时触发,event.detail = {html, text, delta}
@blur (event: UniEvent) => void -
编辑器失去焦点时触发,detail = {html, text, delta}
@input (event: UniEvent) => void -
编辑器内容改变时触发,detail = {html, text, delta}
@statuschange (event: UniEvent) => void -
通过 Context 方法改变编辑器内样式时触发,返回选区已设置的样式

# 上下文对象API

editor组件有上下文对象,api为uni.createEditorContextAsync()

给editor组件设一个id属性,将id的值传入uni.createEditorContextAsync(),即可得到editor组件的上下文对象,进一步可使用对象上的方法。

# 示例

示例为hello uni-app x alpha分支,与最新HBuilderX Alpha版同步。与最新正式版同步的master分支示例另见

扫码体验(手机浏览器跳转到App直达页)

示例

<template>
  <view id="editor-page-shell" :class="pageShellClass">
    <view id="editor-layout" :class="editorLayoutClass">
    <view
      class="toolbar-wrap"

      :style="toolbarWrapStyle"
      <!--#ifdef WEB -->
      @mousedown="onToolbarMouseDown"
      <!--#endif-->
    >
      <view class="toolbar-frame">
        <view :class="desktopToolbarShellClass" @mousemove="hideToolbarTooltip">
          <view :class="desktopToolbarLeadingGroupClass">
            <view id="toolbar-button-undo" @tap="undo" @mousemove.stop="showToolbarTooltip('toolbar-button-undo', '撤回')" class="toolbar-icon-button">
              <text class="toolbar-icon-text iconfont">{{"&#xe6a3;"}}</text>
            </view>
            <view id="toolbar-button-redo" @tap="redo" @mousemove.stop="showToolbarTooltip('toolbar-button-redo', '重做')" class="toolbar-icon-button">
              <text class="toolbar-icon-text iconfont">{{"&#xe6a5;"}}</text>
            </view>
            <view id="toolbar-button-clear-format" @tap="clearFormatting" @mousemove.stop="showToolbarTooltip('toolbar-button-clear-format', '清除格式')" class="toolbar-icon-button">
              <text class="toolbar-icon-text iconfont">{{"&#xe6b9;"}}</text>
            </view>
          </view>

          <view :class="desktopToolbarDividerClass"></view>

          <view :class="desktopToolbarGroupClass">
            <view id="toolbar-button-font-size" @tap="openFontSizeSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-font-size', '设置字号')" :class="fontSizeSheetHighlighted ? 'toolbar-select toolbar-select-active' : 'toolbar-select'">
              <text :class="fontSizeSheetHighlighted ? 'toolbar-select-label toolbar-select-label-active toolbar-size-label' : 'toolbar-select-label toolbar-size-label'">{{ getDesktopFontSizeLabel() }}</text>
              <text :class="fontSizeSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-font-family" @tap="openFontFamilySheet" @mousemove.stop="showToolbarTooltip('toolbar-button-font-family', '设置字体')" :class="fontFamilySheetHighlighted ? 'toolbar-select toolbar-select-font toolbar-select-active' : 'toolbar-select toolbar-select-font'">
              <!-- <text :class="fontFamilySheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{"&#xe68e;"}}</text> -->
              <text :class="fontFamilySheetHighlighted ? 'toolbar-select-label toolbar-select-label-active toolbar-select-font-label' : 'toolbar-select-label toolbar-select-font-label'">{{ getDesktopFontFamilyLabel() }}</text>
              <text :class="fontFamilySheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-bold" @tap="toggleBold" @mousemove.stop="showToolbarTooltip('toolbar-button-bold', '加粗')" :class="data.formats.bold ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.bold ? 'toolbar-icon-text toolbar-icon-text-active toolbar-font-action iconfont relative-top-1' : 'toolbar-icon-text toolbar-font-action iconfont relative-top-1'">{{"&#xe6ae;"}}</text>
            </view>
            <view id="toolbar-button-italic" @tap="toggleItalic" @mousemove.stop="showToolbarTooltip('toolbar-button-italic', '斜体')" :class="data.formats.italic ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.italic ? 'toolbar-icon-text toolbar-icon-text-active toolbar-font-action iconfont relative-top-1' : 'toolbar-icon-text toolbar-font-action iconfont relative-top-1'">{{"&#xe6af;"}}</text>
            </view>
            <view id="toolbar-button-underline" @tap="toggleUnderline" @mousemove.stop="showToolbarTooltip('toolbar-button-underline', '下划线')" :class="data.formats.underline ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.underline ? 'toolbar-icon-text toolbar-icon-text-active toolbar-font-action iconfont' : 'toolbar-icon-text toolbar-font-action iconfont'">{{"&#xe6a7;"}}</text>
            </view>
            <view id="toolbar-button-strike" @tap="toggleStrike" @mousemove.stop="showToolbarTooltip('toolbar-button-strike', '删除线')" :class="data.formats.strike ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.strike ? 'toolbar-icon-text toolbar-icon-text-active toolbar-font-action iconfont relative-top-1' : 'toolbar-icon-text toolbar-font-action iconfont relative-top-1'">{{"&#xe6aa;"}}</text>
            </view>
          </view>

          <view :class="desktopToolbarDividerClass"></view>

          <view :class="desktopToolbarGroupClass">
            <view id="toolbar-button-text-color" @tap="openTextColorSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-text-color', '字体颜色')" :class="textColorSheetHighlighted ? 'toolbar-color-button toolbar-color-button-active' : 'toolbar-color-button'">
              <view class="toolbar-color-stack">
                <text :class="textColorSheetHighlighted ? 'toolbar-color-symbol toolbar-color-symbol-active iconfont' : 'toolbar-color-symbol iconfont'">{{"&#xe6b8;"}}</text>
                <view class="toolbar-color-underline" :style="toolbarTextColorChipStyle"></view>
              </view>
              <text :class="textColorSheetHighlighted ? 'toolbar-color-arrow toolbar-color-arrow-active iconfont' : 'toolbar-color-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-background-color" @tap="openBackgroundColorSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-background-color', '背景颜色')" :class="backgroundColorSheetHighlighted ? 'toolbar-color-button toolbar-color-button-active' : 'toolbar-color-button'">
              <view class="toolbar-color-stack">
                <text :class="backgroundColorSheetHighlighted ? 'toolbar-color-symbol toolbar-color-symbol-active iconfont' : 'toolbar-color-symbol iconfont'">{{"&#xe6bb;"}}</text>
                <view class="toolbar-color-underline" :style="toolbarBackgroundColorChipStyle"></view>
              </view>
              <text :class="backgroundColorSheetHighlighted ? 'toolbar-color-arrow toolbar-color-arrow-active iconfont' : 'toolbar-color-arrow iconfont'" style="margin-left: 2px">{{"&#xe6be;"}}</text>
            </view>
          </view>
          <view :class="desktopToolbarDividerClass"></view>

          <view :class="desktopToolbarGroupClass">
            <view id="toolbar-button-align" @tap="openAlignSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-align', '对齐方式')" :class="alignSheetHighlighted ? 'toolbar-select toolbar-select-compact toolbar-select-active' : 'toolbar-select toolbar-select-compact'">
              <text :class="alignSheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{ desktopAlignIconGlyph }}</text>
              <text :class="alignSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-text-indent" @tap="toggleTextIndent" @mousemove.stop="showToolbarTooltip('toolbar-button-text-indent', '首行缩进')" :class="textIndentHighlighted ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="textIndentHighlighted ? 'toolbar-icon-text toolbar-icon-text-active iconfont' : 'toolbar-icon-text iconfont'">{{"&#xe6ad;"}}</text>
            </view>
            <view id="toolbar-button-block-indent" @tap="openBlockIndentSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-block-indent', '两端缩进')" :class="blockIndentSheetHighlighted ? 'toolbar-select toolbar-select-compact toolbar-select-active' : 'toolbar-select toolbar-select-compact'">
              <text :class="blockIndentSheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{"&#xe6ba;"}}</text>
              <text :class="blockIndentSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-line-height" @tap="openLineHeightSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-line-height', '行间距')" :class="lineHeightSheetHighlighted ? 'toolbar-select toolbar-select-compact toolbar-select-value toolbar-select-active' : 'toolbar-select toolbar-select-compact toolbar-select-value'">
              <text :class="lineHeightSheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{"&#xe6b0;"}}</text>
              <text :class="lineHeightSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-letter-spacing" @tap="openLetterSpacingSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-letter-spacing', '字间距')" :class="letterSpacingSheetHighlighted ? 'toolbar-select toolbar-select-compact toolbar-select-value toolbar-select-active' : 'toolbar-select toolbar-select-compact toolbar-select-value'">
              <text :class="letterSpacingSheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{"&#xe6ac;"}}</text>
              <text :class="letterSpacingSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <view id="toolbar-button-list" @tap="openListSheet" @mousemove.stop="showToolbarTooltip('toolbar-button-list', '列表')" :class="listSheetHighlighted ? 'toolbar-select toolbar-select-compact toolbar-select-active' : 'toolbar-select toolbar-select-compact'">
              <text :class="listSheetHighlighted ? 'toolbar-icon-text toolbar-icon-text-active toolbar-select-glyph iconfont' : 'toolbar-icon-text toolbar-select-glyph iconfont'">{{"&#xe6a4;"}}</text>
              <text :class="listSheetHighlighted ? 'toolbar-select-arrow toolbar-select-arrow-active iconfont' : 'toolbar-select-arrow iconfont'">{{"&#xe6be;"}}</text>
            </view>
            <!-- #ifndef MP -->
            <view id="toolbar-button-blockquote" @tap="toggleBlockquote" @mousemove.stop="showToolbarTooltip('toolbar-button-blockquote', '引用')" :class="data.formats.blockquote ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.blockquote ? 'toolbar-icon-text toolbar-icon-text-active iconfont' : 'toolbar-icon-text iconfont'">{{"&#xe6b5;"}}</text>
            </view>
            <view id="toolbar-button-code-block" @tap="toggleCodeBlock" @mousemove.stop="showToolbarTooltip('toolbar-button-code-block', '代码块')" :class="data.formats.codeBlock ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.codeBlock ? 'toolbar-icon-text toolbar-icon-text-active iconfont' : 'toolbar-icon-text iconfont'">{{"&#xe6a9;"}}</text>
            </view>
            <!-- #endif -->
            <view id="toolbar-button-divider" @tap="insertDivider" @mousemove.stop="showToolbarTooltip('toolbar-button-divider', '分割线')" class="toolbar-icon-button">
              <text class="toolbar-icon-text iconfont">{{"&#xe6b4;"}}</text>
            </view>
          </view>

          <view :class="desktopToolbarDividerClass"></view>

          <view :class="desktopToolbarGroupClass">
            <view id="toolbar-button-image" @tap="chooseInsertImage" @mousemove.stop="showToolbarTooltip('toolbar-button-image', '图片')" class="toolbar-icon-button">
              <text class="toolbar-icon-text iconfont">{{"&#xe6bc;"}}</text>
            </view>
            <!-- #ifndef MP -->
            <view id="toolbar-button-link" @tap="openLinkModal" @mousemove.stop="showToolbarTooltip('toolbar-button-link', data.formats.link != '' ? '编辑超链接' : '插入超链接')" :class="data.formats.link != '' ? 'toolbar-icon-button toolbar-icon-button-active' : 'toolbar-icon-button'">
              <text :class="data.formats.link != '' ? 'toolbar-icon-text toolbar-icon-text-active iconfont' : 'toolbar-icon-text iconfont'">{{"&#xe6bd;"}}</text>
            </view>
            <!-- #endif -->
          </view>
        </view>
        <!-- #ifdef WEB -->
        <view v-if="data.tooltipVisible" class="toolbar-tooltip" :style="toolbarTooltipStyle">
          <view class="toolbar-tooltip-arrow"></view>
          <view class="toolbar-tooltip-bubble">
            <text class="toolbar-tooltip-text">{{ data.tooltipText }}</text>
          </view>
        </view>
        <view v-if="!data.isFocused && data.activeSheet == ''" class="desktop-toolbar-mask"></view>
        <!-- #endif -->
      </view>
    </view>

    <view v-if="data.activeSheet != ''" class="sheet-backdrop" @tap="closeSheets"></view>

    <view
     id="editor-sheet-host"
     :class="sheetHostClass"
     :style="sheetHostStyle"
     v-if="data.activeSheet != ''"
      <!--#ifdef WEB -->
      @mousedown="onToolbarMouseDown"
      <!--#endif-->
    >
      <view :class="toolbarPanelClass" :style="sheetPanelStyle">
        <!-- #ifdef WEB -->
        <view v-if="!webPhoneLayout" class="toolbar-panel-arrow" :style="toolbarPanelArrowStyle"></view>
        <!-- #endif -->
        <view :class="toolbarPanelHeaderClass">
          <view class="toolbar-panel-title-wrap">
            <text :class="toolbarPanelTitleClass">{{ getPanelTitle() }}</text>
            <text :class="toolbarPanelSubtitleClass">{{ getPanelSubtitle() }}</text>
          </view>
        </view>

        <scroll-view :class="toolbarPanelScrollClass" direction="vertical" :show-scrollbar="false">
          <view class="toolbar-panel-section" v-if="data.activeSheet == 'title'">
            <view :class="data.toolbarChipRowClass">
              <view :class="data.toolbarChipCellClass"><view @tap="setParagraph" :class="(paragraphActive) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(paragraphActive) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">P</text><text :class="(paragraphActive) ? desktopMenuTextActiveClass : desktopMenuTextClass">正文</text></view></view>
              <!-- #ifndef MP -->
              <view :class="data.toolbarChipCellClass"><view @tap="toggleBlockquote" :class="(data.formats.blockquote) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.blockquote) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">BQ</text><text :class="(data.formats.blockquote) ? desktopMenuTextActiveClass : desktopMenuTextClass">引用</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="toggleCodeBlock" :class="(data.formats.codeBlock) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.codeBlock) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">&lt;/&gt;</text><text :class="(data.formats.codeBlock) ? desktopMenuTextActiveClass : desktopMenuTextClass">代码</text></view></view>
              <!-- #endif -->
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(1)" :class="(data.formats.header == 1) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 1) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H1</text><text :class="(data.formats.header == 1) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题1</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(2)" :class="(data.formats.header == 2) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 2) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H2</text><text :class="(data.formats.header == 2) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题2</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(3)" :class="(data.formats.header == 3) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 3) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H3</text><text :class="(data.formats.header == 3) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题3</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(4)" :class="(data.formats.header == 4) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 4) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H4</text><text :class="(data.formats.header == 4) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题4</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(5)" :class="(data.formats.header == 5) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 5) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H5</text><text :class="(data.formats.header == 5) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题5</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setHeadingLevel(6)" :class="(data.formats.header == 6) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.header == 6) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">H6</text><text :class="(data.formats.header == 6) ? desktopMenuTextActiveClass : desktopMenuTextClass">大标题6</text></view></view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'style'">
            <view :class="data.toolbarChipRowClass">
              <view :class="data.toolbarChipCellClass"><view @tap="toggleBoldAndClose" :class="(data.formats.bold) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.bold) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">B</text><text :class="(data.formats.bold) ? desktopMenuTextActiveClass : desktopMenuTextClass">加粗</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="toggleItalicAndClose" :class="(data.formats.italic) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.italic) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">I</text><text :class="(data.formats.italic) ? desktopMenuTextActiveClass : desktopMenuTextClass">斜体</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="toggleUnderlineAndClose" :class="(data.formats.underline) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.underline) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">U</text><text :class="(data.formats.underline) ? desktopMenuTextActiveClass : desktopMenuTextClass">下划线</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="toggleStrikeAndClose" :class="(data.formats.strike) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.strike) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">S</text><text :class="(data.formats.strike) ? desktopMenuTextActiveClass : desktopMenuTextClass">删除线</text></view></view>
            </view>

            <view class="toolbar-color-section">
              <text class="toolbar-color-title">文字颜色</text>
              <view class="color-row color-row-panel">
                <view @tap="setTextColorAndClose('')" :class="(data.formats.color == '') ? clearColorSwatchActiveClass : clearColorSwatchClass"></view>
                <view v-for="color in TEXT_COLOR_PRESETS" :key="color" @tap="setTextColorAndClose(color)" :class="(data.formats.color == color) ? filledColorSwatchActiveClass : filledColorSwatchClass" :style="'background-color: ' + color + ';'"></view>
              </view>
            </view>

            <view class="toolbar-color-section">
              <text class="toolbar-color-title">背景颜色</text>
              <view class="color-row color-row-panel">
                <view @tap="setBackgroundColorAndClose('')" :class="(data.formats.backgroundColor == '') ? clearColorSwatchActiveClass : clearColorSwatchClass"></view>
                <view v-for="color in BACKGROUND_COLOR_PRESETS" :key="color" @tap="setBackgroundColorAndClose(color)" :class="(data.formats.backgroundColor == color) ? filledColorSwatchActiveClass : filledColorSwatchClass" :style="'background-color: ' + color + ';'"></view>
              </view>
            </view>

          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'line-height'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">行间距</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="setLineHeightAndClose('')" :class="(data.formats.lineHeight == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.lineHeight == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">A</text><text :class="(data.formats.lineHeight == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLineHeightAndClose('1.5')" :class="(data.formats.lineHeight == '1.5') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.lineHeight == '1.5') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">1.5</text><text :class="(data.formats.lineHeight == '1.5') ? desktopMenuTextActiveClass : desktopMenuTextClass">紧凑</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLineHeightAndClose('1.8')" :class="(data.formats.lineHeight == '1.8') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.lineHeight == '1.8') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">1.8</text><text :class="(data.formats.lineHeight == '1.8') ? desktopMenuTextActiveClass : desktopMenuTextClass">舒适</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLineHeightAndClose('2')" :class="(data.formats.lineHeight == '2') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.lineHeight == '2') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">2.0</text><text :class="(data.formats.lineHeight == '2') ? desktopMenuTextActiveClass : desktopMenuTextClass">宽松</text></view></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'letter-spacing'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">字间距</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="setLetterSpacingAndClose('')" :class="(data.formats.letterSpacing == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.letterSpacing == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">Aa</text><text :class="(data.formats.letterSpacing == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLetterSpacingAndClose('0px')" :class="(data.formats.letterSpacing == '0px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.letterSpacing == '0px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">0</text><text :class="(data.formats.letterSpacing == '0px') ? desktopMenuTextActiveClass : desktopMenuTextClass">标准</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLetterSpacingAndClose('1px')" :class="(data.formats.letterSpacing == '1px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.letterSpacing == '1px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">+1</text><text :class="(data.formats.letterSpacing == '1px') ? desktopMenuTextActiveClass : desktopMenuTextClass">微松</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setLetterSpacingAndClose('2px')" :class="(data.formats.letterSpacing == '2px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.letterSpacing == '2px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">+2</text><text :class="(data.formats.letterSpacing == '2px') ? desktopMenuTextActiveClass : desktopMenuTextClass">通透</text></view></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'font-size'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">字号</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('12px')" :class="(data.formats.fontSize == '12px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '12px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">12</text><text :class="(data.formats.fontSize == '12px') ? desktopMenuTextActiveClass : desktopMenuTextClass">12px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('14px')" :class="(data.formats.fontSize == '14px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '14px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">14</text><text :class="(data.formats.fontSize == '14px') ? desktopMenuTextActiveClass : desktopMenuTextClass">14px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('15px')" :class="(data.formats.fontSize == '15px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '15px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">15</text><text :class="(data.formats.fontSize == '15px') ? desktopMenuTextActiveClass : desktopMenuTextClass">15px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('16px')" :class="(data.formats.fontSize == '16px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '16px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">16</text><text :class="(data.formats.fontSize == '16px') ? desktopMenuTextActiveClass : desktopMenuTextClass">16px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('17px')" :class="(data.formats.fontSize == '17px' || data.formats.fontSize == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '17px' || data.formats.fontSize == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">17</text><text :class="(data.formats.fontSize == '17px' || data.formats.fontSize == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('19px')" :class="(data.formats.fontSize == '19px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '19px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">19</text><text :class="(data.formats.fontSize == '19px') ? desktopMenuTextActiveClass : desktopMenuTextClass">19px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('20px')" :class="(data.formats.fontSize == '20px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '20px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">20</text><text :class="(data.formats.fontSize == '20px') ? desktopMenuTextActiveClass : desktopMenuTextClass">20px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('24px')" :class="(data.formats.fontSize == '24px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '24px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">24</text><text :class="(data.formats.fontSize == '24px') ? desktopMenuTextActiveClass : desktopMenuTextClass">24px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('32px')" :class="(data.formats.fontSize == '32px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '32px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">32</text><text :class="(data.formats.fontSize == '32px') ? desktopMenuTextActiveClass : desktopMenuTextClass">32px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontSizeAndClose('48px')" :class="(data.formats.fontSize == '48px') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontSize == '48px') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">48</text><text :class="(data.formats.fontSize == '48px') ? desktopMenuTextActiveClass : desktopMenuTextClass">48px</text></view></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'font-family'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">字体</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('')" :class="(data.formats.fontFamily == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">D</text><text :class="(data.formats.fontFamily == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('Microsoft YaHei')" :class="(data.formats.fontFamily == 'Microsoft YaHei') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'Microsoft YaHei') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">雅黑</text><text :class="(data.formats.fontFamily == 'Microsoft YaHei') ? desktopMenuTextActiveClass : desktopMenuTextClass">微软雅黑</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('SimHei')" :class="(data.formats.fontFamily == 'SimHei') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'SimHei') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(data.formats.fontFamily == 'SimHei') ? desktopMenuTextActiveClass : desktopMenuTextClass">黑体</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('SimSun')" :class="(data.formats.fontFamily == 'SimSun') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'SimSun') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(data.formats.fontFamily == 'SimSun') ? desktopMenuTextActiveClass : desktopMenuTextClass">宋体</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('KaiTi')" :class="(data.formats.fontFamily == 'KaiTi') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'KaiTi') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(data.formats.fontFamily == 'KaiTi') ? desktopMenuTextActiveClass : desktopMenuTextClass">楷体</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('FangSong')" :class="(data.formats.fontFamily == 'FangSong') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'FangSong') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">仿宋</text><text :class="(data.formats.fontFamily == 'FangSong') ? desktopMenuTextActiveClass : desktopMenuTextClass">仿宋</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('Georgia')" :class="(data.formats.fontFamily == 'Georgia') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'Georgia') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">Geo</text><text :class="(data.formats.fontFamily == 'Georgia') ? desktopMenuTextActiveClass : desktopMenuTextClass">Georgia</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('Times New Roman')" :class="(data.formats.fontFamily == 'Times New Roman') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'Times New Roman') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">TNR</text><text :class="(data.formats.fontFamily == 'Times New Roman') ? desktopMenuTextActiveClass : desktopMenuTextClass">Times</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setFontFamilyAndClose('Courier New')" :class="(data.formats.fontFamily == 'Courier New') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.fontFamily == 'Courier New') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">Mono</text><text :class="(data.formats.fontFamily == 'Courier New') ? desktopMenuTextActiveClass : desktopMenuTextClass">Courier</text></view></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'align'">
            <view :class="data.toolbarChipRowClass">
              <view :class="data.toolbarChipCellClass"><view @tap="clearAlign" :class="(data.formats.align == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.align == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">D</text><text :class="(data.formats.align == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setAlignLeft" :class="(data.formats.align == 'left') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.align == 'left') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">L</text><text :class="(data.formats.align == 'left') ? desktopMenuTextActiveClass : desktopMenuTextClass">居左</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setAlignCenter" :class="(data.formats.align == 'center') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.align == 'center') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">C</text><text :class="(data.formats.align == 'center') ? desktopMenuTextActiveClass : desktopMenuTextClass">居中</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setAlignRight" :class="(data.formats.align == 'right') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.align == 'right') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">R</text><text :class="(data.formats.align == 'right') ? desktopMenuTextActiveClass : desktopMenuTextClass">居右</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setAlignJustify" :class="(data.formats.align == 'justify') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.align == 'justify') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">J</text><text :class="(data.formats.align == 'justify') ? desktopMenuTextActiveClass : desktopMenuTextClass">两端</text></view></view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'block-indent'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">两端缩进</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('0px')" :class="((data.formats.marginLeft == '' && data.formats.marginRight == '')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '' && data.formats.marginRight == '')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">0</text><text :class="((data.formats.marginLeft == '' && data.formats.marginRight == '')) ? desktopMenuTextActiveClass : desktopMenuTextClass">0px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('8px')" :class="((data.formats.marginLeft == '8px' && data.formats.marginRight == '8px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '8px' && data.formats.marginRight == '8px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">8</text><text :class="((data.formats.marginLeft == '8px' && data.formats.marginRight == '8px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">8px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('16px')" :class="((data.formats.marginLeft == '16px' && data.formats.marginRight == '16px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '16px' && data.formats.marginRight == '16px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">16</text><text :class="((data.formats.marginLeft == '16px' && data.formats.marginRight == '16px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">16px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('24px')" :class="((data.formats.marginLeft == '24px' && data.formats.marginRight == '24px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '24px' && data.formats.marginRight == '24px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">24</text><text :class="((data.formats.marginLeft == '24px' && data.formats.marginRight == '24px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">24px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('32px')" :class="((data.formats.marginLeft == '32px' && data.formats.marginRight == '32px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '32px' && data.formats.marginRight == '32px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">32</text><text :class="((data.formats.marginLeft == '32px' && data.formats.marginRight == '32px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">32px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('40px')" :class="((data.formats.marginLeft == '40px' && data.formats.marginRight == '40px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '40px' && data.formats.marginRight == '40px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">40</text><text :class="((data.formats.marginLeft == '40px' && data.formats.marginRight == '40px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">40px</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="setBlockIndentAndClose('48px')" :class="((data.formats.marginLeft == '48px' && data.formats.marginRight == '48px')) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="((data.formats.marginLeft == '48px' && data.formats.marginRight == '48px')) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">48</text><text :class="((data.formats.marginLeft == '48px' && data.formats.marginRight == '48px')) ? desktopMenuTextActiveClass : desktopMenuTextClass">48px</text></view></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'list'">
            <view :class="data.toolbarChipRowClass">
              <view :class="data.toolbarChipCellClass"><view @tap="clearListFormat" :class="(data.formats.list == '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.list == '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">D</text><text :class="(data.formats.list == '') ? desktopMenuTextActiveClass : desktopMenuTextClass">默认</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setBulletList" :class="(data.formats.list == 'bullet') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.list == 'bullet') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(data.formats.list == 'bullet') ? desktopMenuTextActiveClass : desktopMenuTextClass">无序列表</text></view></view>
              <view :class="data.toolbarChipCellClass"><view @tap="setOrderedList" :class="(data.formats.list == 'ordered') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.list == 'ordered') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">1.</text><text :class="(data.formats.list == 'ordered') ? desktopMenuTextActiveClass : desktopMenuTextClass">有序列表</text></view></view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'text-color'">
            <view class="toolbar-color-section">
              <view class="desktop-color-sheet-label">
                <view class="desktop-color-sheet-icon"></view>
                <text class="desktop-color-sheet-label-text">默认颜色</text>
              </view>
              <view class="color-row color-row-panel">
                <view @tap="setTextColorAndClose('')" :class="(data.formats.color == '') ? clearColorSwatchActiveClass : clearColorSwatchClass"></view>
                <view v-for="color in TEXT_COLOR_PRESETS" :key="color" @tap="setTextColorAndClose(color)" :class="(data.formats.color == color) ? filledColorSwatchActiveClass : filledColorSwatchClass" :style="'background-color: ' + color + ';'"></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'background-color'">
            <view class="toolbar-color-section">
              <view class="desktop-color-sheet-label">
                <view class="desktop-color-sheet-icon"></view>
                <text class="desktop-color-sheet-label-text">默认背景</text>
              </view>
              <view class="color-row color-row-panel">
                <view @tap="setBackgroundColorAndClose('')" :class="(data.formats.backgroundColor == '') ? clearColorSwatchActiveClass : clearColorSwatchClass"></view>
                <view v-for="color in BACKGROUND_COLOR_PRESETS" :key="color" @tap="setBackgroundColorAndClose(color)" :class="(data.formats.backgroundColor == color) ? filledColorSwatchActiveClass : filledColorSwatchClass" :style="'background-color: ' + color + ';'"></view>
              </view>
            </view>
          </view>

          <view class="toolbar-panel-section" v-if="data.activeSheet == 'more'">
            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">插入内容</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="toggleChecklistTool" :class="(data.formats.list == 'check') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.list == 'check') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">TODO</text><text :class="(data.formats.list == 'check') ? desktopMenuTextActiveClass : desktopMenuTextClass">TODO</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="insertDivider" :class="(false) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(false) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">---</text><text :class="(false) ? desktopMenuTextActiveClass : desktopMenuTextClass">分割线</text></view></view>
                <!-- #ifndef MP -->
                <!-- <view :class="data.toolbarChipCellClass"><view @tap="openLinkModal" :class="(data.formats.link != '') ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(data.formats.link != '') ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">URL</text><text :class="(data.formats.link != '') ? desktopMenuTextActiveClass : desktopMenuTextClass">超链接</text></view></view> -->
                <!-- #endif -->
              </view>
            </view>

            <view class="toolbar-panel-group">
              <text class="toolbar-color-title">编辑操作</text>
              <view :class="data.toolbarChipRowClass">
                <view :class="data.toolbarChipCellClass"><view @tap="clearFormatting" :class="(false) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(false) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass">CLR</text><text :class="(false) ? desktopMenuTextActiveClass : desktopMenuTextClass">清除样式</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="undo" :class="(false) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(false) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(false) ? desktopMenuTextActiveClass : desktopMenuTextClass">撤销</text></view></view>
                <view :class="data.toolbarChipCellClass"><view @tap="redo" :class="(false) ? desktopMenuItemActiveClass : desktopMenuItemClass"><text :class="(false) ? desktopMenuBadgeActiveClass : desktopMenuBadgeClass"></text><text :class="(false) ? desktopMenuTextActiveClass : desktopMenuTextClass">重做</text></view></view>
              </view>
            </view>
          </view>
        </scroll-view>
      </view>
    </view>

    <!-- #ifndef MP -->
    <!-- #ifdef WEB -->
    <view v-if="data.linkModalVisible" class="link-modal-mask link-modal-mask-floating" @tap="closeLinkModal">
      <view class="link-modal-card link-modal-card-floating" @tap.stop>
        <view class="link-modal-header">
          <text class="link-modal-title">{{ data.formats.link != '' ? '编辑超链接' : '插入超链接' }}</text>
          <text class="link-modal-subtitle">{{ data.formats.link != '' ? '仅修改链接地址' : (data.linkUseSelectionText ? '将为选中文本添加链接' : '填写显示文本与链接地址') }}</text>
        </view>
        <view class="link-modal-form">
          <view v-if="data.formats.link == '' && !data.linkUseSelectionText" class="link-modal-field">
            <text class="link-modal-label">显示文本</text>
            <input class="link-modal-input" v-model="data.linkDraftText" placeholder="请输入显示文本" :maxlength="-1" />
          </view>
          <view v-if="data.formats.link == '' && data.linkUseSelectionText" class="link-modal-field">
            <text class="link-modal-label">已选中文本</text>
            <view class="link-modal-selection-chip">
              <text class="link-modal-selection-text">{{ data.linkDraftText }}</text>
            </view>
          </view>
          <view class="link-modal-field">
            <text class="link-modal-label">链接地址</text>
            <input class="link-modal-input" v-model="data.linkDraftHref" placeholder="https://example.com" :maxlength="-1" />
          </view>
        </view>
        <view class="link-modal-actions">
          <view v-if="data.formats.link != ''" class="link-modal-danger" @tap="removeLink">
            <text class="link-modal-danger-text">删除链接</text>
          </view>
          <view class="link-modal-action-group">
            <view class="link-modal-button link-modal-button-secondary" @tap="closeLinkModal">
              <text class="link-modal-button-text link-modal-button-text-secondary">取消</text>
            </view>
            <view class="link-modal-button link-modal-button-primary" @tap="confirmLinkModal">
              <text class="link-modal-button-text">确定</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- #endif -->
    <!-- #ifndef WEB -->
    <view v-if="data.linkModalVisible" class="link-modal-mask link-modal-mask-app" @tap="closeLinkModal">
      <view class="link-modal-card link-modal-card-app" @tap.stop>
        <view class="link-modal-header">
          <text class="link-modal-title link-modal-title-app">{{ data.formats.link != '' ? '编辑超链接' : '插入超链接' }}</text>
          <text class="link-modal-subtitle link-modal-subtitle-app">{{ data.formats.link != '' ? '仅修改链接地址' : (data.linkUseSelectionText ? '将为选中文本添加链接' : '填写显示文本与链接地址') }}</text>
        </view>
        <view class="link-modal-form">
          <view v-if="data.formats.link == '' && !data.linkUseSelectionText" class="link-modal-field">
            <text class="link-modal-label link-modal-label-app">显示文本</text>
            <input class="link-modal-input link-modal-input-app" v-model="data.linkDraftText" placeholder="请输入显示文本" :maxlength="-1" />
          </view>
          <view v-if="data.formats.link == '' && data.linkUseSelectionText" class="link-modal-field">
            <text class="link-modal-label link-modal-label-app">已选中文本</text>
            <view class="link-modal-selection-chip link-modal-selection-chip-app">
              <text class="link-modal-selection-text link-modal-selection-text-app">{{ data.linkDraftText }}</text>
            </view>
          </view>
          <view class="link-modal-field">
            <text class="link-modal-label link-modal-label-app">链接地址</text>
            <input class="link-modal-input link-modal-input-app" v-model="data.linkDraftHref" placeholder="https://example.com" :maxlength="-1" />
          </view>
        </view>
        <view class="link-modal-actions link-modal-actions-app">
          <!-- 蒸汽模式下闪退 暂时屏蔽 -->
          <!-- #ifndef VUE3-VAPOR -->
          <view v-if="data.formats.link != ''" class="link-modal-danger link-modal-danger-app" @tap="removeLink">
            <text class="link-modal-danger-text link-modal-danger-text-app">删除链接</text>
          </view>
          <!-- #endif -->
          <view class="link-modal-action-group link-modal-action-group-app">
            <view class="link-modal-button link-modal-button-secondary link-modal-button-secondary-app" @tap="closeLinkModal">
              <text class="link-modal-button-text link-modal-button-text-secondary link-modal-button-text-secondary-app">取消</text>
            </view>
            <view class="link-modal-button link-modal-button-primary link-modal-button-primary-app" @tap="confirmLinkModal">
              <text class="link-modal-button-text link-modal-button-text-app">确定</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    <!-- #endif -->
    <!-- #endif -->

    <view class="editor-stage" :style="editorStageStyle">
      <!-- editor 切换 hidekeyboard: 注释掉 type -->
      <editor
        id="editor"
        class="editor-field"
        placeholder="请输入正文内容..."
        style="padding: 20px;"
        show-img-size
        show-img-toolbar
        show-img-resize
        @ready="onEditorReady"
        @focus="onEditorFocus"
        @blur="onEditorBlur"
        @statuschange="onStatusChange"
      />
    </view>
    </view>
  </view>
</template>

<script setup lang="uts">

type Formats = {
  bold: boolean;
  italic: boolean;
  underline: boolean;
  strike: boolean;
  blockquote: boolean;
  codeBlock: boolean;
  link: string;
  header: number;
  list: string;
  align: string;
  textIndent: string;
  marginLeft: string;
  marginRight: string;
  lineHeight: string;
  letterSpacing: string;
  fontFamily: string;
  fontSize: string;
  color: string;
  backgroundColor: string;
}

const TEXT_COLOR_PRESETS: string[] = [
  '#000000', '#3f3f3f', '#6a6a6a', '#949494', '#bfbfbf', '#d8d8d8', '#ececec', '#f5f5f5', '#ffffff',
  '#f03a37', '#f26c2e', '#f59a33', '#f7d447', '#7bc943', '#59c3c3', '#4a90e2', '#3d5ed7', '#6f42c1', '#d13f96',
  '#f3ddd8', '#f7e8da', '#f7ecd3', '#fbf4d0', '#e5f1d4', '#d9eeea', '#d8eaf6', '#dbe3f4', '#e4ddf1', '#f1dde8',
  '#ff9d9a', '#ffb389', '#ffcf8a', '#fff17a', '#b8e986', '#7ed6d4', '#8cc8f6', '#9fb6f0', '#c3a5ea', '#f09ac5',
  '#ff4d57', '#ff7a45', '#ffad47', '#ffe44d', '#7edc3f', '#32c5c0', '#4aa8f0', '#5b7bea', '#9556d9', '#ec4fa3',
  '#d0021b', '#d9480f', '#df7b00', '#d9b300', '#38a000', '#1398a0', '#1769d2', '#2946c7', '#5b2db5', '#c2187a',
  '#98001a', '#952700', '#9a4b00', '#8a6a00', '#1f6f00', '#0d5c63', '#0d47a1', '#16269a', '#3c177c', '#8e105f'
]

const BACKGROUND_COLOR_PRESETS: string[] = [
  '#ffffff', '#e8e8e8', '#d9d9d9', '#c4c4c4', '#a8a8a8', '#8a8a8a', '#6d6d6d', '#4f4f4f', '#2f2f2f',
  '#fff1f0', '#fff4ec', '#fff7e8', '#fffbe6', '#f4fde8', '#edfdfb', '#eef7ff', '#f1f4ff', '#f5f0ff', '#fff0f7',
  '#ffd9d6', '#ffe3cf', '#ffecc8', '#fff5bf', '#e6f7c9', '#d8f3f2', '#d9ecff', '#dfe6ff', '#eadfff', '#ffd9ee',
  '#ffb8b5', '#ffc89f', '#ffd997', '#fff08a', '#d0f0a8', '#b8e9e7', '#b8dcff', '#c4d2ff', '#d9c2ff', '#ffb9de',
  '#ff8b87', '#ffa96f', '#ffc15f', '#ffe65c', '#a9e45f', '#86d9d5', '#8fc4ff', '#9eb5ff', '#c29af5', '#ff8cc8',
  '#ff5f5b', '#ff8a3d', '#ffaa2b', '#ffd400', '#7fd13b', '#42bebc', '#5da9ff', '#738ef5', '#9a68e0', '#ef5aae',
  '#ffdede', '#ffe9dd', '#fff1e0', '#fff7db', '#edf8df', '#e1f5f3', '#e3f0ff', '#e7ebff', '#efe8ff', '#ffe5f3'
]

type DataType = {
  pageWindowHeight: number;
  pageWindowWidth: number;
  toolbarChipRowClass: string;
  toolbarChipCellClass: string;
  tooltipVisible: boolean;
  tooltipText: string;
  tooltipLeft: number;
  tooltipTop: number;
  sheetHeight: number;
  sheetTop: number;
  sheetLeft: number;
  sheetArrowLeft: number;
  sheetAnimated: boolean;
  editorInputType: string;
  isFocused: boolean;
  ignoreNextBlur: boolean;
  activeSheet: string;
  autoTest: boolean;
  editorReadyTest: boolean;
  undoTest: boolean;
  redoTest: boolean;
  removeFormatTest: boolean;
  insertImageTest: boolean;
  blurTest: boolean;
  clearTest: boolean;
  getContentDeltaTest: any | null;
  formatPainterActive: boolean;
  formatPainterFormats: Formats;
  formats: Formats;
  linkModalVisible: boolean;
  linkUseSelectionText: boolean;
  linkDraftText: string;
  linkDraftHref: string;
  linkSelectionText: string;
  linkOriginalHref: string;
}

function createDefaultFormats() : Formats {
  return {
    bold: false,
    italic: false,
    underline: false,
    strike: false,
    blockquote: false,
    codeBlock: false,
    link: '',
    header: 0,
    list: '',
    align: '',
    textIndent: '',
    marginLeft: '',
    marginRight: '',
    lineHeight: '',
    letterSpacing: '',
    fontFamily: '',
    fontSize: '',
    color: '',
    backgroundColor: ''
  }
}

function assignFormats(target: Formats, source: Formats) {
  target.bold = source.bold
  target.italic = source.italic
  target.underline = source.underline
  target.strike = source.strike
  target.blockquote = source.blockquote
  target.codeBlock = source.codeBlock
  target.link = source.link
  target.header = source.header
  target.list = source.list
  target.align = source.align
  target.textIndent = source.textIndent
  target.marginLeft = source.marginLeft
  target.marginRight = source.marginRight
  target.lineHeight = source.lineHeight
  target.letterSpacing = source.letterSpacing
  target.fontFamily = source.fontFamily
  target.fontSize = source.fontSize
  target.color = source.color
  target.backgroundColor = source.backgroundColor
}

const data = reactive<DataType>({
  pageWindowHeight: 0,
  pageWindowWidth: 0,
  toolbarChipRowClass: 'toolbar-chip-row',
  toolbarChipCellClass: 'toolbar-chip-cell',
  tooltipVisible: false,
  tooltipText: '',
  tooltipLeft: 0,
  tooltipTop: 0,
  sheetHeight: 320,
  sheetTop: 74,
  sheetLeft: 12,
  sheetArrowLeft: 32,
  sheetAnimated: false,
  editorInputType: '',
  isFocused: false,
  ignoreNextBlur: false,
  activeSheet: '',
  autoTest: false,
  editorReadyTest: false,
  undoTest: false,
  redoTest: false,
  removeFormatTest: false,
  insertImageTest: false,
  blurTest: false,
  clearTest: false,
  getContentDeltaTest: null,
  formatPainterActive: false,
  formatPainterFormats: createDefaultFormats(),
  formats: createDefaultFormats(),
  linkModalVisible: false,
  linkUseSelectionText: false,
  linkDraftText: '',
  linkDraftHref: '',
  linkSelectionText: '',
  linkOriginalHref: ''
})

const editorContext = ref<EditorContext | null>(null)

let toolbarHideTimer = 0
let statusSyncLockedUntil = 0

function clearToolbarHideTimer() {
  if (toolbarHideTimer > 0) {
    clearTimeout(toolbarHideTimer)
    toolbarHideTimer = 0
  }
}

function scheduleToolbarHide() {
  clearToolbarHideTimer()
  // 工具栏改为公共常驻模式,这里仅重置计时器状态。
  toolbarHideTimer = 0
}

function getEditor() : EditorContext | null {
  return editorContext.value
}

function getToolbarWrapStyle() : string {
  return ''
}

function hasInlineStyleActive() : boolean {
  return data.formats.bold || data.formats.italic || data.formats.underline || data.formats.strike || data.formats.lineHeight != '' || data.formats.letterSpacing != '' || data.formats.fontSize != '' || data.formats.fontFamily != '' || data.formats.color != '' || data.formats.backgroundColor != ''
}

function getToolbarTooltipStyle() : string {
  return `left:${data.tooltipLeft}px;top:${data.tooltipTop}px;`
}

function isWideLayout() : boolean {
  return data.pageWindowWidth >= 1120
}

function isWebPhoneLayout() : boolean {
  // #ifdef WEB
  const windowWidth = data.pageWindowWidth > 0 ? data.pageWindowWidth : uni.getWindowInfo().windowWidth
  return windowWidth < 760
  // #endif
  return false
}


function syncToolbarBottom() {
}

function syncToolbarVisibility() {
}

function getPageShellClass() : string {
  if (isWideLayout()) {
    return 'page-shell page-shell-wide'
  }
  return 'page-shell'
}

function getEditorLayoutClass() : string {
  if (isWideLayout()) {
    return 'editor-layout editor-layout-wide'
  }
  return 'editor-layout'
}

function getSheetPanelStyle() : string {
  return ''
}

function getSheetWidth() : number {
  const windowWidth = data.pageWindowWidth > 0 ? data.pageWindowWidth : uni.getWindowInfo().windowWidth
  let maxWidth = windowWidth - 24
  if (maxWidth < 240) {
    maxWidth = windowWidth - 12
  }
  if (maxWidth < 220) {
    return maxWidth
  }
  if (data.activeSheet == 'title') {
    return maxWidth < 280 ? maxWidth : 280
  }
  if (data.activeSheet == 'style') {
    return maxWidth < 440 ? maxWidth : 440
  }
  if (data.activeSheet == 'text-color' || data.activeSheet == 'background-color') {
    return maxWidth < 372 ? maxWidth : 372
  }
  if (data.activeSheet == 'line-height' || data.activeSheet == 'letter-spacing') {
    return maxWidth < 220 ? maxWidth : 220
  }
  if (data.activeSheet == 'align') {
    return maxWidth < 220 ? maxWidth : 220
  }
  return maxWidth < 260 ? maxWidth : 260
}

function syncToolbarChipClasses() {
  const panelWidth = getSheetWidth()
  if (panelWidth <= 300) {
    data.toolbarChipRowClass = 'toolbar-chip-row toolbar-chip-row-panel toolbar-chip-row-panel-compact'
  } else {
    data.toolbarChipRowClass = 'toolbar-chip-row toolbar-chip-row-panel'
  }

  if (data.activeSheet == 'title' || data.activeSheet == 'align' || data.activeSheet == 'more' || data.activeSheet == 'line-height' || data.activeSheet == 'letter-spacing' || data.activeSheet == 'font-size' || data.activeSheet == 'font-family' || data.activeSheet == 'block-indent' || data.activeSheet == 'list') {
    data.toolbarChipCellClass = 'toolbar-chip-cell toolbar-chip-cell-full'
    return
  }
  if (data.activeSheet == 'style') {
    data.toolbarChipCellClass = 'toolbar-chip-cell toolbar-chip-cell-third'
    return
  }
  data.toolbarChipCellClass = 'toolbar-chip-cell toolbar-chip-cell-quarter'
}

function getSheetHostStyle() : string {
  // #ifndef WEB
  return ''
  // #endif
  if (isWebPhoneLayout()) {
    syncToolbarChipClasses()
    return ''
  }
  syncToolbarChipClasses()
  return `left:${data.sheetLeft}px;top:${data.sheetTop}px;width:${getSheetWidth()}px;`
}

function getSheetHostClass() : string {
  // #ifdef WEB
  if (isWebPhoneLayout()) {
    return 'sheet-host sheet-host-mobile-web'
  }
  return 'sheet-host sheet-host-floating'
  // #endif
  return 'sheet-host'
}

function getToolbarPanelArrowStyle() : string {
  return `left:${data.sheetArrowLeft}px;`
}

function isCompactToolbar() : boolean {
  const windowWidth = data.pageWindowWidth > 0 ? data.pageWindowWidth : uni.getWindowInfo().windowWidth
  return windowWidth < 1240
}

function isDenseToolbar() : boolean {
  const windowWidth = data.pageWindowWidth > 0 ? data.pageWindowWidth : uni.getWindowInfo().windowWidth
  return windowWidth < 980
}

function isStyleSheetHighlighted() : boolean {
  return data.activeSheet == 'style' || hasInlineStyleActive()
}
function getDesktopFontFamilyLabel() : string {
  if (data.formats.fontFamily == 'Microsoft YaHei') {
    return '雅黑'
  }
  if (data.formats.fontFamily == 'SimHei') {
    return '黑体'
  }
  if (data.formats.fontFamily == 'SimSun') {
    return '宋体'
  }
  if (data.formats.fontFamily == 'KaiTi') {
    return '楷体'
  }
  if (data.formats.fontFamily == 'FangSong') {
    return '仿宋'
  }
  if (data.formats.fontFamily == 'Georgia') {
    return 'Geo'
  }
  if (data.formats.fontFamily == 'Times New Roman') {
    return 'Times'
  }
  if (data.formats.fontFamily == 'Courier New') {
    return 'Mono'
  }
  return '字体'
}

function getDesktopToolbarShellClass() : string {
  let className = 'toolbar-shell'
  if (isCompactToolbar()) {
    className += ' toolbar-shell-compact'
  }
  if (isDenseToolbar()) {
    className += ' toolbar-shell-dense'
  }

  return className
}

function getDesktopToolbarGroupClass(isLeading: boolean) : string {
  let className = 'toolbar-group'
  if (isLeading) {
    className += ' toolbar-group-leading'
  }
  if (isCompactToolbar()) {
    className += ' toolbar-group-compact'
    if (isLeading) {
      className += ' toolbar-group-leading-compact'
    }
  }
  if (isDenseToolbar()) {
    className += ' toolbar-group-dense'
  }
  return className
}

function getDesktopToolbarDividerClass() : string {
  if (isCompactToolbar()) {
    return 'toolbar-divider toolbar-divider-hidden'
  }
  return 'toolbar-divider'
}

function getToolbarPanelClass() : string {
  // #ifndef WEB
  return 'toolbar-panel toolbar-panel-attached'
  // #endif
  if (isWebPhoneLayout()) {
    return 'toolbar-panel toolbar-panel-mobile-web'
  }
  if (data.sheetAnimated) {
    return 'toolbar-panel toolbar-panel-attached toolbar-panel-floating toolbar-panel-floating-attached toolbar-panel-floating-entered'
  }
  return 'toolbar-panel toolbar-panel-attached toolbar-panel-floating toolbar-panel-floating-attached'
}

function getToolbarPanelHeaderClass() : string {
  // #ifndef WEB
  return 'toolbar-panel-header'
  // #endif
  if (isWebPhoneLayout()) {
    return 'toolbar-panel-header toolbar-panel-header-mobile-web'
  }
  return 'toolbar-panel-header toolbar-panel-header-floating'
}

function getToolbarPanelTitleClass() : string {
  // #ifndef WEB
  return 'toolbar-panel-title'
  // #endif
  if (isWebPhoneLayout()) {
    return 'toolbar-panel-title toolbar-panel-title-mobile-web'
  }
  return 'toolbar-panel-title toolbar-panel-title-floating'
}

function getToolbarPanelSubtitleClass() : string {
  // #ifndef WEB
  return 'toolbar-panel-subtitle'
  // #endif
  if (isWebPhoneLayout()) {
    return 'toolbar-panel-subtitle toolbar-panel-subtitle-mobile-web'
  }
  return 'toolbar-panel-subtitle toolbar-panel-subtitle-floating'
}

function getToolbarPanelScrollClass() : string {
  // #ifndef WEB
  return 'toolbar-panel-scroll'
  // #endif
  if (isWebPhoneLayout()) {
    return 'toolbar-panel-scroll toolbar-panel-scroll-mobile-web'
  }
  return 'toolbar-panel-scroll toolbar-panel-scroll-floating'
}

function getDesktopMenuItemClass(active: boolean) : string {
  return active ? 'desktop-menu-item desktop-menu-item-active' : 'desktop-menu-item'
}

function getDesktopMenuBadgeClass(active: boolean) : string {
  return active ? 'desktop-menu-badge desktop-menu-badge-active' : 'desktop-menu-badge'
}

function getDesktopMenuTextClass(active: boolean) : string {
  return active ? 'desktop-menu-text desktop-menu-text-active' : 'desktop-menu-text'
}

function closeSheets() {
  data.sheetAnimated = false
  data.editorInputType = ''
  // #ifdef WEB
  hideToolbarTooltip()
  // #endif
  data.activeSheet = ''
  syncToolbarChipClasses()
  syncToolbarBottom()
  syncToolbarVisibility()
}

function getClearColorSwatchClass(isActive: boolean) : string {
  let className = 'color-swatch color-swatch-panel'
  className += ' color-swatch-clear'
  if (isActive) {
    className += ' color-swatch-active'
  }
  return className
}

function getFilledColorSwatchClass(isActive: boolean) : string {
  let className = 'color-swatch color-swatch-panel'
  if (isActive) {
    className += ' color-swatch-active'
  }
  return className
}

function refocusEditorAfterToolbarAction() {
  // #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
  setTimeout(() => {
    const editorElement = uni.getElementById('editor')
    if (editorElement != null) {
      editorElement.focus()
    }
  }, 30)
  // #endif
}

function updatePageWindowHeight() {
  const windowInfo = uni.getWindowInfo()
  data.pageWindowHeight = windowInfo.windowHeight
  data.pageWindowWidth = windowInfo.windowWidth
  syncToolbarChipClasses()
}

function getSheetButtonId(sheet: string) : string {
  if (sheet == 'more') {
    return 'toolbar-button-align'
  }
  if (sheet == 'title') {
    return 'toolbar-button-title'
  }
  if (sheet == 'style') {
    return 'toolbar-button-style'
  }
  if (sheet == 'font-size') {
    return 'toolbar-button-font-size'
  }
  if (sheet == 'font-family') {
    return 'toolbar-button-font-family'
  }
  if (sheet == 'text-color') {
    return 'toolbar-button-text-color'
  }
  if (sheet == 'background-color') {
    return 'toolbar-button-background-color'
  }
  if (sheet == 'line-height') {
    return 'toolbar-button-line-height'
  }
  if (sheet == 'letter-spacing') {
    return 'toolbar-button-letter-spacing'
  }
  if (sheet == 'block-indent') {
    return 'toolbar-button-block-indent'
  }
  if (sheet == 'list') {
    return 'toolbar-button-list'
  }
  return 'toolbar-button-align'
}

function updateSheetAnchor(sheet: string) {
  updatePageWindowHeight()
  // #ifndef WEB
  data.sheetTop = 74
  data.sheetLeft = 12
  data.sheetArrowLeft = 32
  return
  // #endif
  const layoutElement = uni.getElementById('editor-layout')
  const buttonElement = uni.getElementById(getSheetButtonId(sheet))
  if (buttonElement == null || layoutElement == null) {
    data.sheetTop = 74
    data.sheetLeft = 12
    data.sheetArrowLeft = 32
    return
  }
  const pageRect = layoutElement!.getBoundingClientRect()
  const rect = buttonElement!.getBoundingClientRect()
  const panelWidth = getSheetWidth()
  let panelLeft = rect.left - pageRect.left + rect.width / 2 - panelWidth / 2
  let maxLeft = pageRect.width - panelWidth - 12
  if (maxLeft < 12) {
    maxLeft = 12
  }
  if (panelLeft < 12) {
    panelLeft = 12
  }
  if (panelLeft > maxLeft) {
    panelLeft = maxLeft
  }
  let arrowLeft = rect.left - pageRect.left + rect.width / 2 - panelLeft - 8
  if (arrowLeft < 18) {
    arrowLeft = 18
  }
  if (arrowLeft > panelWidth - 30) {
    arrowLeft = panelWidth - 30
  }
  data.sheetTop = rect.bottom - pageRect.top + 12
  data.sheetLeft = panelLeft
  data.sheetArrowLeft = arrowLeft
}

function onEditorReady() {
  const options: CreateEditorContextAsyncOptions = {
    id: 'editor',
    success: (context: EditorContext) => {
      editorContext.value = context
      data.editorReadyTest = true
    },
    fail: (error: UniError) => {
      console.log('createEditorContextAsync fail', error)
    }
  }
  uni.createEditorContextAsync(options)
}

function applyFormat(name: string, value: string | number | null | undefined) {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  // #ifndef WEB
  statusSyncLockedUntil = Date.now() + 400
  // #endif
  editor.format(name, value)
  editor.scrollIntoView(null)
}

function setLocalFormat(name: string, value: string | number | boolean | null) {
  if (name == 'bold') {
    data.formats.bold = value == true
    return
  }
  if (name == 'italic') {
    data.formats.italic = value == true
    return
  }
  if (name == 'underline') {
    data.formats.underline = value == true
    return
  }
  if (name == 'strike') {
    data.formats.strike = value == true
    return
  }
  if (name == 'blockquote') {
    data.formats.blockquote = value == true
    return
  }
  if (name == 'header') {
    data.formats.header = value != null ? value as number : 0
    return
  }
  if (name == 'code-block') {
    data.formats.codeBlock = value == true
    return
  }
  if (name == 'link') {
    data.formats.link = value != null ? value as string : ''
    return
  }
  if (name == 'list') {
    data.formats.list = value != null ? value as string : ''
    return
  }
  if (name == 'align') {
    data.formats.align = value != null ? value as string : ''
    return
  }
  if (name == 'textIndent') {
    data.formats.textIndent = value != null ? value as string : ''
    return
  }
  if (name == 'marginLeft') {
    data.formats.marginLeft = value != null ? value as string : ''
    return
  }
  if (name == 'marginRight') {
    data.formats.marginRight = value != null ? value as string : ''
    return
  }
  if (name == 'lineHeight') {
    data.formats.lineHeight = value != null ? value as string : ''
    return
  }
  if (name == 'letterSpacing') {
    data.formats.letterSpacing = value != null ? value as string : ''
    return
  }
  if (name == 'fontFamily') {
    data.formats.fontFamily = value != null ? value as string : ''
    return
  }
  if (name == 'fontSize') {
    data.formats.fontSize = value != null ? value as string : ''
    return
  }
  if (name == 'color') {
    data.formats.color = value != null ? value as string : ''
    return
  }
  if (name == 'backgroundColor') {
    data.formats.backgroundColor = value != null ? value as string : ''
  }
}

function applyFormatWithState(name: string, value: string | number | null | undefined) {
  setLocalFormat(name, value)
  applyFormat(name, value)
}

function setToolbarFormatsForTest(header: number, list: string) {
  setLocalFormat('header', header)
  setLocalFormat('list', list)
}

function applyToolbarPresetForTest(preset: string) {
  if (preset == 'title-h2') {
    setToolbarFormatsForTest(2, '')
    return
  }
  if (preset == 'title-h1') {
    setToolbarFormatsForTest(1, '')
    return
  }
  if (preset == 'list-bullet') {
    setToolbarFormatsForTest(0, 'bullet')
    return
  }
  if (preset == 'list-ordered') {
    setToolbarFormatsForTest(0, 'ordered')
    return
  }
  if (preset == 'list-unchecked') {
    setToolbarFormatsForTest(0, 'unchecked')
    return
  }
  if (preset == 'list-none') {
    setToolbarFormatsForTest(0, '')
  }
}

function toggleInlineFormat(name: string) {
  if (name == 'bold') {
    setLocalFormat('bold', !data.formats.bold)
    applyFormat('bold', null)
    return
  }
  if (name == 'italic') {
    setLocalFormat('italic', !data.formats.italic)
    applyFormat('italic', null)
    return
  }
  if (name == 'underline') {
    setLocalFormat('underline', !data.formats.underline)
    applyFormat('underline', null)
    return
  }
  if (name == 'strike') {
    setLocalFormat('strike', !data.formats.strike)
    applyFormat('strike', null)
  }
}

function cloneFormats(source: Formats) : Formats {
  return {
    bold: source.bold,
    italic: source.italic,
    underline: source.underline,
    strike: source.strike,
    blockquote: source.blockquote,
    link: source.link,
    header: source.header,
    list: source.list,
    align: source.align,
    textIndent: source.textIndent,
    marginLeft: source.marginLeft,
    marginRight: source.marginRight,
    lineHeight: source.lineHeight,
    letterSpacing: source.letterSpacing,
    fontFamily: source.fontFamily,
    fontSize: source.fontSize,
    color: source.color,
    backgroundColor: source.backgroundColor,
    codeBlock: source.codeBlock
  }
}

function clearFormatPainterState() {
  data.formatPainterActive = false
}

function resetLocalFormats() {
  assignFormats(data.formats, createDefaultFormats())
}

function resetFormatsForTest() {
  assignFormats(data.formats, createDefaultFormats())
}

function applyCopiedFormats() {
  const copied = cloneFormats(data.formatPainterFormats)
  applyFormat('header', copied.header > 0 ? copied.header : 0)
  applyFormat('list', copied.list != '' ? copied.list : null)
  applyFormat('align', copied.align != '' ? copied.align : null)
  applyFormat('textIndent', copied.textIndent != '' ? copied.textIndent : null)
  applyFormat('marginLeft', copied.marginLeft != '' ? copied.marginLeft : null)
  applyFormat('marginRight', copied.marginRight != '' ? copied.marginRight : null)
  applyFormat('lineHeight', copied.lineHeight != '' ? copied.lineHeight : null)
  applyFormat('letterSpacing', copied.letterSpacing != '' ? copied.letterSpacing : null)
  applyFormat('fontFamily', copied.fontFamily != '' ? copied.fontFamily : null)
  applyFormat('fontSize', copied.fontSize != '' ? copied.fontSize : null)
  applyFormat('color', copied.color != '' ? copied.color : null)
  applyFormat('backgroundColor', copied.backgroundColor != '' ? copied.backgroundColor : null)
  if (copied.bold != data.formats.bold) {
    applyFormat('bold', null)
  }
  if (copied.italic != data.formats.italic) {
    applyFormat('italic', null)
  }
  if (copied.underline != data.formats.underline) {
    applyFormat('underline', null)
  }
  if (copied.strike != data.formats.strike) {
    applyFormat('strike', null)
  }
}

function toggleFormatPainter() {
  if (data.formatPainterActive) {
    clearFormatPainterState()
    return
  }
  data.formatPainterFormats = cloneFormats(data.formats)
  data.formatPainterActive = true
  closeSheets()
}

function getEditorBottomSpacing() : number {
  return 0
}

function getEditorStageStyle() : string {
  const paddingBottom = getEditorBottomSpacing()
  return `padding-bottom:${paddingBottom}px;`
}

function hideKeyboardForSheet() {
  // editor 切换 hidekeyboard: 下边注释放开
  // uni.hideKeyboard()

  data.editorInputType = 'none'
  data.isFocused = false
  uni.hideKeyboard()
}

function toggleSheet(sheet: string) {
  if (data.activeSheet == sheet) {
    closeSheets()
    return
  }

  // #ifdef APP-ANDROID
  uni.showToast({
    title: '请手动收起键盘后选择操作',
    icon: 'none'
  })
  // #endif

  updateSheetAnchor(sheet)
  data.activeSheet = sheet
  syncToolbarChipClasses()
  data.sheetAnimated = false
  setTimeout(() => {
    if (data.activeSheet == sheet) {
      data.sheetAnimated = true
    }
  }, 16)

  // #ifndef APP-ANDROID
  hideKeyboardForSheet()
  // #endif
  syncToolbarBottom()
  syncToolbarVisibility()
}

function openMoreSheet() {
  toggleSheet('more')
}

function openTitleSheet() {
  toggleSheet('title')
}

function openStyleSheet() {
  toggleSheet('style')
}

function openTextColorSheet() {
  toggleSheet('text-color')
}

function openBackgroundColorSheet() {
  toggleSheet('background-color')
}

function openLineHeightSheet() {
  toggleSheet('line-height')
}

function openLetterSpacingSheet() {
  toggleSheet('letter-spacing')
}

function openBlockIndentSheet() {
  toggleSheet('block-indent')
}

function openListSheet() {
  toggleSheet('list')
}

function openFontSizeSheet() {
  toggleSheet('font-size')
}

function openFontFamilySheet() {
  toggleSheet('font-family')
}

function openAlignSheet() {
  toggleSheet('align')
}

function getPanelTitle() : string {
  if (data.activeSheet == 'title') {
    return '设置标题'
  }
  if (data.activeSheet == 'style') {
    return '设置字格式'
  }
  if (data.activeSheet == 'text-color') {
    return '设置文字颜色'
  }
  if (data.activeSheet == 'background-color') {
    return '设置背景颜色'
  }
  if (data.activeSheet == 'line-height') {
    return '设置行间距'
  }
  if (data.activeSheet == 'letter-spacing') {
    return '设置字间距'
  }
  if (data.activeSheet == 'font-size') {
    return '设置字号'
  }
  if (data.activeSheet == 'font-family') {
    return '设置字体'
  }
  if (data.activeSheet == 'block-indent') {
    return '设置两端缩进'
  }
  if (data.activeSheet == 'align') {
    return '对齐方式'
  }
  if (data.activeSheet == 'list') {
    return '设置列表'
  }
  return '更多操作'
}

function getTitleSummary() : string {
  if (data.formats.codeBlock) {
    return '当前为代码块'
  }
  if (data.formats.blockquote) {
    return '当前为引用'
  }
  if (data.formats.header > 0) {
    return `当前为大标题${data.formats.header}`
  }
  return '当前为正文'
}

function openLinkModal() {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  closeSheets()
  data.linkSelectionText = ''
  data.linkOriginalHref = data.formats.link
  // #ifndef WEB
  uni.hideKeyboard()
  // #endif
  data.linkDraftText = ''
  data.linkDraftHref = data.formats.link
  data.linkModalVisible = true
  data.linkUseSelectionText = false
  clearToolbarHideTimer()
  const options: UniEditorElementGetSelectionTextOptions = {
    success: (res: UniEditorElementGetSelectionTextOptionsRes) => {
      const selectionText = res.text.trim()
      if (selectionText == '') {
        return
      }
      data.linkSelectionText = selectionText
      data.linkDraftText = selectionText
      data.linkUseSelectionText = data.formats.link == ''
    },
    fail: () => {},
    complete: () => {}
  }
  editor.getSelectionText(options)
}

function closeLinkModal() {
  data.linkModalVisible = false
  data.linkUseSelectionText = false
  data.linkDraftText = ''
  data.linkDraftHref = ''
  data.linkSelectionText = ''
  data.linkOriginalHref = ''
}

function getDesktopTitleLabel() : string {
  if (data.formats.header > 0) {
    return `标题 ${data.formats.header}`
  }
  return '标题'
}

function getDesktopTitleMiniLabel() : string {
  if (data.formats.header > 0) {
    return `${data.formats.header}`
  }
  return 'P'
}

function getDesktopTitleIconGlyph() : string {
  if (data.formats.header == 1) {
    return "&#xe860;"
  }
  if (data.formats.header == 2) {
    return "&#xe75c;"
  }
  if (data.formats.header == 3) {
    return "&#xe75d;"
  }
  if (data.formats.header == 4) {
    return "&#xe863;"
  }
  if (data.formats.header == 5) {
    return "&#xe864;"
  }
  if (data.formats.header == 6) {
    return "&#xe865;"
  }
  return "&#xe67b;"
}

function getDesktopAlignIconGlyph() : string {
  if (data.formats.align == 'center') {
    return "\ue6b6"
  }
  if (data.formats.align == 'right') {
    return "\ue6a6"
  }
  if (data.formats.align == 'justify') {
    return "\ue6a8"
  }
  return "\ue6b7"
}

function getStyleSummary() : string {
  const names: string[] = []
  if (data.formats.bold) {
    names.push('加粗')
  }
  if (data.formats.italic) {
    names.push('斜体')
  }
  if (data.formats.underline) {
    names.push('下划线')
  }
  if (data.formats.strike) {
    names.push('删除线')
  }
  if (data.formats.lineHeight != '') {
    names.push(`行间距 ${data.formats.lineHeight}`)
  }
  if (data.formats.letterSpacing != '') {
    names.push(`字间距 ${data.formats.letterSpacing}`)
  }
  if (data.formats.fontSize != '') {
    names.push(`字号 ${data.formats.fontSize}`)
  }
  if (data.formats.fontFamily != '') {
    names.push(`字体 ${getDesktopFontFamilyLabel()}`)
  }
  if (names.length == 0) {
    return '当前未设置字格式'
  }
  return names.join(' / ')
}

function getAlignSummary() : string {
  if (data.formats.align == '') {
    return '当前为默认对齐'
  }
  if (data.formats.align == 'center') {
    return '当前为居中'
  }
  if (data.formats.align == 'right') {
    return '当前为居右'
  }
  if (data.formats.align == 'justify') {
    return '当前为两端对齐'
  }
  return '当前为居左'
}

function getDesktopAlignLabel() : string {
  if (data.formats.align == 'center') {
    return '居中'
  }
  if (data.formats.align == 'right') {
    return '居右'
  }
  if (data.formats.align == 'justify') {
    return '两端'
  }
  return '居左'
}

function getDesktopBlockIndentLabel() : string {
  const margin = data.formats.marginLeft
  if (margin != '' && margin == data.formats.marginRight) {
    return margin.replace('px', '')
  }
  return '0'
}

function getDesktopListLabel() : string {
  if (data.formats.list == 'ordered') {
    return '有序列表'
  }
  if (data.formats.list == 'bullet') {
    return '无序列表'
  }
  return '列表'
}

function getDesktopLineHeightLabel() : string {
  if (data.formats.lineHeight == '1.5') {
    return '1.5'
  }
  if (data.formats.lineHeight == '1.8') {
    return '1.8'
  }
  if (data.formats.lineHeight == '2') {
    return '2.0'
  }
  return '行间距'
}

function getDesktopLineHeightMiniLabel() : string {
  if (data.formats.lineHeight == '1.5') {
    return '1.5'
  }
  if (data.formats.lineHeight == '1.8') {
    return '1.8'
  }
  if (data.formats.lineHeight == '2') {
    return '2'
  }
  return 'D'
}

function getDesktopLetterSpacingLabel() : string {
  if (data.formats.letterSpacing == '0px') {
    return '0px'
  }
  if (data.formats.letterSpacing == '1px') {
    return '1px'
  }
  if (data.formats.letterSpacing == '2px') {
    return '2px'
  }
  return '字间距'
}

function getDesktopLetterSpacingMiniLabel() : string {
  if (data.formats.letterSpacing == '0px') {
    return '0'
  }
  if (data.formats.letterSpacing == '1px') {
    return '1'
  }
  if (data.formats.letterSpacing == '2px') {
    return '2'
  }
  return 'D'
}

function getDesktopFontSizeLabel() : string {
  if (data.formats.fontSize != '') {
    return data.formats.fontSize
  }
  return '17px'
}

function getToolbarTextColorChipStyle() : string {
  const color = data.formats.color != '' ? data.formats.color : '#111827'
  return `background-color:${color};`
}

function getToolbarBackgroundColorChipStyle() : string {
  const color = data.formats.backgroundColor != '' ? data.formats.backgroundColor : '#f3f4f6'
  return `background-color:${color};`
}

function getPanelSubtitle() : string {
  if (data.activeSheet == 'title') {
    return getTitleSummary()
  }
  if (data.activeSheet == 'style') {
    return getStyleSummary()
  }
  if (data.activeSheet == 'text-color') {
    if (data.formats.color == '') {
      return '当前使用默认文字颜色'
    }
    return `当前文字颜色 ${data.formats.color}`
  }
  if (data.activeSheet == 'background-color') {
    if (data.formats.backgroundColor == '') {
      return '当前未设置文字背景颜色'
    }
    return `当前背景颜色 ${data.formats.backgroundColor}`
  }
  if (data.activeSheet == 'line-height') {
    if (data.formats.lineHeight == '') {
      return '当前使用默认行间距'
    }
    return `当前行间距 ${data.formats.lineHeight}`
  }
  if (data.activeSheet == 'letter-spacing') {
    if (data.formats.letterSpacing == '') {
      return '当前使用默认字间距'
    }
    return `当前字间距 ${data.formats.letterSpacing}`
  }
  if (data.activeSheet == 'font-size') {
    if (data.formats.fontSize == '') {
      return '当前使用默认字号 17px'
    }
    return `当前字号 ${data.formats.fontSize}`
  }
  if (data.activeSheet == 'font-family') {
    if (data.formats.fontFamily == '') {
      return '当前使用默认字体'
    }
    return `当前字体 ${getDesktopFontFamilyLabel()}`
  }
  if (data.activeSheet == 'block-indent') {
    if (data.formats.marginLeft == '' || data.formats.marginLeft != data.formats.marginRight) {
      return '当前未设置两端缩进'
    }
    return `当前两端缩进 ${data.formats.marginLeft}`
  }
  if (data.activeSheet == 'align') {
    return getAlignSummary()
  }
  if (data.activeSheet == 'list') {
    return data.formats.list == '' ? '当前未设置列表' : `当前列表 ${getDesktopListLabel()}`
  }
  return '插入与编辑快捷操作'
}

function hasTitleFormatActive() : boolean {
  return data.formats.header > 0 || data.formats.blockquote || data.formats.codeBlock
}

function isTitleSheetHighlighted() : boolean {
  return data.activeSheet == 'title' || hasTitleFormatActive()
}

function isAlignSheetHighlighted() : boolean {
  return data.activeSheet == 'align' || data.formats.align != ''
}

function isLineHeightSheetHighlighted() : boolean {
  return data.activeSheet == 'line-height' || data.formats.lineHeight != ''
}

function isLetterSpacingSheetHighlighted() : boolean {
  return data.activeSheet == 'letter-spacing' || data.formats.letterSpacing != ''
}

function isFontSizeSheetHighlighted() : boolean {
  return data.activeSheet == 'font-size' || data.formats.fontSize != ''
}

function isFontFamilySheetHighlighted() : boolean {
  return data.activeSheet == 'font-family' || data.formats.fontFamily != ''
}

function isTextIndentHighlighted() : boolean {
  return data.formats.textIndent != ''
}

function isBlockIndentSheetHighlighted() : boolean {
  return data.activeSheet == 'block-indent' || (data.formats.marginLeft != '' && data.formats.marginLeft == data.formats.marginRight)
}

function isListSheetHighlighted() : boolean {
  return data.activeSheet == 'list' || data.formats.list == 'ordered' || data.formats.list == 'bullet'
}

function isBlockIndentValue(value: string) : boolean {
  if (value == '0px') {
    return data.formats.marginLeft == '' && data.formats.marginRight == ''
  }
  return data.formats.marginLeft == value && data.formats.marginRight == value
}

function isTextColorSheetHighlighted() : boolean {
  return data.activeSheet == 'text-color' || data.formats.color != ''
}

function isBackgroundColorSheetHighlighted() : boolean {
  return data.activeSheet == 'background-color' || data.formats.backgroundColor != ''
}

function isParagraphActive() : boolean {
  return data.formats.header == 0 && data.formats.list == '' && !data.formats.blockquote && !data.formats.codeBlock
}


const pageShellClass = computed((): string => getPageShellClass())
const editorLayoutClass = computed((): string => getEditorLayoutClass())
const toolbarWrapStyle = computed((): string => getToolbarWrapStyle())
const desktopToolbarShellClass = computed((): string => getDesktopToolbarShellClass())
const desktopToolbarLeadingGroupClass = computed((): string => getDesktopToolbarGroupClass(true))
const desktopToolbarGroupClass = computed((): string => getDesktopToolbarGroupClass(false))
const desktopToolbarDividerClass = computed((): string => getDesktopToolbarDividerClass())
const fontSizeSheetHighlighted = computed((): boolean => isFontSizeSheetHighlighted())
const fontFamilySheetHighlighted = computed((): boolean => isFontFamilySheetHighlighted())
const textColorSheetHighlighted = computed((): boolean => isTextColorSheetHighlighted())
const backgroundColorSheetHighlighted = computed((): boolean => isBackgroundColorSheetHighlighted())
const alignSheetHighlighted = computed((): boolean => isAlignSheetHighlighted())
const desktopAlignIconGlyph = computed((): string => getDesktopAlignIconGlyph())
const textIndentHighlighted = computed((): boolean => isTextIndentHighlighted())
const blockIndentSheetHighlighted = computed((): boolean => isBlockIndentSheetHighlighted())
const lineHeightSheetHighlighted = computed((): boolean => isLineHeightSheetHighlighted())
const letterSpacingSheetHighlighted = computed((): boolean => isLetterSpacingSheetHighlighted())
const listSheetHighlighted = computed((): boolean => isListSheetHighlighted())
const styleSheetHighlighted = computed((): boolean => isStyleSheetHighlighted())
const titleSheetHighlighted = computed((): boolean => isTitleSheetHighlighted())
const toolbarTextColorChipStyle = computed((): string => getToolbarTextColorChipStyle())
const toolbarBackgroundColorChipStyle = computed((): string => getToolbarBackgroundColorChipStyle())
const toolbarTooltipStyle = computed((): string => getToolbarTooltipStyle())
const sheetHostClass = computed((): string => getSheetHostClass())
const sheetHostStyle = computed((): string => getSheetHostStyle())
const sheetPanelStyle = computed((): string => getSheetPanelStyle())
const toolbarPanelArrowStyle = computed((): string => getToolbarPanelArrowStyle())
const toolbarPanelClass = computed((): string => getToolbarPanelClass())
const toolbarPanelHeaderClass = computed((): string => getToolbarPanelHeaderClass())
const toolbarPanelTitleClass = computed((): string => getToolbarPanelTitleClass())
const toolbarPanelSubtitleClass = computed((): string => getToolbarPanelSubtitleClass())
const toolbarPanelScrollClass = computed((): string => getToolbarPanelScrollClass())
const paragraphActive = computed((): boolean => isParagraphActive())
const editorStageStyle = computed((): string => getEditorStageStyle())
const desktopMenuItemClass = computed((): string => getDesktopMenuItemClass(false))
const desktopMenuItemActiveClass = computed((): string => getDesktopMenuItemClass(true))
const desktopMenuBadgeClass = computed((): string => getDesktopMenuBadgeClass(false))
const desktopMenuBadgeActiveClass = computed((): string => getDesktopMenuBadgeClass(true))
const desktopMenuTextClass = computed((): string => getDesktopMenuTextClass(false))
const desktopMenuTextActiveClass = computed((): string => getDesktopMenuTextClass(true))
const clearColorSwatchClass = computed((): string => getClearColorSwatchClass(false))
const clearColorSwatchActiveClass = computed((): string => getClearColorSwatchClass(true))
const filledColorSwatchClass = computed((): string => getFilledColorSwatchClass(false))
const filledColorSwatchActiveClass = computed((): string => getFilledColorSwatchClass(true))
const webPhoneLayout = computed((): boolean => isWebPhoneLayout())

function clearTitleFormats() {
  applyFormat('blockquote', null)
  if (data.formats.codeBlock) {
    setLocalFormat('code-block', false)
    applyFormat('code-block', null)
  }
  applyFormatWithState('list', null)
}

function setParagraph() {
  clearTitleFormats()
  applyFormatWithState('header', 0)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setHeadingLevel(level: number) {
  clearTitleFormats()
  if (data.formats.header == level) {
    applyFormatWithState('header', 0)
  } else {
    applyFormatWithState('header', level)
  }
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setOrderedList() {
  applyFormat('blockquote', null)
  if (data.formats.codeBlock) {
    setLocalFormat('code-block', false)
    applyFormat('code-block', null)
  }
  applyFormatWithState('header', 0)
  applyFormatWithState('list', data.formats.list == 'ordered' ? null : 'ordered')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setBulletList() {
  applyFormat('blockquote', null)
  if (data.formats.codeBlock) {
    setLocalFormat('code-block', false)
    applyFormat('code-block', null)
  }
  applyFormatWithState('header', 0)
  applyFormatWithState('list', data.formats.list == 'bullet' ? null : 'bullet')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function clearListFormat() {
  applyFormatWithState('list', null)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleBlockquote() {
  const nextValue = !data.formats.blockquote
  const isListFormat = data.formats.list != ''
  const isHeadFormat = data.formats.header != 0
  const isCodeBlock = data.formats.codeBlock

  if (nextValue && isListFormat) {
    applyFormatWithState('list', null)
  }
  if (nextValue && isHeadFormat) {
    applyFormatWithState('header', 0)
  }
  if (nextValue && isCodeBlock) {
    setLocalFormat('code-block', false)
    applyFormat('code-block', null)
  }

  setLocalFormat('blockquote', nextValue)
  applyFormat('blockquote', null)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleCodeBlock() {
  const nextValue = !data.formats.codeBlock
  if (nextValue) {
    if (data.formats.list != '') {
      applyFormatWithState('list', null)
    }
    if (data.formats.header != 0) {
      applyFormatWithState('header', 0)
    }
    if (data.formats.blockquote) {
      applyFormatWithState('blockquote', null)
    }
  }
  setLocalFormat('code-block', nextValue)
  applyFormat('code-block', null)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleBold() {
  toggleInlineFormat('bold')
}

function toggleItalic() {
  toggleInlineFormat('italic')
}

function toggleUnderline() {
  toggleInlineFormat('underline')
}

function toggleStrike() {
  toggleInlineFormat('strike')
}

function toggleBoldAndClose() {
  toggleBold()
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleItalicAndClose() {
  toggleItalic()
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleUnderlineAndClose() {
  toggleUnderline()
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleStrikeAndClose() {
  toggleStrike()
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setTextColor(color: string) {
  applyFormatWithState('color', color == '' ? null : color)
}

function setTextColorAndClose(color: string) {
  setTextColor(color)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setBackgroundColor(color: string) {
  applyFormatWithState('backgroundColor', color == '' ? null : color)
}

function setBackgroundColorAndClose(color: string) {
  setBackgroundColor(color)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setLineHeight(value: string) {
  applyFormatWithState('lineHeight', value == '' ? null : value)
}

function setLineHeightAndClose(value: string) {
  setLineHeight(value)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setLetterSpacing(value: string) {
  applyFormatWithState('letterSpacing', value == '' ? null : value)
}

function setLetterSpacingAndClose(value: string) {
  setLetterSpacing(value)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setFontSize(value: string) {
  applyFormatWithState('fontSize', value == '' ? null : value)
}

function setFontSizeAndClose(value: string) {
  setFontSize(value)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setFontFamily(value: string) {
  applyFormatWithState('fontFamily', value == '' ? null : value)
}

function setFontFamilyAndClose(value: string) {
  setFontFamily(value)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleTextIndent() {
  const nextValue = data.formats.textIndent == '' ? '2em' : ''
  applyFormatWithState('textIndent', nextValue == '' ? null : nextValue)
  refocusEditorAfterToolbarAction()
}

function setBlockIndent(value: string) {
  const indentValue = value == '0px' ? '' : value
  applyFormatWithState('marginLeft', indentValue == '' ? null : indentValue)
  applyFormatWithState('marginRight', indentValue == '' ? null : indentValue)
}

function setBlockIndentAndClose(value: string) {
  setBlockIndent(value)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setAlignLeft() {
  applyFormatWithState('align', 'left')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setAlignCenter() {
  applyFormatWithState('align', 'center')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setAlignRight() {
  applyFormatWithState('align', 'right')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function setAlignJustify() {
  applyFormatWithState('align', 'justify')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function clearAlign() {
  applyFormatWithState('align', null)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function clearFormatting() {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.removeFormat(null)
  resetLocalFormats()
  clearFormatPainterState()
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function toggleChecklistTool() {
  applyFormatWithState('list', data.formats.list == 'check' ? null : 'check')
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function insertDivider() {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.insertDivider(null)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function getCon () {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.getContents({
    success: (res: UniEditorElementGetContentsOptionsRes) => {
      console.log('文本详情:', res)
      data.getContentDeltaTest = res.delta
    },
    fail: (err: any) => {
      console.log(err)
    }
  })
}

// 自动化测试专用
const setContents = (options: any) => {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.setContents({
    delta: {
      ops: options
    },
    success: (res: any) => {
      console.log('setContents-success', res)
    },
    fail: (err: any) => {
      console.log(err)
    }
  })
}

const blur = () => {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.blur({
    success: (res: any) => {
      console.log('编辑器失焦:', res)
      data.blurTest = true
    },
    fail: (err: any) => {
      console.log(err)
      data.blurTest = false
    }
  })
}

const clear = () => {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.clear({
    success: function (res: any) {
      console.log("clear success",res)
      data.clearTest = true
    },
    fail: (err: any) => {
      console.log(err)
      data.clearTest = false
    }
  })
}

const clearShowModal = () => {
  uni.showModal({
    title: '清空编辑器',
    content: '确定清空编辑器全部内容?',
    success: (res) => {
      if (res.confirm) {
        clear()
      }
    }
  })
}

const removeFormat = () => {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.removeFormat(null)
  data.removeFormatTest = true
}

function insertImage(path: string) {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  const options: UniEditorElementInsertImageOptions = {
    src: path,
    alt: 'image',
    success: () => {
      data.insertImageTest = true
    },
    fail: () => {
      data.insertImageTest = false
    }
  }
  editor.insertImage(options)
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function normalizeLinkHref(value: string) : string {
  const trimmedValue = value.trim()
  if (trimmedValue == '') {
    return ''
  }
  if (trimmedValue.startsWith('http://') || trimmedValue.startsWith('https://') || trimmedValue.startsWith('mailto:') || trimmedValue.startsWith('tel:')) {
    return trimmedValue
  }
  return `https://${trimmedValue}`
}

function escapeRegExp(value: string) : string {
  return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\<!-- UTSCOMJSON.editor.example -->')
}

function getBooleanAttribute(attributes: UTSJSONObject, key: string) : boolean {
  const value = attributes[key]
  return value != null && (value as boolean)
}

function buildOpAttributes(attributes: UTSJSONObject | null, nextLinkValue: string | null, preserveLink: boolean) : UTSJSONObject | null {
  let hasValue = false
  const nextAttributes: UTSJSONObject = {}
  if (attributes != null) {
    if (getBooleanAttribute(attributes, 'bold')) {
      nextAttributes['bold'] = true
      hasValue = true
    }
    if (getBooleanAttribute(attributes, 'italic')) {
      nextAttributes['italic'] = true
      hasValue = true
    }
    if (getBooleanAttribute(attributes, 'underline')) {
      nextAttributes['underline'] = true
      hasValue = true
    }
    if (getBooleanAttribute(attributes, 'strike')) {
      nextAttributes['strike'] = true
      hasValue = true
    }
    if (getBooleanAttribute(attributes, 'blockquote')) {
      nextAttributes['blockquote'] = true
      hasValue = true
    }
    if (getBooleanAttribute(attributes, 'code-block')) {
      nextAttributes['code-block'] = true
      hasValue = true
    }
    if (attributes['header'] != null && (attributes['header'] as number) != 0) {
      nextAttributes['header'] = attributes['header']
      hasValue = true
    }
    if (attributes['list'] != null && (attributes['list'] as string) != '') {
      nextAttributes['list'] = attributes['list']
      hasValue = true
    }
    if (attributes['align'] != null && (attributes['align'] as string) != '') {
      nextAttributes['align'] = attributes['align']
      hasValue = true
    }
    if (attributes['textIndent'] != null && (attributes['textIndent'] as string) != '') {
      nextAttributes['textIndent'] = attributes['textIndent']
      hasValue = true
    }
    if (attributes['marginLeft'] != null && (attributes['marginLeft'] as string) != '') {
      nextAttributes['marginLeft'] = attributes['marginLeft']
      hasValue = true
    }
    if (attributes['marginRight'] != null && (attributes['marginRight'] as string) != '') {
      nextAttributes['marginRight'] = attributes['marginRight']
      hasValue = true
    }
    if (attributes['lineHeight'] != null && (attributes['lineHeight'] as string) != '') {
      nextAttributes['lineHeight'] = attributes['lineHeight']
      hasValue = true
    }
    if (attributes['letterSpacing'] != null && (attributes['letterSpacing'] as string) != '') {
      nextAttributes['letterSpacing'] = attributes['letterSpacing']
      hasValue = true
    }
    if (attributes['fontFamily'] != null && (attributes['fontFamily'] as string) != '') {
      nextAttributes['fontFamily'] = attributes['fontFamily']
      hasValue = true
    }
    if (attributes['fontSize'] != null && (attributes['fontSize'] as string) != '') {
      nextAttributes['fontSize'] = attributes['fontSize']
      hasValue = true
    }
    if (attributes['color'] != null && (attributes['color'] as string) != '') {
      nextAttributes['color'] = attributes['color']
      hasValue = true
    }
    if (attributes['backgroundColor'] != null && (attributes['backgroundColor'] as string) != '') {
      nextAttributes['backgroundColor'] = attributes['backgroundColor']
      hasValue = true
    }
    if (preserveLink && attributes['link'] != null && (attributes['link'] as string) != '') {
      nextAttributes['link'] = attributes['link']
      hasValue = true
    }
  }
  if (nextLinkValue != null && nextLinkValue != '') {
    nextAttributes['link'] = nextLinkValue
    hasValue = true
  }
  return hasValue ? nextAttributes : null
}

function pushTextOp(ops: any[], text: string, attributes: any | null) {
  if (text == '') {
    return
  }
  if (attributes != null) {
    ops.push({
      insert: text,
      attributes: attributes
    })
    return
  }
  ops.push({
    insert: text
  })
}

function replaceLinkInDeltaOps(ops: any[], selectionText: string, originalHref: string, nextHref: string | null) : any[] | null {
  const nextOps: any[] = []
  let replaced = false
  for (const op of ops) {
    const opItem = op as UTSJSONObject
    if (replaced) {
      nextOps.push(op)
      continue
    }
    const insertValue = opItem['insert']
    if (typeof insertValue != 'string') {
      nextOps.push(op)
      continue
    }
    const text = insertValue as string
    const attributes = opItem['attributes'] as UTSJSONObject | null
    const currentHref = attributes != null && attributes['link'] != null ? attributes['link'] as string : ''
    const matchesExistingLink = originalHref != '' && currentHref == originalHref
    const matchesPlainSelection = originalHref == '' && selectionText != '' && currentHref == ''
    if (!matchesExistingLink && !matchesPlainSelection) {
      nextOps.push(op)
      continue
    }

    let matchStart = 0
    let matchLength = text.length
    if (selectionText != '') {
      const index = text.indexOf(selectionText)
      if (index < 0) {
        nextOps.push(op)
        continue
      }
      matchStart = index
      matchLength = selectionText.length
    }

    const beforeText = text.slice(0, matchStart)
    const matchedText = text.slice(matchStart, matchStart + matchLength)
    const afterText = text.slice(matchStart + matchLength)
    pushTextOp(nextOps, beforeText, buildOpAttributes(attributes, null, true))

    const targetAttributes = buildOpAttributes(attributes, nextHref, false)
    pushTextOp(nextOps, matchedText, targetAttributes)
    pushTextOp(nextOps, afterText, buildOpAttributes(attributes, null, true))
    replaced = true
  }
  if (!replaced) {
    return null
  }
  return nextOps
}

function applyLinkDeltaMutation(nextHref: string | null) {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  const selectionText = data.linkSelectionText
  const originalHref = data.linkOriginalHref
  editor.getContents({
    success: (res: UniEditorElementGetContentsOptionsRes) => {
      const delta = res.delta as UTSJSONObject | null
      const ops = delta != null ? delta['ops'] as any[] : null
      if (ops == null) {
        uni.showToast({
          title: nextHref == null ? '删除链接失败' : '编辑链接失败',
          icon: 'none'
        })
        return
      }
      const nextOps = replaceLinkInDeltaOps(ops, selectionText, originalHref, nextHref)
      if (nextOps == null) {
        uni.showToast({
          title: nextHref == null ? '未定位到原超链接' : '未定位到原超链接文本',
          icon: 'none'
        })
        return
      }
      editor.setContents({
        delta: {
          ops: nextOps
        },
        success: () => {
          setLocalFormat('link', nextHref)
          closeSheets()
          closeLinkModal()
          refocusEditorAfterToolbarAction()
        },
        fail: () => {
          uni.showToast({
            title: nextHref == null ? '删除链接失败' : '编辑链接失败',
            icon: 'none'
          })
        }
      })
    },
    fail: () => {
      uni.showToast({
        title: nextHref == null ? '读取内容失败' : '读取内容失败',
        icon: 'none'
      })
    }
  })
}

function insertLink(text: string, href: string) {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  const normalizedHref = normalizeLinkHref(href)
  if (normalizedHref == '') {
    uni.showToast({
      title: '请输入链接地址',
      icon: 'none'
    })
    return
  }
  const trimmedText = text.trim()
  const linkText = trimmedText != '' ? trimmedText : normalizedHref
  const options: UniEditorElementInsertLinkOptions = {
    href: normalizedHref,
    text: linkText,
    success: () => {
      data.formats.link = normalizedHref
    },
    fail: () => {
      uni.showToast({
        title: '插入链接失败',
        icon: 'none'
      })
    }
  }
  editor.insertLink(options)
  closeSheets()
  closeLinkModal()
  refocusEditorAfterToolbarAction()
}

function confirmLinkModal() {
  const href = data.linkDraftHref.trim()
  if (href == '') {
    uni.showToast({
      title: '链接地址不能为空',
      icon: 'none'
    })
    return
  }
  // #ifndef WEB
  const normalizedHref = normalizeLinkHref(href)
  if (data.linkOriginalHref != '' || data.linkSelectionText != '') {
    applyLinkDeltaMutation(normalizedHref)
    return
  }
  // #endif
  insertLink(data.linkDraftText, href)
}

function removeLink() {
  const editor = getEditor()
  if (editor == null) {
    return
  }

  // #ifndef WEB
  if (data.linkOriginalHref != '' || data.linkSelectionText != '') {
    applyLinkDeltaMutation(null)
    return
  }
  // #endif

  applyFormatWithState('link', null)
  closeSheets()
  closeLinkModal()
  refocusEditorAfterToolbarAction()
}

function chooseInsertImage() {
  uni.chooseImage({
    count: 1,
    success: (res: ChooseImageSuccess) => {
      const paths = res.tempFilePaths
      if (paths.length > 0) {
        insertImage(paths[0])
      }
    }
  })
}

function undo() {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.undo({
    success: () => {
      data.undoTest = true
    },
    fail: () => {
      data.undoTest = false
    }
  })
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function redo() {
  const editor = getEditor()
  if (editor == null) {
    return
  }
  editor.redo({
    success: () => {
      data.redoTest = true
    },
    fail: () => {
      data.redoTest = false
    }
  })
  closeSheets()
  refocusEditorAfterToolbarAction()
}

function showToolbarTooltip(buttonId: string, text: string) {
  // #ifndef WEB
  return
  // #endif
  const buttonElement = uni.getElementById(buttonId)
  if (buttonElement == null) {
    return
  }
  const rect = buttonElement!.getBoundingClientRect()
  data.tooltipText = text
  data.tooltipVisible = true
  data.tooltipLeft = rect.left + rect.width / 2
  data.tooltipTop = rect.bottom + 8
}

function hideToolbarTooltip() {
  // #ifndef WEB
  return
  // #endif
  if (!data.tooltipVisible && data.tooltipText == '') {
    return
  }
  data.tooltipVisible = false
  data.tooltipText = ''
}

// #ifdef WEB
function onToolbarMouseDown(event: UniMouseEvent) {
  clearToolbarHideTimer()
  hideToolbarTooltip()
  data.ignoreNextBlur = true
  data.isFocused = true
  setTimeout(() => {
    data.ignoreNextBlur = false
  }, 0)
  event.preventDefault()
}
// #endif

function onEditorFocus(event: UniEditorFocusEvent) {
  console.log('onEditorFocus')
  clearToolbarHideTimer()
  data.editorInputType = ''
  // #ifdef WEB
  hideToolbarTooltip()
  // #endif
  if (data.activeSheet != '') {
    data.activeSheet = ''
    syncToolbarChipClasses()
  }
  data.isFocused = true
  syncToolbarVisibility()
  syncToolbarBottom()
  if (data.formatPainterActive) {
    setTimeout(() => {
      if (!data.formatPainterActive) {
        return
      }
      applyCopiedFormats()
      clearFormatPainterState()
    }, 0)
  }
}

function onEditorBlur(event: UniEditorBlurEvent) {
  if (data.ignoreNextBlur) {
    data.isFocused = true
    return
  }
  data.editorInputType = ''
  data.isFocused = false
  if (data.activeSheet == '') {
    syncToolbarBottom()
    syncToolbarVisibility()
    scheduleToolbarHide()
  }
}

function onStatusChange(event: UniEditorStatusChangeEvent) {
  // #ifndef WEB
  if (Date.now() < statusSyncLockedUntil) {
    return
  }
  // #endif
  const formats = event.detail
  const formatObject = JSON.parse<UTSJSONObject>(JSON.stringify(formats))
  if (formatObject == null) {
    return
  }
  const nextFormats = createDefaultFormats()

  nextFormats.bold = formats.bold == true
  nextFormats.italic = formats.italic == true
  nextFormats.underline = formats.underline == true
  nextFormats.strike = formats.strike == true
  nextFormats.blockquote = formats.blockquote == true
  const codeBlockValue = formatObject['code-block']
  nextFormats.codeBlock = codeBlockValue == true
  const linkValue = formatObject['link']
  if (linkValue != null) {
    nextFormats.link = linkValue as string
  } else if (data.linkModalVisible) {
    nextFormats.link = data.formats.link
  }
  if (formats.header != null) {
    nextFormats.header = formats.header
  }
  if (formats.list != null) {
    nextFormats.list = formats.list
  }
  if (formats.align != null) {
    nextFormats.align = formats.align
  }
  const textIndentValue = formatObject['textIndent']
  if (textIndentValue != null) {
    nextFormats.textIndent = textIndentValue as string
  }
  const marginLeftValue = formatObject['marginLeft']
  if (marginLeftValue != null) {
    nextFormats.marginLeft = marginLeftValue as string
  }
  const marginRightValue = formatObject['marginRight']
  if (marginRightValue != null) {
    nextFormats.marginRight = marginRightValue as string
  }
  if (formats.lineHeight != null) {
    nextFormats.lineHeight = formats.lineHeight
  }
  if (formats.letterSpacing != null) {
    nextFormats.letterSpacing = formats.letterSpacing
  }
  if (formats.fontFamily != null) {
    nextFormats.fontFamily = formats.fontFamily
  }
  if (formats.fontSize != null) {
    nextFormats.fontSize = formats.fontSize
  }
  if (formats.color != null) {
    nextFormats.color = formats.color
  }
  if (formats.backgroundColor != null) {
    nextFormats.backgroundColor = formats.backgroundColor
  }
  assignFormats(data.formats, nextFormats)
}

onReady(() => {
  updatePageWindowHeight()
})

onResize((options: OnResizeOptions) => {
  data.pageWindowHeight = options.size.windowHeight
  data.pageWindowWidth = options.size.windowWidth
  if (data.activeSheet != '') {
    updateSheetAnchor(data.activeSheet)
  }
})

onUnload(() => {
  clearToolbarHideTimer()
})

defineExpose({
  data,
  openMoreSheet,
  openTitleSheet,
  openStyleSheet,
  openTextColorSheet,
  openBackgroundColorSheet,
  openLineHeightSheet,
  openLetterSpacingSheet,
  openBlockIndentSheet,
  openListSheet,
  openFontSizeSheet,
  openFontFamilySheet,
  openAlignSheet,
  closeSheets,
  getPanelTitle,
  getPanelSubtitle,
  getTitleSummary,
  setToolbarFormatsForTest,
  applyToolbarPresetForTest,
  resetFormatsForTest,
  setParagraph,
  setHeadingLevel,
  toggleBold,
  toggleItalic,
  toggleUnderline,
  toggleStrike,
  setTextColorAndClose,
  setBackgroundColorAndClose,
  setLineHeightAndClose,
  setLetterSpacingAndClose,
  setFontSizeAndClose,
  setFontFamilyAndClose,
  toggleTextIndent,
  setBlockIndentAndClose,
  setAlignLeft,
  setAlignCenter,
  setAlignRight,
  setAlignJustify,
  toggleBlockquote,
  toggleCodeBlock,
  setBulletList,
  setOrderedList,
  toggleChecklistTool,
  openLinkModal,
  closeLinkModal,
  confirmLinkModal,
  removeLink,
  insertLink,
  setContents,
  blur,
  clear,
  insertDivider,
  undo,
  redo,
  insertImage,
  removeFormat,
  getCon
})
</script>

<style>
@import './editor-icon.css';

.page-shell {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  background-color: #edf1f4;
  padding-top: 16px;
  padding-right: 12px;
  padding-bottom: 12px;
  padding-left: 12px;
}

.page-shell-wide {
  padding-top: 24px;
  padding-right: 24px;
  padding-bottom: 20px;
  padding-left: 24px;
}

.editor-layout {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 1320px;
  align-self: center;
}

.editor-layout-wide {
  max-width: 1360px;
}

.editor-stage {
  flex: 1;
  display: flex;
  flex-direction: column;
  width: 100%;
  /* max-width: 1120px; */
  align-self: center;
  background-color: #ffffff;
  border-width: 1px;
  border-color: #d6dde4;
  border-radius: 0px;
  /* box-shadow: 0px 18px 36px rgba(15, 23, 42, 0.07); */
  overflow: hidden;
}

/* #ifdef WEB */
.editor-stage {
  border-top-width: 0px;
  border-top-left-radius: 0px;
  border-top-right-radius: 0px;
  border-bottom-left-radius: 0px;
  border-bottom-right-radius: 0px;
}
/* #endif */

.editor-field {
  flex: 1;
  background-color: #ffffff;
}

.toolbar-wrap {
  position: relative;
  left: auto;
  right: auto;
  bottom: auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  z-index: 130;
  margin-bottom: 0px;
  box-shadow: none;
}

.toolbar-frame {
  width: 100%;
  align-self: center;
  display: flex;
  flex-direction: column;
  background-color: #fafafa;
  border-width: 1px;
  border-color: #ebebeb;
  border-bottom-width: 1px;
  border-radius: 0px;
  padding-top: 0px;
  padding-right: 0px;
  padding-bottom: 0px;
  padding-left: 0px;
}

.sheet-host {
  position: fixed;
  left: 0px;
  right: 0px;
  bottom: 0px;
  display: flex;
  flex-direction: column;
  z-index: 131;
}

.sheet-backdrop {
  position: fixed;
  left: 0px;
  top: 0px;
  right: 0px;
  bottom: 0px;
  z-index: 129;
  background-color: rgba(15, 23, 42, 0.04);
}

.sheet-host-floating {
  position: absolute;
  left: 12px;
  right: auto;
  top: 58px;
  bottom: auto;
  z-index: 160;
  box-shadow: 0px 10px 24px rgba(25, 25, 25, 0.05);
  border-radius: 14px;
}

.sheet-host-mobile-web {
  left: 0px;
  right: 0px;
  bottom: 0px;
}

.toolbar-panel {
  overflow: hidden;
  background-color: rgba(255, 255, 255, 0.98);
  border-width: 1px;
  border-color: #d6dde4;
  padding-top: 14px;
  padding-right: 14px;
  padding-bottom: 12px;
  padding-left: 14px;
  /* box-shadow: 0px 10px 24px rgba(25, 25, 25, 0.12); */
}

.toolbar-panel-attached {
  border-top-left-radius: 0px;
  border-top-right-radius: 0px;
  border-top-width: 0px;
  /* box-shadow: 0px 10px 20px rgba(25, 25, 25, 0.08); */
}

.toolbar-shell {
  align-self: center;
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
  background-color: #fafafa;
  border-width: 0px;
  border-radius: 0px;
  min-height: 40px;
  padding-top: 0px;
  padding-right: 0px;
  padding-bottom: 0px;
  padding-left: 0px;
  box-shadow: none;
}

.toolbar-shell-compact {
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  padding-top: 6px;
  padding-bottom: 6px;
}

.toolbar-group {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: nowrap;
}

.toolbar-group-compact {
  justify-content: center;
  margin-top: 0px;
}

.toolbar-group-leading {
  min-width: 70px;
}

.toolbar-group-leading-compact {
  margin-top: 0px;
}

.toolbar-group-actions {
  margin-left: auto;
}

.toolbar-group-actions .toolbar-icon-button {
  margin-left: 2px;
  margin-right: 0px;
}

.toolbar-divider {
  width: 1px;
  height: 20px;
  background-color: #d8d8d8;
  margin-left: 3px;
  margin-right: 3px;
}

.toolbar-divider-hidden {
  display: none;
}

.toolbar-select {
  height: 28px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  border-width: 1px;
  border-color: transparent;
  border-radius: 4px;
  background-color: transparent;
  padding-left: 5px;
  padding-right: 5px;
  margin-right: 1px;
  transition-property: background-color, border-color;
  transition-duration: 160ms;
}

.toolbar-select-value {
  padding-left: 4px;
  padding-right: 4px;
}

.toolbar-select-active {
  background-color: #e7eaee;
  border-color: #d7dde5;
}

.toolbar-select-label {
  color: #50565f;
  font-size: 12px;
  line-height: 18px;
}

.toolbar-select-glyph {
  font-size: 16px;
  line-height: 16px;
}

.toolbar-size-label {
  font-size: 14px;
  line-height: 18px;
  /* padding: 0px 5px; */
}

.toolbar-select-font-label {
  font-size: 12px;
  line-height: 16px;
  /* padding: 0px 5px; */
}

.toolbar-select-mini-label {
  min-width: 10px;
  margin-left: 3px;
  font-size: 9px;
  line-height: 12px;
  text-align: center;
}

.toolbar-select-label-active {
  color: #2b3137;
}

.toolbar-shell-dense .toolbar-select {
  padding-left: 4px;
  padding-right: 4px;
}

.toolbar-shell-dense .toolbar-select-value,
.toolbar-shell-dense .toolbar-select-compact {
  min-width: 40px;
}

.toolbar-shell-dense .toolbar-icon-text {
  font-size: 16px;
  line-height: 16px;
}

.toolbar-shell-dense .toolbar-select-font-label,
.toolbar-shell-dense .toolbar-select-label {
  font-size: 11px;
}


.toolbar-select-arrow {
  min-width: 8px;
  color: #6b7280;
  font-size: 4px;
  line-height: 12px;
  text-align: center;
  margin-left: 2px;
  margin-top: 1px;
}

.toolbar-select-arrow.iconfont {
  font-size: 4px;
  line-height: 12px;
}

.toolbar-select-arrow-active {
  color: #475569;
}

.toolbar-icon-button {
  width: 28px;
  height: 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-width: 1px;
  border-color: transparent;
  border-radius: 4px;
  background-color: transparent;
  margin-right: 1px;
  transition-property: background-color, border-color;
  transition-duration: 160ms;
}

.toolbar-icon-button-disabled {
  opacity: 0.42;
}

.toolbar-color-button {
  height: 28px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  border-width: 1px;
  border-color: transparent;
  border-radius: 4px;
  background-color: transparent;
  margin-left: 1px;
  padding-left: 4px;
  padding-right: 4px;
  transition-property: background-color, border-color;
  transition-duration: 160ms;
}

.toolbar-color-button-active {
  border-color: #d7dde5;
  background-color: #e7eaee;
}

.toolbar-color-label {
  color: #334155;
  font-size: 14px;
  line-height: 18px;
  font-weight: 700;
}

.toolbar-color-label-active {
  color: #111827;
}

.toolbar-color-symbol {
  color: #50565f;
  font-size: 15px;
  line-height: 15px;
  font-weight: 600;
}

.toolbar-color-symbol-active {
  color: #111827;
}

.toolbar-color-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.toolbar-color-underline {
  width: 12px;
  height: 2px;
  border-radius: 2px;
  margin-top: -1px;
  margin-left: 1px;
}

.toolbar-color-ab {
  font-size: 13px;
}

.toolbar-color-arrow {
  min-width: 8px;
  color: #6b7280;
  font-size: 4px;
  line-height: 12px;
  text-align: center;
  margin-left: 0px;
  margin-top: 1px;
}

.toolbar-color-arrow.iconfont {
  font-size: 4px;
  line-height: 12px;
}

.toolbar-color-arrow-active {
  color: #475569;
}

.toolbar-icon-button-active {
  background-color: #e7eaee;
  border-color: #d7dde5;
}

.toolbar-icon-text {
  color: #50565f;
  font-size: 17px;
  line-height: 17px;
}

.toolbar-symbol-text {
  font-size: 14px;
  line-height: 16px;
}

.toolbar-symbol-text-small {
  font-size: 11px;
}

.link-modal-mask {
  position: fixed;
  left: 0px;
  top: 0px;
  right: 0px;
  bottom: 0px;
  z-index: 220;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba(15, 23, 42, 0.26);
  padding-left: 16px;
  padding-right: 16px;
}

.link-modal-mask-floating {
  background-color: rgba(15, 23, 42, 0.26);
}

.link-modal-mask-app {
  background-color: rgba(15, 23, 42, 0.38);
}

.link-modal-card {
  display: flex;
  flex-direction: column;
  background-color: #ffffff;
  border-radius: 14px;
  padding-top: 20px;
  padding-right: 18px;
  padding-bottom: 18px;
  padding-left: 18px;
}

.link-modal-card-floating {
  width: 100%;
  max-width: 420px;
  box-shadow: 0px 18px 36px rgba(15, 23, 42, 0.16);
}

.link-modal-card-app {
  width: 84%;
  border-width: 1px;
  border-color: #dbe4ee;
  background-color: #ffffff;
}

.link-modal-title-app {
  color: #111827;
  font-size: 20px;
  line-height: 28px;
  font-weight: 700;
}

.link-modal-subtitle-app {
  color: #64748b;
  font-size: 14px;
  line-height: 20px;
}

.link-modal-label-app {
  color: #0f172a;
  font-size: 15px;
  line-height: 22px;
  font-weight: 600;
}

.link-modal-header {
  display: flex;
  flex-direction: column;
}

.link-modal-title {
  color: #111827;
  font-size: 17px;
  line-height: 24px;
  font-weight: 600;
}

.link-modal-subtitle {
  margin-top: 4px;
  color: #6b7280;
  font-size: 13px;
  line-height: 18px;
}

.link-modal-form {
  display: flex;
  flex-direction: column;
  margin-top: 16px;
}

.link-modal-field {
  display: flex;
  flex-direction: column;
  margin-top: 12px;
}

.link-modal-label {
  color: #374151;
  font-size: 13px;
  line-height: 18px;
}

.link-modal-input {
  height: 40px;
  margin-top: 8px;
  border-width: 1px;
  border-color: #d1d5db;
  border-radius: 10px;
  padding-left: 12px;
  padding-right: 12px;
  background-color: #f8fafc;
  color: #111827;
  font-size: 14px;
}

.link-modal-input-app {
  border-color: #cbd5e1;
  background-color: #ffffff;
  color: #111827;
}

.link-modal-selection-chip {
  min-height: 40px;
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: 10px;
  padding-left: 12px;
  padding-right: 12px;
  background-color: #eef2ff;
}

.link-modal-selection-text {
  color: #1e3a8a;
  font-size: 14px;
  line-height: 20px;
}

.link-modal-selection-chip-app {
  border-width: 1px;
  border-color: #c7d2fe;
  background-color: #eef2ff;
}

.link-modal-selection-text-app {
  color: #1d4ed8;
}

.link-modal-actions {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  margin-top: 20px;
}

.link-modal-actions-app {
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
}

.link-modal-danger {
  min-height: 36px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding-left: 10px;
  padding-right: 10px;
}

.link-modal-danger-app {
  width: 100%;
  min-width: 0px;
  margin-bottom: 12px;
  border-width: 1px;
  border-color: #fecaca;
  border-radius: 10px;
  background-color: #fff5f5;
}

.link-modal-danger-text {
  color: #dc2626;
  font-size: 13px;
  line-height: 18px;
  font-weight: 600;
}

.link-modal-danger-text-app {
  color: #dc2626;
}

.link-modal-action-group {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;
}

.link-modal-action-group-app {
  flex: 1;
  width: 100%;
  justify-content: flex-end;
}

.link-modal-button {
  min-width: 74px;
  min-height: 36px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  padding-left: 14px;
  padding-right: 14px;
}

.link-modal-button-secondary {
  background-color: #eef2f7;
}

.link-modal-button-primary {
  margin-left: 10px;
  background-color: #2563eb;
}

.link-modal-button-secondary-app {
  flex: 1;
  border-width: 1px;
  border-color: #d6dde8;
  background-color: #f8fafc;
}

.link-modal-button-primary-app {
  flex: 1;
  background-color: #1d4ed8;
}

.link-modal-button-text {
  color: #ffffff;
  font-size: 14px;
  line-height: 20px;
  font-weight: 600;
}

.link-modal-button-text-app {
  color: #ffffff;
}

.link-modal-button-text-secondary {
  color: #334155;
}

.link-modal-button-text-secondary-app {
  color: #334155;
}

.toolbar-font-action-italic {
  font-style: italic;
}

.toolbar-font-action-underline {
  text-decoration-line: underline;
}

.toolbar-font-action-strike {
  text-decoration-line: line-through;
}

.toolbar-icon-text-active {
  color: #2b3137;
}

.relative-top-1 {
  position: relative;
  top: 1px;
}
.desktop-toolbar-mask {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    background: rgba(255, 255, 255, 0.5);
}
/* #ifdef WEB */
.toolbar-tooltip {
  position: fixed;
  z-index: 131;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  transform: translateX(-50%);
  overflow: visible;
}

.toolbar-tooltip-bubble {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba(15, 23, 42, 0.92);
  border-radius: 4px;
  padding-top: 5px;
  padding-right: 8px;
  padding-bottom: 5px;
  padding-left: 8px;
  box-shadow: 0px 8px 20px rgba(15, 23, 42, 0.18);
}

.toolbar-tooltip-text {
  color: #ffffff;
  font-size: 11px;
  line-height: 14px;
}

.toolbar-tooltip-arrow {
  width: 8px;
  height: 8px;
  margin-bottom: -4px;
  background-color: rgba(15, 23, 42, 0.92);
  transform: rotate(45deg);
  border-radius: 1px;
}

.toolbar-select:hover,
.toolbar-icon-button:hover,
.toolbar-color-button:hover {
  background-color: #f3f4f6;
  border-color: #e4e7eb;
}
/* #endif */

.toolbar-panel-floating {
  position: relative;
  height: auto;
  border-width: 1px;
  border-color: #cfd6dd;
  border-radius: 8px;
  padding-top: 8px;
  padding-right: 8px;
  padding-bottom: 8px;
  padding-left: 8px;
  background-color: #ffffff;
  box-shadow: 0px 10px 24px rgba(15, 23, 42, 0.08);
  transition-property: transform, opacity;
  transition-duration: 180ms;
  opacity: 0;
  transform: translateY(-8px);
}

.toolbar-panel-section .desktop-color-sheet-label {
  display: none;
}

.toolbar-panel-floating-attached {
  border-top-width: 1px;
}

.toolbar-panel-floating-entered {
  opacity: 1;
  transform: translateY(0px);
}

.toolbar-panel-mobile-web {
  border-top-left-radius: 18px;
  border-top-right-radius: 18px;
  border-bottom-left-radius: 0px;
  border-bottom-right-radius: 0px;
  border-top-width: 1px;
  padding-top: 16px;
  padding-right: 12px;
  padding-bottom: 16px;
  padding-left: 12px;
  box-shadow: 0px -10px 30px rgba(15, 23, 42, 0.12);
}

.toolbar-panel-arrow {
  position: absolute;
  top: -7px;
  width: 14px;
  height: 14px;
  background-color: #ffffff;
  border-width: 0px;
  box-shadow: none;
  transform: rotate(45deg);
}

.toolbar-panel-header-floating {
  margin-bottom: 6px;
}

.toolbar-panel-header-mobile-web {
  margin-bottom: 10px;
}

.toolbar-panel-title-floating {
  font-size: 12px;
}

.toolbar-panel-title-mobile-web {
  font-size: 15px;
}

.toolbar-panel-subtitle-floating {
  font-size: 10px;
  margin-top: 2px;
}

.toolbar-panel-subtitle-mobile-web {
  font-size: 11px;
  margin-top: 4px;
}

.toolbar-panel-scroll-floating {
  flex: 0;
}

.toolbar-panel-scroll-mobile-web {
  flex: 0;
  /* max-height: 62vh; */
}

.toolbar-chip-row-panel {
  margin-left: 0px;
  margin-right: 0px;
}

.toolbar-chip-row-panel-compact {
  margin-left: 0px;
  margin-right: 0px;
}

.toolbar-panel-section .desktop-color-sheet-label {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-bottom: 12px;
}

.desktop-color-sheet-icon {
  width: 14px;
  height: 14px;
  border-width: 1px;
  border-color: #6b7280;
  border-radius: 2px;
  margin-right: 10px;
  transform: rotate(45deg);
}

.desktop-color-sheet-label-text {
  color: #374151;
  font-size: 12px;
  line-height: 18px;
}

.desktop-menu-item {
  width: 100%;
  min-width: 0px;
  min-height: 0px;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  border-width: 1px;
  border-color: transparent;
  border-radius: 6px;
  background-color: #ffffff;
  padding-top: 8px;
  padding-right: 10px;
  padding-bottom: 8px;
  padding-left: 10px;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 4px;
}

.desktop-menu-item-active {
  background-color: #eef3f8;
  border-color: #ccd6e0;
}

.desktop-menu-badge {
  min-width: 24px;
  height: 24px;
  border-radius: 4px;
  background-color: #f1f5f9;
  color: #475569;
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  line-height: 24px;
  margin-right: 10px;
}

.desktop-menu-badge-active {
  background-color: #dbe7f3;
  color: #0f172a;
}

.desktop-menu-text {
  color: #1f2937;
  font-size: 13px;
  text-align: left;
  line-height: 18px;
  letter-spacing: 0px;
}

.desktop-menu-text-active {
  color: #0f172a;
}

.toolbar-panel-header {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.toolbar-panel-title-wrap {
  display: flex;
  flex-direction: column;
  flex: 1;
}

.toolbar-panel-title {
  color: #0f172a;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.4px;
}

.toolbar-panel-subtitle {
  color: #64748b;
  font-size: 11px;
  margin-top: 4px;
  line-height: 16px;
}

.toolbar-panel-close {
  width: 28px;
  height: 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 14px;
  background-color: #f3f1ec;
}

.toolbar-panel-close-icon {
  width: 14px;
  height: 14px;
}

.toolbar-panel-section {
  display: flex;
  flex-direction: column;
}

.toolbar-panel-scroll {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.toolbar-panel-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 6px;
}

.toolbar-chip-row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-start;
  margin-left: -4px;
  margin-right: -4px;
}

.toolbar-chip-cell {
  width: 25%;
  padding-left: 4px;
  padding-right: 4px;
  margin-bottom: 8px;
}

.toolbar-chip {
  min-width: 72px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border-radius: 14px;
  background-color: #f3f6f9;
  border-width: 1px;
  border-color: #d6dde4;
  padding-top: 10px;
  padding-right: 12px;
  padding-bottom: 10px;
  padding-left: 12px;
  margin-left: 4px;
  margin-right: 4px;
  margin-bottom: 8px;
}

.toolbar-chip-grid {
  width: 100%;
  min-width: 0px;
  min-height: 72px;
  justify-content: flex-start;
  margin-left: 0px;
  margin-right: 0px;
  margin-bottom: 0px;
  padding-top: 12px;
  padding-right: 8px;
  padding-bottom: 10px;
  padding-left: 8px;
}

.toolbar-color-section {
  display: flex;
  flex-direction: column;
  margin-top: 4px;
}

.toolbar-color-title {
  color: #64748b;
  font-size: 12px;
  margin-bottom: 8px;
  letter-spacing: 0.3px;
}

.toolbar-chip-cell {
  padding-left: 3px;
  padding-right: 3px;
  margin-bottom: 6px;
}

.toolbar-chip-cell-quarter {
  width: 25%;
}

.toolbar-chip-cell-full {
  width: 100%;
}

.toolbar-chip-cell-third {
  width: 33.33%;
}

.toolbar-chip-cell-half {
  width: 50%;
  padding-left: 2px;
  padding-right: 2px;
}

.color-row-panel {
  width: 340px;
  align-self: center;
  margin-left: 0px;
  margin-right: 0px;
}

.toolbar-color-title {
  font-size: 11px;
  margin-bottom: 6px;
}

.color-row {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  margin-left: -4px;
  margin-right: -4px;
}

.color-swatch {
  width: 48px;
  height: 36px;
  border-radius: 10px;
  border-width: 2px;
  border-style: solid;
  border-color: rgba(15, 23, 42, 0.08);
  margin-left: 4px;
  margin-right: 4px;
  margin-bottom: 10px;
}

.color-swatch.color-swatch-panel {
  width: 28px;
  height: 28px;
  border-radius: 4px;
  border-width: 1px;
  margin-left: 3px;
  margin-right: 3px;
  margin-bottom: 10px;
}

.color-swatch-active {
  border-color: #111827;
  border-width: 3px;
  /* box-shadow: 0px 0px 0px 2px rgba(31, 31, 31, 0.12); */
}

.color-swatch-clear {
  background-color: #ffffff;
  border-color: #cbd5e1;
}
</style>

# 参见

# tips

  • 虽然editor组件被分类到form组件,但并不能在form的submit中提交