apostrophe 3.38.1 → 3.39.1

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 (22) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/modules/@apostrophecms/area/index.js +13 -8
  3. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  4. package/modules/@apostrophecms/attachment/index.js +5 -0
  5. package/modules/@apostrophecms/attachment/lib/tasks/download-all.js +77 -0
  6. package/modules/@apostrophecms/attachment/lib/tasks/rescale.js +3 -2
  7. package/modules/@apostrophecms/command-menu/index.js +46 -0
  8. package/modules/@apostrophecms/i18n/i18n/en.json +19 -0
  9. package/modules/@apostrophecms/i18n/index.js +25 -14
  10. package/modules/@apostrophecms/image/index.js +36 -0
  11. package/modules/@apostrophecms/image-widget/views/widget.html +6 -12
  12. package/modules/@apostrophecms/rich-text-widget/index.js +195 -58
  13. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +30 -1
  14. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapAnchor.vue +1 -8
  15. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapButton.vue +1 -1
  16. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapImage.vue +261 -0
  17. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -8
  18. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapTable.vue +173 -0
  19. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Div.js +43 -0
  20. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Image.js +106 -0
  21. package/package.json +7 -1
  22. package/test/command-menu.js +70 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.39.1 (2023-02-02)
4
+
5
+ ### Fixes
6
+
7
+ * Rescaling cropped images with the `@apostrophecms/attachment:rescale` task now works correctly. Thanks to [Waldemar Pankratz](https://github.com/waldemar-p) for this contribution.
8
+
9
+ ## 3.39.0 (2023-02-01)
10
+
11
+ ### Adds
12
+
13
+ * Basic support for editing tables by adding `table` to the rich text toolbar. Enabling `table` allows you to create tables, including `td` and `th` tags, with the ability to merge and split cells. For now the table editing UI is basic, all of the functionality is there but we plan to add more conveniences for easy table editing soon. See the "Table" dropdown for actions that are permitted based on the current selection.
14
+ * `superscript` and `subscript` may now be added to the rich text widget's `toolbar` option.
15
+ * Early beta-quality support for adding inline images to rich text, by adding `image` to the rich text toolbar. This feature works reliably, however the UI is not mature yet. In particular you must search for images by typing part of the title. We will support a proper "browse" experience here soon. For good results you should also configure the `imageStyles` option. You will also want to style the `figure` tags produced. See the documentation for more information.
16
+ * Support for `div` tags in the rich text toolbar, if you choose to include them in `styles`. This is often necessary for A2 content migration and can potentially be useful in new work when combined with a `class` if there is no suitable semantic block tag.
17
+ * The new `@apostrophecms/attachment:download-all --to=folder` command line task is useful to download all of your attachments from an uploadfs backend other than local storage, especially if you do not have a more powerful "sync" utility for that particular storage backend.
18
+ * A new `loadingType` option can now be set for `image-widget` when configuring an `area` field. This sets the `loading` attribute of the `img` tag, which can be used to enable lazy loading in most browsers. Thanks to [Waldemar Pankratz](https://github.com/waldemar-p) for this contribution.
19
+ * Two new module-level options have been added to the `image-widget` module: `loadingType` and `size`. These act as fallbacks for the same options at the area level. Thanks to [Waldemar Pankratz](https://github.com/waldemar-p) for this contribution.
20
+
21
+ ### Fixes
22
+
23
+ * Adding missing require (`bluebird`) and fallback (`file.crops || []`) to `@apostrophecms/attachment:rescale`-task
24
+
3
25
  ## 3.38.1 (2023-01-23)
4
26
 
5
27
  ### Fixes
@@ -29,9 +51,12 @@ node app @apostrophecms/i18n:rename-locale --old=de-DE --new=de-de --keep=de-de
29
51
  * Emit a `beforeSaveSafe` event from the `@apostrophecms:user` module, with `req`, `safeUser` and `user` as arguments, in order to give the possibility to override properties of the `safeUser` object which contains password hashes and other information too sensitive to be stored in the aposDocs collection.
30
52
  * Automatically convert failed uppercase URLs to their lowercase version - can be disabled with `redirectFailedUpperCaseUrls: false` in `@apostrophecms/page/index.js` options. This only comes into play if a 404 is about to happen.
31
53
  * Automatically convert country codes in locales like `xx-yy` to `xx-YY` before passing them to `i18next`, which is strict about uppercase country codes.
54
+ * Keyboard shortcuts conflicts are detected and logged on to the terminal.
32
55
 
33
56
  ### Fixes
34
57
 
58
+ * Invalid locales passed to the i18n locale switching middleware are politely mapped to 400 errors.
59
+ * Any other exceptions thrown in the i18n locale switching middleware can no longer crash the process.
35
60
  * Documents kept as the `previous` version for undo purposes were not properly marked as such, breaking the public language switcher in some cases. This was fixed and a migration was added for existing data.
36
61
  * Uploading an image in an apostrophe area with `minSize` requirements will not trigger an unexpected error anymore. If the image is too small, a notification will be displayed with the minimum size requirements. The `Edit Image` modal will now display the minimum size requirements, if any, above the `Browse Images` field.
37
62
  * Some browsers saw the empty `POST` response for new notifications as invalid XML. It will now return an empty JSON object with the `Content-Type` set to `application/json`.
@@ -324,15 +324,20 @@ module.exports = {
324
324
  // to update a widget on the page after it is saved, or for
325
325
  // preview when editing.
326
326
  async renderWidget(req, type, data, options) {
327
- const manager = self.getWidgetManager(type);
328
- if (!manager) {
329
- // No manager available - possibly a stale widget in the database
330
- // of a type no longer in the project
331
- self.warnMissingWidgetType(type);
332
- return '';
327
+ try {
328
+ const manager = self.getWidgetManager(type);
329
+ if (!manager) {
330
+ // No manager available - possibly a stale widget in the database
331
+ // of a type no longer in the project
332
+ self.warnMissingWidgetType(type);
333
+ return '';
334
+ }
335
+ data.type = type;
336
+ return manager.output(req, data, options);
337
+ } catch (e) {
338
+ console.error(e);
339
+ throw e;
333
340
  }
334
- data.type = type;
335
- return manager.output(req, data, options);
336
341
  },
337
342
  // Update or create an area at the specified
338
343
  // dot path in the document with the specified
@@ -56,6 +56,8 @@ module.exports = {
56
56
  'format-list-numbered-icon': 'FormatListNumbered',
57
57
  'format-quote-close-icon': 'FormatQuoteClose',
58
58
  'format-strikethrough-variant-icon': 'FormatStrikethroughVariant',
59
+ 'format-superscript-icon': 'FormatSuperscript',
60
+ 'format-subscript-icon': 'FormatSubscript',
59
61
  'format-underline-icon': 'FormatUnderline',
60
62
  'help-circle-icon': 'HelpCircle',
61
63
  'image-edit-outline': 'ImageEditOutline',
@@ -112,6 +112,7 @@ module.exports = {
112
112
  self.sizeAvailableInArchive = self.options.sizeAvailableInArchive || 'one-sixth';
113
113
 
114
114
  self.rescaleTask = require('./lib/tasks/rescale.js')(self);
115
+ self.downloadAllTask = require('./lib/tasks/download-all.js')(self);
115
116
  self.addFieldType();
116
117
  self.enableBrowserData();
117
118
 
@@ -128,6 +129,10 @@ module.exports = {
128
129
  usage: 'Usage: node app @apostrophecms/attachment:rescale\n\nRegenerate all sizes of all image attachments. Useful after a new size\nis added to the configuration. Takes a long time!',
129
130
  task: self.rescaleTask
130
131
  },
132
+ 'download-all': {
133
+ usage: 'Usage: node app @apostrophecms/attachment:download-all --to=public/uploads/attachments [--resume] [--parallel=3]\n\nDownload all attachments to a local folder, usually to sync\nfrom a non-local uploadfs backend. Takes a long time!',
134
+ task: self.downloadAllTask
135
+ },
131
136
  'migrate-to-disabled-file-key': {
132
137
  usage: 'Usage: node app @apostrophecms/attachment:migrate-to-disabled-file-key\n\nThis task should be run after adding the disabledFileKey option to uploadfs\nfor the first time. It should only be relevant for storage backends where\nthat option is not mandatory, i.e. only local storage as of this writing.',
133
138
  task: self.migrateToDisabledFileKeyTask
@@ -0,0 +1,77 @@
1
+ // Direct use of console is appropriate in tasks. -Tom
2
+ /* eslint-disable no-console */
3
+
4
+ const fs = require('fs');
5
+ const sep = require('path').sep;
6
+
7
+ // Download everything Apostrophe believes to be in uploadfs.
8
+ // Useful when uploadfs is not using a storage backend we
9
+ // can conveniently sync from in some other way
10
+
11
+ module.exports = function(self) {
12
+ return async function(argv) {
13
+ const copyOut = require('util').promisify(self.uploadfs.copyOut);
14
+ if (!argv.to) {
15
+ throw 'You must specify a --to=directory argument';
16
+ }
17
+ console.log(`Downloading all attachments to ${argv.to} (this takes time)`);
18
+ const files = fs.readdirSync(argv.to);
19
+ if ((files.length > 0) && (!argv.resume)) {
20
+ throw `The directory ${argv.to} is not empty and --resume not specified, exiting`;
21
+ }
22
+ if (!argv.to.endsWith(sep)) {
23
+ argv.to += sep;
24
+ }
25
+ const total = await self.db.count();
26
+ let n = 0;
27
+ const parallel = argv.parallel ? parseInt(argv.parallel) : 1;
28
+ await self.each({}, parallel, async function(file) {
29
+ const isImage = [ 'jpg', 'png', 'gif', 'webp' ].includes(file.extension);
30
+ const originalFile = filename(file);
31
+ n++;
32
+ console.log(n + ' of ' + total + ': ' + originalFile);
33
+ const files = [
34
+ originalFile
35
+ ];
36
+ if (isImage) {
37
+ files.push(...self.imageSizes.map(size => {
38
+ return filename(file, size);
39
+ }));
40
+ for (const crop of (file.crops || [])) {
41
+ files.push(filename(file, false, crop));
42
+ files.push(...self.imageSizes.map(size => filename(file, size, crop)));
43
+ }
44
+ }
45
+ for (const file of files) {
46
+ const to = argv.to + file;
47
+ const tmp = to + '.tmp';
48
+ if (fs.existsSync(to)) {
49
+ console.log(`${to} already exists, skipping`);
50
+ } else {
51
+ try {
52
+ console.log(`about to copy out: ${file}`);
53
+ await copyOut(`/attachments/${file}`, tmp);
54
+ fs.renameSync(tmp, argv.to + file);
55
+ } catch (e) {
56
+ if (e.code === 'ENOSPC') {
57
+ throw e;
58
+ } else {
59
+ console.log(`${e.code}: ${file} was probably never uploaded to uploadfs, skipping`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ function filename(file, size, crop) {
65
+ let name = file._id + '-' + file.name;
66
+ if (crop) {
67
+ name += '.' + crop.left + '.' + crop.top + '.' + crop.width + '.' + crop.height;
68
+ }
69
+ if (size) {
70
+ name += '.' + size.name;
71
+ }
72
+ name += '.' + file.extension;
73
+ return name;
74
+ }
75
+ });
76
+ };
77
+ };
@@ -3,6 +3,7 @@
3
3
 
4
4
  const _ = require('lodash');
5
5
  const fs = require('fs');
6
+ const Promise = require('bluebird');
6
7
 
7
8
  // Regenerate all scaled images. Useful after changing the configured sizes
8
9
 
@@ -61,12 +62,12 @@ module.exports = function(self) {
61
62
  return;
62
63
  }
63
64
  }
64
- for (const crop of file.crops) {
65
+ for (const crop of file.crops || []) {
65
66
  console.log('RECROPPING');
66
67
  const originalFile = '/attachments/' + file._id + '-' + file.name + '.' + crop.left + '.' + crop.top + '.' + crop.width + '.' + crop.height + '.' + file.extension;
67
68
  console.log('Cropping ' + tempFile + ' to ' + originalFile);
68
69
  try {
69
- Promise.promisify(self.uploadfs.copyImageIn)(tempFile, originalFile, {
70
+ await Promise.promisify(self.uploadfs.copyImageIn)(tempFile, originalFile, {
70
71
  crop: crop,
71
72
  sizes: self.imageSizes
72
73
  });
@@ -363,12 +363,58 @@ module.exports = {
363
363
  }
364
364
 
365
365
  const { groups, modals } = self.getVisible(req);
366
+ self.notifyConflicts(req, modals);
366
367
 
367
368
  return {
368
369
  components: { the: self.options.components.the || 'TheAposCommandMenu' },
369
370
  groups,
370
371
  modals
371
372
  };
373
+ },
374
+ notifyConflicts(req, modals = self.modals) {
375
+ const shortcuts = {
376
+ modal: {},
377
+ list: {},
378
+ conflict: {}
379
+ };
380
+
381
+ Object.entries(modals)
382
+ .forEach(([ modal, groups ]) => Object.values(groups)
383
+ .forEach(group => Object.entries(group.commands)
384
+ .forEach(([ name, field ]) => {
385
+ self.detectShortcutConflict({
386
+ shortcuts,
387
+ shortcut: field.shortcut.toUpperCase(),
388
+ modal: modal === 'default' ? 'admin-bar' : modal,
389
+ moduleName: name
390
+ });
391
+ })
392
+ )
393
+ );
394
+
395
+ self.apos.util.warnDev(
396
+ req.t('apostrophe:shortcutConflictNotification'),
397
+ shortcuts.conflict
398
+ );
399
+ },
400
+ detectShortcutConflict({
401
+ shortcuts, shortcut, modal, moduleName
402
+ }) {
403
+ shortcuts.modal[modal] = shortcuts.modal[modal] || [];
404
+ shortcuts.list[modal] = shortcuts.list[modal] || {};
405
+ shortcuts.list[modal][shortcut] = shortcuts.list[modal][shortcut] || [];
406
+
407
+ const existingShortcut = shortcuts.modal[modal].includes(shortcut);
408
+
409
+ if (existingShortcut) {
410
+ shortcuts.conflict[modal] = shortcuts.conflict[modal] || {};
411
+ shortcuts.conflict[modal][shortcut] = shortcuts.list[modal][shortcut];
412
+ } else {
413
+ shortcuts.modal[modal].push(shortcut);
414
+ }
415
+ shortcuts.list[modal][shortcut].push(moduleName);
416
+
417
+ return shortcuts;
372
418
  }
373
419
  };
374
420
  }
@@ -1,6 +1,10 @@
1
1
  {
2
+ "addColumnBefore": "Add Column Before",
3
+ "addColumnAfter": "Add Column After",
2
4
  "addContent": "Add Content",
3
5
  "addItem": "Add Item",
6
+ "addRowBefore": "Add Row Before",
7
+ "addRowAfter": "Add Row After",
4
8
  "addWidgetType": "Add {{ label }}",
5
9
  "admin": "Admin",
6
10
  "affirmativeLabel": "Yes, continue.",
@@ -45,6 +49,7 @@
45
49
  "browseDocType": "Browse {{ type }}",
46
50
  "cancel": "Cancel",
47
51
  "cannotMoveArchive": "You cannot move the Archive",
52
+ "caption": "Caption",
48
53
  "changed": "Changed",
49
54
  "changesAwaitingApproval": "Changes to this document are awaiting approval by an admin or editor.",
50
55
  "changesDiscarded": "Changes discarded",
@@ -93,9 +98,12 @@
93
98
  "dayjsMediaCreatedDateFormat": "MMM Do, YYYY",
94
99
  "deduplicateSlugReserved": "The deduplicate- slug is reserved.",
95
100
  "delete": "Delete",
101
+ "deleteColumn": "Delete Column",
96
102
  "deleteDraft": "Delete Draft",
97
103
  "deleteDraftAffirmativeLabel": "Yes, delete document",
98
104
  "deleteDraftDescription": "Since {{ title }} has never been published, this will completely delete the document.",
105
+ "deleteRow": "Delete Row",
106
+ "deleteTable": "Delete Table",
99
107
  "description": "Description",
100
108
  "disabled": "Disabled",
101
109
  "discardChanges": "Discard Changes",
@@ -188,6 +196,7 @@
188
196
  "insertAndRedirect": "{{ saveLabel }} {{ typeLabel }} and be redirected to the {{ typeLabel }}.",
189
197
  "insertAndReturn": "{{ saveLabel }} and return to the {{ typeLabel }} listing.",
190
198
  "insertAndNew": "{{ saveLabel }} {{ typeLabel }} and create a new one.",
199
+ "insertTable": "Insert Table",
191
200
  "lastEdited": "Last Edited",
192
201
  "leavePageDescription": "The content you're trying to edit belongs to another document and must be edited there.\nChanges made to {{ oldTitle }} are saved automatically.",
193
202
  "leavePageHeading": "Leave {{ oldTitle }} to edit {{ newTitle }}?",
@@ -241,6 +250,7 @@
241
250
  "mediaMB": "{{ size }}MB",
242
251
  "mediaUploadViaDrop": "Drop ’em when you’re ready",
243
252
  "mediaUploadViaExplorer": "Or click to open the file explorer",
253
+ "mergeCells": "Merge Cells",
244
254
  "minLabel": "Min:",
245
255
  "minUi": "Min: {{ number }}",
246
256
  "modify": "Modify",
@@ -403,6 +413,14 @@
403
413
  "shareDraftEnable": "Enable draft sharing",
404
414
  "shareDraftHeader": "Share this page",
405
415
  "shareDraftError": "This document cannot be shared at this time",
416
+ "shortcutConflictNotification": "Shortcut conflicts detected:",
417
+ "subscript": "Subscript",
418
+ "superscript": "Superscript",
419
+ "splitCell": "Split Cell",
420
+ "style": "Style",
421
+ "toggleHeaderCell": "Toggle Header (Cell)",
422
+ "toggleHeaderColumn": "Toggle Header (Column)",
423
+ "toggleHeaderRow": "Toggle Header (Row)",
406
424
  "visibilityHelp": "Select whether this content is public or private",
407
425
  "slug": "Slug",
408
426
  "slugInUse": "Slug already in use",
@@ -414,6 +432,7 @@
414
432
  "submitUpdate": "Submit Update",
415
433
  "suggestionsHeader": "Try one of these suggestions:",
416
434
  "switchLocalesAndLocalizePage": "Switch locales and localize page to {{ label }}?",
435
+ "table": "Table",
417
436
  "tags": "Tags",
418
437
  "tagYourImages": "Tag your images to make searching and filtering the media manager easier",
419
438
  "takeActionAndCreateNew": "{{ saveLabel }} and Create New",
@@ -287,6 +287,9 @@ module.exports = {
287
287
  post: {
288
288
  async locale(req) {
289
289
  const sanitizedLocale = self.sanitizeLocaleName(req.body.locale);
290
+ if (!sanitizedLocale) {
291
+ throw self.apos.error('invalid', 'invalid locale');
292
+ }
290
293
  // Clipboards transferring between locales needs to jump
291
294
  // from LocalStorage to the cross-domain session cache
292
295
  let clipboard = req.body.clipboard;
@@ -626,21 +629,29 @@ module.exports = {
626
629
  // if possible, to the corresponding version in toLocale.
627
630
  toLocaleRouteFactory(module) {
628
631
  return async (req, res) => {
629
- const _id = module.inferIdLocaleAndMode(req, req.params._id);
630
- const toLocale = req.params.toLocale;
631
- const localeReq = req.clone({
632
- locale: toLocale
633
- });
634
- const corresponding = await module.find(localeReq, {
635
- _id: `${_id.split(':')[0]}:${localeReq.locale}:${localeReq.mode}`
636
- }).toObject();
637
- if (!corresponding) {
638
- return res.status(404).send('not found');
639
- }
640
- if (!corresponding._url) {
641
- return res.status(400).send('invalid (has no URL)');
632
+ try {
633
+ const _id = module.inferIdLocaleAndMode(req, req.params._id);
634
+ const toLocale = self.sanitizeLocaleName(req.params.toLocale);
635
+ if (!toLocale) {
636
+ return res.status(400).send('invalid locale name');
637
+ }
638
+ const localeReq = req.clone({
639
+ locale: toLocale
640
+ });
641
+ const corresponding = await module.find(localeReq, {
642
+ _id: `${_id.split(':')[0]}:${localeReq.locale}:${localeReq.mode}`
643
+ }).toObject();
644
+ if (!corresponding) {
645
+ return res.status(404).send('not found');
646
+ }
647
+ if (!corresponding._url) {
648
+ return res.status(400).send('invalid (has no URL)');
649
+ }
650
+ return res.redirect(corresponding._url);
651
+ } catch (e) {
652
+ self.apos.util.error(e);
653
+ return res.status(500).send('error');
642
654
  }
643
- return res.redirect(corresponding._url);
644
655
  };
645
656
  },
646
657
  // Exclude private locales when logged out
@@ -290,6 +290,34 @@ module.exports = {
290
290
  }
291
291
  }
292
292
  }),
293
+ routes(self, options) {
294
+ return {
295
+ get: {
296
+ // Convenience route to get the URL of the image
297
+ // knowing only the image id. Useful in the rich text editor.
298
+ // Not performant for frontend use
299
+ ':imageId/src': async (req, res) => {
300
+ const size = req.query.size || self.getLargestSize();
301
+ try {
302
+ const image = await self.find(req, {
303
+ aposDocId: req.params.imageId
304
+ }).toObject();
305
+ if (!image) {
306
+ return res.status(404).send('notfound');
307
+ }
308
+ const url = image.attachment && image.attachment._urls && image.attachment._urls[size];
309
+ if (url) {
310
+ return res.redirect(image.attachment._urls[size]);
311
+ }
312
+ return res.status(404).send('notfound');
313
+ } catch (e) {
314
+ self.apos.util.error(e);
315
+ return res.status(500).send('error');
316
+ }
317
+ }
318
+ }
319
+ };
320
+ },
293
321
  methods(self) {
294
322
  return {
295
323
  // This method is available as a template helper: apos.image.first
@@ -417,6 +445,14 @@ module.exports = {
417
445
  self.getComponentName('managerModal', 'AposMediaManager'),
418
446
  { moduleName: self.__meta.name }
419
447
  );
448
+ },
449
+ getLargestSize() {
450
+ return self.apos.attachment.imageSizes.reduce((a, size) => {
451
+ return size.width > a.width ? size : a;
452
+ }, {
453
+ name: 'dummy',
454
+ width: 0
455
+ }).name;
420
456
  }
421
457
  };
422
458
  },
@@ -5,24 +5,18 @@
5
5
  class="image-widget-placeholder"
6
6
  />
7
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
- {% 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 %}
8
+ {% set className = data.options.className or data.manager.options.className %}
9
+ {% set dimensionAttrs = data.options.dimensionAttrs or data.manager.options.dimensionAttrs %}
10
+ {% set loadingType = data.options.loadingType or data.manager.options.loadingType %}
11
+ {% set size = data.options.size or data.manager.options.size or 'full' %}
19
12
 
20
13
  {% set attachment = apos.image.first(data.widget._image) %}
21
14
 
22
15
  {% if attachment %}
23
16
  <img {% if className %} class="{{ className }}"{% endif %}
17
+ {% if loadingType %} loading="{{ loadingType }}"{% endif %}
24
18
  srcset="{{ apos.image.srcset(attachment) }}"
25
- src="{{ apos.attachment.url(attachment, { size: data.options.size or 'full' }) }}"
19
+ src="{{ apos.attachment.url(attachment, { size: size }) }}"
26
20
  alt="{{ attachment._alt or '' }}"
27
21
  {% if dimensionAttrs %}
28
22
  {% if attachment.width %} width="{{ apos.attachment.getWidth(attachment) }}" {% endif %}