Skip to content

Commit

Permalink
feat(tinymce): add rich editor
Browse files Browse the repository at this point in the history
  • Loading branch information
jq002 committed Oct 22, 2020
1 parent d8b25b4 commit c0e4c9e
Show file tree
Hide file tree
Showing 19 changed files with 1,047 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@

**/*.svg
**/*.sh

/public/*
2 changes: 2 additions & 0 deletions .stylelintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/dist/*
/public/*
389 changes: 389 additions & 0 deletions public/resource/tinymce/langs/zh_CN.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/components/Tinymce/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Tinymce } from './src/Editor.vue';
90 changes: 90 additions & 0 deletions src/components/Tinymce/src/Editor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<div class="tinymce-container" :style="{ width: containerWidth }">
<tinymce-editor
:id="id"
:init="initOptions"
:modelValue="tinymceContent"
@update:modelValue="handleChange"
:tinymceScriptSrc="tinymceScriptSrc"
></tinymce-editor>
</div>
</template>

<script lang="ts">
import TinymceEditor from './lib'; // TinyMCE vue wrapper
import { defineComponent, computed } from 'vue';
import { basicProps } from './props';
import toolbar from './toolbar';
import plugins from './plugins';
const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
const tinymceScriptSrc = `${CDN_URL}/tinymce.min.js`;
export default defineComponent({
name: 'Tinymce',
components: { TinymceEditor },
props: basicProps,
setup(props, { emit }) {
const tinymceContent = computed(() => {
return props.value;
});
function handleChange(value: string) {
emit('change', value);
}
const containerWidth = computed(() => {
const width = props.width;
// Test matches `100`, `'100'`
if (/^[\d]+(\.[\d]+)?$/.test(width.toString())) {
return `${width}px`;
}
return width;
});
const initOptions = computed(() => {
const { id, height, menubar } = props;
return {
selector: `#${id}`,
height: height,
toolbar: toolbar,
menubar: menubar,
plugins: plugins,
// 语言包
language_url: 'resource/tinymce/langs/zh_CN.js',
// 中文
language: 'zh_CN',
};
});
return { containerWidth, initOptions, tinymceContent, handleChange, tinymceScriptSrc };
},
});
</script>

<style lang="less" scoped>
.tinymce-container {
position: relative;
line-height: normal;
.mce-fullscreen {
z-index: 10000;
}
}
.editor-custom-btn-container {
position: absolute;
top: 6px;
right: 6px;
&.fullscreen {
position: fixed;
z-index: 10000;
}
}
.editor-upload-btn {
display: inline-block;
}
textarea {
z-index: -1;
visibility: hidden;
}
</style>
72 changes: 72 additions & 0 deletions src/components/Tinymce/src/lib/ScriptLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { uuid } from './Utils';

export type callbackFn = () => void;
export interface IStateObj {
listeners: callbackFn[];
scriptId: string;
scriptLoaded: boolean;
}

const createState = (): IStateObj => {
return {
listeners: [],
scriptId: uuid('tiny-script'),
scriptLoaded: false
};
};

interface ScriptLoader {
load: (doc: Document, url: string, callback: callbackFn) => void;
reinitialize: () => void;
}

const CreateScriptLoader = (): ScriptLoader => {
let state: IStateObj = createState();

const injectScriptTag = (scriptId: string, doc: Document, url: string, callback: callbackFn) => {
const scriptTag = doc.createElement('script');
scriptTag.referrerPolicy = 'origin';
scriptTag.type = 'application/javascript';
scriptTag.id = scriptId;
scriptTag.src = url;

const handler = () => {
scriptTag.removeEventListener('load', handler);
callback();
};
scriptTag.addEventListener('load', handler);
if (doc.head) {
doc.head.appendChild(scriptTag);
}
};

const load = (doc: Document, url: string, callback: callbackFn) => {
if (state.scriptLoaded) {
callback();
} else {
state.listeners.push(callback);
if (!doc.getElementById(state.scriptId)) {
injectScriptTag(state.scriptId, doc, url, () => {
state.listeners.forEach((fn) => fn());
state.scriptLoaded = true;
});
}
}
};

// Only to be used by tests.
const reinitialize = () => {
state = createState();
};

return {
load,
reinitialize
};
};

const ScriptLoader = CreateScriptLoader();

export {
ScriptLoader
};
9 changes: 9 additions & 0 deletions src/components/Tinymce/src/lib/TinyMCE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const getGlobal = (): any => (typeof window !== 'undefined' ? window : global);

const getTinymce = () => {
const global = getGlobal();

return global && global.tinymce ? global.tinymce : null;
};

export { getTinymce };
151 changes: 151 additions & 0 deletions src/components/Tinymce/src/lib/Utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { ComponentPublicInstance } from 'vue';

const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid'
];

const isValidKey = (key: string) => validEvents.indexOf(key) !== -1;

const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => {
Object.keys(listeners)
.filter(isValidKey)
.forEach((key: string) => {
const handler = listeners[key];
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor);
} else {
editor.on(key.substring(2), (e: any) => handler(e, editor));
}
}
});
};

const bindModelHandlers = (ctx: ComponentPublicInstance, editor: any) => {
const modelEvents = ctx.$props.modelEvents ? ctx.$props.modelEvents : null;
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
// @ts-ignore
ctx.$watch('modelValue', (val: string, prevVal: string) => {
if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: ctx.$props.outputFormat })) {
editor.setContent(val);
}
});

editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
ctx.$emit('update:modelValue', editor.getContent({ format: ctx.$props.outputFormat }));
});
};

const initEditor = (initEvent: Event, ctx: ComponentPublicInstance, editor: any) => {
const value = ctx.$props.modelValue ? ctx.$props.modelValue : '';
const initialValue = ctx.$props.initialValue ? ctx.$props.initialValue : '';

editor.setContent(value || initialValue);

// checks if the v-model shorthand is used (which sets an v-on:input listener) and then binds either
// specified the events or defaults to "change keyup" event and emits the editor content on that event
if (ctx.$attrs['onUpdate:modelValue']) {
bindModelHandlers(ctx, editor);
}

bindHandlers(initEvent, ctx.$attrs, editor);
};

let unique = 0;

const uuid = (prefix: string): string => {
const time = Date.now();
const random = Math.floor(Math.random() * 1000000000);

unique++;

return prefix + '_' + random + unique + String(time);
};

const isTextarea = (element: Element | null): element is HTMLTextAreaElement => {
return element !== null && element.tagName.toLowerCase() === 'textarea';
};

const normalizePluginArray = (plugins?: string | string[]): string[] => {
if (typeof plugins === 'undefined' || plugins === '') {
return [];
}

return Array.isArray(plugins) ? plugins : plugins.split(' ');
};

const mergePlugins = (initPlugins: string | string[], inputPlugins?: string | string[]) =>
normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins));

const isNullOrUndefined = (value: any): value is null | undefined => value === null || value === undefined;

export {
bindHandlers,
bindModelHandlers,
initEditor,
uuid,
isTextarea,
mergePlugins,
isNullOrUndefined
};
Loading

0 comments on commit c0e4c9e

Please sign in to comment.