@windward/core 0.3.0 → 0.4.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 (43) hide show
  1. package/components/Content/Blocks/Accordion.vue +37 -0
  2. package/components/Content/Blocks/ClickableIcons.vue +107 -20
  3. package/components/Content/Blocks/Email.vue +9 -0
  4. package/components/Content/Blocks/Image.vue +47 -19
  5. package/components/Content/Blocks/Video.vue +80 -9
  6. package/components/Settings/AccordionSettings.vue +53 -0
  7. package/components/Settings/ClickableIconsSettings.vue +84 -7
  8. package/components/Settings/EmailSettings.vue +0 -9
  9. package/components/Settings/ImageSettings.vue +142 -39
  10. package/components/Settings/OpenResponseSettings.vue +1 -2
  11. package/components/Settings/VideoSettings.vue +100 -63
  12. package/components/utils/ContentViewer.vue +6 -1
  13. package/components/utils/MathExpressionEditor.vue +10 -5
  14. package/components/utils/TinyMCEWrapper.vue +114 -18
  15. package/components/utils/assets/tinymce/content/dark/content.scss +4 -0
  16. package/components/utils/assets/tinymce/{css/content.scss → content/global.scss} +38 -37
  17. package/components/utils/assets/tinymce/content/light/content.scss +4 -0
  18. package/components/utils/assets/tinymce/ui/dark/content.scss +803 -0
  19. package/components/utils/assets/tinymce/ui/dark/skin.scss +4727 -0
  20. package/components/utils/assets/tinymce/ui/global.scss +19 -0
  21. package/components/utils/assets/tinymce/ui/light/content.scss +822 -0
  22. package/components/utils/assets/tinymce/ui/light/skin.scss +4731 -0
  23. package/components/utils/glossary/CourseGlossary.vue +1 -1
  24. package/config/tinymce.config.ts +22 -14
  25. package/helpers/FillInBlankHelper.ts +34 -28
  26. package/helpers/GlossaryHelper.ts +90 -73
  27. package/helpers/MathHelper.ts +49 -28
  28. package/helpers/tinymce/plugin.ts +9 -7
  29. package/i18n/en-US/components/settings/clickable_icon.ts +2 -0
  30. package/i18n/en-US/components/settings/image.ts +6 -1
  31. package/i18n/en-US/shared/settings.ts +3 -0
  32. package/i18n/es-ES/components/settings/clickable_icon.ts +2 -0
  33. package/i18n/es-ES/components/settings/image.ts +8 -1
  34. package/i18n/es-ES/shared/settings.ts +3 -0
  35. package/i18n/sv-SE/components/settings/clickable_icon.ts +2 -0
  36. package/i18n/sv-SE/components/settings/image.ts +6 -1
  37. package/i18n/sv-SE/shared/settings.ts +3 -0
  38. package/package.json +4 -3
  39. package/test/Components/Settings/AccordionSettings.spec.js +16 -2
  40. package/test/__mocks__/contentBlockMock.js +6 -0
  41. package/test/__mocks__/contentSettingsMock.js +6 -0
  42. package/test/helpers/MathHelper.spec.js +22 -3
  43. package/tsconfig.json +1 -0
@@ -38,6 +38,27 @@
38
38
  "
39
39
  ></TextEditor>
40
40
  </v-container>
41
+ <v-container>
42
+ <v-img
43
+ v-if="block.metadata.config.items[itemIndex].file"
44
+ :aria-describedby="
45
+ block.metadata.config.items[itemIndex]
46
+ .ariaDescribedBy
47
+ "
48
+ :alt="
49
+ getImageAlt(
50
+ block.metadata.config.items[itemIndex].file,
51
+ block.metadata.config.items[itemIndex]
52
+ .altText
53
+ )
54
+ "
55
+ :src="
56
+ getImagePublicUrl(
57
+ block.metadata.config.items[itemIndex].file
58
+ )
59
+ "
60
+ ></v-img>
61
+ </v-container>
41
62
  </v-expansion-panel-content>
42
63
  </v-expansion-panel>
43
64
  </v-expansion-panels>
@@ -73,6 +94,9 @@ export default {
73
94
  header: '',
74
95
  expand: false,
75
96
  content: '',
97
+ file: null,
98
+ altText: '',
99
+ ariaDescribedBy: '',
76
100
  }
77
101
  this.block.metadata.config.items = []
78
102
  this.block.metadata.config.items.push(defaultObject)
@@ -129,6 +153,19 @@ export default {
129
153
  },
130
154
  },
131
155
  methods: {
156
+ getImagePublicUrl(asset) {
157
+ const foundAsset = this.resolveAsset(asset)
158
+
159
+ return _.get(foundAsset, 'asset.public_url', null)
160
+ },
161
+ getImageAlt(asset, defaultText = '') {
162
+ // If a default / override was defined
163
+ if (defaultText) {
164
+ return defaultText
165
+ }
166
+ const foundAsset = this.resolveAsset(asset)
167
+ return _.get(foundAsset, 'asset.metadata.props.alt', null)
168
+ },
132
169
  async onBeforeSave() {
133
170
  this.block.metadata.config.items.forEach((element) => {
134
171
  element.expand = false
@@ -20,23 +20,34 @@
20
20
  no-gutters
21
21
  :class="rowClass(itemIndex)"
22
22
  >
23
- <v-col cols="12" xl="2" lg="3" md="3" sm="3">
24
- <v-btn
25
- class="button-icon pa-1 mb-4"
26
- :color="itemColor(itemIndex)"
27
- :fab="block.metadata.config.display.round_icon"
28
- outlined
23
+ <v-col cols="12" v-bind="iconColumnAttrs" class="pa-4">
24
+ <button
25
+ :class="activatorButtonClass(itemIndex)"
29
26
  @click="item.active = !item.active"
30
27
  >
31
- <v-icon v-if="isIcon(item.icon)" class="black--text">{{
32
- item.icon
33
- }}</v-icon>
28
+ <v-avatar
29
+ v-if="item.iconImage && item.icon"
30
+ class="clickable--image"
31
+ :rounded="
32
+ block.metadata.config.display.round_icon
33
+ ? '100%'
34
+ : '0'
35
+ "
36
+ size="100%"
37
+ >
38
+ <v-img :src="getImagePublicUrl(item.iconAsset)" />
39
+ </v-avatar>
40
+ <v-icon
41
+ v-else-if="isIcon(item.icon)"
42
+ class="clickable--icon black--text"
43
+ >{{ item.icon }}</v-icon
44
+ >
34
45
  <span v-else :class="iconClass + ' black--text'">{{
35
46
  decode(item.icon)
36
47
  }}</span>
37
- </v-btn>
48
+ </button>
38
49
  </v-col>
39
- <v-col cols="12" xl="10" lg="9" md="9" sm="9">
50
+ <v-col cols="12" v-bind="bodyColumnAttrs">
40
51
  <h4
41
52
  v-if="
42
53
  block.metadata.config.display.show_title ||
@@ -62,6 +73,7 @@
62
73
  <script>
63
74
  import _ from 'lodash'
64
75
  import he from 'he'
76
+ import Uuid from '~/helpers/Uuid'
65
77
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
66
78
  import TextViewer from '~/components/Text/TextViewer'
67
79
 
@@ -111,7 +123,7 @@ export default {
111
123
  },
112
124
  rowClass() {
113
125
  return (itemIndex) => {
114
- let classes = 'option-container mb-4 pa-1'
126
+ let classes = 'option-container pa-1'
115
127
  // If show background is enabled and the item has a color
116
128
  // Otherwise we do NOT want to apply the `black--text` class since there's no color
117
129
  // And things will look bad in dark mode
@@ -128,8 +140,42 @@ export default {
128
140
  return classes
129
141
  }
130
142
  },
143
+ iconColumnAttrs() {
144
+ if (this.block.metadata.config.display.large_icon) {
145
+ return {
146
+ xl: '3',
147
+ lg: '4',
148
+ md: '4',
149
+ sm: '4',
150
+ }
151
+ } else {
152
+ return {
153
+ xl: '2',
154
+ lg: '3',
155
+ md: '3',
156
+ sm: '3',
157
+ }
158
+ }
159
+ },
160
+ bodyColumnAttrs() {
161
+ if (this.block.metadata.config.display.large_icon) {
162
+ return {
163
+ xl: '9',
164
+ lg: '8',
165
+ md: '8',
166
+ sm: '8',
167
+ }
168
+ } else {
169
+ return {
170
+ xl: '10',
171
+ lg: '9',
172
+ md: '9',
173
+ sm: '9',
174
+ }
175
+ }
176
+ },
131
177
  iconClass() {
132
- let classes = 'text-icon'
178
+ let classes = 'clickable--text'
133
179
  if (
134
180
  _.get(this.block, 'metadata.config.display.italic_icon', false)
135
181
  ) {
@@ -137,6 +183,22 @@ export default {
137
183
  }
138
184
  return classes
139
185
  },
186
+ activatorButtonClass() {
187
+ return (itemIndex) => {
188
+ let classes =
189
+ 'button-icon button-icon--outline pa-1 ' +
190
+ this.itemColor(itemIndex)
191
+ if (this.block.metadata.config.display.large_icon) {
192
+ classes += ' button-icon--large'
193
+ } else {
194
+ classes += ' button-icon--normal'
195
+ }
196
+ if (this.block.metadata.config.display.round_icon) {
197
+ classes += ' button-icon--rounded'
198
+ }
199
+ return classes
200
+ }
201
+ },
140
202
  itemColor() {
141
203
  return (itemIndex) => {
142
204
  // If autocolor is enabled then calculate the color on the index
@@ -179,20 +241,45 @@ export default {
179
241
  isIcon(str) {
180
242
  return str && _.isString(str) && str.indexOf('mdi-') === 0
181
243
  },
244
+ getImagePublicUrl(asset) {
245
+ const foundAsset = this.resolveAsset(asset)
246
+
247
+ return _.get(foundAsset, 'asset.public_url', null)
248
+ },
182
249
  },
183
250
  }
184
251
  </script>
185
- <style scoped>
252
+ <style lang="scss" scoped>
186
253
  .option-container {
187
254
  border-radius: 1rem;
188
255
  }
189
- .button-icon {
256
+ button.button-icon {
257
+ border-width: 4px;
258
+ width: 100% !important;
259
+ height: auto !important;
260
+ aspect-ratio: 1 / 1;
261
+ }
262
+
263
+ button.button-icon.button-icon--outline {
264
+ background-color: #fff !important;
190
265
  border-width: 4px;
191
- background: #fff;
192
- height: 74px !important;
193
- width: 74px;
266
+ border-style: solid;
267
+ }
268
+
269
+ button.button-icon.button-icon--rounded {
270
+ border-radius: 100%;
271
+ }
272
+
273
+ .button-icon--normal {
274
+ .clickable--icon,
275
+ .clickable--text {
276
+ font-size: 1.5rem;
277
+ }
194
278
  }
195
- .text-icon {
196
- font-size: 1.5rem;
279
+ .button-icon--large {
280
+ .clickable--icon,
281
+ .clickable--text {
282
+ font-size: 2.5rem;
283
+ }
197
284
  }
198
285
  </style>
@@ -294,6 +294,15 @@ export default {
294
294
  onRemoveTags(body) {
295
295
  return body.replace(/(<([^>]+)>)/gi, '')
296
296
  },
297
+ async onBeforeSave() {
298
+ this.block.metadata.config.emails.forEach((element) => {
299
+ if (element.from) {
300
+ let matches = element.from.match(/\b(\w)/g)
301
+ element.initials = matches.join('')
302
+ }
303
+ element.tinymce_expand = false
304
+ })
305
+ },
297
306
  },
298
307
  }
299
308
  </script>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div>
3
3
  <v-container class="pa-0 ma-0">
4
- <div v-if="!block.body" class="img-holder">
4
+ <div v-if="!imageUrl" class="img-holder">
5
5
  <v-skeleton-loader
6
6
  height="300px"
7
7
  :elevation="2"
@@ -19,11 +19,11 @@
19
19
  </div>
20
20
  <v-responsive :aspect-ratio="aspectRatio">
21
21
  <v-img
22
- v-if="block.body"
23
- :alt="block.metadata.config.alt"
24
- :aria-describedby="block.metadata.config.aria_described_by"
22
+ v-if="imageUrl"
23
+ :alt="altText"
24
+ :aria-describedby="describedByText"
25
25
  :class="imageClass"
26
- :src="block.body"
26
+ :src="imageUrl"
27
27
  contain
28
28
  @click="onHandleModal"
29
29
  >
@@ -52,13 +52,11 @@
52
52
  v-on="on"
53
53
  >
54
54
  <v-img
55
- v-if="block.body"
56
- :alt="block.metadata.config.alt"
57
- :aria-describedby="
58
- block.metadata.config.aria_described_by
59
- "
55
+ v-if="imageUrl"
56
+ :alt="altText"
57
+ :aria-describedby="describedByText"
60
58
  :class="imageClass"
61
- :src="block.body"
59
+ :src="imageUrl"
62
60
  contain
63
61
  @click="onHandleModal"
64
62
  >
@@ -104,27 +102,57 @@ export default {
104
102
  if (_.isEmpty(this.block.metadata.config.alt)) {
105
103
  this.block.metadata.config.alt = ''
106
104
  }
107
- if (!_.isBoolean(this.block.metadata.config.hide_background)) {
108
- this.block.metadata.config.hide_background = false
105
+ if (!_.isBoolean(this.block.metadata.config.hideBackground)) {
106
+ this.block.metadata.config.hideBackground = false
109
107
  }
110
108
  if (!_.isBoolean(this.block.metadata.config.modal)) {
111
109
  this.block.metadata.config.modal = false
112
110
  }
113
- if (_.isEmpty(this.block.metadata.config.aria_describedby)) {
114
- this.block.metadata.config.aria_describedby = ''
111
+ if (_.isEmpty(this.block.metadata.config.ariaDescribedBy)) {
112
+ this.block.metadata.config.ariaDescribedBy = ''
115
113
  }
116
114
  },
117
115
  computed: {
118
116
  describedById() {
119
117
  // If there's a described by
120
- if (_.get(this.block.metadata, 'config.aria_describedby', null)) {
118
+ if (this.describedByText) {
121
119
  return this.id
122
120
  } else {
123
121
  return null
124
122
  }
125
123
  },
124
+ altText() {
125
+ // Get the asset info first and fallback to the block metadata if inherit is set to false
126
+ if (_.get(this.block.metadata, 'config.inherit', true)) {
127
+ return _.get(this.fileAsset, 'metadata.props.alt', null)
128
+ } else {
129
+ return _.get(this.block.metadata, 'config.alt', null)
130
+ }
131
+ },
126
132
  describedByText() {
127
- return _.get(this.block.metadata, 'config.aria_describedby', null)
133
+ // Get the asset info first and fallback to the block metadata if inherit is set to false
134
+ if (_.get(this.block.metadata, 'config.inherit', true)) {
135
+ return _.get(
136
+ this.fileAsset,
137
+ 'metadata.props.aria_describedby',
138
+ null
139
+ )
140
+ } else {
141
+ return _.get(
142
+ this.block.metadata,
143
+ 'config.ariaDescribedBy',
144
+ null
145
+ )
146
+ }
147
+ },
148
+ fileAsset() {
149
+ return this.resolveAsset(
150
+ _.get(this.block, 'metadata.config.asset', null)
151
+ )
152
+ },
153
+ imageUrl() {
154
+ // Get the image public url and fallback to the block body
155
+ return _.get(this.fileAsset, 'asset.public_url', this.block.body)
128
156
  },
129
157
  imageClass() {
130
158
  let imageClass = ''
@@ -133,7 +161,7 @@ export default {
133
161
  imageClass += ' container-pointer'
134
162
  }
135
163
  // If NOT hide background, inclide the extra class
136
- if (!_.get(this.block.metadata, 'config.hide_background', false)) {
164
+ if (!_.get(this.block.metadata, 'config.hideBackground', false)) {
137
165
  imageClass += ' img-white'
138
166
  }
139
167
  return 'img-display' + imageClass
@@ -164,7 +192,7 @@ export default {
164
192
  }
165
193
  </script>
166
194
 
167
- <style scoped>
195
+ <style lang="scss" scoped>
168
196
  .img-display {
169
197
  border-radius: 3px;
170
198
  }
@@ -28,7 +28,7 @@
28
28
  v-if="hasSource"
29
29
  :language="$i18n && $i18n.locale ? $i18n.locale : 'en-US'"
30
30
  :type="block.metadata.config.type"
31
- :playlist="block.metadata.config.playlist"
31
+ :playlist="linkedPlaylist"
32
32
  :autoplay="block.metadata.config.attributes.autoplay"
33
33
  :autopictureinpicture="
34
34
  block.metadata.config.attributes.autopictureinpicture
@@ -48,7 +48,7 @@
48
48
  :loop="block.metadata.config.attributes.loop"
49
49
  :muted="block.metadata.config.attributes.muted"
50
50
  :playsinline="block.metadata.config.attributes.playsinline"
51
- :poster="block.metadata.config.attributes.poster"
51
+ :poster="linkedPosterPublicUrl"
52
52
  :preload="block.metadata.config.attributes.preload"
53
53
  :captionsmenu="block.metadata.config.attributes.captionsmenu"
54
54
  :playlistmenu="block.metadata.config.attributes.playlistmenu"
@@ -63,14 +63,12 @@
63
63
  <script>
64
64
  import _ from 'lodash'
65
65
  import VuetifyPlayer from '@mindedge/vuetify-player'
66
- import ContentBlockAsset from '~/components/Content/ContentBlockAsset.vue'
67
66
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
68
67
 
69
68
  export default {
70
69
  name: 'ContentBlockVideo',
71
70
  components: {
72
71
  VuetifyPlayer,
73
- ContentBlockAsset,
74
72
  },
75
73
  extends: BaseContentBlock,
76
74
  computed: {
@@ -89,6 +87,73 @@ export default {
89
87
  )
90
88
  )
91
89
  },
90
+ linkedPosterPublicUrl() {
91
+ const file = this.resolveAsset(
92
+ _.get(this.block, 'metadata.config.attributes.poster', null)
93
+ )
94
+ return _.get(file, 'asset.public_url')
95
+ },
96
+ /**
97
+ * Resolve assets to their playlist items
98
+ */
99
+ linkedPlaylist() {
100
+ const playlist = _.cloneDeep(this.block.metadata.config.playlist)
101
+
102
+ for (const index in playlist) {
103
+ for (const sourceIndex in playlist[index].sources) {
104
+ let file = this.resolveAsset(
105
+ playlist[index].sources[sourceIndex]
106
+ )
107
+
108
+ playlist[index].sources[sourceIndex] = {
109
+ src: _.get(file, 'asset.public_url', ''),
110
+ type: _.get(file, 'asset.metadata.mime', ''),
111
+ }
112
+ }
113
+
114
+ for (const trackIndex in playlist[index].tracks) {
115
+ let file = this.resolveAsset(
116
+ playlist[index].tracks[trackIndex]
117
+ )
118
+
119
+ playlist[index].tracks[trackIndex] = {
120
+ src: _.get(file, 'asset.public_url', ''),
121
+ kind: 'captions',
122
+ srclang: 'en-US',
123
+ default: true,
124
+ }
125
+ }
126
+
127
+ for (const adIndex in playlist[index].ads) {
128
+ for (const adSourceIndex in playlist[index].ads[adIndex]
129
+ .sources) {
130
+ let file = this.resolveAsset(
131
+ playlist[index].ads[adIndex].sources[adSourceIndex]
132
+ )
133
+
134
+ playlist[index].ads[adIndex].sources[adSourceIndex] = {
135
+ src: _.get(file, 'asset.public_url', ''),
136
+ type: _.get(file, 'asset.metadata.mime', ''),
137
+ }
138
+ }
139
+ for (const adTrackIndex in playlist[index].ads[adIndex]
140
+ .tracks) {
141
+ let file = this.resolveAsset(
142
+ playlist[index].ads[adIndex].tracks[adTrackIndex]
143
+ )
144
+
145
+ playlist[index].ads[adIndex].tracks[adTrackIndex] = {
146
+ src: _.get(file, 'asset.public_url', ''),
147
+ kind: 'captions',
148
+ srclang: 'en-US',
149
+ default: true,
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ return playlist
156
+ },
92
157
  },
93
158
  data() {
94
159
  return {
@@ -170,13 +235,19 @@ export default {
170
235
  if (_.isEmpty(this.block.metadata.config)) {
171
236
  this.block.metadata.config = this.defaultConfig
172
237
  }
238
+ // If no playback rates were set then re-init it as an array with the rate 1 or normal only
239
+ if (
240
+ _.isEmpty(
241
+ _.get(this.block, 'metadata.config.attributes.playbackrates')
242
+ )
243
+ ) {
244
+ this.block.metadata.config.attributes.playbackrates = [1]
245
+ }
246
+ if (_.isEmpty(this.block.assets)) {
247
+ this.$set(this.block, 'assets', [])
248
+ }
173
249
  },
174
250
  mounted() {
175
- // TODO Check privs
176
- //const hasPrivs = true
177
- //if (hasPrivs) {
178
- //this.blockSettings = this.initEditorSettings(this.settings)
179
- //}
180
251
  this.saveKey = this.settingsKey()
181
252
  },
182
253
  methods: {
@@ -79,6 +79,53 @@
79
79
  "
80
80
  :disabled="render"
81
81
  ></TextEditor>
82
+ <v-container class="pa-0 pt-3">
83
+ <h4>
84
+ {{
85
+ $t(
86
+ 'windward.core.shared.settings.upload_file'
87
+ )
88
+ }}
89
+ </h4>
90
+ <v-text-field
91
+ v-model="
92
+ block.metadata.config.items[index]
93
+ .altText
94
+ "
95
+ class="pt-3 pb-3"
96
+ :hide-details="true"
97
+ dense
98
+ outlined
99
+ :autofocus="true"
100
+ :label="
101
+ $t(
102
+ 'windward.core.shared.settings.alt_image'
103
+ )
104
+ "
105
+ ></v-text-field>
106
+ <v-text-field
107
+ v-model="
108
+ block.metadata.config.items[index]
109
+ .ariaDescribedBy
110
+ "
111
+ class="pt-3 pb-3"
112
+ :hide-details="true"
113
+ dense
114
+ outlined
115
+ :label="
116
+ $t(
117
+ 'windward.core.shared.settings.aria_described'
118
+ )
119
+ "
120
+ ></v-text-field>
121
+ <ContentBlockAsset
122
+ v-model="
123
+ block.metadata.config.items[index].file
124
+ "
125
+ mimes="image/png,image/jpeg"
126
+ :assets.sync="block.assets"
127
+ ></ContentBlockAsset>
128
+ </v-container>
82
129
  </v-container>
83
130
  </template>
84
131
  </SortableExpansionPanel>
@@ -141,6 +188,9 @@ export default {
141
188
  header: '',
142
189
  expand: false,
143
190
  content: '',
191
+ file: null,
192
+ altText: '',
193
+ ariaDescribedBy: '',
144
194
  }
145
195
  this.block.metadata.config.items = []
146
196
  this.block.metadata.config.items.push(defaultObject)
@@ -180,6 +230,9 @@ export default {
180
230
  header: '',
181
231
  expand: false,
182
232
  content: '',
233
+ file: null,
234
+ altText: '',
235
+ ariaDescribedBy: '',
183
236
  }
184
237
  this.block.metadata.config.items.push(default_item)
185
238
  this.block.metadata.config.selectedPanels =