26 changed files with 349 additions and 534 deletions
@ -1,9 +1,6 @@ |
|||||
const getGlobal = (): any => (typeof window !== 'undefined' ? window : global); |
const getGlobal = (): any => (typeof window !== 'undefined' ? window : global); |
||||
|
|
||||
const getTinymce = () => { |
export const getTinymce = () => { |
||||
const global = getGlobal(); |
const global = getGlobal(); |
||||
|
|
||||
return global && global.tinymce ? global.tinymce : null; |
return global && global.tinymce ? global.tinymce : null; |
||||
}; |
}; |
||||
|
|
||||
export { getTinymce }; |
|
||||
@ -0,0 +1,81 @@ |
|||||
|
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; |
||||
|
|
||||
|
export 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)); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}; |
||||
@ -1,72 +0,0 @@ |
|||||
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 |
|
||||
}; |
|
||||
@ -1,151 +0,0 @@ |
|||||
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 |
|
||||
}; |
|
||||
@ -1,111 +0,0 @@ |
|||||
/** |
|
||||
* Copyright (c) 2018-present, Ephox, Inc. |
|
||||
* |
|
||||
* This source code is licensed under the Apache 2 license found in the |
|
||||
* LICENSE file in the root directory of this source tree. |
|
||||
* |
|
||||
*/ |
|
||||
|
|
||||
// import { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options';
|
|
||||
// import { CreateElement, Vue } from 'vue/types/vue';
|
|
||||
|
|
||||
import { ScriptLoader } from '../ScriptLoader'; |
|
||||
import { getTinymce } from '../TinyMCE'; |
|
||||
import { initEditor, isTextarea, mergePlugins, uuid, isNullOrUndefined } from '../Utils'; |
|
||||
import { editorProps, IPropTypes } from './EditorPropTypes'; |
|
||||
import { h, defineComponent, ComponentPublicInstance } from 'vue' |
|
||||
|
|
||||
|
|
||||
export interface IEditor { |
|
||||
$props: Partial<IPropTypes> |
|
||||
} |
|
||||
|
|
||||
declare module '@vue/runtime-core' { |
|
||||
interface ComponentCustomProperties { |
|
||||
elementId: string; |
|
||||
element: Element | null; |
|
||||
editor: any; |
|
||||
inlineEditor: boolean; |
|
||||
$props: Partial<IPropTypes>; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
const renderInline = (id: string, tagName?: string) => { |
|
||||
return h(tagName ? tagName : 'div', { |
|
||||
id |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
const renderIframe = (id: string) => { |
|
||||
return h('textarea', { |
|
||||
id, |
|
||||
visibility: 'hidden' |
|
||||
}); |
|
||||
}; |
|
||||
|
|
||||
const initialise = (ctx: ComponentPublicInstance) => () => { |
|
||||
const finalInit = { |
|
||||
...ctx.$props.init, |
|
||||
readonly: ctx.$props.disabled, |
|
||||
selector: `#${ctx.elementId}`, |
|
||||
plugins: mergePlugins(ctx.$props.init && ctx.$props.init.plugins, ctx.$props.plugins), |
|
||||
toolbar: ctx.$props.toolbar || (ctx.$props.init && ctx.$props.init.toolbar), |
|
||||
inline: ctx.inlineEditor, |
|
||||
setup: (editor: any) => { |
|
||||
ctx.editor = editor; |
|
||||
editor.on('init', (e: Event) => initEditor(e, ctx, editor)); |
|
||||
|
|
||||
if (ctx.$props.init && typeof ctx.$props.init.setup === 'function') { |
|
||||
ctx.$props.init.setup(editor); |
|
||||
} |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
if (isTextarea(ctx.element)) { |
|
||||
ctx.element.style.visibility = ''; |
|
||||
} |
|
||||
|
|
||||
getTinymce().init(finalInit); |
|
||||
}; |
|
||||
|
|
||||
export const Editor = defineComponent({ |
|
||||
props: editorProps, |
|
||||
created() { |
|
||||
this.elementId = this.$props.id || uuid('tiny-vue'); |
|
||||
this.inlineEditor = (this.$props.init && this.$props.init.inline) || this.$props.inline; |
|
||||
}, |
|
||||
watch: { |
|
||||
disabled() { |
|
||||
(this as any).editor.setMode(this.disabled ? 'readonly' : 'design'); |
|
||||
} |
|
||||
}, |
|
||||
mounted() { |
|
||||
this.element = this.$el; |
|
||||
|
|
||||
if (getTinymce() !== null) { |
|
||||
initialise(this)(); |
|
||||
} else if (this.element && this.element.ownerDocument) { |
|
||||
const channel = this.$props.cloudChannel ? this.$props.cloudChannel : '5'; |
|
||||
const apiKey = this.$props.apiKey ? this.$props.apiKey : 'no-api-key'; |
|
||||
|
|
||||
const scriptSrc = isNullOrUndefined(this.$props.tinymceScriptSrc) ? |
|
||||
`https://cdn.tiny.cloud/1/${apiKey}/tinymce/${channel}/tinymce.min.js` : |
|
||||
this.$props.tinymceScriptSrc; |
|
||||
|
|
||||
ScriptLoader.load( |
|
||||
this.element.ownerDocument, |
|
||||
scriptSrc, |
|
||||
initialise(this) |
|
||||
); |
|
||||
} |
|
||||
}, |
|
||||
beforeUnmount() { |
|
||||
if (getTinymce() !== null) { |
|
||||
getTinymce().remove(this.editor); |
|
||||
} |
|
||||
}, |
|
||||
render() { |
|
||||
return this.inlineEditor ? renderInline(this.elementId, this.$props.tagName) : renderIframe(this.elementId); |
|
||||
} |
|
||||
}) |
|
||||
@ -1,46 +0,0 @@ |
|||||
/** |
|
||||
* Copyright (c) 2018-present, Ephox, Inc. |
|
||||
* |
|
||||
* This source code is licensed under the Apache 2 license found in the |
|
||||
* LICENSE file in the root directory of this source tree. |
|
||||
* |
|
||||
*/ |
|
||||
|
|
||||
export type CopyProps<T> = { [P in keyof T]: any }; |
|
||||
|
|
||||
export interface IPropTypes { |
|
||||
apiKey: string; |
|
||||
cloudChannel: string; |
|
||||
id: string; |
|
||||
init: any; |
|
||||
initialValue: string; |
|
||||
outputFormat: 'html' | 'text'; |
|
||||
inline: boolean; |
|
||||
modelEvents: string[] | string; |
|
||||
plugins: string[] | string; |
|
||||
tagName: string; |
|
||||
toolbar: string[] | string; |
|
||||
modelValue: string; |
|
||||
disabled: boolean; |
|
||||
tinymceScriptSrc: string; |
|
||||
} |
|
||||
|
|
||||
export const editorProps: CopyProps<IPropTypes> = { |
|
||||
apiKey: String, |
|
||||
cloudChannel: String, |
|
||||
id: String, |
|
||||
init: Object, |
|
||||
initialValue: String, |
|
||||
inline: Boolean, |
|
||||
modelEvents: [String, Array], |
|
||||
plugins: [String, Array], |
|
||||
tagName: String, |
|
||||
toolbar: [String, Array], |
|
||||
modelValue: String, |
|
||||
disabled: Boolean, |
|
||||
tinymceScriptSrc: String, |
|
||||
outputFormat: { |
|
||||
type: String, |
|
||||
validator: (prop: string) => prop === 'html' || prop === 'text' |
|
||||
}, |
|
||||
}; |
|
||||
@ -1,4 +0,0 @@ |
|||||
// Global compile-time constants
|
|
||||
declare var __DEV__: boolean |
|
||||
declare var __BROWSER__: boolean |
|
||||
declare var __CI__: boolean |
|
||||
@ -1,3 +0,0 @@ |
|||||
import { Editor } from './components/Editor'; |
|
||||
|
|
||||
export default Editor; |
|
||||
@ -1,19 +1,24 @@ |
|||||
<template> |
<template> |
||||
<div class="flex p-4"> |
<div class="flex p-4"> |
||||
<Tinymce value="Hello, World!" @change="handleChange" width="100%" /> |
{{ value }} |
||||
|
<Tinymce v-model="value" @change="handleChange" width="100%" /> |
||||
</div> |
</div> |
||||
</template> |
</template> |
||||
<script lang="ts"> |
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
import { defineComponent, ref } from 'vue'; |
||||
import { Tinymce } from '/@/components/Tinymce/index'; |
import { Tinymce } from '/@/components/Tinymce/index'; |
||||
|
|
||||
export default defineComponent({ |
export default defineComponent({ |
||||
components: { Tinymce }, |
components: { Tinymce }, |
||||
setup() { |
setup() { |
||||
|
const value = ref('hello world!'); |
||||
function handleChange(value: string) { |
function handleChange(value: string) { |
||||
console.log(value); |
console.log(value); |
||||
} |
} |
||||
return { handleChange }; |
// setTimeout(() => { |
||||
|
// value.value = '1233'; |
||||
|
// }, 5000); |
||||
|
return { handleChange, value }; |
||||
}, |
}, |
||||
}); |
}); |
||||
</script> |
</script> |
||||
Loading…
Reference in new issue