Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy command #140

Merged
merged 22 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion addon/commands/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ export default abstract class Command<A extends unknown[] = unknown[], R = void>
abstract name: string;
protected model: Model;
protected logger: Logger;
createSnapshot: boolean;

protected constructor(model: Model) {
protected constructor(model: Model, createSnapshot = true) {
this.model = model;
this.createSnapshot = createSnapshot;
this.logger = createLogger(`command:${this.constructor.name}`);
}

Expand Down
80 changes: 3 additions & 77 deletions addon/commands/delete-selection-command.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,10 @@
import Command from "@lblod/ember-rdfa-editor/commands/command";
import SelectionCommand from "@lblod/ember-rdfa-editor/commands/selection-command";
import Model from "@lblod/ember-rdfa-editor/model/model";
import ModelSelection from "@lblod/ember-rdfa-editor/model/model-selection";
import {
ImpossibleModelStateError,
MisbehavedSelectionError
} from "@lblod/ember-rdfa-editor/utils/errors";
import ModelTreeWalker from "@lblod/ember-rdfa-editor/model/util/model-tree-walker";
import ModelNode from "@lblod/ember-rdfa-editor/model/model-node";
import ModelElement from "@lblod/ember-rdfa-editor/model/model-element";
import ModelRange from "@lblod/ember-rdfa-editor/model/model-range";
import ModelPosition from "@lblod/ember-rdfa-editor/model/model-position";
import ModelNodeUtils from "@lblod/ember-rdfa-editor/model/util/model-node-utils";

export default class DeleteSelectionCommand extends Command<unknown[], ModelNode[]> {
export default class DeleteSelectionCommand extends SelectionCommand {
name = "delete-selection";

constructor(model: Model) {
super(model);
}

execute(selection: ModelSelection = this.model.selection): ModelNode[] {
if (!ModelSelection.isWellBehaved(selection)) {
throw new MisbehavedSelectionError();
}

let modelNodes: ModelNode[] = [];
const range = selection.lastRange;
let commonAncestor = range.getCommonAncestor();

if (ModelNodeUtils.isListContainer(commonAncestor)
|| (ModelNodeUtils.isListElement(commonAncestor) && this.isElementFullySelected(commonAncestor, range))
) {
const newAncestor = ModelNodeUtils.findAncestor(commonAncestor, node => !ModelNodeUtils.isListContainer(node));
if (!newAncestor || !ModelElement.isModelElement(newAncestor)) {
throw new ImpossibleModelStateError("No ancestor found that is not list container.");
}

commonAncestor = newAncestor;
}

this.model.change(mutator => {
let contentRange = mutator.splitRangeUntilElements(range, commonAncestor, commonAncestor);
let treeWalker = new ModelTreeWalker({
range: contentRange,
descend: false
});

// Check if selection is inside table cell. If this is the case, cut children of said cell.
// Assumption: if table cell is selected, no other nodes at the same level can be selected.
const firstModelNode = treeWalker.currentNode;
if (ModelNodeUtils.isTableCell(firstModelNode)) {
contentRange = range;
treeWalker = new ModelTreeWalker({
range: contentRange,
descend: false
});
}

modelNodes = [...treeWalker];
selection.selectRange(mutator.insertNodes(contentRange));
});

return modelNodes;
}

isElementFullySelected(element: ModelElement, range: ModelRange): boolean {
let startPosition = range.start;
while (startPosition.parent !== element && startPosition.parentOffset === 0) {
startPosition = ModelPosition.fromBeforeNode(startPosition.parent);
}

if (startPosition.parentOffset !== 0) {
return false;
}

let endPosition = range.end;
while (endPosition.parent !== element && endPosition.parentOffset === endPosition.parent.getMaxOffset()) {
endPosition = ModelPosition.fromAfterNode(endPosition.parent);
}

return endPosition.parentOffset === endPosition.parent.getMaxOffset();
super(model, true);
}
}
10 changes: 10 additions & 0 deletions addon/commands/read-selection-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SelectionCommand from "@lblod/ember-rdfa-editor/commands/selection-command";
import Model from "@lblod/ember-rdfa-editor/model/model";

export default class ReadSelectionCommand extends SelectionCommand {
name = "read-selection";

constructor(model: Model) {
super(model, false);
}
}
113 changes: 113 additions & 0 deletions addon/commands/selection-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import Command from "@lblod/ember-rdfa-editor/commands/command";
import ModelNode from "@lblod/ember-rdfa-editor/model/model-node";
import Model from "@lblod/ember-rdfa-editor/model/model";
import ModelSelection from "@lblod/ember-rdfa-editor/model/model-selection";
import {ImpossibleModelStateError, MisbehavedSelectionError} from "@lblod/ember-rdfa-editor/utils/errors";
import ModelNodeUtils from "@lblod/ember-rdfa-editor/model/util/model-node-utils";
import ModelTreeWalker from "@lblod/ember-rdfa-editor/model/util/model-tree-walker";
import ModelElement from "@lblod/ember-rdfa-editor/model/model-element";
import ModelRange from "@lblod/ember-rdfa-editor/model/model-range";
import ModelPosition from "@lblod/ember-rdfa-editor/model/model-position";
import SimplifiedModel from "@lblod/ember-rdfa-editor/model/simplified-model";

/**
* The core purpose of this command is to return a valid html structure that best represents
* the selection. It splits where necessary to achieve this, but restores the
* model by default.
* Optionally, it can also delete the selected content before returning it.
*/
export default abstract class SelectionCommand extends Command<unknown[], ModelNode[]> {
protected deleteSelection: boolean;

protected constructor(model: Model, createSnapshot: boolean) {
super(model, createSnapshot);
this.deleteSelection = createSnapshot;
}

execute(selection: ModelSelection = this.model.selection): ModelNode[] {
if (!ModelSelection.isWellBehaved(selection)) {
throw new MisbehavedSelectionError();
}

let buffer: SimplifiedModel | null = null;
if (!this.deleteSelection) {
buffer = this.model.createSnapshot();
}

let modelNodes: ModelNode[] = [];
const range = selection.lastRange;
let commonAncestor = range.getCommonAncestor();

// special cases:
// either inside a list with CA the list container
// or inside a list with CA the list item, but the list item is entirely surrounded with selection
if (ModelNodeUtils.isListContainer(commonAncestor)
|| (ModelNodeUtils.isListElement(commonAncestor) && SelectionCommand.isElementFullySelected(commonAncestor, range))
) {
const newAncestor = ModelNodeUtils.findAncestor(commonAncestor, node => !ModelNodeUtils.isListContainer(node));
if (!newAncestor || !ModelElement.isModelElement(newAncestor)) {
throw new ImpossibleModelStateError("No ancestor found that is not list container.");
}

commonAncestor = newAncestor;
}

this.model.change(mutator => {
let contentRange = mutator.splitRangeUntilElements(range, commonAncestor, commonAncestor);
let treeWalker = new ModelTreeWalker({
range: contentRange,
descend: false
});

// Check if selection is inside table cell. If this is the case, cut children of said cell.
// Assumption: if table cell is selected, no other nodes at the same level can be selected.
const firstModelNode = treeWalker.currentNode;
if (ModelNodeUtils.isTableCell(firstModelNode)) {
contentRange = range;
treeWalker = new ModelTreeWalker({
range: contentRange,
descend: false
});
}
modelNodes = [...treeWalker];

if (this.deleteSelection) {
selection.selectRange(mutator.insertNodes(contentRange));
}
}, this.deleteSelection);

if (buffer) {
// If `deleteSelection` is false, we will have stored a snapshot of the model right before the execution of this
// command. This means we will enter this if-case. Since, we don't want the changes on the VDOM to get written
// back in this case, we restore the stored model.
this.model.restoreSnapshot(buffer, false);
}

return modelNodes;
}

/**
* Check if range perfectly surrounds element, e.g.:
* <ul>|<li>foo</li>|</ul>
* @param element
* @param range
* @private
*/
private static isElementFullySelected(element: ModelElement, range: ModelRange): boolean {
let startPosition = range.start;
while (startPosition.parent !== element && startPosition.parentOffset === 0) {
startPosition = ModelPosition.fromBeforeNode(startPosition.parent);
}

if (startPosition.parentOffset !== 0) {
return false;
}

let endPosition = range.end;
while (endPosition.parent !== element && endPosition.parentOffset === endPosition.parent.getMaxOffset()) {
endPosition = ModelPosition.fromAfterNode(endPosition.parent);
}

return endPosition.parentOffset === endPosition.parent.getMaxOffset();
}
}
14 changes: 14 additions & 0 deletions addon/commands/undo-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Command from "@lblod/ember-rdfa-editor/commands/command";
import Model from "@lblod/ember-rdfa-editor/model/model";

export default class UndoCommand extends Command {
name = "undo";

constructor(model: Model) {
super(model, false);
}

execute(): void {
this.model.restoreSnapshot();
}
}
Loading