aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFarbod Zamani <zamani@elan-ev.de>2023-06-14 08:43:35 +0000
committerRon Lucke <lucke@elan-ev.de>2023-06-14 08:43:35 +0000
commit133cf46df6e0eac0590386a0db580c7f312eb496 (patch)
treeb26955b1221ddf9e1dc73099d00d490611e43f22
parent022a1da0418ea5e3068893934d09b06cc974f5ed (diff)
CW: Enhanced DocumentBlock PDF viewer
Closes #2018 Merge request studip/studip!1321
-rw-r--r--package.json1
-rw-r--r--public/assets/images/icons/black/hand.svg1
-rw-r--r--public/assets/images/icons/blue/hand.svg1
-rw-r--r--public/assets/images/icons/green/hand.svg1
-rw-r--r--public/assets/images/icons/grey/hand.svg1
-rw-r--r--public/assets/images/icons/red/hand.svg1
-rw-r--r--public/assets/images/icons/white/hand.svg1
-rw-r--r--public/assets/images/icons/yellow/hand.svg1
-rw-r--r--resources/assets/stylesheets/scss/courseware.scss202
-rw-r--r--resources/vue/components/courseware/CoursewareDocumentBlock.vue681
-rw-r--r--resources/vue/components/courseware/CoursewarePDFTableOfContent.vue30
11 files changed, 777 insertions, 144 deletions
diff --git a/package.json b/package.json
index 3b376af..d0f89c8 100644
--- a/package.json
+++ b/package.json
@@ -122,6 +122,7 @@
"typescript": "^5.0.2",
"vrp-vue-resizable": "1.2.7",
"vue": "^2.6.12",
+ "vue-dragscroll": "^3.0.1",
"vue-gettext": "^2.1.12",
"vue-loader": "^15.9.8",
"vue-router": "^3.5.1",
diff --git a/public/assets/images/icons/black/hand.svg b/public/assets/images/icons/black/hand.svg
new file mode 100644
index 0000000..56988eb
--- /dev/null
+++ b/public/assets/images/icons/black/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#000;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/blue/hand.svg b/public/assets/images/icons/blue/hand.svg
new file mode 100644
index 0000000..fccdf6a
--- /dev/null
+++ b/public/assets/images/icons/blue/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#28497c;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/green/hand.svg b/public/assets/images/icons/green/hand.svg
new file mode 100644
index 0000000..5a9d851
--- /dev/null
+++ b/public/assets/images/icons/green/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#00962d;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/grey/hand.svg b/public/assets/images/icons/grey/hand.svg
new file mode 100644
index 0000000..bcc47b0
--- /dev/null
+++ b/public/assets/images/icons/grey/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#6e6e6e;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/red/hand.svg b/public/assets/images/icons/red/hand.svg
new file mode 100644
index 0000000..30443fb
--- /dev/null
+++ b/public/assets/images/icons/red/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#cb1800;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/white/hand.svg b/public/assets/images/icons/white/hand.svg
new file mode 100644
index 0000000..7be6a27
--- /dev/null
+++ b/public/assets/images/icons/white/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#fff;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/public/assets/images/icons/yellow/hand.svg b/public/assets/images/icons/yellow/hand.svg
new file mode 100644
index 0000000..f7c9d48
--- /dev/null
+++ b/public/assets/images/icons/yellow/hand.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#ffad00;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m50.71,13h-.42c-.82,0-1.59.2-2.29.54v-1.04c0-1.97-1.04-3.69-2.59-4.66,0,0,0,0,0,0-.26-.16-.52-.3-.8-.42-.05-.02-.09-.03-.14-.05-.25-.09-.5-.17-.77-.23-.02,0-.04-.01-.06-.02,0,0-.01,0-.02,0-.32-.07-.65-.1-.99-.11-.03,0-.05,0-.08,0-.02,0-.03,0-.05,0-.3,0-.59.03-.87.08-.08.01-.16.03-.24.05-.22.05-.44.11-.65.18-.08.03-.16.05-.24.08-.27.11-.54.23-.79.38-.73-2.19-2.79-3.76-5.22-3.76-2.74,0-5,2-5.42,4.62-.74-.38-1.56-.62-2.45-.62h-.25c-2.97,0-5.37,2.41-5.37,5.37v19.91l-3.18-6c-.93-1.76-2.74-2.76-4.61-2.76-.82,0-1.65.19-2.42.6-2.55,1.33-3.53,4.45-2.19,6.98l12.17,22.9c2.01,3.99,4.02,4.99,7.04,4.99h17.2c7.18,0,11-5.82,11-12.99v-28.71c0-2.92-2.37-5.29-5.29-5.29Zm-5.71,43h-17.2c-1.32,0-2.05,0-3.46-2.8-.01-.03-.03-.05-.04-.08l-12.16-22.9c-.18-.35-.15-.67-.08-.87.07-.21.22-.5.6-.69.18-.09.37-.14.56-.14.36,0,.82.17,1.07.63l3.18,6c.71,1.33,2.08,2.13,3.53,2.13.32,0,.64-.04.97-.12,1.78-.44,3.03-2.04,3.03-3.88V13.37c0-.76.62-1.37,1.37-1.37h.25c.19,0,.39.06.61.17.04.02.08.03.12.04.39.24.64.67.64,1.16v16.13c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5V9.5c0-.08.01-.16.02-.24,0,0,0,0,0,0,.03-.17.09-.33.17-.48,0-.01.01-.03.02-.04.08-.13.18-.25.29-.35.02-.02.04-.04.07-.05.12-.09.24-.17.38-.23.02,0,.04-.01.07-.02.15-.05.31-.09.48-.09s.32.03.46.08c.03,0,.06.02.08.03.13.05.25.12.36.2.02.01.04.03.06.05.22.18.37.42.45.65.06.16.09.32.09.5v18c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-15c0-.56.31-1.04.76-1.3.1-.06.2-.1.31-.13.02,0,.04-.02.07-.02.12-.03.24-.05.36-.05.83,0,1.5.67,1.5,1.5v19c0,1.38,1.12,2.5,2.5,2.5s2.5-1.12,2.5-2.5v-13.21c0-.5.29-.92.71-1.14,0,0,.02,0,.03-.01.06-.03.12-.05.18-.07.12-.04.24-.07.37-.07h.42c.71,0,1.29.58,1.29,1.29v28.71c0,2.11-.5,9-7,9Z"/></g></g></svg> \ No newline at end of file
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss
index f1a71ff..a2c1b35 100644
--- a/resources/assets/stylesheets/scss/courseware.scss
+++ b/resources/assets/stylesheets/scss/courseware.scss
@@ -3218,77 +3218,169 @@ c a n v a s b l o c k e n d
d o c u m e n t b l o c k
* * * * * * * * * * * * */
.cw-block-document {
- .cw-pdf-header {
+ .cw-pdf-main-container {
+ width: calc(100% - 2px);
+ border: solid thin var(--content-color-40);
+ .cw-block-title {
+ border: none;
+ border-bottom: solid thin var(--content-color-40);
+ }
+ }
+ .cw-pdf-toolbar {
position: relative;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: baseline;
+ align-content: space-around;
+ background-color: var(--content-color-20);
+ padding: 4px 8px;
- .cw-pdf-button-prev,
- .cw-pdf-button-next {
- position: absolute;
- border: none;
- background-repeat: no-repeat;
- background-color: transparent;
- height: 24px;
- width: 24px;
- margin: 2px 12px;
- cursor: pointer;
+ button {
+ height: 100%;
+ margin: 0 2px 0 0;
+ padding: 4px;
+
+ &.active {
+ background-color: var(--base-color);
+ }
}
- .cw-pdf-button-prev {
- left: 0;
- @include background-icon(arr_1left, clickable, 18);
- &.inactive {
- @include background-icon(arr_1left, inactive, 18);
+ .cw-pdf-toolbar-left {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: baseline;
+ align-content: space-between;
+ width: 33%;
+ }
+ .cw-pdf-toolbar-middle {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ width: 34%;
+
+ .cw-pdf-zoom-buttons {
+ margin-right: 8px;
+
+ button {
+ margin: 0;
+ padding: 4px 0;
+ }
}
}
+ .cw-pdf-toolbar-right {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: baseline;
+ align-content: space-between;
+ position: relative;
+ width: 33%;
+ margin-right: 4px;
+
+ }
+ .cw-pdf-page-nav {
+ margin: 0 4px;
- .cw-pdf-button-next {
- right: 0;
- @include background-icon(arr_1right, clickable, 18);
- &.inactive {
- @include background-icon(arr_1right, inactive, 18);
+ button {
+ margin: 0;
+ padding: 4px 0;
+ }
+ .cw-pdf-page-num {
+ text-align: right;
+ width: 2em;
}
}
- .cw-pdf-download {
- display: inline-block;
- width: 18px;
- height: 18px;
- margin: 0 0.25em;
- border: none;
- cursor: pointer;
- vertical-align: sub;
+ .cw-pdf-search-box {
+ position: absolute;
+ top: 33px;
+ left: 22px;
+ width: auto;
+ background-color: var(--content-color-20);
+ border-top: none;
+ padding: 6px;
+ z-index: 2;
+ line-height: normal;
- background: no-repeat scroll 0 0;
- @include background-icon(download, clickable, 18);
+ .cw-pdf-search-num {
+ margin: 4px 0 0 0;
+ display: block;
+ }
+ .cw-pdf-search-navs {
+ display: inline-block;
+ button {
+ margin: 0;
+ padding: 0;
+ }
+ }
}
}
- .cw-pdf-canvas {
- border: solid thin $content-color-40;
- width: calc(100% - 2px);
- }
- .cw-pdf-downloadbox {
- border: solid thin $content-color-40;
- padding: 0.5em 1em;
+ .cw-pdf-outer-container {
+ position: relative;
+ width: 100%;
- .cw-pdf-file-info {
- @include background-icon(file, clickable, 24);
- display: inline-block;
- background-repeat: no-repeat;
- padding-left: 26px;
- margin: 1em;
- line-height: 24px;
- color: $base-color;
- &.cw-pdf-fileicon-pdf {
- @include background-icon(file-pdf, clickable, 24);
+ .cw-pdf-content {
+ display: flex;
+ flex-direction: row;
+
+ .cw-pdf-sidebar {
+ width: 25%;
+ min-width: 270px;;
+ align-self: stretch;
+ background-color: var(--white);
+ border-right: solid 1px var(--content-color-40);
+
+ ul.cw-pdf-toc-list, ul.cw-pdf-toc-sub-list {
+ padding: 0;
+ list-style: none;
+
+ li {
+ padding: 0.5em 1em;
+ }
+ }
+ ul.cw-pdf-toc-list {
+ margin-top: 1em;
+ }
}
+
+ .cw-pdf-viewer-container {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ cursor: text;
+
+ &.hand-cursor-grab {
+ cursor: grab;
+ &.grabbing {
+ cursor: grabbing;
+ }
+ }
+ &.has-error {
+ display: none;
+ }
+ .page {
+ position: relative;
+ margin: 0 auto;
+ }
+ }
+
}
- .cw-pdf-download-icon {
- float: right;
- @include background-icon(download, clickable, 24);
- height: 24px;
- width: 24px;
- background-repeat: no-repeat;
- margin: 1em;
+
+
+ .cw-pdf-viewer-fake-container {
+ position: absolute;
+ }
+
+
+ .cw-pdf-error-page {
+ overflow: hidden;
+ width: calc(100% - 16px);
+ height: 100%;
+ padding: 8px;
+ display:table;
}
}
}
diff --git a/resources/vue/components/courseware/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/CoursewareDocumentBlock.vue
index 00a5136..58b0af7 100644
--- a/resources/vue/components/courseware/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/CoursewareDocumentBlock.vue
@@ -10,35 +10,183 @@
@closeEdit="initCurrentData"
>
<template #content>
- <div v-if="hasFile" class="cw-pdf-header cw-block-title">
- <button class="cw-pdf-button-prev" :class="{ inactive: pageNum - 1 === 0 }" @click="prevPage" />
- <span class="cw-pdf-title">{{ currentTitle }}</span>
- <a v-if="fileDownloadable" :href="currentUrl" class="cw-pdf-download" download></a>
- <span>
- <translate :translate-params="{pageNum, pageCount}">
- (Seite %{ pageNum } von %{ pageCount })
- </translate>
- </span>
- <button class="cw-pdf-button-next" :class="{ inactive: pageNum === pageCount }" @click="nextPage" />
+ <div class="cw-pdf-main-container">
+ <template v-if="hasFile">
+ <div v-if="currentTitle !== ''" class="cw-block-title">
+ {{ currentTitle }}
+ </div>
+ <div class="cw-pdf-toolbar">
+ <div class="cw-pdf-toolbar-left">
+ <div class="cw-pdf-toc">
+ <button
+ class="undecorated"
+ :class="{active: pdfTOCDisplay}"
+ :title="$gettext('Inhaltsverzeichnis')"
+ :aria-pressed="pdfTOCDisplay ? 'true' : 'false'"
+ @click="toggleTOCViewer"
+ >
+ <studip-icon
+ shape="table-of-contents"
+ :role="pdfTOC.length === 0 ? 'inactive' : pdfTOCDisplay ? 'info_alt' :'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ </div>
+ <div class="cw-pdf-search-toggle-btn">
+ <button
+ class="undecorated"
+ :class="{active: showPdfSearchBox}"
+ :title="$gettext('Suche')"
+ :aria-pressed="showPdfSearchBox ? 'true' : 'false'"
+ @click="togglePdfSearchBox"
+ >
+ <studip-icon
+ shape="search"
+ :role="showPdfSearchBox ? 'info_alt' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ </div>
+ <div class="cw-pdf-search-box" v-show="showPdfSearchBox">
+ <input ref="pdfSearchInput" type="text" v-model="pdfSearch" @change="doSearchInPdf">
+ <div class="cw-pdf-search-navs" v-if="pdfSearchFoundNums > 1">
+ <button class="undecorated" @click="prevPdfSearch" :title="$gettext('Letzte')">
+ <studip-icon
+ shape="arr_1left"
+ :role="pdfSearchFoundSelectedIndex === 0 ? 'inactive' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ <button class="undecorated" @click="nextPdfSearch" :title="$gettext('Nächste')">
+ <studip-icon
+ shape="arr_1right"
+ :role="pdfSearchFoundSelectedIndex === pdfSearchFoundNums - 1 ? 'inactive' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ </div>
+ <span class="cw-pdf-search-num" v-if="pdfSearchFoundNums > 0">
+ {{ (pdfSearchFoundSelectedIndex + 1) }} / {{ pdfSearchFoundNums }} {{ $gettext('Treffer') }}
+ </span>
+ </div>
+ <div class="cw-pdf-page-nav">
+ <button class="undecorated" @click="prevPage" :title="$gettext('Eine Seite zurück')">
+ <studip-icon
+ shape="arr_1up"
+ :role="pageNum - 1 === 0 ? 'inactive' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ <button class="undecorated" @click="nextPage" :title="$gettext('Eine Seite vor')">
+ <studip-icon
+ shape="arr_1down"
+ :role="pageNum === pageCount ? 'inactive' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ <input
+ type="text"
+ ref="pageNumInput"
+ class="cw-pdf-page-num"
+ :aria-label="$gettext('Seite')"
+ :value="pageNum"
+ @change="updatePageNum"
+ >
+ <span>
+ {{ $gettext('von') }} {{ pageCount }}
+ </span>
+ </div>
+ </div>
+ <div class="cw-pdf-toolbar-middle">
+ <div class="cw-pdf-zoom-buttons">
+ <button class="undecorated" @click="zoomIn" :title="$gettext('Vergrößern')">
+ <studip-icon shape="add" :size="18" class="text-bottom" />
+ </button>
+ <button class="undecorated" @click="zoomOut" :title="$gettext('Verkleinern')">
+ <studip-icon shape="remove" :size="18" class="text-bottom" />
+ </button>
+ <select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom">
+ <option v-show="false" :value="currentScale">{{ formattedZoom }}%</option>
+ <option v-for="(value, index) in scaleValues" :key="index" :value="value">{{ value * 100 }}%</option>
+ </select>
+ </div>
+ <div class="cw-pdf-rotate">
+ <button class="undecorated" @click="doRotatePdf" :title="$gettext('Drehen')">
+ <studip-icon shape="rotate-right" :size="18" class="text-bottom" />
+ </button>
+ </div>
+ </div>
+ <div class="cw-pdf-toolbar-right">
+ <div class="cw-pdf-handtool">
+ <button
+ class="undecorated"
+ :class="{active: pdfHandTool}"
+ :title="$gettext('Hand-Werkzeug')"
+ :aria-pressed="pdfHandTool ? 'true' : 'false'"
+ @click="toggleHandTool"
+ >
+ <studip-icon
+ shape="hand"
+ :role="pdfHandTool ? 'info_alt' : 'clickable'"
+ :size="18"
+ class="text-bottom"
+ />
+ </button>
+ </div>
+ <div class="cw-pdf-download">
+ <a v-if="downloadable === 'true'" :href="currentUrl" download :title="$gettext('Speichern')">
+ <studip-icon shape="download" :size="18" class="text-bottom"/>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="cw-pdf-outer-container" ref="outerContainer">
+ <div class="cw-pdf-content">
+ <span class="sr-only" aria-live="polite">{{ srMessage }}</span>
+ <div class="cw-pdf-sidebar" v-show="pdfTOCDisplay">
+ <ul class="cw-pdf-toc-list">
+ <CoursewarePDFTableOfContent v-for="(item, index) in pdfTOC" :item="item" :key="index" @tocPageNav="tocPageNav" />
+ </ul>
+ </div>
+ <div
+ ref="container"
+ class="cw-pdf-viewer-container"
+ :class="{'hand-cursor-grab': pdfHandTool, 'grabbing': pdfGrabbing, 'has-error': pdfError}"
+ v-dragscroll="pdfHandTool"
+ @mousedown="handleMouseDown"
+ @mouseup="handleMouseUp"
+ >
+ <div class="pdfViewer"/>
+ </div>
+ </div>
+ <div v-show="pdfError" class="cw-pdf-error-page">
+ <courseware-companion-box
+ mood="sad"
+ :msgCompanion="$gettext('Es gab einen Fehler. Bitte versuchen Sie es erneut!')"
+ >
+ </courseware-companion-box>
+ </div>
+ <div ref="fakeContainer" class="cw-pdf-viewer-fake-container">
+ <div class="pdfViewer"/>
+ </div>
+ </div>
+ </template>
</div>
- <canvas
- v-if="hasFile"
- ref="pdfcanvas"
- class="cw-pdf-canvas"
- @mousedown="browse = true"
- @mouseup="browse = false"
- @mouseleave="browse = false"
- @mousemove="browsePdf"
- />
</template>
<template v-if="canEdit" #edit>
<form class="default" @submit.prevent="">
<label>
- <translate>Überschrift</translate>
+ {{ $gettext('Überschrift') }}
<input type="text" v-model="currentTitle" />
</label>
<label>
- <translate>Datei</translate>
+ {{ $gettext('Datei') }}
<courseware-file-chooser
v-model="currentFileId"
:isDocument="true"
@@ -46,14 +194,14 @@
/>
</label>
<label>
- <translate>Download-Icon anzeigen</translate>
+ {{ $gettext('Download-Icon anzeigen') }}
<select v-model="currentDownloadable">
- <option value="true"><translate>Ja</translate></option>
- <option value="false"><translate>Nein</translate></option>
+ <option value="true">{{ $gettext('Ja') }}</option>
+ <option value="false">{{ $gettext('Nein') }}</option>
</select>
</label>
<label>
- <translate>Dateityp</translate>
+ {{ $gettext('Dateityp') }}
<select v-model="currentDocType">
<option value="pdf">PDF</option>
</select>
@@ -61,27 +209,48 @@
</form>
</template>
<template #info>
- <p><translate>Informationen zum Dokument-Block</translate></p>
+ <p>{{ $gettext('Informationen zum Dokument-Block') }}</p>
</template>
</courseware-default-block>
</div>
</template>
<script>
+import CoursewareCompanionBox from './CoursewareCompanionBox.vue';
import CoursewareDefaultBlock from './CoursewareDefaultBlock.vue';
import CoursewareFileChooser from './CoursewareFileChooser.vue';
+import CoursewarePDFTableOfContent from './CoursewarePDFTableOfContent.vue';
import { blockMixin } from './block-mixin.js';
-import * as pdfjsLib from 'pdfjs-dist';
+import { getDocument } from 'pdfjs-dist';
+import {
+ DefaultAnnotationLayerFactory,
+ DefaultTextLayerFactory,
+ DefaultXfaLayerFactory,
+ DefaultStructTreeLayerFactory,
+ PDFFindController,
+ PDFLinkService,
+ PDFPageView,
+ PDFViewer,
+ EventBus
+} from 'pdfjs-dist/web/pdf_viewer.js';
+// pdfjsWorker must be imported!
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
+import { dragscroll } from 'vue-dragscroll'
import { mapActions } from 'vuex';
+import 'pdfjs-dist/web/pdf_viewer.css';
export default {
name: 'courseware-document-block',
mixins: [blockMixin],
components: {
+ CoursewareCompanionBox,
CoursewareDefaultBlock,
CoursewareFileChooser,
+ CoursewarePDFTableOfContent
+ },
+ directives: {
+ dragscroll
},
props: {
block: Object,
@@ -96,18 +265,38 @@ export default {
currentDownloadable: '',
currentDocType: '',
- PdfViewer: true,
+ pdfError: false,
+ pdfBasePage: null,
+ pdfPage: null,
+ pdfTextContent: null,
+ pdfHandTool: false,
+ pdfGrabbing: false,
+ pdfTextLayer: null,
+ pdfAnnotationLayer: null,
+ pdfAnnotation: false,
+ pdfRotate: 0,
+ PdfViewer: null,
+ pdfEventBus: null,
+ pdfLinkService: null,
+ pdfFindController: null,
pdfDoc: null,
+ pdfLoadingTask: null,
+ pdfSearch: '',
+ pdfSearchMatchesMapping: [],
+ pdfSearchFoundNums: 0,
+ pdfSearchFoundSelectedIndex: 0,
+ pdfSearchHighlightedList: [],
+ showPdfSearchBox: false,
+ pdfTOC: [],
+ pdfTOCDisplay: false,
pageNum: 1,
- pageRendering: false,
- pageNumPending: null,
pageCount: 0,
- scale: 2,
- canvas: {},
- context: {},
- browse: false,
- browseDirection: [],
- file: null
+ scale: 1,
+ currentScale: 1,
+ scaleValues: [0.5, 1, 1.5, 2, 3, 4],
+ file: null,
+
+ srMessage: ''
};
},
computed: {
@@ -135,20 +324,30 @@ export default {
},
hasFile() {
return this.currentFileId !== '';
- }
+ },
+ formattedZoom () {
+ return Number.parseInt(this.scale * 100, 10);
+ },
},
watch: {
- browseDirection: function (val) {
- if (val.length > 6) {
- this.evaluateBrowseAction();
- }
+ scale(newValue) {
+ let overflow = newValue > 1 ? 'auto' : 'hidden';
+ let container = this.$refs.container;
+ container.style.overflow = overflow;
+ this.currentScale = newValue;
+ },
+ pageNum(newValue) {
+ this.resetPdfViewer();
},
+ showPdfSearchBox() {
+ this.resetPdfSearch();
+ }
},
mounted() {
this.loadFileRefs(this.block.id).then((response) => {
this.file = response[0];
this.currentFile = this.file;
- this.loadPdfViewer();
+ this.initPdfTask();
});
this.initCurrentData();
},
@@ -168,80 +367,380 @@ export default {
this.currentFile = file;
this.currentFileId = file.id;
},
- loadPdfViewer() {
- if (this.PdfViewer && this.currentUrl) {
+ initPdfTask() {
+ if (this.currentUrl) {
let view = this;
- this.canvas = this.$refs.pdfcanvas;
- this.context = this.canvas.getContext('2d');
- pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
- pdfjsLib.getDocument(this.currentUrl).promise.then(function (pdf) {
- view.pdfDoc = pdf;
- view.pageCount = view.pdfDoc.numPages;
- view.renderPage(view.pageNum);
+ view.pdfEventBus = new EventBus();
+ view.pdfLoadingTask = getDocument(this.currentUrl).promise;
+ view.pdfLoadingTask.__PDFDocumentLoadingTask = true;
+ // Link Service
+ view.pdfLinkService = new PDFLinkService({
+ eventBus: view.pdfEventBus,
+ });
+ // Find Controller
+ view.pdfFindController = new PDFFindController({
+ eventBus: view.pdfEventBus,
+ linkService: view.pdfLinkService
+ });
+ // Annotation Layer
+ view.pdfAnnotationLayer = new DefaultAnnotationLayerFactory();
+ // Text Layer
+ view.pdfTextLayer = new DefaultTextLayerFactory();
+ // Load Pdf Document
+ view.loadPdfDocument();
+
+ // Handle search results.
+ view.pdfEventBus.on('updatetextlayermatches', ({source, pageIndex}) => {
+ if (view.pdfViewer.pdfPage._pageIndex == pageIndex) {
+ setTimeout(() => {
+ view.handleSearchMatches();
+ }, 260);
+ }
});
}
},
- renderPage(num) {
- let view = this;
- this.pageRendering = true;
- this.pdfDoc.getPage(num).then(function (page) {
- let viewport = page.getViewport({ scale: view.scale });
- view.canvas.height = viewport.height;
- view.canvas.width = viewport.width;
+ loadPdfDocument() {
+ if (this.pdfLoadingTask) {
+ let view = this;
+ view.pdfLoadingTask.then((pdfDocument) => {
+ view.pdfDoc = pdfDocument;
+ view.pageCount = pdfDocument.numPages;
+ // get table of contents if any.
+ view.loadPdfTOC();
+ // Rendering PDF viewer
+ view.loadPdfViewer();
+ view.pdfLinkService.setDocument(view.pdfDoc, null);
+ view.pdfFindController.setDocument(view.pdfDoc);
+ });
+ }
+ },
+ loadPdfTOC() {
+ if (this.pdfDoc) {
+ let view = this;
+ view.pdfTOC = [];
+ // Get the tree outline
+ view.pdfDoc.getOutline().then((outline) => {
+ if (outline) {
+ view.pdfTOC = outline;
+ }
+ });
+ }
+ },
+ loadPdfViewer() {
+ if (this.pdfDoc) {
+ let view = this;
+ this.pdfError = false;
+ let container = this.$refs.container;
+ let outerContainer = this.$refs.outerContainer;
+ let fakeContainer = this.$refs.fakeContainer;
+ this.pdfDoc.getPage(parseInt(view.pageNum)).then((pdfPage) => {
+ view.pdfPage = pdfPage;
+ // Creating the page view with default parameters.
+ let defaultViewport = pdfPage.getViewport({
+ scale: 1.35,
+ });
- let renderContext = {
- canvasContext: view.context,
- viewport: viewport,
- };
- let renderTask = page.render(renderContext);
+ view.pdfBasePage = new PDFViewer({
+ container: fakeContainer,
+ eventBus: view.pdfEventBus,
+ findController: view.pdfFindController
+ });
- renderTask.promise.then(function () {
- view.pageRendering = false;
- if (view.pageNumPending !== null) {
- view.renderPage(view.pageNumPending);
- view.pageNumPending = null;
+ let pdfPageViewOptions = {
+ container: container,
+ id: view.pageNum,
+ scale: view.scale,
+ defaultViewport: defaultViewport,
+ eventBus: view.pdfEventBus,
+ findController: view.pdfFindController,
+ textHighlighterFactory: view.pdfBasePage,
+ xfaLayerFactory: view.pdfDoc.isPureXfa
+ ? new DefaultXfaLayerFactory()
+ : null,
+ structTreeLayerFactory: new DefaultStructTreeLayerFactory()
+ };
+ if (view.pdfHandTool === false) {
+ pdfPageViewOptions.textLayerFactory = view.pdfTextLayer;
+ pdfPageViewOptions.annotationLayerFactory = view.pdfAnnotationLayer;
+ } else {
+ pdfPageViewOptions.textLayerMode = 0;
+ pdfPageViewOptions.annotationMode = 0;
+ }
+ // Force annotation to be disabled.
+ if (!this.pdfAnnotation && pdfPageViewOptions?.annotationLayerFactory) {
+ pdfPageViewOptions.annotationLayerFactory = null;
+ pdfPageViewOptions.annotationMode = 0;
}
+ view.pdfViewer = new PDFPageView(pdfPageViewOptions);
+ // Associates the actual page with the view, and drawing it
+ view.pdfViewer.setPdfPage(view.pdfPage);
+ // Set LinkService viewer
+ view.pdfLinkService.setViewer(view.pdfViewer);
+ // Set outer container height
+ outerContainer.style.height = container.offsetHeight + 'px';
+ view.renderPage();
+ }).catch(err => {
+ console.log(err);
+ outerContainer.style.minHeight = '350px';
+ view.pdfError = true;
});
- });
+ }
},
- queueRenderPage(num) {
- if (this.pageRendering) {
- this.pageNumPending = num;
- } else {
- this.renderPage(num);
+ renderPage() {
+ if (this.pdfViewer) {
+ this.updatePdfViewer();
+ this.pdfViewer.draw();
+ if (!this.pdfHandTool) {
+ this.pdfViewer.textLayer.findController = this.pdfFindController;
+ }
+ if (this.pdfPage) {
+ this.pdfPage.getTextContent().then((textContent) => {
+ this.pdfTextContent = textContent;
+ });
+ }
+ if (this.pdfSearchMatchesMapping.length) {
+ this.pdfSearchDisplayHandler();
+ }
}
},
+ resetPdfViewer() {
+ this.pdfViewer.destroy();
+ let container = this.$refs.container;
+ while (!container.lastChild.classList.contains('pdfViewer')) {
+ container.removeChild(container.lastChild);
+ }
+ this.loadPdfViewer();
+ },
+ updatePdfViewer(resetScale = false) {
+ let updateArgs = {
+ scale: resetScale ? 1 : this.scale,
+ rotation: this.pdfRotate,
+ };
+ this.pdfViewer.update(updateArgs);
+ },
prevPage() {
if (this.pageNum <= 1) {
return;
}
this.pageNum--;
- this.queueRenderPage(this.pageNum);
},
nextPage() {
if (this.pageNum >= this.pdfDoc.numPages) {
return;
}
this.pageNum++;
- this.queueRenderPage(this.pageNum);
},
- browsePdf(e) {
- if (this.browse) {
- this.browseDirection.push(e.clientX);
+ goToPage(page) {
+ const pageNum = Number.parseInt(page, 10);
+ if (pageNum < 1 || pageNum > this.pdfDoc.numPages) {
+ return;
}
+ this.pageNum = pageNum;
},
- evaluateBrowseAction() {
- this.browse = false;
- let first = this.browseDirection[0];
- let last = this.browseDirection.pop();
- this.browseDirection = [];
- if (first < last) {
- this.prevPage();
+ tocPageNav(dest) {
+ let view = this;
+ let destObj = dest.find((ref) =>
+ typeof ref === "object" && ref !== null &&
+ Number.isInteger(ref.num) && ref.num >= 0 &&
+ Number.isInteger(ref.gen) && ref.gen >= 0);
+ if (destObj) {
+ view.pdfDoc.getPageIndex(destObj).then((pageIndex) => {
+ view.goToPage(pageIndex + 1);
+ });
+ }
+ },
+ updatePageNum() {
+ let pageNumInput = this.$refs.pageNumInput;
+ let value = Number.parseInt(pageNumInput.value, 10);
+ if (Number.isInteger(value) && value > 0 && value <= Number.parseInt(this.pageCount, 10)) {
+ this.pageNum = value;
} else {
- this.nextPage();
+ pageNumInput.value = this.pageNum;
+ }
+ },
+ doRotatePdf() {
+ let rotationDegs = [0, 90, 180, 270, 360];
+ let index = rotationDegs.indexOf(this.pdfRotate);
+ let nextIndex = index + 1 >= rotationDegs.length ? 0 : index + 1;
+ let nextDeg = rotationDegs[nextIndex];
+ this.pdfRotate = nextDeg;
+ this.renderPage();
+ this.updateSrMessage(this.$gettext('gedreht'));
+ },
+ zoomIn() {
+ this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale;
+ this.renderPage();
+ this.updateSrMessage(this.$gettext('vergrößert'));
+ },
+ zoomOut() {
+ this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale;
+ this.renderPage();
+ this.updateSrMessage(this.$gettext('verkleinert'));
+ },
+ updateZoom(e) {
+ const value = e.target.value;
+ if (this.scale === value) {
+ return;
+ }
+ this.scale = value;
+ this.renderPage();
+ this.updateSrMessage(this.$gettext('Zoom Stufe ausgweählt'));
+ },
+ toggleHandTool() {
+ this.pdfHandTool = !this.pdfHandTool;
+ this.resetPdfViewer();
+ this.showPdfSearchBox = false;
+ },
+ handleHandToolDisplay(event) {
+ this.pdfGrabbing = event.type === 'mousedown';
+ },
+ handleMouseDown(e) {
+ this.handleHandToolDisplay(e);
+ },
+ handleMouseUp(e) {
+ this.handleHandToolDisplay(e);
+ },
+ togglePdfSearchBox() {
+ this.showPdfSearchBox = this.pdfHandTool ? false : !this.showPdfSearchBox;
+ if (this.showPdfSearchBox) {
+ this.$nextTick(() => {
+ this.$refs.pdfSearchInput.focus();
+ });
+ }
+ },
+ handleSearchMatches() {
+ let view = this;
+ let allMatches = view.pdfFindController.pageMatches;
+ let totalMatches = 0;
+ let searchSelectIndex = 0;
+ let matchesPageCount = 0;
+ view.pdfSearchMatchesMapping = [];
+ for (let pageIndex = 0; pageIndex < view.pageCount; pageIndex++) {
+ let pageNum = pageIndex + 1;
+ let pageMatches = allMatches[pageIndex];
+ totalMatches += pageMatches.length;
+ if (pageMatches.length) {
+ matchesPageCount++;
+ }
+ for (let i in pageMatches) {
+ let matchIndex = parseInt(i, 10);
+ let mappingObj = {
+ selectIndex: searchSelectIndex,
+ matchIndex: matchIndex,
+ pageNum: pageNum,
+ }
+ view.pdfSearchMatchesMapping.push(mappingObj);
+ searchSelectIndex++;
+ }
+ }
+ // Find next match if there the current page has nothing.
+ if (
+ view.pdfSearchFoundSelectedIndex === 0
+ && view.pdfViewer.pdfPage._pageIndex > 0
+ && matchesPageCount > 0
+ ) {
+ let nextMapped = view.pdfSearchMatchesMapping.filter(map => map.pageNum >= view.pdfViewer.pdfPage._pageIndex + 1);
+ if (nextMapped.length) {
+ view.pdfSearchFoundSelectedIndex = nextMapped[0].selectIndex;
+ }
+ }
+ view.pdfSearchFoundNums = totalMatches;
+ view.pdfSearchDisplayHandler();
+ },
+ doSearchInPdf() {
+ let findObj = {
+ type: '',
+ query: this.pdfSearch,
+ phraseSearch: true,
+ caseSensitive: false,
+ entireWord: true,
+ highlightAll: true,
+ findPrevious: false,
+ matchDiacritics: false
+ };
+ this.pdfEventBus.dispatch('find', findObj);
+ },
+ prevPdfSearch() {
+ if (this.pdfSearchFoundSelectedIndex === 0) {
+ return;
+ }
+ this.pdfSearchFoundSelectedIndex--;
+ this.pdfSearchDisplayHandler();
+ },
+ nextPdfSearch() {
+ if (this.pdfSearchFoundSelectedIndex === this.pdfSearchFoundNums - 1) {
+ return;
+ }
+ this.pdfSearchFoundSelectedIndex++;
+ this.pdfSearchDisplayHandler();
+ },
+ pdfSearchDisplayHandler() {
+ // Go to page based on selected index.
+ let pageMatches = this.pdfSearchMatchesMapping.filter(map =>
+ map.selectIndex === this.pdfSearchFoundSelectedIndex
+ );
+ if (pageMatches.length) {
+ let matchObj = pageMatches[0];
+ // A timeout of > 250ms is needed when page is changed!
+ let highlightRenderTimeout = 0;
+ if (matchObj.pageNum !== this.pageNum) {
+ this.goToPage(matchObj.pageNum);
+ highlightRenderTimeout = 260;
+ }
+ setTimeout(() => {
+ this.setPdfSearchHighlighted();
+ this.scrollToSearchFounds(matchObj.matchIndex);
+ }, highlightRenderTimeout);
+ }
+ },
+ scrollToSearchFounds(matchIndex) {
+ if (this.pdfSearchHighlightedList?.length) {
+ let selectedSpan = this.pdfSearchHighlightedList[matchIndex];
+ if (selectedSpan) {
+ selectedSpan.classList.add('selected');
+ selectedSpan.scrollIntoView({ behavior: 'smooth', block: "center" });
+ }
+ }
+ },
+ setPdfSearchHighlighted() {
+ if (this.pdfViewer?.textLayer?.textDivs) {
+ let textDivs = this.pdfViewer.textLayer.textDivs;
+ let highlightedSpans = [];
+ for (let textSpan of textDivs) {
+ if (textSpan?.children) {
+ let children = [...textSpan.children];
+ for (let child of children) {
+ if (child.nodeName == 'SPAN' && child.classList.contains('highlight')) {
+ child.classList.remove('selected');
+ highlightedSpans.push(child);
+ }
+ }
+ }
+ }
+ // Sort the array based on the top of the span.
+ highlightedSpans.sort((current, next) => {
+ let currentTop = parseInt(current.parentNode.style.top, 10);
+ let nextTop = parseInt(next.parentNode.style.top, 10);
+ return currentTop > nextTop;
+ });
+ this.pdfSearchHighlightedList = highlightedSpans;
+ }
+ },
+ resetPdfSearch() {
+ this.pdfSearch = '';
+ this.pdfSearchFoundNums = 0;
+ this.pdfSearchFoundSelectedIndex = 0;
+ this.pdfSearchHighlightedList = [];
+ this.pdfSearchMatchesMapping = [];
+ this.doSearchInPdf();
+ },
+ toggleTOCViewer() {
+ if (this.pdfTOC.length) {
+ this.pdfTOCDisplay = !this.pdfTOCDisplay;
+ } else {
+ this.pdfTOCDisplay = false;
}
},
-
storeBlock() {
if (this.currentFile === undefined) {
this.companionWarning({
@@ -253,7 +752,7 @@ export default {
attributes.payload = {};
attributes.payload.title = this.currentTitle;
attributes.payload.file_id = this.currentFile.id;
- attributes.payload.downloadable = this.currentDownloadable;
+ attributes.payload.downloadable = this.currentDownloadable.toString();
attributes.payload.doc_type = this.currentDocType;
this.updateBlock({
@@ -263,6 +762,10 @@ export default {
});
}
},
+ updateSrMessage(message) {
+ this.srMessage = '';
+ this.srMessage = message;
+ }
},
};
</script>
diff --git a/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue b/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue
new file mode 100644
index 0000000..989e2ed
--- /dev/null
+++ b/resources/vue/components/courseware/CoursewarePDFTableOfContent.vue
@@ -0,0 +1,30 @@
+<template>
+ <li class="cw-pdf-toc-item">
+ <a href="#" @click.prevent="tocPageNav(item.dest)">
+ <strong v-if="item.bold">{{ item.title }}</strong>
+ <span v-else>{{ item.title }}</span>
+ </a>
+ <template v-if="item.items">
+ <ul class="cw-pdf-toc-sub-list">
+ <courseware-pdf-toc-item v-for="(item, index) in item.items" :item="item" :key="index" @tocPageNav="tocPageNav"></courseware-pdf-toc-item>
+ </ul>
+ </template>
+ </li>
+</template>
+
+<script>
+export default {
+ name: 'courseware-pdf-toc-item',
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ tocPageNav(dest) {
+ this.$emit('tocPageNav', dest);
+ }
+ },
+}
+</script>