Improve copy paste support.

This commit is contained in:
Paulo Gustavo Veiga 2023-01-14 19:31:01 -08:00
parent 57934487f7
commit e54d4ff543
26 changed files with 120 additions and 75 deletions

View File

@ -0,0 +1,19 @@
/// <reference types="cypress" />
describe('Edit Topic', () => {
beforeEach(() => {
// Remove storage for autosave ...
cy.visit('/editor.html');
cy.waitEditorLoaded();
cy.get('[test-id=2]').click();
});
it('Copy and Paste', () => {
cy.get(`[aria-label="Topic Style"]`).first().trigger('mouseover');
cy.get('body').type('{meta}c');
// Copy & Paste require permissions. More reseach needed.
// cy.get('body').type('{meta}v');
// cy.get('[test-id=50]').click();
// cy.matchImageSnapshot('copyandpaste');
});
});

View File

@ -56,6 +56,7 @@ import FeatureType from './model/FeatureType';
import WidgetManager from './WidgetManager';
import { TopicShapeType } from './model/INodeModel';
import { LineType } from './ConnectionLine';
import XMLSerializerFactory from './persistence/XMLSerializerFactory';
class Designer extends Events {
private _mindmap: Mindmap | null;
@ -74,8 +75,6 @@ class Designer extends Events {
private _relPivot: RelationshipPivot;
private _clipboard: NodeModel[];
private _cleanScreen!: () => void;
constructor(options: DesignerOptions, divElement: JQuery) {
@ -129,7 +128,6 @@ class Designer extends Events {
this._relPivot = new RelationshipPivot(this._workspace, this);
TopicEventDispatcher.configure(this.isReadOnly());
this._clipboard = [];
// Hack: There are static reference to designer variable. Needs to be reviewed.
globalThis.designer = this;
@ -381,44 +379,79 @@ class Designer extends Events {
copyToClipboard(): void {
let topics = this.getModel().filterSelectedTopics();
if (topics.length <= 0) {
// If there are more than one node selected,
$notify($msg('AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED'));
return;
if (topics.length > 0) {
const mindmap = new Mindmap();
const central: NodeModel = new NodeModel('CentralTopic', mindmap);
mindmap.addBranch(central);
// Exclude central topic ..
topics = topics.filter((topic) => !topic.isCentralTopic());
topics.forEach((topic) => {
const nodeModel: NodeModel = topic.getModel().deepCopy();
nodeModel.connectTo(central);
});
// Serialize to mindmap ...
const serializer = XMLSerializerFactory.createFromMindmap(mindmap);
const document = serializer.toXML(mindmap);
const xmmStr: string = new XMLSerializer().serializeToString(document);
// Convert to node, only text/html is supported...
const type = 'text/plain';
const blob = new Blob([xmmStr], { type });
const clipboard = new ClipboardItem({
[blob.type]: blob,
});
// Copy to clipboard ...
navigator.clipboard.write([clipboard]).then(
() => console.log('Copy of node success'),
(e) => console.error(e),
);
}
// Exclude central topic ..
topics = topics.filter((topic) => !topic.isCentralTopic());
this._clipboard = topics.map((topic) => {
const nodeModel = topic.getModel().deepCopy();
// Change position to make the new topic evident...
const pos = nodeModel.getPosition();
nodeModel.setPosition(pos.x + 60 * Math.sign(pos.x), pos.y + 30);
return nodeModel;
});
$notify($msg('SELECTION_COPIED_TO_CLIPBOARD'));
}
pasteClipboard(): void {
// If the no selection has been made, update with the text on the clipboard.
if (this._clipboard.length !== 0) {
this._actionDispatcher.addTopics(this._clipboard, null);
this._clipboard = [];
} else {
const topics = this.getModel().filterSelectedTopics();
if (topics.length > 0) {
navigator.clipboard.readText().then((text) => {
async pasteClipboard(): Promise<void> {
const type = 'text/plain';
const clipboardItems = await navigator.clipboard.read();
clipboardItems.forEach(async (item) => {
if (item.types.includes(type)) {
const blob: Blob = await item.getType(type);
const text: string = await blob.text();
// Is a mindmap ?. Try to infer if it's a text or a map...
if (text.indexOf('</map>') !== -1) {
const dom = new DOMParser().parseFromString(text, 'application/xml');
const serializer = XMLSerializerFactory.createFromDocument(dom);
const mindmap = serializer.loadFromDom(dom, 'application/xml');
// Remove reference to the parent mindmap and clean up to support multiple copy of the nodes ...
const central = mindmap.getBranches()[0];
let children = central.getChildren();
children.forEach((c) => c.disconnect());
children = children.map((m: NodeModel) => m.deepCopy());
// Charge position to avoid overlap ...
children.forEach((m) => {
const pos = m.getPosition();
m.setPosition(
pos.x * Math.sign(pos.x) + Math.random() * 60,
pos.y + Math.random() * 30,
);
});
// Finally, add the node ...
this._actionDispatcher.addTopics(children, null);
} else {
const topics = this.getModel().filterSelectedTopics();
this._actionDispatcher.changeTextToTopic(
topics.map((t) => t.getId()),
text.trim(),
);
});
}
}
}
});
}
getModel(): DesignerModel {

View File

@ -173,8 +173,9 @@ class MindplotWebComponent extends HTMLElement {
const persistenceManager = PersistenceManager.getInstance();
// If the map could not be loaded, partial map load could happen.
if (mindmap) {
persistenceManager.unlockMap(mindmap.getId());
const mapId = mindmap.getId();
if (mindmap && mapId) {
persistenceManager.unlockMap(mapId);
}
}
}

View File

@ -38,10 +38,10 @@ abstract class PersistenceManager {
$assert(mindmap, 'mindmap can not be null');
$assert(editorProperties, 'editorProperties can not be null');
const mapId = mindmap.getId();
const mapId = mindmap.getId() || 'WiseMapping';
$assert(mapId, 'mapId can not be null');
const serializer = XMLSerializerFactory.createInstanceFromMindmap(mindmap);
const serializer = XMLSerializerFactory.createFromMindmap(mindmap);
const domMap = serializer.toXML(mindmap);
const pref = JSON.stringify(editorProperties);
try {
@ -106,7 +106,7 @@ abstract class PersistenceManager {
$assert(mapId, 'mapId can not be null');
$assert(mapDom, 'mapDom can not be null');
const serializer = XMLSerializerFactory.createInstanceFromDocument(mapDom);
const serializer = XMLSerializerFactory.createFromDocument(mapDom);
return serializer.loadFromDom(mapDom, mapId);
}
}

View File

@ -80,7 +80,7 @@ class RESTPersistenceManager extends PersistenceManager {
if (response.ok) {
events.onSuccess();
} else {
console.log(`Saving error: ${response.status}`);
console.error(`Saving error: ${response.status}`);
let userMsg: PersistenceError | null = null;
if (response.status === 405) {
userMsg = {

View File

@ -30,7 +30,6 @@ class AddTopicCommand extends Command {
* the mindmap.
*/
constructor(models: NodeModel[], parentTopicsId: number[] | null) {
$assert(models, 'models can not be null');
$assert(
parentTopicsId == null || parentTopicsId.length === models.length,
'parents and models must have the same size',

View File

@ -29,7 +29,7 @@ class WiseXMLExporter extends Exporter {
export(): Promise<string> {
const { mindmap } = this;
const serializer = XMLSerializerFactory.createInstanceFromMindmap(mindmap);
const serializer = XMLSerializerFactory.createFromMindmap(mindmap);
const document: Document = serializer.toXML(mindmap);
const xmlStr: string = new XMLSerializer().serializeToString(document);

View File

@ -77,7 +77,7 @@ export default class FreemindImporter extends Importer {
this.mindmap.setDescription(description);
this.mindmap.addBranch(wiseTopic);
const serialize = XMLSerializerFactory.createInstanceFromMindmap(this.mindmap);
const serialize = XMLSerializerFactory.createFromMindmap(this.mindmap);
const domMindmap = serialize.toXML(this.mindmap);
const xmlToString = new XMLSerializer().serializeToString(domMindmap);
const formatXml = xmlFormatter(xmlToString, {

View File

@ -13,7 +13,7 @@ export default class WisemappingImporter extends Importer {
const parser = new DOMParser();
const wiseDoc = parser.parseFromString(this.wisemappingInput, 'application/xml');
const serialize = XMLSerializerFactory.createInstanceFromDocument(wiseDoc);
const serialize = XMLSerializerFactory.createFromDocument(wiseDoc);
const mindmap = serialize.loadFromDom(wiseDoc, nameMap);
mindmap.setDescription(description);

View File

@ -13,11 +13,9 @@ const DE = {
SUB_TOPIC: 'Unterthema',
ISOLATED_TOPIC: 'Isoliertes Thema',
CENTRAL_TOPIC: 'Zentrales Thema',
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: 'Es muss mindestens ein Thema ausgewählt sein.',
CLIPBOARD_IS_EMPTY: 'Es gibt nichts zu kopieren. Die Zwischenablage ist leer.',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: 'Das zentrale Thema kann nicht gelöscht werden.',
RELATIONSHIP_COULD_NOT_BE_CREATED: 'Die Beziehung konnte nicht angelegt werden. Es muss erst ein Vater-Thema ausgewählt werden, um die Beziehung herzustellen.',
SELECTION_COPIED_TO_CLIPBOARD: 'Themen in der Zwischenablage',
SESSION_EXPIRED: 'Deine Sitzung ist abgelaufen, bitte melde dich erneut an.',
CENTRAL_TOPIC_CONNECTION_STYLE_CAN_NOT_BE_CHANGED: 'Le style de connexion ne peut pas être modifié pour le sujet central.',
CENTRAL_TOPIC_STYLE_CAN_NOT_BE_CHANGED: 'Le sujet central ne peut pas être changé en style de ligne.',

View File

@ -13,11 +13,9 @@ const EN = {
ISOLATED_TOPIC: 'Isolated Topic',
CENTRAL_TOPIC: 'Central Topic',
ENTITIES_COULD_NOT_BE_DELETED: 'Could not delete topic or relation. At least one map entity must be selected.',
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: 'At least one topic must be selected.',
CLIPBOARD_IS_EMPTY: 'Nothing to copy. Clipboard is empty.',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: 'Central topic can not be deleted.',
RELATIONSHIP_COULD_NOT_BE_CREATED: 'Relationship could not be created. A parent relationship topic must be selected first.',
SELECTION_COPIED_TO_CLIPBOARD: 'Topics copied to the clipboard',
SESSION_EXPIRED: 'Your session has expired, please log-in again.',
CENTRAL_TOPIC_CONNECTION_STYLE_CAN_NOT_BE_CHANGED: 'Connection style can not be changed for central topic.',
CENTRAL_TOPIC_STYLE_CAN_NOT_BE_CHANGED: 'Central topic can not be changed to line style.',

View File

@ -13,11 +13,9 @@ const ES = {
ISOLATED_TOPIC: 'Tópico Aislado',
CENTRAL_TOPIC: 'Tópico Central',
ENTITIES_COULD_NOT_BE_DELETED: 'El tópico o la relación no pudo ser borrada. Debe selecionar al menos una.',
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: 'Al menos un tópico debe ser seleccionado.',
CLIPBOARD_IS_EMPTY: 'Nada que copiar. Clipboard está vacio.',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: 'El tópico central no puede ser borrado.',
RELATIONSHIP_COULD_NOT_BE_CREATED: 'La relación no pudo ser creada. Una relación padre debe ser seleccionada primero.',
SELECTION_COPIED_TO_CLIPBOARD: 'Tópicos copiados al clipboard',
SESSION_EXPIRED: 'Su session ha expirado. Por favor, ingrese nuevamente.',
CENTRAL_TOPIC_CONNECTION_STYLE_CAN_NOT_BE_CHANGED: 'El estilo de conexión no se puede cambiar para el tópico central.',
CENTRAL_TOPIC_STYLE_CAN_NOT_BE_CHANGED: 'Tópico central no se puede cambiar al estilo de línea.',

View File

@ -13,11 +13,9 @@ const FR = {
ISOLATED_TOPIC: 'Noeud isolé',
CENTRAL_TOPIC: 'Noeud racine',
ENTITIES_COULD_NOT_BE_DELETED: "Impossible d'effacer un noeud ou une relation. Au moins un objet de la carte doit être sélectionné.",
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: 'Au moins un objet de la carte doit être sélectionné.',
CLIPBOARD_IS_EMPTY: 'Rien à copier. Presse-papier vide.',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: 'Le noeud racine ne peut pas être effacé.',
RELATIONSHIP_COULD_NOT_BE_CREATED: 'Impossible de créer relation. Un noeud parent doit être sélectionné au préalable.',
SELECTION_COPIED_TO_CLIPBOARD: 'Noeuds sélectionnés copiés dans le presse-papiers.',
SESSION_EXPIRED: 'Votre session a expiré, veuillez vous reconnecter.',
CENTRAL_TOPIC_CONNECTION_STYLE_CAN_NOT_BE_CHANGED: 'Le style de connexion ne peut pas être modifié pour le sujet central.',
CENTRAL_TOPIC_STYLE_CAN_NOT_BE_CHANGED: 'Le sujet central ne peut pas être changé en style de ligne.',

View File

@ -14,11 +14,9 @@ const RU = {
ISOLATED_TOPIC: 'Isolated Topic',
CENTRAL_TOPIC: 'Central Topic',
ENTITIES_COULD_NOT_BE_DELETED: 'Could not delete topic or relation. At least one map entity must be selected.',
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: 'At least one topic must be selected.',
CLIPBOARD_IS_EMPTY: 'Nothing to copy. Clipboard is empty.',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: 'Central topic can not be deleted.',
RELATIONSHIP_COULD_NOT_BE_CREATED: 'Relationship could not be created. A parent relationship topic must be selected first.',
SELECTION_COPIED_TO_CLIPBOARD: 'Topics copied to the clipboard',
SESSION_EXPIRED: 'Your session has expired, please log-in again.',
};

View File

@ -14,11 +14,9 @@ const ZH = {
ISOLATED_TOPIC: '独立主题',
CENTRAL_TOPIC: '中心主题',
ENTITIES_COULD_NOT_BE_DELETED: '无法删除主题或关系。至少选择一个脑图实体。',
AT_LEAST_ONE_TOPIC_MUST_BE_SELECTED: '至少要选择一个主题',
CLIPBOARD_IS_EMPTY: '没有东西可以复制。剪贴板是空的。',
CENTRAL_TOPIC_CAN_NOT_BE_DELETED: '无法删除中心主题。',
RELATIONSHIP_COULD_NOT_BE_CREATED: '无法创建关系。必须先选择要建立关系的主题。',
SELECTION_COPIED_TO_CLIPBOARD: '主题已复制到剪贴板',
SESSION_EXPIRED: '您的会话已过期,请重新登录。',
};

View File

@ -19,6 +19,7 @@
*/
import { $assert } from '@wisemapping/core-js';
import INodeModel, { NodeModelType as NodeType } from './INodeModel';
import NodeModel from './NodeModel';
import RelationshipModel from './RelationshipModel';
abstract class IMindmap {
@ -30,7 +31,7 @@ abstract class IMindmap {
abstract setDescription(value: string): void;
abstract getId(): string;
abstract getId(): string | undefined;
abstract setId(id: string): void;
@ -38,11 +39,11 @@ abstract class IMindmap {
abstract setVersion(version: string): void;
abstract addBranch(nodeModel): void;
abstract addBranch(nodeModel: INodeModel): void;
abstract getBranches();
abstract getBranches(): NodeModel[];
abstract removeBranch(node): void;
abstract removeBranch(node: INodeModel): void;
abstract getRelationships(): RelationshipModel[];

View File

@ -27,16 +27,14 @@ class Mindmap extends IMindmap {
private _version: string;
private _id: string;
private _id: string | undefined;
private _branches: Array<NodeModel>;
private _relationships: Array<RelationshipModel>;
constructor(id: string, version: string = ModelCodeName.TANGO) {
constructor(id?: string, version: string = ModelCodeName.TANGO) {
super();
$assert(id, 'Id can not be null');
this._branches = [];
this._description = '';
this._relationships = [];
@ -54,8 +52,7 @@ class Mindmap extends IMindmap {
this._description = value;
}
/** */
getId(): string {
getId(): string | undefined {
return this._id;
}

View File

@ -49,7 +49,7 @@ class XMLSerializerFactory {
* @return {mindplot.persistence.XMLSerializer_Beta|mindplot.persistence.XMLSerializer_Pela|
* mindplot.persistence.XMLSerializer_Tango} serializer corresponding to the mindmap's version
*/
static createInstanceFromMindmap(mindmap: Mindmap) {
static createFromMindmap(mindmap: Mindmap): XMLMindmapSerializer {
return XMLSerializerFactory.getSerializer(mindmap.getVersion());
}
@ -57,7 +57,7 @@ class XMLSerializerFactory {
* @param domDocument
* @return serializer corresponding to the mindmap's version
*/
static createInstanceFromDocument(domDocument: Document) {
static createFromDocument(domDocument: Document): XMLMindmapSerializer {
const rootElem = domDocument.documentElement;
// Legacy version don't have version defined.
@ -71,8 +71,6 @@ class XMLSerializerFactory {
* retrieves the serializer for the mindmap's version and migrates to the current version,
* e.g. for a Beta mindmap and current version Tango:
* serializer = new Pela2TangoMigrator(new Beta2PelaMigrator(new XMLSerializer_Beta()))
* @param {String} version the version name
* @return serializer
*/
static getSerializer(version = ModelCodeName.TANGO): XMLMindmapSerializer {
let found = false;

View File

@ -45,7 +45,7 @@ class XMLSerializerTango implements XMLMindmapSerializer {
// Store map attributes ...
const mapElem = document.createElement('map');
const name = mindmap.getId();
if ($defined(name)) {
if (name) {
mapElem.setAttribute('name', this._rmXmlInv(name));
}
const version = mindmap.getVersion();

View File

@ -20,7 +20,7 @@ describe('WXML export test execution', () => {
const mapDocument = parseXMLFile(mindmapPath, 'text/xml');
// Convert to mindmap ...
const serializer = XMLSerializerFactory.createInstanceFromDocument(mapDocument);
const serializer = XMLSerializerFactory.createFromDocument(mapDocument);
const mindmap: Mindmap = serializer.loadFromDom(mapDocument, testName);
const exporter = TextExporterFactory.create('wxml', mindmap);
@ -35,7 +35,7 @@ describe('Txt export test execution', () => {
const mapDocument = parseXMLFile(mindmapPath, 'text/xml');
// Convert to mindmap ...
const serializer = XMLSerializerFactory.createInstanceFromDocument(mapDocument);
const serializer = XMLSerializerFactory.createFromDocument(mapDocument);
const mindmap: Mindmap = serializer.loadFromDom(mapDocument, testName);
const exporter = TextExporterFactory.create('txt', mindmap);
@ -50,7 +50,7 @@ describe('MD export test execution', () => {
const mapDocument = parseXMLFile(mindmapPath, 'text/xml');
// Convert to mindmap ...
const serializer = XMLSerializerFactory.createInstanceFromDocument(mapDocument);
const serializer = XMLSerializerFactory.createFromDocument(mapDocument);
const mindmap: Mindmap = serializer.loadFromDom(mapDocument, testName);
const exporter = TextExporterFactory.create('md', mindmap);
@ -65,7 +65,7 @@ describe('MM export test execution', () => {
const mapDocument = parseXMLFile(mindmapPath, 'text/xml');
// Convert to mindmap...
const serializer = XMLSerializerFactory.createInstanceFromDocument(mapDocument);
const serializer = XMLSerializerFactory.createFromDocument(mapDocument);
const mindmap: Mindmap = serializer.loadFromDom(mapDocument, testName);
const exporter = TextExporterFactory.create('mm', mindmap);

View File

@ -1,3 +1,5 @@
/// <reference types="cypress" />
describe('Editor Page', () => {
beforeEach(() => {
cy.visit('/c/maps/11/edit');

View File

@ -1,3 +1,5 @@
/// <reference types="cypress" />
describe('Forgot Password Page', () => {
beforeEach(() => {
cy.visit('/c/forgot-password');

View File

@ -1,3 +1,5 @@
/// <reference types="cypress" />
describe('Login Page', () => {
beforeEach(() => {
cy.visit('/c/login');

View File

@ -1,3 +1,5 @@
/// <reference types="cypress" />
describe('Maps Page', () => {
beforeEach(() => {
cy.visit('/c/maps');

View File

@ -1,3 +1,5 @@
/// <reference types="cypress" />
describe('Registration Page', () => {
beforeEach(() => {
cy.visit('/c/registration');

View File

@ -13,7 +13,6 @@
}
],
"permissions" : [
"http://*/*",
"https://*/*",
"clipboardRead",
"clipboardWrite",