Skip to content
Open
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
12 changes: 8 additions & 4 deletions blocks/canvas/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
persistCanvasEditorView,
} from './utils/view.js';
import { shouldAutoOpenAfterPanel } from './utils/panel.js';
import { toolbarController } from './editor-utils/toolbar-controller.js';
import './ew-canvas-header/ew-canvas-header.js';
import './ew-editor-doc/ew-editor-doc.js';
import './ew-editor-wysiwyg/ew-editor-wysiwyg.js';
Expand Down Expand Up @@ -39,6 +40,7 @@ function buildCanvasDocPath(state) {

function notifyCanvasEditorActive(view) {
const v = normalizeCanvasEditorView(view);
toolbarController.setEditorMode(v);
canvasBus.editorViewState.emit({ view: v });
}

Expand Down Expand Up @@ -117,15 +119,17 @@ async function syncCanvasEditorsToHash({ mountRoot, header, state }) {
return;
}
removeNotPermitted(mountRoot);
const canWrite = (session.permissions ?? []).some((p) => p === 'write');
const canWrite = session.permissions?.some((permission) => permission === 'write') === true;
header.authorized = true;
header.canWrite = canWrite;
const docEl = ensureNxEditorDoc(mountRoot);
docEl.session = session;
docEl.ctx = ctx;
const frameEl = ensureNxEditorWysiwyg(mountRoot);
frameEl.canWrite = canWrite;
frameEl.ctx = ctx;
const wysiwygEl = ensureNxEditorWysiwyg(mountRoot);
// Must be set before `ctx`: the ctx change reloads the iframe, and the quick-edit
// INIT payload sent on load reads `canWrite` to decide contenteditable.
wysiwygEl.canWrite = canWrite;
wysiwygEl.ctx = ctx;
finalizeSplitEditorMountOrder(mountRoot);
notifyCanvasEditorActive(header.editorView);
syncEditorSplitLayout({ mountRoot, view: header.editorView });
Expand Down
30 changes: 24 additions & 6 deletions blocks/canvas/editor-utils/editor-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,33 @@ import { TextSelection } from 'da-y-wrapper';
import prose2aem from '../../shared/prose2aem.js';
import { getNx } from '../../../scripts/utils.js';
import { daFetch, fetchDaConfigs, getFirstSheet } from '../../shared/utils.js';
import { getSelectionToolbar } from './selection-toolbar.js';
import { toolbarController } from './toolbar-controller.js';
import { MESSAGE_TYPES } from '../utils/quick-edit-messages.js';
import { canvasBus, registerEditorSelectEnricher } from '../utils/canvas-bus.js';

const { DA_CONTENT } = await import(`${getNx()}/utils/utils.js`);

/**
* Dispatch a transaction mirrored from the quick-edit iframe.
*
* `suppressRerender` stops the resulting update echoing straight back to the iframe
* that produced it. The reset is in a `finally` deliberately: stranding the flag
* `true` (a throw inside `dispatch`) silently stops the iframe receiving any
* further body updates for the rest of the session.
*
* No focus is faked here. The doc view's cursor broadcast is handled by
* `daCursorPlugin`'s `shouldBroadcast` predicate, which reads the active surface
* rather than `view.hasFocus()`.
*/
export function dispatchMirror(view, tr, ctx) {
ctx.suppressRerender = true;
try {
view.dispatch(tr);
} finally {
ctx.suppressRerender = false;
}
}

// --- state.js ---

function findInsertedRange(oldText, newText) {
Expand Down Expand Up @@ -96,12 +117,9 @@ export function updateState(data, ctx) {
const restoredTo = Math.min(selTo, maxPos);
tr.setSelection(TextSelection.create(tr.doc, restoredFrom, restoredTo));

ctx.suppressRerender = true;
view.dispatch(tr);
ctx.suppressRerender = false;
dispatchMirror(view, tr, ctx);

const tb = getSelectionToolbar();
if (tb.open && !tb.isInteracting) tb.requestUpdate();
toolbarController.refresh();

// Sync the updated node (with marks applied) back to the portal's mini editor.
// Without this, the portal's editor retains the plain-text version, so the next
Expand Down
118 changes: 22 additions & 96 deletions blocks/canvas/editor-utils/selection-toolbar.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
/* eslint-disable import/no-unresolved -- importmap */
import { Plugin, PluginKey, NodeSelection } from 'da-y-wrapper';
import { getTableBlockName, getTableBlockVariant } from './blocks.js';
import { canvasBus } from '../utils/canvas-bus.js';
import { toolbarController } from './toolbar-controller.js';

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

/** Editor views the selection/block toolbars may appear in. */
const TOOLBAR_EDITOR_VIEWS = new Set(['content', 'split', 'layout']);

/** Set on transactions that mirror WYSIWYG iframe text selection into ProseMirror. */
export const NX_QUICK_EDIT_IFRAME_SELECTION_META = 'nxQuickEditIframeSelection';

Expand All @@ -16,106 +13,44 @@ export const NX_QUICK_EDIT_CLEAR_IFRAME_SELECTION_ORIGIN_META = 'nxClearQuickEdi

const selectionToolbarOriginKey = new PluginKey('nxSelectionToolbarOrigin');

function getSelectionOriginFromIframe(state) {
export function getSelectionOriginFromIframe(state) {
return selectionToolbarOriginKey.getState(state)?.fromIframe ?? false;
}

let toolbar;
let componentLoaded;

let selectionToolbarCanWrite = false;

export function getSelectionToolbar() {
if (toolbar) return toolbar;
componentLoaded ??= import('../ew-selection-toolbar/ew-selection-toolbar.js');
toolbar = document.createElement('ew-selection-toolbar');
document.body.append(toolbar);
return toolbar;
}

let blockToolbar;
let blockComponentLoaded;

export function getBlockToolbar() {
if (blockToolbar) return blockToolbar;
blockComponentLoaded ??= import('../ew-block-toolbar/ew-block-toolbar.js');
blockToolbar = document.createElement('ew-block-toolbar');
document.body.append(blockToolbar);
return blockToolbar;
}

export function hideBlockToolbar() {
blockToolbar?.hide?.();
}

export function canShowSelectionToolbar() {
return selectionToolbarCanWrite;
}

export function setSelectionToolbarCtx({
org = null,
site = null,
sourceUrl = null,
canWrite = false,
} = {}) {
selectionToolbarCanWrite = canWrite === true;
const tb = getSelectionToolbar();
export function setSelectionToolbarCtx({ org = null, site = null, sourceUrl = null } = {}) {
const tb = toolbarController.ensureToolbar();
tb.org = org;
tb.site = site;
tb.sourceUrl = sourceUrl;
const blockTb = getBlockToolbar();
// The block toolbar needs org/site to resolve the block library (variants,
// "Replace block") — it serves both editing surfaces, so wire it here too.
const blockTb = toolbarController.ensureBlockToolbar();
blockTb.org = org;
blockTb.site = site;
}

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

export function openLinkDialog(view) {
getSelectionToolbar().openLinkDialog(view);
toolbarController.ensureToolbar().openLinkDialog(view);
}

export function openAltDialog() {
getSelectionToolbar().openAltDialog();
toolbarController.ensureToolbar().openAltDialog();
}

export function triggerAddImage() {
getSelectionToolbar().triggerAddImage();
toolbarController.ensureToolbar().triggerAddImage();
}

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

function syncToolbar(view, editorView, blockEditOpen) {
if (!view) return;
if (!selectionToolbarCanWrite) {
hideSelectionToolbar();
return;
}
const tb = getSelectionToolbar();
if (tb.linkDialogOpen || tb.altDialogOpen || tb.isInteracting) return;
if (isNonTextSelection(view.state)) {
// A block is selected — show the block toolbar in every editor view.
hideSelectionToolbar();
const blockTb = getBlockToolbar();
blockTb.view = view;
const { node } = view.state.selection;
blockTb.show(getTableBlockName(node), getTableBlockVariant(node));
return;
}
hideBlockToolbar();
// The text toolbar is only relevant when the doc editor is visible, and never
// for selections that originate in (and are already served by) the WYSIWYG iframe.
if (getSelectionOriginFromIframe(view.state)) return;
// In layout view the doc editor is hidden — except while the block-edit modal is open,
// which puts the (single-block) doc editor on screen.
if (editorView === 'layout' && !blockEditOpen) return;
if (!view.hasFocus()) return;
tb.view = view;
tb.show();
/** `{ name, variant }` when a whole block node is selected, else null. */
export function selectedBlockDescriptor(state) {
if (!isNonTextSelection(state)) return null;
const { node } = state.selection;
return { name: getTableBlockName(node), variant: getTableBlockVariant(node) };
}

export function createSelectionToolbarPlugin() {
Expand All @@ -133,26 +68,17 @@ export function createSelectionToolbarPlugin() {
},
},
view() {
// Track the active editor view and block-edit state off the canvas bus rather
// than querying ew-canvas-header / ew-editor-doc from the DOM. Both channels
// replay their last value, so a plugin created after the last emit still starts
// with the current state.
let editorView = 'layout';
let blockEditOpen = false;
const unsubscribeEditorView = canvasBus.editorViewState
.subscribe(({ view }) => { editorView = view; });
const unsubscribeBlockEdit = canvasBus.blockEditState
.subscribe(({ open }) => { blockEditOpen = open; });
return {
update(view) {
if (!blockEditOpen && !TOOLBAR_EDITOR_VIEWS.has(editorView)) return;
syncToolbar(view, editorView, blockEditOpen);
// Iframe-origin dispatches are owned by the wysiwyg handlers; the doc
// plugin only reports the *doc* selection, and never claims the surface
// (activation comes from real focus — see toolbar-controller.js).
if (getSelectionOriginFromIframe(view.state)) return;
const block = selectedBlockDescriptor(view.state);
toolbarController.setDocSelection({ showable: block === null, block });
},
destroy() {
unsubscribeEditorView();
unsubscribeBlockEdit();
hideSelectionToolbar();
hideBlockToolbar();
toolbarController.reset();
},
};
},
Expand Down
Loading
Loading