import * as React from 'react'; import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; // eslint-disable-next-line no-unused-vars import { DefaultEditorState, OnChangeEvent, TextEditorUtils, EditorCommand, resourcesStatus } from '../utils/NoteText'; const { MarkupToHtml } = require('lib/joplin-renderer'); const taboverride = require('taboverride'); const { reg } = require('lib/registry.js'); const { _ } = require('lib/locale'); const BaseItem = require('lib/models/BaseItem'); const { themeStyle, buildStyle } = require('../../theme.js'); interface TinyMCEProps { style: any, theme: number, onChange(event: OnChangeEvent): void, onWillChange(event:any): void, onMessage(event:any): void, defaultEditorState: DefaultEditorState, markupToHtml: Function, allAssets: Function, attachResources: Function, joplinHtml: Function, disabled: boolean, } function markupRenderOptions(override:any = null) { return { plugins: { checkbox: { renderingType: 2, }, link_open: { linkRenderingType: 2, }, }, ...override, }; } function findBlockSource(node:any) { const sources = node.getElementsByClassName('joplin-source'); if (!sources.length) throw new Error('No source for node'); const source = sources[0]; return { openCharacters: source.getAttribute('data-joplin-source-open'), closeCharacters: source.getAttribute('data-joplin-source-close'), content: source.textContent, node: source, language: source.getAttribute('data-joplin-language') || '', }; } function newBlockSource(language:string = '', content:string = ''):any { const fence = language === 'katex' ? '$$' : '```'; return { openCharacters: `\n${fence}${language}\n`, closeCharacters: `\n${fence}\n`, content: content, node: null, language: language, }; } function findEditableContainer(node:any):any { while (node) { if (node.classList && node.classList.contains('joplin-editable')) return node; node = node.parentNode; } return null; } function editableInnerHtml(html:string):string { const temp = document.createElement('div'); temp.innerHTML = html; const editable = temp.getElementsByClassName('joplin-editable'); if (!editable.length) throw new Error(`Invalid joplin-editable: ${html}`); return editable[0].innerHTML; } function dialogTextArea_keyDown(event:any) { if (event.key === 'Tab') { window.requestAnimationFrame(() => event.target.focus()); } } // Allows pressing tab in a textarea to input an actual tab (instead of changing focus) // taboverride will take care of actually inserting the tab character, while the keydown // event listener will override the default behaviour, which is to focus the next field. function enableTextAreaTab(enable:boolean) { const textAreas = document.getElementsByClassName('tox-textarea'); for (const textArea of textAreas) { taboverride.set(textArea, enable); if (enable) { textArea.addEventListener('keydown', dialogTextArea_keyDown); } else { textArea.removeEventListener('keydown', dialogTextArea_keyDown); } } } export const utils:TextEditorUtils = { editorContentToHtml(content:any):Promise { return content ? content : ''; }, }; interface TinyMceCommand { name: string, value?: any, ui?: boolean } interface JoplinCommandToTinyMceCommands { [key:string]: TinyMceCommand, } const joplinCommandToTinyMceCommands:JoplinCommandToTinyMceCommands = { 'textBold': { name: 'mceToggleFormat', value: 'bold' }, 'textItalic': { name: 'mceToggleFormat', value: 'italic' }, 'textLink': { name: 'mceLink' }, 'search': { name: 'SearchReplace' }, }; function styles_(props:TinyMCEProps) { return buildStyle('TinyMCE', props.theme, (/* theme:any */) => { return { disabledOverlay: { zIndex: 10, position: 'absolute', backgroundColor: 'white', opacity: 0.7, height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', padding: 20, paddingTop: 50, textAlign: 'center', }, rootStyle: { position: 'relative', ...props.style, }, }; }); } let loadedAssetFiles_:string[] = []; let dispatchDidUpdateIID_:any = null; let changeId_:number = 1; const TinyMCE = (props:TinyMCEProps, ref:any) => { const [editor, setEditor] = useState(null); const [scriptLoaded, setScriptLoaded] = useState(false); const [editorReady, setEditorReady] = useState(false); const attachResources = useRef(null); attachResources.current = props.attachResources; const markupToHtml = useRef(null); markupToHtml.current = props.markupToHtml; const joplinHtml = useRef(null); joplinHtml.current = props.joplinHtml; const rootIdRef = useRef(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`); const editorRef = useRef(null); editorRef.current = editor; const styles = styles_(props); const theme = themeStyle(props.theme); const dispatchDidUpdate = (editor:any) => { if (dispatchDidUpdateIID_) clearTimeout(dispatchDidUpdateIID_); dispatchDidUpdateIID_ = setTimeout(() => { dispatchDidUpdateIID_ = null; if (editor && editor.getDoc()) editor.getDoc().dispatchEvent(new Event('joplin-noteDidUpdate')); }, 10); }; const onEditorContentClick = useCallback((event:any) => { const nodeName = event.target ? event.target.nodeName : ''; if (nodeName === 'INPUT' && event.target.getAttribute('type') === 'checkbox') { editor.fire('joplinChange'); dispatchDidUpdate(editor); } if (nodeName === 'A' && (event.ctrlKey || event.metaKey)) { const href = event.target.getAttribute('href'); const joplinUrl = href.indexOf('joplin://') === 0 ? href : null; if (joplinUrl) { props.onMessage({ name: 'openInternal', args: { url: joplinUrl, }, }); } else if (href.indexOf('#') === 0) { const anchorName = href.substr(1); const anchor = editor.getDoc().getElementById(anchorName); if (anchor) { anchor.scrollIntoView(); } else { reg.logger().warn('TinyMce: could not find anchor with ID ', anchorName); } } else { props.onMessage({ name: 'openExternal', args: { url: href, }, }); } } }, [editor, props.onMessage]); useImperativeHandle(ref, () => { return { content: () => editor ? editor.getContent() : '', execCommand: async (cmd:EditorCommand) => { if (!editor) return false; reg.logger().debug('TinyMce: execCommand', cmd); let commandProcessed = true; if (cmd.name === 'insertText') { const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, cmd.value, { bodyOnly: true }); editor.insertContent(result.html); } else if (cmd.name === 'focus') { editor.focus(); } else { commandProcessed = false; } if (commandProcessed) return true; if (!joplinCommandToTinyMceCommands[cmd.name]) { reg.logger().warn('TinyMCE: unsupported Joplin command: ', cmd); return false; } const tinyMceCmd:TinyMceCommand = { ...joplinCommandToTinyMceCommands[cmd.name] }; if (!('ui' in tinyMceCmd)) tinyMceCmd.ui = false; if (!('value' in tinyMceCmd)) tinyMceCmd.value = null; editor.execCommand(tinyMceCmd.name, tinyMceCmd.ui, tinyMceCmd.value); return true; }, }; }, [editor]); // ----------------------------------------------------------------------------------------- // Load the TinyMCE library. The lib loads additional JS and CSS files on startup // (for themes), and so it needs to be loaded via