Skip to content

Commit d29ced2

Browse files
authored
Pro 7371 link pages (#4901)
* fixes chrome bug: cannot check box when text is selected when in sortablejs item * fixes rich text insert menu popover not being properly closed * workarounds to avoid tiptap toolbat staying stuck (no blur close)
1 parent a627e9d commit d29ced2

13 files changed

+85
-22
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "extends": ["apostrophe"] }
1+
{ "extends": "apostrophe" }

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
* Change user's email field type to `email`.
1010
* Improve media manager experience after uploading images. No additional server requests are made, no broken UI on error.
1111
* Change reset password form button label to `Reset Password`.
12+
* Bumps eslint-config-apostrophe, fix errors and a bunch of warnings.
1213

1314
### Fixes
1415

1516
* Fixes an edge case where reordering a page in the Page Manager might affect another locale.
17+
* Fixes chrome bug when pages manager checkboxes need a double click when coming from the rich text editor (because some text is selected).
18+
* Fixes the rich text insert menu image menu not being properly closed.
19+
* Fixes the rich text toolbar not closing sometimes when unfocusing the editor.
1620

1721
## 4.14.2 (2025-04-02)
1822

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue

+25-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
class-list="apos-rich-text-toolbar"
2424
:has-tip="false"
2525
>
26-
<div class="apos-rich-text-toolbar__inner">
26+
<div
27+
ref="toolbar"
28+
class="apos-rich-text-toolbar__inner"
29+
>
2730
<component
2831
:is="(tools[item] && tools[item].component) || 'AposTiptapUndefined'"
2932
v-for="(item, index) in toolbar"
@@ -32,6 +35,9 @@
3235
:tool="tools[item]"
3336
:options="editorOptions"
3437
:editor="editor"
38+
@open-popover="openPopover"
39+
@close="closeToolbar"
40+
@focusout="onBtnBlur"
3541
/>
3642
</div>
3743
</AposContextMenuDialog>
@@ -186,7 +192,8 @@ export default {
186192
showPlaceholder: null,
187193
activeInsertMenuComponent: false,
188194
suppressInsertMenu: false,
189-
insertMenuKey: null
195+
insertMenuKey: null,
196+
openedPopover: false
190197
};
191198
},
192199
computed: {
@@ -405,6 +412,18 @@ export default {
405412
apos.bus.$off('apos-refreshing', this.onAposRefreshing);
406413
},
407414
methods: {
415+
openPopover() {
416+
this.openedPopover = true;
417+
},
418+
onBtnBlur(e) {
419+
if (this.openedPopover) {
420+
return;
421+
}
422+
if (this.$refs.toolbar?.contains(e.relatedTarget)) {
423+
return;
424+
}
425+
this.closeToolbar();
426+
},
408427
onBubbleHide() {
409428
apos.bus.$emit('close-context-menus', 'richText');
410429
},
@@ -651,6 +670,10 @@ export default {
651670
},
652671
setActiveInsertMenu(isActive = true) {
653672
this.activeInsertMenuComponent = isActive;
673+
},
674+
closeToolbar() {
675+
this.openedPopover = false;
676+
this.editor.chain().focus().run();
654677
}
655678
}
656679
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export default {
7272
required: true
7373
}
7474
},
75+
emits: [ 'close' ],
7576
data () {
7677
return {
7778
generation: 1,
@@ -144,11 +145,11 @@ export default {
144145
removeAnchor() {
145146
this.docFields.data = {};
146147
this.editor.commands.unsetAnchor();
148+
this.editor.chain().focus().blur().run();
147149
this.close();
148150
},
149151
close() {
150152
this.$refs.contextMenu.hide();
151-
this.editor.chain().focus();
152153
},
153154
async save() {
154155
this.triggerValidation = true;
@@ -160,6 +161,7 @@ export default {
160161
this.editor.commands.setAnchor({
161162
id: this.docFields.data.anchor
162163
});
164+
this.editor.chain().focus().blur().run();
163165
this.close();
164166
},
165167
keyboardHandler(e) {
@@ -195,6 +197,7 @@ export default {
195197
},
196198
closePopover() {
197199
window.removeEventListener('keydown', this.keyboardHandler);
200+
this.$emit('close');
198201
}
199202
}
200203
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapColor.vue

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const props = defineProps({
8080
default: () => ({})
8181
}
8282
});
83+
const emit = defineEmits([ 'open-popover', 'close' ]);
8384
8485
const moduleOptions = window.apos.modules['@apostrophecms/rich-text-widget'];
8586
@@ -117,9 +118,11 @@ watch(
117118
118119
function openPopover() {
119120
addEventListener('keydown', keyboardHandler);
121+
emit('open-popover');
120122
}
121123
function closePopover() {
122124
removeEventListener('keydown', keyboardHandler);
125+
emit('close');
123126
}
124127
function keyboardHandler(e) {
125128
if ([ 'Escape', 'Enter' ].includes(e.key)) {
@@ -129,6 +132,7 @@ function keyboardHandler(e) {
129132
130133
function close() {
131134
if (contextMenu.value) {
135+
props.editor.chain().focus().blur().run();
132136
contextMenu.value.hide();
133137
}
134138
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
menu-placement="bottom-end"
66
:button="button"
77
:rich-text-menu="true"
8+
@open="$emit('open-popover')"
9+
@close="$emit('close')"
810
>
911
<AposImageControlDialog
1012
:editor="editor"
@@ -28,6 +30,7 @@ export default {
2830
required: true
2931
}
3032
},
33+
emits: [ 'open-popover', 'close' ],
3134
computed: {
3235
button() {
3336
return {
@@ -69,7 +72,6 @@ export default {
6972
methods: {
7073
close() {
7174
this.$refs.contextMenu.hide();
72-
this.editor.chain().focus();
7375
}
7476
}
7577
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapInsertItem.vue

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<AposContextMenu
33
v-if="menuItem.component && !menuItem.noPopover"
4+
ref="contextMenu"
45
menu-placement="bottom-end"
56
:rich-text-menu="true"
67
@open="openPopover"
@@ -65,6 +66,7 @@ const props = defineProps({
6566
const emit = defineEmits([ 'set-active-insert-menu', 'done' ]);
6667
6768
const isInlineComponentActive = ref(false);
69+
const contextMenu = ref(null);
6870
const isInlineComponentShowed = computed(() => {
6971
return Boolean(props.menuItem.noPopover &&
7072
props.menuItem.component &&
@@ -105,6 +107,9 @@ function closeInsertMenuItem() {
105107
removeSlash();
106108
emit('set-active-insert-menu', false);
107109
isInlineComponentActive.value = false;
110+
if (contextMenu.value) {
111+
contextMenu.value.hide();
112+
}
108113
}
109114
110115
function openPopover() {

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue

+8-2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default {
7373
required: true
7474
}
7575
},
76+
emits: [ 'open-popover', 'close' ],
7677
data() {
7778
const moduleOptions = apos.modules['@apostrophecms/rich-text-widget'];
7879
return {
@@ -151,11 +152,11 @@ export default {
151152
removeLink() {
152153
this.docFields.data = {};
153154
this.editor.commands.unsetLink();
155+
this.editor.chain().focus().blur().run();
154156
this.close();
155157
},
156158
close() {
157159
this.$refs.contextMenu.hide();
158-
this.editor.chain().focus();
159160
},
160161
async save() {
161162
this.triggerValidation = true;
@@ -188,6 +189,7 @@ export default {
188189
attrs.href = this.docFields.data.href;
189190
this.editor.commands.setLink(attrs);
190191
192+
this.editor.chain().focus().blur().run();
191193
this.close();
192194
},
193195
keyboardHandler(e) {
@@ -208,7 +210,9 @@ export default {
208210
this.docFields.data = {};
209211
this.schema.forEach((item) => {
210212
if (item.htmlAttribute && item.type === 'checkboxes') {
211-
this.docFields.data[item.name] = attrs[item.htmlAttribute] ? [ attrs[item.htmlAttribute] ] : [];
213+
this.docFields.data[item.name] = attrs[item.htmlAttribute]
214+
? [ attrs[item.htmlAttribute] ]
215+
: [];
212216
return;
213217
}
214218
if (item.htmlAttribute && item.type === 'boolean') {
@@ -258,9 +262,11 @@ export default {
258262
window.addEventListener('keydown', this.keyboardHandler);
259263
await this.populateFields();
260264
this.evaluateConditions();
265+
this.$emit('open-popover');
261266
},
262267
closePopover() {
263268
window.removeEventListener('keydown', this.keyboardHandler);
269+
this.$emit('close');
264270
}
265271
}
266272
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapMarks.vue

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
<template>
22
<div class="apos-marks-control">
33
<AposContextMenu
4+
ref="contextMenu"
45
menu-placement="bottom-start"
56
:button="button"
67
:rich-text-menu="true"
78
:center-on-icon="true"
9+
@open="openPopover"
10+
@close="closePopover"
811
>
912
<div class="apos-popover apos-marks-control__dialog">
1013
<div class="apos-marks-control__content-wrapper">
@@ -62,6 +65,7 @@ export default {
6265
}
6366
}
6467
},
68+
emits: [ 'open-popover', 'close' ],
6569
data() {
6670
return {
6771
classes: this.options.marks.map(m => m.class)
@@ -132,17 +136,21 @@ export default {
132136
},
133137
methods: {
134138
toggleStyle(mark) {
135-
this.editor.commands.focus();
136139
this.editor.commands[mark.command](mark.type, mark.options || {});
140+
this.editor.chain().focus().blur().run();
137141
this.close();
138142
},
139143
click() {
140144
this.toggleOpen();
141145
},
142-
toggleOpen() {
143-
},
144146
close() {
145-
this.editor.chain().focus();
147+
this.$refs.contextMenu.hide();
148+
},
149+
openPopover() {
150+
this.$emit('open-popover');
151+
},
152+
closePopover() {
153+
this.$emit('close');
146154
}
147155
}
148156
};

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue

+5-2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default {
5959
}
6060
}
6161
},
62+
emits: [ 'close' ],
6263
data() {
6364
return {
6465
multipleSelected: false
@@ -145,15 +146,17 @@ export default {
145146
methods: {
146147
setStyle($event) {
147148
const style = this.nodeOptions[$event.target.value];
148-
this.editor.commands.focus();
149149
this.editor.commands[style.command](style.type, style.options || {});
150+
this.editor.chain().focus().blur().run();
151+
this.$emit('close');
150152
}
151153
}
152154
};
153155
</script>
154156
155157
<style lang="scss" scoped>
156-
// If another select el is needed for the rich-text toolbar these styles should be made global
158+
// If another select el is needed for the rich-text toolbar
159+
// these styles should be made global
157160
.apos-tiptap-control--select {
158161
@include apos-button-reset();
159162
@include apos-transition();

modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapTable.vue

+7-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default {
4646
}
4747
}
4848
},
49+
emits: [ 'close' ],
4950
data() {
5051
return {
5152
current: ''
@@ -124,26 +125,26 @@ export default {
124125
methods: {
125126
takeAction() {
126127
const action = this.current;
127-
this.editor.commands.focus();
128128
if (action === 'insertTable') {
129129
// Reach into prosemirror for current selection,
130130
// then turn it into a cursor position only so we don't
131131
// delete the existing selection which would mean
132132
// you can only create a table by deleting some work
133-
this.editor.commands.setTextSelection(this.editor.view.state.selection.$anchor.pos);
133+
this.editor.commands.setTextSelection(
134+
this.editor.view.state.selection.$anchor.pos
135+
);
134136
}
135137
this.editor.commands[action]();
136-
// We are using the select as a menu of one-time actions, it's not really a persisted value
138+
// We are using the select as a menu of one-time actions,
139+
// it's not really a persisted value
137140
this.current = '';
141+
this.$emit('close');
138142
}
139143
}
140144
};
141145
</script>
142146

143147
<style lang="scss" scoped>
144-
// "If another select el is needed for the rich-text toolbar these styles should be made global."
145-
// ... And here we are, but first let's see if we decide to rebuild this UI without the menu. -Tom
146-
147148
.apos-tiptap-control--select {
148149
@include apos-button-reset();
149150
@include type-small;

modules/@apostrophecms/ui/ui/apos/components/AposTreeRows.vue

+6
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
readOnly: maxReached && !checked.includes(row._id)
8282
}"
8383
:choice="{ value: row._id }"
84+
@pointerdown="pointerEvent"
8485
/>
8586
<span class="apos-tree__cell__value">
8687
<AposIndicator
@@ -255,6 +256,11 @@ export default {
255256
});
256257
},
257258
methods: {
259+
// Fix for chrome when some text is selected (needed double click to check the box)
260+
// Comes from sortablejs, so we avoid the event to propagate to sortablejs listener
261+
pointerEvent(event) {
262+
event.stopPropagation();
263+
},
258264
setHeights() {
259265
this.treeBranches.forEach(branch => {
260266
// Add padding to the max-height to avoid needing a `resize`

0 commit comments

Comments
 (0)