You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-10 22:11:50 +02:00
This commit is contained in:
@@ -1059,7 +1059,10 @@ packages/editor/ProseMirror/commands.js
|
|||||||
packages/editor/ProseMirror/createEditor.js
|
packages/editor/ProseMirror/createEditor.js
|
||||||
packages/editor/ProseMirror/index.js
|
packages/editor/ProseMirror/index.js
|
||||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||||
@@ -1075,6 +1078,9 @@ packages/editor/ProseMirror/types.js
|
|||||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1032,7 +1032,10 @@ packages/editor/ProseMirror/commands.js
|
|||||||
packages/editor/ProseMirror/createEditor.js
|
packages/editor/ProseMirror/createEditor.js
|
||||||
packages/editor/ProseMirror/index.js
|
packages/editor/ProseMirror/index.js
|
||||||
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
packages/editor/ProseMirror/plugins/inputRulesPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditablePlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/createEditorDialog.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.test.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/joplinEditablePlugin.js
|
||||||
|
packages/editor/ProseMirror/plugins/joplinEditablePlugin/postProcessRenderedHtml.js
|
||||||
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
packages/editor/ProseMirror/plugins/joplinEditorApiPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
packages/editor/ProseMirror/plugins/keymapPlugin.js
|
||||||
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
packages/editor/ProseMirror/plugins/linkTooltipPlugin.test.js
|
||||||
@@ -1048,6 +1051,9 @@ packages/editor/ProseMirror/types.js
|
|||||||
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
packages/editor/ProseMirror/utils/UndoStackSynchronizer.js
|
||||||
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
packages/editor/ProseMirror/utils/canReplaceSelectionWith.js
|
||||||
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
packages/editor/ProseMirror/utils/computeSelectionFormatting.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createTextArea.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createTextNode.js
|
||||||
|
packages/editor/ProseMirror/utils/dom/createUniqueId.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.test.js
|
||||||
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
packages/editor/ProseMirror/utils/extractSelectedLinesTo.js
|
||||||
packages/editor/ProseMirror/utils/jumpToHash.js
|
packages/editor/ProseMirror/utils/jumpToHash.js
|
||||||
|
@@ -410,6 +410,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
|||||||
onSelectPastBeginning={onSelectPastBeginning}
|
onSelectPastBeginning={onSelectPastBeginning}
|
||||||
externalSearch={props.searchMarkers}
|
externalSearch={props.searchMarkers}
|
||||||
useLocalSearch={props.useLocalSearch}
|
useLocalSearch={props.useLocalSearch}
|
||||||
|
onLocalize={_}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -132,6 +132,7 @@ const useRerenderHandler = (props: Props) => {
|
|||||||
highlightedKeywords: props.highlightedKeywords,
|
highlightedKeywords: props.highlightedKeywords,
|
||||||
resources: props.noteResources,
|
resources: props.noteResources,
|
||||||
pluginAssetContainerSelector: '#joplin-container-pluginAssetsContainer',
|
pluginAssetContainerSelector: '#joplin-container-pluginAssetsContainer',
|
||||||
|
removeUnusedPluginAssets: true,
|
||||||
|
|
||||||
// If the hash changed, we don't set initial scroll -- we want to scroll to the hash
|
// If the hash changed, we don't set initial scroll -- we want to scroll to the hash
|
||||||
// instead.
|
// instead.
|
||||||
|
@@ -121,6 +121,7 @@ const MarkdownEditor: React.FC<EditorProps> = props => {
|
|||||||
initialText: props.initialText,
|
initialText: props.initialText,
|
||||||
initialNoteId: props.noteId,
|
initialNoteId: props.noteId,
|
||||||
settings: props.editorSettings,
|
settings: props.editorSettings,
|
||||||
|
onLocalize: _,
|
||||||
},
|
},
|
||||||
webviewRef,
|
webviewRef,
|
||||||
});
|
});
|
||||||
|
@@ -11,6 +11,7 @@ export const initializeEditor = ({
|
|||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
settings,
|
settings,
|
||||||
|
onLocalize,
|
||||||
}: EditorProps) => {
|
}: EditorProps) => {
|
||||||
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('markdownEditor', null);
|
const messenger = new WebViewToRNMessenger<EditorProcessApi, MainProcessApi>('markdownEditor', null);
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export const initializeEditor = ({
|
|||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
settings,
|
settings,
|
||||||
|
onLocalize,
|
||||||
|
|
||||||
onPasteFile: async (data) => {
|
onPasteFile: async (data) => {
|
||||||
const base64 = await readFileToBase64(data);
|
const base64 = await readFileToBase64(data);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { EditorEvent } from '@joplin/editor/events';
|
import { EditorEvent } from '@joplin/editor/events';
|
||||||
import { EditorControl, EditorSettings } from '@joplin/editor/types';
|
import { EditorControl, EditorSettings, OnLocalize } from '@joplin/editor/types';
|
||||||
|
|
||||||
export interface EditorProcessApi {
|
export interface EditorProcessApi {
|
||||||
editor: EditorControl;
|
editor: EditorControl;
|
||||||
@@ -14,6 +14,7 @@ export interface EditorProps {
|
|||||||
parentElementClassName: string;
|
parentElementClassName: string;
|
||||||
initialText: string;
|
initialText: string;
|
||||||
initialNoteId: string;
|
initialNoteId: string;
|
||||||
|
onLocalize: OnLocalize;
|
||||||
settings: EditorSettings;
|
settings: EditorSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ const defaultRendererSettings: RenderSettings = {
|
|||||||
noteHash: '',
|
noteHash: '',
|
||||||
initialScroll: 0,
|
initialScroll: 0,
|
||||||
readAssetBlob: async (_path: string) => new Blob(),
|
readAssetBlob: async (_path: string) => new Blob(),
|
||||||
|
removeUnusedPluginAssets: true,
|
||||||
|
|
||||||
createEditPopupSyntax: '',
|
createEditPopupSyntax: '',
|
||||||
destroyEditPopupSyntax: '',
|
destroyEditPopupSyntax: '',
|
||||||
|
@@ -23,6 +23,7 @@ export interface RenderSettings {
|
|||||||
initialScroll: number;
|
initialScroll: number;
|
||||||
// If [null], plugin assets are not added to the document
|
// If [null], plugin assets are not added to the document
|
||||||
pluginAssetContainerSelector: string|null;
|
pluginAssetContainerSelector: string|null;
|
||||||
|
removeUnusedPluginAssets: boolean;
|
||||||
|
|
||||||
splitted?: boolean; // Move CSS into a separate output
|
splitted?: boolean; // Move CSS into a separate output
|
||||||
mapsToLine?: boolean; // Sourcemaps
|
mapsToLine?: boolean; // Sourcemaps
|
||||||
@@ -156,6 +157,7 @@ export default class Renderer {
|
|||||||
inlineAssets: this.setupOptions_.useTransferredFiles,
|
inlineAssets: this.setupOptions_.useTransferredFiles,
|
||||||
readAssetBlob: settings.readAssetBlob,
|
readAssetBlob: settings.readAssetBlob,
|
||||||
container: document.querySelector(settings.pluginAssetContainerSelector),
|
container: document.querySelector(settings.pluginAssetContainerSelector),
|
||||||
|
removeUnusedPluginAssets: settings.removeUnusedPluginAssets,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Some plugins require this event to be dispatched just after being added.
|
// Some plugins require this event to be dispatched just after being added.
|
||||||
|
@@ -39,6 +39,7 @@ const rewriteInternalAssetLinks = async (asset: RenderResultPluginAsset, content
|
|||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
inlineAssets: boolean;
|
inlineAssets: boolean;
|
||||||
|
removeUnusedPluginAssets: boolean;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
readAssetBlob?(path: string): Promise<Blob>;
|
readAssetBlob?(path: string): Promise<Blob>;
|
||||||
}
|
}
|
||||||
@@ -137,16 +138,22 @@ const addPluginAssets = async (assets: RenderResultPluginAsset[], options: Optio
|
|||||||
// light to dark theme, and then back to light theme - in that case
|
// light to dark theme, and then back to light theme - in that case
|
||||||
// the viewer would remain dark because it would use the dark
|
// the viewer would remain dark because it would use the dark
|
||||||
// stylesheet that would still be in the DOM.
|
// stylesheet that would still be in the DOM.
|
||||||
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
|
//
|
||||||
if (!processedAssetIds.includes(assetId)) {
|
// In some cases, however, we only want to rerender part of the document.
|
||||||
try {
|
// In this case, old plugin assets may have been from the last full-page
|
||||||
asset.element.remove();
|
// render and should not be removed.
|
||||||
} catch (error) {
|
if (options.removeUnusedPluginAssets) {
|
||||||
// We don't throw an exception but we log it since
|
for (const [assetId, asset] of Object.entries(pluginAssetsAdded_)) {
|
||||||
// it shouldn't happen
|
if (!processedAssetIds.includes(assetId)) {
|
||||||
console.warn('Tried to remove an asset but got an error', error);
|
try {
|
||||||
|
asset.element.remove();
|
||||||
|
} catch (error) {
|
||||||
|
// We don't throw an exception but we log it since
|
||||||
|
// it shouldn't happen
|
||||||
|
console.warn('Tried to remove an asset but got an error', error);
|
||||||
|
}
|
||||||
|
pluginAssetsAdded_[assetId] = null;
|
||||||
}
|
}
|
||||||
pluginAssetsAdded_[assetId] = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -54,8 +54,13 @@ export interface RenderOptions {
|
|||||||
highlightedKeywords: string[];
|
highlightedKeywords: string[];
|
||||||
resources: ResourceInfos;
|
resources: ResourceInfos;
|
||||||
themeOverrides: Record<string, string|number>;
|
themeOverrides: Record<string, string|number>;
|
||||||
|
|
||||||
// If null, plugin assets will not be added to the document.
|
// If null, plugin assets will not be added to the document.
|
||||||
pluginAssetContainerSelector: string|null;
|
pluginAssetContainerSelector: string|null;
|
||||||
|
// When true, plugin assets are removed from the container when not used by the render result.
|
||||||
|
// This should be true for full-page renders.
|
||||||
|
removeUnusedPluginAssets: boolean;
|
||||||
|
|
||||||
noteHash: string;
|
noteHash: string;
|
||||||
initialScroll: number;
|
initialScroll: number;
|
||||||
|
|
||||||
|
@@ -219,6 +219,7 @@ const useWebViewSetup = (props: Props): SetUpResult<RendererControl> => {
|
|||||||
}
|
}
|
||||||
return shim.fsDriver().fileAtPath(resolvedPath);
|
return shim.fsDriver().fileAtPath(resolvedPath);
|
||||||
},
|
},
|
||||||
|
removeUnusedPluginAssets: options.removeUnusedPluginAssets,
|
||||||
};
|
};
|
||||||
|
|
||||||
await transferResources(options.resources);
|
await transferResources(options.resources);
|
||||||
|
@@ -16,22 +16,6 @@ const postprocessHtml = (html: HTMLElement) => {
|
|||||||
resource.src = `:/${resourceId}`;
|
resource.src = `:/${resourceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-add newlines to data-joplin-source-* that were removed
|
|
||||||
// by ProseMirror.
|
|
||||||
// TODO: Try to find a better solution
|
|
||||||
const sourceBlocks = html.querySelectorAll<HTMLPreElement>(
|
|
||||||
'pre[data-joplin-source-open][data-joplin-source-close].joplin-source',
|
|
||||||
);
|
|
||||||
for (const sourceBlock of sourceBlocks) {
|
|
||||||
const isBlock = sourceBlock.parentElement.tagName !== 'SPAN';
|
|
||||||
if (isBlock) {
|
|
||||||
const originalOpen = sourceBlock.getAttribute('data-joplin-source-open');
|
|
||||||
const originalClose = sourceBlock.getAttribute('data-joplin-source-close');
|
|
||||||
sourceBlock.setAttribute('data-joplin-source-open', `${originalOpen}\n`);
|
|
||||||
sourceBlock.setAttribute('data-joplin-source-close', `\n${originalClose}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +57,7 @@ export const initialize = async ({
|
|||||||
settings,
|
settings,
|
||||||
initialText,
|
initialText,
|
||||||
initialNoteId,
|
initialNoteId,
|
||||||
|
onLocalize: messenger.remoteApi.onLocalize,
|
||||||
|
|
||||||
onPasteFile: async (data) => {
|
onPasteFile: async (data) => {
|
||||||
const base64 = await readFileToBase64(data);
|
const base64 = await readFileToBase64(data);
|
||||||
@@ -85,14 +70,20 @@ export const initialize = async ({
|
|||||||
void messenger.remoteApi.onEditorEvent(event);
|
void messenger.remoteApi.onEditorEvent(event);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
renderMarkupToHtml: async (markup) => {
|
renderMarkupToHtml: async (markup, options) => {
|
||||||
|
let language = MarkupLanguage.Markdown;
|
||||||
|
if (settings.language === EditorLanguageType.Html && !options.forceMarkdown) {
|
||||||
|
language = MarkupLanguage.Html;
|
||||||
|
}
|
||||||
|
|
||||||
return await messenger.remoteApi.onRender({
|
return await messenger.remoteApi.onRender({
|
||||||
markup,
|
markup,
|
||||||
language: settings.language === EditorLanguageType.Html ? MarkupLanguage.Html : MarkupLanguage.Markdown,
|
language,
|
||||||
}, {
|
}, {
|
||||||
pluginAssetContainerSelector: `#${assetContainer.id}`,
|
pluginAssetContainerSelector: `#${assetContainer.id}`,
|
||||||
splitted: true,
|
splitted: true,
|
||||||
mapsToLine: true,
|
mapsToLine: true,
|
||||||
|
removeUnusedPluginAssets: options.isFullPageRender,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderHtmlToMarkup: (node) => {
|
renderHtmlToMarkup: (node) => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { EditorEvent } from '@joplin/editor/events';
|
import { EditorEvent } from '@joplin/editor/events';
|
||||||
import { EditorControl, EditorSettings, SearchState } from '@joplin/editor/types';
|
import { EditorControl, EditorSettings, OnLocalize, SearchState } from '@joplin/editor/types';
|
||||||
import { MarkupRecord, RendererControl } from '../rendererBundle/types';
|
import { MarkupRecord, RendererControl } from '../rendererBundle/types';
|
||||||
import { RenderResult } from '@joplin/renderer/types';
|
import { RenderResult } from '@joplin/renderer/types';
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ type RenderOptionsSlice = {
|
|||||||
pluginAssetContainerSelector: string;
|
pluginAssetContainerSelector: string;
|
||||||
splitted: boolean;
|
splitted: boolean;
|
||||||
mapsToLine: true;
|
mapsToLine: true;
|
||||||
|
removeUnusedPluginAssets: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MainProcessApi {
|
export interface MainProcessApi {
|
||||||
@@ -26,6 +27,7 @@ export interface MainProcessApi {
|
|||||||
logMessage(message: string): Promise<void>;
|
logMessage(message: string): Promise<void>;
|
||||||
onRender(markup: MarkupRecord, options: RenderOptionsSlice): Promise<RenderResult>;
|
onRender(markup: MarkupRecord, options: RenderOptionsSlice): Promise<RenderResult>;
|
||||||
onPasteFile(type: string, base64: string): Promise<void>;
|
onPasteFile(type: string, base64: string): Promise<void>;
|
||||||
|
onLocalize: OnLocalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RichTextEditorControl {
|
export interface RichTextEditorControl {
|
||||||
|
@@ -11,6 +11,7 @@ import shim from '@joplin/lib/shim';
|
|||||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||||
import { RendererControl, RenderOptions } from '../rendererBundle/types';
|
import { RendererControl, RenderOptions } from '../rendererBundle/types';
|
||||||
import { ResourceInfos } from '@joplin/renderer/types';
|
import { ResourceInfos } from '@joplin/renderer/types';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { defaultSearchState } from '../../components/NoteEditor/SearchPanel';
|
import { defaultSearchState } from '../../components/NoteEditor/SearchPanel';
|
||||||
|
|
||||||
const logger = Logger.create('useWebViewSetup');
|
const logger = Logger.create('useWebViewSetup');
|
||||||
@@ -50,6 +51,7 @@ const useMessenger = (props: UseMessengerProps) => {
|
|||||||
noteHash: '',
|
noteHash: '',
|
||||||
initialScroll: 0,
|
initialScroll: 0,
|
||||||
pluginAssetContainerSelector: null,
|
pluginAssetContainerSelector: null,
|
||||||
|
removeUnusedPluginAssets: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@@ -70,6 +72,7 @@ const useMessenger = (props: UseMessengerProps) => {
|
|||||||
splitted: options.splitted,
|
splitted: options.splitted,
|
||||||
pluginAssetContainerSelector: options.pluginAssetContainerSelector,
|
pluginAssetContainerSelector: options.pluginAssetContainerSelector,
|
||||||
mapsToLine: options.mapsToLine,
|
mapsToLine: options.mapsToLine,
|
||||||
|
removeUnusedPluginAssets: options.removeUnusedPluginAssets,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return renderResult;
|
return renderResult;
|
||||||
@@ -77,6 +80,7 @@ const useMessenger = (props: UseMessengerProps) => {
|
|||||||
onPasteFile: async (type: string, base64: string) => {
|
onPasteFile: async (type: string, base64: string) => {
|
||||||
onAttachRef.current(type, base64);
|
onAttachRef.current(type, base64);
|
||||||
},
|
},
|
||||||
|
onLocalize: _,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messenger = new RNToWebViewMessenger<MainProcessApi, EditorProcessApi>(
|
const messenger = new RNToWebViewMessenger<MainProcessApi, EditorProcessApi>(
|
||||||
|
@@ -40,6 +40,7 @@ describe('createEditor', () => {
|
|||||||
settings: editorSettings,
|
settings: editorSettings,
|
||||||
onEvent: _event => {},
|
onEvent: _event => {},
|
||||||
onLogMessage: _message => {},
|
onLogMessage: _message => {},
|
||||||
|
onLocalize: input => input,
|
||||||
onPasteFile: null,
|
onPasteFile: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ describe('createEditor', () => {
|
|||||||
settings: editorSettings,
|
settings: editorSettings,
|
||||||
onEvent: _event => {},
|
onEvent: _event => {},
|
||||||
onLogMessage: _message => {},
|
onLogMessage: _message => {},
|
||||||
|
onLocalize: input => input,
|
||||||
onPasteFile: null,
|
onPasteFile: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,6 +140,7 @@ describe('createEditor', () => {
|
|||||||
settings: editorSettings,
|
settings: editorSettings,
|
||||||
onEvent: _event => {},
|
onEvent: _event => {},
|
||||||
onLogMessage: _message => {},
|
onLogMessage: _message => {},
|
||||||
|
onLocalize: input => input,
|
||||||
onPasteFile: null,
|
onPasteFile: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,6 +191,7 @@ describe('createEditor', () => {
|
|||||||
settings: editorSettings,
|
settings: editorSettings,
|
||||||
onEvent: () => {},
|
onEvent: () => {},
|
||||||
onLogMessage: () => {},
|
onLogMessage: () => {},
|
||||||
|
onLocalize: input => input,
|
||||||
onPasteFile: null,
|
onPasteFile: null,
|
||||||
});
|
});
|
||||||
const editorState = editor.editor.state;
|
const editorState = editor.editor.state;
|
||||||
|
@@ -12,6 +12,7 @@ const createEditorControl = (initialText: string) => {
|
|||||||
onEvent: _event => {},
|
onEvent: _event => {},
|
||||||
onLogMessage: _message => {},
|
onLogMessage: _message => {},
|
||||||
onPasteFile: null,
|
onPasteFile: null,
|
||||||
|
onLocalize: input=>input,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -99,7 +99,10 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
|||||||
if (canReplaceSelectionWith(state.selection, nodeType)) {
|
if (canReplaceSelectionWith(state.selection, nodeType)) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const separator = block ? '$$' : '$';
|
const separator = block ? '$$' : '$';
|
||||||
const rendered = await renderer.renderMarkupToHtml(`${separator}${selectedText}${separator}`);
|
const rendered = await renderer.renderMarkupToHtml(`${separator}${selectedText}${separator}`, {
|
||||||
|
forceMarkdown: true,
|
||||||
|
isFullPageRender: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (view) {
|
if (view) {
|
||||||
view.pasteHTML(rendered.html);
|
view.pasteHTML(rendered.html);
|
||||||
|
@@ -11,7 +11,7 @@ import { EditorEventType } from '../events';
|
|||||||
import UndoStackSynchronizer from './utils/UndoStackSynchronizer';
|
import UndoStackSynchronizer from './utils/UndoStackSynchronizer';
|
||||||
import computeSelectionFormatting from './utils/computeSelectionFormatting';
|
import computeSelectionFormatting from './utils/computeSelectionFormatting';
|
||||||
import { defaultSelectionFormatting, selectionFormattingEqual } from '../SelectionFormatting';
|
import { defaultSelectionFormatting, selectionFormattingEqual } from '../SelectionFormatting';
|
||||||
import joplinEditablePlugin from './plugins/joplinEditablePlugin';
|
import joplinEditablePlugin from './plugins/joplinEditablePlugin/joplinEditablePlugin';
|
||||||
import keymapExtension from './plugins/keymapPlugin';
|
import keymapExtension from './plugins/keymapPlugin';
|
||||||
import inputRulesExtension from './plugins/inputRulesPlugin';
|
import inputRulesExtension from './plugins/inputRulesPlugin';
|
||||||
import originalMarkupPlugin from './plugins/originalMarkupPlugin';
|
import originalMarkupPlugin from './plugins/originalMarkupPlugin';
|
||||||
@@ -44,7 +44,10 @@ const createEditor = async (
|
|||||||
const { plugin: searchPlugin, updateState: updateSearchState } = searchExtension(props.onEvent);
|
const { plugin: searchPlugin, updateState: updateSearchState } = searchExtension(props.onEvent);
|
||||||
|
|
||||||
const renderAndPostprocessHtml = async (markup: string) => {
|
const renderAndPostprocessHtml = async (markup: string) => {
|
||||||
const renderResult = await renderer.renderMarkupToHtml(markup);
|
const renderResult = await renderer.renderMarkupToHtml(markup, {
|
||||||
|
forceMarkdown: false,
|
||||||
|
isFullPageRender: true,
|
||||||
|
});
|
||||||
|
|
||||||
const dom = new DOMParser().parseFromString(renderResult.html, 'text/html');
|
const dom = new DOMParser().parseFromString(renderResult.html, 'text/html');
|
||||||
preprocessEditorInput(dom, markup);
|
preprocessEditorInput(dom, markup);
|
||||||
@@ -81,10 +84,20 @@ const createEditor = async (
|
|||||||
].flat(),
|
].flat(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cachedLocalizations = new Map<string, string|Promise<string>>();
|
||||||
state = state.apply(
|
state = state.apply(
|
||||||
setEditorApi(state.tr, {
|
setEditorApi(state.tr, {
|
||||||
onEvent: props.onEvent,
|
onEvent: props.onEvent,
|
||||||
renderer,
|
renderer,
|
||||||
|
localize: async (input: string) => {
|
||||||
|
if (cachedLocalizations.has(input)) {
|
||||||
|
return cachedLocalizations.get(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = props.onLocalize(input);
|
||||||
|
cachedLocalizations.set(input, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,69 +0,0 @@
|
|||||||
import { Plugin } from 'prosemirror-state';
|
|
||||||
import { Node, NodeSpec } from 'prosemirror-model';
|
|
||||||
import { NodeView } from 'prosemirror-view';
|
|
||||||
import sanitizeHtml from '../utils/sanitizeHtml';
|
|
||||||
|
|
||||||
// See the fold example for more information about
|
|
||||||
// writing similar ProseMirror plugins:
|
|
||||||
// https://prosemirror.net/examples/fold/
|
|
||||||
|
|
||||||
|
|
||||||
const makeJoplinEditableSpec = (inline: boolean): NodeSpec => ({
|
|
||||||
group: inline ? 'inline' : 'block',
|
|
||||||
inline: inline,
|
|
||||||
draggable: true,
|
|
||||||
attrs: {
|
|
||||||
contentHtml: { default: '', validate: 'string' },
|
|
||||||
},
|
|
||||||
parseDOM: [
|
|
||||||
{
|
|
||||||
tag: `${inline ? 'span' : 'div'}.joplin-editable`,
|
|
||||||
getAttrs: node => ({
|
|
||||||
contentHtml: node.innerHTML,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
toDOM: node => {
|
|
||||||
const content = document.createElement(inline ? 'span' : 'div');
|
|
||||||
content.classList.add('joplin-editable');
|
|
||||||
content.innerHTML = sanitizeHtml(node.attrs.contentHtml);
|
|
||||||
return content;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const nodeSpecs = {
|
|
||||||
joplinEditableInline: makeJoplinEditableSpec(true),
|
|
||||||
joplinEditableBlock: makeJoplinEditableSpec(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditableSourceBlockView implements NodeView {
|
|
||||||
public readonly dom: HTMLElement;
|
|
||||||
public constructor(node: Node, inline: boolean) {
|
|
||||||
if ((node.attrs.contentHtml ?? undefined) === undefined) {
|
|
||||||
throw new Error(`Unable to create a SourceBlockView for a node lacking contentHtml. Node: ${node}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dom = document.createElement(inline ? 'span' : 'div');
|
|
||||||
this.dom.classList.add('joplin-editable');
|
|
||||||
this.dom.innerHTML = sanitizeHtml(node.attrs.contentHtml);
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectNode() {
|
|
||||||
this.dom.classList.add('-selected');
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectNode() {
|
|
||||||
this.dom.classList.remove('-selected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const joplinEditablePlugin = new Plugin({
|
|
||||||
props: {
|
|
||||||
nodeViews: {
|
|
||||||
joplinEditableInline: node => new EditableSourceBlockView(node, true),
|
|
||||||
joplinEditableBlock: node => new EditableSourceBlockView(node, false),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default joplinEditablePlugin;
|
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
import createTextNode from '../../utils/dom/createTextNode';
|
||||||
|
import createTextArea from '../../utils/dom/createTextArea';
|
||||||
|
|
||||||
|
interface SourceBlockData {
|
||||||
|
start: string;
|
||||||
|
content: string;
|
||||||
|
end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
editorLabel: string|Promise<string>;
|
||||||
|
doneLabel: string|Promise<string>;
|
||||||
|
block: SourceBlockData;
|
||||||
|
onSave: (newContent: SourceBlockData)=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEditorDialog = ({ editorLabel, doneLabel, block, onSave }: Options) => {
|
||||||
|
const dialog = document.createElement('dialog');
|
||||||
|
dialog.classList.add('editor-dialog', '-visible');
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
|
||||||
|
dialog.onclose = () => {
|
||||||
|
dialog.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { textArea, label: textAreaLabel } = createTextArea({
|
||||||
|
label: editorLabel,
|
||||||
|
initialContent: block.content,
|
||||||
|
onChange: (newContent) => {
|
||||||
|
block = {
|
||||||
|
...block,
|
||||||
|
content: newContent,
|
||||||
|
};
|
||||||
|
onSave(block);
|
||||||
|
},
|
||||||
|
spellCheck: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const submitButton = document.createElement('button');
|
||||||
|
submitButton.appendChild(createTextNode(doneLabel));
|
||||||
|
submitButton.classList.add('submit');
|
||||||
|
submitButton.onclick = () => {
|
||||||
|
if (dialog.close) {
|
||||||
|
dialog.close();
|
||||||
|
} else {
|
||||||
|
// .remove the dialog in browsers with limited support for
|
||||||
|
// HTMLDialogElement (and in JSDOM).
|
||||||
|
dialog.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.appendChild(textAreaLabel);
|
||||||
|
dialog.appendChild(textArea);
|
||||||
|
dialog.appendChild(submitButton);
|
||||||
|
|
||||||
|
|
||||||
|
// .showModal is not defined in JSDOM and some older (pre-2022) browsers
|
||||||
|
if (dialog.showModal) {
|
||||||
|
dialog.showModal();
|
||||||
|
} else {
|
||||||
|
dialog.classList.add('-fake-modal');
|
||||||
|
focus('createEditorDialog/legacy', textArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createEditorDialog;
|
@@ -0,0 +1,102 @@
|
|||||||
|
import { htmlentities } from '@joplin/utils/html';
|
||||||
|
import { RenderResult } from '../../../../renderer/types';
|
||||||
|
import createTestEditor from '../../testing/createTestEditor';
|
||||||
|
import joplinEditorApiPlugin, { getEditorApi, setEditorApi } from '../joplinEditorApiPlugin';
|
||||||
|
import joplinEditablePlugin from './joplinEditablePlugin';
|
||||||
|
import { Second } from '@joplin/utils/time';
|
||||||
|
|
||||||
|
const createEditor = (html: string) => {
|
||||||
|
return createTestEditor({
|
||||||
|
plugins: [joplinEditablePlugin, joplinEditorApiPlugin],
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findEditButton = (ancestor: Element): HTMLButtonElement => {
|
||||||
|
return ancestor.querySelector('.joplin-editable > button.edit');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findEditorDialog = () => {
|
||||||
|
const dialog = document.querySelector('dialog.editor-dialog');
|
||||||
|
if (!dialog) {
|
||||||
|
throw new Error('Could not find an open editor dialog.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = dialog.querySelector('textarea');
|
||||||
|
const submitButton = dialog.querySelector('button');
|
||||||
|
|
||||||
|
return {
|
||||||
|
dialog,
|
||||||
|
editor,
|
||||||
|
submitButton,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('joplinEditablePlugin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
document.body.replaceChildren();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
// Inline
|
||||||
|
'<span class="joplin-editable"><pre class="joplin-source">test</pre>rendered</span>',
|
||||||
|
// Block
|
||||||
|
'<div class="joplin-editable"><pre class="joplin-source">test</pre>rendered</div>',
|
||||||
|
// Nested inline
|
||||||
|
'<p>Test: <mark><span class="joplin-editable"><pre class="joplin-source">test</pre>rendered</span></mark></p>',
|
||||||
|
])('should show an edit button on source blocks (case %#)', (htmlSource) => {
|
||||||
|
const editor = createEditor(htmlSource);
|
||||||
|
const editButton = findEditButton(editor.dom);
|
||||||
|
expect(editButton.textContent).toBe('Edit');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the edit button should show an editor dialog', () => {
|
||||||
|
const editor = createEditor('<span class="joplin-editable"><pre class="joplin-source">test source</pre>rendered</span>');
|
||||||
|
const editButton = findEditButton(editor.dom);
|
||||||
|
editButton.click();
|
||||||
|
|
||||||
|
// Should show the dialog
|
||||||
|
const dialog = findEditorDialog();
|
||||||
|
expect(dialog.editor).toBeTruthy();
|
||||||
|
expect(dialog.submitButton).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editing the content of an editor dialog should update the source block', async () => {
|
||||||
|
const editor = createEditor('<div class="joplin-editable"><pre class="joplin-source">test source</pre>rendered</div>');
|
||||||
|
|
||||||
|
// Mock render functions:
|
||||||
|
editor.dispatch(setEditorApi(editor.state.tr, {
|
||||||
|
...getEditorApi(editor.state),
|
||||||
|
renderer: {
|
||||||
|
renderMarkupToHtml: jest.fn(async source => ({
|
||||||
|
html: `<pre class="joplin-source">${htmlentities(source)}</pre><p class="test-content">Mocked!</p></div>`,
|
||||||
|
} as RenderResult)),
|
||||||
|
renderHtmlToMarkup: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const editButton = findEditButton(editor.dom);
|
||||||
|
editButton.click();
|
||||||
|
|
||||||
|
const dialog = findEditorDialog();
|
||||||
|
dialog.editor.value = 'Updated!';
|
||||||
|
dialog.editor.dispatchEvent(new Event('input'));
|
||||||
|
|
||||||
|
// Should update the editor state with the new source immediately.
|
||||||
|
expect(editor.state.doc.toJSON()).toMatchObject({
|
||||||
|
content: [{
|
||||||
|
type: 'joplinEditableBlock',
|
||||||
|
attrs: {
|
||||||
|
source: 'Updated!',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should render and update the display within a short amount of time
|
||||||
|
await jest.advanceTimersByTimeAsync(Second);
|
||||||
|
const renderedEditable = editor.dom.querySelector('.joplin-editable');
|
||||||
|
// Should render the updated content
|
||||||
|
expect(renderedEditable.querySelector('.test-content').innerHTML).toBe('Mocked!');
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,182 @@
|
|||||||
|
import { Plugin } from 'prosemirror-state';
|
||||||
|
import { Node, NodeSpec } from 'prosemirror-model';
|
||||||
|
import { EditorView, NodeView } from 'prosemirror-view';
|
||||||
|
import sanitizeHtml from '../../utils/sanitizeHtml';
|
||||||
|
import createEditorDialog from './createEditorDialog';
|
||||||
|
import { getEditorApi } from '../joplinEditorApiPlugin';
|
||||||
|
import { msleep } from '@joplin/utils/time';
|
||||||
|
import createTextNode from '../../utils/dom/createTextNode';
|
||||||
|
import postProcessRenderedHtml from './postProcessRenderedHtml';
|
||||||
|
|
||||||
|
// See the fold example for more information about
|
||||||
|
// writing similar ProseMirror plugins:
|
||||||
|
// https://prosemirror.net/examples/fold/
|
||||||
|
|
||||||
|
|
||||||
|
const makeJoplinEditableSpec = (inline: boolean): NodeSpec => ({
|
||||||
|
group: inline ? 'inline' : 'block',
|
||||||
|
inline: inline,
|
||||||
|
draggable: true,
|
||||||
|
attrs: {
|
||||||
|
contentHtml: { default: '', validate: 'string' },
|
||||||
|
source: { default: '', validate: 'string' },
|
||||||
|
language: { default: '', validate: 'string' },
|
||||||
|
openCharacters: { default: '', validate: 'string' },
|
||||||
|
closeCharacters: { default: '', validate: 'string' },
|
||||||
|
},
|
||||||
|
parseDOM: [
|
||||||
|
{
|
||||||
|
tag: `${inline ? 'span' : 'div'}.joplin-editable`,
|
||||||
|
getAttrs: node => {
|
||||||
|
const sourceNode = node.querySelector('.joplin-source');
|
||||||
|
return {
|
||||||
|
contentHtml: node.innerHTML,
|
||||||
|
source: sourceNode?.textContent,
|
||||||
|
openCharacters: sourceNode?.getAttribute('data-joplin-source-open'),
|
||||||
|
closeCharacters: sourceNode?.getAttribute('data-joplin-source-close'),
|
||||||
|
language: sourceNode?.getAttribute('data-joplin-language'),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
toDOM: node => {
|
||||||
|
const content = document.createElement(inline ? 'span' : 'div');
|
||||||
|
content.classList.add('joplin-editable');
|
||||||
|
content.innerHTML = sanitizeHtml(node.attrs.contentHtml);
|
||||||
|
|
||||||
|
const sourceNode = content.querySelector('.joplin-source');
|
||||||
|
if (sourceNode) {
|
||||||
|
sourceNode.textContent = node.attrs.source;
|
||||||
|
sourceNode.setAttribute('data-joplin-source-open', node.attrs.openCharacters);
|
||||||
|
sourceNode.setAttribute('data-joplin-source-close', node.attrs.closeCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const nodeSpecs = {
|
||||||
|
joplinEditableInline: makeJoplinEditableSpec(true),
|
||||||
|
joplinEditableBlock: makeJoplinEditableSpec(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetPosition = ()=> number;
|
||||||
|
|
||||||
|
class EditableSourceBlockView implements NodeView {
|
||||||
|
public readonly dom: HTMLElement;
|
||||||
|
public constructor(private node: Node, inline: boolean, private view: EditorView, private getPosition: GetPosition) {
|
||||||
|
if ((node.attrs.contentHtml ?? undefined) === undefined) {
|
||||||
|
throw new Error(`Unable to create a SourceBlockView for a node lacking contentHtml. Node: ${node}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dom = document.createElement(inline ? 'span' : 'div');
|
||||||
|
this.dom.classList.add('joplin-editable');
|
||||||
|
this.updateContent_();
|
||||||
|
}
|
||||||
|
|
||||||
|
private showEditDialog_() {
|
||||||
|
const { localize: _ } = getEditorApi(this.view.state);
|
||||||
|
|
||||||
|
let saveCounter = 0;
|
||||||
|
createEditorDialog({
|
||||||
|
doneLabel: _('Done'),
|
||||||
|
editorLabel: _('Code:'),
|
||||||
|
block: {
|
||||||
|
content: this.node.attrs.source,
|
||||||
|
start: this.node.attrs.openCharacters,
|
||||||
|
end: this.node.attrs.closeCharacters,
|
||||||
|
},
|
||||||
|
onSave: async (block) => {
|
||||||
|
this.view.dispatch(
|
||||||
|
this.view.state.tr.setNodeAttribute(
|
||||||
|
this.getPosition(), 'source', block.content,
|
||||||
|
).setNodeAttribute(
|
||||||
|
this.getPosition(), 'openCharacters', block.start,
|
||||||
|
).setNodeAttribute(
|
||||||
|
this.getPosition(), 'closeCharacters', block.end,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
saveCounter ++;
|
||||||
|
const initialSaveCounter = saveCounter;
|
||||||
|
const cancelled = () => saveCounter !== initialSaveCounter;
|
||||||
|
|
||||||
|
// Debounce rendering
|
||||||
|
await msleep(400);
|
||||||
|
if (cancelled()) return;
|
||||||
|
|
||||||
|
const rendered = await getEditorApi(this.view.state).renderer.renderMarkupToHtml(
|
||||||
|
`${block.start}${block.content}${block.end}`,
|
||||||
|
{ forceMarkdown: true, isFullPageRender: false },
|
||||||
|
);
|
||||||
|
if (cancelled()) return;
|
||||||
|
|
||||||
|
const html = postProcessRenderedHtml(rendered.html, this.node.isInline);
|
||||||
|
this.view.dispatch(
|
||||||
|
this.view.state.tr.setNodeAttribute(
|
||||||
|
this.getPosition(), 'contentHtml', html,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateContent_() {
|
||||||
|
const setDomContentSafe = (html: string) => {
|
||||||
|
this.dom.innerHTML = sanitizeHtml(html);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEditButton = () => {
|
||||||
|
const editButton = document.createElement('button');
|
||||||
|
editButton.classList.add('edit');
|
||||||
|
|
||||||
|
const { localize: _ } = getEditorApi(this.view.state);
|
||||||
|
|
||||||
|
editButton.appendChild(createTextNode(_('Edit')));
|
||||||
|
editButton.onclick = (event) => {
|
||||||
|
this.showEditDialog_();
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
this.dom.appendChild(editButton);
|
||||||
|
};
|
||||||
|
|
||||||
|
setDomContentSafe(this.node.attrs.contentHtml);
|
||||||
|
postProcessRenderedHtml(this.dom, this.node.isInline);
|
||||||
|
addEditButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectNode() {
|
||||||
|
this.dom.classList.add('-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
public deselectNode() {
|
||||||
|
this.dom.classList.remove('-selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopEvent(event: Event) {
|
||||||
|
// Allow using the keyboard to activate the "edit" button:
|
||||||
|
return event.target === this.dom.querySelector('button.edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(node: Node) {
|
||||||
|
if (node.type.spec !== this.node.type.spec) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.node = node;
|
||||||
|
this.updateContent_();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const joplinEditablePlugin = new Plugin({
|
||||||
|
props: {
|
||||||
|
nodeViews: {
|
||||||
|
joplinEditableInline: (node, view, getPos) => new EditableSourceBlockView(node, true, view, getPos),
|
||||||
|
joplinEditableBlock: (node, view, getPos) => new EditableSourceBlockView(node, false, view, getPos),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default joplinEditablePlugin;
|
@@ -0,0 +1,42 @@
|
|||||||
|
// If renderedHtml is an HTMLElement, the content is modified in-place.
|
||||||
|
const postProcessRenderedHtml = <InputType extends string|HTMLElement> (renderedHtml: InputType, isInline: boolean): InputType => {
|
||||||
|
let rootElement;
|
||||||
|
if (typeof renderedHtml === 'string') {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const parsed = parser.parseFromString(
|
||||||
|
`<!DOCTYPE html><html><body>${renderedHtml}</body></html>`,
|
||||||
|
'text/html',
|
||||||
|
);
|
||||||
|
rootElement = parsed.body;
|
||||||
|
} else {
|
||||||
|
rootElement = renderedHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replaceChildMatching = (selector: string) => {
|
||||||
|
const toReplace = [...rootElement.children].find(
|
||||||
|
child => child.matches(selector),
|
||||||
|
);
|
||||||
|
toReplace?.replaceWith(...toReplace.childNodes);
|
||||||
|
};
|
||||||
|
// If the original HTML is from .renderToMarkup, it may have a <div> wrapper:
|
||||||
|
replaceChildMatching('#rendered-md');
|
||||||
|
|
||||||
|
if (rootElement.children.length === 1 && isInline) {
|
||||||
|
replaceChildMatching('p, div');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the 'joplin-editable' container if it's the only thing in the content
|
||||||
|
// (since this.dom is itself a joplin-editable)
|
||||||
|
if (rootElement.children.length === 1) {
|
||||||
|
replaceChildMatching('.joplin-editable');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match the input type
|
||||||
|
if (typeof renderedHtml === 'string') {
|
||||||
|
return rootElement.innerHTML as InputType;
|
||||||
|
} else {
|
||||||
|
return rootElement as InputType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default postProcessRenderedHtml;
|
@@ -1,10 +1,11 @@
|
|||||||
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
||||||
import { OnEventCallback } from '../../types';
|
import { OnEventCallback, OnLocalize } from '../../types';
|
||||||
import { RendererControl } from '../types';
|
import { RendererControl } from '../types';
|
||||||
|
|
||||||
export interface EditorApi {
|
export interface EditorApi {
|
||||||
renderer: RendererControl;
|
renderer: RendererControl;
|
||||||
onEvent: OnEventCallback;
|
onEvent: OnEventCallback;
|
||||||
|
localize: OnLocalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ const joplinEditorApiPlugin = new Plugin<EditorApi>({
|
|||||||
throw new Error('Not initialized');
|
throw new Error('Not initialized');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
settings: null,
|
||||||
|
localize: input => input,
|
||||||
}),
|
}),
|
||||||
apply: (tr, value) => {
|
apply: (tr, value) => {
|
||||||
const proposedValue = tr.getMeta(joplinEditorApiPlugin);
|
const proposedValue = tr.getMeta(joplinEditorApiPlugin);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { AttributeSpec, DOMOutputSpec, MarkSpec, NodeSpec, Schema } from 'prosemirror-model';
|
import { AttributeSpec, DOMOutputSpec, MarkSpec, NodeSpec, Schema } from 'prosemirror-model';
|
||||||
import { nodeSpecs as joplinEditableNodes } from './plugins/joplinEditablePlugin';
|
import { nodeSpecs as joplinEditableNodes } from './plugins/joplinEditablePlugin/joplinEditablePlugin';
|
||||||
import { tableNodes } from 'prosemirror-tables';
|
import { tableNodes } from 'prosemirror-tables';
|
||||||
import { nodeSpecs as listNodes } from './plugins/listPlugin';
|
import { nodeSpecs as listNodes } from './plugins/listPlugin';
|
||||||
import { nodeSpecs as resourcePlaceholderNodes } from './plugins/resourcePlaceholderPlugin';
|
import { nodeSpecs as resourcePlaceholderNodes } from './plugins/resourcePlaceholderPlugin';
|
||||||
|
@@ -2,7 +2,9 @@
|
|||||||
import 'prosemirror-view/style/prosemirror.css';
|
import 'prosemirror-view/style/prosemirror.css';
|
||||||
import 'prosemirror-search/style/search.css';
|
import 'prosemirror-search/style/search.css';
|
||||||
import './styles/joplin-editable.css';
|
import './styles/joplin-editable.css';
|
||||||
|
import './styles/editor-dialog.css';
|
||||||
import './styles/prosemirror-editor.css';
|
import './styles/prosemirror-editor.css';
|
||||||
import './styles/table.css';
|
import './styles/table.css';
|
||||||
import './styles/checklist-item.css';
|
import './styles/checklist-item.css';
|
||||||
import './styles/link-tooltip.css';
|
import './styles/link-tooltip.css';
|
||||||
|
|
||||||
|
30
packages/editor/ProseMirror/styles/editor-dialog.css
Normal file
30
packages/editor/ProseMirror/styles/editor-dialog.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
.editor-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--joplin-background-color);
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 0px 2px var(--joplin-color);
|
||||||
|
color: var(--joplin-color);
|
||||||
|
|
||||||
|
width: min(80vw, 600px);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-dialog > textarea {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-height: min(70vh, 400px);
|
||||||
|
resize: none;
|
||||||
|
color: var(--joplin-color);
|
||||||
|
background-color: var(--joplin-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-dialog > button {
|
||||||
|
color: var(--joplin-color);
|
||||||
|
background-color: var(--joplin-background-color3);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 38px;
|
||||||
|
}
|
@@ -1,4 +1,28 @@
|
|||||||
|
|
||||||
|
.joplin-editable {
|
||||||
|
position: relative;
|
||||||
|
/* Override the "white-space" setting for the editor */
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joplin-editable > .edit {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
transition-duration: 0.2s;
|
||||||
|
transition-property: opacity, display;
|
||||||
|
}
|
||||||
|
|
||||||
.joplin-editable.-selected {
|
.joplin-editable.-selected {
|
||||||
outline: 4px solid var(--joplin-text-selection-color);
|
outline: 4px solid var(--joplin-text-selection-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.joplin-editable.-selected > .edit {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { RenderResult } from '../../renderer/types';
|
import { RenderResult } from '../../renderer/types';
|
||||||
|
|
||||||
export type MarkupToHtml = (markup: string)=> Promise<RenderResult>;
|
interface MarkupToHtmlOptions {
|
||||||
|
isFullPageRender: boolean;
|
||||||
|
forceMarkdown: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkupToHtml = (markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||||
export type HtmlToMarkup = (html: Node|DocumentFragment)=> string;
|
export type HtmlToMarkup = (html: Node|DocumentFragment)=> string;
|
||||||
|
|
||||||
export interface RendererControl {
|
export interface RendererControl {
|
||||||
|
31
packages/editor/ProseMirror/utils/dom/createTextArea.ts
Normal file
31
packages/editor/ProseMirror/utils/dom/createTextArea.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { LocalizationResult } from '../../../types';
|
||||||
|
import createTextNode from './createTextNode';
|
||||||
|
import createUniqueId from './createUniqueId';
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
label: LocalizationResult;
|
||||||
|
spellCheck: boolean;
|
||||||
|
initialContent: string;
|
||||||
|
onChange: (newContent: string)=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createTextArea = ({ label, initialContent, spellCheck, onChange }: Options) => {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.spellcheck = spellCheck;
|
||||||
|
textArea.oninput = () => {
|
||||||
|
onChange(textArea.value);
|
||||||
|
};
|
||||||
|
textArea.value = initialContent;
|
||||||
|
textArea.id = createUniqueId();
|
||||||
|
|
||||||
|
const labelElement = document.createElement('label');
|
||||||
|
labelElement.htmlFor = textArea.id;
|
||||||
|
labelElement.appendChild(createTextNode(label));
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: labelElement,
|
||||||
|
textArea,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createTextArea;
|
11
packages/editor/ProseMirror/utils/dom/createTextNode.ts
Normal file
11
packages/editor/ProseMirror/utils/dom/createTextNode.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { LocalizationResult } from '../../../types';
|
||||||
|
|
||||||
|
const createTextNode = (content: LocalizationResult) => {
|
||||||
|
const result = document.createTextNode(typeof content === 'string' ? content : '...');
|
||||||
|
void (async () => {
|
||||||
|
result.textContent = await content;
|
||||||
|
})();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createTextNode;
|
8
packages/editor/ProseMirror/utils/dom/createUniqueId.ts
Normal file
8
packages/editor/ProseMirror/utils/dom/createUniqueId.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
let idCounter = 0;
|
||||||
|
|
||||||
|
const createUniqueId = () => {
|
||||||
|
return `__joplin-id-${idCounter++}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createUniqueId;
|
@@ -190,6 +190,8 @@ export type LogMessageCallback = (message: string)=> void;
|
|||||||
export type OnEventCallback = (event: EditorEvent)=> void;
|
export type OnEventCallback = (event: EditorEvent)=> void;
|
||||||
export type PasteFileCallback = (data: File)=> Promise<void>;
|
export type PasteFileCallback = (data: File)=> Promise<void>;
|
||||||
type OnScrollPastBeginningCallback = ()=> void;
|
type OnScrollPastBeginningCallback = ()=> void;
|
||||||
|
export type LocalizationResult = Promise<string>|string;
|
||||||
|
export type OnLocalize = (input: string)=> LocalizationResult;
|
||||||
|
|
||||||
interface Localisations {
|
interface Localisations {
|
||||||
[editorString: string]: string;
|
[editorString: string]: string;
|
||||||
@@ -199,6 +201,7 @@ export interface EditorProps {
|
|||||||
settings: EditorSettings;
|
settings: EditorSettings;
|
||||||
initialText: string;
|
initialText: string;
|
||||||
initialNoteId: string;
|
initialNoteId: string;
|
||||||
|
onLocalize: OnLocalize;
|
||||||
// Used mostly for internal editor library strings
|
// Used mostly for internal editor library strings
|
||||||
localisations?: Localisations;
|
localisations?: Localisations;
|
||||||
|
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import * as React from 'react';
|
import type * as React from 'react';
|
||||||
import { NoteEntity, ResourceEntity } from './services/database/types';
|
import type { NoteEntity, ResourceEntity } from './services/database/types';
|
||||||
import type FsDriverBase from './fs-driver-base';
|
import type FsDriverBase from './fs-driver-base';
|
||||||
import type FileApiDriverLocal from './file-api-driver-local';
|
import type FileApiDriverLocal from './file-api-driver-local';
|
||||||
import { Crypto } from './services/e2ee/types';
|
import type { Crypto } from './services/e2ee/types';
|
||||||
import { MarkupLanguage } from '@joplin/renderer';
|
import type { MarkupLanguage } from '@joplin/renderer';
|
||||||
|
|
||||||
export interface CreateResourceFromPathOptions {
|
export interface CreateResourceFromPathOptions {
|
||||||
resizeLargeImages?: 'always' | 'never' | 'ask';
|
resizeLargeImages?: 'always' | 'never' | 'ask';
|
||||||
|
Reference in New Issue
Block a user