mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(editor): Update sticky content when checkbox state changes (#9596)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
744c94d94b
commit
5361e9f69a
|
@ -5,6 +5,8 @@
|
|||
ref="editor"
|
||||
:class="$style[theme]"
|
||||
@click="onClick"
|
||||
@mousedown="onMouseDown"
|
||||
@change="onChange"
|
||||
v-html="htmlContent"
|
||||
/>
|
||||
<div v-else :class="$style.markdown">
|
||||
|
@ -17,7 +19,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { Options as MarkdownOptions } from 'markdown-it';
|
||||
import Markdown from 'markdown-it';
|
||||
import markdownLink from 'markdown-it-link-attributes';
|
||||
|
@ -26,7 +28,7 @@ import markdownTaskLists from 'markdown-it-task-lists';
|
|||
import xss, { friendlyAttrValue, whiteList } from 'xss';
|
||||
|
||||
import N8nLoading from '../N8nLoading';
|
||||
import { escapeMarkdown } from '../../utils/markdown';
|
||||
import { escapeMarkdown, toggleCheckbox } from '../../utils/markdown';
|
||||
|
||||
interface IImage {
|
||||
id: string;
|
||||
|
@ -79,6 +81,8 @@ const props = withDefaults(defineProps<MarkdownProps>(), {
|
|||
}),
|
||||
});
|
||||
|
||||
const editor = ref<HTMLDivElement | undefined>(undefined);
|
||||
|
||||
const { options } = props;
|
||||
const md = new Markdown(options.markdown)
|
||||
.use(markdownLink, options.linkAttributes)
|
||||
|
@ -151,7 +155,7 @@ const htmlContent = computed(() => {
|
|||
return safeHtml;
|
||||
});
|
||||
|
||||
const $emit = defineEmits(['markdown-click']);
|
||||
const $emit = defineEmits(['markdown-click', 'update-content']);
|
||||
const onClick = (event: MouseEvent) => {
|
||||
let clickedLink: HTMLAnchorElement | null = null;
|
||||
|
||||
|
@ -167,6 +171,40 @@ const onClick = (event: MouseEvent) => {
|
|||
}
|
||||
$emit('markdown-click', clickedLink, event);
|
||||
};
|
||||
|
||||
// Handle checkbox changes
|
||||
const onChange = async (event: Event) => {
|
||||
if (event.target instanceof HTMLInputElement && event.target.type === 'checkbox') {
|
||||
const checkboxes = editor.value?.querySelectorAll('input[type="checkbox"]');
|
||||
if (checkboxes) {
|
||||
// Get the index of the checkbox that was clicked
|
||||
const index = Array.from(checkboxes).indexOf(event.target);
|
||||
if (index !== -1) {
|
||||
onCheckboxChange(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
// Mouse down on input fields is caught by node view handlers
|
||||
// which prevents checking them, this will prevent that
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Update markdown when checkbox state changes
|
||||
const onCheckboxChange = (index: number) => {
|
||||
const currentContent = props.content;
|
||||
if (!currentContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We are using index to connect the checkbox with the corresponding line in the markdown
|
||||
const newContent = toggleCheckbox(currentContent, index);
|
||||
$emit('update-content', newContent);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -243,6 +281,14 @@ const onClick = (event: MouseEvent) => {
|
|||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
input[type='checkbox'] + label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
color: var(--color-sticky-font);
|
||||
|
||||
|
@ -297,6 +343,11 @@ const onClick = (event: MouseEvent) => {
|
|||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--font-line-height-regular);
|
||||
}
|
||||
|
||||
&:has(input[type='checkbox']) {
|
||||
list-style-type: none;
|
||||
padding-left: var(--spacing-5xs);
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import { render, fireEvent } from '@testing-library/vue';
|
||||
import N8nMarkdown from '../Markdown.vue';
|
||||
|
||||
describe('components', () => {
|
||||
|
@ -15,6 +15,7 @@ describe('components', () => {
|
|||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render checked checkboxes', () => {
|
||||
const wrapper = render(N8nMarkdown, {
|
||||
props: {
|
||||
|
@ -27,6 +28,26 @@ describe('components', () => {
|
|||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle checkboxes when clicked', async () => {
|
||||
const wrapper = render(N8nMarkdown, {
|
||||
props: {
|
||||
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
||||
},
|
||||
});
|
||||
const checkboxes = wrapper.getAllByRole('checkbox');
|
||||
expect(checkboxes).toHaveLength(2);
|
||||
expect(checkboxes[0]).not.toBeChecked();
|
||||
expect(checkboxes[1]).not.toBeChecked();
|
||||
|
||||
await fireEvent.click(checkboxes[0]);
|
||||
expect(checkboxes[0]).toBeChecked();
|
||||
expect(checkboxes[1]).not.toBeChecked();
|
||||
|
||||
const updatedContent = wrapper.emitted()['update-content'][0];
|
||||
expect(updatedContent).toEqual(['__TODO__\n- [x] Buy milk\n- [ ] Buy socks\n']);
|
||||
});
|
||||
|
||||
it('should render inputs as plain text', () => {
|
||||
const wrapper = render(N8nMarkdown, {
|
||||
props: {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
:content="modelValue"
|
||||
:with-multi-breaks="true"
|
||||
@markdown-click="onMarkdownClick"
|
||||
@update-content="onUpdateModelValue"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
27
packages/design-system/src/utils/__tests__/markdown.spec.ts
Normal file
27
packages/design-system/src/utils/__tests__/markdown.spec.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { toggleCheckbox } from '../markdown';
|
||||
|
||||
describe('toggleCheckbox', () => {
|
||||
it('should do nothing when there are no checkboxes', () => {
|
||||
const content = '"## I\'m a note \n Test\n"';
|
||||
expect(toggleCheckbox(content, 0)).toBe(content);
|
||||
});
|
||||
|
||||
it('should toggle a checkbox at a specific index', () => {
|
||||
const content = '"## I\'m a note \n* [ ] First\n* [ ] Second\n* [ ] Third\n"';
|
||||
expect(toggleCheckbox(content, 0)).toBe(
|
||||
'"## I\'m a note \n* [x] First\n* [ ] Second\n* [ ] Third\n"',
|
||||
);
|
||||
expect(toggleCheckbox(content, 1)).toBe(
|
||||
'"## I\'m a note \n* [ ] First\n* [x] Second\n* [ ] Third\n"',
|
||||
);
|
||||
expect(toggleCheckbox(content, 2)).toBe(
|
||||
'"## I\'m a note \n* [ ] First\n* [ ] Second\n* [x] Third\n"',
|
||||
);
|
||||
|
||||
const updatedContent = toggleCheckbox(content, 1);
|
||||
expect(toggleCheckbox(updatedContent, 0)).toBe(
|
||||
'"## I\'m a note \n* [x] First\n* [x] Second\n* [ ] Third\n"',
|
||||
);
|
||||
expect(toggleCheckbox(updatedContent, 1)).toBe(content);
|
||||
});
|
||||
});
|
|
@ -11,3 +11,36 @@ export const escapeMarkdown = (html: string | undefined): string => {
|
|||
|
||||
return withQuotes;
|
||||
};
|
||||
|
||||
const checkedRegEx = /(\*|-) \[x\]/;
|
||||
const uncheckedRegEx = /(\*|-) \[\s\]/;
|
||||
|
||||
/**
|
||||
* Toggles the checkbox at the specified index in the given markdown string.
|
||||
*
|
||||
* @param markdown - The markdown string containing checkboxes.
|
||||
* @param index - The index of the checkbox to toggle.
|
||||
* @returns The updated markdown string with the checkbox toggled.
|
||||
*/
|
||||
export const toggleCheckbox = (markdown: string, index: number) => {
|
||||
let cursor = 0;
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
||||
const line = lines[lineNumber];
|
||||
const checked = checkedRegEx.test(line);
|
||||
const unchecked = uncheckedRegEx.test(line);
|
||||
|
||||
if (checked || unchecked) {
|
||||
if (cursor === index) {
|
||||
const regExp = checked ? checkedRegEx : uncheckedRegEx;
|
||||
const replacement = checked ? '[ ]' : '[x]';
|
||||
lines[lineNumber] = line.replace(regExp, `$1 ${replacement}`);
|
||||
break;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue