Skip to content

Commit

Permalink
Can set CSS classes on <p> and <table> #8782
Browse files Browse the repository at this point in the history
  • Loading branch information
PowerKiKi committed Mar 21, 2022
1 parent 8727a67 commit 7c2feb0
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 153 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"navigator",
"sessionStorage",
"window"
]
],
"prefer-const": ["error"]
}
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h2 i18n mat-dialog-title>Sélectionner une couleur</h2>
<h2 i18n mat-dialog-title>Saisir les classes CSS</h2>

<mat-dialog-content [formGroup]="form">
<mat-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,4 @@ mat-dialog-content {
width: 70vw;
max-width: 30em;
display: grid;
justify-content: center;
row-gap: 25px;
}

$size: 25px;
$margin: 3px;
.class {
display: inline-block;
width: $size;
height: $size;
margin: $margin;
cursor: pointer;

// Make the square bigger
&:hover {
padding: $margin;
margin: 0;
}
}

$sizeSample: 27px;
.sample {
width: $sizeSample;
height: $sizeSample;
}
19 changes: 19 additions & 0 deletions projects/natural-editor/src/lib/editor/editor.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@
i18n
>Titre 6
</button>

<mat-divider></mat-divider>

<button
mat-menu-item
*ngIf="menu.paragraphClass"
[disabled]="menu.paragraphClass.disabled"
(click)="run($event, 'paragraphClass')"
i18n
>Classe...
</button>
</mat-menu>

<button mat-button [matMenuTriggerFor]="tableMenu" *ngIf="menu.addColumnBefore">
Expand Down Expand Up @@ -168,6 +179,14 @@
i18n
>Couleur de fond...
</button>
<button
mat-menu-item
*ngIf="menu.tableClass"
[disabled]="menu.tableClass.disabled"
(click)="run($event, 'tableClass')"
i18n
>Classe...
</button>

<mat-divider></mat-divider>

Expand Down
48 changes: 40 additions & 8 deletions projects/natural-editor/src/lib/editor/editor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const html = `<h1>h1</h1>
<p style="text-align: right;">right aligned</p>
<p><strong>strong</strong></p>
<p><em>em</em></p>
<p><a href="a" title="a">a</a></p>
<table><tbody><tr><td style="background-color: #168253;"><p>table</p></td></tr></tbody></table>
<p class="my-paragraph-class"><a href="a" title="a">a</a></p>
<table class="my-table-class"><tbody><tr><td style="background-color: #168253;"><p>table</p></td></tr></tbody></table>
<p><img alt="foo" src="some url"></p>
<ul><li><p>ul</p></li></ul>
<ol><li><p>ol</p></li></ol>
Expand Down Expand Up @@ -76,10 +76,27 @@ describe('NaturalEditorComponent', () => {
hostComponent.myValue = html;
fixture.detectChanges();

// TODO this should contain `class="my-table-class"` somewhere but somehow it doesn't even though it does appear correctly in the demo app
const expected = `<h1>h1</h1>
<h2>h1</h2>
<h3>h3</h3>
<h4>h4</h4>
<h5>h5</h5>
<h6>h6</h6>
<pre><code>code</code></pre>
<p>p"foo</p>
<p style="text-align: right;">right aligned</p>
<p><strong>strong</strong></p>
<p><em>em</em></p>
<p class="my-paragraph-class"><a href="a" title="a">a</a></p>
<div class="tableWrapper"><table style="min-width: 25px;"><colgroup><col></colgroup><tbody><tr><td style="background-color: rgb(22, 130, 83);"><p>table</p></td></tr></tbody></table></div>
<p><img src="some url" alt="foo" contenteditable="false" draggable="true"><img class="ProseMirror-separator"><br class="ProseMirror-trailingBreak"></p>
<ul><li><p>ul</p></li></ul>
<ol><li><p>ol</p></li></ol>
<blockquote><p>blockquote</p></blockquote>`.replace(/\n/g, '');

setTimeout(() => {
expect(getProsemirrorContent(fixture)).toBe(
'<h1>h1</h1><h2>h1</h2><h3>h3</h3><h4>h4</h4><h5>h5</h5><h6>h6</h6><pre><code>code</code></pre><p>p"foo</p><p style="text-align: right;">right aligned</p><p><strong>strong</strong></p><p><em>em</em></p><p><a href="a" title="a">a</a></p><div class="tableWrapper"><table style="min-width: 25px;"><colgroup><col></colgroup><tbody><tr><td style="background-color: rgb(22, 130, 83);"><p>table</p></td></tr></tbody></table></div><p><img src="some url" alt="foo" contenteditable="false" draggable="true"><img class="ProseMirror-separator"><br class="ProseMirror-trailingBreak"></p><ul><li><p>ul</p></li></ul><ol><li><p>ol</p></li></ol><blockquote><p>blockquote</p></blockquote>',
);
expect(getProsemirrorContent(fixture)).toBe(expected);
done();
});
});
Expand All @@ -99,11 +116,26 @@ describe('NaturalEditorComponent', () => {

hostComponent.myValue = html;
fixture.detectChanges();
const expected = `<h1>h1</h1>
<h2>h1</h2>
<h3>h3</h3>
<h4>h4</h4>
<h5>h5</h5>
<h6>h6</h6>
<h1>code</h1>
<p>p"foo</p>
<p>right aligned</p>
<p><strong>strong</strong></p>
<p><em>em</em></p>
<p><a href="a" title="a">a</a></p>
<p>table</p>
<p><br class="ProseMirror-trailingBreak"></p>
<ul><li><p>ul</p></li></ul>
<ol><li><p>ol</p></li></ol>
<p>blockquote</p>`.replace(/\n/g, '');

setTimeout(() => {
expect(getProsemirrorContent(fixture)).toBe(
'<h1>h1</h1><h2>h1</h2><h3>h3</h3><h4>h4</h4><h5>h5</h5><h6>h6</h6><h1>code</h1><p>p"foo</p><p>right aligned</p><p><strong>strong</strong></p><p><em>em</em></p><p><a href="a" title="a">a</a></p><p>table</p><p><br class="ProseMirror-trailingBreak"></p><ul><li><p>ul</p></li></ul><ol><li><p>ol</p></li></ol><p>blockquote</p>',
);
expect(getProsemirrorContent(fixture)).toBe(expected);
done();
});
});
Expand Down
87 changes: 39 additions & 48 deletions projects/natural-editor/src/lib/utils/items/class-item.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import {Node, NodeType, Schema} from 'prosemirror-model';
import {AllSelection, TextSelection, Transaction} from 'prosemirror-state';
import {Node, NodeType} from 'prosemirror-model';
import {AllSelection, EditorState, TextSelection, Transaction} from 'prosemirror-state';
import {Item} from './item';
import {MatDialog} from '@angular/material/dialog';
import {ClassDialogComponent, ClassDialogData} from '../../class-dialog/class-dialog.component';

type Alignment = 'left' | 'right' | 'center' | 'justify';

function setTextAlign(tr: Transaction, schema: Schema, alignment: null | Alignment): Transaction {
function setClass(tr: Transaction, classValue: string, allowedNodeType: NodeType): Transaction {
const {selection, doc} = tr;
if (!selection || !doc) {
return tr;
}
const {from, to} = selection;
const {nodes} = schema;

const tasks: {
node: Node;
pos: number;
nodeType: NodeType;
}[] = [];

alignment = alignment || null;

const allowedNodeTypes = new Set([
nodes.paragraph,
// nodes['blockquote'],
// nodes['listItem'],
// nodes['heading'],
]);

doc.nodesBetween(from, to, (node, pos) => {
const nodeType = node.type;
const align = node.attrs.align || null;
if (align !== alignment && allowedNodeTypes.has(nodeType)) {
const currentClass = node.attrs.class || null;
if (currentClass !== classValue && allowedNodeType === nodeType) {
tasks.push({
node,
pos,
Expand All @@ -50,66 +38,69 @@ function setTextAlign(tr: Transaction, schema: Schema, alignment: null | Alignme
const {node, pos, nodeType} = job;
const newAttrs = {
...node.attrs,
align: alignment ? alignment : null,
class: classValue ? classValue : null,
};
console.log('newAttrs', newAttrs);

tr = tr.setNodeMarkup(pos, nodeType, newAttrs, node.marks);
});

return tr;
}

/**
* Returns the first `class` attribute that is non-empty in the selection.
* If not found, return empty string.
*/
function findFirstClassInSelection(state: EditorState): string {
const {selection, doc} = state;
const {from, to} = selection;
let keepLooking = true;
let foundClass: string = '';

doc.nodesBetween(from, to, node => {
if (keepLooking && node.attrs.class) {
keepLooking = false;
foundClass = node.attrs.class;
}

return keepLooking;
});

return foundClass;
}

export class ClassItem extends Item {
public constructor(dialog: MatDialog) {
public constructor(dialog: MatDialog, nodeType: NodeType) {
super({
active: state => {
const {selection, doc} = state;
const {from, to} = selection;
let keepLooking = true;
let active = false;
doc.nodesBetween(from, to, node => {
if (keepLooking && node.attrs.align === alignment) {
keepLooking = false;
active = true;
}
return keepLooking;
});

return active;
return !!findFirstClassInSelection(state);
},

enable: state => {
const {selection} = state;
return selection instanceof TextSelection || selection instanceof AllSelection;
},

run: (state, dispatch): boolean => {
run: (state, dispatch, view): void => {
dialog
.open<ClassDialogComponent, ClassDialogData, ClassDialogData>(ClassDialogComponent, {
data: {
class: '',
class: findFirstClassInSelection(state),
},
})
.afterClosed()
.subscribe(result => {
if (dispatch && result) {
const cmd = setCellBackgroundColor(result.class);
cmd(state, dispatch);
const {selection} = state;

const tr = setClass(state.tr.setSelection(selection), result.class, nodeType);
if (tr.docChanged) {
dispatch?.(tr);
}
}

view.focus();
});

const {schema, selection} = state;

console.log(this);
const tr = setTextAlign(state.tr.setSelection(selection), schema, this.active ? null : alignment);
if (tr.docChanged) {
dispatch?.(tr);
return true;
} else {
return false;
}
},
});
}
Expand Down
80 changes: 80 additions & 0 deletions projects/natural-editor/src/lib/utils/items/table-item.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {EditorState, TextSelection, Transaction} from 'prosemirror-state';
import {Fragment, Node as ProsemirrorNode, NodeType} from 'prosemirror-model';
import {tableNodeTypes} from 'prosemirror-tables';
import {Item} from './item';

function createCell(
cellType: NodeType,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
): ProsemirrorNode<any> | null | undefined {
return cellContent ? cellType.createChecked(null, cellContent) : cellType.createAndFill();
}

function createTable(
state: EditorState,
rowsCount: number,
colsCount: number,
withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
): ProsemirrorNode {
const types = tableNodeTypes(state.schema);
const headerCells = [];
const cells = [];

for (let index = 0; index < colsCount; index += 1) {
const cell = createCell(types.cell, cellContent);

if (cell) {
cells.push(cell);
}

if (withHeaderRow) {
const headerCell = createCell(types.header_cell, cellContent);

if (headerCell) {
headerCells.push(headerCell);
}
}
}

const rows = [];

for (let index = 0; index < rowsCount; index += 1) {
rows.push(types.row.createChecked(null, withHeaderRow && index === 0 ? headerCells : cells));
}

return types.table.createChecked(null, rows);
}

function addTable(
state: EditorState,
dispatch?: (tr: Transaction) => void,
{
rowsCount = 3,
colsCount = 3,
withHeaderRow = true,
cellContent,
}: {
rowsCount?: number;
colsCount?: number;
withHeaderRow?: boolean;
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>;
} = {},
): void {
const offset = state.tr.selection.anchor + 1;

const nodes = createTable(state, rowsCount, colsCount, withHeaderRow, cellContent);
const tr = state.tr.replaceSelectionWith(nodes).scrollIntoView();
const resolvedPos = tr.doc.resolve(offset);

// move cursor into table
tr.setSelection(TextSelection.near(resolvedPos));

dispatch?.(tr);
}

export class AddTableItem extends Item {
public constructor() {
super({run: (editor, tr) => addTable(editor, tr)});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function setTextAlign(tr: Transaction, schema: Schema, alignment: null | Alignme
...node.attrs,
align: alignment ? alignment : null,
};
console.log('newAttrs', newAttrs);

tr = tr.setNodeMarkup(pos, nodeType, newAttrs, node.marks);
});

Expand Down
Loading

0 comments on commit 7c2feb0

Please sign in to comment.