Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions WORKLOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Worklog

## 2026-04-23

### Canvas actions — no constructor
- `canvas-actions.js`: `HashController` and initial `_busy` moved to class fields so the custom constructor can be dropped; `_sendIcon` is not a reactive property (set once in `firstUpdated` + `requestUpdate()`); dropped redundant `requestUpdate()` after `_busy` / `_error` changes (Lit `@state` assignments schedule updates).

### Canvas prose — undo/redo keymap
- `prose.js`: removed custom `handleUndo` / `handleRedo` that duplicated `yUndo` / `yRedo` from y-prosemirror (same pattern as `nx-editor-wysiwyg/utils/handlers.js` and da.live’s underlying commands).

## 2026-04-22

### Canvas prose — keymap order aligned with da.live
- `prose.js`: moved `keymap(baseKeymap)` to after `buildKeymap` + `handleTableBackspace` (and `codemark` after `baseKeymap`), matching `da-live/blocks/edit/prose/index.js`, so full-table delete with Backspace and Enter in lists behave like da.live.

### Canvas prose — plugins ported from da.live
- Added `nx2/blocks/canvas/nx-editor-doc/prose-plugins/`: `codemark`, `columnResizing` (from `da-y-wrapper`), `imageDrop`, `imageFocalPoint`, `tableSelectHandle`, `sectionPasteHandler`, `base64Uploader`, plus `sourceUploadContext`, `tableUtils`, `inlinesvg`, `focalPointDialog` (native `<dialog>`; no face-api).
- Wired plugins in `prose.js` for writable sessions; styles in `nx-editor-doc.css`. Upload paths derive from the editor `source` URL. Focal-point block metadata still loads from `https://da.live/.../da-library/helpers/`.

## 2026-04-21

### Canvas editor — selection toolbar + slash shared helpers
Expand Down
100 changes: 89 additions & 11 deletions nx2/blocks/canvas-actions/canvas-actions.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,119 @@
color: var(--s2-gray-800);
}

.canvas-actions button {
.canvas-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--s2-spacing-100);
}

.canvas-actions .right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--s2-spacing-75);
}

.preview-row {
display: inline-flex;
align-items: center;
gap: var(--s2-spacing-100);
flex-wrap: wrap;
justify-content: flex-end;
}

.preview-dropdown-btn {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-height: 32px;
padding: 0 16px;
width: 32px;
height: 32px;
padding: 0;
margin: 0;
border: none;
border-radius: var(--s2-corner-radius-800);
font-family: inherit;
font-size: var(--s2-body-size-s, 0.875rem);
font-weight: 600;
line-height: var(--s2-body-line-height, 1.5);
color: light-dark(var(--s2-gray-25), #fff);
background-color: var(--s2-blue-900);
cursor: pointer;
}

.canvas-actions button:focus-visible {
.preview-dropdown-btn:focus-visible {
outline: 2px solid var(--s2-blue-800);
outline-offset: 2px;
}

.canvas-actions button:disabled {
.preview-dropdown-btn:disabled {
color: var(--s2-gray-400);
background-color: var(--s2-gray-200);
cursor: not-allowed;
}

.canvas-actions button:hover:not(:disabled) {
.preview-dropdown-btn:hover:not(:disabled) {
background-color: var(--s2-blue-1000);
}

.canvas-actions button:active:not(:disabled) {
.preview-dropdown-btn:active:not(:disabled) {
background-color: var(--s2-blue-1100);
}

.preview-dropdown-icon {
display: inline-flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
}

.preview-dropdown-icon svg {
display: block;
width: 18px;
height: 18px;
}

.preview-dropdown-label {
white-space: nowrap;
}

.send-popover {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 148px;
}

.send-popover-item {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: var(--s2-spacing-75) var(--s2-spacing-100);
border: none;
border-radius: var(--s2-corner-radius-200);
background: none;
color: light-dark(var(--s2-gray-900), var(--s2-gray-800));
font-family: inherit;
font-size: var(--s2-body-size-s);
font-weight: 500;
line-height: var(--s2-body-line-height, 1.5);
text-align: left;
cursor: pointer;
}

.send-popover-item:hover {
background: light-dark(var(--s2-gray-200), var(--s2-gray-300));
}

.send-popover-item:focus-visible {
outline: 2px solid var(--s2-blue-800);
outline-offset: 1px;
}

.action-error {
margin: 0;
max-width: 280px;
font-size: var(--s2-body-size-xs, 0.75rem);
font-weight: 500;
color: var(--s2-negative-900, #c00);
text-align: right;
}
115 changes: 112 additions & 3 deletions nx2/blocks/canvas-actions/canvas-actions.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,128 @@
import { LitElement, html } from 'da-lit';
import { LitElement, html, nothing } from 'da-lit';

import { loadStyle } from '../../utils/utils.js';
import { loadStyle, HashController } from '../../utils/utils.js';
import {
buildAemPathFromHashState,
formatAemPreviewPublishError,
runAemPreviewOrPublish,
} from '../../utils/aem-preview-publish.js';
import { loadHrefSvg } from '../../utils/svg.js';
import '../shared/popover/popover.js';

const style = await loadStyle(import.meta.url);

const SEND_ICON_HREF = new URL('../img/icons/S2_Icon_Send_20_N.svg', import.meta.url).href;

class NXCanvasActions extends LitElement {
static properties = {
_busy: { state: true },
_error: { state: true },
};

_hash = new HashController(this);

_busy = false;

get _popover() {
return this.shadowRoot?.querySelector('nx-popover');
}

get _menuAnchor() {
return this.shadowRoot?.querySelector('.preview-dropdown-btn');
}

connectedCallback() {
super.connectedCallback();
this.shadowRoot.adoptedStyleSheets = [style];
}

async firstUpdated() {
this._sendIcon = await loadHrefSvg(SEND_ICON_HREF);
this.requestUpdate();
}

get _hashState() {
return this._hash.value;
}

_togglePreviewPopover(e) {
e.preventDefault();
if (!buildAemPathFromHashState(this._hashState) || this._busy) return;
const pop = this._popover;
const anchor = this._menuAnchor;
if (!pop || !anchor) return;
if (pop.open) {
pop.close();
} else {
pop.show({ anchor, placement: 'below' });
anchor.setAttribute('aria-expanded', 'true');
}
}

_onSendPopoverClose() {
this._menuAnchor?.setAttribute('aria-expanded', 'false');
}

_pickAem(action) {
if (action !== 'preview' && action !== 'publish') return;
this._popover?.close();
this._runAemAction(action);
}

async _runAemAction(action) {
const aemPath = buildAemPathFromHashState(this._hashState);
if (!aemPath || this._busy) return;

this._error = undefined;
this._busy = true;

const result = await runAemPreviewOrPublish({ aemPath, action });
if (!result.ok) {
this._error = formatAemPreviewPublishError(result.error);
this._busy = false;
return;
}

window.open(result.url, result.url);

this._busy = false;
Comment thread
sharanyavinod marked this conversation as resolved.
}

render() {
const hasDoc = Boolean(buildAemPathFromHashState(this._hashState));
const disabled = !hasDoc || this._busy;
const sendIcon = this._sendIcon
? html`<span class="preview-dropdown-icon" aria-hidden="true">${this._sendIcon.cloneNode(true)}</span>`
: nothing;

return html`
<div class="canvas-actions">
<button>Publish</button>
<div class="right">
<div class="preview-row">
<button
type="button"
class="preview-dropdown-btn"
aria-label="Preview and publish"
aria-haspopup="menu"
aria-expanded="false"
?disabled=${disabled}
@click=${this._togglePreviewPopover}
>
${sendIcon}
</button>
<nx-popover placement="below" @close=${this._onSendPopoverClose}>
<div class="send-popover" role="menu">
<button type="button" class="send-popover-item" role="menuitem" @click=${() => this._pickAem('preview')}>
Preview
</button>
<button type="button" class="send-popover-item" role="menuitem" @click=${() => this._pickAem('publish')}>
Publish
</button>
</div>
</nx-popover>
</div>
${this._error ? html`<p class="action-error" role="alert">${this._error}</p>` : nothing}
</div>
</div>
`;
}
Expand Down
13 changes: 13 additions & 0 deletions nx2/blocks/canvas/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function editorCtxFromHashState(state, fullPath) {
}

function syncCanvasEditorsToHash({ mountRoot, header, state }) {
header.undoAvailable = false;
header.redoAvailable = false;
const fullPath = buildCanvasDocPath(state);
if (!fullPath) {
removeCanvasEditors(mountRoot);
Expand Down Expand Up @@ -145,6 +147,12 @@ function installCanvasHeader(block) {
persistCanvasEditorView(view);
notifyCanvasEditorActive(canvasHeaderApplyTarget(block), view);
});
header.addEventListener('nx-canvas-undo', () => {
canvasEditorMountRoot(block).querySelector('nx-editor-doc')?.undo();
});
header.addEventListener('nx-canvas-redo', () => {
canvasEditorMountRoot(block).querySelector('nx-editor-doc')?.redo();
});
block.before(header);
return header;
}
Expand All @@ -155,6 +163,11 @@ export default async function decorate(block) {
const mountRoot = canvasEditorMountRoot(block);
mountRoot.classList.add('nx-canvas-editor-mount');

mountRoot.addEventListener('nx-editor-undo-state', (e) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future - maybe we should move all events out to constants and reuse so we have a clear view + avoid regressions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this would be a good refactoring task.

header.undoAvailable = e.detail?.canUndo ?? false;
header.redoAvailable = e.detail?.canRedo ?? false;
});

hashChange.subscribe((state) => {
syncCanvasEditorsToHash({ mountRoot, header, state });
});
Expand Down
5 changes: 1 addition & 4 deletions nx2/blocks/canvas/editor-utils/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ export function getInstrumentedHTML(view) {
const originalTables = view.dom.querySelectorAll('table');
const clonedTables = editorClone.querySelectorAll('table');
clonedTables.forEach((table, index) => {
const div = document.createElement('div');
div.className = 'tableWrapper';
table.insertAdjacentElement('afterend', div);
div.append(table);
const div = table.parentElement;
const blockMarker = document.createElement('div');
blockMarker.className = 'block-marker';
try {
Expand Down
13 changes: 10 additions & 3 deletions nx2/blocks/canvas/editor-utils/selection-toolbar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable import/no-unresolved -- importmap */
import { Plugin } from 'da-y-wrapper';
import { Plugin, NodeSelection } from 'da-y-wrapper';

const NON_TEXT_NODES = new Set(['table', 'image']);

export const TOOLBAR_PADDING_GAP = 64;

Expand All @@ -15,13 +17,18 @@ export function getSelectionToolbar() {
}

export function hideSelectionToolbar() {
toolbar?.hide();
toolbar?.hide?.();
}

function isNonTextSelection({ selection }) {
return selection instanceof NodeSelection
&& NON_TEXT_NODES.has(selection.node.type.name);
}

function syncToolbar(view) {
const tb = getSelectionToolbar();
if (tb.linkDialogOpen) return;
if (view.state.selection.empty) {
if (view.state.selection.empty || isNonTextSelection(view.state)) {
hideSelectionToolbar();
return;
}
Expand Down
Loading
Loading