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:
parent
d5fcffbac1
commit
8920db5537
@ -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
3
.gitignore
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user