1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Laurent Cozic
b519d55abf Merge branch 'dev' into table_editor 2022-06-14 00:43:23 +01:00
Laurent Cozic
5a862443d8 Merge branch 'dev' into table_editor 2022-06-07 18:30:40 +01:00
Laurent
30e191663d Merge branch 'dev' into table_editor 2022-04-17 12:42:46 +01:00
Laurent
70cd2395fb Merge branch 'dev' into table_editor 2022-04-12 23:31:35 +01:00
Laurent
2f1b6fbee1 Merge branch 'dev' into table_editor 2022-04-12 15:25:01 +01:00
Laurent
8c0d4a0f71 Merge branch 'dev' into table_editor 2022-04-11 20:04:24 +01:00
Laurent
bc08c6dcc3 Merge branch 'dev' into table_editor 2022-04-05 19:17:37 +01:00
Laurent Cozic
a06365039d table editor init 2022-03-07 17:17:41 +00:00
13 changed files with 374 additions and 6 deletions

View File

@@ -421,6 +421,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -688,6 +691,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map

6
.gitignore vendored
View File

@@ -411,6 +411,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
@@ -678,6 +681,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
packages/app-desktop/gui/SyncWizard/Dialog.js
packages/app-desktop/gui/SyncWizard/Dialog.js.map
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
packages/app-desktop/gui/TableEditorDialog/Dialog.js
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
packages/app-desktop/gui/TagList.d.ts
packages/app-desktop/gui/TagList.js
packages/app-desktop/gui/TagList.js.map

View File

@@ -38,6 +38,7 @@ import ErrorBoundary from '../../../ErrorBoundary';
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
import eventManager from '@joplin/lib/eventManager';
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
import { checkTableIsUnderCursor, readTableAroundCursor } from './utils/tables';
const menuUtils = new MenuUtils(CommandService.instance());
@@ -753,7 +754,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const menu = new Menu();
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
const cm = editorRef.current;
const hasSelectedText = cm && !!cm.getSelection() ;
const tableIsUnderCursor = checkTableIsUnderCursor(cm);
let tableUnderCursor: string = null;
if (tableIsUnderCursor) tableUnderCursor = readTableAroundCursor(cm);
menu.append(
new MenuItem({
@@ -785,6 +793,27 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
})
);
if (tableUnderCursor) {
menu.append(
new MenuItem({ type: 'separator' })
);
menu.append(
new MenuItem({
label: _('Edit table...'),
click: async () => {
props.dispatch({
type: 'DIALOG_OPEN',
name: 'tableEditor',
props: {
markdownTable: tableUnderCursor,
},
});
},
})
);
}
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
for (const item of spellCheckerMenuItems) {

View File

@@ -0,0 +1,48 @@
function findElementWithClass(element: any, className: string): any {
if (element.classList && element.classList.contains(className)) return element;
for (const child of element.childNodes) {
const hasClass = findElementWithClass(child, className);
if (hasClass) return hasClass;
}
return null;
}
export const checkTableIsUnderCursor = (cm: any) => {
if (!cm) return false;
const coords = cm.cursorCoords(cm.getCursor());
const element = document.elementFromPoint(coords.left, coords.top);
if (!element) return false;
return !!findElementWithClass(element, 'cm-jn-table-item');
};
export const readTableAroundCursor = (cm: any) => {
const idxAtCursor = cm.doc.getCursor().line;
const lineCount = cm.lineCount();
const lines: string[] = [];
for (let i = idxAtCursor - 1; i >= 0; i--) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.splice(0, 0, line);
} else {
break;
}
}
lines.push(cm.doc.getLine(idxAtCursor));
for (let i = idxAtCursor + 1; i < lineCount; i++) {
const line: string = cm.doc.getLine(i);
if (line.startsWith('|')) {
lines.push(line);
} else {
break;
}
}
return lines.join('\n');
};

View File

@@ -165,6 +165,7 @@ export default function useJoplinMode(CodeMirror: any) {
}
if (isMonospace) { token = `${token} jn-monospace`; }
if (state.inTable) { token = `${token} jn-table-item`; }
// //////// End Monospace //////////
return token;

View File

@@ -22,6 +22,7 @@ import Dialog from './Dialog';
import SyncWizardDialog from './SyncWizard/Dialog';
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
import EditFolderDialog from './EditFolderDialog/Dialog';
import TableEditorDialog from './TableEditorDialog/Dialog';
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
const { ImportScreen } = require('./ImportScreen.min.js');
const { ResourceScreen } = require('./ResourceScreen.js');
@@ -38,6 +39,7 @@ interface Props {
zoomFactor: number;
needApiAuth: boolean;
dialogs: AppStateDialog[];
dialogContentMaxSize: Size;
}
interface ModalDialogProps {
@@ -51,6 +53,7 @@ interface RegisteredDialogProps {
themeId: number;
key: string;
dispatch: Function;
dialogContentMaxSize: Size;
}
interface RegisteredDialog {
@@ -60,19 +63,25 @@ interface RegisteredDialog {
const registeredDialogs: Record<string, RegisteredDialog> = {
syncWizard: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
masterPassword: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
editFolder: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
return <EditFolderDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
tableEditor: {
render: (props: RegisteredDialogProps, customProps: any) => {
return <TableEditorDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
},
},
};
@@ -195,10 +204,12 @@ class RootComponent extends React.Component<Props, any> {
for (const dialog of props.dialogs) {
const md = registeredDialogs[dialog.name];
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
output.push(md.render({
key: dialog.name,
themeId: props.themeId,
dispatch: props.dispatch,
dialogContentMaxSize: props.dialogContentMaxSize,
}, dialog.props));
}
return output;
@@ -245,6 +256,11 @@ const mapStateToProps = (state: AppState) => {
themeId: state.settings.theme,
needApiAuth: state.needApiAuth,
dialogs: state.dialogs,
dialogContentMaxSize: {
// Minus padding, margins and dialog header and button bar.
width: state.windowContentSize.width - 36 * 2,
height: state.windowContentSize.height - 36 * 2 - 28 - 30 - 20,
},
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
};
};

View File

@@ -0,0 +1,101 @@
import * as React from 'react';
import { useCallback, useEffect } from 'react';
import { _ } from '@joplin/lib/locale';
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
import Dialog from '../Dialog';
import DialogTitle from '../DialogTitle';
import { parseMarkdownTable } from '../../../lib/markdownUtils';
import { Size } from '../ResizableLayout/utils/types';
interface Props {
themeId: number;
dispatch: Function;
markdownTable: string;
dialogContentMaxSize: Size;
}
const markdownTableToObject = (markdownTable: string): any => {
const table = parseMarkdownTable(markdownTable);
return {
columns: table.headers.map(h => {
return {
title: h.label,
field: h.name,
hozAlign: h.justify,
editor: 'input',
};
}),
data: table.rows.map(row => {
return {
...row,
};
}),
};
};
export default function(props: Props) {
const elementId = `tabulator_${Math.floor(Math.random() * 1000000)}`;
const onClose = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'tableEditor',
});
}, [props.dispatch]);
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
if (event.buttonName === 'cancel') {
onClose();
return;
}
if (event.buttonName === 'ok') {
return;
}
}, [onClose]);
useEffect(() => {
const table = markdownTableToObject(props.markdownTable);
const Tabulator = (window as any).Tabulator;
// TODO: probably doesn't need to be called every time
// TODO: Load CSS/JS dynamically?
// TODO: Clean up on exit
Tabulator.extendModule('edit', 'editors', {});
new Tabulator(`#${elementId}`, {
...table,
height: props.dialogContentMaxSize.height,
});
}, []);
function renderContent() {
return (
<div className="dialog-content">
<div id={elementId}></div>
</div>
);
}
function renderDialogWrapper() {
return (
<div className="dialog-root">
<DialogTitle title={_('Edit table')}/>
{renderContent()}
<DialogButtonRow
themeId={props.themeId}
onClick={onButtonRowClick}
okButtonLabel={_('Save')}
/>
</div>
);
}
return (
<Dialog onClose={onClose} renderContent={renderDialogWrapper}/>
);
}

View File

@@ -7,6 +7,10 @@
uses 'eval'.
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
-->
<!--
To add files below, then need to be in the "vendor" directory. To make this happen, use copyApplicationAssets.js
-->
<title>Joplin</title>
<link rel="stylesheet" href="style.min.css">
<link rel="stylesheet" href="style/icons/style.css">
@@ -15,6 +19,9 @@
<link rel="stylesheet" href="vendor/lib/smalltalk/css/smalltalk.css">
<link rel="stylesheet" href="vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css">
<link rel="stylesheet" href="vendor/lib/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="vendor/lib/tabulator-tables/dist/css/tabulator.min.css">
<script type="text/javascript" src="vendor/lib/tabulator-tables/dist/js/tabulator.min.js"></script>
<style>
.smalltalk {

View File

@@ -174,6 +174,7 @@
"styled-components": "5.1.1",
"styled-system": "5.1.5",
"taboverride": "^4.0.3",
"tabulator-tables": "^5.1.4",
"tinymce": "^5.2.0"
}
}

View File

@@ -83,6 +83,8 @@ async function main() {
'codemirror/addon/dialog/dialog.css',
'@joeattardi/emoji-button/dist/index.js',
'mark.js/dist/mark.min.js',
'tabulator-tables/dist/css/tabulator.min.css',
'tabulator-tables/dist/js/tabulator.min.js',
{
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,

View File

@@ -1,4 +1,4 @@
import markdownUtils from './markdownUtils';
import markdownUtils, { parseMarkdownTable } from './markdownUtils';
describe('Should detect list items', () => {
test('should detect `- lorem ipsum` as list item ', () => {
@@ -91,4 +91,51 @@ describe('Should detect list items', () => {
expect(markdownUtils.isEmptyListItem('+ [x]')).toBe(false);
});
test('should parse a Markdown table', () => {
const table = parseMarkdownTable(`
| Name | Town | Comment |
|----------:|:-------:|----|
| John | London | None |
| Paul | Liverpool | **test bold** |
| Ringo | Sheffield | <a href="#">link</a> ![](https://image.com/img.png) |
`.trim().split('\n').map(l => l.trim()).join('\n'));
expect(table).toEqual({
'headers': [
{
'label': 'Name',
'name': 'c0',
'justify': 'right',
},
{
'label': 'Town',
'name': 'c1',
'justify': 'center',
},
{
'label': 'Comment',
'name': 'c2',
'justify': 'left',
},
],
'rows': [
{
'c0': 'John',
'c1': 'London',
'c2': 'None',
},
{
'c0': 'Paul',
'c1': 'Liverpool',
'c2': '**test bold**',
},
{
'c0': 'Ringo',
'c1': 'Sheffield',
'c2': '<a href="#">link</a> ![](https://image.com/img.png)',
},
],
});
});
});

View File

@@ -10,7 +10,7 @@ const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s+)$/;
export enum MarkdownTableJustify {
Left = 'left',
Center = 'center',
Right = 'right,',
Right = 'right',
}
export interface MarkdownTableHeader {
@@ -25,6 +25,11 @@ export interface MarkdownTableRow {
[key: string]: string;
}
export interface MarkdownTable {
headers: MarkdownTableHeader[];
rows: MarkdownTableRow[];
}
const markdownUtils = {
// Titles for markdown links only need escaping for [ and ]
escapeTitleText(text: string) {
@@ -206,4 +211,95 @@ const markdownUtils = {
},
};
export const parseMarkdownTable = (tableMarkdown: string): MarkdownTable => {
interface Token {
type: string;
content: string;
attrGet: (name: string)=> string;
}
const getJustifyFromStyle = (token: Token): MarkdownTableJustify => {
const style = token.attrGet('style');
if (!style) return MarkdownTableJustify.Left;
if (style.includes('text-align:right')) return MarkdownTableJustify.Right;
if (style.includes('text-align:left')) return MarkdownTableJustify.Left;
if (style.includes('text-align:center')) return MarkdownTableJustify.Center;
return MarkdownTableJustify.Left;
};
const env = {};
const markdownIt = new MarkdownIt();
const tokens: Token[] = markdownIt.parse(tableMarkdown, env);
const headers: MarkdownTableHeader[] = [];
const rows: MarkdownTableRow[] = [];
let state = 'start';
let headerIndex = 0;
let rowIndex = -1;
for (const token of tokens) {
if (state === 'start') {
if (token.type !== 'table_open') {
throw new Error('Expected table_open token');
} else {
state = 'open';
}
continue;
}
if (token.type === 'thead_open') {
state = 'header';
continue;
}
if (state === 'header') {
if (token.type === 'th_open') {
headers.push({
label: '',
name: `c${headerIndex}`,
justify: getJustifyFromStyle(token),
});
}
if (token.type === 'inline') {
headers[headerIndex].label += token.content;
}
if (token.type === 'th_close') {
headerIndex++;
}
if (token.type === 'thead_close') {
state = 'content';
}
continue;
}
if (state === 'content') {
if (token.type === 'tr_open') {
state = 'row';
rows.push({});
rowIndex++;
headerIndex = 0;
}
continue;
}
if (state === 'row') {
if (token.type === 'inline') {
rows[rowIndex][`c${headerIndex}`] = token.content;
headerIndex++;
}
if (token.type === 'tr_close') {
state = 'content';
}
continue;
}
}
return { headers, rows };
};
export default markdownUtils;

View File

@@ -3298,6 +3298,7 @@ __metadata:
styled-components: 5.1.1
styled-system: 5.1.5
taboverride: ^4.0.3
tabulator-tables: ^5.1.4
tinymce: ^5.2.0
typescript: 4.0.5
dependenciesMeta:
@@ -29096,6 +29097,13 @@ __metadata:
languageName: node
linkType: hard
"tabulator-tables@npm:^5.1.4":
version: 5.1.4
resolution: "tabulator-tables@npm:5.1.4"
checksum: f77f9e975502253ec945d66d5d2f1fb615743824a05c6e8b67013c8f53976edb3c734ef1286c24253d85dd3990e596e3f04ecc1fbd0682541b5ed1a5a6c0d762
languageName: node
linkType: hard
"taketalk@npm:^1.0.0":
version: 1.0.0
resolution: "taketalk@npm:1.0.0"