Editor.vue 9.3 KB
Newer Older
J
jq 已提交
1
<template>
V
vben 已提交
2 3
  <div :class="prefixCls" :style="{ width: containerWidth }">
    <ImgUpload
4
      :fullscreen="fullscreen"
V
vben 已提交
5 6 7 8
      @uploading="handleImageUploading"
      @done="handleDone"
      v-if="showImageUpload"
      v-show="editorRef"
9
      :disabled="disabled"
V
vben 已提交
10
    />
无木 已提交
11 12 13 14 15 16 17
    <textarea
      :id="tinymceId"
      ref="elRef"
      :style="{ visibility: 'hidden' }"
      v-if="!initOptions.inline"
    ></textarea>
    <slot v-else></slot>
J
jq 已提交
18 19 20 21
  </div>
</template>

<script lang="ts">
22
  import type { Editor, RawEditorSettings } from 'tinymce';
V
Vben 已提交
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
  import tinymce from 'tinymce/tinymce';
  import 'tinymce/themes/silver';
  import 'tinymce/icons/default/icons';
  import 'tinymce/plugins/advlist';
  import 'tinymce/plugins/anchor';
  import 'tinymce/plugins/autolink';
  import 'tinymce/plugins/autosave';
  import 'tinymce/plugins/code';
  import 'tinymce/plugins/codesample';
  import 'tinymce/plugins/directionality';
  import 'tinymce/plugins/fullscreen';
  import 'tinymce/plugins/hr';
  import 'tinymce/plugins/insertdatetime';
  import 'tinymce/plugins/link';
  import 'tinymce/plugins/lists';
  import 'tinymce/plugins/media';
  import 'tinymce/plugins/nonbreaking';
  import 'tinymce/plugins/noneditable';
  import 'tinymce/plugins/pagebreak';
  import 'tinymce/plugins/paste';
  import 'tinymce/plugins/preview';
  import 'tinymce/plugins/print';
  import 'tinymce/plugins/save';
  import 'tinymce/plugins/searchreplace';
  import 'tinymce/plugins/spellchecker';
  import 'tinymce/plugins/tabfocus';
  // import 'tinymce/plugins/table';
  import 'tinymce/plugins/template';
  import 'tinymce/plugins/textpattern';
  import 'tinymce/plugins/visualblocks';
  import 'tinymce/plugins/visualchars';
  import 'tinymce/plugins/wordcount';

V
vben 已提交
56 57 58 59 60 61 62 63
  import {
    defineComponent,
    computed,
    nextTick,
    ref,
    unref,
    watch,
    onDeactivated,
64
    onBeforeUnmount,
V
vben 已提交
65
    PropType,
V
vben 已提交
66
  } from 'vue';
V
Vben 已提交
67
  import ImgUpload from './ImgUpload.vue';
V
Vben 已提交
68
  import { toolbar, plugins } from './tinymce';
V
Vben 已提交
69
  import { buildShortUUID } from '/@/utils/uuid';
V
vben 已提交
70
  import { bindHandlers } from './helper';
V
vben 已提交
71
  import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
V
vben 已提交
72
  import { useDesign } from '/@/hooks/web/useDesign';
73
  import { isNumber } from '/@/utils/is';
74 75
  import { useLocale } from '/@/locales/useLocale';
  import { useAppStore } from '/@/store/modules/app';
J
jq 已提交
76

77 78
  const tinymceProps = {
    options: {
79
      type: Object as PropType<Partial<RawEditorSettings>>,
80
      default: () => ({}),
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    },
    value: {
      type: String,
    },

    toolbar: {
      type: Array as PropType<string[]>,
      default: toolbar,
    },
    plugins: {
      type: Array as PropType<string[]>,
      default: plugins,
    },
    modelValue: {
      type: String,
    },
    height: {
      type: [Number, String] as PropType<string | number>,
      required: false,
      default: 400,
    },
    width: {
      type: [Number, String] as PropType<string | number>,
      required: false,
      default: 'auto',
    },
    showImageUpload: {
      type: Boolean,
      default: true,
    },
  };
J
jq 已提交
112 113 114

  export default defineComponent({
    name: 'Tinymce',
V
vben 已提交
115
    components: { ImgUpload },
V
vben 已提交
116
    inheritAttrs: false,
117
    props: tinymceProps,
118
    emits: ['change', 'update:modelValue', 'inited', 'init-error'],
V
vben 已提交
119
    setup(props, { emit, attrs }) {
V
vben 已提交
120
      const editorRef = ref<Editor | null>(null);
121
      const fullscreen = ref(false);
V
Vben 已提交
122
      const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
V
vben 已提交
123
      const elRef = ref<HTMLElement | null>(null);
V
vben 已提交
124

V
vben 已提交
125 126
      const { prefixCls } = useDesign('tinymce-container');

127 128
      const appStore = useAppStore();

V
Vben 已提交
129
      const tinymceContent = computed(() => props.modelValue);
V
vben 已提交
130

J
jq 已提交
131 132
      const containerWidth = computed(() => {
        const width = props.width;
133
        if (isNumber(width)) {
J
jq 已提交
134 135 136 137
          return `${width}px`;
        }
        return width;
      });
V
vben 已提交
138

139 140 141 142 143 144 145 146 147
      const skinName = computed(() => {
        return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
      });

      const langName = computed(() => {
        const lang = useLocale().getLocale.value;
        return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
      });

148
      const initOptions = computed((): RawEditorSettings => {
149
        const { height, options, toolbar, plugins } = props;
150
        const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
J
jq 已提交
151
        return {
V
vben 已提交
152
          selector: `#${unref(tinymceId)}`,
153 154
          height,
          toolbar,
V
vben 已提交
155
          menubar: 'file edit insert view format table',
156
          plugins,
157 158
          language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
          language: langName.value,
159
          branding: false,
V
vben 已提交
160 161 162
          default_link_target: '_blank',
          link_title: false,
          object_resizing: false,
163
          auto_focus: true,
164 165 166 167
          skin: skinName.value,
          skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
          content_css:
            publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
V
vben 已提交
168
          ...options,
169
          setup: (editor: Editor) => {
V
vben 已提交
170
            editorRef.value = editor;
171
            editor.on('init', (e) => initSetup(e));
V
vben 已提交
172
          },
J
jq 已提交
173 174
        };
      });
V
vben 已提交
175

176 177 178
      const disabled = computed(() => {
        const { options } = props;
        const getdDisabled = options && Reflect.get(options, 'readonly');
179 180 181 182
        const editor = unref(editorRef);
        if (editor) {
          editor.setMode(getdDisabled ? 'readonly' : 'design');
        }
183 184 185
        return getdDisabled ?? false;
      });

V
vben 已提交
186 187 188 189
      watch(
        () => attrs.disabled,
        () => {
          const editor = unref(editorRef);
V
Vben 已提交
190 191 192
          if (!editor) {
            return;
          }
V
vben 已提交
193
          editor.setMode(attrs.disabled ? 'readonly' : 'design');
V
vben 已提交
194
        },
V
vben 已提交
195
      );
196

V
vben 已提交
197
      onMountedOrActivated(() => {
198
        if (!initOptions.value.inline) {
无木 已提交
199 200
          tinymceId.value = buildShortUUID('tiny-vue');
        }
V
vben 已提交
201
        nextTick(() => {
202 203 204
          setTimeout(() => {
            initEditor();
          }, 30);
V
vben 已提交
205 206 207
        });
      });

208
      onBeforeUnmount(() => {
V
vben 已提交
209 210 211 212 213 214 215 216
        destory();
      });

      onDeactivated(() => {
        destory();
      });

      function destory() {
217
        if (tinymce !== null) {
218
          tinymce?.remove?.(unref(initOptions).selector!);
V
vben 已提交
219 220 221 222
        }
      }

      function initEditor() {
223 224 225 226
        const el = unref(elRef);
        if (el) {
          el.style.visibility = '';
        }
227 228 229 230 231 232 233 234
        tinymce
          .init(unref(initOptions))
          .then((editor) => {
            emit('inited', editor);
          })
          .catch((err) => {
            emit('init-error', err);
          });
V
vben 已提交
235 236
      }

237
      function initSetup(e) {
V
vben 已提交
238
        const editor = unref(editorRef);
V
Vben 已提交
239 240 241
        if (!editor) {
          return;
        }
V
vben 已提交
242 243 244 245 246 247 248
        const value = props.modelValue || '';

        editor.setContent(value);
        bindModelHandlers(editor);
        bindHandlers(e, attrs, unref(editorRef));
      }

V
vben 已提交
249
      function setValue(editor: Record<string, any>, val: string, prevVal?: string) {
V
vben 已提交
250 251 252 253 254 255 256 257 258 259
        if (
          editor &&
          typeof val === 'string' &&
          val !== prevVal &&
          val !== editor.getContent({ format: attrs.outputFormat })
        ) {
          editor.setContent(val);
        }
      }

V
vben 已提交
260 261 262
      function bindModelHandlers(editor: any) {
        const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
        const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
263

V
vben 已提交
264 265 266
        watch(
          () => props.modelValue,
          (val: string, prevVal: string) => {
V
vben 已提交
267
            setValue(editor, val, prevVal);
V
vben 已提交
268
          },
V
vben 已提交
269 270 271 272 273 274 275 276 277
        );

        watch(
          () => props.value,
          (val: string, prevVal: string) => {
            setValue(editor, val, prevVal);
          },
          {
            immediate: true,
V
vben 已提交
278
          },
V
vben 已提交
279 280 281
        );

        editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
V
vben 已提交
282 283 284
          const content = editor.getContent({ format: attrs.outputFormat });
          emit('update:modelValue', content);
          emit('change', content);
V
vben 已提交
285
        });
286 287 288 289

        editor.on('FullscreenStateChanged', (e) => {
          fullscreen.value = e.state;
        });
V
vben 已提交
290 291
      }

V
vben 已提交
292 293
      function handleImageUploading(name: string) {
        const editor = unref(editorRef);
V
Vben 已提交
294 295 296
        if (!editor) {
          return;
        }
297
        editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
V
vben 已提交
298
        const content = editor?.getContent() ?? '';
299
        setValue(editor, content);
V
vben 已提交
300 301 302 303
      }

      function handleDone(name: string, url: string) {
        const editor = unref(editorRef);
V
Vben 已提交
304 305 306
        if (!editor) {
          return;
        }
V
vben 已提交
307
        const content = editor?.getContent() ?? '';
V
Vben 已提交
308
        const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
V
vben 已提交
309 310 311
        setValue(editor, val);
      }

V
Vben 已提交
312
      function getUploadingImgName(name: string) {
V
vben 已提交
313 314 315
        return `[uploading:${name}]`;
      }

V
vben 已提交
316
      return {
V
vben 已提交
317
        prefixCls,
V
vben 已提交
318 319 320 321 322
        containerWidth,
        initOptions,
        tinymceContent,
        elRef,
        tinymceId,
V
vben 已提交
323 324 325
        handleImageUploading,
        handleDone,
        editorRef,
326
        fullscreen,
327
        disabled,
V
vben 已提交
328
      };
J
jq 已提交
329 330 331 332
    },
  });
</script>

V
vben 已提交
333
<style lang="less" scoped></style>
J
jq 已提交
334

V
vben 已提交
335 336
<style lang="less">
  @prefix-cls: ~'@{namespace}-tinymce-container';
J
jq 已提交
337

V
vben 已提交
338 339 340
  .@{prefix-cls} {
    position: relative;
    line-height: normal;
J
jq 已提交
341

V
vben 已提交
342 343
    textarea {
      visibility: hidden;
V
vben 已提交
344
      z-index: -1;
J
jq 已提交
345 346 347
    }
  }
</style>