@windward/core 0.0.7 → 0.0.9

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 (80) hide show
  1. package/components/Content/Blocks/ClickableIcons.vue +128 -40
  2. package/components/Content/Blocks/Image.vue +37 -10
  3. package/components/Content/Blocks/OpenResponse.vue +32 -10
  4. package/components/Content/Blocks/OpenResponseCollate.vue +69 -42
  5. package/components/Content/Blocks/ScenarioChoice.vue +262 -0
  6. package/components/Content/Blocks/Tab.vue +2 -7
  7. package/components/Settings/AccordionSettings.vue +97 -74
  8. package/components/Settings/ClickableIconsSettings.vue +101 -86
  9. package/components/Settings/ImageSettings.vue +10 -0
  10. package/components/Settings/OpenResponseCollateSettings.vue +9 -8
  11. package/components/Settings/ScenarioChoiceSettings.vue +329 -0
  12. package/components/Settings/TabSettings.vue +75 -61
  13. package/components/Settings/TextEditorSettings.vue +2 -3
  14. package/components/utils/FillInBlank/FillInBlankInput.vue +4 -1
  15. package/components/utils/TinyMCEWrapper.vue +11 -4
  16. package/components/utils/assets/tinymce/css/content.scss +1 -1
  17. package/helpers/FillInBlankHelper.ts +0 -2
  18. package/helpers/tinymce/plugin.ts +1 -1
  19. package/i18n/en-US/components/content/blocks/index.ts +2 -2
  20. package/i18n/en-US/components/content/blocks/open_response_collate.ts +1 -0
  21. package/i18n/en-US/components/content/blocks/scenario_choice.ts +5 -0
  22. package/i18n/en-US/components/settings/accordion.ts +5 -0
  23. package/i18n/en-US/components/settings/clickable_icon.ts +8 -0
  24. package/i18n/en-US/components/settings/image.ts +3 -1
  25. package/i18n/en-US/components/settings/index.ts +6 -0
  26. package/i18n/en-US/components/settings/scenario_choice.ts +19 -0
  27. package/i18n/en-US/components/settings/tab.ts +7 -0
  28. package/i18n/en-US/components/utils/FillInBlank/FillInBlankInput.ts +1 -1
  29. package/i18n/en-US/shared/content_blocks.ts +1 -0
  30. package/i18n/en-US/shared/settings.ts +2 -0
  31. package/i18n/es-ES/components/content/blocks/feedback.ts +2 -0
  32. package/i18n/es-ES/components/content/blocks/index.ts +2 -2
  33. package/i18n/es-ES/components/content/blocks/open_response_collate.ts +1 -0
  34. package/i18n/es-ES/components/content/blocks/scenario_choice.ts +6 -0
  35. package/i18n/es-ES/components/navigation/index.ts +2 -0
  36. package/i18n/es-ES/components/settings/accordion.ts +5 -0
  37. package/i18n/es-ES/components/settings/clickable_icon.ts +8 -0
  38. package/i18n/es-ES/components/settings/image.ts +3 -1
  39. package/i18n/es-ES/components/settings/index.ts +6 -0
  40. package/i18n/es-ES/components/settings/scenario_choice.ts +19 -0
  41. package/i18n/es-ES/components/{content/blocks → settings}/tab.ts +3 -0
  42. package/i18n/es-ES/components/utils/FillInBlank/FillInBlankInput.ts +13 -0
  43. package/i18n/es-ES/components/utils/FillInBlank/FillInTheBlanksManager.ts +11 -0
  44. package/i18n/es-ES/components/utils/FillInBlank/index.ts +6 -0
  45. package/i18n/es-ES/components/utils/index.ts +2 -0
  46. package/i18n/es-ES/components/utils/tiny_mce_wrapper.ts +1 -0
  47. package/i18n/es-ES/shared/content_blocks.ts +1 -0
  48. package/i18n/es-ES/shared/menu.ts +1 -0
  49. package/i18n/es-ES/shared/settings.ts +2 -0
  50. package/i18n/index.ts +11 -0
  51. package/i18n/sv-SE/components/content/blocks/feedback.ts +2 -0
  52. package/i18n/sv-SE/components/content/blocks/index.ts +2 -2
  53. package/i18n/sv-SE/components/content/blocks/open_response_collate.ts +1 -0
  54. package/i18n/sv-SE/components/content/blocks/scenario_choice.ts +5 -0
  55. package/i18n/sv-SE/components/navigation/index.ts +2 -0
  56. package/i18n/sv-SE/components/settings/accordion.ts +5 -0
  57. package/i18n/sv-SE/components/settings/clickable_icon.ts +8 -0
  58. package/i18n/sv-SE/components/settings/image.ts +3 -1
  59. package/i18n/sv-SE/components/settings/index.ts +6 -0
  60. package/i18n/sv-SE/components/settings/scenario_choice.ts +19 -0
  61. package/i18n/sv-SE/components/{content/blocks → settings}/tab.ts +3 -0
  62. package/i18n/sv-SE/components/utils/FillInBlank/FillInBlankInput.ts +13 -0
  63. package/i18n/sv-SE/components/utils/FillInBlank/FillInTheBlanksManager.ts +11 -0
  64. package/i18n/sv-SE/components/utils/FillInBlank/index.ts +6 -0
  65. package/i18n/sv-SE/components/utils/index.ts +2 -0
  66. package/i18n/sv-SE/components/utils/tiny_mce_wrapper.ts +1 -0
  67. package/i18n/sv-SE/shared/content_blocks.ts +1 -0
  68. package/i18n/sv-SE/shared/menu.ts +1 -0
  69. package/i18n/sv-SE/shared/settings.ts +2 -0
  70. package/package.json +2 -1
  71. package/plugin.js +24 -5
  72. package/test/Components/Content/Blocks/ScenarioChoice.spec.js +21 -0
  73. package/test/Components/Settings/ClickableIconsSettings.spec.js +1 -1
  74. package/test/Components/Settings/ScenarioChoiceSettings.spec.js +20 -0
  75. package/test/Feature/LocaleKeys.spec.js +9 -0
  76. package/test/__mocks__/componentsMock.js +24 -0
  77. package/test/__mocks__/helpersMock.js +3 -0
  78. package/test/__mocks__/modelMock.js +4 -0
  79. package/test/locales.js +95 -0
  80. package/i18n/en-US/components/content/blocks/tab.ts +0 -4
@@ -1,42 +1,60 @@
1
1
  <template>
2
2
  <div>
3
3
  <v-container>
4
- <v-row no-gutters>
5
- <h3>{{ block.metadata.config.title }}</h3>
6
- </v-row>
7
- <v-row no-gutters>
8
- <h4>{{ block.metadata.config.description }}</h4>
9
- </v-row>
10
- <v-row no-gutters>
11
- <p>
12
- {{
13
- $t(
14
- 'windward.core.components.settings.clickable_icon.information'
15
- )
16
- }}
17
- </p>
18
- </v-row>
4
+ <h3>{{ block.metadata.config.title }}</h3>
5
+
6
+ <h4>{{ block.metadata.config.description }}</h4>
7
+
8
+ <p>
9
+ {{
10
+ $t(
11
+ 'windward.core.components.settings.clickable_icon.information'
12
+ )
13
+ }}
14
+ </p>
19
15
  </v-container>
20
16
  <v-container>
21
17
  <v-row
22
18
  v-for="(item, itemIndex) in block.metadata.config.items"
23
19
  :key="itemIndex"
24
20
  no-gutters
21
+ :class="rowClass(itemIndex)"
25
22
  >
26
23
  <v-col cols="2">
27
24
  <v-btn
28
25
  class="pt-8 pb-8 outlined-button mb-4"
29
- :color="itemColor(item.color)"
26
+ :color="itemColor(itemIndex)"
27
+ :fab="block.metadata.config.display.round_icon"
30
28
  x-large
31
29
  outlined
32
30
  @click="item.active = !item.active"
33
31
  >
34
- <v-icon>{{ onHandleHtmlEntities(item.icon) }}</v-icon>
32
+ <v-icon v-if="isIcon(item.icon)" class="black--text">{{
33
+ item.icon
34
+ }}</v-icon>
35
+ <span v-else :class="iconClass + ' black--text'">{{
36
+ decode(item.icon)
37
+ }}</span>
35
38
  </v-btn>
36
39
  </v-col>
37
- <v-col cols="10" v-if="item.active">
38
- <h4>{{ item.title }}</h4>
39
- <TextViewer v-model="item.body"></TextViewer>
40
+ <v-col cols="10">
41
+ <h4
42
+ v-if="
43
+ block.metadata.config.display.show_title ||
44
+ item.active
45
+ "
46
+ class="mt-4"
47
+ role="button"
48
+ @click="item.active = !item.active"
49
+ >
50
+ {{ item.title }}
51
+ </h4>
52
+ <v-expand-transition>
53
+ <div v-if="item.active">
54
+ <v-divider light class="my-4" />
55
+ <TextViewer v-model="item.body"></TextViewer>
56
+ </div>
57
+ </v-expand-transition>
40
58
  </v-col>
41
59
  </v-row>
42
60
  </v-container>
@@ -44,6 +62,7 @@
44
62
  </template>
45
63
  <script>
46
64
  import _ from 'lodash'
65
+ import he from 'he'
47
66
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
48
67
  import TextViewer from '~/components/Text/TextViewer'
49
68
 
@@ -53,13 +72,77 @@ export default {
53
72
  TextViewer,
54
73
  },
55
74
  extends: BaseContentBlock,
75
+ data() {
76
+ return {}
77
+ },
56
78
  computed: {
79
+ decode() {
80
+ return (str) => {
81
+ return he.decode(str || '')
82
+ }
83
+ },
84
+ rowClass() {
85
+ return (itemIndex) => {
86
+ let classes = 'option-container mb-4 pa-1'
87
+ // If show background is enabled and the item has a color
88
+ // Otherwise we do NOT want to apply the `black--text` class since there's no color
89
+ // And things will look bad in dark mode
90
+ if (
91
+ _.get(
92
+ this.block,
93
+ 'metadata.config.display.show_background',
94
+ false
95
+ ) &&
96
+ this.itemColor(itemIndex)
97
+ ) {
98
+ classes += ' ' + this.itemColor(itemIndex) + ' black--text'
99
+ }
100
+ return classes
101
+ }
102
+ },
103
+ iconClass() {
104
+ let classes = 'text-icon'
105
+ if (
106
+ _.get(this.block, 'metadata.config.display.italic_icon', false)
107
+ ) {
108
+ classes += ' font-italic'
109
+ }
110
+ return classes
111
+ },
57
112
  itemColor() {
58
- return (v) => {
59
- if (_.isObject(v)) {
60
- return _.get(v, 'class', '')
113
+ return (itemIndex) => {
114
+ // If autocolor is enabled then calculate the color on the index
115
+ if (this.block.metadata.config.display.autocolor) {
116
+ const colors = [
117
+ 'light-blue lighten-4',
118
+ 'light-green lighten-4',
119
+ 'yellow lighten-4',
120
+ 'orange lighten-3',
121
+ 'red lighten-3',
122
+ 'deep-purple lighten-3',
123
+ ]
124
+ let colorIndex = itemIndex
125
+ // If we exceed the above list of colors loop back around to the beginning
126
+ if (colorIndex >= colors.length) {
127
+ colorIndex =
128
+ colorIndex -
129
+ Math.floor(colorIndex / colors.length) *
130
+ colors.length
131
+ }
132
+
133
+ return colors[colorIndex]
61
134
  } else {
62
- return v
135
+ // Otherwise get the predefined color
136
+ const color = _.get(
137
+ this.block,
138
+ 'metadata.config.items[' + itemIndex + '].color',
139
+ ''
140
+ )
141
+ if (_.isObject(color)) {
142
+ return _.get(color, 'class', '')
143
+ } else {
144
+ return color
145
+ }
63
146
  }
64
147
  }
65
148
  },
@@ -76,32 +159,37 @@ export default {
76
159
  if (_.isEmpty(this.block.metadata.config.description)) {
77
160
  this.block.metadata.config.description = ''
78
161
  }
79
- if (this.block.metadata.config.items) {
80
- this.block.metadata.config.items.forEach((element) => {
81
- element.active = false
82
- })
162
+ if (_.isEmpty(this.block.metadata.config.display)) {
163
+ this.block.metadata.config.display = {
164
+ show_title: false,
165
+ show_background: false,
166
+ round_icon: false,
167
+ italic_icon: false,
168
+ autocolor: true,
169
+ }
83
170
  }
84
- },
85
- data() {
86
- return {
87
- api_key: process.env.TINY_MCE_API_KEY,
88
- title: 'Title',
89
- displayText: false,
171
+ if (this.block.metadata.config.items.length) {
172
+ for (const index in this.block.metadata.config.items) {
173
+ this.block.metadata.config.items[index].active = false
174
+ }
90
175
  }
91
176
  },
92
177
  methods: {
93
- onHandleHtmlEntities(str) {
94
- let txt = document.createElement('textarea')
95
-
96
- txt.innerHTML = str
97
-
98
- return txt.value
178
+ isIcon(str) {
179
+ return str && _.isString(str) && str.indexOf('mdi-') === 0
99
180
  },
100
181
  },
101
182
  }
102
183
  </script>
103
184
  <style scoped>
185
+ .option-container {
186
+ border-radius: 1rem;
187
+ }
104
188
  .outlined-button {
105
189
  border-width: 4px;
190
+ background: #fff;
191
+ }
192
+ .text-icon {
193
+ font-size: 1.5rem;
106
194
  }
107
195
  </style>
@@ -1,22 +1,31 @@
1
1
  <template>
2
2
  <div>
3
- <p v-if="!block.body">
4
- <v-img
5
- class="img-holder"
6
- src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTNCyiabrwN6YV6s3Mj5IzREZCnQqqwT4A3Bw&usqp=CAU"
7
- contain
8
- ></v-img>
9
- </p>
3
+ <div v-if="!block.body" class="img-holder">
4
+ <v-skeleton-loader
5
+ height="300px"
6
+ :elevation="2"
7
+ type="image"
8
+ ></v-skeleton-loader>
9
+
10
+ <div class="no-source-overlay">
11
+ <v-icon x-large>mdi-file-question</v-icon><br />
12
+ {{
13
+ $t(
14
+ 'windward.core.components.content.blocks.image.no_image_url'
15
+ )
16
+ }}
17
+ </div>
18
+ </div>
10
19
  <v-responsive :aspect-ratio="aspectRatio">
11
20
  <v-img
12
21
  v-if="block.body"
13
22
  :alt="block.metadata.config.alt"
14
23
  :aria-describedby="block.metadata.config.aria_described_by"
15
- class="img-display"
24
+ :class="imageClass"
16
25
  :src="block.body"
17
26
  contain
18
27
  >
19
- <template v-slot:placeholder>
28
+ <template #placeholder>
20
29
  <v-skeleton-loader
21
30
  type="image, image, list-item-avatar"
22
31
  height="100%"
@@ -59,6 +68,9 @@ export default {
59
68
  if (_.isEmpty(this.block.metadata.config.alt)) {
60
69
  this.block.metadata.config.alt = ''
61
70
  }
71
+ if (!_.isBoolean(this.block.metadata.config.hide_background)) {
72
+ this.block.metadata.config.hide_background = false
73
+ }
62
74
  if (_.isEmpty(this.block.metadata.config.aria_describedby)) {
63
75
  this.block.metadata.config.aria_describedby = ''
64
76
  }
@@ -75,6 +87,14 @@ export default {
75
87
  describedByText() {
76
88
  return _.get(this.block.metadata, 'config.aria_describedby', null)
77
89
  },
90
+ imageClass() {
91
+ let imageClass = ''
92
+ // If NOT hide background, inclide the extra class
93
+ if (!_.get(this.block.metadata, 'config.hide_background', false)) {
94
+ imageClass += ' img-white'
95
+ }
96
+ return 'img-display' + imageClass
97
+ },
78
98
  },
79
99
  watch: {
80
100
  value(newValue) {
@@ -101,7 +121,14 @@ export default {
101
121
  border-radius: 3px;
102
122
  }
103
123
  .img-holder {
104
- max-height: 300px;
124
+ height: 300px;
125
+ }
126
+ .img-white {
127
+ background: #fff;
128
+ }
129
+ .no-source-overlay {
130
+ text-align: center;
131
+ margin-top: -175px;
105
132
  }
106
133
  ::v-deep .v-skeleton-loader.v-skeleton-loader--is-loading {
107
134
  .v-skeleton-loader__image {
@@ -75,9 +75,11 @@
75
75
 
76
76
  <script>
77
77
  import _ from 'lodash'
78
+ import { mapGetters } from 'vuex'
78
79
  import TextViewer from '~/components/Text/TextViewer.vue'
79
80
  import TextEditor from '~/components/Text/TextEditor.vue'
80
81
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
82
+ import UserContentBlockState from '~/models/UserContentBlockState'
81
83
 
82
84
  export default {
83
85
  name: 'ContentBlockOpenResponse',
@@ -93,6 +95,18 @@ export default {
93
95
  submitted: false,
94
96
  }
95
97
  },
98
+ computed: {
99
+ ...mapGetters({
100
+ enrollment: 'enrollment/get',
101
+ }),
102
+ canSubmit() {
103
+ // Make sure the response is not empty and not equal to the starting text
104
+ return (
105
+ this.response &&
106
+ this.response !== this.block.metadata.config.starting_text
107
+ )
108
+ },
109
+ },
96
110
  beforeMount() {
97
111
  if (_.isEmpty(this.block.body)) {
98
112
  this.block.body = ''
@@ -107,23 +121,31 @@ export default {
107
121
  this.block.metadata.config.starting_text = ''
108
122
  }
109
123
  },
110
- computed: {
111
- canSubmit() {
112
- // Make sure the response is not empty and not equal to the starting text
113
- return (
114
- this.response &&
115
- this.response !== this.block.metadata.config.starting_text
116
- )
117
- },
118
- },
119
124
  watch: {},
120
125
  mounted() {},
121
126
  methods: {
122
- onAfterSetContentBlockState() {
127
+ async onAfterSetContentBlockState() {
128
+ // Check to see if we have a state already for this block with the same block_id
129
+ // States are loaded via the ContentBlock.id but in this particular case we want to
130
+ // maintain the state ACROSS different ContentBlock.ids but with the same linked Block.id
131
+ const userState = await UserContentBlockState.where({
132
+ 'metadata->block->tag': 'plugin-core-open-response',
133
+ course_user_id: this.enrollment.id,
134
+ })
135
+ .where('metadata->block->block_id', this.block.block_id)
136
+ .first()
137
+
138
+ // Apply the "True" state
139
+ if (!_.isEmpty(userState)) {
140
+ this.response = _.get(userState, 'metadata.response', '')
141
+ this.submitted = _.get(userState, 'metadata.submitted', false)
142
+ }
143
+
123
144
  // If after the state is applied the response is still empty then apply the default response
124
145
  if (this.response === '') {
125
146
  this.response = this.block.metadata.config.starting_text
126
147
  }
148
+
127
149
  this.stateLoaded = true
128
150
  },
129
151
  onSubmit() {
@@ -31,7 +31,9 @@ export default {
31
31
  extends: BaseContentBlock,
32
32
  components: {},
33
33
  data() {
34
- return {}
34
+ return {
35
+ saveState: false, // Override the base block to disable state saving
36
+ }
35
37
  },
36
38
  beforeMount() {
37
39
  if (_.isEmpty(this.block.body)) {
@@ -46,7 +48,8 @@ export default {
46
48
  if (_.isEmpty(this.block.metadata.config.filename)) {
47
49
  this.block.metadata.config.filename = ''
48
50
  }
49
- if (_.isEmpty(this.block.metadata.config.include_prompts)) {
51
+ // _.isEmpty(true) returns false. use isBoolean to check if this prop exists
52
+ if (!_.isBoolean(this.block.metadata.config.include_prompts)) {
50
53
  this.block.metadata.config.include_prompts = false
51
54
  }
52
55
  },
@@ -62,50 +65,74 @@ export default {
62
65
  mounted() {},
63
66
  methods: {
64
67
  async onCollate() {
65
- let userState = await UserContentBlockState.where({
66
- 'metadata->block->tag': 'plugin-core-open-response',
67
- course_user_id: this.enrollment.id,
68
- })
69
- .whereIn('content_block_id', this.block.metadata.config.linked)
70
- .get()
71
- let collated = ''
68
+ try {
69
+ let userState = await UserContentBlockState.where({
70
+ 'metadata->block->tag': 'plugin-core-open-response',
71
+ course_user_id: this.enrollment.id,
72
+ })
73
+ .whereIn(
74
+ 'metadata->block->block_id',
75
+ this.block.metadata.config.linked
76
+ )
77
+ .get()
78
+ let collated = ''
79
+ const sortedStates = []
72
80
 
73
- userState.forEach((state) => {
74
- // Prepend the prompt from the state if include prompts is enabled
75
- if (this.block.metadata.config.include_prompts) {
76
- collated +=
77
- '<strong>' +
78
- state.metadata.block.body +
79
- '</strong><hr />'
80
- }
81
- if (!_.isEmpty(state.metadata.response)) {
82
- collated += '\n' + state.metadata.response
83
- } else {
84
- collated +=
85
- '\n<p>' +
86
- this.$t(
87
- 'windward.core.components.content.blocks.open_response_collate.no_response'
88
- ) +
89
- '</p>'
81
+ // Sorted the states based on the linked order
82
+ this.block.metadata.config.linked.forEach((linkedId) => {
83
+ const found = userState.find((state) => {
84
+ return state.metadata.block.block_id === linkedId
85
+ })
86
+ if (found) {
87
+ sortedStates.push(found)
88
+ }
89
+ })
90
+
91
+ sortedStates.forEach((state) => {
92
+ // Prepend the prompt from the state if include prompts is enabled
93
+ if (this.block.metadata.config.include_prompts) {
94
+ collated +=
95
+ '<strong>' +
96
+ state.metadata.block.body +
97
+ '</strong><hr />'
98
+ }
99
+ if (!_.isEmpty(state.metadata.response)) {
100
+ collated += '\n' + state.metadata.response
101
+ } else {
102
+ collated +=
103
+ '\n<p>' +
104
+ this.$t(
105
+ 'windward.core.components.content.blocks.open_response_collate.no_response'
106
+ ) +
107
+ '</p>'
108
+ }
109
+ })
110
+ let filename = this.block.metadata.config.filename
111
+ if (_.isEmpty(this.block.metadata.config.filename)) {
112
+ // Default filename is the users name + the current page
113
+ filename =
114
+ this.$auth.user.last_name +
115
+ '_' +
116
+ this.$auth.user.first_name +
117
+ '_' +
118
+ _.get(this.content, 'content.name_prefix') +
119
+ _.get(this.content, 'content.name')
120
+
121
+ // Change spaces to underscores and remove special characters
122
+ filename = filename.replaceAll(/\s+/gi, '_')
123
+ filename = filename.replaceAll(/[^a-z0-9\-\_]/gi, '')
90
124
  }
91
- })
92
- let filename = this.block.metadata.config.filename
93
- if (_.isEmpty(this.block.metadata.config.filename)) {
94
- // Default filename is the users name + the current page
95
- filename =
96
- this.$auth.user.last_name +
97
- '_' +
98
- this.$auth.user.first_name +
99
- '_' +
100
- _.get(this.content, 'content.name_prefix') +
101
- _.get(this.content, 'content.name')
102
125
 
103
- // Change spaces to underscores and remove special characters
104
- filename = filename.replaceAll(/\s+/gi, '_')
105
- filename = filename.replaceAll(/[^a-z0-9\-\_]/gi, '')
126
+ this.generateDocument(collated, filename)
127
+ } catch (e) {
128
+ // eslint-disable-next-line no-console
129
+ console.error(e)
130
+ this.$dialog.error(
131
+ this.$t(
132
+ 'windward.core.components.content.blocks.open_response_collate.generate_error'
133
+ )
134
+ )
106
135
  }
107
-
108
- this.generateDocument(collated, filename)
109
136
  },
110
137
  generateDocument(htmlBody, filename = '') {
111
138
  // Specify file name. If one isn't supplied then a default name of `exported_document_YYYY-MM-DD.doc` is used