From b107dcb650a90e039549f7375e39911c943174c9 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 4 Dec 2024 14:17:10 +0100 Subject: [PATCH 01/16] feat: Add edit api v3 support --- packages/core/foundation.ts | 39 ++-- .../core/foundation/deprecated/edit-event.ts | 78 ++++++++ packages/core/foundation/deprecated/editor.ts | 2 +- .../core/foundation/deprecated/history.ts | 2 +- .../core/foundation/edit-completed-event.ts | 6 +- packages/core/foundation/edit-event.ts | 77 ++------ packages/core/foundation/edit.ts | 79 ++++++++ packages/core/foundation/handle-edit.ts | 178 ++++++++++++++++++ packages/openscd/src/addons/Editor.ts | 83 ++++---- packages/openscd/src/addons/History.ts | 6 +- .../addons/editor/edit-v1-to-v2-converter.ts | 18 +- packages/openscd/src/plugins.ts | 8 + packages/plugins/src/editors/EditTest.ts | 107 +++++++++++ 13 files changed, 560 insertions(+), 123 deletions(-) create mode 100644 packages/core/foundation/deprecated/edit-event.ts create mode 100644 packages/core/foundation/edit.ts create mode 100644 packages/core/foundation/handle-edit.ts create mode 100644 packages/plugins/src/editors/EditTest.ts diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index c7cd164a43..501e917534 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -7,22 +7,39 @@ export { newOpenEvent } from './foundation/open-event.js'; export type { OpenEvent, OpenDetail } from './foundation/open-event.js'; export { - newEditEvent, - isComplex, - isInsert, - isNamespaced, - isUpdate, - isRemove, -} from './foundation/edit-event.js'; + newEditEvent as newEditEventV1, + isComplex as isComplexV1, + isInsert as isInsertV1, + isNamespaced as isNamespacedV1, + isUpdate as isUpdateV1, + isRemove as isRemoveV1, +} from './foundation/deprecated/edit-event.js'; export type { - EditEvent, - Edit, - Insert, + EditEvent as EditEventV1, + Edit as EditV1, + Insert as InsertV1, AttributeValue, NamespacedAttributeValue, - Update, + Update as UpdateV1, + Remove as RemoveV1, +} from './foundation/deprecated/edit-event.js'; + +export type { + Edit, + Insert, Remove, + SetTextContent, + SetAttributes, + isEdit +} from './foundation/edit.js'; +export type { + EditEvent, + EditEventOptions, + EditDetail } from './foundation/edit-event.js'; +export { newEditEvent } from './foundation/edit-event.js'; + +export { handleEdit } from './foundation/handle-edit.js'; export { cyrb64 } from './foundation/cyrb64.js'; diff --git a/packages/core/foundation/deprecated/edit-event.ts b/packages/core/foundation/deprecated/edit-event.ts new file mode 100644 index 0000000000..1cd6649932 --- /dev/null +++ b/packages/core/foundation/deprecated/edit-event.ts @@ -0,0 +1,78 @@ +export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; + +/** Intent to `parent.insertBefore(node, reference)` */ +export type Insert = { + parent: Node; + node: Node; + reference: Node | null; +}; + +export type NamespacedAttributeValue = { + value: string | null; + namespaceURI: string | null; +}; +export type AttributeValue = string | null | NamespacedAttributeValue; +/** Intent to set or remove (if null) attributes on element */ +export type Update = { + element: Element; + attributes: Partial>; +}; + +/** Intent to remove a node from its ownerDocument */ +export type Remove = { + node: Node; +}; + +/** Represents the user's intent to change an XMLDocument */ +export type Edit = Insert | Update | Remove | Edit[]; + +export function isComplex(edit: Edit): edit is Edit[] { + return edit instanceof Array; +} + +export function isInsert(edit: Edit): edit is Insert { + return (edit as Insert).parent !== undefined; +} + +export function isNamespaced( + value: AttributeValue +): value is NamespacedAttributeValue { + return value !== null && typeof value !== 'string'; +} + +export function isUpdate(edit: Edit): edit is Update { + return (edit as Update).element !== undefined; +} + +export function isRemove(edit: Edit): edit is Remove { + return ( + (edit as Insert).parent === undefined && (edit as Remove).node !== undefined + ); +} + +export interface EditEventDetail { + edit: E; + initiator: Initiator; +} + +export type EditEvent = CustomEvent; + +export function newEditEvent( + edit: E, + initiator: Initiator = 'user' +): EditEvent { + return new CustomEvent('oscd-edit', { + composed: true, + bubbles: true, + detail: { + edit: edit, + initiator: initiator, + }, + }); +} + +declare global { + interface ElementEventMap { + ['oscd-edit']: EditEvent; + } +} diff --git a/packages/core/foundation/deprecated/editor.ts b/packages/core/foundation/deprecated/editor.ts index 5aa7350bac..5f7b53c2dc 100644 --- a/packages/core/foundation/deprecated/editor.ts +++ b/packages/core/foundation/deprecated/editor.ts @@ -1,4 +1,4 @@ -import { Initiator } from '../edit-event.js'; +import { Initiator } from './edit-event.js'; /** Inserts `new.element` to `new.parent` before `new.reference`. */ export interface Create { diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index d6c6d6c648..0a0667595d 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -1,4 +1,4 @@ -import { Edit } from '../edit-event.js'; +import { Edit } from './edit-event.js'; type InfoEntryKind = 'info' | 'warning' | 'error'; diff --git a/packages/core/foundation/edit-completed-event.ts b/packages/core/foundation/edit-completed-event.ts index b2128ded9a..8a7a12a088 100644 --- a/packages/core/foundation/edit-completed-event.ts +++ b/packages/core/foundation/edit-completed-event.ts @@ -1,9 +1,9 @@ -import { Edit, Initiator } from './edit-event.js'; +import { Edit as EditV1, Initiator } from './deprecated/edit-event.js'; import { EditorAction } from './deprecated/editor.js'; export type EditCompletedDetail = { - edit: Edit | EditorAction; + edit: EditV1 | EditorAction; initiator: Initiator; }; @@ -11,7 +11,7 @@ export type EditCompletedDetail = { export type EditCompletedEvent = CustomEvent; export function newEditCompletedEvent( - edit: Edit | EditorAction, + edit: EditV1 | EditorAction, initiator: Initiator = 'user' ): EditCompletedEvent { return new CustomEvent('oscd-edit-completed', { diff --git a/packages/core/foundation/edit-event.ts b/packages/core/foundation/edit-event.ts index 1cd6649932..b33d799a7b 100644 --- a/packages/core/foundation/edit-event.ts +++ b/packages/core/foundation/edit-event.ts @@ -1,78 +1,33 @@ -export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; +import { Edit } from './edit.js'; -/** Intent to `parent.insertBefore(node, reference)` */ -export type Insert = { - parent: Node; - node: Node; - reference: Node | null; +export type EditDetail = { + edit: E; + title?: string; + squash?: boolean; }; -export type NamespacedAttributeValue = { - value: string | null; - namespaceURI: string | null; -}; -export type AttributeValue = string | null | NamespacedAttributeValue; -/** Intent to set or remove (if null) attributes on element */ -export type Update = { - element: Element; - attributes: Partial>; -}; +export type EditEvent = CustomEvent< + EditDetail +>; -/** Intent to remove a node from its ownerDocument */ -export type Remove = { - node: Node; +export type EditEventOptions = { + title?: string; + squash?: boolean; }; -/** Represents the user's intent to change an XMLDocument */ -export type Edit = Insert | Update | Remove | Edit[]; - -export function isComplex(edit: Edit): edit is Edit[] { - return edit instanceof Array; -} - -export function isInsert(edit: Edit): edit is Insert { - return (edit as Insert).parent !== undefined; -} - -export function isNamespaced( - value: AttributeValue -): value is NamespacedAttributeValue { - return value !== null && typeof value !== 'string'; -} - -export function isUpdate(edit: Edit): edit is Update { - return (edit as Update).element !== undefined; -} - -export function isRemove(edit: Edit): edit is Remove { - return ( - (edit as Insert).parent === undefined && (edit as Remove).node !== undefined - ); -} - -export interface EditEventDetail { - edit: E; - initiator: Initiator; -} - -export type EditEvent = CustomEvent; - export function newEditEvent( edit: E, - initiator: Initiator = 'user' -): EditEvent { - return new CustomEvent('oscd-edit', { + options?: EditEventOptions +): EditEvent { + return new CustomEvent>('oscd-edit-v2', { composed: true, bubbles: true, - detail: { - edit: edit, - initiator: initiator, - }, + detail: { ...options, edit }, }); } declare global { interface ElementEventMap { - ['oscd-edit']: EditEvent; + ['oscd-edit-v2']: EditEvent; } } diff --git a/packages/core/foundation/edit.ts b/packages/core/foundation/edit.ts new file mode 100644 index 0000000000..36e9836721 --- /dev/null +++ b/packages/core/foundation/edit.ts @@ -0,0 +1,79 @@ +/** Intent to `parent.insertBefore(node, reference)` */ +export type Insert = { + parent: Node; + node: Node; + reference: Node | null; +}; + +/** Intent to remove a `node` from its `ownerDocument` */ +export type Remove = { + node: Node; +}; + +/** Intent to set the `textContent` of `element` */ +export type SetTextContent = { + element: Element; + textContent: string; +}; + +/** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */ +export type SetAttributes = { + element: Element; + attributes: Partial>; + attributesNS: Partial>>>; +}; + +/** Intent to change some XMLDocuments */ +export type Edit = + | Insert + | SetAttributes + | SetTextContent + | Remove + | Edit[]; + +export function isComplex(edit: Edit): edit is Edit[] { + return edit instanceof Array; +} + +export function isSetTextContent(edit: Edit): edit is SetTextContent { + return ( + (edit as SetTextContent).element !== undefined && + (edit as SetTextContent).textContent !== undefined + ); +} + +export function isRemove(edit: Edit): edit is Remove { + return ( + (edit as Insert).parent === undefined && (edit as Remove).node !== undefined + ); +} + +export function isSetAttributes(edit: Edit): edit is SetAttributes { + return ( + (edit as SetAttributes).element !== undefined && + (edit as SetAttributes).attributes !== undefined && + (edit as SetAttributes).attributesNS !== undefined + ); +} + +export function isInsert(edit: Edit): edit is Insert { + return ( + (edit as Insert).parent !== undefined && + (edit as Insert).node !== undefined && + (edit as Insert).reference !== undefined + ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isEdit(edit: any): edit is Edit { + if (isComplex(edit)) { + return !edit.some((e) => !isEdit(e)); + } + + return ( + isSetAttributes(edit) || + isSetTextContent(edit) || + isInsert(edit) || + isRemove(edit) + ); +} diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts new file mode 100644 index 0000000000..74b843d90f --- /dev/null +++ b/packages/core/foundation/handle-edit.ts @@ -0,0 +1,178 @@ +import { + Edit, + Insert, + isComplex, + isInsert, + isRemove, + isSetAttributes, + isSetTextContent, + Remove, + SetAttributes, + SetTextContent, +} from './edit.js'; + +function handleSetTextContent({ + element, + textContent, +}: SetTextContent): (SetTextContent | Insert)[] { + const { childNodes } = element; + + const restoreChildNodes: Insert[] = Array.from(childNodes).map((node) => ({ + parent: element, + node, + reference: null, + })); + + element.textContent = textContent; + + const undoTextContent: SetTextContent = { element, textContent: '' }; + + return [undoTextContent, ...restoreChildNodes]; +} + +function uniqueNSPrefix(element: Element, ns: string): string { + let i = 1; + const attributes = Array.from(element.attributes); + const hasSamePrefix = (attribute: Attr) => + attribute.prefix === `ens${i}` && attribute.namespaceURI !== ns; + const nsOrNull = new Set([null, ns]); + const differentNamespace = (prefix: string) => + !nsOrNull.has(element.lookupNamespaceURI(prefix)); + while (differentNamespace(`ens${i}`) || attributes.find(hasSamePrefix)) + i += 1; + return `ens${i}`; +} + +const xmlAttributeName = /^(?!xml|Xml|xMl|xmL|XMl|xML|XmL|XML)[A-Za-z_][A-Za-z0-9-_.]*(:[A-Za-z_][A-Za-z0-9-_.]*)?$/; + +function validName(name: string): boolean { + return xmlAttributeName.test(name); +} + +function handleSetAttributes({ + element, + attributes, + attributesNS, +}: SetAttributes): SetAttributes { + const oldAttributes = { ...attributes }; + const oldAttributesNS = { ...attributesNS }; + + // save element's non-prefixed attributes for undo + Object.keys(attributes) + .reverse() + .forEach((name) => { + oldAttributes[name] = element.getAttribute(name); + }); + + // change element's non-prefixed attributes + for (const entry of Object.entries(attributes)) { + try { + const [name, value] = entry as [string, string | null]; + if (value === null) element.removeAttribute(name); + else element.setAttribute(name, value); + } catch (_e) { + // undo nothing if update didn't work on this attribute + delete oldAttributes[entry[0]]; + } + } + + // save element's namespaced attributes for undo + Object.entries(attributesNS).forEach(([ns, attrs]) => { + Object.keys(attrs!) + .filter(validName) + .reverse() + .forEach((name) => { + oldAttributesNS[ns] = { + ...oldAttributesNS[ns], + [name]: element.getAttributeNS(ns, name.split(':').pop()!), + }; + }); + Object.keys(attrs!) + .filter((name) => !validName(name)) + .forEach((name) => { + delete oldAttributesNS[ns]![name]; + }); + }); + + // change element's namespaced attributes + for (const nsEntry of Object.entries(attributesNS)) { + const [ns, attrs] = nsEntry as [ + string, + Partial>, + ]; + for (const entry of Object.entries(attrs).filter(([name]) => + validName(name), + )) { + try { + const [name, value] = entry as [string, string | null]; + if (value === null) { + element.removeAttributeNS(ns, name.split(':').pop()!); + } else { + let qualifiedName = name; + if (!qualifiedName.includes(':')) { + let prefix = element.lookupPrefix(ns); + if (!prefix) prefix = uniqueNSPrefix(element, ns); + qualifiedName = `${prefix}:${name}`; + } + element.setAttributeNS(ns, qualifiedName, value); + } + } catch (_e) { + delete oldAttributesNS[ns]![entry[0]]; + } + } +} + +return { + element, + attributes: oldAttributes, + attributesNS: oldAttributesNS, +}; +} + +function handleRemove({ node }: Remove): Insert | [] { + const { parentNode: parent, nextSibling: reference } = node; + node.parentNode?.removeChild(node); + if (parent) + return { + node, + parent, + reference, + }; + return []; +} + +function handleInsert({ + parent, + node, + reference, +}: Insert): Insert | Remove | [] { + try { + const { parentNode, nextSibling } = node; + parent.insertBefore(node, reference); + if (parentNode) { + // undo: move child node back to original place + return { + node, + parent: parentNode, + reference: nextSibling, + }; + } + + // undo: remove orphaned node + return { node }; + } catch (_e) { + // undo nothing if insert doesn't work on these nodes + return []; + } +} + +/** Applies an Edit, returning the corresponding 'undo' Edit. */ +export function handleEdit(edit: Edit): Edit { + if (isInsert(edit)) return handleInsert(edit); + if (isRemove(edit)) return handleRemove(edit); + if (isSetAttributes(edit)) return handleSetAttributes(edit); + if (isSetTextContent(edit)) return handleSetTextContent(edit); + if (isComplex(edit)) return edit.map((edit) => handleEdit(edit)).reverse(); + + return []; +} diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index a2ea531dae..702c05ae10 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,4 +1,11 @@ -import { OpenEvent, newEditCompletedEvent, newEditEvent } from '@openscd/core'; +import { + Edit, + EditEvent, + OpenEvent, + newEditCompletedEvent, + newEditEventV1, + handleEdit +} from '@openscd/core'; import { property, LitElement, @@ -19,16 +26,16 @@ import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js' import { AttributeValue, - Edit, - EditEvent, - Insert, - isComplex, - isInsert, - isNamespaced, - isRemove, - isUpdate, - Remove, - Update, + EditV1, + EditEventV1, + InsertV1, + isComplexV1, + isInsertV1, + isNamespacedV1, + isRemoveV1, + isUpdateV1, + RemoveV1, + UpdateV1, } from '@openscd/core'; import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @@ -48,21 +55,21 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - private getLogText(edit: Edit): { title: string, message?: string } { - if (isInsert(edit)) { + private getLogText(edit: EditV1): { title: string, message?: string } { + if (isInsertV1(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.created', { name }) }; - } else if (isUpdate(edit)) { + } else if (isUpdateV1(edit)) { const name = edit.element.tagName; return { title: get('editing.updated', { name }) }; - } else if (isRemove(edit)) { + } else if (isRemoveV1(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.deleted', { name }) }; - } else if (isComplex(edit)) { + } else if (isComplexV1(edit)) { const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); return { title: get('editing.complex'), message }; } @@ -74,7 +81,7 @@ export class OscdEditor extends LitElement { const edit = convertEditV1toV2(event.detail.action); const initiator = event.detail.initiator; - this.host.dispatchEvent(newEditEvent(edit, initiator)); + this.host.dispatchEvent(newEditEventV1(edit, initiator)); } /** @@ -110,6 +117,7 @@ export class OscdEditor extends LitElement { this.host.addEventListener('editor-action', this.onAction.bind(this)); this.host.addEventListener('oscd-edit', event => this.handleEditEvent(event)); + this.host.addEventListener('oscd-edit-v2', event => this.handleEditEventV2(event)); this.host.addEventListener('open-doc', this.onOpenDoc); this.host.addEventListener('oscd-open', this.handleOpenDoc); } @@ -118,7 +126,14 @@ export class OscdEditor extends LitElement { return html``; } - async handleEditEvent(event: EditEvent) { + handleEditEventV2(event: EditEvent) { + console.log('Edit event v2', event); + const edit = event.detail.edit; + + const undoEdit = handleEdit(edit); + } + + async handleEditEvent(event: EditEventV1) { /** * This is a compatibility fix for plugins based on open energy tools edit events * because their edit event look slightly different @@ -129,7 +144,7 @@ export class OscdEditor extends LitElement { } const edit = event.detail.edit; - const undoEdit = handleEdit(edit); + const undoEdit = handleEditV1(edit); this.dispatchEvent( newEditCompletedEvent(event.detail.edit, event.detail.initiator) @@ -154,11 +169,11 @@ export class OscdEditor extends LitElement { } } -function handleEdit(edit: Edit): Edit { - if (isInsert(edit)) return handleInsert(edit); - if (isUpdate(edit)) return handleUpdate(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isComplex(edit)) return edit.map(handleEdit).reverse(); +function handleEditV1(edit: EditV1): EditV1 { + if (isInsertV1(edit)) return handleInsert(edit); + if (isUpdateV1(edit)) return handleUpdate(edit); + if (isRemoveV1(edit)) return handleRemove(edit); + if (isComplexV1(edit)) return edit.map(handleEditV1).reverse(); return []; } @@ -170,7 +185,7 @@ function handleInsert({ parent, node, reference, -}: Insert): Insert | Remove | [] { +}: InsertV1): InsertV1 | RemoveV1 | [] { try { const { parentNode, nextSibling } = node; @@ -197,13 +212,13 @@ function handleInsert({ } } -function handleUpdate({ element, attributes }: Update): Update { +function handleUpdate({ element, attributes }: UpdateV1): UpdateV1 { const oldAttributes = { ...attributes }; Object.entries(attributes) .reverse() .forEach(([name, value]) => { let oldAttribute: AttributeValue; - if (isNamespaced(value!)) + if (isNamespacedV1(value!)) oldAttribute = { value: element.getAttributeNS( value.namespaceURI, @@ -223,7 +238,7 @@ function handleUpdate({ element, attributes }: Update): Update { for (const entry of Object.entries(attributes)) { try { const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespaced(value)) { + if (isNamespacedV1(value)) { if (value.value === null) element.removeAttributeNS( value.namespaceURI, @@ -243,7 +258,7 @@ function handleUpdate({ element, attributes }: Update): Update { }; } -function handleRemove({ node }: Remove): Insert | [] { +function handleRemove({ node }: RemoveV1): InsertV1 | [] { const { parentNode: parent, nextSibling: reference } = node; node.parentNode?.removeChild(node); if (parent) @@ -256,11 +271,11 @@ function handleRemove({ node }: Remove): Insert | [] { } function isOpenEnergyEditEvent(event: CustomEvent): boolean { - const eventDetail = event.detail as Edit; - return isComplex(eventDetail) || isInsert(eventDetail) || isUpdate(eventDetail) || isRemove(eventDetail); + const eventDetail = event.detail as EditV1; + return isComplexV1(eventDetail) || isInsertV1(eventDetail) || isUpdateV1(eventDetail) || isRemoveV1(eventDetail); } -function convertOpenEnergyEditEventToEditEvent(event: CustomEvent): EditEvent { - const eventDetail = event.detail as Edit; - return newEditEvent(eventDetail); +function convertOpenEnergyEditEventToEditEvent(event: CustomEvent): EditEventV1 { + const eventDetail = event.detail as EditV1; + return newEditEventV1(eventDetail); } diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index a6db95729a..dfa8be438a 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { newEditEvent } from '@openscd/core'; +import { newEditEventV1 } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -213,7 +213,7 @@ export class OscdHistory extends LitElement { if (!this.canUndo) return false; const undoEdit = (this.history[this.editCount]).undo; - this.host.dispatchEvent(newEditEvent(undoEdit, 'undo')); + this.host.dispatchEvent(newEditEventV1(undoEdit, 'undo')); this.setEditCount(this.previousAction); return true; @@ -222,7 +222,7 @@ export class OscdHistory extends LitElement { if (!this.canRedo) return false; const redoEdit = (this.history[this.nextAction]).redo; - this.host.dispatchEvent(newEditEvent(redoEdit, 'redo')); + this.host.dispatchEvent(newEditEventV1(redoEdit, 'redo')); this.setEditCount(this.nextAction); return true; diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index f33d76d27f..cb6988e73b 100644 --- a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -13,11 +13,11 @@ import { SimpleAction, Update } from '@openscd/core/foundation/deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { EditV1, InsertV1, RemoveV1, UpdateV1 as UpdateV2 } from '@openscd/core'; import { getReference, SCLTag } from '../../foundation.js'; -export function convertEditV1toV2(action: EditorAction): Edit { +export function convertEditV1toV2(action: EditorAction): EditV1 { if (isSimple(action)) { return convertSimpleAction(action); } else { @@ -25,7 +25,7 @@ export function convertEditV1toV2(action: EditorAction): Edit { } } -function convertSimpleAction(action: SimpleAction): Edit { +function convertSimpleAction(action: SimpleAction): EditV1 { if (isCreate(action)) { return convertCreate(action); } else if (isDelete(action)) { @@ -41,7 +41,7 @@ function convertSimpleAction(action: SimpleAction): Edit { throw new Error('Unknown action type'); } -function convertCreate(action: Create): Insert { +function convertCreate(action: Create): InsertV1 { let reference: Node | null = null; if ( action.new.reference === undefined && @@ -63,7 +63,7 @@ function convertCreate(action: Create): Insert { }; } -function convertDelete(action: Delete): Remove { +function convertDelete(action: Delete): RemoveV1 { return { node: action.old.element }; @@ -86,7 +86,7 @@ function convertUpdate(action: Update): UpdateV2 { }; } -function convertMove(action: Move): Insert { +function convertMove(action: Move): InsertV1 { if (action.new.reference === undefined) { action.new.reference = getReference( action.new.parent, @@ -101,7 +101,7 @@ function convertMove(action: Move): Insert { } } -function convertReplace(action: Replace): Edit { +function convertReplace(action: Replace): EditV1 { const oldChildren = action.old.element.children; // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); @@ -116,8 +116,8 @@ function convertReplace(action: Replace): Edit { const reference = action.old.element.nextSibling; - const remove: Remove = { node: action.old.element }; - const insert: Insert = { + const remove: RemoveV1 = { node: action.old.element }; + const insert: InsertV1 = { parent, node: newNode, reference diff --git a/packages/openscd/src/plugins.ts b/packages/openscd/src/plugins.ts index 0c33dfa6c1..5465cbd689 100644 --- a/packages/openscd/src/plugins.ts +++ b/packages/openscd/src/plugins.ts @@ -3,6 +3,14 @@ function generatePluginPath(plugin: string): string { } export const officialPlugins = [ + { + name: 'Edit Test', + src: generatePluginPath('plugins/src/editors/EditTest.js'), + icon: 'margin', + default: true, + kind: 'editor', + requireDoc: true, + }, { name: 'IED', src: generatePluginPath('plugins/src/editors/IED.js'), diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts new file mode 100644 index 0000000000..6cc2949d8c --- /dev/null +++ b/packages/plugins/src/editors/EditTest.ts @@ -0,0 +1,107 @@ +import { Insert, newEditEvent, Remove, SetAttributes, SetTextContent } from '@openscd/core'; +import { LitElement, html, TemplateResult, property, css, state } from 'lit-element'; + +export default class SubstationPlugin extends LitElement { + @property() + doc!: XMLDocument; + @property({ type: Number }) + editCount = -1; + @state() + docString: string = ''; + + create() { + const bay2 = this.doc.querySelector('Bay[name="B1"]')!; + const newLnode = this.doc.createElement('LNode'); + newLnode.setAttribute('desc', 'Create Test'); + + const edit: Insert = { + parent: bay2, + node: newLnode, + reference: null + } + + const editEvent = newEditEvent(edit) + + this.dispatchEvent(editEvent); + + this.updateDoc(); + } + + delete() { + const bay2 = this.doc.querySelector('Bay[name="B2"]')!; + + const edit: Remove = { + node: bay2 + } + + this.dispatchEvent(newEditEvent(edit)); + + this.updateDoc(); + } + + setAttribute(): void { + const bay1 = this.doc.querySelector('Bay[name="B1"]')!; + + const edit: SetAttributes = { + element: bay1, + attributes: { + desc: 'New description' + }, + attributesNS: { + nsAttribute: { + 'sxy:x': 'New xmlTest' + } + } + } + + this.dispatchEvent(newEditEvent(edit)); + + this.updateDoc(); + } + + setTextcontent(): void { + const bay1 = this.doc.querySelector('Bay[name="B1"]')!; + + const edit: SetTextContent = { + element: bay1, + textContent: 'New text content' + } + + this.dispatchEvent(newEditEvent(edit)); + + this.updateDoc(); + } + + updateDoc() { + this.docString = new XMLSerializer().serializeToString(this.doc); + } + + render(): TemplateResult { + return html`
+

Edit Test

+
+ this.create()}> + Create + + this.setAttribute()}> + Set Attributes + + this.setTextcontent()}> + Set Textcontent + + this.delete()}> + Delete + +
+
+
${this.docString}
+
+
`; + } + + static styles = css` + .edit-test { + margin: 24px; + } + `; +} \ No newline at end of file From d22d203727c1e49ce089831c39f1cb8ec9a8540b Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Tue, 10 Dec 2024 10:48:58 +0100 Subject: [PATCH 02/16] chore: Rename exports --- packages/core/foundation.ts | 44 +++++------ .../core/foundation/edit-completed-event.ts | 6 +- packages/core/foundation/edit-event.ts | 20 ++--- packages/core/foundation/edit.ts | 52 ++++++------- packages/core/foundation/handle-edit.ts | 26 +++---- packages/openscd/src/addons/Editor.ts | 76 +++++++++---------- packages/openscd/src/addons/History.ts | 6 +- .../addons/editor/edit-v1-to-v2-converter.ts | 18 ++--- packages/plugins/src/editors/EditTest.ts | 18 ++--- 9 files changed, 133 insertions(+), 133 deletions(-) diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 501e917534..153d4db5b0 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -7,39 +7,39 @@ export { newOpenEvent } from './foundation/open-event.js'; export type { OpenEvent, OpenDetail } from './foundation/open-event.js'; export { - newEditEvent as newEditEventV1, - isComplex as isComplexV1, - isInsert as isInsertV1, - isNamespaced as isNamespacedV1, - isUpdate as isUpdateV1, - isRemove as isRemoveV1, + newEditEvent, + isComplex, + isInsert, + isNamespaced, + isUpdate, + isRemove, } from './foundation/deprecated/edit-event.js'; export type { - EditEvent as EditEventV1, - Edit as EditV1, - Insert as InsertV1, + EditEvent, + Edit, + Insert, AttributeValue, NamespacedAttributeValue, - Update as UpdateV1, - Remove as RemoveV1, + Update, + Remove, } from './foundation/deprecated/edit-event.js'; export type { - Edit, - Insert, - Remove, - SetTextContent, - SetAttributes, - isEdit + EditV2, + InsertV2, + RemoveV2, + SetTextContentV2, + SetAttributesV2, + isEditV2 } from './foundation/edit.js'; export type { - EditEvent, - EditEventOptions, - EditDetail + EditEventV2, + EditEventOptionsV2, + EditDetailV2 } from './foundation/edit-event.js'; -export { newEditEvent } from './foundation/edit-event.js'; +export { newEditEventV2 } from './foundation/edit-event.js'; -export { handleEdit } from './foundation/handle-edit.js'; +export { handleEditV2 } from './foundation/handle-edit.js'; export { cyrb64 } from './foundation/cyrb64.js'; diff --git a/packages/core/foundation/edit-completed-event.ts b/packages/core/foundation/edit-completed-event.ts index 8a7a12a088..ed8ad8df08 100644 --- a/packages/core/foundation/edit-completed-event.ts +++ b/packages/core/foundation/edit-completed-event.ts @@ -1,9 +1,9 @@ -import { Edit as EditV1, Initiator } from './deprecated/edit-event.js'; +import { Edit, Initiator } from './deprecated/edit-event.js'; import { EditorAction } from './deprecated/editor.js'; export type EditCompletedDetail = { - edit: EditV1 | EditorAction; + edit: Edit | EditorAction; initiator: Initiator; }; @@ -11,7 +11,7 @@ export type EditCompletedDetail = { export type EditCompletedEvent = CustomEvent; export function newEditCompletedEvent( - edit: EditV1 | EditorAction, + edit: Edit | EditorAction, initiator: Initiator = 'user' ): EditCompletedEvent { return new CustomEvent('oscd-edit-completed', { diff --git a/packages/core/foundation/edit-event.ts b/packages/core/foundation/edit-event.ts index b33d799a7b..65237975f9 100644 --- a/packages/core/foundation/edit-event.ts +++ b/packages/core/foundation/edit-event.ts @@ -1,25 +1,25 @@ -import { Edit } from './edit.js'; +import { EditV2 } from './edit.js'; -export type EditDetail = { +export type EditDetailV2 = { edit: E; title?: string; squash?: boolean; }; -export type EditEvent = CustomEvent< - EditDetail +export type EditEventV2 = CustomEvent< + EditDetailV2 >; -export type EditEventOptions = { +export type EditEventOptionsV2 = { title?: string; squash?: boolean; }; -export function newEditEvent( +export function newEditEventV2( edit: E, - options?: EditEventOptions -): EditEvent { - return new CustomEvent>('oscd-edit-v2', { + options?: EditEventOptionsV2 +): EditEventV2 { + return new CustomEvent>('oscd-edit-v2', { composed: true, bubbles: true, detail: { ...options, edit }, @@ -28,6 +28,6 @@ export function newEditEvent( declare global { interface ElementEventMap { - ['oscd-edit-v2']: EditEvent; + ['oscd-edit-v2']: EditEventV2; } } diff --git a/packages/core/foundation/edit.ts b/packages/core/foundation/edit.ts index 36e9836721..a264295e7d 100644 --- a/packages/core/foundation/edit.ts +++ b/packages/core/foundation/edit.ts @@ -1,73 +1,73 @@ /** Intent to `parent.insertBefore(node, reference)` */ -export type Insert = { +export type InsertV2 = { parent: Node; node: Node; reference: Node | null; }; /** Intent to remove a `node` from its `ownerDocument` */ -export type Remove = { +export type RemoveV2 = { node: Node; }; /** Intent to set the `textContent` of `element` */ -export type SetTextContent = { +export type SetTextContentV2 = { element: Element; textContent: string; }; /** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */ -export type SetAttributes = { +export type SetAttributesV2 = { element: Element; attributes: Partial>; attributesNS: Partial>>>; }; /** Intent to change some XMLDocuments */ -export type Edit = - | Insert - | SetAttributes - | SetTextContent - | Remove - | Edit[]; +export type EditV2 = + | InsertV2 + | SetAttributesV2 + | SetTextContentV2 + | RemoveV2 + | EditV2[]; -export function isComplex(edit: Edit): edit is Edit[] { +export function isComplex(edit: EditV2): edit is EditV2[] { return edit instanceof Array; } -export function isSetTextContent(edit: Edit): edit is SetTextContent { +export function isSetTextContent(edit: EditV2): edit is SetTextContentV2 { return ( - (edit as SetTextContent).element !== undefined && - (edit as SetTextContent).textContent !== undefined + (edit as SetTextContentV2).element !== undefined && + (edit as SetTextContentV2).textContent !== undefined ); } -export function isRemove(edit: Edit): edit is Remove { +export function isRemove(edit: EditV2): edit is RemoveV2 { return ( - (edit as Insert).parent === undefined && (edit as Remove).node !== undefined + (edit as InsertV2).parent === undefined && (edit as RemoveV2).node !== undefined ); } -export function isSetAttributes(edit: Edit): edit is SetAttributes { +export function isSetAttributes(edit: EditV2): edit is SetAttributesV2 { return ( - (edit as SetAttributes).element !== undefined && - (edit as SetAttributes).attributes !== undefined && - (edit as SetAttributes).attributesNS !== undefined + (edit as SetAttributesV2).element !== undefined && + (edit as SetAttributesV2).attributes !== undefined && + (edit as SetAttributesV2).attributesNS !== undefined ); } -export function isInsert(edit: Edit): edit is Insert { +export function isInsert(edit: EditV2): edit is InsertV2 { return ( - (edit as Insert).parent !== undefined && - (edit as Insert).node !== undefined && - (edit as Insert).reference !== undefined + (edit as InsertV2).parent !== undefined && + (edit as InsertV2).node !== undefined && + (edit as InsertV2).reference !== undefined ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isEdit(edit: any): edit is Edit { +export function isEditV2(edit: any): edit is EditV2 { if (isComplex(edit)) { - return !edit.some((e) => !isEdit(e)); + return !edit.some((e) => !isEditV2(e)); } return ( diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts index 74b843d90f..222cc4aad6 100644 --- a/packages/core/foundation/handle-edit.ts +++ b/packages/core/foundation/handle-edit.ts @@ -1,23 +1,23 @@ import { - Edit, - Insert, + EditV2, + InsertV2, isComplex, isInsert, isRemove, isSetAttributes, isSetTextContent, - Remove, - SetAttributes, - SetTextContent, + RemoveV2, + SetAttributesV2, + SetTextContentV2, } from './edit.js'; function handleSetTextContent({ element, textContent, -}: SetTextContent): (SetTextContent | Insert)[] { +}: SetTextContentV2): (SetTextContentV2 | InsertV2)[] { const { childNodes } = element; - const restoreChildNodes: Insert[] = Array.from(childNodes).map((node) => ({ + const restoreChildNodes: InsertV2[] = Array.from(childNodes).map((node) => ({ parent: element, node, reference: null, @@ -25,7 +25,7 @@ function handleSetTextContent({ element.textContent = textContent; - const undoTextContent: SetTextContent = { element, textContent: '' }; + const undoTextContent: SetTextContentV2 = { element, textContent: '' }; return [undoTextContent, ...restoreChildNodes]; } @@ -53,7 +53,7 @@ function handleSetAttributes({ element, attributes, attributesNS, -}: SetAttributes): SetAttributes { +}: SetAttributesV2): SetAttributesV2 { const oldAttributes = { ...attributes }; const oldAttributesNS = { ...attributesNS }; @@ -129,7 +129,7 @@ return { }; } -function handleRemove({ node }: Remove): Insert | [] { +function handleRemove({ node }: RemoveV2): InsertV2 | [] { const { parentNode: parent, nextSibling: reference } = node; node.parentNode?.removeChild(node); if (parent) @@ -145,7 +145,7 @@ function handleInsert({ parent, node, reference, -}: Insert): Insert | Remove | [] { +}: InsertV2): InsertV2 | RemoveV2 | [] { try { const { parentNode, nextSibling } = node; parent.insertBefore(node, reference); @@ -167,12 +167,12 @@ function handleInsert({ } /** Applies an Edit, returning the corresponding 'undo' Edit. */ -export function handleEdit(edit: Edit): Edit { +export function handleEditV2(edit: EditV2): EditV2 { if (isInsert(edit)) return handleInsert(edit); if (isRemove(edit)) return handleRemove(edit); if (isSetAttributes(edit)) return handleSetAttributes(edit); if (isSetTextContent(edit)) return handleSetTextContent(edit); - if (isComplex(edit)) return edit.map((edit) => handleEdit(edit)).reverse(); + if (isComplex(edit)) return edit.map((edit) => handleEditV2(edit)).reverse(); return []; } diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 702c05ae10..05b00d0435 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -1,10 +1,10 @@ import { - Edit, - EditEvent, + EditV2, + EditEventV2, OpenEvent, newEditCompletedEvent, - newEditEventV1, - handleEdit + newEditEvent, + handleEditV2 } from '@openscd/core'; import { property, @@ -26,16 +26,16 @@ import { OpenDocEvent } from '@openscd/core/foundation/deprecated/open-event.js' import { AttributeValue, - EditV1, - EditEventV1, - InsertV1, - isComplexV1, - isInsertV1, - isNamespacedV1, - isRemoveV1, - isUpdateV1, - RemoveV1, - UpdateV1, + Edit, + EditEvent, + Insert, + isComplex, + isInsert, + isNamespaced, + isRemove, + isUpdate, + Remove, + Update, } from '@openscd/core'; import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @@ -55,21 +55,21 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - private getLogText(edit: EditV1): { title: string, message?: string } { - if (isInsertV1(edit)) { + private getLogText(edit: Edit): { title: string, message?: string } { + if (isInsert(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.created', { name }) }; - } else if (isUpdateV1(edit)) { + } else if (isUpdate(edit)) { const name = edit.element.tagName; return { title: get('editing.updated', { name }) }; - } else if (isRemoveV1(edit)) { + } else if (isRemove(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.deleted', { name }) }; - } else if (isComplexV1(edit)) { + } else if (isComplex(edit)) { const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); return { title: get('editing.complex'), message }; } @@ -81,7 +81,7 @@ export class OscdEditor extends LitElement { const edit = convertEditV1toV2(event.detail.action); const initiator = event.detail.initiator; - this.host.dispatchEvent(newEditEventV1(edit, initiator)); + this.host.dispatchEvent(newEditEvent(edit, initiator)); } /** @@ -126,14 +126,14 @@ export class OscdEditor extends LitElement { return html``; } - handleEditEventV2(event: EditEvent) { + handleEditEventV2(event: EditEventV2) { console.log('Edit event v2', event); const edit = event.detail.edit; - const undoEdit = handleEdit(edit); + const undoEdit = handleEditV2(edit); } - async handleEditEvent(event: EditEventV1) { + async handleEditEvent(event: EditEvent) { /** * This is a compatibility fix for plugins based on open energy tools edit events * because their edit event look slightly different @@ -169,11 +169,11 @@ export class OscdEditor extends LitElement { } } -function handleEditV1(edit: EditV1): EditV1 { - if (isInsertV1(edit)) return handleInsert(edit); - if (isUpdateV1(edit)) return handleUpdate(edit); - if (isRemoveV1(edit)) return handleRemove(edit); - if (isComplexV1(edit)) return edit.map(handleEditV1).reverse(); +function handleEditV1(edit: Edit): Edit { + if (isInsert(edit)) return handleInsert(edit); + if (isUpdate(edit)) return handleUpdate(edit); + if (isRemove(edit)) return handleRemove(edit); + if (isComplex(edit)) return edit.map(handleEditV1).reverse(); return []; } @@ -185,7 +185,7 @@ function handleInsert({ parent, node, reference, -}: InsertV1): InsertV1 | RemoveV1 | [] { +}: Insert): Insert | Remove | [] { try { const { parentNode, nextSibling } = node; @@ -212,13 +212,13 @@ function handleInsert({ } } -function handleUpdate({ element, attributes }: UpdateV1): UpdateV1 { +function handleUpdate({ element, attributes }: Update): Update { const oldAttributes = { ...attributes }; Object.entries(attributes) .reverse() .forEach(([name, value]) => { let oldAttribute: AttributeValue; - if (isNamespacedV1(value!)) + if (isNamespaced(value!)) oldAttribute = { value: element.getAttributeNS( value.namespaceURI, @@ -238,7 +238,7 @@ function handleUpdate({ element, attributes }: UpdateV1): UpdateV1 { for (const entry of Object.entries(attributes)) { try { const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespacedV1(value)) { + if (isNamespaced(value)) { if (value.value === null) element.removeAttributeNS( value.namespaceURI, @@ -258,7 +258,7 @@ function handleUpdate({ element, attributes }: UpdateV1): UpdateV1 { }; } -function handleRemove({ node }: RemoveV1): InsertV1 | [] { +function handleRemove({ node }: Remove): Insert | [] { const { parentNode: parent, nextSibling: reference } = node; node.parentNode?.removeChild(node); if (parent) @@ -271,11 +271,11 @@ function handleRemove({ node }: RemoveV1): InsertV1 | [] { } function isOpenEnergyEditEvent(event: CustomEvent): boolean { - const eventDetail = event.detail as EditV1; - return isComplexV1(eventDetail) || isInsertV1(eventDetail) || isUpdateV1(eventDetail) || isRemoveV1(eventDetail); + const eventDetail = event.detail as Edit; + return isComplex(eventDetail) || isInsert(eventDetail) || isUpdate(eventDetail) || isRemove(eventDetail); } -function convertOpenEnergyEditEventToEditEvent(event: CustomEvent): EditEventV1 { - const eventDetail = event.detail as EditV1; - return newEditEventV1(eventDetail); +function convertOpenEnergyEditEventToEditEvent(event: CustomEvent): EditEvent { + const eventDetail = event.detail as Edit; + return newEditEvent(eventDetail); } diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index dfa8be438a..a6db95729a 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { newEditEventV1 } from '@openscd/core'; +import { newEditEvent } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -213,7 +213,7 @@ export class OscdHistory extends LitElement { if (!this.canUndo) return false; const undoEdit = (this.history[this.editCount]).undo; - this.host.dispatchEvent(newEditEventV1(undoEdit, 'undo')); + this.host.dispatchEvent(newEditEvent(undoEdit, 'undo')); this.setEditCount(this.previousAction); return true; @@ -222,7 +222,7 @@ export class OscdHistory extends LitElement { if (!this.canRedo) return false; const redoEdit = (this.history[this.nextAction]).redo; - this.host.dispatchEvent(newEditEventV1(redoEdit, 'redo')); + this.host.dispatchEvent(newEditEvent(redoEdit, 'redo')); this.setEditCount(this.nextAction); return true; diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index cb6988e73b..f33d76d27f 100644 --- a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -13,11 +13,11 @@ import { SimpleAction, Update } from '@openscd/core/foundation/deprecated/editor.js'; -import { EditV1, InsertV1, RemoveV1, UpdateV1 as UpdateV2 } from '@openscd/core'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; import { getReference, SCLTag } from '../../foundation.js'; -export function convertEditV1toV2(action: EditorAction): EditV1 { +export function convertEditV1toV2(action: EditorAction): Edit { if (isSimple(action)) { return convertSimpleAction(action); } else { @@ -25,7 +25,7 @@ export function convertEditV1toV2(action: EditorAction): EditV1 { } } -function convertSimpleAction(action: SimpleAction): EditV1 { +function convertSimpleAction(action: SimpleAction): Edit { if (isCreate(action)) { return convertCreate(action); } else if (isDelete(action)) { @@ -41,7 +41,7 @@ function convertSimpleAction(action: SimpleAction): EditV1 { throw new Error('Unknown action type'); } -function convertCreate(action: Create): InsertV1 { +function convertCreate(action: Create): Insert { let reference: Node | null = null; if ( action.new.reference === undefined && @@ -63,7 +63,7 @@ function convertCreate(action: Create): InsertV1 { }; } -function convertDelete(action: Delete): RemoveV1 { +function convertDelete(action: Delete): Remove { return { node: action.old.element }; @@ -86,7 +86,7 @@ function convertUpdate(action: Update): UpdateV2 { }; } -function convertMove(action: Move): InsertV1 { +function convertMove(action: Move): Insert { if (action.new.reference === undefined) { action.new.reference = getReference( action.new.parent, @@ -101,7 +101,7 @@ function convertMove(action: Move): InsertV1 { } } -function convertReplace(action: Replace): EditV1 { +function convertReplace(action: Replace): Edit { const oldChildren = action.old.element.children; // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); @@ -116,8 +116,8 @@ function convertReplace(action: Replace): EditV1 { const reference = action.old.element.nextSibling; - const remove: RemoveV1 = { node: action.old.element }; - const insert: InsertV1 = { + const remove: Remove = { node: action.old.element }; + const insert: Insert = { parent, node: newNode, reference diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts index 6cc2949d8c..cc7d726ee0 100644 --- a/packages/plugins/src/editors/EditTest.ts +++ b/packages/plugins/src/editors/EditTest.ts @@ -1,4 +1,4 @@ -import { Insert, newEditEvent, Remove, SetAttributes, SetTextContent } from '@openscd/core'; +import { InsertV2, newEditEventV2, RemoveV2, SetAttributesV2, SetTextContentV2 } from '@openscd/core'; import { LitElement, html, TemplateResult, property, css, state } from 'lit-element'; export default class SubstationPlugin extends LitElement { @@ -14,13 +14,13 @@ export default class SubstationPlugin extends LitElement { const newLnode = this.doc.createElement('LNode'); newLnode.setAttribute('desc', 'Create Test'); - const edit: Insert = { + const edit: InsertV2 = { parent: bay2, node: newLnode, reference: null } - const editEvent = newEditEvent(edit) + const editEvent = newEditEventV2(edit) this.dispatchEvent(editEvent); @@ -30,11 +30,11 @@ export default class SubstationPlugin extends LitElement { delete() { const bay2 = this.doc.querySelector('Bay[name="B2"]')!; - const edit: Remove = { + const edit: RemoveV2 = { node: bay2 } - this.dispatchEvent(newEditEvent(edit)); + this.dispatchEvent(newEditEventV2(edit)); this.updateDoc(); } @@ -42,7 +42,7 @@ export default class SubstationPlugin extends LitElement { setAttribute(): void { const bay1 = this.doc.querySelector('Bay[name="B1"]')!; - const edit: SetAttributes = { + const edit: SetAttributesV2 = { element: bay1, attributes: { desc: 'New description' @@ -54,7 +54,7 @@ export default class SubstationPlugin extends LitElement { } } - this.dispatchEvent(newEditEvent(edit)); + this.dispatchEvent(newEditEventV2(edit)); this.updateDoc(); } @@ -62,12 +62,12 @@ export default class SubstationPlugin extends LitElement { setTextcontent(): void { const bay1 = this.doc.querySelector('Bay[name="B1"]')!; - const edit: SetTextContent = { + const edit: SetTextContentV2 = { element: bay1, textContent: 'New text content' } - this.dispatchEvent(newEditEvent(edit)); + this.dispatchEvent(newEditEventV2(edit)); this.updateDoc(); } From 908a931f8241dacce4365b2b4fc4412436a782f5 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 11 Dec 2024 13:26:34 +0100 Subject: [PATCH 03/16] feat: Add converter and use edit api v3 --- packages/core/foundation.ts | 9 +- .../core/foundation/deprecated/history.ts | 7 +- packages/core/foundation/edit-event.ts | 10 +- packages/core/foundation/edit.ts | 20 +-- packages/core/foundation/handle-edit.ts | 20 +-- packages/openscd/src/addons/Editor.ts | 170 ++++-------------- packages/openscd/src/addons/History.ts | 6 +- .../editor/edit-action-to-v1-converter.ts | 130 ++++++++++++++ .../addons/editor/edit-v1-to-v2-converter.ts | 152 +++------------- packages/plugins/src/editors/EditTest.ts | 2 +- 10 files changed, 239 insertions(+), 287 deletions(-) create mode 100644 packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts diff --git a/packages/core/foundation.ts b/packages/core/foundation.ts index 153d4db5b0..4b34b7b25d 100644 --- a/packages/core/foundation.ts +++ b/packages/core/foundation.ts @@ -30,7 +30,14 @@ export type { RemoveV2, SetTextContentV2, SetAttributesV2, - isEditV2 +} from './foundation/edit.js'; +export { + isEditV2, + isRemoveV2, + isInsertV2, + isComplexV2, + isSetAttributesV2, + isSetTextContentV2 } from './foundation/edit.js'; export type { EditEventV2, diff --git a/packages/core/foundation/deprecated/history.ts b/packages/core/foundation/deprecated/history.ts index 0a0667595d..b78f919432 100644 --- a/packages/core/foundation/deprecated/history.ts +++ b/packages/core/foundation/deprecated/history.ts @@ -1,4 +1,4 @@ -import { Edit } from './edit-event.js'; +import { EditV2 } from '../edit.js'; type InfoEntryKind = 'info' | 'warning' | 'error'; @@ -12,8 +12,9 @@ export interface LogDetailBase { /** The [[`LogEntry`]] for a committed [[`EditorAction`]]. */ export interface CommitDetail extends LogDetailBase { kind: 'action'; - redo: Edit; - undo: Edit; + redo: EditV2; + undo: EditV2; + squash?: boolean; } /** A [[`LogEntry`]] for notifying the user. */ export interface InfoDetail extends LogDetailBase { diff --git a/packages/core/foundation/edit-event.ts b/packages/core/foundation/edit-event.ts index 65237975f9..79b1a62155 100644 --- a/packages/core/foundation/edit-event.ts +++ b/packages/core/foundation/edit-event.ts @@ -1,18 +1,20 @@ import { EditV2 } from './edit.js'; -export type EditDetailV2 = { +export type EditDetailV2 = EditEventOptionsV2 & { edit: E; - title?: string; - squash?: boolean; }; export type EditEventV2 = CustomEvent< EditDetailV2 >; -export type EditEventOptionsV2 = { +type BaseEditEventOptionsV2 = { title?: string; squash?: boolean; +} + +export type EditEventOptionsV2 = BaseEditEventOptionsV2 & { + createHistoryEntry?: boolean; }; export function newEditEventV2( diff --git a/packages/core/foundation/edit.ts b/packages/core/foundation/edit.ts index a264295e7d..cce63f144d 100644 --- a/packages/core/foundation/edit.ts +++ b/packages/core/foundation/edit.ts @@ -31,24 +31,24 @@ export type EditV2 = | RemoveV2 | EditV2[]; -export function isComplex(edit: EditV2): edit is EditV2[] { +export function isComplexV2(edit: EditV2): edit is EditV2[] { return edit instanceof Array; } -export function isSetTextContent(edit: EditV2): edit is SetTextContentV2 { +export function isSetTextContentV2(edit: EditV2): edit is SetTextContentV2 { return ( (edit as SetTextContentV2).element !== undefined && (edit as SetTextContentV2).textContent !== undefined ); } -export function isRemove(edit: EditV2): edit is RemoveV2 { +export function isRemoveV2(edit: EditV2): edit is RemoveV2 { return ( (edit as InsertV2).parent === undefined && (edit as RemoveV2).node !== undefined ); } -export function isSetAttributes(edit: EditV2): edit is SetAttributesV2 { +export function isSetAttributesV2(edit: EditV2): edit is SetAttributesV2 { return ( (edit as SetAttributesV2).element !== undefined && (edit as SetAttributesV2).attributes !== undefined && @@ -56,7 +56,7 @@ export function isSetAttributes(edit: EditV2): edit is SetAttributesV2 { ); } -export function isInsert(edit: EditV2): edit is InsertV2 { +export function isInsertV2(edit: EditV2): edit is InsertV2 { return ( (edit as InsertV2).parent !== undefined && (edit as InsertV2).node !== undefined && @@ -66,14 +66,14 @@ export function isInsert(edit: EditV2): edit is InsertV2 { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function isEditV2(edit: any): edit is EditV2 { - if (isComplex(edit)) { + if (isComplexV2(edit)) { return !edit.some((e) => !isEditV2(e)); } return ( - isSetAttributes(edit) || - isSetTextContent(edit) || - isInsert(edit) || - isRemove(edit) + isSetAttributesV2(edit) || + isSetTextContentV2(edit) || + isInsertV2(edit) || + isRemoveV2(edit) ); } diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts index 222cc4aad6..89d8638df0 100644 --- a/packages/core/foundation/handle-edit.ts +++ b/packages/core/foundation/handle-edit.ts @@ -1,11 +1,11 @@ import { EditV2, InsertV2, - isComplex, - isInsert, - isRemove, - isSetAttributes, - isSetTextContent, + isComplexV2, + isInsertV2, + isRemoveV2, + isSetAttributesV2, + isSetTextContentV2, RemoveV2, SetAttributesV2, SetTextContentV2, @@ -168,11 +168,11 @@ function handleInsert({ /** Applies an Edit, returning the corresponding 'undo' Edit. */ export function handleEditV2(edit: EditV2): EditV2 { - if (isInsert(edit)) return handleInsert(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isSetAttributes(edit)) return handleSetAttributes(edit); - if (isSetTextContent(edit)) return handleSetTextContent(edit); - if (isComplex(edit)) return edit.map((edit) => handleEditV2(edit)).reverse(); + if (isInsertV2(edit)) return handleInsert(edit); + if (isRemoveV2(edit)) return handleRemove(edit); + if (isSetAttributesV2(edit)) return handleSetAttributes(edit); + if (isSetTextContentV2(edit)) return handleSetTextContent(edit); + if (isComplexV2(edit)) return edit.map((edit) => handleEditV2(edit)).reverse(); return []; } diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 05b00d0435..9fe8423c2f 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -4,7 +4,13 @@ import { OpenEvent, newEditCompletedEvent, newEditEvent, - handleEditV2 + handleEditV2, + isInsertV2, + isRemoveV2, + isSetAttributesV2, + isSetTextContentV2, + isComplexV2, + newEditEventV2 } from '@openscd/core'; import { property, @@ -38,6 +44,7 @@ import { Update, } from '@openscd/core'; +import { convertEditActiontoV1 } from './editor/edit-action-to-v1-converter.js'; import { convertEditV1toV2 } from './editor/edit-v1-to-v2-converter.js'; @customElement('oscd-editor') @@ -55,21 +62,21 @@ export class OscdEditor extends LitElement { }) host!: HTMLElement; - private getLogText(edit: Edit): { title: string, message?: string } { - if (isInsert(edit)) { + private getLogText(edit: EditV2): { title: string, message?: string } { + if (isInsertV2(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.created', { name }) }; - } else if (isUpdate(edit)) { + } else if (isSetAttributesV2(edit) || isSetTextContentV2(edit)) { const name = edit.element.tagName; return { title: get('editing.updated', { name }) }; - } else if (isRemove(edit)) { + } else if (isRemoveV2(edit)) { const name = edit.node instanceof Element ? edit.node.tagName : get('editing.node'); return { title: get('editing.deleted', { name }) }; - } else if (isComplex(edit)) { + } else if (isComplexV2(edit)) { const message = edit.map(e => this.getLogText(e)).map(({ title }) => title).join(', '); return { title: get('editing.complex'), message }; } @@ -78,10 +85,26 @@ export class OscdEditor extends LitElement { } private onAction(event: EditorActionEvent) { - const edit = convertEditV1toV2(event.detail.action); - const initiator = event.detail.initiator; + const edit = convertEditActiontoV1(event.detail.action); + const editV2 = convertEditV1toV2(edit); - this.host.dispatchEvent(newEditEvent(edit, initiator)); + this.host.dispatchEvent(newEditEventV2(editV2)); + } + + handleEditEvent(event: EditEvent) { + /** + * This is a compatibility fix for plugins based on open energy tools edit events + * because their edit event look slightly different + * see https://github.com/OpenEnergyTools/open-scd-core/blob/main/foundation/edit-event-v1.ts for details + */ + if (isOpenEnergyEditEvent(event)) { + event = convertOpenEnergyEditEventToEditEvent(event); + } + + const edit = event.detail.edit; + const editV2 = convertEditV1toV2(edit); + + this.host.dispatchEvent(newEditEventV2(editV2)); } /** @@ -115,8 +138,9 @@ export class OscdEditor extends LitElement { // Deprecated editor action API, use 'oscd-edit' instead. this.host.addEventListener('editor-action', this.onAction.bind(this)); - + // Deprecated edit event API, use 'oscd-edit-v2' instead. this.host.addEventListener('oscd-edit', event => this.handleEditEvent(event)); + this.host.addEventListener('oscd-edit-v2', event => this.handleEditEventV2(event)); this.host.addEventListener('open-doc', this.onOpenDoc); this.host.addEventListener('oscd-open', this.handleOpenDoc); @@ -126,41 +150,24 @@ export class OscdEditor extends LitElement { return html``; } - handleEditEventV2(event: EditEventV2) { + async handleEditEventV2(event: EditEventV2) { console.log('Edit event v2', event); const edit = event.detail.edit; const undoEdit = handleEditV2(edit); - } - - async handleEditEvent(event: EditEvent) { - /** - * This is a compatibility fix for plugins based on open energy tools edit events - * because their edit event look slightly different - * see https://github.com/OpenEnergyTools/open-scd-core/blob/main/foundation/edit-event-v1.ts for details - */ - if (isOpenEnergyEditEvent(event)) { - event = convertOpenEnergyEditEventToEditEvent(event); - } - - const edit = event.detail.edit; - const undoEdit = handleEditV1(edit); - - this.dispatchEvent( - newEditCompletedEvent(event.detail.edit, event.detail.initiator) - ); - const shouldCreateHistoryEntry = event.detail.initiator !== 'redo' && event.detail.initiator !== 'undo'; + const shouldCreateHistoryEntry = event.detail.createHistoryEntry !== false; if (shouldCreateHistoryEntry) { const { title, message } = this.getLogText(edit); this.dispatchEvent(newLogEvent({ kind: 'action', - title, + title: event.detail.title ?? title, message, redo: edit, undo: undoEdit, + squash: event.detail.squash })); } @@ -169,107 +176,6 @@ export class OscdEditor extends LitElement { } } -function handleEditV1(edit: Edit): Edit { - if (isInsert(edit)) return handleInsert(edit); - if (isUpdate(edit)) return handleUpdate(edit); - if (isRemove(edit)) return handleRemove(edit); - if (isComplex(edit)) return edit.map(handleEditV1).reverse(); - return []; -} - -function localAttributeName(attribute: string): string { - return attribute.includes(':') ? attribute.split(':', 2)[1] : attribute; -} - -function handleInsert({ - parent, - node, - reference, -}: Insert): Insert | Remove | [] { - try { - const { parentNode, nextSibling } = node; - - /** - * This is a workaround for converted edit api v1 events, - * because if multiple edits are converted, they are converted before the changes from the previous edits are applied to the document - * so if you first remove an element and then add a clone with changed attributes, the reference will be the element to remove since it hasnt been removed yet - */ - if (!parent.contains(reference)) { - reference = null; - } - - parent.insertBefore(node, reference); - if (parentNode) - return { - node, - parent: parentNode, - reference: nextSibling, - }; - return { node }; - } catch (e) { - // do nothing if insert doesn't work on these nodes - return []; - } -} - -function handleUpdate({ element, attributes }: Update): Update { - const oldAttributes = { ...attributes }; - Object.entries(attributes) - .reverse() - .forEach(([name, value]) => { - let oldAttribute: AttributeValue; - if (isNamespaced(value!)) - oldAttribute = { - value: element.getAttributeNS( - value.namespaceURI, - localAttributeName(name) - ), - namespaceURI: value.namespaceURI, - }; - else - oldAttribute = element.getAttributeNode(name)?.namespaceURI - ? { - value: element.getAttribute(name), - namespaceURI: element.getAttributeNode(name)!.namespaceURI!, - } - : element.getAttribute(name); - oldAttributes[name] = oldAttribute; - }); - for (const entry of Object.entries(attributes)) { - try { - const [attribute, value] = entry as [string, AttributeValue]; - if (isNamespaced(value)) { - if (value.value === null) - element.removeAttributeNS( - value.namespaceURI, - localAttributeName(attribute) - ); - else element.setAttributeNS(value.namespaceURI, attribute, value.value); - } else if (value === null) element.removeAttribute(attribute); - else element.setAttribute(attribute, value); - } catch (e) { - // do nothing if update doesn't work on this attribute - delete oldAttributes[entry[0]]; - } - } - return { - element, - attributes: oldAttributes, - }; -} - -function handleRemove({ node }: Remove): Insert | [] { - const { parentNode: parent, nextSibling: reference } = node; - node.parentNode?.removeChild(node); - if (parent) - return { - node, - parent, - reference, - }; - return []; -} - function isOpenEnergyEditEvent(event: CustomEvent): boolean { const eventDetail = event.detail as Edit; return isComplex(eventDetail) || isInsert(eventDetail) || isUpdate(eventDetail) || isRemove(eventDetail); diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index a6db95729a..ac9e4d86ec 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { newEditEvent } from '@openscd/core'; +import { newEditEventV2 } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -213,7 +213,7 @@ export class OscdHistory extends LitElement { if (!this.canUndo) return false; const undoEdit = (this.history[this.editCount]).undo; - this.host.dispatchEvent(newEditEvent(undoEdit, 'undo')); + this.host.dispatchEvent(newEditEventV2(undoEdit, { createHistoryEntry: false })); this.setEditCount(this.previousAction); return true; @@ -222,7 +222,7 @@ export class OscdHistory extends LitElement { if (!this.canRedo) return false; const redoEdit = (this.history[this.nextAction]).redo; - this.host.dispatchEvent(newEditEvent(redoEdit, 'redo')); + this.host.dispatchEvent(newEditEventV2(redoEdit, { createHistoryEntry: false })); this.setEditCount(this.nextAction); return true; diff --git a/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts b/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts new file mode 100644 index 0000000000..8dadf86bed --- /dev/null +++ b/packages/openscd/src/addons/editor/edit-action-to-v1-converter.ts @@ -0,0 +1,130 @@ +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { getReference, SCLTag } from '../../foundation.js'; + + +export function convertEditActiontoV1(action: EditorAction): Edit { + if (isSimple(action)) { + return convertSimpleAction(action); + } else { + return action.actions.map(convertSimpleAction); + } +} + +function convertSimpleAction(action: SimpleAction): Edit { + if (isCreate(action)) { + return convertCreate(action); + } else if (isDelete(action)) { + return convertDelete(action); + } else if (isUpdate(action)) { + return convertUpdate(action); + } else if (isMove(action)) { + return convertMove(action); + } else if (isReplace(action)) { + return convertReplace(action); + } + + throw new Error('Unknown action type'); +} + +function convertCreate(action: Create): Insert { + let reference: Node | null = null; + if ( + action.new.reference === undefined && + action.new.element instanceof Element && + action.new.parent instanceof Element + ) { + reference = getReference( + action.new.parent, + action.new.element.tagName + ); + } else { + reference = action.new.reference ?? null; + } + + return { + parent: action.new.parent, + node: action.new.element, + reference + }; +} + +function convertDelete(action: Delete): Remove { + return { + node: action.old.element + }; +} + +function convertUpdate(action: Update): UpdateV2 { + const oldAttributesToRemove: Record = {}; + Array.from(action.element.attributes).forEach(attr => { + oldAttributesToRemove[attr.name] = null; + }); + + const attributes = { + ...oldAttributesToRemove, + ...action.newAttributes + }; + + return { + element: action.element, + attributes + }; +} + +function convertMove(action: Move): Insert { + if (action.new.reference === undefined) { + action.new.reference = getReference( + action.new.parent, + action.old.element.tagName + ); + } + + return { + parent: action.new.parent, + node: action.old.element, + reference: action.new.reference ?? null + } +} + +function convertReplace(action: Replace): Edit { + const oldChildren = action.old.element.children; + // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent + const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); + + const newNode = action.new.element.cloneNode(true) as Element; + newNode.append(...Array.from(copiedChildren)); + const parent = action.old.element.parentElement; + + if (!parent) { + throw new Error('Replace action called without parent in old element'); + } + + const reference = action.old.element.nextSibling; + + const remove: Remove = { node: action.old.element }; + const insert: Insert = { + parent, + node: newNode, + reference + }; + + return [ + remove, + insert + ]; +} diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index f33d76d27f..8b7e4f9211 100644 --- a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -1,130 +1,36 @@ -import { - Create, - Delete, - EditorAction, - isCreate, - isDelete, - isMove, - isReplace, - isSimple, - isUpdate, - Move, - Replace, - SimpleAction, - Update -} from '@openscd/core/foundation/deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; -import { getReference, SCLTag } from '../../foundation.js'; - - -export function convertEditV1toV2(action: EditorAction): Edit { - if (isSimple(action)) { - return convertSimpleAction(action); - } else { - return action.actions.map(convertSimpleAction); - } -} - -function convertSimpleAction(action: SimpleAction): Edit { - if (isCreate(action)) { - return convertCreate(action); - } else if (isDelete(action)) { - return convertDelete(action); - } else if (isUpdate(action)) { - return convertUpdate(action); - } else if (isMove(action)) { - return convertMove(action); - } else if (isReplace(action)) { - return convertReplace(action); - } - - throw new Error('Unknown action type'); -} - -function convertCreate(action: Create): Insert { - let reference: Node | null = null; - if ( - action.new.reference === undefined && - action.new.element instanceof Element && - action.new.parent instanceof Element - ) { - reference = getReference( - action.new.parent, - action.new.element.tagName - ); +import { Edit, EditV2, isComplex, isInsert, isNamespaced, isRemove, isUpdate, Update } from '@openscd/core'; + +export function convertEditV1toV2(edit: Edit): EditV2 { + if (isComplex(edit)) { + return edit.map(convertEditV1toV2); + } else if (isRemove(edit)) { + return edit as EditV2; + } else if (isInsert(edit)) { + return edit as EditV2; + } else if (isUpdate(edit)) { + return convertUpdate(edit); } else { - reference = action.new.reference ?? null; + throw new Error('Unknown edit type'); } - - return { - parent: action.new.parent, - node: action.new.element, - reference - }; } -function convertDelete(action: Delete): Remove { - return { - node: action.old.element - }; -} - -function convertUpdate(action: Update): UpdateV2 { - const oldAttributesToRemove: Record = {}; - Array.from(action.element.attributes).forEach(attr => { - oldAttributesToRemove[attr.name] = null; +function convertUpdate(edit: Update): EditV2 { + const attributes: Partial> = {}; + const attributesNS: Partial< + Record>> + > = {}; + + Object.entries(edit.attributes).forEach(([key, value]) => { + if (isNamespaced(value!)) { + const ns = value.namespaceURI; + if (!ns) return; + + if (!attributesNS[ns]) { + attributesNS[ns] = {}; + } + attributesNS[ns][key] = value.value; + } else attributes[key] = value; }); - const attributes = { - ...oldAttributesToRemove, - ...action.newAttributes - }; - - return { - element: action.element, - attributes - }; -} - -function convertMove(action: Move): Insert { - if (action.new.reference === undefined) { - action.new.reference = getReference( - action.new.parent, - action.old.element.tagName - ); - } - - return { - parent: action.new.parent, - node: action.old.element, - reference: action.new.reference ?? null - } -} - -function convertReplace(action: Replace): Edit { - const oldChildren = action.old.element.children; - // We have to clone the children, because otherwise undoing the action would remove the children from the old element, because append removes the old parent - const copiedChildren = Array.from(oldChildren).map(e => e.cloneNode(true)); - - const newNode = action.new.element.cloneNode(true) as Element; - newNode.append(...Array.from(copiedChildren)); - const parent = action.old.element.parentElement; - - if (!parent) { - throw new Error('Replace action called without parent in old element'); - } - - const reference = action.old.element.nextSibling; - - const remove: Remove = { node: action.old.element }; - const insert: Insert = { - parent, - node: newNode, - reference - }; - - return [ - remove, - insert - ]; + return { element: edit.element, attributes, attributesNS }; } diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts index cc7d726ee0..42cc705752 100644 --- a/packages/plugins/src/editors/EditTest.ts +++ b/packages/plugins/src/editors/EditTest.ts @@ -20,7 +20,7 @@ export default class SubstationPlugin extends LitElement { reference: null } - const editEvent = newEditEventV2(edit) + const editEvent = newEditEventV2(edit, { title: 'Hello Test' }) this.dispatchEvent(editEvent); From 0027dff5238c100197495eb06607e85ec3869690 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 12 Dec 2024 11:31:20 +0100 Subject: [PATCH 04/16] feat: Add squash to history --- packages/openscd/src/addons/History.ts | 58 +++++++++++++++++++++++- packages/plugins/src/editors/EditTest.ts | 2 +- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index ac9e4d86ec..62e1a3fe51 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -38,7 +38,7 @@ import { import { getFilterIcon, iconColors } from '../icons/icons.js'; import { Plugin } from '../plugin.js'; -import { newEditEventV2 } from '@openscd/core'; +import { EditV2, isComplexV2, newEditEventV2 } from '@openscd/core'; export const historyStateEvent = 'history-state'; export interface HistoryState { @@ -238,11 +238,65 @@ export class OscdHistory extends LitElement { this.history.splice(this.nextAction); } - this.history.push(entry); + this.addHistoryEntry(entry); this.setEditCount(this.history.length - 1); this.requestUpdate('history', []); } + private addHistoryEntry(entry: CommitEntry) { + const shouldSquash = Boolean(entry.squash) && this.history.length > 0; + + if (shouldSquash) { + const previousEntry = this.history.pop() as CommitEntry; + const squashedEntry = this.squashHistoryEntries(entry, previousEntry); + this.history.push(squashedEntry); + } else { + this.history.push(entry); + } + } + + private squashHistoryEntries(current: CommitEntry, previous: CommitEntry): CommitEntry { + const title = current.title ?? previous.title; + const message = current.message ?? previous.message; + + const undo = this.squashUndo(current.undo, previous.undo); + const redo = this.squashRedo(current.redo, previous.redo); + + return { + ...current, + title, + message, + undo, + redo + }; + } + + private squashUndo(current: EditV2, previous: EditV2): EditV2 { + const isCurrentComplex = isComplexV2(current); + const isPreviousComplex = isComplexV2(previous); + + const previousUndos: EditV2[] = isPreviousComplex ? previous : [ previous ]; + const currentUndos: EditV2[] = isCurrentComplex ? current : [ current ]; + + return [ + ...currentUndos, + ...previousUndos + ]; + } + + private squashRedo(current: EditV2, previous: EditV2): EditV2 { + const isCurrentComplex = isComplexV2(current); + const isPreviousComplex = isComplexV2(previous); + + const previousRedos: EditV2[] = isPreviousComplex ? previous : [ previous ]; + const currentRedos: EditV2[] = isCurrentComplex ? current : [ current ]; + + return [ + ...previousRedos, + ...currentRedos + ]; + } + private onReset() { this.log = []; this.history = []; diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts index 42cc705752..7dd8e78c81 100644 --- a/packages/plugins/src/editors/EditTest.ts +++ b/packages/plugins/src/editors/EditTest.ts @@ -54,7 +54,7 @@ export default class SubstationPlugin extends LitElement { } } - this.dispatchEvent(newEditEventV2(edit)); + this.dispatchEvent(newEditEventV2(edit, { squash: true })); this.updateDoc(); } From 8bc54737c4db1ed0f59ef4d0afe277e701279e9a Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 12 Dec 2024 12:10:54 +0100 Subject: [PATCH 05/16] feat: Use current title --- packages/openscd/src/addons/History.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index 62e1a3fe51..c16930d586 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -256,16 +256,11 @@ export class OscdHistory extends LitElement { } private squashHistoryEntries(current: CommitEntry, previous: CommitEntry): CommitEntry { - const title = current.title ?? previous.title; - const message = current.message ?? previous.message; - const undo = this.squashUndo(current.undo, previous.undo); const redo = this.squashRedo(current.redo, previous.redo); return { ...current, - title, - message, undo, redo }; From 504085ee93fad046194eb43a76f02b994c2929f4 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 8 Jan 2025 13:56:08 +0100 Subject: [PATCH 06/16] chore: Fix build --- .../addons/editor/edit-v1-to-v2-converter.ts | 2 +- packages/openscd/src/plugins.ts | 8 -- packages/openscd/test/unit/Editor.test.ts | 11 -- .../test/unit/edit-v1-to-v2-converter.test.ts | 12 +- packages/plugins/src/editors/EditTest.ts | 107 ------------------ 5 files changed, 7 insertions(+), 133 deletions(-) delete mode 100644 packages/plugins/src/editors/EditTest.ts diff --git a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts index 8b7e4f9211..fd5909f4e5 100644 --- a/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts +++ b/packages/openscd/src/addons/editor/edit-v1-to-v2-converter.ts @@ -28,7 +28,7 @@ function convertUpdate(edit: Update): EditV2 { if (!attributesNS[ns]) { attributesNS[ns] = {}; } - attributesNS[ns][key] = value.value; + attributesNS[ns]![key] = value.value; } else attributes[key] = value; }); diff --git a/packages/openscd/src/plugins.ts b/packages/openscd/src/plugins.ts index 5465cbd689..0c33dfa6c1 100644 --- a/packages/openscd/src/plugins.ts +++ b/packages/openscd/src/plugins.ts @@ -3,14 +3,6 @@ function generatePluginPath(plugin: string): string { } export const officialPlugins = [ - { - name: 'Edit Test', - src: generatePluginPath('plugins/src/editors/EditTest.js'), - icon: 'margin', - default: true, - kind: 'editor', - requireDoc: true, - }, { name: 'IED', src: generatePluginPath('plugins/src/editors/IED.js'), diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts index f26079472a..427890b96a 100644 --- a/packages/openscd/test/unit/Editor.test.ts +++ b/packages/openscd/test/unit/Editor.test.ts @@ -310,17 +310,6 @@ describe('OSCD-Editor', () => { expect(logEntry.redo).to.deep.equal(remove); }); - it('should not log edit for undo or redo event', () => { - const remove: Remove = { - node: bay2, - }; - - host.dispatchEvent(newEditEvent(remove, 'redo')); - host.dispatchEvent(newEditEvent(remove, 'undo')); - - expect(log).to.have.lengthOf(0); - }); - describe('validate after edit', () => { let hasTriggeredValidate = false; beforeEach(() => { diff --git a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts index 6676a1ffed..e52341155c 100644 --- a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts +++ b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts @@ -18,7 +18,7 @@ import { } from '@openscd/core/foundation/deprecated/editor.js'; import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; -import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter.js'; +import { convertEditActiontoV1 } from '../../src/addons/editor/edit-action-to-v1-converter.js'; describe('edit-v1-to-v2-converter', () => { @@ -44,7 +44,7 @@ describe('edit-v1-to-v2-converter', () => { } }; - const remove = convertEditV1toV2(deleteAction); + const remove = convertEditActiontoV1(deleteAction); const expectedRemove: Remove = { node: bay @@ -64,7 +64,7 @@ describe('edit-v1-to-v2-converter', () => { } }; - const insert = convertEditV1toV2(createAction); + const insert = convertEditActiontoV1(createAction); const expectedInsert: Insert = { parent: substation, @@ -81,7 +81,7 @@ describe('edit-v1-to-v2-converter', () => { }; const updateAction = createUpdateAction(bay, newAttributes); - const updateV2 = convertEditV1toV2(updateAction); + const updateV2 = convertEditActiontoV1(updateAction); const expectedUpdateV2: UpdateV2 = { element: bay, @@ -107,7 +107,7 @@ describe('edit-v1-to-v2-converter', () => { } }; - const insert = convertEditV1toV2(moveAction); + const insert = convertEditActiontoV1(moveAction); const expectedInsert: Insert = { parent: substation2, @@ -131,7 +131,7 @@ describe('edit-v1-to-v2-converter', () => { } }; - const [ remove, insert ] = convertEditV1toV2(replace) as Edit[]; + const [ remove, insert ] = convertEditActiontoV1(replace) as Edit[]; const expectedRemove: Remove = { node: bay diff --git a/packages/plugins/src/editors/EditTest.ts b/packages/plugins/src/editors/EditTest.ts deleted file mode 100644 index 7dd8e78c81..0000000000 --- a/packages/plugins/src/editors/EditTest.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { InsertV2, newEditEventV2, RemoveV2, SetAttributesV2, SetTextContentV2 } from '@openscd/core'; -import { LitElement, html, TemplateResult, property, css, state } from 'lit-element'; - -export default class SubstationPlugin extends LitElement { - @property() - doc!: XMLDocument; - @property({ type: Number }) - editCount = -1; - @state() - docString: string = ''; - - create() { - const bay2 = this.doc.querySelector('Bay[name="B1"]')!; - const newLnode = this.doc.createElement('LNode'); - newLnode.setAttribute('desc', 'Create Test'); - - const edit: InsertV2 = { - parent: bay2, - node: newLnode, - reference: null - } - - const editEvent = newEditEventV2(edit, { title: 'Hello Test' }) - - this.dispatchEvent(editEvent); - - this.updateDoc(); - } - - delete() { - const bay2 = this.doc.querySelector('Bay[name="B2"]')!; - - const edit: RemoveV2 = { - node: bay2 - } - - this.dispatchEvent(newEditEventV2(edit)); - - this.updateDoc(); - } - - setAttribute(): void { - const bay1 = this.doc.querySelector('Bay[name="B1"]')!; - - const edit: SetAttributesV2 = { - element: bay1, - attributes: { - desc: 'New description' - }, - attributesNS: { - nsAttribute: { - 'sxy:x': 'New xmlTest' - } - } - } - - this.dispatchEvent(newEditEventV2(edit, { squash: true })); - - this.updateDoc(); - } - - setTextcontent(): void { - const bay1 = this.doc.querySelector('Bay[name="B1"]')!; - - const edit: SetTextContentV2 = { - element: bay1, - textContent: 'New text content' - } - - this.dispatchEvent(newEditEventV2(edit)); - - this.updateDoc(); - } - - updateDoc() { - this.docString = new XMLSerializer().serializeToString(this.doc); - } - - render(): TemplateResult { - return html`
-

Edit Test

-
- this.create()}> - Create - - this.setAttribute()}> - Set Attributes - - this.setTextcontent()}> - Set Textcontent - - this.delete()}> - Delete - -
-
-
${this.docString}
-
-
`; - } - - static styles = css` - .edit-test { - margin: 24px; - } - `; -} \ No newline at end of file From 4dadd20e8124f8f41f80b0494a0710d6b06e2683 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 8 Jan 2025 14:42:13 +0100 Subject: [PATCH 07/16] fix: Fix broken reference workaround --- packages/core/foundation/handle-edit.ts | 11 ++++++++++- packages/openscd/src/addons/Editor.ts | 1 - packages/openscd/src/addons/History.ts | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts index 89d8638df0..5b9b2835f8 100644 --- a/packages/core/foundation/handle-edit.ts +++ b/packages/core/foundation/handle-edit.ts @@ -148,7 +148,16 @@ function handleInsert({ }: InsertV2): InsertV2 | RemoveV2 | [] { try { const { parentNode, nextSibling } = node; - parent.insertBefore(node, reference); + + /** + * This is a workaround for converted edit api v1 events, + * because if multiple edits are converted, they are converted before the changes from the previous edits are applied to the document + * so if you first remove an element and then add a clone with changed attributes, the reference will be the element to remove since it hasnt been removed yet + */ + if (!parent.contains(reference)) { + reference = null; + } + if (parentNode) { // undo: move child node back to original place return { diff --git a/packages/openscd/src/addons/Editor.ts b/packages/openscd/src/addons/Editor.ts index 9fe8423c2f..70a07fca52 100644 --- a/packages/openscd/src/addons/Editor.ts +++ b/packages/openscd/src/addons/Editor.ts @@ -151,7 +151,6 @@ export class OscdEditor extends LitElement { } async handleEditEventV2(event: EditEventV2) { - console.log('Edit event v2', event); const edit = event.detail.edit; const undoEdit = handleEditV2(edit); diff --git a/packages/openscd/src/addons/History.ts b/packages/openscd/src/addons/History.ts index c16930d586..2b330facdf 100644 --- a/packages/openscd/src/addons/History.ts +++ b/packages/openscd/src/addons/History.ts @@ -270,8 +270,8 @@ export class OscdHistory extends LitElement { const isCurrentComplex = isComplexV2(current); const isPreviousComplex = isComplexV2(previous); - const previousUndos: EditV2[] = isPreviousComplex ? previous : [ previous ]; - const currentUndos: EditV2[] = isCurrentComplex ? current : [ current ]; + const previousUndos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; + const currentUndos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; return [ ...currentUndos, @@ -283,8 +283,8 @@ export class OscdHistory extends LitElement { const isCurrentComplex = isComplexV2(current); const isPreviousComplex = isComplexV2(previous); - const previousRedos: EditV2[] = isPreviousComplex ? previous : [ previous ]; - const currentRedos: EditV2[] = isCurrentComplex ? current : [ current ]; + const previousRedos: EditV2[] = (isPreviousComplex ? previous : [ previous ]) as EditV2[]; + const currentRedos: EditV2[] = (isCurrentComplex ? current : [ current ]) as EditV2[]; return [ ...previousRedos, From 0988a1ad4ee6ca264b6efa4a95937624526a45da Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Wed, 8 Jan 2025 16:01:00 +0100 Subject: [PATCH 08/16] fix: Fix insert --- packages/core/foundation/handle-edit.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/foundation/handle-edit.ts b/packages/core/foundation/handle-edit.ts index 5b9b2835f8..9122dc5d02 100644 --- a/packages/core/foundation/handle-edit.ts +++ b/packages/core/foundation/handle-edit.ts @@ -158,6 +158,8 @@ function handleInsert({ reference = null; } + parent.insertBefore(node, reference); + if (parentNode) { // undo: move child node back to original place return { From 9a319454e223731b0aba72ae655f583549c43648 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 9 Jan 2025 14:36:23 +0100 Subject: [PATCH 09/16] test: Adjust tests to V2 --- packages/openscd/test/unit/Editor.test.ts | 170 +++++++++++++--------- 1 file changed, 98 insertions(+), 72 deletions(-) diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts index 427890b96a..2f3553fec8 100644 --- a/packages/openscd/test/unit/Editor.test.ts +++ b/packages/openscd/test/unit/Editor.test.ts @@ -2,7 +2,17 @@ import { html, fixture, expect } from '@open-wc/testing'; import '../../src/addons/Editor.js'; import { OscdEditor } from '../../src/addons/Editor.js'; -import { Insert, newEditEvent, Remove, Update } from '@openscd/core'; +import { + Insert, + InsertV2, + newEditEvent, + newEditEventV2, + Remove, + Update, + SetAttributesV2, + SetTextContentV2, + RemoveV2 +} from '@openscd/core'; import { CommitDetail, LogDetail } from '@openscd/core/foundation/deprecated/history.js'; @@ -63,13 +73,13 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); @@ -80,13 +90,13 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const newNodeFromScd = scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]'); @@ -94,13 +104,13 @@ describe('OSCD-Editor', () => { }); it('should move node when inserting existing node', () => { - const insertMove: Insert = { + const insertMove: InsertV2 = { parent: voltageLevel1, node: bay2, reference: null }; - host.dispatchEvent(newEditEvent(insertMove)); + host.dispatchEvent(newEditEventV2(insertMove)); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b2"]')).to.deep.equal(bay2); @@ -111,7 +121,7 @@ describe('OSCD-Editor', () => { node: bay1 }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b1"]')).to.be.null; }); @@ -125,12 +135,13 @@ describe('OSCD-Editor', () => { const oldAttributes = elementAttributesToMap(bay1); - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector('Bay[name="b1"]')!; @@ -147,12 +158,13 @@ describe('OSCD-Editor', () => { kind: null }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector('Bay[name="b1"]')!; @@ -168,12 +180,13 @@ describe('OSCD-Editor', () => { const oldAttributes = elementAttributesToMap(bay1); - const update: Update = { + const update: SetAttributesV2 = { element: bay1, - attributes: bay1NewAttributes + attributes: bay1NewAttributes, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); const updatedElement = scd.querySelector(`Bay[name="${bay1NewAttributes.name}"]`)!; @@ -187,67 +200,77 @@ describe('OSCD-Editor', () => { describe('namespaced attributes', () => { it('should update attribute with namespace', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newType', namespaceURI: 'xsi' } + attributes: { }, + attributesNS: { + [nsXsi]: { type: 'newType' } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - expect(lnode1.getAttributeNS('xsi', 'type')).to.equal('newType'); + expect(lnode1.getAttributeNS(nsXsi, 'type')).to.equal('newType'); }); it('should handle multiple namespaces', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newTypeXSI', namespaceURI: nsXsi } + attributes: { }, + attributesNS: { + [nsXsi]: { type: 'newTypeXSI' } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - const update2: Update = { + const update2: SetAttributesV2 = { element: lnode1, - attributes: { - type: { value: 'newTypeTD', namespaceURI: nsTd } + attributes: { }, + attributesNS: { + [nsTd]: { type: 'newTypeTD' } } }; - host.dispatchEvent(newEditEvent(update2)); + host.dispatchEvent(newEditEventV2(update2)); expect(lnode1.getAttributeNS(nsXsi, 'type')).to.equal('newTypeXSI'); expect(lnode1.getAttributeNS(nsTd, 'type')).to.equal('newTypeTD'); }); it('should remove namespaced attribute', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode2, - attributes: { - type: { value: null, namespaceURI: nsXsi } + attributes: { }, + attributesNS: { + [nsXsi]: { type: null } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; expect(lnode2.getAttributeNS(nsTd, 'type')).to.equal('typeTD'); }); it('should add and remove multiple normal and namespaced attributes', () => { - const update: Update = { + const update: SetAttributesV2 = { element: lnode2, attributes: { - type: { value: null, namespaceURI: nsXsi }, - kind: { value: 'td-kind', namespaceURI: nsTd }, normalAttribute: 'normalValue', lnClass: null + }, + attributesNS: { + [nsXsi]: { + type: null + }, + [nsTd]: { + kind: 'td-kind' + } } }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); expect(lnode2.getAttributeNS(nsXsi, 'type')).to.be.null; expect(lnode2.getAttributeNS(nsTd, 'kind')).to.equal('td-kind'); @@ -261,24 +284,25 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - const remove: Remove = { + const remove: RemoveV2 = { node: bay2 }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent([insert, remove, update])); + host.dispatchEvent(newEditEventV2([insert, remove, update])); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; @@ -296,12 +320,12 @@ describe('OSCD-Editor', () => { }); }); - it('should log edit for user event', () => { - const remove: Remove = { + it('should log edit by default', () => { + const remove: RemoveV2 = { node: bay2, }; - host.dispatchEvent(newEditEvent(remove, 'user')); + host.dispatchEvent(newEditEventV2(remove)); expect(log).to.have.lengthOf(1); const logEntry = log[0] as CommitDetail; @@ -321,11 +345,11 @@ describe('OSCD-Editor', () => { }); it('should dispatch validate event after edit', async () => { - const remove: Remove = { + const remove: RemoveV2 = { node: bay2, }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); await element.updateComplete; @@ -350,50 +374,51 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); - const undoInsert = log[0].undo as Remove; + const undoInsert = log[0].undo as RemoveV2; - host.dispatchEvent(newEditEvent(undoInsert)); + host.dispatchEvent(newEditEventV2(undoInsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; }); it('should undo remove', () => { - const remove: Remove = { + const remove: RemoveV2 = { node: bay4 }; - host.dispatchEvent(newEditEvent(remove)); + host.dispatchEvent(newEditEventV2(remove)); - const undoRemove = log[0].undo as Insert; + const undoRemove = log[0].undo as InsertV2; - host.dispatchEvent(newEditEvent(undoRemove)); + host.dispatchEvent(newEditEventV2(undoRemove)); const bay4FromScd = scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b4"]'); expect(bay4FromScd).to.deep.equal(bay4); }); it('should undo update', () => { - const update: Update = { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description', kind: 'superbay' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent(update)); + host.dispatchEvent(newEditEventV2(update)); - const undoUpdate = log[0].undo as Update; + const undoUpdate = log[0].undo as SetAttributesV2; - host.dispatchEvent(newEditEvent(undoUpdate)); + host.dispatchEvent(newEditEventV2(undoUpdate)); expect(bay1.getAttribute('desc')).to.be.null; expect(bay1.getAttribute('kind')).to.equal('bay'); @@ -403,22 +428,22 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: null }; - host.dispatchEvent(newEditEvent(insert)); + host.dispatchEvent(newEditEventV2(insert)); const undoIsert = log[0].undo; const redoInsert = log[0].redo; - host.dispatchEvent(newEditEvent(undoIsert)); + host.dispatchEvent(newEditEventV2(undoIsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; - host.dispatchEvent(newEditEvent(redoInsert)); + host.dispatchEvent(newEditEventV2(redoInsert)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); }); @@ -427,35 +452,36 @@ describe('OSCD-Editor', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); - const insert: Insert = { + const insert: InsertV2 = { parent: voltageLevel1, node: newNode, reference: bay1 }; - const remove: Remove = { + const remove: RemoveV2 = { node: bay2 }; - const update: Update = { + const update: SetAttributesV2 = { element: bay1, attributes: { desc: 'new description' - } + }, + attributesNS: {} }; - host.dispatchEvent(newEditEvent([insert, remove, update])); + host.dispatchEvent(newEditEventV2([insert, remove, update])); const undoComplex = log[0].undo; const redoComplex = log[0].redo; - host.dispatchEvent(newEditEvent(undoComplex)); + host.dispatchEvent(newEditEventV2(undoComplex)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.be.null; expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.deep.equal(bay2); expect(bay1.getAttribute('desc')).to.be.null; - host.dispatchEvent(newEditEvent(redoComplex)); + host.dispatchEvent(newEditEventV2(redoComplex)); expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b3"]')).to.deep.equal(newNode); expect(scd.querySelector('VoltageLevel[name="v2"] > Bay[name="b2"]')).to.be.null; From 28e32af0e0914a0c04b658bb2cf38bab867aaeeb Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 10 Jan 2025 12:25:13 +0100 Subject: [PATCH 10/16] test: Add tests --- packages/openscd/test/unit/Editor.test.ts | 52 ++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/openscd/test/unit/Editor.test.ts b/packages/openscd/test/unit/Editor.test.ts index 2f3553fec8..0b1cbd6d18 100644 --- a/packages/openscd/test/unit/Editor.test.ts +++ b/packages/openscd/test/unit/Editor.test.ts @@ -27,6 +27,7 @@ describe('OSCD-Editor', () => { let bay2: Element; let bay4: Element; let bay5: Element; + let bayWithoutTextContent: Element; let lnode1: Element; let lnode2: Element; @@ -49,6 +50,7 @@ describe('OSCD-Editor', () => { + `, 'application/xml', @@ -64,6 +66,7 @@ describe('OSCD-Editor', () => { bay2 = scd.querySelector('Bay[name="b2"]')!; bay4 = scd.querySelector('Bay[name="b4"]')!; bay5 = scd.querySelector('Bay[name="b5"]')!; + bayWithoutTextContent = scd.querySelector('Bay[name="bWithoutTextContent"]')!; lnode1 = scd.querySelector('LNode[name="l1"]')!; lnode2 = scd.querySelector('LNode[name="l2"]')!; }); @@ -126,7 +129,7 @@ describe('OSCD-Editor', () => { expect(scd.querySelector('VoltageLevel[name="v1"] > Bay[name="b1"]')).to.be.null; }); - describe('Update', () => { + describe('SetAttributes', () => { it('should add new attributes and leave old attributes', () => { const bay1NewAttributes = { desc: 'new description', @@ -279,6 +282,19 @@ describe('OSCD-Editor', () => { }); }); + describe('SetTextContent', () => { + it('should set text content', () => { + const update: SetTextContentV2 = { + element: bay1, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + expect(bay1.textContent).to.equal('new text'); + }); + }); + describe('Complex action', () => { it('should apply each edit from a complex edit', () => { const newNode = scd.createElement('Bay'); @@ -404,7 +420,7 @@ describe('OSCD-Editor', () => { expect(bay4FromScd).to.deep.equal(bay4); }); - it('should undo update', () => { + it('should undo set attributes', () => { const update: SetAttributesV2 = { element: bay1, attributes: { @@ -424,6 +440,38 @@ describe('OSCD-Editor', () => { expect(bay1.getAttribute('kind')).to.equal('bay'); }); + it('should undo set textcontent', () => { + const update: SetTextContentV2 = { + element: bayWithoutTextContent, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + const undoUpdate = log[0].undo as SetTextContentV2; + + host.dispatchEvent(newEditEventV2(undoUpdate)); + + expect(bayWithoutTextContent.textContent).to.be.empty; + }); + + it('should restore children when undoing set textcontent', () => { + const update: SetTextContentV2 = { + element: bay2, + textContent: 'new text' + }; + + host.dispatchEvent(newEditEventV2(update)); + + expect(bay2.children).to.be.empty; + + const undoUpdate = log[0].undo as SetTextContentV2; + + host.dispatchEvent(newEditEventV2(undoUpdate)); + + expect(bay2.children[0]).to.deep.equal(lnode2); + }); + it('should redo previously undone action', () => { const newNode = scd.createElement('Bay'); newNode.setAttribute('name', 'b3'); From 3b40cbefc03a094a2239c8ab55ee4e04ed117868 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 10 Jan 2025 12:52:16 +0100 Subject: [PATCH 11/16] test: Add tests --- .../unit/edit-action-to-v1-converter.test.ts | 148 ++++++++++++++++++ .../test/unit/edit-v1-to-v2-converter.test.ts | 144 ++++++++--------- 2 files changed, 209 insertions(+), 83 deletions(-) create mode 100644 packages/openscd/test/unit/edit-action-to-v1-converter.test.ts diff --git a/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts b/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts new file mode 100644 index 0000000000..b7ef2bc2e4 --- /dev/null +++ b/packages/openscd/test/unit/edit-action-to-v1-converter.test.ts @@ -0,0 +1,148 @@ +import { html, fixture, expect } from '@open-wc/testing'; + +import { + Create, + Delete, + EditorAction, + isCreate, + isDelete, + isMove, + isReplace, + isSimple, + isUpdate, + Move, + Replace, + SimpleAction, + Update, + createUpdateAction +} from '@openscd/core/foundation/deprecated/editor.js'; +import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; + +import { convertEditActiontoV1 } from '../../src/addons/editor/edit-action-to-v1-converter.js'; + + +describe('edit-action-to-v1-converter', () => { + const doc = new DOMParser().parseFromString( + ` + + + + + + `, + 'application/xml' + ); + const substation = doc.querySelector('Substation')!; + const substation2 = doc.querySelector('Substation[name="sub2"]')!; + const bay = doc.querySelector('Bay')!; + + it('should convert delete to remove', () => { + const deleteAction: Delete = { + old: { + parent: substation, + element: bay + } + }; + + const remove = convertEditActiontoV1(deleteAction); + + const expectedRemove: Remove = { + node: bay + }; + + expect(remove).to.deep.equal(expectedRemove); + }); + + it('should convert create to insert', () => { + const newBay = doc.createElement('Bay'); + newBay.setAttribute('name', 'bay2'); + + const createAction: Create = { + new: { + parent: substation, + element: newBay + } + }; + + const insert = convertEditActiontoV1(createAction); + + const expectedInsert: Insert = { + parent: substation, + node: newBay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert update to updateV2', () => { + const newAttributes = { + name: 'newBayName', + }; + const updateAction = createUpdateAction(bay, newAttributes); + + const updateV2 = convertEditActiontoV1(updateAction); + + const expectedUpdateV2: UpdateV2 = { + element: bay, + attributes: { + ...newAttributes, + desc: null + } + }; + + expect(updateV2).to.deep.equal(expectedUpdateV2); + }); + + it('should convert move to insert', () => { + const moveAction: Move = { + old: { + parent: substation, + element: bay, + reference: null + }, + new: { + parent: substation2, + reference: null + } + }; + + const insert = convertEditActiontoV1(moveAction); + + const expectedInsert: Insert = { + parent: substation2, + node: bay, + reference: null + }; + + expect(insert).to.deep.equal(expectedInsert); + }); + + it('should convert replace to complex action with remove and insert', () => { + const ied = doc.createElement('IED'); + ied.setAttribute('name', 'ied'); + + const replace: Replace = { + old: { + element: bay + }, + new: { + element: ied + } + }; + + const [ remove, insert ] = convertEditActiontoV1(replace) as Edit[]; + + const expectedRemove: Remove = { + node: bay + }; + const expectedInsert: Insert = { + parent: substation, + node: ied, + reference: bay.nextSibling + }; + + expect(remove).to.deep.equal(expectedRemove); + expect(insert).to.deep.equal(expectedInsert); + }); +}); diff --git a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts index e52341155c..da9bf79415 100644 --- a/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts +++ b/packages/openscd/test/unit/edit-v1-to-v2-converter.test.ts @@ -16,15 +16,17 @@ import { Update, createUpdateAction } from '@openscd/core/foundation/deprecated/editor.js'; -import { Edit, Insert, Remove, Update as UpdateV2 } from '@openscd/core'; +import { Edit, Insert, InsertV2, Remove, Update as UpdateV1, RemoveV2, SetAttributesV2 } from '@openscd/core'; -import { convertEditActiontoV1 } from '../../src/addons/editor/edit-action-to-v1-converter.js'; +import { convertEditV1toV2 } from '../../src/addons/editor/edit-v1-to-v2-converter'; describe('edit-v1-to-v2-converter', () => { + const nsXsi = 'urn:example.com'; + const doc = new DOMParser().parseFromString( ` - + @@ -35,114 +37,90 @@ describe('edit-v1-to-v2-converter', () => { const substation = doc.querySelector('Substation')!; const substation2 = doc.querySelector('Substation[name="sub2"]')!; const bay = doc.querySelector('Bay')!; - - it('should convert delete to remove', () => { - const deleteAction: Delete = { - old: { - parent: substation, - element: bay - } + + it('should keep remove as is', () => { + const remove: Remove = { + node: bay }; - - const remove = convertEditActiontoV1(deleteAction); - - const expectedRemove: Remove = { + + const removeV2 = convertEditV1toV2(remove); + + const expectedRemoveV2: RemoveV2 = { node: bay }; - - expect(remove).to.deep.equal(expectedRemove); + + expect(removeV2).to.deep.equal(expectedRemoveV2); }); - it('should convert create to insert', () => { + it('should keep insert as is', () => { const newBay = doc.createElement('Bay'); newBay.setAttribute('name', 'bay2'); - const createAction: Create = { - new: { - parent: substation, - element: newBay - } - }; - - const insert = convertEditActiontoV1(createAction); - - const expectedInsert: Insert = { + const insert: Insert = { + node: newBay, parent: substation, + reference: null + }; + + const insertV2 = convertEditV1toV2(insert); + + const expectedInsertV2: InsertV2 = { node: newBay, + parent: substation, reference: null }; - - expect(insert).to.deep.equal(expectedInsert); + + expect(insertV2).to.deep.equal(expectedInsertV2); }); - - it('should convert update to updateV2', () => { + + it('should convert update to set attributes', () => { const newAttributes = { name: 'newBayName', }; - const updateAction = createUpdateAction(bay, newAttributes); - - const updateV2 = convertEditActiontoV1(updateAction); - - const expectedUpdateV2: UpdateV2 = { + const update: UpdateV1 = { element: bay, - attributes: { - ...newAttributes, - desc: null - } + attributes: newAttributes + } + + const setAttributesV2 = convertEditV1toV2(update); + + const expectedSetAttributesV2: SetAttributesV2 = { + element: bay, + attributes: newAttributes, + attributesNS: {} }; - - expect(updateV2).to.deep.equal(expectedUpdateV2); + + expect(setAttributesV2).to.deep.equal(expectedSetAttributesV2); }); - it('should convert move to insert', () => { - const moveAction: Move = { - old: { - parent: substation, - element: bay, - reference: null - }, - new: { - parent: substation2, - reference: null + it('shoudl convert update with namespaced attributes', () => { + const newAttributes = { + name: 'newBayName', + type: { + value: 'new value', + namespaceURI: nsXsi } }; - const insert = convertEditActiontoV1(moveAction); - - const expectedInsert: Insert = { - parent: substation2, - node: bay, - reference: null - }; - - expect(insert).to.deep.equal(expectedInsert); - }); + const update: UpdateV1 = { + element: bay, + attributes: newAttributes + } - it('should convert replace to complex action with remove and insert', () => { - const ied = doc.createElement('IED'); - ied.setAttribute('name', 'ied'); + const setAttributesV2 = convertEditV1toV2(update); - const replace: Replace = { - old: { - element: bay + const expectedSetAttributesV2: SetAttributesV2 = { + element: bay, + attributes: { + name: 'newBayName' }, - new: { - element: ied + attributesNS: { + [nsXsi]: { + type: 'new value' + } } }; - const [ remove, insert ] = convertEditActiontoV1(replace) as Edit[]; - - const expectedRemove: Remove = { - node: bay - }; - const expectedInsert: Insert = { - parent: substation, - node: ied, - reference: bay.nextSibling - }; - - expect(remove).to.deep.equal(expectedRemove); - expect(insert).to.deep.equal(expectedInsert); + expect(setAttributesV2).to.deep.equal(expectedSetAttributesV2); }); }); From 8ca5e9ca828d9cff3642d675aaa7d4bb792eae57 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 10 Jan 2025 13:09:48 +0100 Subject: [PATCH 12/16] chore: Set github action ubuntu version to 22 --- .github/workflows/test-and-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 3b475e0e11..3f107044f3 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -3,7 +3,7 @@ on: pull_request jobs: test-and-build: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2.3.1 From 1334d008b6a958ea18e39e4222eefeefaec6466e Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 10 Jan 2025 15:57:47 +0100 Subject: [PATCH 13/16] doc: Add edit API v3 doc --- docs/core-api/edit-api.md | 103 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 3 deletions(-) diff --git a/docs/core-api/edit-api.md b/docs/core-api/edit-api.md index e5e3a459f9..97e27e457f 100644 --- a/docs/core-api/edit-api.md +++ b/docs/core-api/edit-api.md @@ -1,6 +1,6 @@ -# Edit Event API +# Edit Event API v2 -Open SCD offers an API for editing the scd document which can be used with [Html Custom Events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). The main Open SCD components listens to events of the type `oscd-edit`, applies the changes to the `doc` and updates the `editCount` property. +Open SCD offers an API for editing the scd document which can be used with [Html Custom Events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). The main Open SCD components listens to events of the type `oscd-edit-v2`, applies the changes to the `doc` and updates the `editCount` property. The edits to the `doc` will be done in place, e.g. the `doc` changes but will keep the same reference. If your plugin needs to react to changes in the doc, you should listen to changes in the `editCount` property. @@ -8,6 +8,103 @@ The edits to the `doc` will be done in place, e.g. the `doc` changes but will ke Open SCD core exports a factory function for edit events, so you do not have to build them manually. +```ts +function newEditEventV2( + edit: E, + options?: EditEventOptionsV2 +): EditEventV2 + +type EditV2 = InsertV2 | SetAttributesV2 | SetTextContentV2 | RemoveV2 | EditV2[]; + +interface EditEventOptionsV2 = { + title?: string; + squash?: boolean; + createHistoryEntry?: boolean; +}; +``` + +### EditEventOptionsV2 + +* `title` set a title to be shown in the history. +* `squash` squash edit with previous history entry, this is useful if you want to create multiple edits based on an user action, but need the updated `doc` before applying each edit. Defaults to `false`. +* `createHistoryEntry` decides whether a history for the `edit` should be created. Defaults to `true`. + +### Insert + +Insert events can be used to add new nodes or move existing nodes in the document. Since a node can only have one parent, using an insert on an existing node will replace it's previous parent with the new parent, essentially moving the node to a different position in the xml tree. + +If the reference is not `null`, the node will be inserted before the reference node. The reference has to be a child node of the parent. And if the reference is `null` the node will be added as the last child of the parent. + +```ts +interface InsertV2 { + parent: Node; + node: Node; + reference: Node | null; +} +``` + +### Remove + +This event will remove the node from the document. + +```ts +interface RemoveV2 { + node: Node; +} +``` + +### SetAttributes + +Sets attributes for the element, can set both regular and namespaced attributes. + +```ts +interface SetAttributesV2 { + element: Element; + attributes: Partial>; + attributesNS: Partial>>>; +} +``` + +To set a namespaced attribute see the following example. Here we are setting the attribute `exa:type` for the namespace `https://example.com` to `secondary`. + +```ts +const setNamespacedAttributes: SetAttributesV2 = { + element, + attributes: {}, + attributesNS: { + "https://example.com": { + "exa:type": "secondary" + } + } +} +``` + +### SetTextContent + +Sets the text content of the element, removes any other children. To remove text content you can pass `null` as value for `textContent`. + +```ts +interface SetTextContentV2 { + element: Element; + textContent: string; +} +``` + +## History + +All edit events with the option `createHistoryEntry` will create a history log entry and can be undone and redone through the history addon. + + +# Archives + +## Edit Event API v1 (deprecated) + +The edit event API v1 is still available and listens to events of the type `oscd-edit`. + +## Event factory + +Open SCD core exports a factory function for edit events, so you do not have to build them manually. + ```ts function newEditEvent( edit: E, @@ -158,7 +255,7 @@ With open SCD version **v0.36.0** and higher some editor action features are no --- -# Archives - Editor Action API (deprecated) +## Editor Action API (deprecated) ### Event factory From 852c7ab020b9650dd8cb26f16a6cfee547abe2b9 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Fri, 10 Jan 2025 16:10:24 +0100 Subject: [PATCH 14/16] doc: Update docs --- docs/core-api/edit-api.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/core-api/edit-api.md b/docs/core-api/edit-api.md index 97e27e457f..3029221682 100644 --- a/docs/core-api/edit-api.md +++ b/docs/core-api/edit-api.md @@ -90,6 +90,19 @@ interface SetTextContentV2 { } ``` +### Complex edits + +Complex edits can be used to apply multiple edits as a single event. This will create a single entry in the history. You can create complex edit events by passing an array of edit events to the `newEditEventV2` factory function. + +```ts +import { newEditEventV2 } from '@openscd/core'; + +const complexEditEvent = newEditEventV2([ insert, update, remove ]); + +someComponent.dispatchEvent(complexEditEvent); + +``` + ## History All edit events with the option `createHistoryEntry` will create a history log entry and can be undone and redone through the history addon. From 83d25321238dba51d67dd0938c619edd909cbbb8 Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 23 Jan 2025 12:39:36 +0100 Subject: [PATCH 15/16] chore: Add deprecation notice for edit event v1 --- .../core/foundation/deprecated/edit-event.ts | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/core/foundation/deprecated/edit-event.ts b/packages/core/foundation/deprecated/edit-event.ts index 1cd6649932..4abf0dae56 100644 --- a/packages/core/foundation/deprecated/edit-event.ts +++ b/packages/core/foundation/deprecated/edit-event.ts @@ -1,62 +1,103 @@ +/** + * @deprecated Use the new edit event V2 API instead. + */ export type Initiator = 'user' | 'system' | 'undo' | 'redo' | string; -/** Intent to `parent.insertBefore(node, reference)` */ +/** + * @deprecated Use the new edit event V2 API instead. + */ export type Insert = { parent: Node; node: Node; reference: Node | null; }; +/** + * @deprecated Use the new edit event V2 API instead. + */ export type NamespacedAttributeValue = { value: string | null; namespaceURI: string | null; }; +/** + * @deprecated Use the new edit event V2 API instead. + */ export type AttributeValue = string | null | NamespacedAttributeValue; -/** Intent to set or remove (if null) attributes on element */ +/** + * @deprecated Use the new edit event V2 API instead. + */ export type Update = { element: Element; attributes: Partial>; }; -/** Intent to remove a node from its ownerDocument */ +/** + * @deprecated Use the new edit event V2 API instead. + */ export type Remove = { node: Node; }; -/** Represents the user's intent to change an XMLDocument */ +/** + * @deprecated Use the new edit event V2 API instead. + */ export type Edit = Insert | Update | Remove | Edit[]; +/** + * @deprecated Use the new edit event V2 API instead. + */ export function isComplex(edit: Edit): edit is Edit[] { return edit instanceof Array; } +/** + * @deprecated Use the new edit event V2 API instead. + */ export function isInsert(edit: Edit): edit is Insert { return (edit as Insert).parent !== undefined; } +/** + * @deprecated Use the new edit event V2 API instead. + */ export function isNamespaced( value: AttributeValue ): value is NamespacedAttributeValue { return value !== null && typeof value !== 'string'; } +/** + * @deprecated Use the new edit event V2 API instead. + */ export function isUpdate(edit: Edit): edit is Update { return (edit as Update).element !== undefined; } +/** + * @deprecated Use the new edit event V2 API instead. + */ export function isRemove(edit: Edit): edit is Remove { return ( (edit as Insert).parent === undefined && (edit as Remove).node !== undefined ); } +/** + * @deprecated Use the new edit event V2 API instead. + */ export interface EditEventDetail { edit: E; initiator: Initiator; } +/** + * @deprecated Use the new edit event V2 API instead. + */ export type EditEvent = CustomEvent; +/** + * @deprecated Use the new edit event V2 API instead. + */ export function newEditEvent( edit: E, initiator: Initiator = 'user' From 84f8a34bce066c65c44267976009fe057a52a6be Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 23 Jan 2025 12:45:26 +0100 Subject: [PATCH 16/16] test: Fix test action --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe51a19b74..12bfc92f87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v2.3.1