diff --git a/package.json b/package.json index 3b376af3d6086c1199dc471ba4e696ba5316fbef..d0f89c87238bd596e908e1bdbd7d9e75cd8f7c40 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 0000000000000000000000000000000000000000..56988ebde4d067ff8ee77f3abba10ced8d27bdc9 --- /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 0000000000000000000000000000000000000000..fccdf6ab90c2149914b7839489b286430cd59bbc --- /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 0000000000000000000000000000000000000000..5a9d85152e53c0eb9a51d0c6081407d259eeaf24 --- /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 0000000000000000000000000000000000000000..bcc47b068719fe0e15772e52f44ca74e7a8dc517 --- /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 0000000000000000000000000000000000000000..30443fb3d4d0f823dba8eb37f2d6eb425f369287 --- /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 0000000000000000000000000000000000000000..7be6a2773204aa38ca4909fa779fd63eac8015a6 --- /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 0000000000000000000000000000000000000000..f7c9d48cef5e3c0913920a52b08adb8c1263a06f --- /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 f1a71ff87826b80aed5f2420eda87cc81d94bccf..a2c1b35b11a733d59e4260119673db5b14165d54 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 00a5136b0602bc2d6ca3e51f99e46fa83d0dbcbe..58b0af70db95f8771fca0fba782d3e3d4c8ab28f 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 0000000000000000000000000000000000000000..989e2edabfa90e3a17c53e24681c8c8033ba399a --- /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>