@windward/core 0.16.0 → 0.17.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Changelog
2
2
 
3
+ ### Release [0.17.0] created - 2025-05-13
4
+
5
+
3
6
  ### Release [0.16.0] created - 2025-04-29
4
7
 
5
8
 
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <div>
3
3
  <v-container
4
- class="pa-0"
5
4
  v-if="
6
5
  block.metadata.config.title ||
7
6
  block.metadata.config.instructions
8
7
  "
8
+ class="pa-0"
9
9
  >
10
10
  <h2 v-if="block.metadata.config.title" tabindex="0">
11
11
  {{ block.metadata.config.title }}
@@ -31,9 +31,10 @@
31
31
  :key="itemIndex"
32
32
  >
33
33
  <v-expansion-panel-header
34
+ :ref="'expansion-panel-' + itemIndex"
34
35
  class="expansion-panel-header"
35
36
  color="primary"
36
- :ref="'expansion-panel-' + itemIndex"
37
+ role="button"
37
38
  @click="onOpenAccordion(itemIndex)"
38
39
  >
39
40
  {{
@@ -43,8 +44,8 @@
43
44
  }}
44
45
  </v-expansion-panel-header>
45
46
  <v-expansion-panel-content
46
- class="expansion-panel-body"
47
47
  :key="expansionPanelKey"
48
+ class="expansion-panel-body"
48
49
  >
49
50
  <v-container
50
51
  class="px-0 d-flex"
@@ -87,9 +88,9 @@
87
88
  import _ from 'lodash'
88
89
  import Crypto from '~/helpers/Crypto'
89
90
  import ImageAssetViewer from '~/components/Content/ImageAssetViewer.vue'
91
+ import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
90
92
  import TextEditor from '~/components/Text/TextEditor'
91
93
  import TextViewer from '~/components/Text/TextViewer'
92
- import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
93
94
 
94
95
  export default {
95
96
  name: 'ContentBlockAccordion',
@@ -99,40 +100,6 @@ export default {
99
100
  ImageAssetViewer,
100
101
  },
101
102
  extends: BaseContentBlock,
102
- beforeMount() {
103
- if (_.isEmpty(this.block)) {
104
- this.block = {}
105
- }
106
- if (_.isEmpty(this.block.metadata)) {
107
- this.block.metadata = {}
108
- }
109
- if (_.isEmpty(this.block.metadata.config)) {
110
- this.block.metadata.config = {}
111
- }
112
- if (_.isEmpty(this.block.metadata.config.title)) {
113
- this.block.metadata.config.title = ''
114
- }
115
- if (_.isEmpty(this.block.metadata.config.items)) {
116
- const defaultObject = {
117
- header: '',
118
- expand: false,
119
- content: '',
120
- fileConfig: {
121
- display: {
122
- width: 100,
123
- margin: '',
124
- padding: '',
125
- },
126
- hideBackground: true,
127
- },
128
- }
129
- this.block.metadata.config.items = []
130
- this.block.metadata.config.items.push(defaultObject)
131
- }
132
- this.block.body = this.$t(
133
- 'windward.core.shared.content_blocks.title.accordion'
134
- )
135
- },
136
103
  data() {
137
104
  return {
138
105
  expansionPanelKey: '0',
@@ -184,6 +151,40 @@ export default {
184
151
  }
185
152
  },
186
153
  },
154
+ beforeMount() {
155
+ if (_.isEmpty(this.block)) {
156
+ this.block = {}
157
+ }
158
+ if (_.isEmpty(this.block.metadata)) {
159
+ this.block.metadata = {}
160
+ }
161
+ if (_.isEmpty(this.block.metadata.config)) {
162
+ this.block.metadata.config = {}
163
+ }
164
+ if (_.isEmpty(this.block.metadata.config.title)) {
165
+ this.block.metadata.config.title = ''
166
+ }
167
+ if (_.isEmpty(this.block.metadata.config.items)) {
168
+ const defaultObject = {
169
+ header: '',
170
+ expand: false,
171
+ content: '',
172
+ fileConfig: {
173
+ display: {
174
+ width: 100,
175
+ margin: '',
176
+ padding: '',
177
+ },
178
+ hideBackground: true,
179
+ },
180
+ }
181
+ this.block.metadata.config.items = []
182
+ this.block.metadata.config.items.push(defaultObject)
183
+ }
184
+ this.block.body = this.$t(
185
+ 'windward.core.shared.content_blocks.title.accordion'
186
+ )
187
+ },
187
188
  mounted() {
188
189
  if (this.render) {
189
190
  // ensure panels are always loaded closed
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <div>
3
3
  <v-container
4
- class="pa-0"
5
4
  v-if="
6
5
  block.metadata.config.title ||
7
6
  block.metadata.config.instructions
8
7
  "
8
+ class="pa-0"
9
9
  >
10
10
  <h2 v-if="block.metadata.config.title" tabindex="0">
11
11
  {{ block.metadata.config.title }}
@@ -23,6 +23,7 @@
23
23
  <div v-if="block.body && !submitted">
24
24
  <TextViewer v-model="block.body" :height="200"></TextViewer>
25
25
  <TextEditor
26
+ :key="updateKey"
26
27
  v-model="response"
27
28
  :height="200"
28
29
  menubar=""
@@ -104,23 +105,25 @@
104
105
  <script>
105
106
  import _ from 'lodash'
106
107
  import { mapGetters } from 'vuex'
107
- import TextViewer from '~/components/Text/TextViewer'
108
- import TextEditor from '~/components/Text/TextEditor'
109
108
  import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
110
109
  import UserContentBlockState from '~/models/UserContentBlockState'
110
+ import Crypto from '~/helpers/Crypto'
111
+ import TextViewer from '~/components/Text/TextViewer'
112
+ import TextEditor from '~/components/Text/TextEditor'
111
113
 
112
114
  export default {
113
115
  name: 'ContentBlockOpenResponse',
114
- extends: BaseContentBlock,
115
116
  components: {
116
117
  TextViewer,
117
118
  TextEditor,
118
119
  },
120
+ extends: BaseContentBlock,
119
121
  data() {
120
122
  return {
121
123
  stateLoaded: false,
122
124
  response: '',
123
125
  submitted: false,
126
+ updateKey: Crypto.id(),
124
127
  }
125
128
  },
126
129
  computed: {
@@ -135,6 +138,13 @@ export default {
135
138
  )
136
139
  },
137
140
  },
141
+ watch: {
142
+ render(newVal) {
143
+ if (newVal) {
144
+ this.onAfterSetContentBlockState()
145
+ }
146
+ },
147
+ },
138
148
  beforeMount() {
139
149
  if (_.isEmpty(this.block.body)) {
140
150
  this.block.body = ''
@@ -155,13 +165,6 @@ export default {
155
165
  this.block.metadata.config.starting_text = ''
156
166
  }
157
167
  },
158
- watch: {
159
- render(newVal) {
160
- if (newVal) {
161
- this.onAfterSetContentBlockState()
162
- }
163
- },
164
- },
165
168
  mounted() {},
166
169
  methods: {
167
170
  async onAfterSetContentBlockState() {
@@ -194,6 +197,7 @@ export default {
194
197
  // If we don't have a block_id then we are in the initial setup state
195
198
  this.response = this.block.metadata.config.starting_text
196
199
  }
200
+ this.updateKey = Crypto.id()
197
201
  this.stateLoaded = true
198
202
  },
199
203
  onSubmit() {
@@ -2,6 +2,7 @@
2
2
  <v-tooltip right>
3
3
  <template #activator="{ on, attrs }">
4
4
  <v-list-item
5
+ :class="color"
5
6
  :to="
6
7
  '/course/' +
7
8
  course.id +
@@ -38,6 +39,9 @@ import { mapGetters } from 'vuex'
38
39
  export default {
39
40
  components: {},
40
41
  middleware: ['auth'],
42
+ props: {
43
+ color: { type: String, required: false, default: '' },
44
+ },
41
45
  data() {
42
46
  return {}
43
47
  },
@@ -5,6 +5,7 @@
5
5
  autofill
6
6
  :disabled="render"
7
7
  allow-read
8
+ root-block="p"
8
9
  show-glossary
9
10
  :hide-text-editor="hideTextEditor"
10
11
  >
@@ -67,6 +67,9 @@ export default {
67
67
  </script>
68
68
 
69
69
  <style lang="scss">
70
+ .text-viewer {
71
+ overflow-x: auto !important;
72
+ }
70
73
  .text-viewer table tr td {
71
74
  padding: 10px !important;
72
75
  }
@@ -30,7 +30,8 @@
30
30
  color="primary"
31
31
  elevation="0"
32
32
  outlined
33
- @click="show = false"
33
+ @click="close"
34
+ @keydown.esc="close"
34
35
  >
35
36
  {{
36
37
  $t(
@@ -46,12 +47,11 @@
46
47
 
47
48
  <div class="fib">
48
49
  <v-text-field
50
+ v-model="userInput"
49
51
  :disabled="false"
50
52
  outlined
51
53
  dense
52
- v-model="userInput"
53
54
  clear-icon="mdi-close-circle-outline"
54
- @click:append-outer="toggleToolTip"
55
55
  clearable
56
56
  :background-color="feedback"
57
57
  :placeholder="
@@ -59,18 +59,19 @@
59
59
  'windward.core.components.utils.fill_in_the_blank.fill_in_blank_input.add_answer'
60
60
  )
61
61
  "
62
- @input="showAnswer = false"
63
- @keydown="onPressEnter"
64
62
  :aria-label="
65
63
  $t(
66
64
  'windward.core.components.utils.tiny_mce_wrapper.fill_in_blank'
67
65
  )
68
66
  "
67
+ @click:append-outer="toggleToolTip"
68
+ @input="showAnswer = false"
69
+ @keydown="onPressEnter"
69
70
  ></v-text-field>
70
71
  &nbsp;
71
72
  <v-btn
72
73
  v-if="userInput && userInput.length > 0"
73
- @click="toggleToolTip"
74
+ ref="btnCheck"
74
75
  elevation="0"
75
76
  color="primary"
76
77
  outlined
@@ -79,25 +80,22 @@
79
80
  'windward.core.components.utils.fill_in_the_blank.fill_in_blank_input.check_answer'
80
81
  )
81
82
  "
83
+ @click="toggleToolTip"
82
84
  >{{
83
- show
84
- ? $t(
85
- 'windward.core.components.utils.fill_in_the_blank.fill_in_blank_input.reset'
86
- )
87
- : $t(
88
- 'windward.core.components.utils.fill_in_the_blank.fill_in_blank_input.check'
89
- )
85
+ $t(
86
+ 'windward.core.components.utils.fill_in_the_blank.fill_in_blank_input.check'
87
+ )
90
88
  }}</v-btn
91
89
  >
92
90
  </div>
93
91
  </span>
94
92
  </template>
95
93
  <script>
96
- import ContentViewer from '../ContentViewer.vue'
97
94
  import _ from 'lodash'
95
+
98
96
  export default {
99
97
  name: 'FillInBlankInput',
100
- components: { ContentViewer },
98
+ components: {},
101
99
  props: {
102
100
  answer: { type: String, required: true },
103
101
  description: {
@@ -144,6 +142,20 @@ export default {
144
142
  return ''
145
143
  },
146
144
  },
145
+ watch: {
146
+ // set focus on the action button when dialog loaded for accessibility
147
+ show: function (newValue) {
148
+ if (newValue) {
149
+ this.timerId = setTimeout(() => {
150
+ this.setFocusAction()
151
+ }, 100)
152
+ } else {
153
+ if (this.timerId !== '') {
154
+ clearTimeout(this.timeout)
155
+ }
156
+ }
157
+ },
158
+ },
147
159
  methods: {
148
160
  setFocusAction() {
149
161
  this.$refs.action_button_1.$el.focus()
@@ -171,19 +183,11 @@ export default {
171
183
 
172
184
  this.show = !this.show
173
185
  },
174
- },
175
- watch: {
176
- //set focus on the action button when dialog loaded for accessibility
177
- show: function (newValue) {
178
- if (newValue) {
179
- this.timerId = setTimeout(() => {
180
- this.setFocusAction()
181
- }, 100)
182
- } else {
183
- if (this.timerId !== '') {
184
- clearTimeout(this.timeout)
185
- }
186
- }
186
+ close() {
187
+ this.show = false
188
+ this.$nextTick(() => {
189
+ this.$refs.btnCheck.$el.focus()
190
+ })
187
191
  },
188
192
  },
189
193
  }
@@ -40,6 +40,25 @@
40
40
  return-object
41
41
  ></v-select>
42
42
  </v-col>
43
+ <v-col
44
+ cols="auto"
45
+ class="d-flex align-center"
46
+ v-if="isFlashcardType"
47
+ >
48
+ <v-switch
49
+ v-model="replaceExisting"
50
+ hide-details
51
+ dense
52
+ color="secondary"
53
+ class="mt-0 pt-0"
54
+ :label="
55
+ $t(
56
+ 'windward.games.components.settings.flashcard.form.replace_existing'
57
+ )
58
+ "
59
+ :disabled="isLoading"
60
+ ></v-switch>
61
+ </v-col>
43
62
  <v-col>
44
63
  <v-btn
45
64
  elevation="0"
@@ -76,7 +95,7 @@ import AssessmentQuestion from '~/models/AssessmentQuestion'
76
95
  import Course from '~/models/Course'
77
96
  import Assessment from '~/models/Assessment'
78
97
  import Content from '~/models/Content'
79
- import Crypto from '~/helpers/Crypto'
98
+ import Activity from '../../models/Activity'
80
99
 
81
100
  export default {
82
101
  name: 'GenerateAIQuestionButton',
@@ -85,6 +104,7 @@ export default {
85
104
  content: { type: Object, required: true },
86
105
  block: { type: Object, required: true },
87
106
  questionType: { type: String, required: true },
107
+ replaceExistingMode: { type: Boolean, default: false },
88
108
  },
89
109
  data() {
90
110
  return {
@@ -96,12 +116,16 @@ export default {
96
116
  'windward.core.components.content.blocks.generate_questions.blooms.none'
97
117
  ),
98
118
  },
119
+ replaceExisting: this.replaceExistingMode,
99
120
  }
100
121
  },
101
122
  computed: {
102
123
  ...mapGetters({
103
124
  contentTree: 'content/getTree',
104
125
  }),
126
+ isFlashcardType() {
127
+ return this.questionType === 'flashcard'
128
+ },
105
129
  flattenedContent() {
106
130
  let cloneContentTree = _.cloneDeep(this.contentTree)
107
131
  const homepage = this.$ContentService.getHomepage()
@@ -131,6 +155,7 @@ export default {
131
155
  return fullTree
132
156
  },
133
157
  taxonomyLevels() {
158
+ // Basic Bloom's taxonomy levels available to all question types
134
159
  let basicBloomTaxonomy = [
135
160
  {
136
161
  value: 'None',
@@ -158,10 +183,13 @@ export default {
158
183
  },
159
184
  ]
160
185
 
186
+ // Only add higher-level Bloom's taxonomy for supported question types
187
+ // Flashcards use only basic levels
161
188
  if (
162
- this.questionType === 'multi_choice_single_answer' ||
163
- this.questionType === 'ordering' ||
164
- this.questionType === 'multi_choice_multi_answer'
189
+ !this.isFlashcardType &&
190
+ (this.questionType === 'multi_choice_single_answer' ||
191
+ this.questionType === 'ordering' ||
192
+ this.questionType === 'multi_choice_multi_answer')
165
193
  ) {
166
194
  const multiBlooms = [
167
195
  {
@@ -185,32 +213,96 @@ export default {
185
213
  methods: {
186
214
  async generateAIQuestion() {
187
215
  this.isLoading = true
188
- let bloomsRequest = ''
189
- if (
190
- this.selectedDifficulty.text !==
191
- this.$t(
192
- 'windward.core.components.content.blocks.generate_questions.blooms.none'
193
- )
194
- ) {
195
- // send value to api call so its not changing with language
196
- bloomsRequest = `?blooms_level=${this.selectedDifficulty.value}`
197
- }
198
216
  try {
199
- const response = await AssessmentQuestion.custom(
200
- new Course(this.course),
201
- new Content(this.selectedContent),
202
- new Assessment({ id: this.block.id }),
203
- new AssessmentQuestion(),
204
- `suggest/${this.questionType}${bloomsRequest}`
205
- ).get()
206
-
207
- if (response && response.length > 0) {
208
- const generatedQuestion = response[0]
209
- this.$emit('click:generate', generatedQuestion)
217
+ let bloomsRequest = ''
218
+ if (
219
+ this.selectedDifficulty.text !==
220
+ this.$t(
221
+ 'windward.core.components.content.blocks.generate_questions.blooms.none'
222
+ )
223
+ ) {
224
+ bloomsRequest = `?blooms_level=${this.selectedDifficulty.value}`
225
+ }
226
+
227
+ const course = new Course(this.course)
228
+ const content = new Content(
229
+ this.selectedContent || this.content
230
+ )
231
+
232
+ let response
233
+ if (this.questionType === 'flashcard') {
234
+ // FLASHCARD GENERATION
235
+ const activity = new Activity()
236
+
237
+ const endpoint = `suggest/flashcard${bloomsRequest}`
238
+
239
+ // Call the endpoint exactly like FlashCardSlidesManager does
240
+ response = await Activity.custom(
241
+ course,
242
+ content,
243
+ activity,
244
+ endpoint
245
+ ).get()
246
+
247
+ let activityData = null
248
+
249
+ if (response && response.activity) {
250
+ activityData = response.activity
251
+ } else if (
252
+ response &&
253
+ response.length > 0 &&
254
+ response[0] &&
255
+ response[0].activity
256
+ ) {
257
+ activityData = response[0].activity
258
+ } else if (Array.isArray(response) && response.length > 0) {
259
+ activityData = response[0]
260
+ }
261
+
262
+ if (
263
+ activityData &&
264
+ activityData.metadata &&
265
+ activityData.metadata.config &&
266
+ activityData.metadata.config.cards &&
267
+ Array.isArray(activityData.metadata.config.cards)
268
+ ) {
269
+ // We pass the activity data and the replace flag to the parent component
270
+ this.$emit(
271
+ 'click:generate',
272
+ activityData,
273
+ this.replaceExisting
274
+ )
275
+ } else {
276
+ throw new Error(
277
+ 'Invalid response from flashcard generation'
278
+ )
279
+ }
280
+ } else {
281
+ // ASSESSMENT QUESTION GENERATION
282
+ const assessment = new Assessment({ id: this.block.id })
283
+ const question = new AssessmentQuestion()
284
+
285
+ response = await AssessmentQuestion.custom(
286
+ course,
287
+ content,
288
+ assessment,
289
+ question,
290
+ `suggest/${this.questionType}${bloomsRequest}`
291
+ ).get()
292
+
293
+ if (response && response.length > 0) {
294
+ const generatedQuestion = response[0]
295
+ this.$emit('click:generate', generatedQuestion)
296
+ } else {
297
+ throw new Error(
298
+ 'Invalid response from question generation'
299
+ )
300
+ }
210
301
  }
211
302
  } catch (error) {
212
303
  const errorMessage =
213
304
  error.response?.data?.error?.message ||
305
+ error.message ||
214
306
  'assessment.error.technical'
215
307
  const errorType = errorMessage.split('.').pop()
216
308
  const basePath =
@@ -138,7 +138,7 @@ export default {
138
138
  type: String,
139
139
  required: false,
140
140
  default:
141
- 'undo redo | styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent |glossaryButton fibFormatButton mathButton a11yButton',
141
+ 'styles | bold italic underline strikethrough removeformat | alignleft aligncenter alignright | table tablerowprops tablecellprops |bullist numlist outdent indent |glossaryButton fibFormatButton mathButton a11yButton | undo redo',
142
142
  },
143
143
  rootBlock: { type: String, required: false, default: 'div' },
144
144
  label: { type: String, required: false, default: '' },
@@ -212,7 +212,7 @@ export default {
212
212
  },
213
213
  format: {
214
214
  title: 'Format',
215
- items: ' bold italic underline strikethrough superscript subscript codeformat | formats align | language | removeformat',
215
+ items: ' bold italic underline strikethrough superscript subscript codeformat | formats align | language | removeformat | glossary',
216
216
  },
217
217
  },
218
218
  autoresize_min_height: 100,
@@ -337,6 +337,9 @@ export default {
337
337
  },
338
338
  formats: {
339
339
  glossary: {
340
+ title: this.$t(
341
+ 'windward.core.components.utils.tiny_mce_wrapper.glossary'
342
+ ),
340
343
  inline: 'span',
341
344
  attributes: {
342
345
  'aria-label': this.$t(
@@ -378,67 +381,22 @@ export default {
378
381
  ],
379
382
  },
380
383
  style_formats: [
381
- {
382
- title: 'Headings',
383
- items: [
384
- { title: 'Heading 2', format: 'h2' },
385
- { title: 'Heading 3', format: 'h3' },
386
- { title: 'Heading 4', format: 'h4' },
387
- { title: 'Heading 5', format: 'h5' },
388
- { title: 'Heading 6', format: 'h6' },
389
- ],
390
- },
391
- {
392
- title: 'Inline',
393
- items: [
394
- { title: 'Bold', format: 'bold' },
395
- { title: 'Italic', format: 'italic' },
396
- { title: 'Underline', format: 'underline' },
397
- { title: 'Strikethrough', format: 'strikethrough' },
398
- { title: 'Superscript', format: 'superscript' },
399
- { title: 'Subscript', format: 'subscript' },
400
- { title: 'Code', format: 'code' },
401
- ],
402
- },
403
- {
404
- title: 'Blocks',
405
- items: [{ title: 'Paragraph', format: 'p' }],
406
- },
407
- {
408
- title: 'Align',
409
- items: [
410
- { title: 'Left', format: 'alignleft' },
411
- { title: 'Center', format: 'aligncenter' },
412
- { title: 'Right', format: 'alignright' },
413
- { title: 'Justify', format: 'alignjustify' },
414
- ],
415
- },
416
- {
417
- title: 'LearningEdge',
418
- items: [
419
- {
420
- title: this.$t(
421
- 'windward.core.components.utils.tiny_mce_wrapper.term'
422
- ),
423
- format: 'glossary',
424
- },
425
- {
426
- title: this.$t(
427
- 'windward.core.components.utils.tiny_mce_wrapper.fill_blank'
428
- ),
429
- format: 'fib',
430
- },
431
- ],
432
- },
384
+ { title: 'Normal Text', format: 'div' },
385
+ { title: 'Paragraph', format: 'p' },
386
+ { title: 'Heading 2', format: 'h2' },
387
+ { title: 'Heading 3', format: 'h3' },
388
+ { title: 'Heading 4', format: 'h4' },
389
+ { title: 'Heading 5', format: 'h5' },
390
+ { title: 'Heading 6', format: 'h6' },
433
391
  ],
434
392
  placeholder: this.label
435
393
  ? this.label
436
394
  : this.$t('components.content.settings.base.placeholder'),
437
- //required as it will be displayed as inline style in tinymce renderer
395
+ // required as it will be displayed as inline style in tinymce renderer
438
396
  skin: false,
439
397
  content_css: this.$vuetify.theme.isDark ? 'dark' : 'default',
440
398
 
441
- //we need to inject the glossary style directly
399
+ // we need to inject the glossary style directly
442
400
  content_style:
443
401
  ContentCss +
444
402
  EditorCss +
@@ -477,7 +435,6 @@ export default {
477
435
  this.text = this.value
478
436
  }
479
437
  },
480
-
481
438
  methods: {
482
439
  getEditor() {
483
440
  if (this.$refs.editor && this.$refs.editor.editor) {
@@ -142,7 +142,17 @@ export class WindwardPlugins {
142
142
  'glossaryIcon',
143
143
  this.$t('windward.core.components.utils.tiny_mce_wrapper.glossary'),
144
144
  () => {
145
- this.editor.formatter.apply('glossary')
145
+ if (
146
+ this.editor.selection
147
+ .getSel()
148
+ .anchorNode.parentElement.classList.contains(
149
+ 'glossary-word'
150
+ )
151
+ ) {
152
+ this.editor.formatter.remove('glossary')
153
+ } else {
154
+ this.editor.formatter.apply('glossary')
155
+ }
146
156
  }
147
157
  )
148
158
  this.addButtonToEditor(
@@ -210,13 +220,29 @@ export class WindwardPlugins {
210
220
  itemKey: string,
211
221
  itemText: string,
212
222
  command: string,
213
- icon: string
223
+ icon: string,
224
+ type: string = 'command'
214
225
  ): void {
215
226
  this.editor.ui.registry.addMenuItem(itemKey, {
216
227
  text: itemText,
217
228
  icon,
218
229
  onAction: () => {
219
- this.editor.execCommand(command, true)
230
+ switch (type) {
231
+ case 'format':
232
+ if (
233
+ this.editor.selection
234
+ .getNode()
235
+ .classList.contains('glossary-word')
236
+ ) {
237
+ this.editor.formatter.remove(command)
238
+ } else {
239
+ this.editor.formatter.apply(command)
240
+ }
241
+ break
242
+ default:
243
+ this.editor.execCommand(command, true)
244
+ break
245
+ }
220
246
  },
221
247
  })
222
248
  }
@@ -233,6 +259,13 @@ export class WindwardPlugins {
233
259
  'fib-window',
234
260
  'fibIcon'
235
261
  )
262
+ this.addEditorMenuItem(
263
+ 'glossary',
264
+ 'Glossary',
265
+ 'glossary',
266
+ 'glossaryIcon',
267
+ 'format'
268
+ )
236
269
  }
237
270
 
238
271
  /**
@@ -0,0 +1,9 @@
1
+ // @ts-ignore
2
+ import BaseModel from './BaseModel'
3
+
4
+ export default class Activity extends BaseModel {
5
+ // Set the resource route of the model
6
+ resource() {
7
+ return 'activities'
8
+ }
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windward/core",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Windward UI Core Plugins",
5
5
  "main": "plugin.js",
6
6
  "scripts": {
package/utils/index.js CHANGED
@@ -7,7 +7,7 @@ import TinyMCEWrapper from '../components/utils/TinyMCEWrapper.vue'
7
7
  import MathExpressionEditor from '../components/utils/MathExpressionEditor'
8
8
  import CourseGlossary from '../components/utils/glossary/CourseGlossary.vue'
9
9
  import CourseGlossaryForm from '../components/utils/glossary/CourseGlossaryForm.vue'
10
- import GenerateAIQuestionButton from '../components/Content/Blocks/GenerateAIQuestionButton.vue'
10
+ import GenerateAIQuestionButton from '../components/utils/GenerateAIQuestionButton.vue'
11
11
 
12
12
  export {
13
13
  MathHelper,