From 5b8ad91c1198e5300b3fdbbd4bfc104e0805f3e4 Mon Sep 17 00:00:00 2001 From: btopro Date: Mon, 3 Aug 2026 14:11:10 -0400 Subject: [PATCH 01/10] [multiple-choice] Wire OER Schema relationship properties to question/assessment family Add gradingFormat, hasLearningObjective, and forCourse as Lit String properties on the shared QuestionElement base class and emit them as conditional tags in render() only when set. All three are valid on oer:Assessment via subClassOf chains in the v1.2.0 vocabulary: gradingFormat domain [Activity, Assessment]; hasLearningObjective domain [InstructionalPattern] (Assessment subClassOf InstructionalPattern); forCourse domain [Resource] (Assessment subClassOf Resource via LearningComponent). Add matching gradingFormat/hasLearningObjective/forCourse textfield inputs to settings.configure in all 6 subclass haxProperties.json files (multiple-choice, matching-question, sorting-question, tagging-question, mark-the-words, fill-in-the-blanks) so authors can set these relationships in the HAX editor. Existing oer:assessing, oer:name, oer:answer, oer:correctFeedback, and oer:incorrectFeedback emissions are left intact. Co-Authored-By: Oz --- .../lib/fill-in-the-blanks.haxProperties.json | 18 +++++++++++ .../lib/mark-the-words.haxProperties.json | 18 +++++++++++ .../lib/matching-question.haxProperties.json | 18 +++++++++++ .../multiple-choice/lib/QuestionElement.js | 30 +++++++++++++++++++ .../lib/multiple-choice.haxProperties.json | 18 +++++++++++ .../lib/sorting-question.haxProperties.json | 18 +++++++++++ .../lib/tagging-question.haxProperties.json | 18 +++++++++++ 7 files changed, 138 insertions(+) diff --git a/elements/fill-in-the-blanks/lib/fill-in-the-blanks.haxProperties.json b/elements/fill-in-the-blanks/lib/fill-in-the-blanks.haxProperties.json index 7928e0a6ea..ad2ca32733 100644 --- a/elements/fill-in-the-blanks/lib/fill-in-the-blanks.haxProperties.json +++ b/elements/fill-in-the-blanks/lib/fill-in-the-blanks.haxProperties.json @@ -34,6 +34,24 @@ "title": "Statement", "description": "Statement that will be converted into the mark the words interaction.", "inputMethod": "textarea" + }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" } ], "advanced": [], diff --git a/elements/mark-the-words/lib/mark-the-words.haxProperties.json b/elements/mark-the-words/lib/mark-the-words.haxProperties.json index 2aa40bb6d8..4cc69b7522 100644 --- a/elements/mark-the-words/lib/mark-the-words.haxProperties.json +++ b/elements/mark-the-words/lib/mark-the-words.haxProperties.json @@ -41,6 +41,24 @@ "description": "Statement that will be converted into the mark the words interaction.", "inputMethod": "textarea" }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" + }, { "property": "answers", "title": "Answers", diff --git a/elements/matching-question/lib/matching-question.haxProperties.json b/elements/matching-question/lib/matching-question.haxProperties.json index db4ad35c52..fd8b2d4abc 100644 --- a/elements/matching-question/lib/matching-question.haxProperties.json +++ b/elements/matching-question/lib/matching-question.haxProperties.json @@ -29,6 +29,24 @@ "description": "Question for users. Mark drop targets, then list answers until the next target.", "inputMethod": "textfield" }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" + }, { "property": "answers", "title": "Answers", diff --git a/elements/multiple-choice/lib/QuestionElement.js b/elements/multiple-choice/lib/QuestionElement.js index aeefa5a8c7..c139281f20 100644 --- a/elements/multiple-choice/lib/QuestionElement.js +++ b/elements/multiple-choice/lib/QuestionElement.js @@ -332,6 +332,27 @@ export class QuestionElement extends SchemaBehaviors( type: String, attribute: "related-resource", }, + /** + * OER Schema: grading format for this assessment + */ + gradingFormat: { + type: String, + attribute: "grading-format", + }, + /** + * OER Schema: learning objective associated with this assessment + */ + hasLearningObjective: { + type: String, + attribute: "has-learning-objective", + }, + /** + * OER Schema: course this assessment is for + */ + forCourse: { + type: String, + attribute: "for-course", + }, /** * Question to ask */ @@ -781,6 +802,15 @@ export class QuestionElement extends SchemaBehaviors( render() { return html` + ${this.gradingFormat + ? html`` + : nothing} + ${this.hasLearningObjective + ? html`` + : nothing} + ${this.forCourse + ? html`` + : nothing}
diff --git a/elements/multiple-choice/lib/multiple-choice.haxProperties.json b/elements/multiple-choice/lib/multiple-choice.haxProperties.json index 30c17c0c65..d582f736ee 100644 --- a/elements/multiple-choice/lib/multiple-choice.haxProperties.json +++ b/elements/multiple-choice/lib/multiple-choice.haxProperties.json @@ -51,6 +51,24 @@ "description": "Allow only one answer.", "inputMethod": "boolean" }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" + }, { "property": "answers", "title": "Answers", diff --git a/elements/sorting-question/lib/sorting-question.haxProperties.json b/elements/sorting-question/lib/sorting-question.haxProperties.json index ee36ba3fa1..14bb3a9aa2 100644 --- a/elements/sorting-question/lib/sorting-question.haxProperties.json +++ b/elements/sorting-question/lib/sorting-question.haxProperties.json @@ -28,6 +28,24 @@ "description": "Question Header.", "inputMethod": "textfield" }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" + }, { "property": "answers", "title": "Answers", diff --git a/elements/tagging-question/lib/tagging-question.haxProperties.json b/elements/tagging-question/lib/tagging-question.haxProperties.json index fc3f1c0bc1..3cfd1199c8 100644 --- a/elements/tagging-question/lib/tagging-question.haxProperties.json +++ b/elements/tagging-question/lib/tagging-question.haxProperties.json @@ -29,6 +29,24 @@ "description": "Question for users to respond to.", "inputMethod": "textfield" }, + { + "property": "gradingFormat", + "title": "Grading format", + "description": "OER Schema: grading format for this assessment (e.g. points, letter, percent, completion).", + "inputMethod": "textfield" + }, + { + "property": "hasLearningObjective", + "title": "Learning objective", + "description": "OER Schema: learning objective associated with this assessment.", + "inputMethod": "textfield" + }, + { + "property": "forCourse", + "title": "Course", + "description": "OER Schema: course this assessment is intended for.", + "inputMethod": "textfield" + }, { "property": "answers", "title": "Answers", From 14e5e2467247d1454169ef4e4167c106d7ab8f14 Mon Sep 17 00:00:00 2001 From: btopro Date: Mon, 3 Aug 2026 14:16:08 -0400 Subject: [PATCH 02/10] Wire rubric/assessment elements to emit OER Schema v1.2.0 metadata Phase 2 OER Schema migration: add SchemaBehaviors mixin and emit RDFa typeof/property attributes for the Rubric family and Assessment class across four pedagogical elements. editable-table: opt-in rubricMode emits typeof=oer:Rubric on the host (via setAttribute), oer:rubricType as a meta tag, a RubricScale with hasLevel children for each header column, a RubricCriterion for each body row with criterionWeight, and a RubricLevel (levelOrdinal + levelPoints) for each criterion/level cell. haxProperties.json gains a rubricMode boolean toggle and a rubricType select. star-rating: opt-in rubricScaleMode emits typeof=oer:RubricScale on the wrapper div and typeof=oer:RubricLevel + property=oer:hasLevel on each star icon, with levelOrdinal and levelPoints meta children. Adds a static haxProperties() method with a rubricScaleMode toggle. grade-book: SchemaBehaviors added to the class chain; emits typeof=oer:Assessment per database assignment with gradingFormat (points) and forCourse (when set) via a hidden RDFa block. Adds a forCourse property and a static haxProperties() method with a forCourse textfield. self-check: typeof=oer:Assessment set on the host in firstUpdated (matching the QuestionElement idiom); hasLearningObjective and assessing properties emitted as meta tags in render() when linked. haxProperties.json gains hasLearningObjective and assessing textfields. Co-Authored-By: Oz --- elements/editable-table/editable-table.js | 110 ++++++++++++++++- .../lib/editable-table.haxProperties.json | 19 +++ elements/grade-book/grade-book.js | 85 ++++++++++++- .../lib/self-check.haxProperties.json | 12 ++ elements/self-check/self-check.js | 31 +++++ elements/star-rating/star-rating.js | 112 +++++++++++++++++- 6 files changed, 361 insertions(+), 8 deletions(-) diff --git a/elements/editable-table/editable-table.js b/elements/editable-table/editable-table.js index 240856016b..9efe873030 100644 --- a/elements/editable-table/editable-table.js +++ b/elements/editable-table/editable-table.js @@ -2,8 +2,9 @@ * Copyright 2018 The Pennsylvania State University * @license Apache-2.0, see License.md for full text. */ -import { html, css } from "lit"; +import { html, css, nothing } from "lit"; import { DDD } from "@haxtheweb/d-d-d/d-d-d.js"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import { editBehaviors, editableTableStyles, @@ -78,7 +79,7 @@ Custom property | Description | Default * @extends ResponsiveUtilityBehaviors * @extends editableTableStyles */ -class EditableTable extends editBehaviors(DDD) { +class EditableTable extends editBehaviors(SchemaBehaviors(DDD)) { static get styles() { return [ css` @@ -95,6 +96,7 @@ class EditableTable extends editBehaviors(DDD) { } render() { return html` + ${this.rubricMode ? this.renderRubricSchema() : nothing} element receives typeof="oer:Rubric" via + * setAttribute in firstUpdated/updated. This hidden block emits the + * rubricType property, a RubricScale with hasLevel children for each + * performance-level column, a RubricCriterion for each body row, and a + * RubricLevel (levelOrdinal/levelPoints) for each criterion/level cell. + */ + renderRubricSchema() { + const headerRow = this.thead.length ? this.thead[0] : []; + const startCol = this.rowHeader ? 1 : 0; + return html` + + `; + } + static get tag() { return "editable-table"; } @@ -196,6 +261,8 @@ class EditableTable extends editBehaviors(DDD) { super(); //this.haxUIElement = true; this.editMode = false; + this.rubricMode = false; + this.rubricType = ""; } static get properties() { return { @@ -208,8 +275,47 @@ class EditableTable extends editBehaviors(DDD) { attribute: "edit-mode", reflect: true, }, + /** + * Opt-in: interpret this table as an OER Schema Rubric so that + * Rubric/RubricScale/RubricCriterion/RubricLevel metadata is emitted. + */ + rubricMode: { + type: Boolean, + attribute: "rubric-mode", + reflect: true, + }, + /** + * Rubric style (analytic, holistic, single-point, checklist) emitted + * as oer:rubricType when rubricMode is true. + */ + rubricType: { + type: String, + attribute: "rubric-type", + }, }; } + firstUpdated(changedProperties) { + if (super.firstUpdated) { + super.firstUpdated(changedProperties); + } + if (this.rubricMode) { + this.setAttribute("typeof", "oer:Rubric"); + } + } + updated(changedProperties) { + if (super.updated) { + super.updated(changedProperties); + } + changedProperties.forEach((oldValue, propName) => { + if (propName === "rubricMode") { + if (this.rubricMode) { + this.setAttribute("typeof", "oer:Rubric"); + } else { + this.removeAttribute("typeof"); + } + } + }); + } get display() { return ( diff --git a/elements/editable-table/lib/editable-table.haxProperties.json b/elements/editable-table/lib/editable-table.haxProperties.json index d15b0ba748..c81c58a0bd 100644 --- a/elements/editable-table/lib/editable-table.haxProperties.json +++ b/elements/editable-table/lib/editable-table.haxProperties.json @@ -68,6 +68,25 @@ "title": "Responsive", "description": "Adjust table layout on small screens.", "inputMethod": "boolean" + }, + { + "property": "rubricMode", + "title": "Rubric Mode", + "description": "Interpret this table as an OER Schema Rubric so that Rubric, RubricScale, RubricCriterion, and RubricLevel metadata is emitted.", + "inputMethod": "boolean" + }, + { + "property": "rubricType", + "title": "Rubric Type", + "description": "Rubric style emitted as oer:rubricType (only meaningful when Rubric Mode is on).", + "inputMethod": "select", + "options": { + "": "(none)", + "analytic": "Analytic", + "holistic": "Holistic", + "single-point": "Single Point", + "checklist": "Checklist" + } } ], "advanced": [ diff --git a/elements/grade-book/grade-book.js b/elements/grade-book/grade-book.js index b821a87aa9..389985fd15 100644 --- a/elements/grade-book/grade-book.js +++ b/elements/grade-book/grade-book.js @@ -4,6 +4,7 @@ */ import { html, css, render, nothing } from "lit"; import { SimpleColors } from "@haxtheweb/simple-colors/simple-colors.js"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; import { validURL, @@ -37,7 +38,7 @@ import { XLSXFileSystemBrokerSingleton } from "@haxtheweb/file-system-broker/lib * @demo demo/index.html Grade book * @element grade-book */ -class GradeBook extends UIRenderPieces(I18NMixin(SimpleColors)) { +class GradeBook extends UIRenderPieces(I18NMixin(SchemaBehaviors(SimpleColors))) { constructor() { super(); this.__disposer = []; @@ -71,6 +72,7 @@ class GradeBook extends UIRenderPieces(I18NMixin(SimpleColors)) { surname: true, email: true, }; + this.forCourse = ""; this.disabled = false; // shows progress indicator as it loads this.loading = false; @@ -578,6 +580,10 @@ class GradeBook extends UIRenderPieces(I18NMixin(SimpleColors)) { assessmentView: { type: Object, attribute: false }, activeGrading: { type: Object, attribute: false }, activeStudentSubmissions: { type: Array }, + /** + * Course reference emitted as oer:forCourse on each Assessment. + */ + forCourse: { type: String, attribute: "for-course" }, }; } async changeStudent(e) { @@ -1356,12 +1362,44 @@ class GradeBook extends UIRenderPieces(I18NMixin(SimpleColors)) { ); } } + /** + * Emit OER Schema Assessment metadata for each graded assignment. + * Each assignment in the database becomes an oer:Assessment resource + * with gradingFormat (points) and forCourse (when set) properties. + */ + renderAssessmentSchema() { + if ( + !this.database || + !this.database.assignments || + !this.database.assignments.length + ) { + return nothing; + } + return html` + + `; + } /** * LitElement render method */ render() { return html` + ${this.renderAssessmentSchema()}
@@ -2123,6 +2161,51 @@ class GradeBook extends UIRenderPieces(I18NMixin(SimpleColors)) { static get tag() { return "grade-book"; } + static get haxProperties() { + return { + canScale: false, + gizmo: { + title: "Grade Book", + description: "A headless gradebook that supports multiple backends with rubrics.", + icon: "icons:assignment", + color: "green", + tags: ["Instructional", "gradebook", "grading", "assessment", "rubric"], + handles: [], + meta: { + author: "HAXTheWeb core team", + }, + }, + settings: { + configure: [ + { + property: "source", + title: "Source", + description: "Data source type (googledocs, url, json, filesystem).", + inputMethod: "select", + options: { + googledocs: "Google Docs", + url: "URL endpoint (JSON)", + json: "JSON data blob", + filesystem: "File system", + }, + }, + { + property: "sourceData", + title: "Source Data", + description: "URL or JSON data for the gradebook.", + inputMethod: "textfield", + }, + { + property: "forCourse", + title: "Course Reference", + description: + "Course identifier emitted as oer:forCourse on each Assessment resource.", + inputMethod: "textfield", + }, + ], + }, + }; + } } globalThis.customElements.define(GradeBook.tag, GradeBook); export { GradeBook }; diff --git a/elements/self-check/lib/self-check.haxProperties.json b/elements/self-check/lib/self-check.haxProperties.json index 091a94c023..9fd9280d99 100644 --- a/elements/self-check/lib/self-check.haxProperties.json +++ b/elements/self-check/lib/self-check.haxProperties.json @@ -74,6 +74,18 @@ "error": "Error", "info": "Info" } + }, + { + "property": "hasLearningObjective", + "title": "Learning Objective", + "description": "Learning objective reference emitted as oer:hasLearningObjective.", + "inputMethod": "textfield" + }, + { + "property": "assessing", + "title": "Assessing", + "description": "Activity reference emitted as oer:assessing.", + "inputMethod": "textfield" } ], "advanced": [ diff --git a/elements/self-check/self-check.js b/elements/self-check/self-check.js index f5d6ec97d4..130f518da1 100644 --- a/elements/self-check/self-check.js +++ b/elements/self-check/self-check.js @@ -42,6 +42,8 @@ class SelfCheck extends I18NMixin(lazyImageLoader(SchemaBehaviors(DDD))) { this.accentColor = "primary"; this.title = "Self-Check"; this.fullWidthImage = false; + this.hasLearningObjective = ""; + this.assessing = ""; this.t = { revealAnswer: "Reveal Answer", close: "Close", @@ -398,6 +400,15 @@ class SelfCheck extends I18NMixin(lazyImageLoader(SchemaBehaviors(DDD))) { } render() { return html` + ${this.hasLearningObjective + ? html`` + : ``} + ${this.assessing + ? html`` + : ``}
${this.image @@ -502,6 +513,12 @@ class SelfCheck extends I18NMixin(lazyImageLoader(SchemaBehaviors(DDD))) { static get tag() { return "self-check"; } + firstUpdated(changedProperties) { + if (super.firstUpdated) { + super.firstUpdated(changedProperties); + } + this.setAttribute("typeof", "oer:Assessment"); + } static get properties() { return { ...super.properties, @@ -559,6 +576,20 @@ class SelfCheck extends I18NMixin(lazyImageLoader(SchemaBehaviors(DDD))) { attribute: "accent-color", reflect: true, }, + /** + * Learning objective reference emitted as oer:hasLearningObjective. + */ + hasLearningObjective: { + type: String, + attribute: "has-learning-objective", + }, + /** + * Activity reference emitted as oer:assessing. + */ + assessing: { + type: String, + attribute: "assessing", + }, }; } /** diff --git a/elements/star-rating/star-rating.js b/elements/star-rating/star-rating.js index 2764b8cf40..028dc8f3c9 100644 --- a/elements/star-rating/star-rating.js +++ b/elements/star-rating/star-rating.js @@ -2,10 +2,11 @@ * Copyright 2021 The Pennsylvania State University * @license Apache-2.0, see License.md for full text. */ -import { html, css } from "lit"; +import { html, css, nothing } from "lit"; import "@haxtheweb/simple-icon/lib/simple-icon-button.js"; import "@haxtheweb/simple-icon/simple-icon.js"; import { SimpleColors } from "@haxtheweb/simple-colors/simple-colors.js"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; /** * `star-rating` * `Rating display widget or button to do rating` @@ -13,11 +14,70 @@ import { SimpleColors } from "@haxtheweb/simple-colors/simple-colors.js"; * @demo demo/index.html * @element star-rating */ -class StarRating extends SimpleColors { +class StarRating extends SchemaBehaviors(SimpleColors) { static get tag() { return "star-rating"; } + static get haxProperties() { + return { + canScale: true, + gizmo: { + title: "Star Rating", + description: "Rating display widget or button to do rating.", + icon: "icons:star", + color: "yellow", + tags: [ + "Instructional", + "rating", + "stars", + "rubric", + "scale", + "feedback", + ], + handles: [], + meta: { + author: "HAXTheWeb core team", + }, + }, + settings: { + configure: [ + { + property: "score", + title: "Score", + description: "Current score value.", + inputMethod: "number", + }, + { + property: "possible", + title: "Possible", + description: "Maximum possible score.", + inputMethod: "number", + }, + { + property: "numStars", + title: "Number of Stars", + description: "Number of stars to display.", + inputMethod: "number", + }, + { + property: "interactive", + title: "Interactive", + description: "Allow user interaction to set the rating.", + inputMethod: "boolean", + }, + { + property: "rubricScaleMode", + title: "Rubric Scale Mode", + description: + "Emit OER Schema RubricScale and RubricLevel metadata (levelOrdinal/levelPoints) for each star.", + inputMethod: "boolean", + }, + ], + }, + }; + } + static get styles() { return [ super.styles, @@ -69,6 +129,14 @@ class StarRating extends SimpleColors { interactive: { type: Boolean, reflect: true }, numStars: { type: Number, attribute: "num-stars" }, _calPercent: { type: Number }, // this value we'll calculate based on changes to score and possible + /** + * Opt-in: emit OER Schema RubricScale + RubricLevel metadata. + */ + rubricScaleMode: { + type: Boolean, + attribute: "rubric-scale-mode", + reflect: true, + }, }; } @@ -82,11 +150,15 @@ class StarRating extends SimpleColors { this.dark = true; this.contrast = 0; this.accentColor = "yellow"; + this.rubricScaleMode = false; } render() { return html` -
+
${this.renderStar(this.numStars, this.interactive)}
@@ -125,7 +197,18 @@ class StarRating extends SimpleColors { contrast="${this.contrast}" class="star" data-value="${Number(count + 1)}" - >`, + typeof="${this.rubricScaleMode ? "oer:RubricLevel" : nothing}" + property="${this.rubricScaleMode ? "oer:hasLevel" : nothing}" + >${this.rubricScaleMode + ? html`` + : nothing}`, ); } else { content.push( @@ -135,7 +218,18 @@ class StarRating extends SimpleColors { ?dark="${this.dark}" contrast="${this.contrast}" class="star" - >`, + typeof="${this.rubricScaleMode ? "oer:RubricLevel" : nothing}" + property="${this.rubricScaleMode ? "oer:hasLevel" : nothing}" + >${this.rubricScaleMode + ? html`` + : nothing}`, ); } count++; @@ -154,6 +248,14 @@ class StarRating extends SimpleColors { }), ); } + /** + * Calculate the points assigned to a given rubric level (1-indexed) + * based on the total possible score spread evenly across all stars. + */ + _rubricLevelPoints(level) { + if (this.numStars === 0) return 0; + return Math.round((this.possible / this.numStars) * level); + } updated(changedProperties) { if (super.updated) { super.updated(changedProperties); From fc08e017d95ac0ec39aecd15e7fbcb0e1a6560e4 Mon Sep 17 00:00:00 2001 From: btopro Date: Mon, 3 Aug 2026 14:16:09 -0400 Subject: [PATCH 03/10] OER Schema v1.2.0: wire citation/license/bibliography elements to emit OER metadata Phase 2 OER wiring for material-citations element group. citation-element: fix mis-classification of typeof oer:Text (a DataType, wrong for a citation) -> oer:ReferencedMaterial. The typeof is now driven by a new overridable typeof property (default oer:ReferencedMaterial) with a textfield in settings.configure so authors can pick a more specific OER class. Emits property=oer:name on the cited work title (with content override for RDFa literal correctness despite href) and property=oer:uri via a meta carrying the source URL. Existing about=${this.relatedResource} RDFa link is preserved. license-element: emit typeof=oer:SupportingMaterial on the .license-body wrapper and property=oer:name / property=oer:uri metas for the title and source. SupportingMaterial subClassOf AssociatedMaterial subClassOf LearningComponent subClassOf Resource, so name/uri are valid. bibliography-builder: add SchemaBehaviors to the class chain and emit typeof=oer:LearningComponent on the list wrapper. Adds a forCourse property (emitted via property=oer:forCourse meta, valid on LearningComponent via Resource) with a forCourse textfield in the external haxProperties.json so authors can associate the bibliography with a Course. bibliography-item (the slotted per-entry element): add SchemaBehaviors to the class chain and emit typeof=oer:ReferencedMaterial on each entry wrapper, property=oer:uri via a meta carrying the entry URL, and property=oer:name on the title element in exportAPA for all three citation types (journal uses a span to avoid italicizing the title; book/web reuse the existing ). ReferencedMaterial subClassOf AssociatedMaterial, so name/uri are valid. All emitted classes/properties verified against the v1.2.0 vocabulary in elements/oer-schema/lib/oerschema.js. No optional chaining, no window references, no build/ubiquity commands run. node --check passes on every edited .js; no lint script or node_modules present in the worktree so lint was skipped per instructions. Co-Authored-By: Oz --- .../bibliography-builder.js | 12 ++++++++--- .../bibliography-builder.haxProperties.json | 7 +++++++ .../lib/bibliography-item.js | 14 ++++++++----- elements/citation-element/citation-element.js | 21 ++++++++++++++++++- elements/license-element/license-element.js | 3 +++ 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/elements/bibliography-builder/bibliography-builder.js b/elements/bibliography-builder/bibliography-builder.js index ca59bc499d..8209d83e22 100644 --- a/elements/bibliography-builder/bibliography-builder.js +++ b/elements/bibliography-builder/bibliography-builder.js @@ -3,6 +3,7 @@ * @license Apache-2.0, see LICENSE for full text. */ import { LitElement, html, css, render } from "lit"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; import "@haxtheweb/simple-modal/simple-modal.js"; @@ -17,7 +18,9 @@ import "./lib/bibliography-item.js" * @demo index.html * @element bibliography-builder */ -export class BibliographyBuilder extends DDDSuper(I18NMixin(LitElement)) { +export class BibliographyBuilder extends SchemaBehaviors( + DDDSuper(I18NMixin(LitElement)), +) { static get tag() { return "bibliography-builder"; @@ -26,6 +29,7 @@ export class BibliographyBuilder extends DDDSuper(I18NMixin(LitElement)) { constructor() { super(); this.title = ""; + this.forCourse = ""; this.t = this.t || {}; this.t = { ...this.t, @@ -49,7 +53,8 @@ export class BibliographyBuilder extends DDDSuper(I18NMixin(LitElement)) { ...super.properties, title: { type: String }, exportMode: { type: String, attribute: "export-mode" }, - citationArr: { type: Array, attribute: "citation-array" } + citationArr: { type: Array, attribute: "citation-array" }, + forCourse: { type: String, attribute: "for-course" } }; } @@ -86,7 +91,8 @@ export class BibliographyBuilder extends DDDSuper(I18NMixin(LitElement)) { // Lit render the HTML render() { return html` -
+
+

${this.t.title}: ${this.title}

diff --git a/elements/bibliography-builder/lib/bibliography-builder.haxProperties.json b/elements/bibliography-builder/lib/bibliography-builder.haxProperties.json index a878d3c260..bb0f66fc6b 100644 --- a/elements/bibliography-builder/lib/bibliography-builder.haxProperties.json +++ b/elements/bibliography-builder/lib/bibliography-builder.haxProperties.json @@ -25,6 +25,13 @@ "title": "Citations", "description": "Citation items in this bibliography", "inputMethod": "code-editor" + }, + { + "property": "forCourse", + "title": "Course", + "description": "Identifier or name of the Course this bibliography is for (OER Schema forCourse).", + "inputMethod": "textfield", + "icon": "link" } ] }, diff --git a/elements/bibliography-builder/lib/bibliography-item.js b/elements/bibliography-builder/lib/bibliography-item.js index 0c1895d751..edbea60c97 100644 --- a/elements/bibliography-builder/lib/bibliography-item.js +++ b/elements/bibliography-builder/lib/bibliography-item.js @@ -3,6 +3,7 @@ * @license Apache-2.0, see LICENSE for full text. */ import { LitElement, html, css, render } from "lit"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; import "@haxtheweb/simple-icon/lib/simple-icons.js"; @@ -15,7 +16,9 @@ import "@haxtheweb/hax-iconset/lib/simple-hax-iconset.js"; * @demo index.html * @element bibliography-item */ -export class BibliographyItem extends DDDSuper(I18NMixin(LitElement)) { +export class BibliographyItem extends SchemaBehaviors( + DDDSuper(I18NMixin(LitElement)), +) { static get tag() { return "bibliography-item"; @@ -89,7 +92,8 @@ export class BibliographyItem extends DDDSuper(I18NMixin(LitElement)) { // Lit render the HTML render() { return html` -
+
+
${this.exportAPA()}
${this.publisher}, ${this._formatSource().getJournalSource("APA")}. ${this.url}`; + return html`${this._formatAuthors()} ${this._formatDate().getPubYear()}. ${this.title}. ${this.publisher}, ${this._formatSource().getJournalSource("APA")}. ${this.url}`; case "book": - return html`${this._formatAuthors()} ${this._formatDate().getPubYear()}. ${this.title} ${this._formatSource().getBookSource("APA")}. ${this.publisher}. ${this.url}` + return html`${this._formatAuthors()} ${this._formatDate().getPubYear()}. ${this.title} ${this._formatSource().getBookSource("APA")}. ${this.publisher}. ${this.url}` case "web": default: - return html`${this._formatAuthors()} ${this._formatDate().getFullPubDate()}. ${this.title}. ${this.publisher}. ${this.url}` + return html`${this._formatAuthors()} ${this._formatDate().getFullPubDate()}. ${this.title}. ${this.publisher}. ${this.url}` } } diff --git a/elements/citation-element/citation-element.js b/elements/citation-element/citation-element.js index 8a3df451b7..c847a6dc71 100644 --- a/elements/citation-element/citation-element.js +++ b/elements/citation-element/citation-element.js @@ -60,9 +60,10 @@ class CitationElement extends SchemaBehaviors(DDDSuper(LitElement)) { + ${this.title} by @@ -111,6 +114,7 @@ class CitationElement extends SchemaBehaviors(DDDSuper(LitElement)) { super(); this.scope = "sibling"; this.source = ""; + this.typeof = "oer:ReferencedMaterial"; } /** * LitElement properties changed @@ -199,6 +203,13 @@ class CitationElement extends SchemaBehaviors(DDDSuper(LitElement)) { license: { type: String, }, + /** + * OER Schema class for the cited work (default oer:ReferencedMaterial). + * Authors can override via the HAX editor to use a more specific OER class. + */ + typeof: { + type: String, + }, }; } /** @@ -360,6 +371,14 @@ class CitationElement extends SchemaBehaviors(DDDSuper(LitElement)) { inputMethod: "textfield", icon: "link", }, + { + property: "typeof", + title: "OER Schema type", + description: + "OER Schema class for this citation. Defaults to oer:ReferencedMaterial; override only with a valid OER class (e.g. oer:SupportingMaterial).", + inputMethod: "textfield", + icon: "code", + }, ], advanced: [], }, diff --git a/elements/license-element/license-element.js b/elements/license-element/license-element.js index 14c3c5f4e5..c7a60e4f1b 100644 --- a/elements/license-element/license-element.js +++ b/elements/license-element/license-element.js @@ -122,9 +122,12 @@ class LicenseElement extends SchemaBehaviors(DDDSuper(LitElement)) { />
+ + ${this.licenseImage ? html` Date: Mon, 3 Aug 2026 14:16:17 -0400 Subject: [PATCH 04/10] [oer-schema] Wire media elements to emit OER Schema metadata Phase 2 OER Schema v1.2.0 migration for media playback elements. - video-player: emit typeof="oer:MediaObject" on host via firstUpdated setAttribute + oer:name (mediaTitle) and oer:uri (source) meta tags at top of render(). SchemaBehaviors already applied in class chain. - audio-player: extends VideoPlayer and does not override render() or firstUpdated(), so inherited MediaObject typeof/name/uri emission applies; no edit required. - a11y-media-player: add SchemaBehaviors import and wrap class chain SchemaBehaviors(I18NMixin(FullscreenBehaviors(DDD))); emit typeof="oer:MediaObject" + oer:name + oer:uri. - media-playlist: add SchemaBehaviors import and wrap class chain; emit typeof="oer:LearningComponent" on host, oer:hasComponent meta per media item, and oer:forCourse meta; add forCourse reactive property + constructor init + forCourse textfield in lib/media-playlist.haxProperties.json settings.configure. Co-Authored-By: Oz --- elements/a11y-media-player/a11y-media-player.js | 8 ++++++-- .../lib/media-playlist.haxProperties.json | 7 +++++++ elements/media-playlist/media-playlist.js | 11 ++++++++++- elements/video-player/video-player.js | 5 ++++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/elements/a11y-media-player/a11y-media-player.js b/elements/a11y-media-player/a11y-media-player.js index f85d07ece4..cc8301f5b4 100644 --- a/elements/a11y-media-player/a11y-media-player.js +++ b/elements/a11y-media-player/a11y-media-player.js @@ -6,6 +6,7 @@ import { LitElement, html, css } from "lit"; import { DDD } from "@haxtheweb/d-d-d/d-d-d.js"; import { FullscreenBehaviors } from "@haxtheweb/fullscreen-behaviors/fullscreen-behaviors.js"; import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import "@haxtheweb/anchor-behaviors/anchor-behaviors.js"; import "@haxtheweb/responsive-utility/responsive-utility.js"; import { normalizeEventPath } from "@haxtheweb/utils/lib/events.js"; @@ -105,7 +106,7 @@ import "./lib/a11y-media-youtube.js"; * @demo ./demo/audio.html audio demo * @demo ./demo/youtube.html YouTube demo */ -class A11yMediaPlayer extends I18NMixin(FullscreenBehaviors(DDD)) { +class A11yMediaPlayer extends SchemaBehaviors(I18NMixin(FullscreenBehaviors(DDD))) { //styles function static get styles() { return [ @@ -931,7 +932,9 @@ class A11yMediaPlayer extends I18NMixin(FullscreenBehaviors(DDD)) { // render function render() { - return html`
+ return html` + +
${this.mediaCaption}
@@ -3507,6 +3510,7 @@ class A11yMediaPlayer extends I18NMixin(FullscreenBehaviors(DDD)) { if (super.firstUpdated) { super.firstUpdated(changedProperties); } + this.setAttribute("typeof", "oer:MediaObject"); this.style.setProperty( "--a11y-media-transcript-max-height", this.height ? "146px" : "unset", diff --git a/elements/media-playlist/lib/media-playlist.haxProperties.json b/elements/media-playlist/lib/media-playlist.haxProperties.json index 89e753d400..c36675acc1 100644 --- a/elements/media-playlist/lib/media-playlist.haxProperties.json +++ b/elements/media-playlist/lib/media-playlist.haxProperties.json @@ -35,6 +35,13 @@ "title": "Active index", "description": "Index of the active item to start on.", "inputMethod": "number" + }, + { + "property": "forCourse", + "title": "Course", + "description": "The Course this playlist is meant for (course identifier or URL).", + "inputMethod": "textfield", + "validationType": "text" } ], "advanced": [], diff --git a/elements/media-playlist/media-playlist.js b/elements/media-playlist/media-playlist.js index 22d76305f3..cf8f107dc3 100644 --- a/elements/media-playlist/media-playlist.js +++ b/elements/media-playlist/media-playlist.js @@ -5,6 +5,7 @@ import { LitElement, html, css } from "lit"; import { DDDSuper } from "@haxtheweb/d-d-d/d-d-d.js"; import { I18NMixin } from "@haxtheweb/i18n-manager/lib/I18NMixin.js"; +import { SchemaBehaviors } from "@haxtheweb/schema-behaviors/schema-behaviors.js"; import { copyToClipboard } from "@haxtheweb/utils/utils.js"; import "@haxtheweb/video-player/video-player.js"; import "@haxtheweb/audio-player/audio-player.js"; @@ -15,7 +16,7 @@ import "@haxtheweb/audio-player/audio-player.js"; * @demo index.html * @element media-playlist */ -export class MediaPlaylist extends DDDSuper(I18NMixin(LitElement)) { +export class MediaPlaylist extends SchemaBehaviors(DDDSuper(I18NMixin(LitElement))) { static get tag() { return "media-playlist"; } @@ -25,6 +26,7 @@ export class MediaPlaylist extends DDDSuper(I18NMixin(LitElement)) { this.edit = false; this.activeIndex = 0; this.mediaItems = []; + this.forCourse = ""; this._playTimeout = null; this.t = this.t || {}; this.t = { @@ -118,6 +120,7 @@ export class MediaPlaylist extends DDDSuper(I18NMixin(LitElement)) { edit: { type: Boolean, reflect: true }, activeIndex: { type: Number, reflect: true, attribute: "active-index" }, mediaItems: { type: Array }, + forCourse: { type: String, attribute: "for-course" }, _haxState: { type: Boolean }, }; } @@ -309,6 +312,11 @@ export class MediaPlaylist extends DDDSuper(I18NMixin(LitElement)) { } const activeItem = this.mediaItems[this.activeIndex]; return html` + ${this.mediaItems.map( + (item) => + html``, + )} +
${activeItem @@ -594,6 +602,7 @@ export class MediaPlaylist extends DDDSuper(I18NMixin(LitElement)) { if (super.firstUpdated) { super.firstUpdated(changedProperties); } + this.setAttribute("typeof", "oer:LearningComponent"); this._updateMediaItems(); const playerArea = this.shadowRoot.querySelector(".player-area"); const playlist = this.shadowRoot.querySelector(".playlist"); diff --git a/elements/video-player/video-player.js b/elements/video-player/video-player.js index e88688eadd..645780240b 100644 --- a/elements/video-player/video-player.js +++ b/elements/video-player/video-player.js @@ -90,7 +90,9 @@ class VideoPlayer extends IntersectionObserverMixin( // render function render() { - return html` ${this.audioDescriptionSource && this.audioDescriptionEnabled + return html` + + ${this.audioDescriptionSource && this.audioDescriptionEnabled ? html`
  • , property=oer:title on the , and a . - outline-player: typeof=oer:TableOfContents on the