apostrophe 3.4.0 → 3.7.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 (87) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +1 -1
  3. package/deploy-test-count +1 -1
  4. package/index.js +125 -5
  5. package/lib/moog-require.js +41 -3
  6. package/lib/moog.js +20 -8
  7. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +42 -23
  8. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +25 -13
  9. package/modules/@apostrophecms/area/index.js +9 -0
  10. package/modules/@apostrophecms/area/lib/custom-tags/area.js +1 -1
  11. package/modules/@apostrophecms/area/lib/custom-tags/widget.js +1 -1
  12. package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +3 -0
  13. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +6 -6
  14. package/modules/@apostrophecms/asset/index.js +8 -8
  15. package/modules/@apostrophecms/asset/lib/globalIcons.js +2 -0
  16. package/modules/@apostrophecms/asset/lib/webpack/src/webpack.scss.js +5 -2
  17. package/modules/@apostrophecms/doc/index.js +13 -3
  18. package/modules/@apostrophecms/doc-type/index.js +1 -1
  19. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +3 -0
  20. package/modules/@apostrophecms/i18n/i18n/en.json +11 -2
  21. package/modules/@apostrophecms/i18n/i18n/es.json +383 -0
  22. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +380 -0
  23. package/modules/@apostrophecms/i18n/i18n/sk.json +381 -0
  24. package/modules/@apostrophecms/i18n/index.js +10 -1
  25. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +153 -121
  26. package/modules/@apostrophecms/image/index.js +2 -1
  27. package/modules/@apostrophecms/image/ui/apos/components/AposMediaManagerEditor.vue +24 -13
  28. package/modules/@apostrophecms/login/index.js +36 -17
  29. package/modules/@apostrophecms/login/ui/apos/components/TheAposLogin.vue +8 -0
  30. package/modules/@apostrophecms/migration/index.js +1 -1
  31. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +6 -2
  32. package/modules/@apostrophecms/modal/ui/apos/components/AposModalConfirm.vue +1 -1
  33. package/modules/@apostrophecms/modal/ui/apos/mixins/AposEditorMixin.js +6 -0
  34. package/modules/@apostrophecms/module/index.js +1 -1
  35. package/modules/@apostrophecms/permission/index.js +1 -1
  36. package/modules/@apostrophecms/permission/ui/apos/components/AposInputRole.vue +4 -2
  37. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +4 -1
  38. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +1 -1
  39. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +42 -10
  40. package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapStyles.vue +3 -0
  41. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Classes.js +6 -10
  42. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Default.js +64 -0
  43. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Document.js +15 -0
  44. package/modules/@apostrophecms/rich-text-widget/ui/apos/tiptap-extensions/Heading.js +23 -0
  45. package/modules/@apostrophecms/schema/index.js +97 -20
  46. package/modules/@apostrophecms/schema/ui/apos/components/AposArrayEditor.vue +1 -0
  47. package/modules/@apostrophecms/schema/ui/apos/components/AposInputArray.vue +4 -1
  48. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRadio.vue +8 -5
  49. package/modules/@apostrophecms/schema/ui/apos/components/AposInputRelationship.vue +24 -2
  50. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSelect.vue +24 -6
  51. package/modules/@apostrophecms/schema/ui/apos/components/AposInputSlug.vue +0 -4
  52. package/modules/@apostrophecms/schema/ui/apos/components/AposInputString.vue +0 -7
  53. package/modules/@apostrophecms/schema/ui/apos/components/AposSchema.vue +25 -3
  54. package/modules/@apostrophecms/schema/ui/apos/mixins/AposInputMixin.js +10 -2
  55. package/modules/@apostrophecms/template/index.js +61 -36
  56. package/modules/@apostrophecms/template/lib/custom-tags/component.js +1 -1
  57. package/modules/@apostrophecms/template/lib/custom-tags/render.js +6 -2
  58. package/modules/@apostrophecms/ui/index.js +6 -2
  59. package/modules/@apostrophecms/ui/ui/apos/components/AposButton.vue +16 -3
  60. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +1 -1
  61. package/modules/@apostrophecms/ui/ui/apos/components/AposIndicator.vue +5 -0
  62. package/modules/@apostrophecms/ui/ui/apos/lib/i18next.js +16 -2
  63. package/modules/@apostrophecms/ui/ui/apos/scss/global/_tables.scss +4 -3
  64. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +3 -0
  65. package/modules/@apostrophecms/ui/ui/apos/scss/global/_widgets.scss +3 -0
  66. package/modules/@apostrophecms/ui/ui/apos/scss/global/import-all.scss +2 -1
  67. package/modules/@apostrophecms/user/index.js +21 -0
  68. package/modules/@apostrophecms/util/index.js +2 -2
  69. package/modules/@apostrophecms/util/ui/src/http.js +12 -8
  70. package/modules/@apostrophecms/widget-type/index.js +1 -1
  71. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +1 -0
  72. package/package.json +3 -3
  73. package/test/extra_node_modules/improve-global/index.js +7 -0
  74. package/test/extra_node_modules/improve-piece-type/index.js +7 -0
  75. package/test/improve-overrides.js +30 -0
  76. package/test/login.js +183 -0
  77. package/test/modules/@apostrophecms/global/index.js +8 -0
  78. package/test/modules/fragment-all/views/aux-test.html +7 -0
  79. package/test/modules/fragment-all/views/fragment.html +5 -0
  80. package/test/moog.js +47 -0
  81. package/test/package.json +5 -4
  82. package/test/reverse-relationship.js +170 -0
  83. package/test/subdir-project/app.js +3 -0
  84. package/test/subdir-project.js +26 -0
  85. package/test/templates.js +7 -1
  86. package/test-lib/test.js +23 -12
  87. package/test-lib/util.js +1 -0
@@ -266,6 +266,9 @@ module.exports = {
266
266
  enableDeserializeUsers() {
267
267
  self.passport.deserializeUser(function (id, cb) {
268
268
  self.deserializeUser(id).then(function (user) {
269
+ if (user) {
270
+ user._viaSession = true;
271
+ }
269
272
  return cb(null, user);
270
273
  }).catch(cb);
271
274
  });
@@ -283,7 +286,12 @@ module.exports = {
283
286
 
284
287
  async deserializeUser(id) {
285
288
  const req = self.apos.task.getReq();
286
- const user = await self.apos.user.find(req, { _id: id }).toObject();
289
+ const user = await self.apos.user.find(req, {
290
+ _id: id,
291
+ disabled: {
292
+ $ne: true
293
+ }
294
+ }).toObject();
287
295
  if (!user) {
288
296
  return null;
289
297
  }
@@ -357,21 +365,6 @@ module.exports = {
357
365
  return 1000 * 60 * 60 * (self.options.passwordResetHours || 48);
358
366
  },
359
367
 
360
- // Invoked by passport after an authentication strategy succeeds
361
- // and the user has been logged in. Invokes `loginAfterLogin` on
362
- // any modules that have one and redirects to `req.redirect` or,
363
- // if it is not set, to `/`.
364
-
365
- async afterLogin(req, res) {
366
- try {
367
- await self.emit('after', req);
368
- } catch (e) {
369
- self.apos.util.error(e);
370
- return res.redirect('/');
371
- }
372
- return res.redirect(req.redirect || '/');
373
- },
374
-
375
368
  getBrowserData(req) {
376
369
  return {
377
370
  action: self.action,
@@ -390,7 +383,7 @@ module.exports = {
390
383
  const adminReq = self.apos.task.getReq();
391
384
  const user = await self.apos.user.find(adminReq, {}).relationships(false).limit(1).toObject();
392
385
 
393
- if (!user) {
386
+ if (!user && !self.apos.options.test) {
394
387
  self.apos.util.warnDev('There are no users created for this installation of ApostropheCMS yet.');
395
388
  }
396
389
  },
@@ -408,10 +401,36 @@ module.exports = {
408
401
  before: '@apostrophecms/i18n',
409
402
  middleware: self.passport.initialize()
410
403
  },
404
+ passportExtendLogin: {
405
+ before: '@apostrophecms/i18n',
406
+ middleware(req, res, next) {
407
+ const superLogin = req.login.bind(req);
408
+ req.login = (user, callback) => {
409
+ return superLogin(user, (err) => {
410
+ if (err) {
411
+ return callback(err);
412
+ }
413
+ req.session.loginAt = Date.now();
414
+ return callback(null);
415
+ });
416
+ };
417
+ return next();
418
+ }
419
+ },
411
420
  passportSession: {
412
421
  before: '@apostrophecms/i18n',
413
422
  middleware: self.passport.session()
414
423
  },
424
+ honorLoginInvalidBefore: {
425
+ before: '@apostrophecms/i18n',
426
+ middleware(req, res, next) {
427
+ if (req.user && req.user._viaSession && req.user.loginInvalidBefore && ((!req.session.loginAt) || (req.session.loginAt < req.user.loginInvalidBefore))) {
428
+ req.session.destroy();
429
+ delete req.user;
430
+ }
431
+ return next();
432
+ }
433
+ },
415
434
  addUserToData: {
416
435
  before: '@apostrophecms/i18n',
417
436
  middleware(req, res, next) {
@@ -37,6 +37,7 @@
37
37
  type="primary"
38
38
  label="apostrophe:login"
39
39
  button-type="submit"
40
+ class="apos-login__submit"
40
41
  :modifiers="['gradient-on-hover', 'block']"
41
42
  @click="submit"
42
43
  />
@@ -121,6 +122,9 @@ export default {
121
122
  },
122
123
  methods: {
123
124
  async submit() {
125
+ if (this.busy) {
126
+ return;
127
+ }
124
128
  this.busy = true;
125
129
  this.error = '';
126
130
  try {
@@ -298,4 +302,8 @@ export default {
298
302
  margin-left: auto;
299
303
  }
300
304
  }
305
+
306
+ .apos-login__submit ::v-deep .apos-button {
307
+ height: 47px;
308
+ }
301
309
  </style>
@@ -260,7 +260,7 @@ module.exports = {
260
260
  // Intentionally emitted regardless of whether the site is new or not.
261
261
  //
262
262
  // This is the right time to park pages, for instance, because the
263
- // database is guaranteed to be in a sane state, whether because the
263
+ // database is guaranteed to be in a stable state, whether because the
264
264
  // site is new or because migrations ran successfully.
265
265
  await self.emit('after');
266
266
  } finally {
@@ -43,7 +43,11 @@
43
43
  </h2>
44
44
  <div class="apos-modal__controls--header" v-if="hasBeenLocalized || hasPrimaryControls">
45
45
  <div class="apos-modal__locale" v-if="hasBeenLocalized">
46
- <span class="apos-modal__locale-label">{{ $t('apostrophe:locale')}}:</span> <span class="apos-modal__locale-name">{{ currentLocale }}</span>
46
+ <span class="apos-modal__locale-label">
47
+ {{ $t('apostrophe:locale') }}:
48
+ </span> <span class="apos-modal__locale-name">
49
+ {{ currentLocale }}
50
+ </span>
47
51
  </div>
48
52
  <div class="apos-modal__controls--primary" v-if="hasPrimaryControls">
49
53
  <slot name="primaryControls" />
@@ -447,9 +451,9 @@ export default {
447
451
  $height: 190px;
448
452
  top: 50%;
449
453
  bottom: -50%;
454
+ display: flex;
450
455
  height: $height;
451
456
  transform: translateY(math.div($height, 2) * -1);
452
- display: flex;
453
457
  justify-content: center;
454
458
  align-items: center;
455
459
  text-align: center;
@@ -171,7 +171,7 @@ export default {
171
171
  right: auto;
172
172
  bottom: auto;
173
173
  left: auto;
174
- width: 550px;
174
+ max-width: 700px;
175
175
  height: auto;
176
176
  text-align: center;
177
177
  }
@@ -188,6 +188,12 @@ export default {
188
188
  dismiss: true
189
189
  });
190
190
  }
191
+ },
192
+ triggerValidate() {
193
+ this.triggerValidation = true;
194
+ this.$nextTick(async () => {
195
+ this.triggerValidation = false;
196
+ });
191
197
  }
192
198
 
193
199
  }
@@ -446,7 +446,7 @@ module.exports = {
446
446
  // By default browser data is pushed only for the `apos` scene, so public
447
447
  // site pages will not be cluttered with it, except on the /login page and
448
448
  // other pages that opt into the `apos` scene. If `scene` is set to `public`
449
- // then the data is available al the time.
449
+ // then the data is available all the time.
450
450
  //
451
451
  // Be sure to use `extendMethods` when implementing `getBrowserData`
452
452
  // as your base class may also implement `getBrowserData`.
@@ -234,7 +234,7 @@ module.exports = {
234
234
  }
235
235
  permissions.push({
236
236
  name: 'edit',
237
- label: module.options.singleton ? 'Modify' : 'Modify / Delete',
237
+ label: module.options.singleton ? 'apostrophe:modify' : 'apostrophe:modifyOrDelete',
238
238
  value: self.can(req, 'edit', module.name)
239
239
  });
240
240
  permissions.push({
@@ -126,7 +126,6 @@ export default {
126
126
  const list = document.createElement('ul');
127
127
  const intro = document.createElement('p');
128
128
  const followUp = document.createElement('p');
129
- const link = document.createElement('a');
130
129
  intro.appendChild(document.createTextNode(this.$t('apostrophe:piecePermissionsIntro')));
131
130
  followUp.appendChild(document.createTextNode(this.$t('apostrophe:piecePermissionsPieceTypeList')));
132
131
  html.appendChild(intro);
@@ -137,7 +136,10 @@ export default {
137
136
  list.appendChild(li);
138
137
  });
139
138
  html.appendChild(list);
140
- return { content: html, localize: false };
139
+ return {
140
+ content: html,
141
+ localize: false
142
+ };
141
143
  },
142
144
  validate(value) {
143
145
  if (this.field.required && !value.length) {
@@ -211,7 +211,10 @@ export default {
211
211
  // Add computed singular label to context menu
212
212
  this.moreMenu.menu.unshift({
213
213
  action: 'new',
214
- label: `New ${this.moduleLabels.singular}`
214
+ label: {
215
+ key: 'apostrophe:newDocType',
216
+ type: this.$t(this.moduleLabels.singular)
217
+ }
215
218
  });
216
219
  }
217
220
  apos.bus.$on('content-changed', this.getPieces);
@@ -26,7 +26,7 @@
26
26
  <th class="apos-table__header" key="contextMenu">
27
27
  <component
28
28
  :is="getEl({})"
29
- class="apos-table__header-label is-hidden"
29
+ class="apos-table__header-label apos-is-hidden"
30
30
  >
31
31
  {{ $t('apostrophe:moreOperations') }}
32
32
  </component>
@@ -26,9 +26,8 @@
26
26
  </AposContextMenuDialog>
27
27
  </bubble-menu>
28
28
  <div class="apos-rich-text-editor__editor" :class="editorModifiers">
29
- <editor-content :editor="editor" :class="moduleOptions.className" />
29
+ <editor-content :editor="editor" :class="editorOptions.className" />
30
30
  </div>
31
- <!-- Using actual DOM element rather than :after to ease localization -->
32
31
  <div class="apos-rich-text-editor__editor_after" :class="editorModifiers">
33
32
  {{ $t('apostrophe:emptyRichTextWidget') }}
34
33
  </div>
@@ -103,11 +102,27 @@ export default {
103
102
 
104
103
  activeOptions.styles = this.enhanceStyles(activeOptions.styles || this.defaultOptions.styles);
105
104
 
105
+ activeOptions.className = (activeOptions.className !== undefined)
106
+ ? activeOptions.className : this.moduleOptions.className;
107
+
106
108
  return activeOptions;
107
109
  },
108
-
110
+ autofocus() {
111
+ // Only true for a new rich text widget
112
+ return !this.stripPlaceholderBrs(this.value.content).length;
113
+ },
109
114
  initialContent() {
110
- return this.stripPlaceholderBrs(this.value.content);
115
+ const content = this.stripPlaceholderBrs(this.value.content);
116
+ if (!content.length) {
117
+ // If we don't supply a valid instance of the first style, then
118
+ // the text align control will not work until the user manually
119
+ // applies a style or refreshes the page
120
+ const defaultStyle = this.editorOptions.styles.find(style => style.def);
121
+ const _class = defaultStyle.class ? ` class="${defaultStyle.class}"` : '';
122
+ return `<${defaultStyle.tag}${_class}></${defaultStyle.tag}>`;
123
+ } else {
124
+ return content;
125
+ }
111
126
  },
112
127
  toolbar() {
113
128
  return this.editorOptions.toolbar;
@@ -136,7 +151,7 @@ export default {
136
151
  aposTiptapExtensions() {
137
152
  return (apos.tiptapExtensions || [])
138
153
  .map(extension => extension({
139
- styles: this.editorOptions.styles,
154
+ styles: this.editorOptions.styles.map(this.localizeStyle),
140
155
  types: this.tiptapTypes
141
156
  }));
142
157
  }
@@ -153,7 +168,7 @@ export default {
153
168
  mounted() {
154
169
  this.editor = new Editor({
155
170
  content: this.initialContent,
156
- autofocus: !this.initialContent,
171
+ autofocus: this.autofocus,
157
172
  onUpdate: this.editorUpdate,
158
173
  extensions: [
159
174
  StarterKit,
@@ -217,7 +232,6 @@ export default {
217
232
  // commands and parameters used internally.
218
233
  enhanceStyles(styles) {
219
234
  const self = this;
220
- const enhanced = [];
221
235
  (styles || []).forEach(style => {
222
236
  style.options = {};
223
237
  for (const key in self.tiptapTextCommands) {
@@ -242,9 +256,7 @@ export default {
242
256
  style.options.class = style.class;
243
257
  }
244
258
 
245
- if (style.type) {
246
- enhanced.push(style);
247
- } else {
259
+ if (!style.type) {
248
260
  apos.notify('apostrophe:richTextStyleConfigWarning', {
249
261
  type: 'warning',
250
262
  dismiss: true,
@@ -256,7 +268,27 @@ export default {
256
268
  });
257
269
  }
258
270
  });
271
+
272
+ // ensure a default so we can rely on it throughout
273
+ const hasDefault = !!styles.find(style => style.def);
274
+ if (!hasDefault && styles.length) {
275
+ // If no dev set default, use the first paragraph we can find
276
+ if (styles.filter(style => style.type === 'paragraph').length) {
277
+ styles.filter(style => style.type === 'paragraph')[0].def = true;
278
+ } else {
279
+ // Otherwise, set the first style
280
+ styles[0].def = true;
281
+ }
282
+ }
259
283
  return styles;
284
+ },
285
+ localizeStyle(style) {
286
+ style.label = this.$t(style.label);
287
+
288
+ return {
289
+ ...style,
290
+ label: this.$t(style.label)
291
+ };
260
292
  }
261
293
  }
262
294
  };
@@ -52,6 +52,9 @@ export default {
52
52
  const style = styles[i];
53
53
  if (this.editor.isActive(style.type, (style.options || {}))) {
54
54
  return i;
55
+ } else if (this.editor.state.selection.$head.parent.type.name === 'defaultNode' && style.def) {
56
+ // Look deeper to see if custom defaultNode is active
57
+ return i;
55
58
  }
56
59
  }
57
60
  return 0;
@@ -25,9 +25,7 @@ export default (options) => {
25
25
  const tag = element.tagName.toLowerCase();
26
26
  // This tag is not configured
27
27
  if (!allow[tag]) {
28
- return {
29
- class: null
30
- };
28
+ return null;
31
29
  }
32
30
  const classes = (element.getAttribute('class') || '')
33
31
  .split(' ')
@@ -36,13 +34,11 @@ export default (options) => {
36
34
  // If no valid classes for this parse, default to the
37
35
  // the first setting for this tag (including null for tags defined without classes).
38
36
  // else, remove classes.
39
- return {
40
- class: classes.length
41
- ? classes.join(' ')
42
- : (
43
- allow[tag].length ? allow[tag][0] : null
44
- )
45
- };
37
+ return classes.length
38
+ ? classes.join(' ')
39
+ : (
40
+ allow[tag].length ? allow[tag][0] : null
41
+ );
46
42
  }
47
43
  }
48
44
  }
@@ -0,0 +1,64 @@
1
+ // If a style has been marked `def`, we need to make sure it
2
+ // and it's attributes are prioritized in our editor.
3
+ // It's easier to create a new Node with our specifications and put it
4
+ // at the front of the line than try to infer between editor instantiation
5
+ // and the editor-user setting styles via the toolbar to know when its needed.
6
+
7
+ import { Node } from '@tiptap/core';
8
+ import Heading from '@tiptap/extension-heading';
9
+ import Paragraph from '@tiptap/extension-paragraph';
10
+
11
+ const nodeMap = {
12
+ heading: Heading,
13
+ paragraph: Paragraph
14
+ };
15
+
16
+ export default (options) => {
17
+ const def = options.styles.filter(style => style.def)[0];
18
+
19
+ // Configuration has a default style
20
+ if (def) {
21
+ const nodeName = 'defaultNode';
22
+ const attrs = {
23
+ class: def.options.class || null
24
+ };
25
+
26
+ if (def.type === 'heading' || def.type === 'paragraph') {
27
+ return nodeMap[def.type].extend({
28
+ name: nodeName,
29
+ defaultOptions: {
30
+ HTMLAttributes: attrs,
31
+ levels: def.options.level ? [ def.options.level ] : null
32
+ }
33
+ });
34
+ }
35
+
36
+ if (def.type === 'textStyle') {
37
+ return Node.create({
38
+ group: 'block',
39
+ content: 'text*',
40
+ name: nodeName,
41
+ defaultOptions: {
42
+ HTMLAttributes: attrs
43
+ },
44
+ renderHTML: () => {
45
+ return [ 'span', attrs, 0 ];
46
+ },
47
+ parseHTML() {
48
+ return [
49
+ {
50
+ tag: 'span',
51
+ getAttrs: element => {
52
+ const hasStyles = element.hasAttribute('style');
53
+ if (!hasStyles) {
54
+ return false;
55
+ }
56
+ return {};
57
+ }
58
+ }
59
+ ];
60
+ }
61
+ });
62
+ }
63
+ }
64
+ };
@@ -0,0 +1,15 @@
1
+ // Acts as a custom Document extension
2
+ import { Node } from '@tiptap/core';
3
+ export default (options) => {
4
+ const def = options.styles.filter(style => style.def)[0];
5
+ let content = 'block+'; // one or more block nodes (default Document setting)
6
+ if (def) {
7
+ // one/more defaultNodes (created in ./Default) or one/more other block nodes
8
+ content = '(defaultNode|block)+';
9
+ }
10
+ return Node.create({
11
+ name: 'doc',
12
+ topNode: true,
13
+ content
14
+ });
15
+ };
@@ -0,0 +1,23 @@
1
+ // Lock Heading levels down to just those provided via configuration
2
+ import Heading from '@tiptap/extension-heading';
3
+
4
+ export default (options) => {
5
+ const headings = options.styles.filter(style => style.type === 'heading');
6
+ const levels = headings.map(heading => heading.options.level);
7
+ const defaultLevel = headings.filter(heading => heading.def).length
8
+ ? headings.filter(heading => heading.def)[0].options.level
9
+ : levels[0];
10
+ return Heading.extend({
11
+ defaultOptions: {
12
+ levels
13
+ },
14
+ addAttributes() {
15
+ return {
16
+ level: {
17
+ default: defaultLevel,
18
+ rendered: false
19
+ }
20
+ };
21
+ }
22
+ });
23
+ };