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
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import FormulasMenu from './components/math/FormulasMenu.vue';
import { preprocessMarkdown } from './utils/markdown';
import { resolveImageSrcs, toStoredImageSrcs } from './utils/imageSrc';
import { toInlineHTML } from './utils/inlineContent';
import MobileTopBar from './components/toolbar/MobileTopBar.vue';
import MobileFormattingBar from './components/toolbar/MobileFormattingBar.vue';
import { getTipTapEditorStrings } from './TipTapEditorStrings';
Expand All @@ -118,6 +119,11 @@
provide('isReady', isReady);
provide('insertContext', insertContext);
provide('insertActions', toRef(props, 'insertActions'));
// Read once: the schema is fixed when the editor is created.
const { inlineOnly } = props;
provide('inlineOnly', inlineOnly);
// The markdown serializer writes each inline child of an inline-only doc as a block.
const isHTML = computed(() => props.format === 'html' || inlineOnly);

// The anchored modals are measured and hit-tested through these refs, so that several
// editors mounted at once each work with their own modal.
Expand Down Expand Up @@ -150,7 +156,8 @@

const handleDrop = event => {
const file = event.dataTransfer.files[0];
if (file) {
// An inline-only schema has no image node to insert.
if (file && !inlineOnly) {
imageHandler.openCreateModal({ file });
}
};
Expand All @@ -160,7 +167,7 @@
// Image srcs are resolved for display on the way in, so they are reduced
// back to their stored form here — leaving this the one place that reads
// content out, whichever form the editor happens to be holding.
if (props.format === 'html') return toStoredImageSrcs(editor.value.getHTML());
if (isHTML.value) return toStoredImageSrcs(editor.value.getHTML());
if (!editor.value.storage?.markdown) return '';
return editor.value.storage.markdown.getMarkdown();
};
Expand Down Expand Up @@ -195,13 +202,16 @@
return;
}

const processedContent =
props.format === 'html' ? resolveImageSrcs(newValue) : preprocessMarkdown(newValue);
let processedContent;
if (inlineOnly) processedContent = toInlineHTML(newValue);
else if (isHTML.value) processedContent = resolveImageSrcs(newValue);
else processedContent = preprocessMarkdown(newValue);

if (!editor.value) {
initializeEditor(processedContent, props.mode, {
autofocus: props.autofocus,
extensions: props.extensions,
inlineOnly,
});
return;
}
Expand Down Expand Up @@ -308,6 +318,15 @@
type: Array,
default: () => [],
},
/**
* Holds one inline run, offering only what QTI allows in `qti-gap-text`;
* blocks in the value are flattened to space-separated inline text. Read once,
* when the editor is created. Reads and writes HTML, whatever `format` says.
*/
inlineOnly: {
type: Boolean,
default: false,
},
/**
* Actions appended to the insert tools of every toolbar. Each is
* `{ name, title, icon, handler, isActive?, isAvailable?, prominent? }`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useEditor } from '../composables/useEditor';
import { resolveInsertAction } from '../composables/useToolbarActions';
import { toInlineHTML } from '../utils/inlineContent';

export default {
name: 'RichTextEditor',
setup(props, { emit, listeners }) {
const input = ref(null);
const { editor, isReady, insertContext, initializeEditor } = useEditor();
const content = value => (props.inlineOnly ? toInlineHTML(value) : value);

// A real editor, so `ready` carries an instance whose schema holds the
// consumer's extensions and whose commands run. Built only for a test that
Expand All @@ -43,7 +45,10 @@
() => Boolean(props.extensions.length || props.insertActions.length || listeners.ready),
needed => {
if (needed && !editor.value) {
initializeEditor(props.value, props.mode, { extensions: props.extensions });
initializeEditor(content(props.value), props.mode, {
extensions: props.extensions,
inlineOnly: props.inlineOnly,
});
}
},
{ immediate: true },
Expand All @@ -55,7 +60,7 @@

watch(
() => props.value,
value => editor.value?.commands.setContent(value || '<p></p>'),
value => editor.value?.commands.setContent(content(value) || '<p></p>'),
);

// The textarea stands in for the editor's view, so focusing it places the
Expand Down Expand Up @@ -106,6 +111,10 @@
type: Array,
default: () => [],
},
inlineOnly: {
type: Boolean,
default: false,
},
},
emits: ['update', 'ready'],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@
<ToolbarDivider />

<!-- Format dropdown -->
<div
role="group"
class="formatting-group toolbar-group"
:aria-label="textFormattingOptions$()"
>
<FormatDropdown />
</div>

<ToolbarDivider />
<template v-if="!inlineOnly">
<div
role="group"
class="formatting-group toolbar-group"
:aria-label="textFormattingOptions$()"
>
<FormatDropdown />
</div>

<ToolbarDivider />
</template>

<!-- Collapsible toolbar groups -->
<KListWithOverflow
Expand Down Expand Up @@ -162,7 +164,7 @@

<script>

import { ref, computed } from 'vue';
import { ref, computed, inject } from 'vue';
import { useToolbarActions } from '../composables/useToolbarActions';
import { getTipTapEditorStrings } from '../TipTapEditorStrings';
import { useDropdowns } from '../composables/useDropdowns';
Expand All @@ -183,13 +185,14 @@
setup(props, { emit }) {
const toolbarRef = ref(null);
useRovingTabIndex(toolbarRef);
const inlineOnly = inject('inlineOnly', false);

const {
handleCopy,
handleClearFormat,
canClearFormat,
historyActions,
alignAction,
alignActions,
textActions,
listActions,
insertTools,
Expand Down Expand Up @@ -262,7 +265,7 @@
},
{
name: 'align',
groupActions: [alignAction.value],
groupActions: alignActions.value,
},
{
name: 'clearFormat',
Expand Down Expand Up @@ -342,9 +345,10 @@
*/
const toolbarGroupsWithDividers = computed(() => {
const groups = [];
toolbarGroups.value.forEach((group, index) => {
const nonEmptyGroups = toolbarGroups.value.filter(group => group.groupActions.length);
nonEmptyGroups.forEach((group, index) => {
groups.push(group);
if (index < toolbarGroups.value.length - 1) {
if (index < nonEmptyGroups.length - 1) {
groups.push({ type: 'divider' });
}
});
Expand All @@ -357,6 +361,7 @@

return {
toolbarRef,
inlineOnly,
toolbarGroupsWithDividers,
flatOverflowOptions,
historyActions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,41 +35,43 @@
@touchstart="event => event.stopPropagation()"
@touchend="event => event.stopPropagation()"
>
<div
class="formatting-buttons"
role="group"
:aria-label="formatSize$()"
>
<button
data-toolbar-item
:aria-disabled="canDecreaseFormat ? 'false' : 'true'"
:title="decreaseFormatSize$()"
:aria-label="decreaseFormatSize$()"
class="format-btn"
@mousedown.prevent
@click="decreaseFormat"
>
-
</button>
<img
src="../../../assets/icon-formatSize.svg"
:alt="formatSize$()"
aria-hidden="true"
<template v-if="!inlineOnly">
<div
class="formatting-buttons"
role="group"
:aria-label="formatSize$()"
>
<button
data-toolbar-item
:aria-disabled="canIncreaseFormat ? 'false' : 'true'"
:title="increaseFormatSize$()"
:aria-label="increaseFormatSize$()"
class="format-btn"
@mousedown.prevent
@click="increaseFormat"
>
+
</button>
</div>

<ToolbarDivider />
<button
data-toolbar-item
:aria-disabled="canDecreaseFormat ? 'false' : 'true'"
:title="decreaseFormatSize$()"
:aria-label="decreaseFormatSize$()"
class="format-btn"
@mousedown.prevent
@click="decreaseFormat"
>
-
</button>
<img
src="../../../assets/icon-formatSize.svg"
:alt="formatSize$()"
aria-hidden="true"
>
<button
data-toolbar-item
:aria-disabled="canIncreaseFormat ? 'false' : 'true'"
:title="increaseFormatSize$()"
:aria-label="increaseFormatSize$()"
class="format-btn"
@mousedown.prevent
@click="increaseFormat"
>
+
</button>
</div>

<ToolbarDivider />
</template>

<ToolbarButton
v-for="action in textActions"
Expand All @@ -80,22 +82,28 @@
@click="action.handler"
/>
<ToolbarDivider />
<ToolbarButton
v-for="action in listActions"
:key="action.name"
:title="action.title"
:icon="action.icon"
:is-active="action.isActive"
@click="action.handler"
/>
<ToolbarDivider />
<ToolbarButton
:title="alignAction.title"
:icon="alignAction.icon"
:is-active="alignAction.isActive"
@click="alignAction.handler"
/>
<ToolbarDivider />
<template v-if="listActions.length">
<ToolbarButton
v-for="action in listActions"
:key="action.name"
:title="action.title"
:icon="action.icon"
:is-active="action.isActive"
@click="action.handler"
/>
<ToolbarDivider />
</template>
<template v-if="alignActions.length">
<ToolbarButton
v-for="action in alignActions"
:key="action.name"
:title="action.title"
:icon="action.icon"
:is-active="action.isActive"
@click="action.handler"
/>
<ToolbarDivider />
</template>
<ToolbarButton
v-for="action in scriptActions"
:key="action.name"
Expand Down Expand Up @@ -138,6 +146,7 @@
const isExpanded = ref(true);
const keyboardOffset = ref(0);
const editor = inject('editor');
const inlineOnly = inject('inlineOnly', false);
const toolbarRef = ref(null);

useRovingTabIndex(toolbarRef);
Expand All @@ -151,7 +160,7 @@
textFormattingToolbar$,
} = getTipTapEditorStrings();

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

const { canIncreaseFormat, canDecreaseFormat, increaseFormat, decreaseFormat } =
Expand Down Expand Up @@ -217,12 +226,13 @@

return {
isExpanded,
inlineOnly,
keyboardOffset,
toolbarRef,
textActions,
listActions,
scriptActions,
alignAction,
alignActions,
insertTools,
toggleToolbar,
canIncreaseFormat,
Expand Down
Loading
Loading