-
-
Notifications
You must be signed in to change notification settings - Fork 945
Add HTML5 article rendering with embedded content viewers #14548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
8c8df77
f1b41eb
c49ad31
1924145
7b138aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ | |
|
|
||
| from django.core.serializers.json import DjangoJSONEncoder | ||
| from django.utils.safestring import mark_safe | ||
| from le_utils.constants import file_formats | ||
| from le_utils.constants import format_presets | ||
|
|
||
| from kolibri.core.webpack.hooks import WebpackBundleHook | ||
| from kolibri.core.webpack.hooks import WebpackInclusionMixin | ||
|
|
@@ -29,6 +31,30 @@ class ContentRendererHook(WebpackBundleHook, WebpackInclusionMixin): | |
| def presets(self): | ||
| pass | ||
|
|
||
| #: Optional tuple of CSS selectors that this content renderer can handle | ||
| css_selectors = () | ||
|
|
||
| #: Whether to allow object tag handling (defaults to False for sandboxing compatibility) | ||
| allow_object_tag = False | ||
|
|
||
| @classmethod | ||
| def all_css_selectors(cls): | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using css_selectors allows for specific content viewers to only render certain DOM nodes if they have particular attributes present, for example. |
||
| """Get all CSS selectors (auto-generated from presets + custom), cached.""" | ||
| if not hasattr(cls, "_cached_css_selectors"): | ||
| selectors = list(cls.css_selectors) | ||
|
|
||
| if cls.allow_object_tag: | ||
| for preset in cls.presets: | ||
| preset_obj = next( | ||
| x for x in format_presets.PRESETLIST if x.id == preset | ||
| ) | ||
| for fmt in preset_obj.allowed_formats: | ||
| fmt_obj = file_formats.getformat(fmt) | ||
| selectors.append(f'object[type="{fmt_obj.mimetype}"]') | ||
|
|
||
| cls._cached_css_selectors = tuple(sorted(set(selectors))) | ||
| return cls._cached_css_selectors | ||
|
|
||
| @classmethod | ||
| def html(cls): | ||
| tags = [] | ||
|
|
@@ -53,7 +79,11 @@ def template_html(self): | |
| '<template data-viewer="{bundle}">{data}</template>'.format( | ||
| bundle=self.unique_id, | ||
| data=json.dumps( | ||
| {"urls": urls, "presets": self.presets}, | ||
| { | ||
| "urls": urls, | ||
| "presets": self.presets, | ||
| "css_selectors": self.all_css_selectors(), | ||
| }, | ||
| separators=(",", ":"), | ||
| ensure_ascii=False, | ||
| cls=DjangoJSONEncoder, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| from le_utils.constants.file_formats import BLOOMPUB | ||
| from le_utils.constants.file_formats import H5P | ||
| from le_utils.constants.file_formats import HTML5 | ||
| from le_utils.constants.file_formats import HTML5_ARTICLE | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to add this to allow larger media files to be loaded via the zip content backend from kpub files. |
||
| from le_utils.constants.file_formats import PERSEUS | ||
| from whitenoise.responders import StaticFile | ||
|
|
||
|
|
@@ -270,7 +271,7 @@ def get_embedded_file( | |
| return response | ||
|
|
||
|
|
||
| archive_file_types = (HTML5, H5P, BLOOMPUB, BLOOMD, PERSEUS) | ||
| archive_file_types = (HTML5, H5P, BLOOMPUB, BLOOMD, PERSEUS, HTML5_ARTICLE) | ||
| archive_file_extension_match = "|".join(archive_file_types) | ||
|
|
||
| # Allows a base url to be passed in the main | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,28 +6,11 @@ | |
| :style="{ width: iframeWidth }" | ||
| @changeFullscreen="isInFullscreen = $event" | ||
| > | ||
| <div | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had copied this across HTML5, EPUB, PDF, and Bloompub - rule of three more than applied! |
||
| class="fullscreen-header" | ||
| :style="{ backgroundColor: $themePalette.grey.v_200 }" | ||
| > | ||
| <KButton | ||
| :primary="false" | ||
| appearance="flat-button" | ||
| @click="$refs.bloompubViewer.toggleFullscreen()" | ||
| > | ||
| <KIcon | ||
| v-if="isInFullscreen" | ||
| icon="fullscreen_exit" | ||
| class="fs-icon" | ||
| /> | ||
| <KIcon | ||
| v-else | ||
| icon="fullscreen" | ||
| class="fs-icon" | ||
| /> | ||
| {{ fullscreenText }} | ||
| </KButton> | ||
| </div> | ||
| <ViewerToolbar | ||
| :isInFullscreen="isInFullscreen" | ||
| :embedded="embedded" | ||
| @toggleFullscreen="$refs.bloompubViewer.toggleFullscreen()" | ||
| /> | ||
| <div | ||
| class="iframe-container" | ||
| :style="containerStyle" | ||
|
|
@@ -58,29 +41,47 @@ | |
| import urls from 'kolibri/urls'; | ||
| import { now } from 'kolibri/utils/serverClock'; | ||
| import CoreFullscreen from 'kolibri-common/components/CoreFullscreen'; | ||
| import ViewerToolbar from 'kolibri-common/components/ViewerToolbar'; | ||
| import Sandbox from 'kolibri-sandbox'; | ||
| import useContentViewer, { contentViewerProps } from 'kolibri/composables/useContentViewer'; | ||
|
|
||
| import useContentViewer from 'kolibri/composables/useContentViewer'; | ||
|
|
||
| const defaultContentHeight = '500px'; | ||
| const frameTopbarHeight = '37px'; | ||
| const frameTopbarHeight = '48px'; | ||
| export default { | ||
| name: 'BloomPubRendererIndex', | ||
| components: { | ||
| CoreFullscreen, | ||
| ViewerToolbar, | ||
| }, | ||
| setup(props, context) { | ||
| const { defaultFile, forceDurationBasedProgress, reportError } = useContentViewer( | ||
| props, | ||
| context, | ||
| { defaultDuration: 300 }, | ||
| ); | ||
| const { | ||
| defaultFile, | ||
| options, | ||
| lang, | ||
| forceDurationBasedProgress, | ||
| reportError, | ||
| embedded, | ||
| extraFields, | ||
| timeSpent, | ||
| userId, | ||
| userFullName, | ||
| progress, | ||
| } = useContentViewer(context, { defaultDuration: 300 }); | ||
| return { | ||
| defaultFile, | ||
| options, | ||
| lang, | ||
| forceDurationBasedProgress, | ||
| reportError, | ||
| embedded, | ||
| extraFields, | ||
| timeSpent, | ||
| userId, | ||
| userFullName, | ||
| progress, | ||
| }; | ||
| }, | ||
| props: contentViewerProps, | ||
| data() { | ||
| return { | ||
| iframeHeight: (this.options && this.options.height) || defaultContentHeight, | ||
|
|
@@ -95,9 +96,6 @@ | |
| iframeWidth() { | ||
| return (this.options && this.options.width) || 'auto'; | ||
| }, | ||
| fullscreenText() { | ||
| return this.isInFullscreen ? this.$tr('exitFullscreen') : this.$tr('enterFullscreen'); | ||
| }, | ||
| userData() { | ||
| return { | ||
| userId: this.userId, | ||
|
|
@@ -162,18 +160,6 @@ | |
| } | ||
| this.$emit('stopTracking'); | ||
| }, | ||
| $trs: { | ||
| exitFullscreen: { | ||
| message: 'Exit fullscreen', | ||
| context: | ||
| "Learners can use the Esc key or the 'exit fullscreen' button to close the fullscreen view on an html5 app.", | ||
| }, | ||
| enterFullscreen: { | ||
| message: 'Enter fullscreen', | ||
| context: | ||
| 'Learners can use the full screen button in the upper right corner to open an html5 app in fullscreen view.\n', | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| </script> | ||
|
|
@@ -182,18 +168,7 @@ | |
| <style lang="scss" scoped> | ||
|
|
||
| @import '~kolibri-design-system/lib/styles/definitions'; | ||
| $frame-topbar-height: 37px; | ||
|
|
||
| .fullscreen-header { | ||
| text-align: right; | ||
| } | ||
|
|
||
| .fs-icon { | ||
| position: relative; | ||
| top: 8px; | ||
| width: 24px; | ||
| height: 24px; | ||
| } | ||
| $frame-topbar-height: 48px; | ||
|
|
||
| .bloompub-viewer { | ||
| position: relative; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,14 +59,12 @@ | |
| /> | ||
| <ContentViewer | ||
| v-if="currentInteraction" | ||
| :contentNode="exercise" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first of many places where we are just passing the contentNode prop now instead of files, lang etc. |
||
| :itemId="currentLearner.item" | ||
| :assessment="true" | ||
| :allowHints="false" | ||
| :files="exercise.files" | ||
| :answerState="answerState" | ||
| :showCorrectAnswer="showCorrectAnswer" | ||
| :interactive="false" | ||
| :extraFields="exercise.extra_fields" | ||
| /> | ||
| </div> | ||
| </template> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question for reviewers - should I move the entire public API surface into the composable? Just make methods for
startTracking,stopTrackingandupdateProgressand skip the emit altogether?