diff --git a/blocks/canvas/canvas.css b/blocks/canvas/canvas.css
index ced573f10..04765afe7 100644
--- a/blocks/canvas/canvas.css
+++ b/blocks/canvas/canvas.css
@@ -14,6 +14,11 @@ html:has(aside.panel[data-position="after"]:not([hidden])) ew-canvas-header::par
display: none;
}
+html:has(ew-canvas-header[has-conflicts]) aside.panel[data-position="before"] {
+ pointer-events: none;
+ opacity: 0.45;
+}
+
.fragment-content:has(nx-chat) {
height: 100%;
diff --git a/blocks/canvas/canvas.js b/blocks/canvas/canvas.js
index 257cd1711..c62ed8353 100644
--- a/blocks/canvas/canvas.js
+++ b/blocks/canvas/canvas.js
@@ -161,6 +161,7 @@ async function installCanvasHeader(block, { org, site }) {
header.editorView = await readInitialCanvasEditorView({ org, site });
canvasBus.editorViewRequest.subscribe(({ view: rawView }) => {
const view = normalizeCanvasEditorView(rawView);
+ header.editorView = view;
persistCanvasEditorView(view);
notifyCanvasEditorActive(view);
syncEditorSplitLayout({ mountRoot: canvasEditorMountRoot(block), view });
@@ -251,6 +252,13 @@ export default async function decorate(block) {
syncEditorSplitLayout({ mountRoot, view: header.editorView });
installEditorSplitDrag(mountRoot);
+ canvasBus.mergeConflictsState.subscribe((detail) => {
+ header.hasUnresolvedMergeConflicts = detail?.hasMergeConflicts ?? false;
+ if (header.hasUnresolvedMergeConflicts && header.editorView !== 'content') {
+ canvasBus.editorViewRequest.emit({ view: 'content' });
+ }
+ });
+
canvasBus.undoState.subscribe((detail) => {
header.undoAvailable = detail?.canUndo ?? false;
header.redoAvailable = detail?.canRedo ?? false;
diff --git a/blocks/canvas/editor-utils/prose-merge-conflicts.js b/blocks/canvas/editor-utils/prose-merge-conflicts.js
new file mode 100644
index 000000000..ab83d9e89
--- /dev/null
+++ b/blocks/canvas/editor-utils/prose-merge-conflicts.js
@@ -0,0 +1,20 @@
+import { Plugin } from 'da-y-wrapper';
+import { checkForLocNodes } from '../../edit/prose/diff/diff-utils.js';
+import { debounce } from '../../edit/utils/helpers.js';
+
+export function createMergeConflictsPlugin(onMergeConflictsChange, hostEl) {
+ const debouncedCheck = debounce((view) => {
+ onMergeConflictsChange?.(checkForLocNodes(view, hostEl));
+ }, 500);
+
+ return new Plugin({
+ view(editorView) {
+ debouncedCheck(editorView);
+ return {
+ update(view, prevState) {
+ if (view.state.doc !== prevState.doc) debouncedCheck(view);
+ },
+ };
+ },
+ });
+}
diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.css b/blocks/canvas/ew-canvas-header/ew-canvas-header.css
index fd3e6b5c7..db4459723 100644
--- a/blocks/canvas/ew-canvas-header/ew-canvas-header.css
+++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.css
@@ -92,6 +92,16 @@
outline: none;
}
+ &:disabled {
+ color: var(--s2-gray-400);
+ cursor: not-allowed;
+
+ &:hover {
+ color: var(--s2-gray-400);
+ background-color: transparent;
+ }
+ }
+
&:hover:not(.is-selected) {
color: var(--s2-gray-900);
background-color: var(--s2-gray-200);
@@ -118,6 +128,10 @@
flex-shrink: 0;
overflow: hidden;
}
+
+ &:disabled.segment-icon svg.icon {
+ opacity: 0.45;
+ }
}
.bar {
diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js
index 810e88406..0ad347e00 100644
--- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js
+++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js
@@ -28,6 +28,7 @@ class EWCanvasHeader extends LitElement {
undoAvailable: { type: Boolean },
redoAvailable: { type: Boolean },
authorized: { type: Boolean },
+ hasUnresolvedMergeConflicts: { type: Boolean, reflect: true, attribute: 'has-conflicts' },
canWrite: { type: Boolean },
_chatDisabled: { state: true },
};
@@ -38,6 +39,7 @@ class EWCanvasHeader extends LitElement {
this.undoAvailable = false;
this.redoAvailable = false;
this.authorized = true;
+ this.hasUnresolvedMergeConflicts = false;
this.canWrite = true;
}
@@ -81,9 +83,9 @@ class EWCanvasHeader extends LitElement {
canvasBus.redoRequest.emit();
}
- _setEditorView(view) {
+ setEditorView(view) {
+ if (this.hasUnresolvedMergeConflicts && view !== 'content') return;
if (!EDITOR_VIEWS.includes(view) || view === this.editorView) return;
- this.editorView = view;
canvasBus.editorViewRequest.emit({ view });
}
@@ -134,21 +136,24 @@ class EWCanvasHeader extends LitElement {
type="button"
class="segment ${this.editorView === 'layout' ? 'is-selected' : ''}"
aria-pressed=${this.editorView === 'layout'}
- @click=${() => this._setEditorView('layout')}
+ title=${this.hasUnresolvedMergeConflicts ? 'Resolve conflicts to switch views' : nothing}
+ ?disabled=${this.hasUnresolvedMergeConflicts}
+ @click=${() => this.setEditorView('layout')}
>Layout
` : nothing}
diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css
new file mode 100644
index 000000000..e1d98e060
--- /dev/null
+++ b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css
@@ -0,0 +1,155 @@
+/* stylelint-disable selector-class-pattern */
+
+.ew-editor-doc-mount .loc-tabbed-container {
+ background: var(--s2-gray-25);
+ box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
+ border-radius: var(--s2-corner-radius-400);
+}
+
+.ew-editor-doc-mount .diff-tab-content {
+ background: var(--s2-gray-25);
+}
+
+.ew-editor-doc-mount .da-diff-btn-base {
+ border-radius: var(--s2-corner-radius-400);
+ height: 28px;
+ background: var(--s2-gray-100);
+}
+
+.ew-editor-doc-mount .da-diff-btn .switch-btn,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-label {
+ padding: 4px 10px;
+}
+
+.ew-editor-doc-mount .da-diff-btn .switch-btn {
+ cursor: default;
+}
+
+.ew-editor-doc-mount .da-diff-btn-base-element {
+ font-size: var(--s2-component-s-regular-font-size, 0.875rem);
+ font-weight: 600;
+
+ &:focus-visible {
+ outline: 2px solid var(--s2-blue-800);
+ outline-offset: 2px;
+ }
+}
+
+.ew-editor-doc-mount .da-diff-btn .confirm-btn,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-accept,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-delete {
+ padding: 0;
+}
+
+.ew-editor-doc-mount .da-diff-btn .confirm-btn::before,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-accept::before {
+ content: '';
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background-color: currentcolor;
+ mask: no-repeat center / 16px url('/img/icons/s2-icon-checkmark-20-n.svg');
+}
+
+.ew-editor-doc-mount .da-diff-btn-3part .diff-delete::before {
+ content: '';
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ background-color: currentcolor;
+ mask: no-repeat center / 16px url('/img/icons/s2-icon-close-20-n.svg');
+}
+
+.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn,
+.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label {
+ background: transparent;
+ color: var(--s2-gray-800);
+}
+
+.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn,
+.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept,
+.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete {
+ background: var(--s2-gray-200);
+ color: var(--s2-gray-800);
+ border-left: 1px solid var(--s2-gray-300);
+}
+
+.ew-editor-doc-mount .da-diff-btn.is-active {
+ border-color: var(--s2-gray-900);
+}
+
+.ew-editor-doc-mount .da-diff-btn.is-active .switch-btn,
+.ew-editor-doc-mount .da-diff-btn.is-active .confirm-btn {
+ background: var(--s2-gray-300);
+ color: var(--s2-gray-900);
+}
+
+.ew-editor-doc-mount .da-diff-btn .confirm-btn:hover,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-accept:hover,
+.ew-editor-doc-mount .da-diff-btn-3part .diff-delete:hover {
+ background: var(--s2-gray-300);
+ color: var(--s2-gray-900);
+}
+
+.ew-editor-doc-mount .diff-tooltip {
+ background: var(--s2-gray-1000);
+ color: var(--s2-gray-25);
+ font-weight: 500;
+ border: none;
+ border-radius: var(--s2-corner-radius-400);
+ box-shadow: 0 2px 6px rgb(0 0 0 / 15%);
+ backdrop-filter: none;
+}
+
+.ew-editor-doc-mount .diff-tooltip::before {
+ display: none;
+}
+
+.ew-editor-doc-mount .diff-tooltip::after {
+ border-top-color: var(--s2-gray-1000);
+ filter: none;
+}
+
+.ew-editor-doc-mount .diff-tab-pane del.diffdel,
+.ew-editor-doc-mount .diff-tab-pane del.diffmod {
+ background-color: var(--s2-red-200);
+ color: var(--s2-red-900);
+ border: none;
+}
+
+.ew-editor-doc-mount .diff-tab-pane ins.diffins,
+.ew-editor-doc-mount .diff-tab-pane ins.diffmod {
+ background-color: var(--s2-green-200);
+ color: var(--s2-green-900);
+ border: none;
+}
+
+.ew-editor-doc-mount .loc-color-overlay.loc-regional {
+ background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent);
+}
+
+.ew-editor-doc-mount .loc-color-overlay.loc-langstore {
+ background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent);
+}
+
+.ew-editor-doc-mount .diff-bg-local {
+ background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent);
+}
+
+.ew-editor-doc-mount .diff-bg-upstream {
+ background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent);
+}
+
+.ew-editor-doc-mount .diff-bg-diff {
+ background-color: color-mix(in srgb, var(--s2-gray-100) 25%, transparent);
+}
+
+.ew-editor-doc-mount .da-regional-edits-overlay {
+ padding: var(--s2-spacing-200);
+ background: var(--s2-gray-25);
+ box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
+}
+
+.ew-editor-doc-mount .da-regional-edits-actions {
+ gap: var(--s2-spacing-200);
+}
diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc.js b/blocks/canvas/ew-editor-doc/ew-editor-doc.js
index 54da2e2ce..686aa91be 100644
--- a/blocks/canvas/ew-editor-doc/ew-editor-doc.js
+++ b/blocks/canvas/ew-editor-doc/ew-editor-doc.js
@@ -1,6 +1,7 @@
import { LitElement, html, nothing } from 'da-lit';
import { yUndo, yRedo, NodeSelection, TextSelection } from 'da-y-wrapper';
-import { getNx } from '../../../scripts/utils.js';
+import { getNx, getNx2 } from '../../../scripts/utils.js';
+import getSheet from '../../shared/sheet.js';
import { updateDocument, updateCursors, getInstrumentedHTML, getEditor } from '../editor-utils/editor-utils.js';
import { getActiveBlockIndex, getBlockPositions } from '../editor-utils/blocks.js';
import {
@@ -41,6 +42,8 @@ const { loadStyle } = await import(`${getNx()}/utils/utils.js`);
const { CHAT_EVENT } = await import(`${getNx()}/blocks/chat/constants.js`);
const style = await loadStyle(import.meta.url);
+const buttons = await getSheet(`${getNx2()}/styles/buttons.css`);
+const diffStyle = await getSheet('/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css');
export class EwEditorDoc extends LitElement {
static properties = {
@@ -95,6 +98,10 @@ export class EwEditorDoc extends LitElement {
canvasBus.undoState.emit({ canUndo, canRedo });
}
+ _emitMergeConflictsState(hasMergeConflicts) {
+ canvasBus.mergeConflictsState.emit({ hasMergeConflicts });
+ }
+
_observeUndoManager(mgr) {
this._stopObservingUndoManager();
if (!mgr) return;
@@ -281,6 +288,8 @@ export class EwEditorDoc extends LitElement {
permissions,
setEditable: (editable) => this._setEditable(editable),
getToken: () => token,
+ onMergeConflictsChange: (hasConflicts) => this._emitMergeConflictsState(hasConflicts),
+ hostEl: this,
extraPlugins: [
createExtensionsBridgePlugin(),
createTrackingPlugin(
@@ -336,7 +345,7 @@ export class EwEditorDoc extends LitElement {
connectedCallback() {
super.connectedCallback();
- this.shadowRoot.adoptedStyleSheets = [style];
+ this.shadowRoot.adoptedStyleSheets = [style, buttons, diffStyle];
this._unsubscribeEditorActive = canvasBus.editorViewState.subscribe(({ view }) => {
this.hidden = view === 'layout';
hideSelectionToolbar();
diff --git a/blocks/canvas/ew-editor-doc/prose.js b/blocks/canvas/ew-editor-doc/prose.js
index b99e01d07..17daf918b 100644
--- a/blocks/canvas/ew-editor-doc/prose.js
+++ b/blocks/canvas/ew-editor-doc/prose.js
@@ -41,6 +41,9 @@ import { getNx } from '../../../scripts/utils.js';
import { getAuthToken } from '../../shared/utils.js';
import { generateColor, getCollabIdentity } from './utils/collab.js';
import { checkBlockLibraryConfigured } from '../editor-utils/block-slash.js';
+import { getDiffClass, addActiveView } from '../../edit/prose/diff/diff-utils.js';
+import { initDaMetadata } from '../../edit/utils/helpers.js';
+import { createMergeConflictsPlugin } from '../editor-utils/prose-merge-conflicts.js';
import { canvasBus } from '../utils/canvas-bus.js';
const { DA_ADMIN, DA_COLLAB, hashChange } = await import(`${getNx()}/utils/utils.js`);
@@ -83,7 +86,7 @@ function checkLibraryConfiguredOnSync(wsProvider, canWrite) {
export default async function initProse({
path, permissions, setEditable, getToken,
- extraPlugins = [],
+ extraPlugins = [], onMergeConflictsChange, hostEl,
}) {
const editor = document.createElement('div');
editor.className = 'da-prose-mirror';
@@ -212,7 +215,7 @@ export default async function initProse({
if (canWrite) {
plugins.unshift(createSlashMenuPlugin(), createSelectionToolbarPlugin());
- plugins.push(imageFocalPoint());
+ plugins.push(imageFocalPoint(), createMergeConflictsPlugin(onMergeConflictsChange, hostEl));
}
let state = EditorState.create({ schema, plugins });
@@ -220,11 +223,27 @@ export default async function initProse({
const fix = fixTables(state);
if (fix) state = state.apply(fix.setMeta('addToHistory', false));
+ const diffNodeViews = canWrite ? {
+ diff_added(node, view, getPos) {
+ const MergeConflictsAddedView = getDiffClass('da-diff-added', getSchema, () => {}, { isUpstream: false, hostEl });
+ return new MergeConflictsAddedView(node, view, getPos);
+ },
+ diff_deleted(node, view, getPos) {
+ const MergeConflictsDeletedView = getDiffClass('da-diff-deleted', getSchema, () => {}, { isUpstream: true, hostEl });
+ return new MergeConflictsDeletedView(node, view, getPos);
+ },
+ } : undefined;
+
viewRef = new EditorView(editor, {
state,
editable() { return canWrite; },
+ nodeViews: diffNodeViews,
});
+ if (canWrite) addActiveView(viewRef);
+
+ initDaMetadata(ydoc.getMap('daMetadata'));
+
const undoManager = yUndoPluginKey.getState(viewRef.state)?.undoManager ?? null;
return { proseEl: editor, wsProvider, view: viewRef, ydoc, undoManager };
diff --git a/blocks/canvas/ew-editor-doc/utils/teardown.js b/blocks/canvas/ew-editor-doc/utils/teardown.js
index a066b17ca..98c59279e 100644
--- a/blocks/canvas/ew-editor-doc/utils/teardown.js
+++ b/blocks/canvas/ew-editor-doc/utils/teardown.js
@@ -1,3 +1,5 @@
+import { removeActiveView } from '../../../edit/prose/diff/diff-utils.js';
+
export function teardownEditorDocResources({
clearPortHandler,
awarenessOff,
@@ -14,6 +16,7 @@ export function teardownEditorDocResources({
wsProvider.disconnect({ data: 'unmount' });
}
if (view) {
+ removeActiveView(view);
view.destroy();
}
if (proseEl?.parentNode) {
diff --git a/blocks/canvas/utils/canvas-bus.js b/blocks/canvas/utils/canvas-bus.js
index c2969aa7f..52154a143 100644
--- a/blocks/canvas/utils/canvas-bus.js
+++ b/blocks/canvas/utils/canvas-bus.js
@@ -42,6 +42,7 @@ export const canvasBus = Object.freeze({
newVersionRequest: createChannel(),
undoState: createChannel(),
+ mergeConflictsState: createChannel(),
editorViewState: createChannel(),
editorHtmlState: createChannel({ replay: true }),
editorSelectState: {
diff --git a/blocks/edit/prose/diff/diff-actions.js b/blocks/edit/prose/diff/diff-actions.js
index 33210242b..00b377189 100644
--- a/blocks/edit/prose/diff/diff-actions.js
+++ b/blocks/edit/prose/diff/diff-actions.js
@@ -117,7 +117,7 @@ function createCompositeButton({
switchBtn.appendChild(createTooltip(switchTooltip, 'diff-tooltip'));
}
- const confirmBtn = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` });
+ const confirmBtn = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` });
confirmBtn.addEventListener('click', handler);
if (tooltip) {
confirmBtn.appendChild(createTooltip(tooltip, 'diff-tooltip'));
diff --git a/blocks/edit/prose/diff/diff-global-dialog.js b/blocks/edit/prose/diff/diff-global-dialog.js
index d30b66ed5..b4c7b153d 100644
--- a/blocks/edit/prose/diff/diff-global-dialog.js
+++ b/blocks/edit/prose/diff/diff-global-dialog.js
@@ -138,7 +138,7 @@ function createGlobalAction(
const label = createElement('span', 'switch-btn da-diff-btn-base-element');
label.textContent = text;
- const confirm = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text });
+ const confirm = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text });
confirm.addEventListener('click', () => handleGlobalAction(action, activeViews, isLocNode));
confirm.appendChild(createTooltip(tooltipText, 'diff-tooltip'));
diff --git a/blocks/edit/prose/diff/diff-overlay-ui.js b/blocks/edit/prose/diff/diff-overlay-ui.js
index 6b21914fa..f4192de5e 100644
--- a/blocks/edit/prose/diff/diff-overlay-ui.js
+++ b/blocks/edit/prose/diff/diff-overlay-ui.js
@@ -20,10 +20,10 @@ export function getLangOverlay(upstream) {
const labelBtn = createElement('span', 'diff-label da-diff-btn-base-element');
labelBtn.textContent = text;
- const acceptBtn = createButton('diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` });
+ const acceptBtn = createButton('nx-action-btn-icon diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` });
acceptBtn.appendChild(createTooltip(`Accept ${text}`, 'diff-tooltip'));
- const deleteBtn = createButton('diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` });
+ const deleteBtn = createButton('nx-action-btn-icon diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` });
deleteBtn.appendChild(createTooltip(`Delete ${text}`, 'diff-tooltip'));
compositeBtn.appendChild(labelBtn);
diff --git a/blocks/edit/prose/diff/diff-utils.css b/blocks/edit/prose/diff/diff-utils.css
index 43fd34733..004632ae3 100644
--- a/blocks/edit/prose/diff/diff-utils.css
+++ b/blocks/edit/prose/diff/diff-utils.css
@@ -17,6 +17,14 @@ da-loc-added,
border-radius: 6px;
}
+.loc-color-overlay.loc-regional {
+ background-color: rgb(144 42 222 / 20%);
+}
+
+.loc-color-overlay.loc-langstore {
+ background-color: rgb(70 130 180 / 20%);
+}
+
.loc-lang-overlay {
z-index: 1001;
}
@@ -84,6 +92,10 @@ da-loc-added,
pointer-events: auto;
}
+.da-prose-mirror.has-regional-edits .loc-sticky-buttons {
+ top: 64px;
+}
+
/* When the container is short, the sticky buttons should be moved down to not block content */
.loc-single-container.is-short .loc-sticky-buttons,
.loc-tabbed-container.is-short .loc-sticky-buttons,
@@ -325,6 +337,10 @@ da-loc-added,
z-index: 2;
}
+.loc-tabbed-color-overlay.is-hidden {
+ display: none;
+}
+
@media (max-width: 600px) {
.loc-tab-button {
font-size: 12px;
@@ -404,27 +420,24 @@ da-loc-added,
.da-regional-edits-overlay {
display: none;
- position: absolute;
- right: -48px;
- z-index: 4;
- padding: 16px 20px;
- margin: 0 48px;
+ position: sticky;
+ top: 0;
+ z-index: 1111;
+ padding: 12px 20px;
box-sizing: border-box;
+ width: 100%;
+ background: #fff;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
}
.da-regional-edits-overlay.show {
display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
+ justify-content: flex-end;
}
.da-regional-edits-actions {
display: flex;
gap: 12px;
align-items: center;
-}
-
-.da-prose-mirror.has-regional-edits .ProseMirror {
- padding-top: 60px;
+ justify-content: flex-end;
}
diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js
index 603002ce2..2e24639a8 100644
--- a/blocks/edit/prose/diff/diff-utils.js
+++ b/blocks/edit/prose/diff/diff-utils.js
@@ -79,22 +79,21 @@ function setDiffLabelCssVars(daEditor) {
}
let locCssLoading = false;
-async function loadLocCss() {
+async function loadLocCss(hostEl) {
if (locCssLoading) return;
locCssLoading = true;
try {
const locSheet = await getSheet('/blocks/edit/prose/diff/diff-utils.css');
- const daEditor = document.querySelector('da-content')?.shadowRoot
- ?.querySelector('da-editor');
-
- if (daEditor?.shadowRoot) {
- const existingSheets = daEditor.shadowRoot.adoptedStyleSheets || [];
- daEditor.shadowRoot.adoptedStyleSheets = [...existingSheets, locSheet];
+ if (hostEl?.shadowRoot) {
+ hostEl.shadowRoot.adoptedStyleSheets = [
+ ...(hostEl.shadowRoot.adoptedStyleSheets || []),
+ locSheet,
+ ];
// Set CSS custom properties for diff labels
- setDiffLabelCssVars(daEditor);
+ setDiffLabelCssVars(hostEl);
}
} catch (error) {
// eslint-disable-next-line no-console
@@ -181,7 +180,7 @@ function hasMatchingContent(nodeA, nodeB) {
return true;
}
-export function checkForLocNodes(view) {
+export function checkForLocNodes(view, hostEl) {
const { doc } = view.state;
const hasListLocNode = (node) => (node.type.name === 'bullet_list' || node.type.name === 'ordered_list')
@@ -192,7 +191,7 @@ export function checkForLocNodes(view) {
.some((node) => isLocNode(node) || hasListLocNode(node));
if (hasLocNodes) {
- loadLocCss();
+ loadLocCss(hostEl);
showGlobalDialog(view);
} else {
hideGlobalDialog();
@@ -276,13 +275,18 @@ export function addActiveView(view) {
activeViews.add(view);
}
-export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream } = {}) {
+export function removeActiveView(view) {
+ activeViews.delete(view);
+}
+
+export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream, hostEl } = {}) {
return class {
constructor(node, view, getPos) {
this.node = node;
this.view = view;
this.getPos = getPos;
this.schema = getSchema();
+ this.hostEl = hostEl;
const pos = getPos();
const { doc } = view.state;
@@ -323,7 +327,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea
}
renderTabbedInterface(nodeA, view, posA, nodeB) {
- loadLocCss();
+ loadLocCss(this.hostEl);
this.dom = createElement('div', 'loc-tabbed-container', { contentEditable: 'false' });
this.contentDOM = null; // Don't let ProseMirror manage content
@@ -362,13 +366,11 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea
});
if (targetTab === 'added') {
- colorOverlay.style.display = 'block';
colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-local';
} else if (targetTab === 'deleted') {
- colorOverlay.style.display = 'block';
colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-upstream';
} else if (targetTab === 'diff') {
- colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff';
+ colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff is-hidden';
const diffTab = tabContent.querySelector('[data-tab="diff"]');
if (diffTab && !diffTab.loaded) {
@@ -430,7 +432,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea
}
renderSingleNode(node, view, pos, upstream) {
- loadLocCss();
+ loadLocCss(this.hostEl);
const isDeleted = node.type.name === 'diff_deleted';
const viewClass = isDeleted ? 'loc-deleted-view' : 'loc-added-view';
@@ -447,8 +449,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea
this.dom.appendChild(nodeDOM);
// Create placeholder cover div immediately
- const coverDiv = createElement('div', 'loc-color-overlay', { 'loc-temp-dom': '' });
- coverDiv.style.backgroundColor = upstream ? '#4682b433' : '#902ade33';
+ const coverDiv = createElement('div', `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`, { 'loc-temp-dom': '' });
this.dom.appendChild(coverDiv);
// Create placeholder overlay
@@ -483,9 +484,6 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea
coverDiv.removeChild(this.langOverlay);
this.langOverlay = overlay;
coverDiv.appendChild(this.langOverlay);
-
- const className = `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`;
- coverDiv.className = className;
} catch (error) {
// eslint-disable-next-line no-console
console.warn('Failed to load enhanced overlays:', error);
diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js
index 54e841610..ab2df8aa5 100644
--- a/blocks/edit/prose/index.js
+++ b/blocks/edit/prose/index.js
@@ -195,6 +195,8 @@ async function loadCustomPlugins() {
let lastCursorPosition = null;
+let diffHostEl = null;
+
function dispatchTransaction(transaction) {
if (!window.view) return;
@@ -202,7 +204,7 @@ function dispatchTransaction(transaction) {
window.view.updateState(newState);
if (transaction.docChanged) {
- debounce(checkForLocNodes, 500)(window.view);
+ debounce(checkForLocNodes, 500)(window.view, diffHostEl);
}
}
@@ -520,6 +522,8 @@ export default async function initProse({ path, permissions, doc, daContent, wsP
delete window.view;
}
+ diffHostEl = daContent?.shadowRoot?.querySelector('da-editor') ?? null;
+
const connectionPromise = wsPromise || createConnection(path);
const editor = document.createElement('div');
@@ -565,11 +569,11 @@ export default async function initProse({ path, permissions, doc, daContent, wsP
dispatchTransaction,
nodeViews: {
diff_added(node, view, getPos) {
- const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false });
+ const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false, hostEl: diffHostEl });
return new LocAddedView(node, view, getPos);
},
diff_deleted(node, view, getPos) {
- const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true });
+ const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true, hostEl: diffHostEl });
return new LocDeletedView(node, view, getPos);
},
},
diff --git a/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js
new file mode 100644
index 000000000..a462fb877
--- /dev/null
+++ b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js
@@ -0,0 +1,54 @@
+import { expect } from '@esm-bundle/chai';
+import { createMergeConflictsPlugin } from '../../../../../blocks/canvas/editor-utils/prose-merge-conflicts.js';
+import { createTestEditor, destroyEditor } from '../../edit/prose/test-helpers.js';
+
+const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
+
+describe('createMergeConflictsPlugin', () => {
+ let editor;
+
+ afterEach(() => {
+ if (editor) destroyEditor(editor);
+ editor = undefined;
+ });
+
+ it('reports false shortly after mount when the doc has no merge-conflict nodes', async () => {
+ const calls = [];
+ const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts));
+ editor = await createTestEditor({ additionalPlugins: [plugin] });
+
+ await waitFor(600);
+
+ expect(calls).to.deep.equal([false]);
+ });
+
+ it('reports true once a diff_added node is inserted', async () => {
+ const calls = [];
+ const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts));
+ editor = await createTestEditor({ additionalPlugins: [plugin] });
+
+ const { schema } = editor.view.state;
+ const para = schema.nodes.paragraph.create(null, schema.text('new'));
+ const diffAdded = schema.nodes.diff_added.create({}, para);
+ const tr = editor.view.state.tr.insert(editor.view.state.doc.content.size, diffAdded);
+ editor.view.dispatch(tr);
+
+ await waitFor(600);
+
+ expect(calls[calls.length - 1]).to.equal(true);
+ });
+
+ it('does not re-check on transactions that leave the doc unchanged', async () => {
+ const calls = [];
+ const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts));
+ editor = await createTestEditor({ additionalPlugins: [plugin] });
+
+ await waitFor(600); // let the initial mount check land
+ calls.length = 0;
+
+ editor.view.dispatch(editor.view.state.tr); // no doc change
+ await waitFor(600);
+
+ expect(calls).to.deep.equal([]);
+ });
+});