apostrophe 3.29.1 → 3.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/modules/@apostrophecms/area/index.js +6 -0
  3. package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +18 -2
  4. package/modules/@apostrophecms/asset/lib/webpack/apos/webpack.config.js +2 -0
  5. package/modules/@apostrophecms/attachment/index.js +23 -0
  6. package/modules/@apostrophecms/cache/index.js +1 -1
  7. package/modules/@apostrophecms/express/index.js +60 -1
  8. package/modules/@apostrophecms/i18n/i18n/en.json +2 -0
  9. package/modules/@apostrophecms/i18n/i18n/es.json +2 -0
  10. package/modules/@apostrophecms/i18n/i18n/fr.json +2 -0
  11. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +2 -0
  12. package/modules/@apostrophecms/i18n/i18n/sk.json +2 -0
  13. package/modules/@apostrophecms/image-widget/index.js +4 -1
  14. package/modules/@apostrophecms/image-widget/public/placeholder.jpg +0 -0
  15. package/modules/@apostrophecms/image-widget/ui/src/index.scss +3 -0
  16. package/modules/@apostrophecms/image-widget/views/widget.html +35 -27
  17. package/modules/@apostrophecms/rich-text-widget/index.js +4 -1
  18. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +73 -11
  19. package/modules/@apostrophecms/video-widget/index.js +4 -1
  20. package/modules/@apostrophecms/video-widget/views/widget.html +24 -16
  21. package/modules/@apostrophecms/widget-type/index.js +27 -5
  22. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidget.vue +5 -2
  23. package/modules/@apostrophecms/widget-type/ui/apos/mixins/AposWidgetMixin.js +11 -0
  24. package/package.json +7 -6
  25. package/test/attachments.js +69 -0
  26. package/test/data/upload_tests/tiny.mp4 +0 -0
  27. package/test/modules/placeholder-page/index.js +21 -0
  28. package/test/modules/placeholder-page/views/page.html +3 -0
  29. package/test/modules/placeholder-widget/index.js +36 -0
  30. package/test/modules/placeholder-widget/views/widget.html +5 -0
  31. package/test/widgets.js +453 -114
package/CHANGELOG.md CHANGED
@@ -1,11 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.31.0 (2022-10-27)
4
+
5
+ ### Adds
6
+
7
+ * Adds `placeholder: true` and `initialModal: false` features to improve the user experience of adding widgets to the page. Checkout the [Widget Placeholders documentation](https://v3.docs.apostrophecms.org/guide/areas-and-widgets.html#adding-placeholder-content-to-widgets) for more detail.
8
+
9
+ ## 3.30.0 (2022-10-12)
10
+
11
+ ### Adds
12
+
13
+ * New `APOS_LOG_ALL_ROUTES` environment variable. If set, Apostrophe logs information about all middleware functions and routes that are executed on behalf of a particular URL.
14
+ * Adds the `addFileGroups` option to the `attachment` module. Additionally it exposes a new method, `addFileGroup(group)`. These allow easier addition of new file groups or extension of the existing groups.
15
+
16
+ ### Fixes
17
+
18
+ * Vue 3 may now be used in a separate webpack build at project level without causing problems for the admin UI Vue 2 build.
19
+ * Fixes `cache` module `clear-cache` CLI task message
20
+ * Fixes help message for `express` module `list-routes` CLI task
21
+
3
22
  ## 3.29.1 (2022-10-03)
4
23
 
5
24
  ### Fixes
6
25
 
7
26
  * Hotfix to restore Node 14 support. Of course Node 16 is also supported.
8
27
 
28
+
9
29
  ## 3.29.0 (2022-10-03)
10
30
 
11
31
  ### Adds
@@ -575,6 +575,8 @@ module.exports = {
575
575
  const widgetEditors = {};
576
576
  const widgetManagers = {};
577
577
  const widgetIsContextual = {};
578
+ const widgetHasPlaceholder = {};
579
+ const widgetHasInitialModal = {};
578
580
  const contextualWidgetDefaultData = {};
579
581
 
580
582
  _.each(self.widgetManagers, function (manager, name) {
@@ -584,6 +586,8 @@ module.exports = {
584
586
  widgetEditors[name] = (browserData && browserData.components && browserData.components.widgetEditor) || 'AposWidgetEditor';
585
587
  widgetManagers[name] = manager.__meta.name;
586
588
  widgetIsContextual[name] = manager.options.contextual;
589
+ widgetHasPlaceholder[name] = manager.options.placeholder;
590
+ widgetHasInitialModal[name] = !manager.options.placeholder && manager.options.initialModal !== false;
587
591
  contextualWidgetDefaultData[name] = manager.options.defaultData;
588
592
  });
589
593
 
@@ -594,6 +598,8 @@ module.exports = {
594
598
  widgetEditors
595
599
  },
596
600
  widgetIsContextual,
601
+ widgetHasPlaceholder,
602
+ widgetHasInitialModal,
597
603
  contextualWidgetDefaultData,
598
604
  widgetManagers,
599
605
  action: self.action
@@ -404,6 +404,8 @@ export default {
404
404
  }
405
405
  },
406
406
  async update(widget) {
407
+ widget.aposPlaceholder = false;
408
+
407
409
  if (this.docId === window.apos.adminBar.contextId) {
408
410
  apos.bus.$emit('context-edited', {
409
411
  [`@${widget._id}`]: widget
@@ -434,9 +436,17 @@ export default {
434
436
  } else if (this.widgetIsContextual(name)) {
435
437
  return this.insert({
436
438
  widget: {
437
- _id: cuid(),
438
439
  type: name,
439
- ...this.contextualWidgetDefaultData(name)
440
+ ...this.contextualWidgetDefaultData(name),
441
+ aposPlaceholder: this.widgetHasPlaceholder(name)
442
+ },
443
+ index
444
+ });
445
+ } else if (!this.widgetHasInitialModal(name)) {
446
+ return this.insert({
447
+ widget: {
448
+ type: name,
449
+ aposPlaceholder: this.widgetHasPlaceholder(name)
440
450
  },
441
451
  index
442
452
  });
@@ -502,6 +512,12 @@ export default {
502
512
  widgetIsContextual(type) {
503
513
  return this.moduleOptions.widgetIsContextual[type];
504
514
  },
515
+ widgetHasPlaceholder(type) {
516
+ return this.moduleOptions.widgetHasPlaceholder[type];
517
+ },
518
+ widgetHasInitialModal(type) {
519
+ return this.moduleOptions.widgetHasInitialModal[type];
520
+ },
505
521
  widgetEditorComponent(type) {
506
522
  return this.moduleOptions.components.widgetEditors[type];
507
523
  },
@@ -24,6 +24,8 @@ module.exports = ({
24
24
 
25
25
  const config = {
26
26
  entry: importFile,
27
+ // Ensure that the correct version of vue-loader is found
28
+ context: __dirname,
27
29
  mode: process.env.NODE_ENV || 'development',
28
30
  optimization: {
29
31
  minimize: process.env.NODE_ENV === 'production'
@@ -87,6 +87,12 @@ module.exports = {
87
87
  }
88
88
  ];
89
89
 
90
+ if (self.options.addFileGroups) {
91
+ self.options.addFileGroups.forEach(newGroup => {
92
+ self.addFileGroup(newGroup);
93
+ });
94
+ };
95
+
90
96
  // Do NOT add keys here unless they have the value `true`
91
97
  self.croppable = {
92
98
  gif: true,
@@ -1183,6 +1189,23 @@ module.exports = {
1183
1189
  });
1184
1190
  });
1185
1191
  },
1192
+
1193
+ addFileGroup(newGroup) {
1194
+ if (self.fileGroups.some(existingGroup => existingGroup.name === newGroup.name)) {
1195
+ const existingGroup = self.fileGroups.find(existingGroup => existingGroup.name === newGroup.name);
1196
+ if (newGroup.extensions) {
1197
+ existingGroup.extensions = [ ...existingGroup.extensions, ...newGroup.extensions ];
1198
+ };
1199
+ if (newGroup.extensionMaps) {
1200
+ existingGroup.extensionMaps = {
1201
+ ...existingGroup.extensionMaps,
1202
+ ...newGroup.extensionMaps
1203
+ };
1204
+ }
1205
+ } else {
1206
+ self.fileGroups.push(newGroup);
1207
+ }
1208
+ },
1186
1209
  ...require('./lib/legacy-migrations')(self)
1187
1210
  };
1188
1211
  },
@@ -97,7 +97,7 @@ module.exports = {
97
97
  tasks(self) {
98
98
  return {
99
99
  'clear-cache': {
100
- help: 'Usage: node app @apostrophecms/cache:clear namespace1 namespace2...\n\nClears all values stored in a given namespace or namespaces. If you are using apos.cache in your own code you will\nknow the namespace name. Standard caches include "@apostrophecms/oembed". Normally it is not necessary to clear them.',
100
+ usage: 'usage: node app @apostrophecms/cache:clear-cache namespace1 namespace2...\n\nClears all values stored in a given namespace or namespaces. If you are using apos.cache in your own code you will\nknow the namespace name. Standard caches include "@apostrophecms/oembed". Normally it is not necessary to clear them.',
101
101
  task: async (argv) => {
102
102
  const namespaces = argv._.slice(1);
103
103
  if (!namespaces.length) {
@@ -171,7 +171,7 @@ module.exports = {
171
171
  tasks(self) {
172
172
  return {
173
173
  'list-routes': {
174
- help: 'List all Express routes registered via routes(), apiRoutes(), etc. (not directly via apos.app)',
174
+ usage: 'Usage: node app @apostrophecms/express:list-routes \n\n List all Express routes registered via routes(), apiRoutes(), etc. (not directly via apos.app)',
175
175
  async task(argv) {
176
176
  for (const info of self.finalModuleMiddlewareAndRoutes) {
177
177
  if (info.route) {
@@ -214,14 +214,23 @@ module.exports = {
214
214
  await self.findModuleMiddlewareAndRoutes();
215
215
  for (const item of self.finalModuleMiddlewareAndRoutes) {
216
216
  if (item.method) {
217
+ if (process.env.APOS_LOG_ALL_ROUTES) {
218
+ item.route._aposItem = item;
219
+ }
217
220
  self.apos.app[item.method](item.url, item.route);
218
221
  } else if (item.middleware) {
222
+ if (process.env.APOS_LOG_ALL_ROUTES) {
223
+ item.middleware._aposItem = item;
224
+ }
219
225
  if (item.url) {
220
226
  self.apos.app.use(item.url, item.middleware);
221
227
  } else {
222
228
  self.apos.app.use(item.middleware);
223
229
  }
224
230
  } else if ((typeof item) === 'function') {
231
+ if (process.env.APOS_LOG_ALL_ROUTES) {
232
+ item._aposItem = item;
233
+ }
225
234
  // Simple middleware
226
235
  self.apos.app.use(item);
227
236
  } else {
@@ -378,6 +387,42 @@ module.exports = {
378
387
  strictNullHandling: true
379
388
  });
380
389
  });
390
+ if (process.env.APOS_LOG_ALL_ROUTES) {
391
+ self.logAllRoutes();
392
+ }
393
+ },
394
+
395
+ logAllRoutes() {
396
+ const superUse = self.apos.app.use.bind(self.apos.app);
397
+ const methods = [ 'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all' ];
398
+ self.apos.app.use = function (path, middleware) {
399
+ if (typeof path === 'function') {
400
+ middleware = path;
401
+ path = '';
402
+ }
403
+ superUse(path, (req, ...args) => {
404
+ const moduleName = middleware._aposItem && middleware._aposItem.moduleName;
405
+ const name = moduleName && middleware._aposItem.name;
406
+ self.apos.util.log(`${req.url} invokes middleware ${path ? `for path ${path} ` : ''}${moduleName && `found at ${moduleName}:${name}`}`);
407
+ return middleware(req, ...args);
408
+ });
409
+ };
410
+ for (const method of methods) {
411
+ const superMethod = self.apos.app[method].bind(self.apos.app);
412
+ self.apos.app[method] = (path, ...args) => {
413
+ if ((method === 'get') && (!args.length)) {
414
+ // Handle app.get in its configuration getter form
415
+ return superMethod(path);
416
+ }
417
+ const middleware = args.slice(0, args.length - 1);
418
+ const fn = args[args.length - 1];
419
+ superMethod(path, ...middleware, (req, ...args) => {
420
+ const moduleName = (fn === self.apos.page.serve) ? '@apostrophecms/page' : (fn._aposItem && fn._aposItem.moduleName);
421
+ self.apos.util.log(`${req.url} invokes ${method.toUpperCase()} route for path ${path} ${moduleName ? `in the module ${moduleName}` : ''}`);
422
+ return fn(req, ...args);
423
+ });
424
+ };
425
+ }
381
426
  },
382
427
 
383
428
  // Patch Express so that all calls to `res.redirect` honor
@@ -650,6 +695,11 @@ module.exports = {
650
695
  const moduleNames = Array.from(new Set([ self.__meta.name, ...Object.keys(self.apos.modules) ]));
651
696
  for (const name of moduleNames) {
652
697
  const middleware = self.apos.modules[name].middleware || {};
698
+ if (process.env.APOS_LOG_ALL_ROUTES) {
699
+ for (const [ name, item ] of Object.entries(middleware)) {
700
+ item.name = name;
701
+ }
702
+ }
653
703
  labeledList.push({
654
704
  name: `middleware:${name}`,
655
705
  middleware: Object.values(middleware).filter(middleware => !middleware.before)
@@ -657,6 +707,11 @@ module.exports = {
657
707
  }
658
708
  for (const name of Object.keys(self.apos.modules)) {
659
709
  const _routes = self.apos.modules[name]._routes;
710
+ if (process.env.APOS_LOG_ALL_ROUTES) {
711
+ for (const [ name, item ] of Object.entries(_routes)) {
712
+ item.name = name;
713
+ }
714
+ }
660
715
  labeledList.push({
661
716
  name: `routes:${name}`,
662
717
  routes: _routes.filter(route => !route.before)
@@ -682,8 +737,12 @@ module.exports = {
682
737
  before.prepending = before.prepending || [];
683
738
  before.prepending.push(item);
684
739
  }
740
+ if (process.env.APOS_LOG_ALL_ROUTES) {
741
+ item.moduleName = name;
742
+ }
685
743
  }
686
744
  }
745
+
687
746
  self.finalModuleMiddlewareAndRoutes = labeledList.map(item => (item.prepending || []).concat(item.middleware || item.routes)).flat();
688
747
  }
689
748
  };
@@ -161,6 +161,7 @@
161
161
  "home": "Home",
162
162
  "image": "Image",
163
163
  "imageFile": "Image File",
164
+ "imagePlaceholder": "Image placeholder",
164
165
  "imageTag": "Image Tag",
165
166
  "imageTags": "Image Tags",
166
167
  "images": "Images",
@@ -263,6 +264,7 @@
263
264
  "richTextItalic": "Italic",
264
265
  "richTextLink": "Link",
265
266
  "richTextParagraph": "Paragraph (P)",
267
+ "richTextPlaceholder": "Start Typing Here...",
266
268
  "richTextH2": "Heading 2 (H2)",
267
269
  "richTextH3": "Heading 3 (H3)",
268
270
  "richTextH4": "Heading 4 (H4)",
@@ -150,6 +150,7 @@
150
150
  "home": "Inicio",
151
151
  "image": "Imagen",
152
152
  "imageFile": "Archivo de Imagen",
153
+ "imagePlaceholder": "Marcador de posición de imagen",
153
154
  "imageTag": "Etiqueta de Imagen",
154
155
  "imageTags": "Etiquetas de Imágenes",
155
156
  "images": "Imágenes",
@@ -242,6 +243,7 @@
242
243
  "richTextItalic": "Cursiva",
243
244
  "richTextLink": "Liga",
244
245
  "richTextParagraph": "Párrafo (P)",
246
+ "richTextPlaceholder": "Comience a escribir aquí...",
245
247
  "richTextH2": "Título 2 (H2)",
246
248
  "richTextH3": "Título 3 (H3)",
247
249
  "richTextH4": "Título 4 (H4)",
@@ -148,6 +148,7 @@
148
148
  "home": "Accueil",
149
149
  "image": "Image",
150
150
  "imageFile": "Fichier d'image",
151
+ "imagePlaceholder": "Espace réservé pour l'image",
151
152
  "imageTag": "Tag d'image",
152
153
  "imageTags": "Tags d'image",
153
154
  "images": "Images",
@@ -249,6 +250,7 @@
249
250
  "richTextItalic": "Italique",
250
251
  "richTextLink": "Lien",
251
252
  "richTextParagraph": "Paragraphe (P)",
253
+ "richTextPlaceholder": "Commencez à écrire ici...",
252
254
  "richTextH2": "Titre de niveau 2 (H2)",
253
255
  "richTextH3": "Titre de niveau 3 (H3)",
254
256
  "richTextH4": "Titre de niveau 4 (H4)",
@@ -150,6 +150,7 @@
150
150
  "home": "Home",
151
151
  "image": "Imagem",
152
152
  "imageFile": "Arquivo de Imagem",
153
+ "imagePlaceholder": "Espaço reservado para imagem",
153
154
  "imageTag": "Tag de Imagem",
154
155
  "imageTags": "Tags de Imagem",
155
156
  "images": "Imagens",
@@ -240,6 +241,7 @@
240
241
  "richTextItalic": "Itálico",
241
242
  "richTextLink": "Link",
242
243
  "richTextParagraph": "Parágrafo (P)",
244
+ "richTextPlaceholder": "Comece a digitar aqui...",
243
245
  "richTextH2": "Título 2 (H2)",
244
246
  "richTextH3": "Título 3 (H3)",
245
247
  "richTextH4": "Título 4 (H4)",
@@ -154,6 +154,7 @@
154
154
  "home": "Domovská stránka",
155
155
  "image": "Obrázok",
156
156
  "imageFile": "Súbor s obrázkom",
157
+ "imagePlaceholder": "Zástupný symbol obrázka",
157
158
  "imageTag": "Značku obrázku",
158
159
  "imageTags": "Značky obrázkov",
159
160
  "images": "Obrázky",
@@ -252,6 +253,7 @@
252
253
  "richTextItalic": "Kurzíva",
253
254
  "richTextLink": "Link",
254
255
  "richTextParagraph": "Odstavec (P)",
256
+ "richTextPlaceholder": "Začnite písať tu...",
255
257
  "richTextH2": "Nadpis 2 (H2)",
256
258
  "richTextH3": "Nadpis 3 (H3)",
257
259
  "richTextH4": "Nadpis 4 (H4)",
@@ -4,7 +4,10 @@ module.exports = {
4
4
  label: 'apostrophe:image',
5
5
  className: false,
6
6
  icon: 'image-icon',
7
- dimensionAttrs: false
7
+ dimensionAttrs: false,
8
+ placeholder: true,
9
+ placeholderClass: false,
10
+ placeholderUrl: '/modules/@apostrophecms/image-widget/placeholder.jpg'
8
11
  },
9
12
  fields: {
10
13
  add: {
@@ -0,0 +1,3 @@
1
+ .image-widget-placeholder {
2
+ width: 100%;
3
+ }
@@ -1,31 +1,39 @@
1
- {% if data.options.className %}
2
- {% set className = data.options.className %}
3
- {% elif data.manager.options.className %}
4
- {% set className = data.manager.options.className %}
5
- {% endif %}
1
+ {% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
2
+ <img
3
+ src="{{ apos.asset.url(data.manager.options.placeholderUrl) }}"
4
+ alt="{{ __t('apostrophe:imagePlaceholder') }}"
5
+ class="image-widget-placeholder"
6
+ />
7
+ {% else %}
8
+ {% if data.options.className %}
9
+ {% set className = data.options.className %}
10
+ {% elif data.manager.options.className %}
11
+ {% set className = data.manager.options.className %}
12
+ {% endif %}
6
13
 
7
- {% if data.options.dimensionAttrs %}
8
- {% set dimensionAttrs = data.options.dimensionAttrs %}
9
- {% elif data.manager.options.dimensionAttrs %}
10
- {% set dimensionAttrs = data.manager.options.dimensionAttrs %}
11
- {% endif %}
14
+ {% if data.options.dimensionAttrs %}
15
+ {% set dimensionAttrs = data.options.dimensionAttrs %}
16
+ {% elif data.manager.options.dimensionAttrs %}
17
+ {% set dimensionAttrs = data.manager.options.dimensionAttrs %}
18
+ {% endif %}
12
19
 
13
- {% set attachment = apos.image.first(data.widget._image) %}
20
+ {% set attachment = apos.image.first(data.widget._image) %}
14
21
 
15
- {% if attachment %}
16
- <img {% if className %} class="{{ className }}"{% endif %}
17
- srcset="{{ apos.image.srcset(attachment) }}"
18
- src="{{ apos.attachment.url(attachment, { size: data.options.size or 'full' }) }}"
19
- alt="{{ attachment._alt or '' }}"
20
- {% if dimensionAttrs %}
21
- {% if attachment.width %} width="{{ apos.attachment.getWidth(attachment) }}" {% endif %}
22
- {% if attachment.height %} height="{{ apos.attachment.getHeight(attachment) }}" {% endif %}
23
- {% endif %}
24
- {% if data.contextOptions and data.contextOptions.sizes %}
25
- sizes="{{ data.contextOptions.sizes }}"
26
- {% endif %}
27
- {% if apos.attachment.hasFocalPoint(attachment) %}
28
- style="object-position: {{ apos.attachment.focalPointToObjectPosition(attachment) }}"
29
- {%- endif -%}
30
- />
22
+ {% if attachment %}
23
+ <img {% if className %} class="{{ className }}"{% endif %}
24
+ srcset="{{ apos.image.srcset(attachment) }}"
25
+ src="{{ apos.attachment.url(attachment, { size: data.options.size or 'full' }) }}"
26
+ alt="{{ attachment._alt or '' }}"
27
+ {% if dimensionAttrs %}
28
+ {% if attachment.width %} width="{{ apos.attachment.getWidth(attachment) }}" {% endif %}
29
+ {% if attachment.height %} height="{{ apos.attachment.getHeight(attachment) }}" {% endif %}
30
+ {% endif %}
31
+ {% if data.contextOptions and data.contextOptions.sizes %}
32
+ sizes="{{ data.contextOptions.sizes }}"
33
+ {% endif %}
34
+ {% if apos.attachment.hasFocalPoint(attachment) %}
35
+ style="object-position: {{ apos.attachment.focalPointToObjectPosition(attachment) }}"
36
+ {%- endif -%}
37
+ />
38
+ {% endif %}
31
39
  {% endif %}
@@ -9,6 +9,8 @@ module.exports = {
9
9
  icon: 'format-text-icon',
10
10
  label: 'apostrophe:richText',
11
11
  contextual: true,
12
+ placeholder: true,
13
+ placeholderText: 'apostrophe:richTextPlaceholder',
12
14
  defaultData: { content: '' },
13
15
  className: false,
14
16
  minimumDefaultOptions: {
@@ -417,7 +419,8 @@ module.exports = {
417
419
  tools: self.options.editorTools,
418
420
  defaultOptions: self.options.defaultOptions,
419
421
  tiptapTextCommands: self.options.tiptapTextCommands,
420
- tiptapTypes: self.options.tiptapTypes
422
+ tiptapTypes: self.options.tiptapTypes,
423
+ placeholderText: self.options.placeholder && self.options.placeholderText
421
424
  };
422
425
  return finalData;
423
426
  }
@@ -28,7 +28,10 @@
28
28
  <div class="apos-rich-text-editor__editor" :class="editorModifiers">
29
29
  <editor-content :editor="editor" :class="editorOptions.className" />
30
30
  </div>
31
- <div class="apos-rich-text-editor__editor_after" :class="editorModifiers">
31
+ <div
32
+ v-if="showPlaceholder !== null && (!placeholderText || !isFocused)"
33
+ class="apos-rich-text-editor__editor_after" :class="editorModifiers"
34
+ >
32
35
  {{ $t('apostrophe:emptyRichTextWidget') }}
33
36
  </div>
34
37
  </div>
@@ -45,6 +48,8 @@ import TextAlign from '@tiptap/extension-text-align';
45
48
  import Highlight from '@tiptap/extension-highlight';
46
49
  import TextStyle from '@tiptap/extension-text-style';
47
50
  import Underline from '@tiptap/extension-underline';
51
+ import Placeholder from '@tiptap/extension-placeholder';
52
+
48
53
  export default {
49
54
  name: 'AposRichTextWidgetEditor',
50
55
  components: {
@@ -88,7 +93,9 @@ export default {
88
93
  },
89
94
  hasErrors: false
90
95
  },
91
- pending: null
96
+ pending: null,
97
+ isFocused: null,
98
+ showPlaceholder: null
92
99
  };
93
100
  },
94
101
  computed: {
@@ -158,6 +165,9 @@ export default {
158
165
  tiptapTypes() {
159
166
  return this.moduleOptions.tiptapTypes;
160
167
  },
168
+ placeholderText() {
169
+ return this.moduleOptions.placeholderText;
170
+ },
161
171
  aposTiptapExtensions() {
162
172
  return (apos.tiptapExtensions || [])
163
173
  .map(extension => extension({
@@ -176,19 +186,63 @@ export default {
176
186
  }
177
187
  },
178
188
  mounted() {
189
+ const extensions = [
190
+ StarterKit,
191
+ TextAlign.configure({
192
+ types: [ 'heading', 'paragraph' ]
193
+ }),
194
+ Highlight,
195
+ TextStyle,
196
+ Underline,
197
+
198
+ // For this contextual widget, no need to check `widget.aposPlaceholder` value
199
+ // since `placeholderText` option is enough to decide whether to display it or not.
200
+ this.placeholderText && Placeholder.configure({
201
+ placeholder: () => {
202
+ // Avoid brief display of the placeholder when loading the page.
203
+ if (this.isFocused === null) {
204
+ return '';
205
+ }
206
+
207
+ // Display placeholder after loading the page.
208
+ if (this.showPlaceholder === null) {
209
+ return this.$t(this.placeholderText);
210
+ }
211
+
212
+ return this.showPlaceholder ? this.$t(this.placeholderText) : '';
213
+ }
214
+ })
215
+ ]
216
+ .filter(Boolean)
217
+ .concat(this.aposTiptapExtensions);
218
+
179
219
  this.editor = new Editor({
180
220
  content: this.initialContent,
181
221
  autofocus: this.autofocus,
182
222
  onUpdate: this.editorUpdate,
183
- extensions: [
184
- StarterKit,
185
- TextAlign.configure({
186
- types: [ 'heading', 'paragraph' ]
187
- }),
188
- Highlight,
189
- TextStyle,
190
- Underline
191
- ].concat(this.aposTiptapExtensions)
223
+ extensions,
224
+
225
+ // The following events are triggered:
226
+ // - before the placeholder configuration function, when loading the page
227
+ // - after it, once the page is loaded and we interact with the editors
228
+ // To solve this issue, use another `this.showPlaceholder` variable
229
+ // and toggle it after the placeholder configuration function is called,
230
+ // thanks to nextTick.
231
+ // The proper thing would be to call nextTick inside the placeholder
232
+ // function so that it can rely on the focus state set by these event
233
+ // listeners, but the placeholder function is called synchronously...
234
+ onFocus: () => {
235
+ this.isFocused = true;
236
+ this.$nextTick(() => {
237
+ this.showPlaceholder = false;
238
+ });
239
+ },
240
+ onBlur: () => {
241
+ this.isFocused = false;
242
+ this.$nextTick(() => {
243
+ this.showPlaceholder = true;
244
+ });
245
+ }
192
246
  });
193
247
  },
194
248
 
@@ -336,6 +390,14 @@ export default {
336
390
  outline: none;
337
391
  }
338
392
 
393
+ .apos-rich-text-editor__editor ::v-deep .ProseMirror p.is-empty:first-child::before {
394
+ content: attr(data-placeholder);
395
+ float: left;
396
+ pointer-events: none;
397
+ height: 0;
398
+ color: var(--a-base-4);
399
+ }
400
+
339
401
  .apos-rich-text-editor__editor {
340
402
  @include apos-transition();
341
403
  position: relative;
@@ -14,7 +14,10 @@ module.exports = {
14
14
  options: {
15
15
  label: 'apostrophe:video',
16
16
  className: false,
17
- icon: 'play-box-icon'
17
+ icon: 'play-box-icon',
18
+ placeholder: true,
19
+ placeholderClass: false,
20
+ placeholderUrl: 'https://youtu.be/Q5UX9yexEyM'
18
21
  },
19
22
  fields: {
20
23
  add: {
@@ -1,20 +1,28 @@
1
- {% if data.options.className %}
2
- {% set className = data.options.className %}
3
- {% elif data.manager.options.className %}
4
- {% set className = data.manager.options.className %}
5
- {% endif %}
6
-
7
- {# oembed repopulates me #}
8
- {% if data.widget.video %}
1
+ {% if data.widget.aposPlaceholder and data.manager.options.placeholderUrl %}
9
2
  <div
10
- {% if className %} class="{{ className }}"{% endif %}
11
3
  data-apos-video-widget
12
- data-apos-video-url={{ data.widget.video.url }}
4
+ data-apos-video-url="{{ data.manager.options.placeholderUrl }}"
13
5
  >
14
- {% if data.widget.video.thumbnail %}
15
- <img src="{{ data.widget.video.thumbnail }}" alt="{{ data.widget.video.thumbnail }}"/>
16
- {% endif %}
17
6
  </div>
18
- {% elif data.user %}
19
- <p {% if data.manager.options.className %} class="{{ data.manager.options.className }} {{ data.manager.options.className }}--error"{% endif %}>No video selected</p>
20
- {% endif %}
7
+ {% else %}
8
+ {% if data.options.className %}
9
+ {% set className = data.options.className %}
10
+ {% elif data.manager.options.className %}
11
+ {% set className = data.manager.options.className %}
12
+ {% endif %}
13
+
14
+ {# oembed repopulates me #}
15
+ {% if data.widget.video %}
16
+ <div
17
+ {% if className %} class="{{ className }}"{% endif %}
18
+ data-apos-video-widget
19
+ data-apos-video-url={{ data.widget.video.url }}
20
+ >
21
+ {% if data.widget.video.thumbnail %}
22
+ <img src="{{ data.widget.video.thumbnail }}" alt="{{ data.widget.video.thumbnail }}"/>
23
+ {% endif %}
24
+ </div>
25
+ {% elif data.user %}
26
+ <p {% if data.manager.options.className %} class="{{ data.manager.options.className }} {{ data.manager.options.className }}--error"{% endif %}>No video selected</p>
27
+ {% endif %}
28
+ {% endif %}