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
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,6 @@
/>
</div>

<div
v-if="linkHandler.isBubbleMenuOpen.value"
:style="linkHandler.popoverStyle.value"
>
<LinkBubbleMenu
v-if="isReady"
:editor="editor"
/>
</div>

<div
v-if="linkHandler.isEditorOpen.value"
class="link-editor-popover-wrapper"
:class="{ 'has-overlay': linkHandler.isEditorCentered.value }"
:style="linkHandler.isEditorCentered.value ? {} : linkHandler.popoverStyle.value"
@click.self="linkHandler.closeLinkEditor"
>
<LinkEditor
:style="linkHandler.isEditorCentered.value ? linkHandler.popoverStyle.value : {}"
:mode="linkHandler.editorMode.value"
:initial-state="linkHandler.editorInitialState.value"
@save="linkHandler.saveLink"
@remove="linkHandler.removeLink"
@close="linkHandler.closeLinkEditor"
/>
</div>

<div
v-if="imageHandler.modalMode.value"
class="image-upload-popover-wrapper"
Expand Down Expand Up @@ -128,9 +101,6 @@
import ImageUploadModal from './components/image/ImageUploadModal.vue';
import { useImageHandling } from './composables/useImageHandling';
import '../assets/styles/code-theme-dark.css';
import { useLinkHandling } from './composables/useLinkHandling';
import LinkBubbleMenu from './components/link/LinkBubbleMenu.vue';
import LinkEditor from './components/link/LinkEditor.vue';
import { useMathHandling } from './composables/useMathHandling';
import FormulasMenu from './components/math/FormulasMenu.vue';
import { preprocessMarkdown } from './utils/markdown';
Expand All @@ -146,8 +116,6 @@
EditorToolbar,
EditorContentWrapper,
ImageUploadModal,
LinkBubbleMenu,
LinkEditor,
FormulasMenu,
MobileTopBar,
MobileFormattingBar,
Expand All @@ -160,9 +128,6 @@
provide('insertContext', insertContext);
provide('insertActions', toRef(props, 'insertActions'));

const linkHandler = useLinkHandling(editor);
provide('linkHandler', linkHandler);

// The anchored modals are measured and hit-tested through these refs, so that several
// editors mounted at once each work with their own modal.
const imageUploadModal = ref(null);
Expand All @@ -180,7 +145,6 @@

const sharedEventHandlers = computed(() => ({
'insert-image': target => imageHandler.openCreateModal({ targetElement: target }),
'insert-link': () => linkHandler.openLinkEditor(),
'insert-math': target => mathHandler.openCreateMathModal({ targetElement: target }),
}));

Expand Down Expand Up @@ -318,12 +282,9 @@
editorContainer,
imageUploadModal,
formulasMenu,
isReady,
hasFocusWithin,
handleFocusout,
handleDrop,
linkHandler,
editor,
mathHandler,
isTouchDevice,
imageHandler,
Expand Down Expand Up @@ -446,7 +407,6 @@
outline-color: #007bff;
}

.link-editor-popover-wrapper,
.image-upload-popover-wrapper,
.math-modal-popover-wrapper {
position: fixed;
Expand All @@ -461,7 +421,6 @@
pointer-events: none;
}

.link-editor-popover-wrapper > *,
.image-upload-popover-wrapper > *,
.math-modal-popover-wrapper > * {
pointer-events: auto;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@
canClearFormat,
historyActions,
alignAction,
alignActionHidden,
textActions,
listActions,
insertTools,
Expand Down Expand Up @@ -267,8 +266,6 @@
{
name: 'align',
groupActions: [alignAction.value],
// Reason on the flag, in useToolbarActions.js.
hide: alignActionHidden,
},
{
name: 'clearFormat',
Expand Down Expand Up @@ -349,9 +346,6 @@
const toolbarGroupsWithDividers = computed(() => {
const groups = [];
toolbarGroups.value.forEach((group, index) => {
if (group.hide) {
return;
}
groups.push(group);
if (index < toolbarGroups.value.length - 1) {
groups.push({ type: 'divider' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,13 @@
:is-active="action.isActive"
@click="action.handler"
/>
<!-- Hidden while alignActionHidden is set; see useToolbarActions.js. -->
<template v-if="!alignActionHidden">
<ToolbarDivider />
<ToolbarButton
:title="alignAction.title"
:icon="alignAction.icon"
:is-active="alignAction.isActive"
@click="alignAction.handler"
/>
</template>
<ToolbarDivider />
<ToolbarButton
:title="alignAction.title"
:icon="alignAction.icon"
:is-active="alignAction.isActive"
@click="alignAction.handler"
/>
<ToolbarDivider />
<ToolbarButton
v-for="action in scriptActions"
Expand Down Expand Up @@ -154,14 +151,8 @@
textFormattingToolbar$,
} = getTipTapEditorStrings();

const {
textActions,
listActions,
scriptActions,
alignAction,
alignActionHidden,
insertTools,
} = useToolbarActions(emit);
const { textActions, listActions, scriptActions, alignAction, insertTools } =
useToolbarActions(emit);

const { canIncreaseFormat, canDecreaseFormat, increaseFormat, decreaseFormat } =
useFormatControls();
Expand Down Expand Up @@ -232,7 +223,6 @@
listActions,
scriptActions,
alignAction,
alignActionHidden,
insertTools,
toggleToolbar,
canIncreaseFormat,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { Superscript } from '@tiptap/extension-superscript';
import { Subscript } from '@tiptap/extension-subscript';
import { TextAlign } from '@tiptap/extension-text-align';
import { Small } from '../extensions/SmallTextExtension';
import { StyledStrike, StyledUnderline } from '../extensions/TextDecoration';
import { Image } from '../extensions/Image';
import { CodeBlockSyntaxHighlight } from '../extensions/CodeBlockSyntaxHighlight';
import { CustomLink } from '../extensions/Link';
import { Math } from '../extensions/Math';
import { createCustomMarkdownSerializer } from '../utils/markdownSerializer';
import { transformPastedHTML } from '../utils/pasteTransform';
Expand Down Expand Up @@ -50,20 +50,23 @@ export function useEditor() {
extensions: [
StarterKitExtension.configure({
codeBlock: false, // Disable default code block to use the extended version
link: false, // Disable default link to use the custom link extension
// The QTI 3.0 HTML profile has no <u> or <s>, so the item schema rejects an
// item carrying either and the save fails. Dropping the marks rather than only
// their toolbar buttons also takes away the keyboard shortcuts and the paste
// path, which would otherwise still produce content that cannot be saved.
// A link has nothing to navigate to on a device with no internet access, so
// the editor offers none and the legacy conversion unwraps the ones it finds
// (utils/assessment/qti/convert.py). Dropping the mark rather than only the
// toolbar button is what keeps a pasted anchor from arriving as one.
link: false,
Comment thread
rtibblesbot marked this conversation as resolved.
// Replaced by the versions in extensions/TextDecoration.js, which write the
// decoration as a style on a <span> — the QTI 3.0 HTML profile has no <u> or <s>.
strike: false,
underline: false,
}),
CodeBlockSyntaxHighlight,
Small,
StyledStrike,
StyledUnderline,
Superscript,
Subscript,
Image,
CustomLink, // Use our custom Link extension
Math,
TextAlign.configure({
types: ['heading', 'paragraph', 'image', 'small'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,11 @@ export function useToolbarActions(emit) {

/**
* Drop the actions marked `hide`, which every toolbar honours — the desktop one and
* the mobile bars alike. Same convention as the hidden groups in EditorToolbar.vue:
* the action stays defined, with the reason it is not offered, so restoring it is a
* matter of deleting one flag.
* the mobile bars alike. The action stays defined, with the reason it is not offered,
* so restoring it is a matter of deleting one flag.
*/
const visible = actions => actions.filter(action => !action.hide);

/*
* TextAlign writes `style="text-align: …"`, and the QTI 3.0 HTML profile declares no
* style attribute — the item schema admits one only through its lax wildcard, so an
* aligned paragraph saves and then ships as non-conformant QTI. The image extension
* carries alignment as `data-text-align` instead, which the schema does allow.
*
* Kept out of `alignAction` so a toolbar can ask whether to offer the control without
* evaluating the action, which reads the editor's current selection to pick its icon.
*/
const alignActionHidden = true;

// helper
const getEffectiveAlignment = editorInstance => {
if (!editorInstance) return 'left';
Expand Down Expand Up @@ -388,44 +376,36 @@ export function useToolbarActions(emit) {
},
]);

const textActions = computed(() =>
visible([
{
name: 'bold',
title: bold$(),
icon: require('../../assets/icon-bold.svg'),
handler: handleBold,
isActive: isMarkActive('bold'),
},
{
name: 'italic',
title: italic$(),
icon: require('../../assets/icon-italic.svg'),
handler: handleItalic,
isActive: isMarkActive('italic'),
},
{
name: 'underline',
title: underline$(),
icon: require('../../assets/icon-underline.svg'),
handler: handleUnderline,
isActive: isMarkActive('underline'),
// The QTI 3.0 HTML profile has no <u> or <s>, so the item schema rejects an item
// carrying either and the save fails. Both marks are switched off in useEditor.js
// as well, since hiding a button leaves its keyboard shortcut behind — offering
// these again means undoing both halves.
hide: true,
},
{
name: 'strikethrough',
title: strikethrough$(),
icon: require('../../assets/icon-strikethrough.svg'),
handler: handleStrikethrough,
isActive: isMarkActive('strike'),
hide: true,
},
]),
);
const textActions = computed(() => [
{
name: 'bold',
title: bold$(),
icon: require('../../assets/icon-bold.svg'),
handler: handleBold,
isActive: isMarkActive('bold'),
},
{
name: 'italic',
title: italic$(),
icon: require('../../assets/icon-italic.svg'),
handler: handleItalic,
isActive: isMarkActive('italic'),
},
{
name: 'underline',
title: underline$(),
icon: require('../../assets/icon-underline.svg'),
handler: handleUnderline,
isActive: isMarkActive('underline'),
},
{
name: 'strikethrough',
title: strikethrough$(),
icon: require('../../assets/icon-strikethrough.svg'),
handler: handleStrikethrough,
isActive: isMarkActive('strike'),
},
]);

const listActions = computed(() => [
{
Expand Down Expand Up @@ -564,7 +544,6 @@ export function useToolbarActions(emit) {
historyActions,
textActions,
alignAction,
alignActionHidden,
listActions,
scriptActions,
insertTools,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Underline and strikethrough, written as a decorated <span> rather than <u> or <s>.
*
* The QTI 3.0 HTML profile has neither element, so an item carrying one is rejected by
* the item schema. It does have <span>, and the schema admits a `style` attribute
* through its lax wildcard, so the decoration travels as a style instead — the same way
* TextAlign already carries alignment.
*
* Only the output changes. Upstream's parse rules already read `text-decoration` off a
* style attribute, so a span written here comes back as the mark that wrote it, and a
* pasted <u> or <s> still arrives as one.
*/
import { mergeAttributes } from '@tiptap/core';
import { Strike } from '@tiptap/extension-strike';
import { Underline } from '@tiptap/extension-underline';

const renderAsDecoratedSpan = decoration =>
Comment thread
rtibblesbot marked this conversation as resolved.
function renderHTML({ HTMLAttributes }) {
return [
'span',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
style: `text-decoration: ${decoration}`,
}),
0,
];
};

export const StyledStrike = Strike.extend({
renderHTML: renderAsDecoratedSpan('line-through'),
});

export const StyledUnderline = Underline.extend({
renderHTML: renderAsDecoratedSpan('underline'),
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import EditorToolbar from '../TipTapEditor/components/EditorToolbar.vue';
import { getTipTapEditorStrings } from '../TipTapEditor/TipTapEditorStrings';
import { tabIn } from 'shared/utils/testing';

const { textFormatOptions$ } = getTipTapEditorStrings();
const { textFormatOptions$, alignRight$ } = getTipTapEditorStrings();

// Every editor read the toolbar makes while rendering: undo/redo availability,
// mark state, the alignment probe in `getEffectiveAlignment`, and the
Expand Down Expand Up @@ -226,3 +226,13 @@ describe('EditorToolbar contributed insert actions', () => {
});
});
});

describe('EditorToolbar alignment control', () => {
// The action itself was always defined; what changed is that the toolbar no longer
// hides it, so the assertion has to be on what renders.
it('renders the alignment control', async () => {
await renderToolbar();

expect(screen.getByRole('button', { name: alignRight$() })).toBeInTheDocument();
});
});
Loading
Loading