1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Fixes #5241: Katex code could be broken after editing it in Rich Text editor

This commit is contained in:
Laurent Cozic 2021-07-26 14:50:31 +01:00
parent d5fcffbac1
commit 8920db5537
4 changed files with 149 additions and 126 deletions

View File

@ -372,6 +372,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map

3
.gitignore vendored
View File

@ -357,6 +357,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map

View File

@ -16,11 +16,11 @@ import { copyHtmlToClipboard } from '../../utils/clipboardUtils';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
const { MarkupToHtml } = require('@joplin/renderer'); const { MarkupToHtml } = require('@joplin/renderer');
const taboverride = require('taboverride');
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import BaseItem from '@joplin/lib/models/BaseItem'; import BaseItem from '@joplin/lib/models/BaseItem';
import setupToolbarButtons from './utils/setupToolbarButtons'; import setupToolbarButtons from './utils/setupToolbarButtons';
import { plainTextToHtml } from '@joplin/lib/htmlUtils'; import { plainTextToHtml } from '@joplin/lib/htmlUtils';
import openEditDialog from './utils/openEditDialog';
const { themeStyle } = require('@joplin/lib/theme'); const { themeStyle } = require('@joplin/lib/theme');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales'); const supportedLocales = require('./supportedLocales');
@ -40,33 +40,6 @@ function markupRenderOptions(override: any = null) {
}; };
} }
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' ? '$$' : '```';
const fenceLanguage = language === 'katex' ? '' : language;
return {
openCharacters: `\n${fence}${fenceLanguage}\n`,
closeCharacters: `\n${fence}\n`,
content: content,
node: null,
language: language,
};
}
// In TinyMCE 5.2, when setting the body to '<div id="rendered-md"></div>', // In TinyMCE 5.2, when setting the body to '<div id="rendered-md"></div>',
// it would end up as '<div id="rendered-md"><br/></div>' once rendered // it would end up as '<div id="rendered-md"><br/></div>' once rendered
// (an additional <br/> was inserted). // (an additional <br/> was inserted).
@ -95,42 +68,12 @@ function findEditableContainer(node: any): any {
return null; 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());
}
}
let markupToHtml_ = new MarkupToHtml(); let markupToHtml_ = new MarkupToHtml();
function stripMarkup(markupLanguage: number, markup: string, options: any = null) { function stripMarkup(markupLanguage: number, markup: string, options: any = null) {
if (!markupToHtml_) markupToHtml_ = new MarkupToHtml(); if (!markupToHtml_) markupToHtml_ = new MarkupToHtml();
return markupToHtml_.stripMarkup(markupLanguage, markup, options); return markupToHtml_.stripMarkup(markupLanguage, markup, options);
} }
// 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);
}
}
}
interface TinyMceCommand { interface TinyMceCommand {
name: string; name: string;
value?: any; value?: any;
@ -618,70 +561,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
joplinSup: { inline: 'sup', remove: 'all' }, joplinSup: { inline: 'sup', remove: 'all' },
}, },
setup: (editor: any) => { setup: (editor: any) => {
function openEditDialog(editable: any) {
const source = editable ? findBlockSource(editable) : newBlockSource();
editor.windowManager.open({
title: _('Edit'),
size: 'large',
initialData: {
codeTextArea: source.content,
languageInput: source.language,
},
onSubmit: async (dialogApi: any) => {
const newSource = newBlockSource(dialogApi.getData().languageInput, dialogApi.getData().codeTextArea);
const md = `${newSource.openCharacters}${newSource.content.trim()}${newSource.closeCharacters}`;
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, md, { bodyOnly: true });
// markupToHtml will return the complete editable HTML, but we only
// want to update the inner HTML, so as not to break additional props that
// are added by TinyMCE on the main node.
if (editable) {
editable.innerHTML = editableInnerHtml(result.html);
} else {
editor.insertContent(result.html);
}
dialogApi.close();
editor.fire('joplinChange');
dispatchDidUpdate(editor);
},
onClose: () => {
enableTextAreaTab(false);
},
body: {
type: 'panel',
items: [
{
type: 'input',
name: 'languageInput',
label: 'Language',
// Katex is a special case with special opening/closing tags
// and we don't currently handle switching the language in this case.
disabled: source.language === 'katex',
},
{
type: 'textarea',
name: 'codeTextArea',
value: source.content,
},
],
},
buttons: [
{
type: 'submit',
text: 'OK',
},
],
});
window.requestAnimationFrame(() => {
enableTextAreaTab(true);
});
}
editor.ui.registry.addButton('joplinAttach', { editor.ui.registry.addButton('joplinAttach', {
tooltip: _('Attach file'), tooltip: _('Attach file'),
icon: 'paperclip', icon: 'paperclip',
@ -696,7 +575,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
tooltip: _('Code Block'), tooltip: _('Code Block'),
icon: 'code-sample', icon: 'code-sample',
onAction: async function() { onAction: async function() {
openEditDialog(null); openEditDialog(editor, markupToHtml, dispatchDidUpdate, null);
}, },
}); });
@ -738,12 +617,10 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.addShortcut('Meta+Shift+8', '', () => editor.execCommand('InsertUnorderedList')); editor.addShortcut('Meta+Shift+8', '', () => editor.execCommand('InsertUnorderedList'));
editor.addShortcut('Meta+Shift+9', '', () => editor.execCommand('InsertJoplinChecklist')); editor.addShortcut('Meta+Shift+9', '', () => editor.execCommand('InsertJoplinChecklist'));
// setupContextMenu(editor);
// TODO: remove event on unmount? // TODO: remove event on unmount?
editor.on('DblClick', (event: any) => { editor.on('DblClick', (event: any) => {
const editable = findEditableContainer(event.target); const editable = findEditableContainer(event.target);
if (editable) openEditDialog(editable); if (editable) openEditDialog(editor, markupToHtml, dispatchDidUpdate, editable);
}); });
// This is triggered when an external file is dropped on the editor // This is triggered when an external file is dropped on the editor

View File

@ -0,0 +1,140 @@
import { _ } from '@joplin/lib/locale';
import { MarkupToHtml } from '@joplin/renderer';
const taboverride = require('taboverride');
interface SourceInfo {
openCharacters: string;
closeCharacters: string;
content: string;
node: any;
language: string;
}
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);
}
}
}
function findBlockSource(node: any): SourceInfo {
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 = '', previousSource: SourceInfo = null): SourceInfo {
let fence = '```';
if (language === 'katex') {
if (previousSource && previousSource.openCharacters === '$') {
fence = '$';
} else {
fence = '$$';
}
}
const fenceLanguage = language === 'katex' ? '' : language;
return {
openCharacters: fence === '$' ? '$' : `\n${fence}${fenceLanguage}\n`,
closeCharacters: fence === '$' ? '$' : `\n${fence}\n`,
content: content,
node: null,
language: language,
};
}
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;
}
export default function openEditDialog(editor: any, markupToHtml: any, dispatchDidUpdate: Function, editable: any) {
const source = editable ? findBlockSource(editable) : newBlockSource();
editor.windowManager.open({
title: _('Edit'),
size: 'large',
initialData: {
codeTextArea: source.content,
languageInput: source.language,
},
onSubmit: async (dialogApi: any) => {
const newSource = newBlockSource(dialogApi.getData().languageInput, dialogApi.getData().codeTextArea, source);
const md = `${newSource.openCharacters}${newSource.content.trim()}${newSource.closeCharacters}`;
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, md, { bodyOnly: true });
// markupToHtml will return the complete editable HTML, but we only
// want to update the inner HTML, so as not to break additional props that
// are added by TinyMCE on the main node.
if (editable) {
editable.innerHTML = editableInnerHtml(result.html);
} else {
editor.insertContent(result.html);
}
dialogApi.close();
editor.fire('joplinChange');
dispatchDidUpdate(editor);
},
onClose: () => {
enableTextAreaTab(false);
},
body: {
type: 'panel',
items: [
{
type: 'input',
name: 'languageInput',
label: 'Language',
// Katex is a special case with special opening/closing tags
// and we don't currently handle switching the language in this case.
disabled: source.language === 'katex',
},
{
type: 'textarea',
name: 'codeTextArea',
value: source.content,
},
],
},
buttons: [
{
type: 'submit',
text: 'OK',
},
],
});
window.requestAnimationFrame(() => {
enableTextAreaTab(true);
});
}