diff --git a/modules/@apostrophecms/i18n/i18n/en.json b/modules/@apostrophecms/i18n/i18n/en.json
index 3611e1c773..41b5dad453 100644
--- a/modules/@apostrophecms/i18n/i18n/en.json
+++ b/modules/@apostrophecms/i18n/i18n/en.json
@@ -263,6 +263,7 @@
"richTextItalic": "Italic",
"richTextLink": "Link",
"richTextParagraph": "Paragraph (P)",
+ "richTextPlaceholder": "Start Typing Here...",
"richTextH2": "Heading 2 (H2)",
"richTextH3": "Heading 3 (H3)",
"richTextH4": "Heading 4 (H4)",
diff --git a/modules/@apostrophecms/i18n/i18n/es.json b/modules/@apostrophecms/i18n/i18n/es.json
index dc670f239f..114e7e225a 100644
--- a/modules/@apostrophecms/i18n/i18n/es.json
+++ b/modules/@apostrophecms/i18n/i18n/es.json
@@ -242,6 +242,7 @@
"richTextItalic": "Cursiva",
"richTextLink": "Liga",
"richTextParagraph": "Párrafo (P)",
+ "richTextPlaceholder": "Comience a escribir aquí...",
"richTextH2": "Título 2 (H2)",
"richTextH3": "Título 3 (H3)",
"richTextH4": "Título 4 (H4)",
diff --git a/modules/@apostrophecms/i18n/i18n/fr.json b/modules/@apostrophecms/i18n/i18n/fr.json
index 726d6da0b4..ed2694f783 100644
--- a/modules/@apostrophecms/i18n/i18n/fr.json
+++ b/modules/@apostrophecms/i18n/i18n/fr.json
@@ -249,6 +249,7 @@
"richTextItalic": "Italique",
"richTextLink": "Lien",
"richTextParagraph": "Paragraphe (P)",
+ "richTextPlaceholder": "Commencez à écrire ici...",
"richTextH2": "Titre de niveau 2 (H2)",
"richTextH3": "Titre de niveau 3 (H3)",
"richTextH4": "Titre de niveau 4 (H4)",
diff --git a/modules/@apostrophecms/i18n/i18n/pt-BR.json b/modules/@apostrophecms/i18n/i18n/pt-BR.json
index bf770b1a30..70c23ba0fc 100644
--- a/modules/@apostrophecms/i18n/i18n/pt-BR.json
+++ b/modules/@apostrophecms/i18n/i18n/pt-BR.json
@@ -240,6 +240,7 @@
"richTextItalic": "Itálico",
"richTextLink": "Link",
"richTextParagraph": "Parágrafo (P)",
+ "richTextPlaceholder": "Comece a digitar aqui...",
"richTextH2": "Título 2 (H2)",
"richTextH3": "Título 3 (H3)",
"richTextH4": "Título 4 (H4)",
diff --git a/modules/@apostrophecms/i18n/i18n/sk.json b/modules/@apostrophecms/i18n/i18n/sk.json
index a7ba795d1d..4019a1594d 100644
--- a/modules/@apostrophecms/i18n/i18n/sk.json
+++ b/modules/@apostrophecms/i18n/i18n/sk.json
@@ -252,6 +252,7 @@
"richTextItalic": "Kurzíva",
"richTextLink": "Link",
"richTextParagraph": "Odstavec (P)",
+ "richTextPlaceholder": "Začnite písať tu...",
"richTextH2": "Nadpis 2 (H2)",
"richTextH3": "Nadpis 3 (H3)",
"richTextH4": "Nadpis 4 (H4)",
diff --git a/modules/@apostrophecms/rich-text-widget/index.js b/modules/@apostrophecms/rich-text-widget/index.js
index 863e8a8ffe..f85c707f10 100644
--- a/modules/@apostrophecms/rich-text-widget/index.js
+++ b/modules/@apostrophecms/rich-text-widget/index.js
@@ -9,6 +9,7 @@ module.exports = {
icon: 'format-text-icon',
label: 'apostrophe:richText',
contextual: true,
+ placeholderText: 'apostrophe:richTextPlaceholder',
defaultData: { content: '' },
className: false,
minimumDefaultOptions: {
@@ -417,7 +418,8 @@ module.exports = {
tools: self.options.editorTools,
defaultOptions: self.options.defaultOptions,
tiptapTextCommands: self.options.tiptapTextCommands,
- tiptapTypes: self.options.tiptapTypes
+ tiptapTypes: self.options.tiptapTypes,
+ placeholderText: self.options.placeholderText
};
return finalData;
}
diff --git a/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue b/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
index 8ed9002c32..eacef9f22c 100644
--- a/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
+++ b/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue
@@ -28,7 +28,10 @@
-
+
{{ $t('apostrophe:emptyRichTextWidget') }}
@@ -45,6 +48,8 @@ import TextAlign from '@tiptap/extension-text-align';
import Highlight from '@tiptap/extension-highlight';
import TextStyle from '@tiptap/extension-text-style';
import Underline from '@tiptap/extension-underline';
+import Placeholder from '@tiptap/extension-placeholder';
+
export default {
name: 'AposRichTextWidgetEditor',
components: {
@@ -88,7 +93,9 @@ export default {
},
hasErrors: false
},
- pending: null
+ pending: null,
+ isFocused: null,
+ showPlaceholder: null
};
},
computed: {
@@ -158,6 +165,9 @@ export default {
tiptapTypes() {
return this.moduleOptions.tiptapTypes;
},
+ placeholderText() {
+ return this.moduleOptions.placeholderText;
+ },
aposTiptapExtensions() {
return (apos.tiptapExtensions || [])
.map(extension => extension({
@@ -176,19 +186,63 @@ export default {
}
},
mounted() {
+ const extensions = [
+ StarterKit,
+ TextAlign.configure({
+ types: [ 'heading', 'paragraph' ]
+ }),
+ Highlight,
+ TextStyle,
+ Underline,
+
+ // For this contextual widget, no need to check `widget.aposPlaceholder` value
+ // since `placeholderText` option is enough to decide whether to display it or not.
+ this.placeholderText && Placeholder.configure({
+ placeholder: () => {
+ // Avoid brief display of the placeholder when loading the page.
+ if (this.isFocused === null) {
+ return '';
+ }
+
+ // Display placeholder after loading the page.
+ if (this.showPlaceholder === null) {
+ return this.$t(this.placeholderText);
+ }
+
+ return this.showPlaceholder ? this.$t(this.placeholderText) : '';
+ }
+ })
+ ]
+ .filter(Boolean)
+ .concat(this.aposTiptapExtensions);
+
this.editor = new Editor({
content: this.initialContent,
autofocus: this.autofocus,
onUpdate: this.editorUpdate,
- extensions: [
- StarterKit,
- TextAlign.configure({
- types: [ 'heading', 'paragraph' ]
- }),
- Highlight,
- TextStyle,
- Underline
- ].concat(this.aposTiptapExtensions)
+ extensions,
+
+ // The following events are triggered:
+ // - before the placeholder configuration function, when loading the page
+ // - after it, once the page is loaded and we interact with the editors
+ // To solve this issue, use another `this.showPlaceholder` variable
+ // and toggle it after the placeholder configuration function is called,
+ // thanks to nextTick.
+ // The proper thing would be to call nextTick inside the placeholder
+ // function so that it can rely on the focus state set by these event
+ // listeners, but the placeholder function is called synchronously...
+ onFocus: () => {
+ this.isFocused = true;
+ this.$nextTick(() => {
+ this.showPlaceholder = false;
+ });
+ },
+ onBlur: () => {
+ this.isFocused = false;
+ this.$nextTick(() => {
+ this.showPlaceholder = true;
+ });
+ }
});
},
@@ -336,6 +390,14 @@ export default {
outline: none;
}
+ .apos-rich-text-editor__editor ::v-deep .ProseMirror p.is-empty:first-child::before {
+ content: attr(data-placeholder);
+ float: left;
+ pointer-events: none;
+ height: 0;
+ color: var(--a-base-4);
+ }
+
.apos-rich-text-editor__editor {
@include apos-transition();
position: relative;
diff --git a/package.json b/package.json
index a279ca9e30..352a99658d 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@opentelemetry/semantic-conventions": "^1.0.1",
"@tiptap/extension-highlight": "^2.0.0-beta.33",
"@tiptap/extension-link": "^2.0.0-beta.38",
+ "@tiptap/extension-placeholder": "^2.0.0-beta.196",
"@tiptap/extension-text-align": "^2.0.0-beta.29",
"@tiptap/extension-text-style": "^2.0.0-beta.23",
"@tiptap/extension-underline": "^2.0.0-beta.23",
@@ -124,16 +125,16 @@
"eslint-config-apostrophe": "^3.4.0",
"eslint-plugin-n": "^15.2.1",
"eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.9.0",
"mocha": "^9.1.2",
"nyc": "^15.1.0",
"replace-in-file": "^6.1.0",
- "vue-eslint-parser": "^7.1.1",
- "webpack-bundle-analyzer": "^3.9.0",
- "eslint-plugin-promise": "^5.1.0",
"stylelint": "^14.6.1",
"stylelint-declaration-strict-value": "^1.8.0",
- "stylelint-order": "^5.0.0"
+ "stylelint-order": "^5.0.0",
+ "vue-eslint-parser": "^7.1.1",
+ "webpack-bundle-analyzer": "^3.9.0"
},
"browserslist": [
"ie >= 10"