From 087d00b85915584af611fc433a7c21b8885052ee Mon Sep 17 00:00:00 2001 From: Abinesh LMSACE Date: Fri, 8 Nov 2024 21:04:50 +0530 Subject: [PATCH 1/6] Free initial source added. - CD-163 --- .github/workflows/moodle-ci.yml | 114 + .gitignore | 50 + amd/build/anime.min.js | 3 + amd/build/anime.min.js.map | 1 + amd/build/editor.min.js | 3 + amd/build/editor.min.js.map | 1 + amd/build/elements.min.js | 3 + amd/build/elements.min.js.map | 1 + amd/src/anime.js | 8 + amd/src/editor.js | 261 ++ amd/src/elements.js | 341 ++ ...up_contentdesigner_activity_task.class.php | 58 + .../backup_contentdesigner_stepslib.php | 96 + ...re_contentdesigner_activity_task.class.php | 67 + .../restore_contentdesigner_stepslib.php | 168 + classes/editor.php | 475 +++ classes/elements.php | 704 ++++ .../course_module_instance_list_viewed.php | 38 + classes/event/course_module_viewed.php | 52 + classes/form/general_element_form.php | 205 + classes/output/renderer.php | 134 + classes/plugininfo/element.php | 106 + .../contentdesignerelements_provider.php | 40 + classes/privacy/provider.php | 359 ++ db/access.php | 58 + db/install.xml | 75 + db/subplugins.json | 5 + editor.php | 65 + element.php | 115 + element/chapter/amd/build/chapter.min.js | 3 + element/chapter/amd/build/chapter.min.js.map | 1 + element/chapter/amd/src/chapter.js | 88 + ...backup_element_chapter_subplugin.class.php | 71 + ...estore_element_chapter_subplugin.class.php | 66 + element/chapter/classes/element.php | 546 +++ element/chapter/classes/external.php | 87 + element/chapter/classes/privacy/provider.php | 188 + element/chapter/db/install.php | 34 + element/chapter/db/install.xml | 37 + element/chapter/db/services.php | 37 + element/chapter/db/upgrade.php | 51 + element/chapter/lang/en/element_chapter.php | 33 + element/chapter/lib.php | 37 + .../chapter/templates/progressbar.mustache | 39 + .../tests/behat/chapter_visibility.feature | 41 + element/chapter/version.php | 29 + element/h5p/amd/build/h5p.min.js | 3 + element/h5p/amd/build/h5p.min.js.map | 1 + element/h5p/amd/src/h5p.js | 163 + .../backup_element_h5p_subplugin.class.php | 76 + .../restore_element_h5p_subplugin.class.php | 65 + element/h5p/classes/element.php | 202 + element/h5p/classes/external.php | 115 + element/h5p/classes/privacy/provider.php | 194 + element/h5p/db/access.php | 37 + element/h5p/db/install.php | 34 + element/h5p/db/install.xml | 41 + element/h5p/db/services.php | 37 + element/h5p/lang/en/element_h5p.php | 37 + .../h5p/tests/behat/h5p_visibility.feature | 54 + element/h5p/version.php | 29 + ...backup_element_heading_subplugin.class.php | 55 + ...estore_element_heading_subplugin.class.php | 64 + element/heading/classes/element.php | 140 + element/heading/db/install.php | 34 + element/heading/db/install.xml | 27 + element/heading/lang/en/element_heading.php | 30 + .../tests/behat/header_visibility.feature | 57 + element/heading/version.php | 29 + .../backup_element_outro_subplugin.class.php | 61 + .../restore_element_outro_subplugin.class.php | 74 + element/outro/classes/element.php | 361 ++ element/outro/db/install.php | 34 + element/outro/db/install.xml | 30 + element/outro/db/upgrade.php | 68 + element/outro/lang/en/element_outro.php | 27 + element/outro/tests/behat/assets/c1.jpg | Bin 0 -> 28539 bytes .../outro/tests/behat/behat_element_outro.php | 54 + .../tests/behat/outro_visibility.feature | 42 + element/outro/version.php | 29 + ...ckup_element_paragraph_subplugin.class.php | 56 + ...tore_element_paragraph_subplugin.class.php | 67 + element/paragraph/classes/element.php | 121 + element/paragraph/db/install.php | 34 + element/paragraph/db/install.xml | 24 + .../paragraph/lang/en/element_paragraph.php | 29 + .../tests/behat/paragraph_visibility.feature | 50 + element/paragraph/version.php | 29 + ...ackup_element_richtext_subplugin.class.php | 59 + ...store_element_richtext_subplugin.class.php | 73 + element/richtext/classes/element.php | 203 + element/richtext/db/install.php | 34 + element/richtext/db/install.xml | 23 + element/richtext/lang/en/element_richtext.php | 28 + .../tests/behat/richtext_visibility.feature | 36 + element/richtext/version.php | 29 + index.php | 99 + lang/en/contentdesigner.php | 202 + lib.php | 498 +++ mod_form.php | 62 + pix/icon.png | Bin 0 -> 859 bytes pix/icon.svg | 17 + pix/monologo.png | Bin 0 -> 859 bytes pix/monologo.svg | 17 + settings.php | 50 + style/animate.css | 3447 +++++++++++++++++ styles.css | 313 ++ templates/chapter.mustache | 150 + templates/content.mustache | 258 ++ templates/editor.mustache | 173 + templates/elementbox.mustache | 145 + tests/behat/element_appearance.feature | 108 + tests/behat/element_generalsettings.feature | 62 + tests/generator/lib.php | 51 + tests/generator_test.php | 52 + tests/lib_test.php | 189 + version.php | 29 + view.php | 69 + 118 files changed, 14184 insertions(+) create mode 100644 .github/workflows/moodle-ci.yml create mode 100644 .gitignore create mode 100644 amd/build/anime.min.js create mode 100644 amd/build/anime.min.js.map create mode 100644 amd/build/editor.min.js create mode 100644 amd/build/editor.min.js.map create mode 100644 amd/build/elements.min.js create mode 100644 amd/build/elements.min.js.map create mode 100644 amd/src/anime.js create mode 100644 amd/src/editor.js create mode 100644 amd/src/elements.js create mode 100644 backup/moodle2/backup_contentdesigner_activity_task.class.php create mode 100644 backup/moodle2/backup_contentdesigner_stepslib.php create mode 100644 backup/moodle2/restore_contentdesigner_activity_task.class.php create mode 100644 backup/moodle2/restore_contentdesigner_stepslib.php create mode 100644 classes/editor.php create mode 100644 classes/elements.php create mode 100644 classes/event/course_module_instance_list_viewed.php create mode 100644 classes/event/course_module_viewed.php create mode 100644 classes/form/general_element_form.php create mode 100644 classes/output/renderer.php create mode 100644 classes/plugininfo/element.php create mode 100644 classes/privacy/contentdesignerelements_provider.php create mode 100644 classes/privacy/provider.php create mode 100644 db/access.php create mode 100644 db/install.xml create mode 100644 db/subplugins.json create mode 100644 editor.php create mode 100644 element.php create mode 100644 element/chapter/amd/build/chapter.min.js create mode 100644 element/chapter/amd/build/chapter.min.js.map create mode 100644 element/chapter/amd/src/chapter.js create mode 100644 element/chapter/backup/moodle2/backup_element_chapter_subplugin.class.php create mode 100644 element/chapter/backup/moodle2/restore_element_chapter_subplugin.class.php create mode 100644 element/chapter/classes/element.php create mode 100644 element/chapter/classes/external.php create mode 100644 element/chapter/classes/privacy/provider.php create mode 100644 element/chapter/db/install.php create mode 100644 element/chapter/db/install.xml create mode 100644 element/chapter/db/services.php create mode 100644 element/chapter/db/upgrade.php create mode 100644 element/chapter/lang/en/element_chapter.php create mode 100644 element/chapter/lib.php create mode 100644 element/chapter/templates/progressbar.mustache create mode 100644 element/chapter/tests/behat/chapter_visibility.feature create mode 100644 element/chapter/version.php create mode 100644 element/h5p/amd/build/h5p.min.js create mode 100644 element/h5p/amd/build/h5p.min.js.map create mode 100644 element/h5p/amd/src/h5p.js create mode 100644 element/h5p/backup/moodle2/backup_element_h5p_subplugin.class.php create mode 100644 element/h5p/backup/moodle2/restore_element_h5p_subplugin.class.php create mode 100644 element/h5p/classes/element.php create mode 100644 element/h5p/classes/external.php create mode 100644 element/h5p/classes/privacy/provider.php create mode 100644 element/h5p/db/access.php create mode 100644 element/h5p/db/install.php create mode 100644 element/h5p/db/install.xml create mode 100644 element/h5p/db/services.php create mode 100644 element/h5p/lang/en/element_h5p.php create mode 100644 element/h5p/tests/behat/h5p_visibility.feature create mode 100644 element/h5p/version.php create mode 100644 element/heading/backup/moodle2/backup_element_heading_subplugin.class.php create mode 100644 element/heading/backup/moodle2/restore_element_heading_subplugin.class.php create mode 100644 element/heading/classes/element.php create mode 100644 element/heading/db/install.php create mode 100644 element/heading/db/install.xml create mode 100644 element/heading/lang/en/element_heading.php create mode 100644 element/heading/tests/behat/header_visibility.feature create mode 100644 element/heading/version.php create mode 100644 element/outro/backup/moodle2/backup_element_outro_subplugin.class.php create mode 100644 element/outro/backup/moodle2/restore_element_outro_subplugin.class.php create mode 100644 element/outro/classes/element.php create mode 100644 element/outro/db/install.php create mode 100644 element/outro/db/install.xml create mode 100644 element/outro/db/upgrade.php create mode 100644 element/outro/lang/en/element_outro.php create mode 100644 element/outro/tests/behat/assets/c1.jpg create mode 100644 element/outro/tests/behat/behat_element_outro.php create mode 100644 element/outro/tests/behat/outro_visibility.feature create mode 100644 element/outro/version.php create mode 100644 element/paragraph/backup/moodle2/backup_element_paragraph_subplugin.class.php create mode 100644 element/paragraph/backup/moodle2/restore_element_paragraph_subplugin.class.php create mode 100644 element/paragraph/classes/element.php create mode 100644 element/paragraph/db/install.php create mode 100644 element/paragraph/db/install.xml create mode 100644 element/paragraph/lang/en/element_paragraph.php create mode 100644 element/paragraph/tests/behat/paragraph_visibility.feature create mode 100644 element/paragraph/version.php create mode 100644 element/richtext/backup/moodle2/backup_element_richtext_subplugin.class.php create mode 100644 element/richtext/backup/moodle2/restore_element_richtext_subplugin.class.php create mode 100644 element/richtext/classes/element.php create mode 100644 element/richtext/db/install.php create mode 100644 element/richtext/db/install.xml create mode 100644 element/richtext/lang/en/element_richtext.php create mode 100644 element/richtext/tests/behat/richtext_visibility.feature create mode 100644 element/richtext/version.php create mode 100644 index.php create mode 100644 lang/en/contentdesigner.php create mode 100644 lib.php create mode 100644 mod_form.php create mode 100644 pix/icon.png create mode 100644 pix/icon.svg create mode 100644 pix/monologo.png create mode 100644 pix/monologo.svg create mode 100644 settings.php create mode 100644 style/animate.css create mode 100644 styles.css create mode 100644 templates/chapter.mustache create mode 100644 templates/content.mustache create mode 100644 templates/editor.mustache create mode 100644 templates/elementbox.mustache create mode 100644 tests/behat/element_appearance.feature create mode 100644 tests/behat/element_generalsettings.feature create mode 100644 tests/generator/lib.php create mode 100644 tests/generator_test.php create mode 100644 tests/lib_test.php create mode 100644 version.php create mode 100644 view.php diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml new file mode 100644 index 0000000..e57198f --- /dev/null +++ b/.github/workflows/moodle-ci.yml @@ -0,0 +1,114 @@ +name: Moodle Plugin CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-18.04 + + services: + postgres: + image: postgres:12.7 + env: + POSTGRES_USER: 'postgres' + POSTGRES_HOST_AUTH_METHOD: 'trust' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 + mariadb: + image: mariadb:10.5 + env: + MYSQL_USER: 'root' + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 3 + + strategy: + fail-fast: false + matrix: + include: + - php: '8.0' + moodle-branch: 'MOODLE_400_STABLE' + database: 'pgsql' + - php: '8.0' + moodle-branch: 'MOODLE_400_STABLE' + database: 'mariadb' + - php: '7.4' + moodle-branch: 'MOODLE_400_STABLE' + database: 'pgsql' + - php: '7.4' + moodle-branch: 'MOODLE_400_STABLE' + database: 'mariadb' + + steps: + - name: Check out repository code + uses: actions/checkout@v2 + with: + path: plugin + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: max_input_vars=6000 + coverage: none + + - name: Initialise moodle-plugin-ci + run: | + composer create-project -n --no-dev --prefer-dist moodlehq/moodle-plugin-ci ci ^3 + echo $(cd ci/bin; pwd) >> $GITHUB_PATH + echo $(cd ci/vendor/bin; pwd) >> $GITHUB_PATH + sudo locale-gen en_AU.UTF-8 + + - name: Install moodle-plugin-ci + run: | + moodle-plugin-ci install --plugin ./plugin --db-host=127.0.0.1 + env: + DB: ${{ matrix.database }} + MOODLE_BRANCH: ${{ matrix.moodle-branch }} + IGNORE_NAMES: 'styles.css,section.mustache,section_info.mustache,module_layout_cards.mustache,module_layout_default.mustache,module_layout_list.mustache,styles.css' + + - name: PHP Lint + if: ${{ always() }} + run: moodle-plugin-ci phplint + + - name: PHP Copy/Paste Detector + if: ${{ always() }} + run: moodle-plugin-ci phpcpd + + - name: PHP Mess Detector + if: ${{ always() }} + run: moodle-plugin-ci phpmd + + - name: Moodle Code Checker + if: ${{ always() }} + run: moodle-plugin-ci codechecker --max-warnings 0 + + - name: Moodle PHPDoc Checker + if: ${{ always() }} + run: moodle-plugin-ci phpdoc + + - name: Validating + if: ${{ always() }} + run: moodle-plugin-ci validate + + - name: Check upgrade savepoints + if: ${{ always() }} + run: moodle-plugin-ci savepoints + + - name: Mustache Lint + if: ${{ always() }} + run: moodle-plugin-ci mustache + + - name: Grunt + if: ${{ always() }} + run: moodle-plugin-ci grunt + + - name: PHPUnit tests + if: ${{ always() }} + run: moodle-plugin-ci phpunit + + - name: Behat features + if: ${{ always() }} + run: moodle-plugin-ci behat --profile chrome diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b24d71e --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + diff --git a/amd/build/anime.min.js b/amd/build/anime.min.js new file mode 100644 index 0000000..b32e937 --- /dev/null +++ b/amd/build/anime.min.js @@ -0,0 +1,3 @@ +var n,e;n=window,e=function(){var n={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},e={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},t=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective","matrix","matrix3d"],r={CSS:{},springs:{}};function a(n,e,t){return Math.min(Math.max(n,e),t)}function o(n,e){return n.indexOf(e)>-1}function u(n,e){return n.apply(null,e)}var i={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),"Object")},pth:function(n){return i.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||i.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},nil:function(n){return i.und(n)||null===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return i.hex(n)||i.rgb(n)||i.hsl(n)},key:function(t){return!n.hasOwnProperty(t)&&!e.hasOwnProperty(t)&&"targets"!==t&&"keyframes"!==t}};function c(n){var e=/\(([^)]+)\)/.exec(n);return e?e[1].split(",").map((function(n){return parseFloat(n)})):[]}function s(n,e){var t=c(n),o=a(i.und(t[0])?1:t[0],.1,100),u=a(i.und(t[1])?100:t[1],.1,100),s=a(i.und(t[2])?10:t[2],.1,100),f=a(i.und(t[3])?0:t[3],.1,100),l=Math.sqrt(u/o),d=s/(2*Math.sqrt(u*o)),p=d<1?l*Math.sqrt(1-d*d):0,h=d<1?(d*l-f)/p:-f+l;function g(n){var t=e?e*n/1e3:n;return t=d<1?Math.exp(-t*d*l)*(1*Math.cos(p*t)+h*Math.sin(p*t)):(1+h*t)*Math.exp(-t*l),0===n||1===n?n:1-t}return e?g:function(){var e=r.springs[n];if(e)return e;for(var t=0,a=0;;)if(1===g(t+=1/6)){if(++a>=16)break}else a=0;var o=t*(1/6)*1e3;return r.springs[n]=o,o}}function f(n){return void 0===n&&(n=10),function(e){return Math.ceil(a(e,1e-6,1)*n)*(1/n)}}var l,d,p=function(){var e=.1;function t(n,e){return 1-3*e+3*n}function r(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((t(e,o)*n+r(e,o))*n+a(e))*n}function u(n,e,o){return 3*t(e,o)*n*n+2*r(e,o)*n+a(e)}return function(t,r,a,i){if(0<=t&&t<=1&&0<=a&&a<=1){var c=new Float32Array(11);if(t!==r||a!==i)for(var s=0;s<11;++s)c[s]=o(s*e,t,a);return function(n){return t===r&&a===i||0===n||1===n?n:o(f(n),r,i)}}function f(r){for(var i=0,s=1;10!==s&&c[s]<=r;++s)i+=e;var l=i+(r-c[--s])/(c[s+1]-c[s])*e,d=u(l,t,a);return d>=.001?function(n,e,t,r){for(var a=0;a<4;++a){var i=u(e,t,r);if(0===i)return e;e-=(o(e,t,r)-n)/i}return e}(r,l,t,a):0===d?l:function(n,e,t,r,a){for(var u,i,c=0;(u=o(i=e+(t-e)/2,r,a)-n)>0?t=i:e=i,Math.abs(u)>1e-7&&++c<10;);return i}(r,i,i+e,t,a)}}}(),v=(l={linear:function(){return function(n){return n}}},d={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,t=4;n<((e=Math.pow(2,--t))-1)/11;);return 1/Math.pow(4,3-t)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var t=a(n,1,10),r=a(e,.1,2);return function(n){return 0===n||1===n?n:-t*Math.pow(2,10*(n-1))*Math.sin((n-1-r/(2*Math.PI)*Math.asin(1/t))*(2*Math.PI)/r)}}},["Quad","Cubic","Quart","Quint","Expo"].forEach((function(n,e){d[n]=function(){return function(n){return Math.pow(n,e+2)}}})),Object.keys(d).forEach((function(n){var e=d[n];l["easeIn"+n]=e,l["easeOut"+n]=function(n,t){return function(r){return 1-e(n,t)(1-r)}},l["easeInOut"+n]=function(n,t){return function(r){return r<.5?e(n,t)(2*r)/2:1-e(n,t)(-2*r+2)/2}},l["easeOutIn"+n]=function(n,t){return function(r){return r<.5?(1-e(n,t)(1-2*r))/2:(e(n,t)(2*r-1)+1)/2}}})),l);function h(n,e){if(i.fnc(n))return n;var t=n.split("(")[0],r=v[t],a=c(n);switch(t){case"spring":return s(n,e);case"cubicBezier":return u(p,a);case"steps":return u(f,a);default:return u(r,a)}}function g(n){try{return document.querySelectorAll(n)}catch(n){return}}function m(n,e){for(var t=n.length,r=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o1&&(t-=1),t<1/6?n+6*(e-n)*t:t<.5?e:t<2/3?n+(e-n)*(2/3-t)*6:n}if(0==u)e=t=r=i;else{var f=i<.5?i*(1+u):i+u-i*u,l=2*i-f;e=s(l,f,o+1/3),t=s(l,f,o),r=s(l,f,o-1/3)}return"rgba("+255*e+","+255*t+","+255*r+","+c+")"}(n):void 0;var e,t,r,a}(n);if(/\s/g.test(n))return n;var t=C(n),r=t?n.substr(0,n.length-t.length):n;return e?r+e:r}function L(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function j(n){for(var e,t=n.points,r=0,a=0;a0&&(r+=L(e,o)),e=o}return r}function q(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return o=n,2*Math.PI*I(o,"r");case"rect":return 2*I(a=n,"width")+2*I(a,"height");case"line":return L({x:I(r=n,"x1"),y:I(r,"y1")},{x:I(r,"x2"),y:I(r,"y2")});case"polyline":return j(n);case"polygon":return t=(e=n).points,j(e)+L(t.getItem(t.numberOfItems-1),t.getItem(0))}var e,t,r,a,o}function H(n,e){var t=e||{},r=t.el||function(n){for(var e=n.parentNode;i.svg(e)&&i.svg(e.parentNode);)e=e.parentNode;return e}(n),a=r.getBoundingClientRect(),o=I(r,"viewBox"),u=a.width,c=a.height,s=t.viewBox||(o?o.split(" "):[0,0,u,c]);return{el:r,viewBox:s,x:s[0]/1,y:s[1]/1,w:u,h:c,vW:s[2],vH:s[3]}}function V(n,e,t){function r(t){void 0===t&&(t=0);var r=e+t>=1?e+t:0;return n.el.getPointAtLength(r)}var a=H(n.el,n.svg),o=r(),u=r(-1),i=r(1),c=t?1:a.w/a.vW,s=t?1:a.h/a.vH;switch(n.property){case"x":return(o.x-a.x)*c;case"y":return(o.y-a.y)*s;case"angle":return 180*Math.atan2(i.y-u.y,i.x-u.x)/Math.PI}}function $(n,e){var t=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,r=S(i.pth(n)?n.totalLength:n,e)+"";return{original:r,numbers:r.match(t)?r.match(t).map(Number):[0],strings:i.str(n)||e?r.split(t):[]}}function W(n){return m(n?y(i.arr(n)?n.map(b):b(n)):[],(function(n,e,t){return t.indexOf(n)===e}))}function X(n){var e=W(n);return e.map((function(n,t){return{target:n,id:t,total:e.length,transforms:{list:E(n)}}}))}function Y(n,e){var t=x(e);if(/^spring/.test(t.easing)&&(t.duration=s(t.easing)),i.arr(n)){var r=n.length;2!==r||i.obj(n[0])?i.fnc(e.duration)||(t.duration=e.duration/r):n={value:n}}var a=i.arr(n)?n:[n];return a.map((function(n,t){var r=i.obj(n)&&!i.pth(n)?n:{value:n};return i.und(r.delay)&&(r.delay=t?0:e.delay),i.und(r.endDelay)&&(r.endDelay=t===a.length-1?e.endDelay:0),r})).map((function(n){return k(n,t)}))}function Z(n,e){var t=[],r=e.keyframes;for(var a in r&&(e=k(function(n){for(var e=m(y(n.map((function(n){return Object.keys(n)}))),(function(n){return i.key(n)})).reduce((function(n,e){return n.indexOf(e)<0&&n.push(e),n}),[]),t={},r=function(r){var a=e[r];t[a]=n.map((function(n){var e={};for(var t in n)i.key(t)?t==a&&(e.value=n[t]):e[t]=n[t];return e}))},a=0;a0?requestAnimationFrame(e):void 0}return"undefined"!=typeof document&&document.addEventListener("visibilitychange",(function(){en.suspendWhenDocumentHidden&&(nn()?n=cancelAnimationFrame(n):(K.forEach((function(n){return n._onDocumentVisibility()})),U()))})),function(){n||nn()&&en.suspendWhenDocumentHidden||!(K.length>0)||(n=requestAnimationFrame(e))}}();function nn(){return!!document&&document.hidden}function en(t){void 0===t&&(t={});var r,o=0,u=0,i=0,c=0,s=null;function f(n){var e=window.Promise&&new Promise((function(n){return s=n}));return n.finished=e,e}var l,d,p,v,h,g,y,b,M=(d=w(n,l=t),v=Z(p=w(e,l),l),y=R(g=_(h=X(l.targets),v),p),b=J,J++,k(d,{id:b,children:[],animatables:h,animations:g,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));function x(){var n=M.direction;"alternate"!==n&&(M.direction="normal"!==n?"normal":"reverse"),M.reversed=!M.reversed,r.forEach((function(n){return n.reversed=M.reversed}))}function O(n){return M.reversed?M.duration-n:n}function C(){o=0,u=O(M.currentTime)*(1/en.speed)}function P(n,e){e&&e.seek(n-e.timelineOffset)}function I(n){for(var e=0,t=M.animations,r=t.length;e2||(b=Math.round(b*p)/p)),v.push(b)}var k=d.length;if(k){g=d[0];for(var O=0;O0&&(M.began=!0,D("begin")),!M.loopBegan&&M.currentTime>0&&(M.loopBegan=!0,D("loopBegin")),d<=t&&0!==M.currentTime&&I(0),(d>=l&&M.currentTime!==e||!e)&&I(e),d>t&&d=e&&(u=0,M.remaining&&!0!==M.remaining&&M.remaining--,M.remaining?(o=i,D("loopComplete"),M.loopBegan=!1,"alternate"===M.direction&&x()):(M.paused=!0,M.completed||(M.completed=!0,D("loopComplete"),D("complete"),!M.passThrough&&"Promise"in window&&(s(),f(M)))))}return f(M),M.reset=function(){var n=M.direction;M.passThrough=!1,M.currentTime=0,M.progress=0,M.paused=!0,M.began=!1,M.loopBegan=!1,M.changeBegan=!1,M.completed=!1,M.changeCompleted=!1,M.reversePlayback=!1,M.reversed="reverse"===n,M.remaining=M.loop,r=M.children;for(var e=c=r.length;e--;)M.children[e].reset();(M.reversed&&!0!==M.loop||"alternate"===n&&1===M.loop)&&M.remaining++,I(M.reversed?M.duration:0)},M._onDocumentVisibility=C,M.set=function(n,e){return z(n,e),M},M.tick=function(n){i=n,o||(o=i),B((i+(u-o))*en.speed)},M.seek=function(n){B(O(n))},M.pause=function(){M.paused=!0,C()},M.play=function(){M.paused&&(M.completed&&M.reset(),M.paused=!1,K.push(M),C(),U())},M.reverse=function(){x(),M.completed=!M.reversed,C()},M.restart=function(){M.reset(),M.play()},M.remove=function(n){rn(W(n),M)},M.reset(),M.autoplay&&M.play(),M}function tn(n,e){for(var t=e.length;t--;)M(n,e[t].animatable.target)&&e.splice(t,1)}function rn(n,e){var t=e.animations,r=e.children;tn(n,t);for(var a=r.length;a--;){var o=r[a],u=o.animations;tn(n,u),u.length||o.children.length||r.splice(a,1)}t.length||r.length||e.pause()}return en.version="3.2.1",en.speed=1,en.suspendWhenDocumentHidden=!0,en.running=K,en.remove=function(n){for(var e=W(n),t=K.length;t--;)rn(e,K[t])},en.get=A,en.set=z,en.convertPx=D,en.path=function(n,e){var t=i.str(n)?g(n)[0]:n,r=e||100;return function(n){return{property:n,el:t,svg:H(t),totalLength:q(t)*(r/100)}}},en.setDashoffset=function(n){var e=q(n);return n.setAttribute("stroke-dasharray",e),e},en.stagger=function(n,e){void 0===e&&(e={});var t=e.direction||"normal",r=e.easing?h(e.easing):null,a=e.grid,o=e.axis,u=e.from||0,c="first"===u,s="center"===u,f="last"===u,l=i.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,v=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,i){if(c&&(u=0),s&&(u=(i-1)/2),f&&(u=i-1),!m.length){for(var h=0;h-1&&K.splice(o,1);for(var s=0;s-1}function u(n,e){return n.apply(null,e)}var i={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),\"Object\")},pth:function(n){return i.obj(n)&&n.hasOwnProperty(\"totalLength\")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||i.svg(n)},str:function(n){return\"string\"==typeof n},fnc:function(n){return\"function\"==typeof n},und:function(n){return void 0===n},nil:function(n){return i.und(n)||null===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return i.hex(n)||i.rgb(n)||i.hsl(n)},key:function(t){return!n.hasOwnProperty(t)&&!e.hasOwnProperty(t)&&\"targets\"!==t&&\"keyframes\"!==t}};function c(n){var e=/\\(([^)]+)\\)/.exec(n);return e?e[1].split(\",\").map(function(n){return parseFloat(n)}):[]}function s(n,e){var t=c(n),o=a(i.und(t[0])?1:t[0],.1,100),u=a(i.und(t[1])?100:t[1],.1,100),s=a(i.und(t[2])?10:t[2],.1,100),f=a(i.und(t[3])?0:t[3],.1,100),l=Math.sqrt(u/o),d=s/(2*Math.sqrt(u*o)),p=d<1?l*Math.sqrt(1-d*d):0,v=1,h=d<1?(d*l-f)/p:-f+l;function g(n){var t=e?e*n/1e3:n;return t=d<1?Math.exp(-t*d*l)*(v*Math.cos(p*t)+h*Math.sin(p*t)):(v+h*t)*Math.exp(-t*l),0===n||1===n?n:1-t}return e?g:function(){var e=r.springs[n];if(e)return e;for(var t=0,a=0;;)if(1===g(t+=1/6)){if(++a>=16)break}else a=0;var o=t*(1/6)*1e3;return r.springs[n]=o,o}}function f(n){return void 0===n&&(n=10),function(e){return Math.ceil(a(e,1e-6,1)*n)*(1/n)}}var l,d,p=function(){var n=11,e=1/(n-1);function t(n,e){return 1-3*e+3*n}function r(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((t(e,o)*n+r(e,o))*n+a(e))*n}function u(n,e,o){return 3*t(e,o)*n*n+2*r(e,o)*n+a(e)}return function(t,r,a,i){if(0<=t&&t<=1&&0<=a&&a<=1){var c=new Float32Array(n);if(t!==r||a!==i)for(var s=0;s=.001?function(n,e,t,r){for(var a=0;a<4;++a){var i=u(e,t,r);if(0===i)return e;e-=(o(e,t,r)-n)/i}return e}(r,l,t,a):0===d?l:function(n,e,t,r,a){for(var u,i,c=0;(u=o(i=e+(t-e)/2,r,a)-n)>0?t=i:e=i,Math.abs(u)>1e-7&&++c<10;);return i}(r,i,i+e,t,a)}}}(),v=(l={linear:function(){return function(n){return n}}},d={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,t=4;n<((e=Math.pow(2,--t))-1)/11;);return 1/Math.pow(4,3-t)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var t=a(n,1,10),r=a(e,.1,2);return function(n){return 0===n||1===n?n:-t*Math.pow(2,10*(n-1))*Math.sin((n-1-r/(2*Math.PI)*Math.asin(1/t))*(2*Math.PI)/r)}}},[\"Quad\",\"Cubic\",\"Quart\",\"Quint\",\"Expo\"].forEach(function(n,e){d[n]=function(){return function(n){return Math.pow(n,e+2)}}}),Object.keys(d).forEach(function(n){var e=d[n];l[\"easeIn\"+n]=e,l[\"easeOut\"+n]=function(n,t){return function(r){return 1-e(n,t)(1-r)}},l[\"easeInOut\"+n]=function(n,t){return function(r){return r<.5?e(n,t)(2*r)/2:1-e(n,t)(-2*r+2)/2}},l[\"easeOutIn\"+n]=function(n,t){return function(r){return r<.5?(1-e(n,t)(1-2*r))/2:(e(n,t)(2*r-1)+1)/2}}}),l);function h(n,e){if(i.fnc(n))return n;var t=n.split(\"(\")[0],r=v[t],a=c(n);switch(t){case\"spring\":return s(n,e);case\"cubicBezier\":return u(p,a);case\"steps\":return u(f,a);default:return u(r,a)}}function g(n){try{return document.querySelectorAll(n)}catch(n){return}}function m(n,e){for(var t=n.length,r=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o1&&(t-=1),t<1/6?n+6*(e-n)*t:t<.5?e:t<2/3?n+(e-n)*(2/3-t)*6:n}if(0==u)e=t=r=i;else{var f=i<.5?i*(1+u):i+u-i*u,l=2*i-f;e=s(l,f,o+1/3),t=s(l,f,o),r=s(l,f,o-1/3)}return\"rgba(\"+255*e+\",\"+255*t+\",\"+255*r+\",\"+c+\")\"}(n):void 0;var e,t,r,a}function C(n){var e=/[+-]?\\d*\\.?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(e)return e[1]}function P(n,e){return i.fnc(n)?n(e.target,e.id,e.total):n}function I(n,e){return n.getAttribute(e)}function D(n,e,t){if(M([t,\"deg\",\"rad\",\"turn\"],C(e)))return e;var a=r.CSS[e+t];if(!i.und(a))return a;var o=document.createElement(n.tagName),u=n.parentNode&&n.parentNode!==document?n.parentNode:document.body;u.appendChild(o),o.style.position=\"absolute\",o.style.width=100+t;var c=100/o.offsetWidth;u.removeChild(o);var s=c*parseFloat(e);return r.CSS[e+t]=s,s}function B(n,e,t){if(e in n.style){var r=e.replace(/([a-z])([A-Z])/g,\"$1-$2\").toLowerCase(),a=n.style[e]||getComputedStyle(n).getPropertyValue(r)||\"0\";return t?D(n,a,t):a}}function T(n,e){return i.dom(n)&&!i.inp(n)&&(!i.nil(I(n,e))||i.svg(n)&&n[e])?\"attribute\":i.dom(n)&&M(t,e)?\"transform\":i.dom(n)&&\"transform\"!==e&&B(n,e)?\"css\":null!=n[e]?\"object\":void 0}function E(n){if(i.dom(n)){for(var e,t=n.style.transform||\"\",r=/(\\w+)\\(([^)]*)\\)/g,a=new Map;e=r.exec(t);)a.set(e[1],e[2]);return a}}function F(n,e,t,r){var a,u=o(e,\"scale\")?1:0+(o(a=e,\"translate\")||\"perspective\"===a?\"px\":o(a,\"rotate\")||o(a,\"skew\")?\"deg\":void 0),i=E(n).get(e)||u;return t&&(t.transforms.list.set(e,i),t.transforms.last=e),r?D(n,i,r):i}function A(n,e,t,r){switch(T(n,e)){case\"transform\":return F(n,e,r,t);case\"css\":return B(n,e,t);case\"attribute\":return I(n,e);default:return n[e]||0}}function N(n,e){var t=/^(\\*=|\\+=|-=)/.exec(n);if(!t)return n;var r=C(n)||0,a=parseFloat(e),o=parseFloat(n.replace(t[0],\"\"));switch(t[0][0]){case\"+\":return a+o+r;case\"-\":return a-o+r;case\"*\":return a*o+r}}function S(n,e){if(i.col(n))return O(n);if(/\\s/g.test(n))return n;var t=C(n),r=t?n.substr(0,n.length-t.length):n;return e?r+e:r}function L(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function j(n){for(var e,t=n.points,r=0,a=0;a0&&(r+=L(e,o)),e=o}return r}function q(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case\"circle\":return o=n,2*Math.PI*I(o,\"r\");case\"rect\":return 2*I(a=n,\"width\")+2*I(a,\"height\");case\"line\":return L({x:I(r=n,\"x1\"),y:I(r,\"y1\")},{x:I(r,\"x2\"),y:I(r,\"y2\")});case\"polyline\":return j(n);case\"polygon\":return t=(e=n).points,j(e)+L(t.getItem(t.numberOfItems-1),t.getItem(0))}var e,t,r,a,o}function H(n,e){var t=e||{},r=t.el||function(n){for(var e=n.parentNode;i.svg(e)&&i.svg(e.parentNode);)e=e.parentNode;return e}(n),a=r.getBoundingClientRect(),o=I(r,\"viewBox\"),u=a.width,c=a.height,s=t.viewBox||(o?o.split(\" \"):[0,0,u,c]);return{el:r,viewBox:s,x:s[0]/1,y:s[1]/1,w:u,h:c,vW:s[2],vH:s[3]}}function V(n,e,t){function r(t){void 0===t&&(t=0);var r=e+t>=1?e+t:0;return n.el.getPointAtLength(r)}var a=H(n.el,n.svg),o=r(),u=r(-1),i=r(1),c=t?1:a.w/a.vW,s=t?1:a.h/a.vH;switch(n.property){case\"x\":return(o.x-a.x)*c;case\"y\":return(o.y-a.y)*s;case\"angle\":return 180*Math.atan2(i.y-u.y,i.x-u.x)/Math.PI}}function $(n,e){var t=/[+-]?\\d*\\.?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/g,r=S(i.pth(n)?n.totalLength:n,e)+\"\";return{original:r,numbers:r.match(t)?r.match(t).map(Number):[0],strings:i.str(n)||e?r.split(t):[]}}function W(n){return m(n?y(i.arr(n)?n.map(b):b(n)):[],function(n,e,t){return t.indexOf(n)===e})}function X(n){var e=W(n);return e.map(function(n,t){return{target:n,id:t,total:e.length,transforms:{list:E(n)}}})}function Y(n,e){var t=x(e);if(/^spring/.test(t.easing)&&(t.duration=s(t.easing)),i.arr(n)){var r=n.length;2===r&&!i.obj(n[0])?n={value:n}:i.fnc(e.duration)||(t.duration=e.duration/r)}var a=i.arr(n)?n:[n];return a.map(function(n,t){var r=i.obj(n)&&!i.pth(n)?n:{value:n};return i.und(r.delay)&&(r.delay=t?0:e.delay),i.und(r.endDelay)&&(r.endDelay=t===a.length-1?e.endDelay:0),r}).map(function(n){return k(n,t)})}function Z(n,e){var t=[],r=e.keyframes;for(var a in r&&(e=k(function(n){for(var e=m(y(n.map(function(n){return Object.keys(n)})),function(n){return i.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),t={},r=function(r){var a=e[r];t[a]=n.map(function(n){var e={};for(var t in n)i.key(t)?t==a&&(e.value=n[t]):e[t]=n[t];return e})},a=0;a0?requestAnimationFrame(e):void 0}return\"undefined\"!=typeof document&&document.addEventListener(\"visibilitychange\",function(){en.suspendWhenDocumentHidden&&(nn()?n=cancelAnimationFrame(n):(K.forEach(function(n){return n._onDocumentVisibility()}),U()))}),function(){n||nn()&&en.suspendWhenDocumentHidden||!(K.length>0)||(n=requestAnimationFrame(e))}}();function nn(){return!!document&&document.hidden}function en(t){void 0===t&&(t={});var r,o=0,u=0,i=0,c=0,s=null;function f(n){var e=window.Promise&&new Promise(function(n){return s=n});return n.finished=e,e}var l,d,p,v,h,g,y,b,M=(d=w(n,l=t),p=w(e,l),v=Z(p,l),h=X(l.targets),g=_(h,v),y=R(g,p),b=J,J++,k(d,{id:b,children:[],animatables:h,animations:g,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));f(M);function x(){var n=M.direction;\"alternate\"!==n&&(M.direction=\"normal\"!==n?\"normal\":\"reverse\"),M.reversed=!M.reversed,r.forEach(function(n){return n.reversed=M.reversed})}function O(n){return M.reversed?M.duration-n:n}function C(){o=0,u=O(M.currentTime)*(1/en.speed)}function P(n,e){e&&e.seek(n-e.timelineOffset)}function I(n){for(var e=0,t=M.animations,r=t.length;e2||(b=Math.round(b*p)/p)),v.push(b)}var k=d.length;if(k){g=d[0];for(var O=0;O0&&(M.began=!0,D(\"begin\")),!M.loopBegan&&M.currentTime>0&&(M.loopBegan=!0,D(\"loopBegin\")),d<=t&&0!==M.currentTime&&I(0),(d>=l&&M.currentTime!==e||!e)&&I(e),d>t&&d=e&&(u=0,M.remaining&&!0!==M.remaining&&M.remaining--,M.remaining?(o=i,D(\"loopComplete\"),M.loopBegan=!1,\"alternate\"===M.direction&&x()):(M.paused=!0,M.completed||(M.completed=!0,D(\"loopComplete\"),D(\"complete\"),!M.passThrough&&\"Promise\"in window&&(s(),f(M)))))}return M.reset=function(){var n=M.direction;M.passThrough=!1,M.currentTime=0,M.progress=0,M.paused=!0,M.began=!1,M.loopBegan=!1,M.changeBegan=!1,M.completed=!1,M.changeCompleted=!1,M.reversePlayback=!1,M.reversed=\"reverse\"===n,M.remaining=M.loop,r=M.children;for(var e=c=r.length;e--;)M.children[e].reset();(M.reversed&&!0!==M.loop||\"alternate\"===n&&1===M.loop)&&M.remaining++,I(M.reversed?M.duration:0)},M._onDocumentVisibility=C,M.set=function(n,e){return z(n,e),M},M.tick=function(n){i=n,o||(o=i),B((i+(u-o))*en.speed)},M.seek=function(n){B(O(n))},M.pause=function(){M.paused=!0,C()},M.play=function(){M.paused&&(M.completed&&M.reset(),M.paused=!1,K.push(M),C(),U())},M.reverse=function(){x(),M.completed=!M.reversed,C()},M.restart=function(){M.reset(),M.play()},M.remove=function(n){rn(W(n),M)},M.reset(),M.autoplay&&M.play(),M}function tn(n,e){for(var t=e.length;t--;)M(n,e[t].animatable.target)&&e.splice(t,1)}function rn(n,e){var t=e.animations,r=e.children;tn(n,t);for(var a=r.length;a--;){var o=r[a],u=o.animations;tn(n,u),u.length||o.children.length||r.splice(a,1)}t.length||r.length||e.pause()}return en.version=\"3.2.1\",en.speed=1,en.suspendWhenDocumentHidden=!0,en.running=K,en.remove=function(n){for(var e=W(n),t=K.length;t--;)rn(e,K[t])},en.get=A,en.set=z,en.convertPx=D,en.path=function(n,e){var t=i.str(n)?g(n)[0]:n,r=e||100;return function(n){return{property:n,el:t,svg:H(t),totalLength:q(t)*(r/100)}}},en.setDashoffset=function(n){var e=q(n);return n.setAttribute(\"stroke-dasharray\",e),e},en.stagger=function(n,e){void 0===e&&(e={});var t=e.direction||\"normal\",r=e.easing?h(e.easing):null,a=e.grid,o=e.axis,u=e.from||0,c=\"first\"===u,s=\"center\"===u,f=\"last\"===u,l=i.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,v=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,i){if(c&&(u=0),s&&(u=(i-1)/2),f&&(u=i-1),!m.length){for(var h=0;h-1&&K.splice(o,1);for(var s=0;s{document.body.addEventListener("click",(e=>{var addElement=e.target.closest(".contentdesigner-addelement"),elementAction=e.target.closest(".element-item .element-actions .action-item"),moduleElement=e.target.closest("div.element-item");if(elementAction&&null!=elementAction&&moduleElement&&null!=moduleElement){var action=elementAction.getAttribute("data-action"),elementId=moduleElement.getAttribute("data-elementid"),element=moduleElement.getAttribute("data-elementshortname"),instanceId=moduleElement.getAttribute("data-instanceid");"delete"===action&&confirmDeleteElement(element,(function(){editElement(moduleElement,elementId,instanceId,action)})),"moveup"!=action&&"movedown"!=action||moveElement(moduleElement,action),"status"==action&&updateStatus(moduleElement)}if(addElement&&null!=addElement){e.preventDefault();var position=addElement.dataset.position,chapter=addElement.dataset.chapter;buildAddElementModal(position,chapter)}}))},moveElement=(moduleElement,action)=>{var promise;if("chapter"==moduleElement.dataset.elementshortname){var item=moduleElement.closest(".chapters-list");"moveup"==action?item.parentNode.insertBefore(item,item.previousElementSibling):item.parentNode.insertBefore(item,item.nextElementSibling.nextElementSibling);let contents=[];document.querySelectorAll("ul.course-content-list .chapters-content").forEach((item=>{contents.push(item.dataset.id)}));let params={chapters:contents.join(","),cmid:Data.cm.id};promise=Fragment.loadFragment("mod_contentdesigner","move_chapter",Data.contextid,params).done(((html,js)=>{Templates.replaceNode(".contentdesigner-content",html,js)})).fail(Notification.exception),LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem,promise)}else{let chapter=moduleElement.closest(".chapters-content"),item=moduleElement.parentNode;if("moveup"==action)if(null===item.previousElementSibling){let previousChapter=item.closest(".chapters-list").previousElementSibling;var append=!1;null!==previousChapter&&(previousChapter.querySelector(".chapter-elements-list").append(item),append=!0),append&&null!=previousChapter.childNodes[1]&&updateChapterElements(previousChapter.childNodes[1])}else item.parentNode.insertBefore(item,item.previousElementSibling);else if(null===item.nextElementSibling){let nextChapter=item.closest(".chapters-list").nextElementSibling;var prepend=!1;null!==nextChapter&&(nextChapter.querySelector(".chapter-elements-list").prepend(item),prepend=!0),prepend&&null!=nextChapter.childNodes[1]&&updateChapterElements(nextChapter.childNodes[1])}else item.parentNode.insertBefore(item,item.nextElementSibling.nextElementSibling);promise=updateChapterElements(chapter),LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem,promise)}},updateChapterElements=chapter=>{let contents=[];chapter.querySelectorAll("li.element-item > div.element-item").forEach((item=>{contents.push(item.dataset.contentid)}));let params={contents:contents.join(","),chapterid:chapter.dataset.id,cmid:Data.cm.id};var promise=Fragment.loadFragment("mod_contentdesigner","move_element",Data.contextid,params);return promise.done(((html,js)=>{Templates.replaceNode(".contentdesigner-content",html,js)})),promise};var updateStatus=moduleElement=>{let statusElement=moduleElement.querySelector('[data-action="status"] > i');var params={element:moduleElement.dataset.elementshortname,instance:moduleElement.dataset.instanceid,status:1!=moduleElement.dataset.visibility,cmid:Data.cm.id};1==moduleElement.dataset.visibility?(statusElement.classList.remove("fa-eye"),statusElement.classList.add("fa-eye-slash"),moduleElement.dataset.visibility=!1):(statusElement.classList.remove("fa-eye-slash"),statusElement.classList.add("fa-eye"),moduleElement.dataset.visibility=!0);var promise=Fragment.loadFragment("mod_contentdesigner","update_visibility",Data.contextid,params).then((()=>!0));LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem,promise)},editElement=function(moduleElement,elementId,instanceId,action){var args={cmid:cmID,action:action,elementid:elementId,instanceid:instanceId};Fragment.loadFragment("mod_contentdesigner","edit_element",contextID,args).then((html=>{moduleElement.parentNode.replaceWith(html)})).fail(Notification.exception)},confirmDeleteElement=function(element,onconfirm){var elementTypename="element_".element;Str.get_string("pluginname",elementTypename).done((function(){var plugindata={element:element};Str.get_strings([{key:"confirm",component:"core"},{key:"deletechecktype",component:"mod_contentdesigner",param:plugindata},{key:"yes"},{key:"no"}]).done((function(s){Notification.confirm(s[0],s[1],s[2],s[3],onconfirm)}))}))};const buildAddElementModal=function(){let position=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"bottom",chapter=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;var params={cmid:Data.cm.id};return Modal.create({type:Modal.TYPE,title:Str.get_string("addelement","contentdesigner"),body:Fragment.loadFragment("mod_contentdesigner","get_elements_list",contextID,params),large:!1}).then((modal=>(modal.getRoot().on(ModalEvents.bodyRendered,(function(){modal.getRoot().get(0).querySelectorAll(".element-item").forEach((e=>{e.addEventListener("click",(function(e){if(e.target.closest(".element-item")){var element=e.currentTarget.dataset.element,params={cmid:Data.cm.id,element:element,chapter:chapter,position:position,sesskey:M.cfg.sesskey};const urlParams=new URLSearchParams(params);window.location=M.cfg.wwwroot+"/mod/contentdesigner/element.php?"+urlParams.toString()}}))}))})),modal.show(),modal)))};return{init:function(contextid,cmid){return((contextid,cmid)=>("page-mod-contentdesigner-editor"!==document.body.id||(contextID=contextid,cmID=cmid,initEventListeners()),null))(contextid,cmid)}}})); + +//# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/amd/build/editor.min.js.map b/amd/build/editor.min.js.map new file mode 100644 index 0000000..dfa9f84 --- /dev/null +++ b/amd/build/editor.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["define(['jquery', 'core/modal_factory', 'core/modal_events', 'core/str',\r\n 'core/fragment', 'core/templates', 'core/notification', 'core/loadingicon'],\r\n function($, Modal, ModalEvents, Str, Fragment, Templates, Notification, LoadingIcon) {\r\n\r\n /* global contentDesigner */\r\n var Data = contentDesigner;\r\n\r\n let contextID;\r\n\r\n let cmID;\r\n\r\n let loaderItem = '.contentdesigner-content';\r\n\r\n const editor = (contextid, cmid) => {\r\n if (document.body.id !== 'page-mod-contentdesigner-editor') {\r\n return null;\r\n }\r\n contextID = contextid;\r\n cmID = cmid;\r\n initEventListeners();\r\n return null;\r\n };\r\n\r\n const initEventListeners = () => {\r\n document.body.addEventListener('click', (e) => {\r\n var addElement = e.target.closest('.contentdesigner-addelement');\r\n var elementAction = e.target.closest(\".element-item .element-actions .action-item\");\r\n var moduleElement = e.target.closest(\"div.element-item\");\r\n if (elementAction && elementAction != undefined\r\n && moduleElement && moduleElement != undefined) {\r\n var action = elementAction.getAttribute('data-action');\r\n var elementId = moduleElement.getAttribute('data-elementid');\r\n var element = moduleElement.getAttribute('data-elementshortname');\r\n var instanceId = moduleElement.getAttribute('data-instanceid');\r\n if (action === 'delete') {\r\n // Deleting requires confirmation.\r\n confirmDeleteElement(element, function() {\r\n editElement(moduleElement, elementId, instanceId, action);\r\n });\r\n }\r\n\r\n if (action == 'moveup' || action == 'movedown') {\r\n moveElement(moduleElement, action);\r\n }\r\n\r\n if (action == 'status') {\r\n updateStatus(moduleElement);\r\n }\r\n }\r\n\r\n if (addElement && addElement != undefined) {\r\n e.preventDefault();\r\n var position = addElement.dataset.position;\r\n var chapter = addElement.dataset.chapter;\r\n buildAddElementModal(position, chapter);\r\n }\r\n\r\n });\r\n };\r\n\r\n\r\n const moveElement = (moduleElement, action) => {\r\n var promise;\r\n if (moduleElement.dataset.elementshortname == 'chapter') {\r\n var item = moduleElement.closest('.chapters-list');\r\n if (action == 'moveup') {\r\n item.parentNode.insertBefore(item, item.previousElementSibling);\r\n } else {\r\n item.parentNode.insertBefore(item, item.nextElementSibling.nextElementSibling);\r\n }\r\n let contents = [];\r\n var items = document.querySelectorAll('ul.course-content-list .chapters-content');\r\n items.forEach((item) => {\r\n contents.push(item.dataset.id);\r\n });\r\n let params = {\r\n chapters: contents.join(','),\r\n cmid: Data.cm.id\r\n };\r\n promise = Fragment.loadFragment('mod_contentdesigner', 'move_chapter', Data.contextid, params).done((html, js) => {\r\n Templates.replaceNode('.contentdesigner-content', html, js);\r\n }).fail(Notification.exception);\r\n LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise);\r\n } else {\r\n\r\n let chapter = moduleElement.closest('.chapters-content');\r\n let item = moduleElement.parentNode;\r\n if (action == 'moveup') {\r\n // Append to the Previous chapter if this item is first in the list.\r\n if (item.previousElementSibling === null) {\r\n let previousChapter = item.closest('.chapters-list').previousElementSibling;\r\n // To fix the moodle CI nested loop count of 5. Tested separetly.\r\n var append = false;\r\n if (previousChapter !== null) {\r\n previousChapter.querySelector('.chapter-elements-list').append(item);\r\n append = true;\r\n }\r\n if (append && previousChapter.childNodes[1] != undefined) {\r\n updateChapterElements(previousChapter.childNodes[1]);\r\n }\r\n } else {\r\n item.parentNode.insertBefore(item, item.previousElementSibling);\r\n }\r\n } else {\r\n // Prepend to the next chapter if this item is last in the list.\r\n if (item.nextElementSibling === null) {\r\n let nextChapter = item.closest('.chapters-list').nextElementSibling;\r\n // To fix the moodle CI nested loop count of 5. Tested separetly.\r\n var prepend = false;\r\n if (nextChapter !== null) {\r\n nextChapter.querySelector('.chapter-elements-list').prepend(item);\r\n prepend = true;\r\n }\r\n if (prepend && nextChapter.childNodes[1] != undefined) {\r\n updateChapterElements(nextChapter.childNodes[1]);\r\n }\r\n\r\n } else {\r\n item.parentNode.insertBefore(item, item.nextElementSibling.nextElementSibling);\r\n }\r\n }\r\n\r\n promise = updateChapterElements(chapter);\r\n LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise);\r\n }\r\n };\r\n\r\n const updateChapterElements = (chapter) => {\r\n let contents = [];\r\n var items = chapter.querySelectorAll('li.element-item > div.element-item');\r\n items.forEach((item) => {\r\n contents.push(item.dataset.contentid);\r\n });\r\n let params = {\r\n contents: contents.join(','),\r\n chapterid: chapter.dataset.id,\r\n cmid: Data.cm.id\r\n };\r\n var promise = Fragment.loadFragment('mod_contentdesigner', 'move_element', Data.contextid, params);\r\n promise.done((html, js) => {\r\n Templates.replaceNode('.contentdesigner-content', html, js);\r\n });\r\n\r\n return promise;\r\n };\r\n\r\n var updateStatus = (moduleElement) => {\r\n let statusElement = moduleElement.querySelector('[data-action=\"status\"] > i');\r\n var params = {\r\n element: moduleElement.dataset.elementshortname,\r\n instance: moduleElement.dataset.instanceid,\r\n status: moduleElement.dataset.visibility == true ? false : true,\r\n cmid: Data.cm.id\r\n };\r\n\r\n if (moduleElement.dataset.visibility == true) {\r\n statusElement.classList.remove('fa-eye');\r\n statusElement.classList.add('fa-eye-slash');\r\n moduleElement.dataset.visibility = false;\r\n } else {\r\n statusElement.classList.remove('fa-eye-slash');\r\n statusElement.classList.add('fa-eye');\r\n moduleElement.dataset.visibility = true;\r\n }\r\n\r\n var promise = Fragment.loadFragment('mod_contentdesigner', 'update_visibility', Data.contextid, params).then(() => {\r\n return true;\r\n });\r\n LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise);\r\n };\r\n\r\n\r\n /**\r\n * Performs an action on a element (moving, deleting, duplicating, hiding, etc.)\r\n *\r\n * @param {JQuery} moduleElement activity element we perform action on\r\n * @param {Number} elementId\r\n * @param {Number} instanceId\r\n * @param {String} action Action of the current clicked element.\r\n */\r\n var editElement = function(moduleElement, elementId, instanceId, action) {\r\n var args = {\r\n cmid: cmID,\r\n action: action,\r\n elementid: elementId,\r\n instanceid: instanceId,\r\n };\r\n Fragment.loadFragment('mod_contentdesigner', 'edit_element', contextID, args).then((html) => {\r\n moduleElement.parentNode.replaceWith(html);\r\n return;\r\n }).fail(Notification.exception);\r\n };\r\n\r\n /**\r\n * Displays the delete confirmation to delete a module\r\n *\r\n * @param {String} element\r\n * @param {Function} onconfirm function to execute on confirm\r\n */\r\n var confirmDeleteElement = function(element, onconfirm) {\r\n var elementTypename = 'element_'.element;\r\n Str.get_string('pluginname', elementTypename).done(function() {\r\n var plugindata = {\r\n element: element\r\n };\r\n Str.get_strings([\r\n {key: 'confirm', component: 'core'},\r\n {key: 'deletechecktype', component: 'mod_contentdesigner', param: plugindata},\r\n {key: 'yes'},\r\n {key: 'no'}\r\n ]).done(function(s) {\r\n Notification.confirm(s[0], s[1], s[2], s[3], onconfirm);\r\n }\r\n );\r\n });\r\n };\r\n\r\n /**\r\n * Load the elements list modal to insert new element\r\n *\r\n * @param {String} position where the element need to insert.\r\n * @param {Boolean} chapter chapter id to insert element.\r\n * @returns {Object}\r\n */\r\n const buildAddElementModal = (position = \"bottom\", chapter = 0) => {\r\n var params = {cmid: Data.cm.id};\r\n return Modal.create({\r\n type: Modal.TYPE,\r\n title: Str.get_string('addelement', 'contentdesigner'),\r\n body: Fragment.loadFragment('mod_contentdesigner', 'get_elements_list', contextID, params),\r\n large: false,\r\n }).then(modal => {\r\n modal.getRoot().on(ModalEvents.bodyRendered, function() {\r\n modal.getRoot().get(0).querySelectorAll('.element-item').forEach((e) => {\r\n e.addEventListener('click', function(e) {\r\n if (e.target.closest('.element-item')) {\r\n var element = e.currentTarget.dataset.element;\r\n var params = {\r\n cmid: Data.cm.id,\r\n element: element,\r\n chapter: chapter,\r\n position: position,\r\n sesskey: M.cfg.sesskey\r\n };\r\n const urlParams = new URLSearchParams(params);\r\n window.location = M.cfg.wwwroot + '/mod/contentdesigner/element.php?' + urlParams.toString();\r\n }\r\n });\r\n });\r\n });\r\n modal.show();\r\n return modal;\r\n });\r\n };\r\n\r\n return {\r\n init: function(contextid, cmid) {\r\n return editor(contextid, cmid);\r\n }\r\n };\r\n});\r\n"],"names":["define","$","Modal","ModalEvents","Str","Fragment","Templates","Notification","LoadingIcon","Data","contentDesigner","contextID","cmID","loaderItem","initEventListeners","document","body","addEventListener","e","addElement","target","closest","elementAction","moduleElement","undefined","action","getAttribute","elementId","element","instanceId","confirmDeleteElement","editElement","moveElement","updateStatus","preventDefault","position","dataset","chapter","buildAddElementModal","promise","elementshortname","item","parentNode","insertBefore","previousElementSibling","nextElementSibling","contents","querySelectorAll","forEach","push","id","params","chapters","join","cmid","cm","loadFragment","contextid","done","html","js","replaceNode","fail","exception","addIconToContainerRemoveOnCompletion","previousChapter","append","querySelector","childNodes","updateChapterElements","nextChapter","prepend","contentid","chapterid","statusElement","instance","instanceid","status","visibility","classList","remove","add","then","args","elementid","replaceWith","onconfirm","elementTypename","get_string","plugindata","get_strings","key","component","param","s","confirm","create","type","TYPE","title","large","modal","getRoot","on","bodyRendered","get","currentTarget","sesskey","M","cfg","urlParams","URLSearchParams","window","location","wwwroot","toString","show","init","editor"],"mappings":"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"} \ No newline at end of file diff --git a/amd/build/elements.min.js b/amd/build/elements.min.js new file mode 100644 index 0000000..91faae0 --- /dev/null +++ b/amd/build/elements.min.js @@ -0,0 +1,3 @@ +define("mod_contentdesigner/elements",["jquery","core/fragment","core/templates","core/loadingicon","mod_contentdesigner/anime"],(function($,Fragment,Templates,LoadingIcon,anime){const SELECTORS={chapter:'[data-elementshortname="chapter"]',chapterList:".chapter-elements-list",elementContent:".element-content",fragments:{nextContents:"load_remain_capter_contents"},contentWrapper:".contentdesigner-wrapper"},contentDesignerData=()=>{var cmdetails=null!==document.querySelector("input[name=contentdesigner_cm_details]")?document.querySelector("input[name=contentdesigner_cm_details]").value:"";return(cmdetails?JSON.parse(cmdetails):"")||contentDesignerElementsData};let animations={},button=()=>document.querySelector(".contentdesigner-content"),courseContent=()=>document.querySelector("ul.course-content-list");const removeMarkBtn=chapterSelector=>{null!=document.querySelector(chapterSelector).querySelector("button.complete-chapter")&&document.querySelector(chapterSelector).querySelector("button.complete-chapter").remove()},animateElements=function(){var leftFrame,rightFrame,fadeIn;leftFrame=[{transform:"translate3d(-100%, 0, 0)"},{transform:"translate3d(0, 0, 0)",opacity:1}],rightFrame=[{transform:"translate3d(100%, 0, 0)"},{transform:"translate3d(10%, 0, 0)",opacity:1}],fadeIn=[{opacity:0},{opacity:1}],document.querySelectorAll(".element-item .general-options.animation").forEach((item=>{var node=item;if(null==node.dataset.entranceanimation||""==node.dataset.entranceanimation)return;var data=JSON.parse(node.dataset.entranceanimation),speed=1500;"slow"==data.duration?speed=3e3:"fast"==data.duration&&(speed=500);const observer=new IntersectionObserver((entries=>{entries.forEach((entry=>{if(entry.isIntersecting){if((node=entry.target).classList.contains("animated"))return;var frame=fadeIn;"slideInLeft"==data.animation?frame=leftFrame:"slideInRight"==data.animation&&(frame=rightFrame),setTimeout((function(){node.classList.add("animated"),item.animate(frame,{duration:speed||1e3})}),data.delay),observer.unobserve(item)}}))}));observer.observe(item)})),document.querySelectorAll(".element-item").forEach((function(item){!function(item){var node=item.childNodes[1];if(null!=node.dataset.scrolleffect&&""!=node.dataset.scrolleffect){var id=node.dataset.contentid,data=JSON.parse(node.dataset.scrolleffect),speed=data.speed?1e4/data.speed:0;animations[id]=anime({targets:item.childNodes[1],translateX:"left"==data.direction?[800,0]:[-800,0],duration:speed||500,autoplay:!1,elasticity:0,easing:"easeInOutSine"}),document.getElementById("page").addEventListener("scroll",(function(){document.getElementById("page").style.overflow="visible";var scroll=animateOnScroll(item,animations[id].duration);animations[id].seek(scroll)})),window.addEventListener("scroll",(function(){var scroll=animateOnScroll(item,animations[id].duration);animations[id].seek(scroll)}));var modalBody=document.querySelector(".path-course-view .modal-dialog-scrollable .modal-body");null!==modalBody&&modalBody.addEventListener("scroll",(function(e){var scroll=animateOnScroll(item,animations[id].duration,e.target);animations[id].seek(scroll)})),window.addEventListener("load",(function(){setTimeout((function(){document.getElementById("page").style.overflow="visible"}),500);var scroll=animateOnScroll(item,animations[id].duration);animations[id].seek(scroll)})),document.getElementById("page").style.overflow="visible"}}(item)}))};const animateOnScroll=function(item,dataspeed){let scrollElement=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;var node=item.childNodes[1];if(null==node.dataset.scrolleffect||""==node.dataset.scrolleffect)return;var data=JSON.parse(node.dataset.scrolleffect);let start=parseInt(data.start)||100,end=200;const docElement=scrollElement||document.documentElement;let clientHeight=docElement.clientHeight,itemY=()=>item.getBoundingClientRect().y,itemTop=()=>item.getBoundingClientRect().top-end,isStartPosition=()=>itemY()+start<=docElement.clientHeight,isEndPosition=()=>itemY()<=end;if(isStartPosition()&&!isEndPosition()){var ls=docElement.scrollHeight-(docElement.clientHeight+docElement.scrollTop),availablescroll=Math.min(clientHeight-(end+start),ls),scrolled=availablescroll>itemTop()?availablescroll-itemTop():item.offsetTop-end-itemTop()-availablescroll,percent=dataspeed/availablescroll,speed=scrolled*percent;return speed}};return{data:contentDesignerData,contentDesignerData:contentDesignerData,refreshContent:()=>{var params={cmid:contentDesignerData().cmid},promise=Fragment.loadFragment("mod_contentdesigner","load_elements",contentDesignerData().contextid,params);promise.done(((html,js)=>{var fakeDiv=document.createElement("div");fakeDiv.innerHTML=html;var chapters=fakeDiv.querySelector(".course-content-list").children,filterChapter=[];chapters.forEach((chapter=>{var elementSelector='li.element-item[data-instanceid="'+chapter.dataset.instanceid+'"]';elementSelector+='[data-elementshortname="'+chapter.dataset.elementshortname+'"]';let chapterSelector='li.chapters-list[data-id="'+chapter.dataset.id+'"]';var _chapter$querySelecto;document.querySelector(chapterSelector)||document.querySelector(elementSelector)?((null!==(_chapter$querySelecto=chapter.querySelectorAll(".element-item"))&&void 0!==_chapter$querySelecto?_chapter$querySelecto:[]).forEach((element=>{var dataset=element.children[0].dataset,selector='li.element-item .element-content[data-instanceid="'+dataset.instanceid+'"]';selector+='[data-elementshortname="'+dataset.elementshortname+'"]';var elementindocument=document.querySelector(selector);document.querySelector(selector)||void 0===document.querySelector(chapterSelector+" .chapter-elements-list")?null!==elementindocument&&1==elementindocument.dataset.replaceonrefresh&&Templates.replaceNode(elementindocument,element,""):document.querySelector(chapterSelector+" .chapter-elements-list").appendChild(element)})),null!==chapter.querySelector(".chapter-elements-list")&&chapter.querySelector(".chapter-elements-list").remove(),null!==chapter.querySelector(".chapter-title")&&chapter.querySelector(".chapter-title").remove(),chapter.children.length>0&&null!=document.querySelector(chapterSelector)&&(removeMarkBtn(chapterSelector),document.querySelector(chapterSelector).append(chapter.children[0]))):filterChapter.push(chapter)})),Templates.appendNodeContents(".contentdesigner-content .course-content-list",filterChapter,js),animateElements(),document.querySelector(SELECTORS.contentWrapper).dispatchEvent(new CustomEvent("elementupdate"))})).catch(),LoadingIcon.addIconToContainerRemoveOnCompletion(button(),promise)},loadNextChapters:function(currentChapter){var params={cmid:contentDesignerData().cmid,chapter:currentChapter},promise=Fragment.loadFragment("mod_contentdesigner","load_next_chapters",contentDesignerData().contextid,params);promise.done(((html,js)=>{var fakeDiv=document.createElement("div");fakeDiv.innerHTML=html;var chapters=fakeDiv.querySelector(".course-content-list").children,filterChapter=[];chapters.forEach((chapter=>{var elementSelector='li.element-item[data-instanceid="'+chapter.dataset.instanceid+'"]';elementSelector+='[data-elementshortname="'+chapter.dataset.elementshortname+'"]',document.querySelector('li.chapters-list[data-id="'+chapter.dataset.id+'"]')||document.querySelector(elementSelector)||filterChapter.push(chapter)})),Templates.appendNodeContents(".contentdesigner-content .course-content-list",filterChapter,js),animateElements()})).catch(),LoadingIcon.addIconToContainerRemoveOnCompletion(button(),promise)},inView:function(el){const rect=el.getBoundingClientRect();return rect.top>=0&&rect.left>=0&&rect.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&rect.right<=(window.innerWidth||document.documentElement.clientWidth)},animateElements:animateElements,courseContent:courseContent,removeWarning:()=>{null!==courseContent()&&null!==courseContent().querySelector(".label.label-warning")&&courseContent().querySelector(".label.label-warning").remove()},loadNextElements:function(currentElement){Fragment.loadFragment("mod_contentdesigner",SELECTORS.fragments.nextContents,contentDesignerData().contextid,{contentid:currentElement.dataset.contentid}).done(((html,js)=>{const selector=currentElement.parentNode.parentNode;Templates.appendNodeContents(selector,html,js)}))},selectors:SELECTORS}})); + +//# sourceMappingURL=elements.min.js.map \ No newline at end of file diff --git a/amd/build/elements.min.js.map b/amd/build/elements.min.js.map new file mode 100644 index 0000000..ea91d01 --- /dev/null +++ b/amd/build/elements.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"elements.min.js","sources":["../src/elements.js"],"sourcesContent":["define(['jquery', 'core/fragment', 'core/templates', 'core/loadingicon', 'mod_contentdesigner/anime'],\n function($, Fragment, Templates, LoadingIcon, anime) {\n\n /**\n * Selectors.\n */\n const SELECTORS = {\n chapter: '[data-elementshortname=\"chapter\"]',\n chapterList: '.chapter-elements-list',\n elementContent: '.element-content',\n fragments: {\n nextContents: 'load_remain_capter_contents'\n },\n contentWrapper: '.contentdesigner-wrapper',\n };\n\n const detailElement = 'input[name=contentdesigner_cm_details]';\n\n const contentDesignerData = () => {\n var cmdetails = document.querySelector(detailElement) !== null ? document.querySelector(detailElement).value : '';\n var cmdata = (cmdetails) ? JSON.parse(cmdetails) : '';\n /* global contentDesignerElementsData */\n return cmdata || contentDesignerElementsData;\n };\n\n let animations = {};\n\n let button = () => document.querySelector('.contentdesigner-content');\n\n let courseContentSelector = 'ul.course-content-list';\n\n let courseContent = () => document.querySelector(courseContentSelector);\n\n const contentWrapper = () => document.querySelector(SELECTORS.contentWrapper);\n\n const refreshContent = ()=>{\n var params = {\n cmid: contentDesignerData().cmid\n };\n var promise = Fragment.loadFragment('mod_contentdesigner', 'load_elements', contentDesignerData().contextid, params);\n promise.done((html, js)=>{\n var fakeDiv = document.createElement('div');\n fakeDiv.innerHTML = html;\n var chapters = fakeDiv.querySelector('.course-content-list').children;\n var filterChapter = [];\n\n chapters.forEach((chapter) => {\n\n var elementSelector = 'li.element-item[data-instanceid=\"' + chapter.dataset.instanceid + '\"]';\n elementSelector += '[data-elementshortname=\"' + chapter.dataset.elementshortname + '\"]';\n let chapterSelector = 'li.chapters-list[data-id=\"' + chapter.dataset.id + '\"]';\n\n if (!document.querySelector(chapterSelector) && !document.querySelector(elementSelector)) {\n filterChapter.push(chapter);\n } else {\n var elements = chapter.querySelectorAll('.element-item') ?? [];\n elements.forEach((element) => {\n var dataset = element.children[0].dataset;\n var selector = 'li.element-item .element-content[data-instanceid=\"' + dataset.instanceid + '\"]';\n selector += '[data-elementshortname=\"' + dataset.elementshortname + '\"]';\n var elementindocument = document.querySelector(selector);\n\n if (!document.querySelector(selector)\n && document.querySelector(chapterSelector + ' .chapter-elements-list') !== undefined) {\n document.querySelector(chapterSelector + ' .chapter-elements-list').appendChild(element);\n } else if (elementindocument !== null && elementindocument.dataset.replaceonrefresh == true) {\n Templates.replaceNode(elementindocument, element, '');\n }\n });\n if (chapter.querySelector('.chapter-elements-list') !== null) {\n chapter.querySelector('.chapter-elements-list').remove();\n }\n if (chapter.querySelector('.chapter-title') !== null) {\n chapter.querySelector('.chapter-title').remove();\n }\n if (chapter.children.length > 0 && document.querySelector(chapterSelector) != undefined) {\n removeMarkBtn(chapterSelector);\n document.querySelector(chapterSelector).append(chapter.children[0]);\n }\n }\n });\n\n Templates.appendNodeContents('.contentdesigner-content .course-content-list', filterChapter, js);\n animateElements();\n contentWrapper().dispatchEvent(new CustomEvent('elementupdate')); // Dispatch the element update event.\n }\n ).catch();\n\n LoadingIcon.addIconToContainerRemoveOnCompletion(button(), promise);\n };\n\n const removeMarkBtn = (chapterSelector) => {\n if (document.querySelector(chapterSelector).querySelector('button.complete-chapter') != undefined) {\n document.querySelector(chapterSelector).querySelector('button.complete-chapter').remove();\n }\n };\n\n /**\n * Remove the warning from response.\n */\n const removeWarning = () => {\n if (courseContent() !== null && courseContent().querySelector('.label.label-warning') !== null) {\n courseContent().querySelector('.label.label-warning').remove();\n }\n };\n\n const loadNextChapters = function(currentChapter) {\n var params = {\n cmid: contentDesignerData().cmid,\n chapter: currentChapter\n };\n var promise = Fragment.loadFragment('mod_contentdesigner', 'load_next_chapters', contentDesignerData().contextid, params);\n promise.done((html, js)=>{\n var fakeDiv = document.createElement('div');\n fakeDiv.innerHTML = html;\n var chapters = fakeDiv.querySelector('.course-content-list').children;\n var filterChapter = [];\n chapters.forEach((chapter) => {\n var elementSelector = 'li.element-item[data-instanceid=\"' + chapter.dataset.instanceid + '\"]';\n elementSelector += '[data-elementshortname=\"' + chapter.dataset.elementshortname + '\"]';\n\n if (!document.querySelector('li.chapters-list[data-id=\"' + chapter.dataset.id + '\"]')\n && !document.querySelector(elementSelector)) {\n filterChapter.push(chapter);\n }\n });\n Templates.appendNodeContents('.contentdesigner-content .course-content-list', filterChapter, js);\n animateElements();\n }).catch();\n\n LoadingIcon.addIconToContainerRemoveOnCompletion(button(), promise);\n };\n\n const loadNextElements = function(currentElement) {\n\n Fragment.loadFragment(\n 'mod_contentdesigner', SELECTORS.fragments.nextContents,\n contentDesignerData().contextid, {contentid: currentElement.dataset.contentid}\n ).done((html, js) => {\n\n const selector = currentElement.parentNode.parentNode;\n\n Templates.appendNodeContents(selector, html, js);\n });\n };\n\n /**\n * Verify the given element in the viewport.\n *\n * @param {HTMLElement} el Element to verify\n * @returns {bool}\n */\n const inView = function(el) {\n const rect = el.getBoundingClientRect();\n return (rect.top >= 0 && rect.left >= 0\n && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)\n && rect.right <= (window.innerWidth || document.documentElement.clientWidth)\n );\n };\n\n /**\n * Animate the module elements when the elements in the viewport.\n */\n const animateElements = function() {\n entranceAnimation(); // Init entrance animation.\n scrollingEffects();\n };\n\n /**\n * Initilaize the scroll reveal animation for elements.\n */\n function entranceAnimation() {\n\n var leftFrame = [\n {transform: 'translate3d(-100%, 0, 0)'},\n {transform: 'translate3d(0, 0, 0)', opacity: 1},\n ];\n\n var rightFrame = [\n {transform: 'translate3d(100%, 0, 0)'},\n {transform: 'translate3d(10%, 0, 0)', opacity: 1},\n ];\n\n var fadeIn = [\n {opacity: 0},\n {opacity: 1}\n ];\n\n const items = document.querySelectorAll('.element-item .general-options.animation');\n items.forEach((item)=>{\n var node = item;\n if (node.dataset.entranceanimation == undefined || node.dataset.entranceanimation == '') {\n return;\n }\n var data = JSON.parse(node.dataset.entranceanimation);\n var speed = 1500;\n if (data.duration == 'slow') {\n speed = 3000;\n } else if (data.duration == 'fast') {\n speed = 500;\n }\n const observer = new IntersectionObserver(entries => {\n entries.forEach(entry => {\n const intersecting = entry.isIntersecting;\n if (intersecting) {\n node = entry.target;\n if (node.classList.contains('animated')) {\n return;\n }\n\n var frame = fadeIn;\n if (data.animation == 'slideInLeft') {\n frame = leftFrame;\n } else if (data.animation == 'slideInRight') {\n frame = rightFrame;\n }\n\n setTimeout(function() {\n node.classList.add('animated');\n item.animate(frame, {duration: speed || 1000});\n }, data.delay);\n observer.unobserve(item);\n }\n });\n });\n\n observer.observe(item);\n });\n }\n\n /**\n * Setup the scrolling effects.\n */\n function scrollingEffects() {\n\n document.querySelectorAll('.element-item').forEach(function(item) {\n animate(item);\n });\n /* Item = document.querySelectorAll('.element-item')[5];\n animate(item); */\n }\n\n /**\n * Animate the element based on scrolling.\n * @param {HTMLELement} item\n * @returns {void}\n */\n function animate(item) {\n\n var node = item.childNodes[1];\n if (node.dataset.scrolleffect == undefined || node.dataset.scrolleffect == '') {\n return;\n }\n var id = node.dataset.contentid;\n var data = JSON.parse(node.dataset.scrolleffect);\n var speed = data.speed ? (10000 / data.speed) : 0;\n animations[id] = anime({\n targets: item.childNodes[1],\n translateX: data.direction == 'left' ? [800, 0] : [-800, 0],\n duration: speed || 500,\n autoplay: false,\n elasticity: 0,\n easing: 'easeInOutSine'\n });\n\n document.getElementById('page').addEventListener('scroll', function() {\n document.getElementById('page').style.overflow = 'visible';\n var scroll = animateOnScroll(item, animations[id].duration);\n animations[id].seek(scroll);\n });\n window.addEventListener('scroll', function() {\n var scroll = animateOnScroll(item, animations[id].duration);\n animations[id].seek(scroll);\n });\n var modalBody = document.querySelector('.path-course-view .modal-dialog-scrollable .modal-body');\n if (modalBody !== null) {\n modalBody.addEventListener('scroll', function(e) {\n var scroll = animateOnScroll(item, animations[id].duration, e.target);\n animations[id].seek(scroll);\n });\n }\n\n window.addEventListener('load', function() {\n setTimeout(function() {\n document.getElementById('page').style.overflow = 'visible';\n }, 500);\n var scroll = animateOnScroll(item, animations[id].duration);\n animations[id].seek(scroll);\n });\n\n document.getElementById('page').style.overflow = 'visible';\n }\n\n /**\n * Find the seek volume for the given element to move the element.\n *\n * @param {HTMLElement} item\n * @param {double} dataspeed\n * @param {HTMLElement} scrollElement\n * @returns {bool}\n */\n const animateOnScroll = function(item, dataspeed, scrollElement = null) {\n\n var node = item.childNodes[1];\n if (node.dataset.scrolleffect == undefined || node.dataset.scrolleffect == '') {\n return;\n }\n var data = JSON.parse(node.dataset.scrolleffect);\n let start = parseInt(data.start) || 100;\n let end = 200;\n const docElement = scrollElement || document.documentElement;\n let clientHeight = docElement.clientHeight;\n let itemY = () => item.getBoundingClientRect().y;\n let itemTop = () => item.getBoundingClientRect().top - end;\n let isStartPosition = () => itemY() + start <= (docElement.clientHeight);\n let isEndPosition = () => itemY() <= end;\n if (isStartPosition() && !isEndPosition()) {\n var ls = docElement.scrollHeight - (docElement.clientHeight + docElement.scrollTop);\n var availablescroll = Math.min(clientHeight - (end + start), ls);\n var scrolled = (availablescroll > itemTop())\n ? availablescroll - itemTop() : (item.offsetTop - end - itemTop()) - availablescroll;\n var percent = dataspeed / availablescroll;\n var speed = scrolled * percent;\n return speed;\n }\n return;\n };\n\n return {\n data: contentDesignerData, // Backward compatibility.\n contentDesignerData: contentDesignerData,\n refreshContent: refreshContent,\n loadNextChapters: loadNextChapters,\n inView: inView,\n animateElements: animateElements,\n courseContent: courseContent,\n removeWarning: removeWarning,\n loadNextElements: loadNextElements,\n selectors: SELECTORS\n };\n});\n"],"names":["define","$","Fragment","Templates","LoadingIcon","anime","SELECTORS","chapter","chapterList","elementContent","fragments","nextContents","contentWrapper","contentDesignerData","cmdetails","document","querySelector","value","JSON","parse","contentDesignerElementsData","animations","button","courseContent","removeMarkBtn","chapterSelector","undefined","remove","animateElements","leftFrame","rightFrame","fadeIn","transform","opacity","querySelectorAll","forEach","item","node","dataset","entranceanimation","data","speed","duration","observer","IntersectionObserver","entries","entry","isIntersecting","target","classList","contains","frame","animation","setTimeout","add","animate","delay","unobserve","observe","childNodes","scrolleffect","id","contentid","targets","translateX","direction","autoplay","elasticity","easing","getElementById","addEventListener","style","overflow","scroll","animateOnScroll","seek","window","modalBody","e","dataspeed","scrollElement","start","parseInt","end","docElement","documentElement","clientHeight","itemY","getBoundingClientRect","y","itemTop","top","isStartPosition","isEndPosition","ls","scrollHeight","scrollTop","availablescroll","Math","min","scrolled","offsetTop","percent","refreshContent","params","cmid","promise","loadFragment","contextid","done","html","js","fakeDiv","createElement","innerHTML","chapters","children","filterChapter","elementSelector","instanceid","elementshortname","element","selector","elementindocument","replaceonrefresh","replaceNode","appendChild","length","append","push","appendNodeContents","dispatchEvent","CustomEvent","catch","addIconToContainerRemoveOnCompletion","loadNextChapters","currentChapter","inView","el","rect","left","bottom","innerHeight","right","innerWidth","clientWidth","removeWarning","loadNextElements","currentElement","parentNode","selectors"],"mappings":"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"} \ No newline at end of file diff --git a/amd/src/anime.js b/amd/src/anime.js new file mode 100644 index 0000000..6c1fe88 --- /dev/null +++ b/amd/src/anime.js @@ -0,0 +1,8 @@ +/* + * anime.js v3.2.1 + * (c) 2020 Julian Garnier + * Released under the MIT license + * animejs.com + */ + +!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.anime=e()}(this,function(){"use strict";var n={update:null,begin:null,loopBegin:null,changeBegin:null,change:null,changeComplete:null,loopComplete:null,complete:null,loop:1,direction:"normal",autoplay:!0,timelineOffset:0},e={duration:1e3,delay:0,endDelay:0,easing:"easeOutElastic(1, .5)",round:0},t=["translateX","translateY","translateZ","rotate","rotateX","rotateY","rotateZ","scale","scaleX","scaleY","scaleZ","skew","skewX","skewY","perspective","matrix","matrix3d"],r={CSS:{},springs:{}};function a(n,e,t){return Math.min(Math.max(n,e),t)}function o(n,e){return n.indexOf(e)>-1}function u(n,e){return n.apply(null,e)}var i={arr:function(n){return Array.isArray(n)},obj:function(n){return o(Object.prototype.toString.call(n),"Object")},pth:function(n){return i.obj(n)&&n.hasOwnProperty("totalLength")},svg:function(n){return n instanceof SVGElement},inp:function(n){return n instanceof HTMLInputElement},dom:function(n){return n.nodeType||i.svg(n)},str:function(n){return"string"==typeof n},fnc:function(n){return"function"==typeof n},und:function(n){return void 0===n},nil:function(n){return i.und(n)||null===n},hex:function(n){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(n)},rgb:function(n){return/^rgb/.test(n)},hsl:function(n){return/^hsl/.test(n)},col:function(n){return i.hex(n)||i.rgb(n)||i.hsl(n)},key:function(t){return!n.hasOwnProperty(t)&&!e.hasOwnProperty(t)&&"targets"!==t&&"keyframes"!==t}};function c(n){var e=/\(([^)]+)\)/.exec(n);return e?e[1].split(",").map(function(n){return parseFloat(n)}):[]}function s(n,e){var t=c(n),o=a(i.und(t[0])?1:t[0],.1,100),u=a(i.und(t[1])?100:t[1],.1,100),s=a(i.und(t[2])?10:t[2],.1,100),f=a(i.und(t[3])?0:t[3],.1,100),l=Math.sqrt(u/o),d=s/(2*Math.sqrt(u*o)),p=d<1?l*Math.sqrt(1-d*d):0,v=1,h=d<1?(d*l-f)/p:-f+l;function g(n){var t=e?e*n/1e3:n;return t=d<1?Math.exp(-t*d*l)*(v*Math.cos(p*t)+h*Math.sin(p*t)):(v+h*t)*Math.exp(-t*l),0===n||1===n?n:1-t}return e?g:function(){var e=r.springs[n];if(e)return e;for(var t=0,a=0;;)if(1===g(t+=1/6)){if(++a>=16)break}else a=0;var o=t*(1/6)*1e3;return r.springs[n]=o,o}}function f(n){return void 0===n&&(n=10),function(e){return Math.ceil(a(e,1e-6,1)*n)*(1/n)}}var l,d,p=function(){var n=11,e=1/(n-1);function t(n,e){return 1-3*e+3*n}function r(n,e){return 3*e-6*n}function a(n){return 3*n}function o(n,e,o){return((t(e,o)*n+r(e,o))*n+a(e))*n}function u(n,e,o){return 3*t(e,o)*n*n+2*r(e,o)*n+a(e)}return function(t,r,a,i){if(0<=t&&t<=1&&0<=a&&a<=1){var c=new Float32Array(n);if(t!==r||a!==i)for(var s=0;s=.001?function(n,e,t,r){for(var a=0;a<4;++a){var i=u(e,t,r);if(0===i)return e;e-=(o(e,t,r)-n)/i}return e}(r,l,t,a):0===d?l:function(n,e,t,r,a){for(var u,i,c=0;(u=o(i=e+(t-e)/2,r,a)-n)>0?t=i:e=i,Math.abs(u)>1e-7&&++c<10;);return i}(r,i,i+e,t,a)}}}(),v=(l={linear:function(){return function(n){return n}}},d={Sine:function(){return function(n){return 1-Math.cos(n*Math.PI/2)}},Circ:function(){return function(n){return 1-Math.sqrt(1-n*n)}},Back:function(){return function(n){return n*n*(3*n-2)}},Bounce:function(){return function(n){for(var e,t=4;n<((e=Math.pow(2,--t))-1)/11;);return 1/Math.pow(4,3-t)-7.5625*Math.pow((3*e-2)/22-n,2)}},Elastic:function(n,e){void 0===n&&(n=1),void 0===e&&(e=.5);var t=a(n,1,10),r=a(e,.1,2);return function(n){return 0===n||1===n?n:-t*Math.pow(2,10*(n-1))*Math.sin((n-1-r/(2*Math.PI)*Math.asin(1/t))*(2*Math.PI)/r)}}},["Quad","Cubic","Quart","Quint","Expo"].forEach(function(n,e){d[n]=function(){return function(n){return Math.pow(n,e+2)}}}),Object.keys(d).forEach(function(n){var e=d[n];l["easeIn"+n]=e,l["easeOut"+n]=function(n,t){return function(r){return 1-e(n,t)(1-r)}},l["easeInOut"+n]=function(n,t){return function(r){return r<.5?e(n,t)(2*r)/2:1-e(n,t)(-2*r+2)/2}},l["easeOutIn"+n]=function(n,t){return function(r){return r<.5?(1-e(n,t)(1-2*r))/2:(e(n,t)(2*r-1)+1)/2}}}),l);function h(n,e){if(i.fnc(n))return n;var t=n.split("(")[0],r=v[t],a=c(n);switch(t){case"spring":return s(n,e);case"cubicBezier":return u(p,a);case"steps":return u(f,a);default:return u(r,a)}}function g(n){try{return document.querySelectorAll(n)}catch(n){return}}function m(n,e){for(var t=n.length,r=arguments.length>=2?arguments[1]:void 0,a=[],o=0;o1&&(t-=1),t<1/6?n+6*(e-n)*t:t<.5?e:t<2/3?n+(e-n)*(2/3-t)*6:n}if(0==u)e=t=r=i;else{var f=i<.5?i*(1+u):i+u-i*u,l=2*i-f;e=s(l,f,o+1/3),t=s(l,f,o),r=s(l,f,o-1/3)}return"rgba("+255*e+","+255*t+","+255*r+","+c+")"}(n):void 0;var e,t,r,a}function C(n){var e=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?(%|px|pt|em|rem|in|cm|mm|ex|ch|pc|vw|vh|vmin|vmax|deg|rad|turn)?$/.exec(n);if(e)return e[1]}function P(n,e){return i.fnc(n)?n(e.target,e.id,e.total):n}function I(n,e){return n.getAttribute(e)}function D(n,e,t){if(M([t,"deg","rad","turn"],C(e)))return e;var a=r.CSS[e+t];if(!i.und(a))return a;var o=document.createElement(n.tagName),u=n.parentNode&&n.parentNode!==document?n.parentNode:document.body;u.appendChild(o),o.style.position="absolute",o.style.width=100+t;var c=100/o.offsetWidth;u.removeChild(o);var s=c*parseFloat(e);return r.CSS[e+t]=s,s}function B(n,e,t){if(e in n.style){var r=e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase(),a=n.style[e]||getComputedStyle(n).getPropertyValue(r)||"0";return t?D(n,a,t):a}}function T(n,e){return i.dom(n)&&!i.inp(n)&&(!i.nil(I(n,e))||i.svg(n)&&n[e])?"attribute":i.dom(n)&&M(t,e)?"transform":i.dom(n)&&"transform"!==e&&B(n,e)?"css":null!=n[e]?"object":void 0}function E(n){if(i.dom(n)){for(var e,t=n.style.transform||"",r=/(\w+)\(([^)]*)\)/g,a=new Map;e=r.exec(t);)a.set(e[1],e[2]);return a}}function F(n,e,t,r){var a,u=o(e,"scale")?1:0+(o(a=e,"translate")||"perspective"===a?"px":o(a,"rotate")||o(a,"skew")?"deg":void 0),i=E(n).get(e)||u;return t&&(t.transforms.list.set(e,i),t.transforms.last=e),r?D(n,i,r):i}function A(n,e,t,r){switch(T(n,e)){case"transform":return F(n,e,r,t);case"css":return B(n,e,t);case"attribute":return I(n,e);default:return n[e]||0}}function N(n,e){var t=/^(\*=|\+=|-=)/.exec(n);if(!t)return n;var r=C(n)||0,a=parseFloat(e),o=parseFloat(n.replace(t[0],""));switch(t[0][0]){case"+":return a+o+r;case"-":return a-o+r;case"*":return a*o+r}}function S(n,e){if(i.col(n))return O(n);if(/\s/g.test(n))return n;var t=C(n),r=t?n.substr(0,n.length-t.length):n;return e?r+e:r}function L(n,e){return Math.sqrt(Math.pow(e.x-n.x,2)+Math.pow(e.y-n.y,2))}function j(n){for(var e,t=n.points,r=0,a=0;a0&&(r+=L(e,o)),e=o}return r}function q(n){if(n.getTotalLength)return n.getTotalLength();switch(n.tagName.toLowerCase()){case"circle":return o=n,2*Math.PI*I(o,"r");case"rect":return 2*I(a=n,"width")+2*I(a,"height");case"line":return L({x:I(r=n,"x1"),y:I(r,"y1")},{x:I(r,"x2"),y:I(r,"y2")});case"polyline":return j(n);case"polygon":return t=(e=n).points,j(e)+L(t.getItem(t.numberOfItems-1),t.getItem(0))}var e,t,r,a,o}function H(n,e){var t=e||{},r=t.el||function(n){for(var e=n.parentNode;i.svg(e)&&i.svg(e.parentNode);)e=e.parentNode;return e}(n),a=r.getBoundingClientRect(),o=I(r,"viewBox"),u=a.width,c=a.height,s=t.viewBox||(o?o.split(" "):[0,0,u,c]);return{el:r,viewBox:s,x:s[0]/1,y:s[1]/1,w:u,h:c,vW:s[2],vH:s[3]}}function V(n,e,t){function r(t){void 0===t&&(t=0);var r=e+t>=1?e+t:0;return n.el.getPointAtLength(r)}var a=H(n.el,n.svg),o=r(),u=r(-1),i=r(1),c=t?1:a.w/a.vW,s=t?1:a.h/a.vH;switch(n.property){case"x":return(o.x-a.x)*c;case"y":return(o.y-a.y)*s;case"angle":return 180*Math.atan2(i.y-u.y,i.x-u.x)/Math.PI}}function $(n,e){var t=/[+-]?\d*\.?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,r=S(i.pth(n)?n.totalLength:n,e)+"";return{original:r,numbers:r.match(t)?r.match(t).map(Number):[0],strings:i.str(n)||e?r.split(t):[]}}function W(n){return m(n?y(i.arr(n)?n.map(b):b(n)):[],function(n,e,t){return t.indexOf(n)===e})}function X(n){var e=W(n);return e.map(function(n,t){return{target:n,id:t,total:e.length,transforms:{list:E(n)}}})}function Y(n,e){var t=x(e);if(/^spring/.test(t.easing)&&(t.duration=s(t.easing)),i.arr(n)){var r=n.length;2===r&&!i.obj(n[0])?n={value:n}:i.fnc(e.duration)||(t.duration=e.duration/r)}var a=i.arr(n)?n:[n];return a.map(function(n,t){var r=i.obj(n)&&!i.pth(n)?n:{value:n};return i.und(r.delay)&&(r.delay=t?0:e.delay),i.und(r.endDelay)&&(r.endDelay=t===a.length-1?e.endDelay:0),r}).map(function(n){return k(n,t)})}function Z(n,e){var t=[],r=e.keyframes;for(var a in r&&(e=k(function(n){for(var e=m(y(n.map(function(n){return Object.keys(n)})),function(n){return i.key(n)}).reduce(function(n,e){return n.indexOf(e)<0&&n.push(e),n},[]),t={},r=function(r){var a=e[r];t[a]=n.map(function(n){var e={};for(var t in n)i.key(t)?t==a&&(e.value=n[t]):e[t]=n[t];return e})},a=0;a0?requestAnimationFrame(e):void 0}return"undefined"!=typeof document&&document.addEventListener("visibilitychange",function(){en.suspendWhenDocumentHidden&&(nn()?n=cancelAnimationFrame(n):(K.forEach(function(n){return n._onDocumentVisibility()}),U()))}),function(){n||nn()&&en.suspendWhenDocumentHidden||!(K.length>0)||(n=requestAnimationFrame(e))}}();function nn(){return!!document&&document.hidden}function en(t){void 0===t&&(t={});var r,o=0,u=0,i=0,c=0,s=null;function f(n){var e=window.Promise&&new Promise(function(n){return s=n});return n.finished=e,e}var l,d,p,v,h,g,y,b,M=(d=w(n,l=t),p=w(e,l),v=Z(p,l),h=X(l.targets),g=_(h,v),y=R(g,p),b=J,J++,k(d,{id:b,children:[],animatables:h,animations:g,duration:y.duration,delay:y.delay,endDelay:y.endDelay}));f(M);function x(){var n=M.direction;"alternate"!==n&&(M.direction="normal"!==n?"normal":"reverse"),M.reversed=!M.reversed,r.forEach(function(n){return n.reversed=M.reversed})}function O(n){return M.reversed?M.duration-n:n}function C(){o=0,u=O(M.currentTime)*(1/en.speed)}function P(n,e){e&&e.seek(n-e.timelineOffset)}function I(n){for(var e=0,t=M.animations,r=t.length;e2||(b=Math.round(b*p)/p)),v.push(b)}var k=d.length;if(k){g=d[0];for(var O=0;O0&&(M.began=!0,D("begin")),!M.loopBegan&&M.currentTime>0&&(M.loopBegan=!0,D("loopBegin")),d<=t&&0!==M.currentTime&&I(0),(d>=l&&M.currentTime!==e||!e)&&I(e),d>t&&d=e&&(u=0,M.remaining&&!0!==M.remaining&&M.remaining--,M.remaining?(o=i,D("loopComplete"),M.loopBegan=!1,"alternate"===M.direction&&x()):(M.paused=!0,M.completed||(M.completed=!0,D("loopComplete"),D("complete"),!M.passThrough&&"Promise"in window&&(s(),f(M)))))}return M.reset=function(){var n=M.direction;M.passThrough=!1,M.currentTime=0,M.progress=0,M.paused=!0,M.began=!1,M.loopBegan=!1,M.changeBegan=!1,M.completed=!1,M.changeCompleted=!1,M.reversePlayback=!1,M.reversed="reverse"===n,M.remaining=M.loop,r=M.children;for(var e=c=r.length;e--;)M.children[e].reset();(M.reversed&&!0!==M.loop||"alternate"===n&&1===M.loop)&&M.remaining++,I(M.reversed?M.duration:0)},M._onDocumentVisibility=C,M.set=function(n,e){return z(n,e),M},M.tick=function(n){i=n,o||(o=i),B((i+(u-o))*en.speed)},M.seek=function(n){B(O(n))},M.pause=function(){M.paused=!0,C()},M.play=function(){M.paused&&(M.completed&&M.reset(),M.paused=!1,K.push(M),C(),U())},M.reverse=function(){x(),M.completed=!M.reversed,C()},M.restart=function(){M.reset(),M.play()},M.remove=function(n){rn(W(n),M)},M.reset(),M.autoplay&&M.play(),M}function tn(n,e){for(var t=e.length;t--;)M(n,e[t].animatable.target)&&e.splice(t,1)}function rn(n,e){var t=e.animations,r=e.children;tn(n,t);for(var a=r.length;a--;){var o=r[a],u=o.animations;tn(n,u),u.length||o.children.length||r.splice(a,1)}t.length||r.length||e.pause()}return en.version="3.2.1",en.speed=1,en.suspendWhenDocumentHidden=!0,en.running=K,en.remove=function(n){for(var e=W(n),t=K.length;t--;)rn(e,K[t])},en.get=A,en.set=z,en.convertPx=D,en.path=function(n,e){var t=i.str(n)?g(n)[0]:n,r=e||100;return function(n){return{property:n,el:t,svg:H(t),totalLength:q(t)*(r/100)}}},en.setDashoffset=function(n){var e=q(n);return n.setAttribute("stroke-dasharray",e),e},en.stagger=function(n,e){void 0===e&&(e={});var t=e.direction||"normal",r=e.easing?h(e.easing):null,a=e.grid,o=e.axis,u=e.from||0,c="first"===u,s="center"===u,f="last"===u,l=i.arr(n),d=l?parseFloat(n[0]):parseFloat(n),p=l?parseFloat(n[1]):0,v=C(l?n[1]:n)||0,g=e.start||0+(l?d:0),m=[],y=0;return function(n,e,i){if(c&&(u=0),s&&(u=(i-1)/2),f&&(u=i-1),!m.length){for(var h=0;h-1&&K.splice(o,1);for(var s=0;s { + if (document.body.id !== 'page-mod-contentdesigner-editor') { + return null; + } + contextID = contextid; + cmID = cmid; + initEventListeners(); + return null; + }; + + const initEventListeners = () => { + document.body.addEventListener('click', (e) => { + var addElement = e.target.closest('.contentdesigner-addelement'); + var elementAction = e.target.closest(".element-item .element-actions .action-item"); + var moduleElement = e.target.closest("div.element-item"); + if (elementAction && elementAction != undefined + && moduleElement && moduleElement != undefined) { + var action = elementAction.getAttribute('data-action'); + var elementId = moduleElement.getAttribute('data-elementid'); + var element = moduleElement.getAttribute('data-elementshortname'); + var instanceId = moduleElement.getAttribute('data-instanceid'); + if (action === 'delete') { + // Deleting requires confirmation. + confirmDeleteElement(element, function() { + editElement(moduleElement, elementId, instanceId, action); + }); + } + + if (action == 'moveup' || action == 'movedown') { + moveElement(moduleElement, action); + } + + if (action == 'status') { + updateStatus(moduleElement); + } + } + + if (addElement && addElement != undefined) { + e.preventDefault(); + var position = addElement.dataset.position; + var chapter = addElement.dataset.chapter; + buildAddElementModal(position, chapter); + } + + }); + }; + + + const moveElement = (moduleElement, action) => { + var promise; + if (moduleElement.dataset.elementshortname == 'chapter') { + var item = moduleElement.closest('.chapters-list'); + if (action == 'moveup') { + item.parentNode.insertBefore(item, item.previousElementSibling); + } else { + item.parentNode.insertBefore(item, item.nextElementSibling.nextElementSibling); + } + let contents = []; + var items = document.querySelectorAll('ul.course-content-list .chapters-content'); + items.forEach((item) => { + contents.push(item.dataset.id); + }); + let params = { + chapters: contents.join(','), + cmid: Data.cm.id + }; + promise = Fragment.loadFragment('mod_contentdesigner', 'move_chapter', Data.contextid, params).done((html, js) => { + Templates.replaceNode('.contentdesigner-content', html, js); + }).fail(Notification.exception); + LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise); + } else { + + let chapter = moduleElement.closest('.chapters-content'); + let item = moduleElement.parentNode; + if (action == 'moveup') { + // Append to the Previous chapter if this item is first in the list. + if (item.previousElementSibling === null) { + let previousChapter = item.closest('.chapters-list').previousElementSibling; + // To fix the moodle CI nested loop count of 5. Tested separetly. + var append = false; + if (previousChapter !== null) { + previousChapter.querySelector('.chapter-elements-list').append(item); + append = true; + } + if (append && previousChapter.childNodes[1] != undefined) { + updateChapterElements(previousChapter.childNodes[1]); + } + } else { + item.parentNode.insertBefore(item, item.previousElementSibling); + } + } else { + // Prepend to the next chapter if this item is last in the list. + if (item.nextElementSibling === null) { + let nextChapter = item.closest('.chapters-list').nextElementSibling; + // To fix the moodle CI nested loop count of 5. Tested separetly. + var prepend = false; + if (nextChapter !== null) { + nextChapter.querySelector('.chapter-elements-list').prepend(item); + prepend = true; + } + if (prepend && nextChapter.childNodes[1] != undefined) { + updateChapterElements(nextChapter.childNodes[1]); + } + + } else { + item.parentNode.insertBefore(item, item.nextElementSibling.nextElementSibling); + } + } + + promise = updateChapterElements(chapter); + LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise); + } + }; + + const updateChapterElements = (chapter) => { + let contents = []; + var items = chapter.querySelectorAll('li.element-item > div.element-item'); + items.forEach((item) => { + contents.push(item.dataset.contentid); + }); + let params = { + contents: contents.join(','), + chapterid: chapter.dataset.id, + cmid: Data.cm.id + }; + var promise = Fragment.loadFragment('mod_contentdesigner', 'move_element', Data.contextid, params); + promise.done((html, js) => { + Templates.replaceNode('.contentdesigner-content', html, js); + }); + + return promise; + }; + + var updateStatus = (moduleElement) => { + let statusElement = moduleElement.querySelector('[data-action="status"] > i'); + var params = { + element: moduleElement.dataset.elementshortname, + instance: moduleElement.dataset.instanceid, + status: moduleElement.dataset.visibility == true ? false : true, + cmid: Data.cm.id + }; + + if (moduleElement.dataset.visibility == true) { + statusElement.classList.remove('fa-eye'); + statusElement.classList.add('fa-eye-slash'); + moduleElement.dataset.visibility = false; + } else { + statusElement.classList.remove('fa-eye-slash'); + statusElement.classList.add('fa-eye'); + moduleElement.dataset.visibility = true; + } + + var promise = Fragment.loadFragment('mod_contentdesigner', 'update_visibility', Data.contextid, params).then(() => { + return true; + }); + LoadingIcon.addIconToContainerRemoveOnCompletion(loaderItem, promise); + }; + + + /** + * Performs an action on a element (moving, deleting, duplicating, hiding, etc.) + * + * @param {JQuery} moduleElement activity element we perform action on + * @param {Number} elementId + * @param {Number} instanceId + * @param {String} action Action of the current clicked element. + */ + var editElement = function(moduleElement, elementId, instanceId, action) { + var args = { + cmid: cmID, + action: action, + elementid: elementId, + instanceid: instanceId, + }; + Fragment.loadFragment('mod_contentdesigner', 'edit_element', contextID, args).then((html) => { + moduleElement.parentNode.replaceWith(html); + return; + }).fail(Notification.exception); + }; + + /** + * Displays the delete confirmation to delete a module + * + * @param {String} element + * @param {Function} onconfirm function to execute on confirm + */ + var confirmDeleteElement = function(element, onconfirm) { + var elementTypename = 'element_'.element; + Str.get_string('pluginname', elementTypename).done(function() { + var plugindata = { + element: element + }; + Str.get_strings([ + {key: 'confirm', component: 'core'}, + {key: 'deletechecktype', component: 'mod_contentdesigner', param: plugindata}, + {key: 'yes'}, + {key: 'no'} + ]).done(function(s) { + Notification.confirm(s[0], s[1], s[2], s[3], onconfirm); + } + ); + }); + }; + + /** + * Load the elements list modal to insert new element + * + * @param {String} position where the element need to insert. + * @param {Boolean} chapter chapter id to insert element. + * @returns {Object} + */ + const buildAddElementModal = (position = "bottom", chapter = 0) => { + var params = {cmid: Data.cm.id}; + return Modal.create({ + type: Modal.TYPE, + title: Str.get_string('addelement', 'contentdesigner'), + body: Fragment.loadFragment('mod_contentdesigner', 'get_elements_list', contextID, params), + large: false, + }).then(modal => { + modal.getRoot().on(ModalEvents.bodyRendered, function() { + modal.getRoot().get(0).querySelectorAll('.element-item').forEach((e) => { + e.addEventListener('click', function(e) { + if (e.target.closest('.element-item')) { + var element = e.currentTarget.dataset.element; + var params = { + cmid: Data.cm.id, + element: element, + chapter: chapter, + position: position, + sesskey: M.cfg.sesskey + }; + const urlParams = new URLSearchParams(params); + window.location = M.cfg.wwwroot + '/mod/contentdesigner/element.php?' + urlParams.toString(); + } + }); + }); + }); + modal.show(); + return modal; + }); + }; + + return { + init: function(contextid, cmid) { + return editor(contextid, cmid); + } + }; +}); diff --git a/amd/src/elements.js b/amd/src/elements.js new file mode 100644 index 0000000..0bb579c --- /dev/null +++ b/amd/src/elements.js @@ -0,0 +1,341 @@ +define(['jquery', 'core/fragment', 'core/templates', 'core/loadingicon', 'mod_contentdesigner/anime'], + function($, Fragment, Templates, LoadingIcon, anime) { + + /** + * Selectors. + */ + const SELECTORS = { + chapter: '[data-elementshortname="chapter"]', + chapterList: '.chapter-elements-list', + elementContent: '.element-content', + fragments: { + nextContents: 'load_remain_capter_contents' + }, + contentWrapper: '.contentdesigner-wrapper', + }; + + const detailElement = 'input[name=contentdesigner_cm_details]'; + + const contentDesignerData = () => { + var cmdetails = document.querySelector(detailElement) !== null ? document.querySelector(detailElement).value : ''; + var cmdata = (cmdetails) ? JSON.parse(cmdetails) : ''; + /* global contentDesignerElementsData */ + return cmdata || contentDesignerElementsData; + }; + + let animations = {}; + + let button = () => document.querySelector('.contentdesigner-content'); + + let courseContentSelector = 'ul.course-content-list'; + + let courseContent = () => document.querySelector(courseContentSelector); + + const contentWrapper = () => document.querySelector(SELECTORS.contentWrapper); + + const refreshContent = ()=>{ + var params = { + cmid: contentDesignerData().cmid + }; + var promise = Fragment.loadFragment('mod_contentdesigner', 'load_elements', contentDesignerData().contextid, params); + promise.done((html, js)=>{ + var fakeDiv = document.createElement('div'); + fakeDiv.innerHTML = html; + var chapters = fakeDiv.querySelector('.course-content-list').children; + var filterChapter = []; + + chapters.forEach((chapter) => { + + var elementSelector = 'li.element-item[data-instanceid="' + chapter.dataset.instanceid + '"]'; + elementSelector += '[data-elementshortname="' + chapter.dataset.elementshortname + '"]'; + let chapterSelector = 'li.chapters-list[data-id="' + chapter.dataset.id + '"]'; + + if (!document.querySelector(chapterSelector) && !document.querySelector(elementSelector)) { + filterChapter.push(chapter); + } else { + var elements = chapter.querySelectorAll('.element-item') ?? []; + elements.forEach((element) => { + var dataset = element.children[0].dataset; + var selector = 'li.element-item .element-content[data-instanceid="' + dataset.instanceid + '"]'; + selector += '[data-elementshortname="' + dataset.elementshortname + '"]'; + var elementindocument = document.querySelector(selector); + + if (!document.querySelector(selector) + && document.querySelector(chapterSelector + ' .chapter-elements-list') !== undefined) { + document.querySelector(chapterSelector + ' .chapter-elements-list').appendChild(element); + } else if (elementindocument !== null && elementindocument.dataset.replaceonrefresh == true) { + Templates.replaceNode(elementindocument, element, ''); + } + }); + if (chapter.querySelector('.chapter-elements-list') !== null) { + chapter.querySelector('.chapter-elements-list').remove(); + } + if (chapter.querySelector('.chapter-title') !== null) { + chapter.querySelector('.chapter-title').remove(); + } + if (chapter.children.length > 0 && document.querySelector(chapterSelector) != undefined) { + removeMarkBtn(chapterSelector); + document.querySelector(chapterSelector).append(chapter.children[0]); + } + } + }); + + Templates.appendNodeContents('.contentdesigner-content .course-content-list', filterChapter, js); + animateElements(); + contentWrapper().dispatchEvent(new CustomEvent('elementupdate')); // Dispatch the element update event. + } + ).catch(); + + LoadingIcon.addIconToContainerRemoveOnCompletion(button(), promise); + }; + + const removeMarkBtn = (chapterSelector) => { + if (document.querySelector(chapterSelector).querySelector('button.complete-chapter') != undefined) { + document.querySelector(chapterSelector).querySelector('button.complete-chapter').remove(); + } + }; + + /** + * Remove the warning from response. + */ + const removeWarning = () => { + if (courseContent() !== null && courseContent().querySelector('.label.label-warning') !== null) { + courseContent().querySelector('.label.label-warning').remove(); + } + }; + + const loadNextChapters = function(currentChapter) { + var params = { + cmid: contentDesignerData().cmid, + chapter: currentChapter + }; + var promise = Fragment.loadFragment('mod_contentdesigner', 'load_next_chapters', contentDesignerData().contextid, params); + promise.done((html, js)=>{ + var fakeDiv = document.createElement('div'); + fakeDiv.innerHTML = html; + var chapters = fakeDiv.querySelector('.course-content-list').children; + var filterChapter = []; + chapters.forEach((chapter) => { + var elementSelector = 'li.element-item[data-instanceid="' + chapter.dataset.instanceid + '"]'; + elementSelector += '[data-elementshortname="' + chapter.dataset.elementshortname + '"]'; + + if (!document.querySelector('li.chapters-list[data-id="' + chapter.dataset.id + '"]') + && !document.querySelector(elementSelector)) { + filterChapter.push(chapter); + } + }); + Templates.appendNodeContents('.contentdesigner-content .course-content-list', filterChapter, js); + animateElements(); + }).catch(); + + LoadingIcon.addIconToContainerRemoveOnCompletion(button(), promise); + }; + + const loadNextElements = function(currentElement) { + + Fragment.loadFragment( + 'mod_contentdesigner', SELECTORS.fragments.nextContents, + contentDesignerData().contextid, {contentid: currentElement.dataset.contentid} + ).done((html, js) => { + + const selector = currentElement.parentNode.parentNode; + + Templates.appendNodeContents(selector, html, js); + }); + }; + + /** + * Verify the given element in the viewport. + * + * @param {HTMLElement} el Element to verify + * @returns {bool} + */ + const inView = function(el) { + const rect = el.getBoundingClientRect(); + return (rect.top >= 0 && rect.left >= 0 + && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + && rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + }; + + /** + * Animate the module elements when the elements in the viewport. + */ + const animateElements = function() { + entranceAnimation(); // Init entrance animation. + scrollingEffects(); + }; + + /** + * Initilaize the scroll reveal animation for elements. + */ + function entranceAnimation() { + + var leftFrame = [ + {transform: 'translate3d(-100%, 0, 0)'}, + {transform: 'translate3d(0, 0, 0)', opacity: 1}, + ]; + + var rightFrame = [ + {transform: 'translate3d(100%, 0, 0)'}, + {transform: 'translate3d(10%, 0, 0)', opacity: 1}, + ]; + + var fadeIn = [ + {opacity: 0}, + {opacity: 1} + ]; + + const items = document.querySelectorAll('.element-item .general-options.animation'); + items.forEach((item)=>{ + var node = item; + if (node.dataset.entranceanimation == undefined || node.dataset.entranceanimation == '') { + return; + } + var data = JSON.parse(node.dataset.entranceanimation); + var speed = 1500; + if (data.duration == 'slow') { + speed = 3000; + } else if (data.duration == 'fast') { + speed = 500; + } + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + const intersecting = entry.isIntersecting; + if (intersecting) { + node = entry.target; + if (node.classList.contains('animated')) { + return; + } + + var frame = fadeIn; + if (data.animation == 'slideInLeft') { + frame = leftFrame; + } else if (data.animation == 'slideInRight') { + frame = rightFrame; + } + + setTimeout(function() { + node.classList.add('animated'); + item.animate(frame, {duration: speed || 1000}); + }, data.delay); + observer.unobserve(item); + } + }); + }); + + observer.observe(item); + }); + } + + /** + * Setup the scrolling effects. + */ + function scrollingEffects() { + + document.querySelectorAll('.element-item').forEach(function(item) { + animate(item); + }); + /* Item = document.querySelectorAll('.element-item')[5]; + animate(item); */ + } + + /** + * Animate the element based on scrolling. + * @param {HTMLELement} item + * @returns {void} + */ + function animate(item) { + + var node = item.childNodes[1]; + if (node.dataset.scrolleffect == undefined || node.dataset.scrolleffect == '') { + return; + } + var id = node.dataset.contentid; + var data = JSON.parse(node.dataset.scrolleffect); + var speed = data.speed ? (10000 / data.speed) : 0; + animations[id] = anime({ + targets: item.childNodes[1], + translateX: data.direction == 'left' ? [800, 0] : [-800, 0], + duration: speed || 500, + autoplay: false, + elasticity: 0, + easing: 'easeInOutSine' + }); + + document.getElementById('page').addEventListener('scroll', function() { + document.getElementById('page').style.overflow = 'visible'; + var scroll = animateOnScroll(item, animations[id].duration); + animations[id].seek(scroll); + }); + window.addEventListener('scroll', function() { + var scroll = animateOnScroll(item, animations[id].duration); + animations[id].seek(scroll); + }); + var modalBody = document.querySelector('.path-course-view .modal-dialog-scrollable .modal-body'); + if (modalBody !== null) { + modalBody.addEventListener('scroll', function(e) { + var scroll = animateOnScroll(item, animations[id].duration, e.target); + animations[id].seek(scroll); + }); + } + + window.addEventListener('load', function() { + setTimeout(function() { + document.getElementById('page').style.overflow = 'visible'; + }, 500); + var scroll = animateOnScroll(item, animations[id].duration); + animations[id].seek(scroll); + }); + + document.getElementById('page').style.overflow = 'visible'; + } + + /** + * Find the seek volume for the given element to move the element. + * + * @param {HTMLElement} item + * @param {double} dataspeed + * @param {HTMLElement} scrollElement + * @returns {bool} + */ + const animateOnScroll = function(item, dataspeed, scrollElement = null) { + + var node = item.childNodes[1]; + if (node.dataset.scrolleffect == undefined || node.dataset.scrolleffect == '') { + return; + } + var data = JSON.parse(node.dataset.scrolleffect); + let start = parseInt(data.start) || 100; + let end = 200; + const docElement = scrollElement || document.documentElement; + let clientHeight = docElement.clientHeight; + let itemY = () => item.getBoundingClientRect().y; + let itemTop = () => item.getBoundingClientRect().top - end; + let isStartPosition = () => itemY() + start <= (docElement.clientHeight); + let isEndPosition = () => itemY() <= end; + if (isStartPosition() && !isEndPosition()) { + var ls = docElement.scrollHeight - (docElement.clientHeight + docElement.scrollTop); + var availablescroll = Math.min(clientHeight - (end + start), ls); + var scrolled = (availablescroll > itemTop()) + ? availablescroll - itemTop() : (item.offsetTop - end - itemTop()) - availablescroll; + var percent = dataspeed / availablescroll; + var speed = scrolled * percent; + return speed; + } + return; + }; + + return { + data: contentDesignerData, // Backward compatibility. + contentDesignerData: contentDesignerData, + refreshContent: refreshContent, + loadNextChapters: loadNextChapters, + inView: inView, + animateElements: animateElements, + courseContent: courseContent, + removeWarning: removeWarning, + loadNextElements: loadNextElements, + selectors: SELECTORS + }; +}); diff --git a/backup/moodle2/backup_contentdesigner_activity_task.class.php b/backup/moodle2/backup_contentdesigner_activity_task.class.php new file mode 100644 index 0000000..a06216d --- /dev/null +++ b/backup/moodle2/backup_contentdesigner_activity_task.class.php @@ -0,0 +1,58 @@ +. + +/** + * Definition backup-activity-task + * + * @package mod_contentdesigner + * @copyright 2022, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('No direct access !'); + +require_once($CFG->dirroot . '/mod/contentdesigner/backup/moodle2/backup_contentdesigner_stepslib.php'); + +/** + * Step to perform instance database backup. + */ +class backup_contentdesigner_activity_task extends backup_activity_task { + + /** + * No specific settings for this activity + */ + public function define_my_settings() { + // Content deisnger don't have any specified settings. + } + + /** + * Define backup structure steps to store the instance data in the contentdesigner.xml. + */ + public function define_my_steps() { + // Only single structure step. + $this->add_step(new backup_contentdesigner_activity_structure_step('contentdesigner_structure', 'contentdesigner.xml')); + } + + /** + * No content encoding needed for this activity + * + * @param string $content some HTML text that eventually contains URLs to the activity instance scripts + * @return string the same content with no changes + */ + public static function encode_content_links($content) { + return $content; + } +} diff --git a/backup/moodle2/backup_contentdesigner_stepslib.php b/backup/moodle2/backup_contentdesigner_stepslib.php new file mode 100644 index 0000000..784ccfc --- /dev/null +++ b/backup/moodle2/backup_contentdesigner_stepslib.php @@ -0,0 +1,96 @@ +. + +/** + * Definition backup-steps + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_contentdesigner\editor; + +/** + * Define the complete contentdesigner structure for backup, with file and id annotations. + */ +class backup_contentdesigner_activity_structure_step extends backup_activity_structure_step { + + /** + * Define backup steps structure. + */ + protected function define_structure() { + + $userinfo = $this->get_setting_value('userinfo'); + + // Define each element separated - table fields. + $contentdesigner = new backup_nested_element('contentdesigner', array('id'), array( + 'course', 'name', 'intro', 'introformat', 'timecreated', + 'timemodified')); + + $this->add_subplugin_structure('element', $contentdesigner, true); + + $elements = new backup_nested_element('elements'); + $contentdesignerelements = new backup_nested_element('contentdesigner_elements', array('id'), array( + 'shortname', 'visible', 'timecreated')); + + $content = new backup_nested_element('content'); + $contentdesignercontent = new backup_nested_element('contentdesigner_content', array('id'), array( + 'contentdesignerid', 'element', 'instance', 'chapter', 'position', 'timecreated', 'timemodified' + )); + + $options = new backup_nested_element('contentdesigneropitons'); + $contentdesigneroptions = new backup_nested_element('contentdesigner_options', array('id'), array( + 'element', 'instance', 'margin', 'padding', 'abovecolorbg', 'abovegradientbg', 'bgimage', 'belowcolorbg', + 'belowgradientbg', 'animation', 'duration', 'delay', 'direction', 'speed', 'viewport', 'hidedesktop', 'hidetablet', + 'hidemobile', 'timecreated', 'timemodified' + )); + + // Build the tree. + $contentdesigner->add_child($elements); + $elements->add_child($contentdesignerelements); + + $contentdesigner->add_child($content); + $content->add_child($contentdesignercontent); + + $contentdesigner->add_child($options); + $options->add_child($contentdesigneroptions); + + // Define sources. + // Define source to backup. + $contentdesigner->set_source_table('contentdesigner', array('id' => backup::VAR_ACTIVITYID)); + $contentdesignerelements->set_source_sql('SELECT * FROM {contentdesigner_elements}', array()); // Get all records. + $contentdesignercontent->set_source_table('contentdesigner_content', array('contentdesignerid' => backup::VAR_PARENTID)); + + $sql = 'SELECT co.* FROM {contentdesigner_content} cc + JOIN {contentdesigner_options} co ON co.element=cc.element AND co.instance=cc.instance + WHERE cc.contentdesignerid=:contentdesignerid'; + + $contentdesigneroptions->set_source_sql($sql, ['contentdesignerid' => backup::VAR_PARENTID]); + + // Define file annotations. + $contentdesigner->annotate_files('mod_contentdesigner', 'intro', null); + + $plugins = editor::get_elements(); + foreach ($plugins as $plugin => $version) { + $filearea = $plugin.'elementbg'; + $contentdesigneroptions->annotate_files('mod_contentdesigner', $filearea, null); + } + + // Return the root element (data), wrapped into standard activity structure. + return $this->prepare_activity_structure($contentdesigner); + } +} diff --git a/backup/moodle2/restore_contentdesigner_activity_task.class.php b/backup/moodle2/restore_contentdesigner_activity_task.class.php new file mode 100644 index 0000000..285b57d --- /dev/null +++ b/backup/moodle2/restore_contentdesigner_activity_task.class.php @@ -0,0 +1,67 @@ +. + +/** + * Definition restore activity task. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die('No direct access!'); + +require_once($CFG->dirroot . '/mod/contentdesigner/backup/moodle2/restore_contentdesigner_stepslib.php'); + +/** + * Pulse restore task that provides all the settings and steps to perform one. complete restore of the activity + */ +class restore_contentdesigner_activity_task extends restore_activity_task { + + /** + * Define particular settings for this activity. + */ + protected function define_my_settings() { + // No particular settings for this activity. + } + + /** + * Define restore structure steps to restore to database from contentdesigner.xml. + */ + protected function define_my_steps() { + $this->add_step(new restore_contentdesigner_activity_structure_step('contentdesigner_structure', 'contentdesigner.xml')); + } + + /** + * Define the contents in the activity that must be + * processed by the link decoder + */ + public static function define_decode_contents() { + $contents = []; + $contents[] = new restore_decode_content('contentdesigner', ['intro'], 'contentdesigner'); + $contents[] = new \restore_decode_content('element_richtext', ['content'], 'richtext_instanceid'); + $contents[] = new \restore_decode_content('element_outro', ['outrocontent'], 'outro_instanceid'); + return $contents; + } + + /** + * Define the decoding rules for links belonging + * to the activity to be executed by the link decoder + */ + public static function define_decode_rules() { + return []; + } +} diff --git a/backup/moodle2/restore_contentdesigner_stepslib.php b/backup/moodle2/restore_contentdesigner_stepslib.php new file mode 100644 index 0000000..bc4e9dc --- /dev/null +++ b/backup/moodle2/restore_contentdesigner_stepslib.php @@ -0,0 +1,168 @@ +. + +/** + * Definition restore structure steps. + * + * @package mod_contentdesigner + * @copyright 2022, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_contentdesigner\editor; + +/** + * Define all the restore steps that will be used by the restore_contentdesigner_activity_task + */ + +/** + * Structure step to restore contentdesigner activity. + */ +class restore_contentdesigner_activity_structure_step extends restore_activity_structure_step { + + /** + * Restore steps structure definition. + */ + protected function define_structure() { + $paths = array(); + + $userinfo = $this->get_setting_value('userinfo'); + + // Restore path. + $element = new restore_path_element('contentdesigner', '/activity/contentdesigner'); + $paths[] = $element; + + // Restore elements. + $elements = new restore_path_element('contentdesigner_elements', + '/activity/contentdesigner/elements/contentdesigner_elements'); + $paths[] = $elements; + + $this->add_subplugin_structure('element', $element); + + $paths[] = new restore_path_element('contentdesigner_content', + '/activity/contentdesigner/content/contentdesigner_content'); + + // Restor general options of element instance. + $paths[] = new restore_path_element('contentdesigner_options', + '/activity/contentdesigner/contentdesigneropitons/contentdesigner_options'); + + // Return the paths wrapped into standard activity structure. + return $this->prepare_activity_structure($paths); + } + + /** + * Process activity contentdesigner restore. + * @param mixed $data restore contentdesigner table data. + */ + protected function process_contentdesigner($data) { + global $DB; + + $data = (object) $data; + $oldid = $data->id; + $data->course = $this->get_courseid(); + // Insert instance into Database. + $newitemid = $DB->insert_record('contentdesigner', $data); + // Immediately after inserting "activity" record, call this. + $this->apply_activity_instance($newitemid); + } + + /** + * Process contentdesigner users records. + * + * @param object $data The data in object form + * @return void + */ + protected function process_contentdesigner_elements($data) { + global $DB; + + $data = (object) $data; + + // If already element inserted during the plugin installation then use the current element id. + $elementid = $DB->get_field('contentdesigner_elements', 'id', ['shortname' => $data->shortname]); + if (!$elementid) { + // If not inserted then create new one. + $elementid = $DB->insert_record('contentdesigner_elements', $data); + } + $this->set_mapping('elements', $data->id, $elementid); + } + + /** + * Process contentdesigner users records. + * + * @param object $data The data in object form + * @return void + */ + protected function process_contentdesigner_content($data) { + global $DB; + + $data = (object) $data; + + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + + $elementid = $this->get_mappingid('elements', $data->element); + $data->element = $elementid; + $elementname = $DB->get_field('contentdesigner_elements', 'shortname', ['id' => $data->element]); + + // Update the new content and chapter instance. + $data->instance = $this->get_mappingid($elementname."_instanceid", $data->instance); + $data->chapter = $this->get_mappingid("chapterid", $data->chapter); + + $data->timemodified = time(); + // Insert the content with new chapter and instance. + $contentid = $DB->insert_record('contentdesigner_content', $data); + $contents = $DB->get_field('element_chapter', 'contents', ['id' => $data->chapter]); + $contents = explode(',', $contents); + array_push($contents, $contentid); + + // Add the latest cotnent id in chapter. + $content = (object) ['id' => $data->chapter, 'contents' => implode(',', $contents)]; + $DB->update_record('element_chapter', $content); + } + + /** + * Process contentdesigner general options records. + * + * @param object $data The data in object form + * @return void + */ + protected function process_contentdesigner_options($data) { + global $DB; + + $data = (object) $data; + + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + + // Update the new content and chapter instance. + $elementid = $this->get_mappingid('elements', $data->element); + $data->element = $elementid; + + $elementname = $DB->get_field('contentdesigner_elements', 'shortname', ['id' => $data->element]); + $data->instance = $this->get_mappingid($elementname."_instanceid", $data->instance); + $data->timemodified = time(); + // Insert the general options for the element instance. + $DB->insert_record('contentdesigner_options', $data); + } + + /** + * Update the files of editors after restore execution. + * + * @return void + */ + protected function after_execute() { + // Add contentdesigner related files. + $this->add_related_files('mod_contentdesigner', 'intro', null); + } +} diff --git a/classes/editor.php b/classes/editor.php new file mode 100644 index 0000000..999c927 --- /dev/null +++ b/classes/editor.php @@ -0,0 +1,475 @@ +. + +/** + * Content designer editor page helps to manage element. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner; + +use html_writer; + +/** + * Mod contnet designer editor class. + */ +class editor { + + /** + * Coursemodule instance + * + * @var cminfo + */ + public $cm; + + /** + * Course record object + * + * @var stdclass + */ + public $course; + + /** + * Course module context object. + * + * @var context_module + */ + public $cmcontext; + + /** + * Chapter element instance. + * + * @var element_chapter\element + */ + public $chapter; + + /** + * Constructor, setup the class variables and course module objects. + * + * @param stdclass $cm Course Moudle instance record. + * @param stdclass $course Course record object. + */ + public function __construct($cm, $course) { + $this->cm = $cm; + $this->course = $course; + $this->cmcontext = \context_module::instance($cm->id); + $this->chapter = new \element_chapter\element($this->cm->id); + } + + /** + * Display the available elements list to manage in the editor view. + * + * @return string HTML of the available elments elementbox. + */ + public function display() { + global $OUTPUT, $DB; + + $data = [ + 'cm' => $this->cm, + 'course' => $this->course, + 'chapters' => $this->chapter->get_chapters_data(), + 'outro' => $this->get_module_outro(), + ]; + + // Hide the add element option after outro. + $data['chapterscount'] = count($data['chapters']); + return $OUTPUT->render_from_template('mod_contentdesigner/editor', $data); + } + + /** + * Render the module elements for student view. + * @param int $chapterafter Load the chapters after the given chapter. + * @return void + */ + public function render_elements($chapterafter=false) { + global $OUTPUT, $DB; + + $data = [ + 'cm' => $this->cm, + 'course' => $this->course, + 'progressbar' => $this->chapter->build_progress(), + 'chapters' => $this->chapter->get_chapters_data(true, true, $chapterafter), + 'outro' => $this->render_module_outro(), + 'cmdetails' => $this->cm_details(), + ]; + + $end = (!empty($data['chapters'])) ? end($data['chapters']) : ''; + if (!empty($end)) { + $data['prevent'] = $end['prevent'] ? true : false; + $data['chapterprevent'] = $end['chapterprevent'] ? true : false; + } + + return $OUTPUT->render_from_template('mod_contentdesigner/content', $data); + } + + /** + * Send the course module details as hidden input, data will fetched in the Element.js file to prevent the global value issue. + * + * @return string + */ + public function cm_details() { + $data = [ + 'cmid' => $this->cm->id, + 'contextid' => \context_module::instance($this->cm->id)->id, + 'contentdesignerid' => $this->cm->instance + ]; + return html_writer::empty_tag('input', [ + 'type' => 'hidden', 'name' => 'contentdesigner_cm_details', 'value' => json_encode($data) + ]); + } + + /** + * Generate the course and cm data used in the JS. + * + * @return void + */ + public function init_data_forjs() { + global $PAGE; + $data = ['cm' => $this->cm, 'course' => $this->course, 'contextid' => \context_module::instance($this->cm->id)->id]; + $PAGE->requires->data_for_js('contentDesigner', $data); + } + + /** + * Initialize the javascript modules from the available elements. + * + * Note: if you want to add js for each instance then insert your module call on render function insteed of here. + * + * @return void + */ + public function initiate_js() { + global $PAGE; + $plugins = \core_plugin_manager::instance()->get_installed_plugins('element'); + foreach ($plugins as $plugin => $version) { + $elementobj = self::get_element($plugin, $this->cm->id); + $elementobj->initiate_js(); + } + } + + /** + * Create the instance of the editor class. + * + * @param int $cmid Course Module id. + * @return editor Mod_contentdeisnger/editor class instance. + */ + public static function get_editor($cmid) { + list($course, $cm) = get_course_and_cm_from_cmid($cmid); + return new self($cm, $course); + } + + /** + * Get list of available elements for the modal to insert. + * + * @param int $cmid course module id. + * @return string HTML of the elements list. + */ + public static function get_elements_list(int $cmid) { + + $plugins = \core_plugin_manager::instance()->get_installed_plugins('element'); + + $li = []; + foreach ($plugins as $plugin => $version) { + $elementobj = self::get_element($plugin, $cmid); + if (!$elementobj->supports_multiple_instance()) { + continue; + } + $info = $elementobj->info(); + $description = html_writer::span($info->description, 'element-description'); + $name = html_writer::span($info->name, 'element-name'); + + $li[] = html_writer::tag('li', + $info->icon . $name . $description, + ['data-element' => $info->shortname, 'class' => 'element-item'] + ); + } + + return html_writer::tag('ul', implode('', $li), ['class' => 'elements-list']); + } + + /** + * Returns the given elements class instance object. + * + * @param int|string $element + * @param int|null $cmid + * @return \elements + */ + public static function get_element($element, $cmid=null) { + global $DB; + if (is_number($element)) { + $element = $DB->get_field('contentdesigner_elements', 'shortname', ['id' => $element]); + } + $class = 'element_'.$element.'\element'; + if (class_exists($class)) { + return new $class($cmid); + } else { + throw new \moodle_exception('elementnotfound', 'mod_contentdesigner'); + } + } + + /** + * Fetch list of installed elements. + * + * @return array List of elements. + */ + public static function get_elements() { + $plugins = \core_plugin_manager::instance()->get_installed_plugins('element'); + return $plugins; + } + + /** + * Fetch the file areas from the elements. Fetch the fileareas and concat the element component name with filearea. + * Use this function and define the fileareas Which is uses the mod_contentdesigner as the component for storing the files. + * + * @param int $cmid course module id + * @return array List of filearea. + */ + public static function get_elements_areafiles($cmid) { + $plugins = self::get_elements(); + $files = []; + foreach ($plugins as $plugin => $version) { + $elementobj = self::get_element($plugin, $cmid); + $areafiles = (method_exists($elementobj, 'areafiles')) ? $elementobj->areafiles() : []; + array_walk($areafiles, function(&$areafile) use ($plugin) { + $areafile = "element_".$plugin."_".$areafile; + }); + $files = array_merge($files, $areafiles); + } + + return $files; + } + + /** + * Get the default outro element for the module. if not available then creates the new one. + * Outro only created automatically, can't have option to create manaully. + * + * @return string Rendered element box view of the outro. + */ + public function get_module_outro() { + global $OUTPUT, $DB; + + $element = self::get_element('outro', $this->cm->id); + $instance = $DB->get_field('element_outro', 'id', + ['contentdesignerid' => $this->cm->instance]); + + if (!$instance) { + $instance = $element->create_basic_instance($this->cm->instance); + } + $instancedata = $element->get_instance($instance); + + $editurl = new \moodle_url('/mod/contentdesigner/element.php', [ + 'cmid' => $this->cm->id, + 'element' => $element->shortname, + 'id' => $instancedata->id, + 'sesskey' => sesskey() + ]); + return $OUTPUT->render_from_template('mod_contentdesigner/elementbox', [ + 'info' => $element->info(), + 'instancedata' => $instancedata, + 'editurl' => $editurl, + 'hidemove' => true, + 'hidevisible' => true, + 'hidedelete' => true + ]); + } + + /** + * Render the module outro element. + * + * @return array Rendered element box view of the outro. + */ + public function render_module_outro() { + global $DB; + $element = self::get_element('outro', $this->cm->id); + $instance = $DB->get_record('element_outro', ['contentdesignerid' => $this->cm->instance]); + if ($instance) { + $instancedata = $element->prepare_formdata($instance->id); + $data = [ + 'contents' => $element->render($instancedata), + 'instancedata' => $instance, + 'element' => $element->elementid, + 'info' => $element->info(), + ]; + return $data; + } + return []; + } + + /** + * Fetch the genernal options as records. + * + * @param int $instanceid Element instance id + * @param int $elementid Element list id. + * @return void + */ + public function get_option($instanceid, $elementid) { + global $DB; + $record = $DB->get_record('contentdesigner_options', ['instance' => $instanceid, 'element' => $elementid]); + if (!empty($record)) { + $element = self::get_element($record->element, $this->cm->id); + $record->backimage = $this->get_element_areafiles($element->shortname."elementbg", $instanceid); + } + return $record; + } + + /** + * Fetch the files from for the filearea. + * + * @param string $filearea Name of the filearea. + * @param int $itemid Id for the filearea. + * @param string $component Plugin component name. + * @param context_module $context Course module instance object. + * @return string File Path of the given fileareas, If not false. + */ + public function get_element_areafiles($filearea, $itemid=0, $component='mod_contentdesigner', $context=null) { + $context = ($context === null) ? \context_module::instance($this->cm->id) : $context; + $files = get_file_storage()->get_area_files( + $context->id, $component, $filearea, $itemid, 'itemid, filepath, filename', false); + if (empty($files) ) { + return ''; + } + $file = current($files); + $fileurl = \moodle_url::make_pluginfile_url( + $file->get_contextid(), + $file->get_component(), + $file->get_filearea(), + $file->get_itemid(), + $file->get_filepath(), + $file->get_filename(), false); + return $fileurl->out(false); + } + + /** + * Create the element instance and add to the module chapter. + * It creates the elements basic instance, works when the elment insert works using ajax. + * + * @param int $elementid + * @param int|null $chapterid + * @return string HTML of the element to insert to editor. + */ + public function insert_element($elementid, $chapterid=null) { + global $OUTPUT, $DB; + + $data = (object) ['cm' => $this->cm->id, 'course' => $this->course->id]; + $element = self::get_element($elementid, $this->cm->id); + $data->info = $element->info(); + + try { + $transaction = $DB->start_delegated_transaction(); + // Create basic instance for element EX:elemnet_h5p. + $data->instance = $element->create_basic_instance($this->cm->instance); + $data->instancedata = $element->get_instance($data->instance); + + if ($element->supports_content()) { + + if ($chapterid == null) { + $chapterid = $this->chapter->get_default($this->cm->instance, true); + } + // Insert element in content section. + $content = $this->add_module_element($element, $data->instance, $chapterid); + $data->id = $content->id; + // Add the content id of the element in chapter sequence. + $this->set_elements_inchapter($chapterid, $content->id); + + $settings = [ + 'element' => $elementid, + 'instance' => $data->instance, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + // Insert global options for element instance. + if (!$DB->insert_record('contentdesigner_options', $settings)) { + throw new \moodle_exception('settingnotcreated', 'mod_contentdesigner'); + } + } + + $transaction->allow_commit(); + $data->instancedata->title = ($element->supports_content()) + ? $element->title_editable($data->instancedata) : $element->element_name(); + + // TODO: want to control multiple elements then implement new method in the abstract elements. + if ($element->info()->shortname == 'chapter') { + $elementsbox = $OUTPUT->render_from_template('mod_contentdesigner/chapter', $data); + return html_writer::tag('li', $elementsbox, ['class' => 'chapters_list']); + } else { + $elementsbox = $OUTPUT->render_from_template('mod_contentdesigner/elementbox', $data); + return html_writer::tag('li', $elementsbox, ['class' => 'elements_list']); + } + + } catch (\Exception $e) { + // Extra cleanup steps. + $transaction->rollback($e); // Rethrows exception. + } + } + + + /** + * Add the element instance to the module contents table. which contains the list of instances. + * + * @param stdclass $element Element class instance. + * @param int $instanceid Element instance ID. + * @param int $chapter Chapter id. + * @param bool $position Insert the element in top( means 1) of the chapter or bootom + * @return object content data to insert. + */ + public function add_module_element($element, $instanceid, $chapter, $position=0) { + global $DB; + + $content = (object) [ + 'contentdesignerid' => $this->cm->instance, + 'element' => $element->elementid, + 'instance' => $instanceid, + 'chapter' => $chapter, + 'timecreated' => time(), + 'timemodified' => time() + ]; + + if ($contentid = $element->get_instance_contentid($instanceid)) { + $content->id = $contentid; + + $content->id = $DB->update_record('contentdesigner_content', $content); + } else { + $lastelement = 0; + if ($position) { + $DB->execute('UPDATE {contentdesigner_content} SET position=position+1 + WHERE contentdesignerid = ? AND chapter=?', [$this->cm->instance, $chapter]); + } else { + // Get the latest positions of the chapter element in element going to insert in bottom. + $lastelement = (int) $DB->get_field_sql('SELECT max(position) from {contentdesigner_content} + WHERE contentdesignerid = ? AND chapter=?', [$this->cm->instance, $chapter] + ); + } + $content->position = $lastelement ? $lastelement + 1 : 1; + $content->id = $DB->insert_record('contentdesigner_content', $content); + + } + return $content; + } + + /** + * Set the element instances to the chapter. + * + * @param int $chapterid Chapter id need to insert. + * @param int $contentid contentdesigner_content id of the element instance. + * @return void + */ + public function set_elements_inchapter($chapterid, $contentid) { + global $DB; + $chapter = new \element_chapter\element($this->cm->id); + return $chapter->set_elements($chapterid, $contentid); + } +} diff --git a/classes/elements.php b/classes/elements.php new file mode 100644 index 0000000..334762b --- /dev/null +++ b/classes/elements.php @@ -0,0 +1,704 @@ +. + +/** + * Base class for Content designer elements. Commonly used elements methods are defined here. + * + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner; + +/** + * Base class for Content designer elements. + */ +abstract class elements { + + /** + * Context data for the current module. + * + * @var context_module + */ + public $context; + + /** + * Course module ID. + * + * @var int $cmid + */ + public $cmid; + + /** + * Elment Short name. + * + * @var string + */ + public $shortname; + + /** + * Elment table name. + * + * @var string + */ + public $tablename; + + /** + * Element ID + * + * @var int $elementid + */ + public $elementid; + + /** + * Course module + * + * @var cm_info $cm course module data + */ + public $cm; + + /** + * Course Objecet + * + * @var mixed $course Course object + */ + public $course; + + /** + * This element is not mandatory. + */ + public const ENABLE_MANDATORY = 1; + + /** + * Disable the mandatory for this elemnet. + */ + public const DISBLE_MANDATORY = 0; + + /** + * Constructor method, Setup the element basic information and context. + * + * @param int $cmid + */ + public function __construct($cmid) { + $this->cmid = $cmid; + $this->shortname = $this->element_shortname(); + $this->tablename = 'element_'.$this->shortname; + $this->elementid = $this->element_id(); + $this->context = $this->get_context(); + if ($cmid) { + list($course, $cm) = get_course_and_cm_from_cmid($cmid); + $this->cm = $cm; + $this->course = $course; + } + } + + /** + * Element name which is visbile for the users + * + * @return string + */ + abstract public function element_name(); + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + abstract public function element_shortname(); + + /** + * Element form element definition. + * + * @param moodle_form $mfrom + * @param genreal_element_form $formobj + * @return void + */ + abstract public function element_form(&$mfrom, $formobj); + + /** + * Render the view of the element instance which is displayed to the users. + * + * @param stdclass $instance + * @return void + */ + abstract public function render($instance); + + /** + * Verify the elements the standard general options list. + * + * @return bool + */ + public function supports_standard_elements() { + // By default all the elmenets will supports the standard options. + return true; + } + + /** + * Verify the element is supports the content render method. + * + * @return bool + */ + public function supports_content() { + return true; + } + + /** + * Is the element supports the multiple instance for one activity instance. ie(element_outro) + * + * @return bool + */ + public function supports_multiple_instance() { + return true; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + return $output->pix_icon('t/viewdetails', get_string('plugin')); + } + + /** + * Element description, By default description tried from plugin strings list. + * + * @return string + */ + public function element_description() { + return (get_string_manager()->string_exists('elementdescription', 'element_'.$this->element_shortname())) + ? get_string('elementdescription', 'element_'.$this->element_shortname()) : ''; + } + + /** + * Save the area files data after the element instance moodle_form submittted. + * If the element override the method then should call the parent to save the baackgroung image files. + * + * @param stdclas $data Submitted moodle_form data. + */ + public function save_areafiles($data) { + if (isset($data->bgimage)) { + file_save_draft_area_files($data->bgimage, $data->contextid, 'mod_contentdesigner', + $data->elementshortname.'elementbg', $data->instance + ); + } + } + + /** + * Prepare the form editor elements file data before render the elemnent form. + * + * @param stdclass $formdata + * @return stdclass + */ + public function prepare_standard_file_editor(&$formdata) { + if (isset($formdata->instance)) { + $draftitemid = file_get_submitted_draft_itemid('bgimage'); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_contentdesigner', $this->element_shortname().'elementbg', + $formdata->instance, array('subdirs' => 0, 'maxfiles' => 1)); + $formdata->bgimage = $draftitemid; + } + return $formdata; + } + + /** + * Get the context instance for the course module. + * + * @return \context_module + */ + public function get_context() { + $context = \context_module::instance($this->cmid); + return $context; + } + + + + /** + * Simple information about the element. Used in the element box. + * + * @return object + */ + public function info() { + global $OUTPUT; + return (object) [ + 'elementid' => $this->element_id(), + 'name' => $this->element_name(), + 'shortname' => $this->shortname, + 'icon' => $this->icon($OUTPUT), + 'description' => $this->element_description(), + ]; + } + + /** + * Fetch the record of the cotnent designer module instance. + * + * @return stdclass + */ + public function get_contentdesigner() { + global $DB; + $cm = get_coursemodule_from_id('contentdesigner', $this->cmid); + return $DB->get_record('contentdesigner', ['id' => $cm->instance]); + } + + /** + * Get the course module data from the module instance. + * + * @param int $contentdesignerid + * @return cminfo Course module record. + */ + public function get_cm_from_modinstance($contentdesignerid) { + $cm = get_coursemodule_from_instance('contentdesigner', $contentdesignerid); + return $cm; + } + + /** + * Vertify the element instance is prevents the loading of next element instance. + * For example please check the element_h5p + * + * @param stdclass $instance Instance data of the element. + * @return bool True if need to stop the next instance Otherwise false if render of next elements. + */ + public function prevent_nextelements($instance): bool { + return false; + } + + /** + * Replace the element on refersh the content. Some elements may need to update the content on refresh the elmenet. + */ + public function supports_replace_onrefresh() : bool { + return false; + } + + /** + * Initiate the element js for the view page. + * + * @return void + */ + public function initiate_js() { + global $PAGE; + $data = [ + 'cmid' => $this->cmid, + 'contextid' => \context_module::instance($this->cmid)->id, + 'contentdesignerid' => $this->cm->instance + ]; + $PAGE->requires->data_for_js('contentDesignerElementsData', $data); + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $instance + * @return void + */ + public function render_element($instance) { + $options = []; + $data = $this->prepare_formdata($instance->id); + $html = $this->render($data); + return ['elementcontent' => $html, 'general' => $options]; + } + + /** + * Load the element title into inline editable method. + * + * @param stdclass $instance Element Instance data. + * @return string + */ + public function title_editable($instance) { + global $OUTPUT, $PAGE; + $title = $instance->title ?: $this->info()->name; + $name = 'instance_title['.$this->shortname.']['.$instance->id.']'; + // TODO: Need to implement capability in place of true 4th param. + $tmpl = new \core\output\inplace_editable('mod_contentdesigner', $name, $this->elementid.$instance->id, + true, format_string($title), $title, "Edit the title of instance" , 'New value for ' . format_string($title)); + + return $OUTPUT->render($tmpl); + } + + + /** + * Add elements in DB. During the plugin installtion elements will inserted and created id for elements. + * Elmenets instance will identified usign the element id. + * + * @param string $shortname Shortname of the element. + * @return bool + */ + public static function insertelement(string $shortname) { + global $DB; + $record = ['shortname' => $shortname, 'timemodified' => time()]; + if (!$DB->record_exists('contentdesigner_elements', ['shortname' => $shortname])) { + return $DB->insert_record('contentdesigner_elements', $record); + } + return true; + } + + /** + * Get the Id of the element in the list of available elements list, this id created during the element installation. + * + * @return int ID of the element. + */ + public function element_id() { + global $DB; + return $DB->get_field('contentdesigner_elements', 'id', ['shortname' => $this->element_shortname()]); + } + + /** + * Get the table name of the current element. By default its the shortname followed by keyword (element_SHORTNAME) + * + * @return string Name of the table. + */ + public function tablename() { + return 'element_'.$this->element_shortname(); + } + + /** + * Verify the element table is exists in the DB. + * + * @return bool + */ + public function is_table_exists() { + global $DB; + $dbman = $DB->get_manager(); + $table = new \xmldb_table($this->tablename); + return ($dbman->table_exists($table)) ? true : false; + } + + /** + * Create the basic instance for the element. Override this function if need to add custom changes. + * + * @param int $contentdesignerid Contnet deisnger instance id. + * @return int Element instance id. + */ + public function create_basic_instance($contentdesignerid) { + global $DB; + + $record = [ + 'contentdesignerid' => $contentdesignerid, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + + if ($this->is_table_exists()) { + + if ($this->tablename = 'element_outro') { + $record['outrocontent'] = ''; + $record['outrocontentformat'] = FORMAT_HTML; + } + + return $DB->insert_record($this->tablename, $record); + } else { + throw new \moodle_exception('tablenotfound', 'contentdesigner'); + } + } + + /** + * Get the element instance data from the give isntanceid. Filter by the visible status. + * + * @param int $instanceid Instance id. + * @param bool $visible Filter by visibility. + * @return stdclass Instance record + */ + public function get_instance(int $instanceid, $visible=false) { + global $DB; + + if ($this->is_table_exists()) { + + $params = ['id' => $instanceid, 'elementid' => $this->elementid]; + $sql = 'SELECT co.*, ee.*, co.id as optionid FROM {'.$this->tablename.'} ee + LEFT JOIN {contentdesigner_options} co ON ee.id = co.instance AND co.element=:elementid + WHERE ee.id = :id'; + if ($visible) { + $sql .= ' AND ee.visible=:visible '; + $params += ['visible' => 1]; + } + + if ($record = $DB->get_record_sql($sql, $params)) { + return $record; + } + } + return false; + } + + /** + * Get the content id of the elemnet instance. + * + * @param int $instanceid Element instance id. + * @return int ID of content. + */ + public function get_instance_contentid(int $instanceid) { + global $DB; + $contentid = $DB->get_field('contentdesigner_content', 'id', ['element' => $this->elementid, 'instance' => $instanceid]); + return ($contentid) ? $contentid : false; + } + + /** + * Get the element instance general options. + * + * @param int $instanceid Element instance id. + * @return stdclass Record of the general options for the elemnent isntnace. + */ + public function get_instance_options($instanceid): array { + global $DB; + + return (array) ($DB->get_record('contentdesigner_options', [ + 'instance' => $instanceid, 'element' => $this->elementid + ]) ?: []); + } + + /** + * Prepare data for the element moodle form. + * + * @param int $instanceid Element instance id. + * @return object + */ + public function prepare_formdata($instanceid) { + $instancedata = (array) $this->get_instance($instanceid); + $instancedata['cmid'] = $this->cmid; + return (object) ($instancedata); + } + + /** + * Process the update of element instance and genreal options. + * If element doesn't have any chapters then create new default chpater and inserted into the chapter. + * + * @param stdclass $data Submitted element moodle form data + * @return void + */ + public function update_element($data) { + global $DB; + + try { + $transaction = $DB->start_delegated_transaction(); + + $instanceid = $this->update_instance($data); + // Setup instanceid if the elment is not inserted before. + $data->instance = ($data->instanceid) ?: $instanceid; + if ($this->supports_content() && $this->supports_multiple_instance()) { + $editor = editor::get_editor($this->cmid); + if (!isset($data->chapterid) || $data->chapterid == null) { + $data->chapterid = $editor->chapter->get_default($this->cm->instance, true); + } + $position = (isset($data->position) && $data->position == 'top') ? 1 : 0; + // Insert element in content section. + $content = $editor->add_module_element($this, $data->instance, $data->chapterid, $position); + + $data->contentid = $content->id; + // Add the content id of the element in chapter sequence. + $editor->set_elements_inchapter($data->chapterid, $content->id); + } + + $this->save_areafiles($data); + + // Update the element general options. + $this->update_options($data); + + $transaction->allow_commit(); + + } catch (\Exception $e) { + // Extra cleanup steps. + $transaction->rollback($e); // Rethrows exception. + } + } + + /** + * Update the general options of the element instance. + * + * @param stdclass $data + * @return void + */ + public function update_options($data) { + global $DB; + + if ($options = $this->get_instance_options($data->instance)) { + $optiondata = $data; + $optiondata->id = $options['id']; + // Update exist settings. + $DB->update_record('contentdesigner_options', $optiondata); + } else { + // Insert new record. + $DB->insert_record('contentdesigner_options', $data); + } + } + + /** + * Update the element instance. Override the function in elements element class to add custom rules. + * + * @param stdclass $data + * @return void + */ + public function update_instance($data) { + global $DB; + + if ($data->instanceid == false) { + $data->timemodified = time(); + $data->timecreated = time(); + return $DB->insert_record($this->tablename, $data); + } else { + $data->timecreated = time(); + $data->id = $data->instanceid; + if ($DB->update_record($this->tablename, $data)) { + return $data->id; + } + } + } + + /** + * Delete the element settings. + * + * @param int $instanceid + * @return boolean status. + */ + public function delete_element($instanceid) { + global $DB; + try { + $transaction = $DB->start_delegated_transaction(); + // Delete the element settings. + if ($this->get_instance($instanceid)) { + $DB->delete_records($this->tablename(), array('id' => $instanceid)); + } + $DB->delete_records('contentdesigner_content', array('element' => $this->element_id(), + 'instance' => $instanceid)); + if ($this->get_instance_options($instanceid)) { + // Delete the element general settings. + $DB->delete_records('contentdesigner_options', array('element' => $this->element_id(), + 'instance' => $instanceid)); + } + $transaction->allow_commit(); + return true; + } catch (\Exception $e) { + // Extra cleanup steps. + $transaction->rollback($e); // Rethrows exception. + throw new \moodle_exception('chapternotdeleted', 'element_chapter'); + } + } + + /** + * Update the visibility of the elements instance. + * + * @param int $instanceid Element instance id. + * @param int $visibility Status of the element visibility + * @return bool Result of the DB update + */ + public function update_visibility($instanceid, $visibility) { + global $DB; + $instance = $this->get_instance($instanceid); + if ($instance) { + $instance->visible = $visibility; + $instance->timemodified = time(); + return $DB->update_record($this->tablename, $instance); + } + } + + /** + * Generate the classes based on the genenral options. + * + * @param stdclass $instance Element instance data + * @param stdclass $option element general options data. + * @return stdclass $instance Instance data. + */ + public function load_option_classes($instance, $option) { + $class[] = ($instance->animation) ? 'animation' : ''; + $instance->entranceanimation = json_encode([ + 'animation' => $instance->animation, + 'duration' => $instance->duration, + 'delay' => $instance->delay ? $instance->delay : '' + ]); + + $class[] = $instance->hidedesktop ? 'd-lg-none' : 'd-lg-block'; + $class[] = $instance->hidetablet ? 'd-md-none' : 'd-md-block'; + $class[] = $instance->hidemobile ? 'd-none' : 'd-block'; + + $style[] = sprintf('margin: %s;', $instance->margin); + $style[] = sprintf('padding: %s;', $instance->padding); + $class[] = !empty($option->backimage) ? 'backimage' : ''; + $class[] = (!empty($option->abovecolorbg) || !empty($option->belowcolorbg)) ? 'backcolor' : ''; + $instance->classes = implode(' ', $class); + $instance->style = implode('', $style); + + $instance->abovecolorbg = !empty($option->abovecolorbg) ? sprintf('background: %s;', $option->abovecolorbg) : ''; + $instance->backimage = !empty($option->backimage) ? sprintf('background-image: url(%s);', $option->backimage) : ''; + $instance->belowcolorbg = !empty($option->belowcolorbg) ? sprintf('background: %s;', $option->belowcolorbg) : ''; + $scrolldata = [ + "start" => $instance->viewport, + "direction" => $instance->direction, + "speed" => $instance->speed ? $instance->speed : 0 + ]; + if ($instance->direction) { + $instance->scrolleffect = json_encode($scrolldata); + } + + return $instance; + } + + /** + * Load the classes from elements, If need to add classes in parent div then use this method + * Otherwise use the render function to add classes. + * + * @param stdclass $instance + * @param stdclass $option + * @return void + */ + public function generate_element_classes(&$instance, $option) { + $instance = $this->load_option_classes($instance, $option); + } + + /** + * Get the completion for all mandatory elements in the content designer. + * + * @return bool + */ + public function get_mandatory_completion() { + global $DB; + + $sql = 'SELECT cc.*, ce.id as elementid, ce.shortname as elementname + FROM {contentdesigner_content} cc + JOIN {contentdesigner_elements} ce ON ce.id = cc.element + WHERE cc.contentdesignerid = ?'; + + $params = [$this->cm->instance]; + $contents = $DB->get_records_sql($sql, $params); + $completemandatory = false; + + // Assume all mandatory elements are complete until proven otherwise. + $completemandatory = true; + $hasmandatory = false; + + foreach ($contents as $content) { + $cm = $this->get_cm_from_modinstance($content->contentdesignerid); + $element = \mod_contentdesigner\editor::get_element($content->elementname, $cm->id); + $instance = $element->get_instance($content->instance); + + // Check if this element is mandatory. + if (isset($instance->mandatory) && !empty($instance->mandatory)) { + $hasmandatory = true; // At least one mandatory element exists. + + // If any mandatory element is incomplete, set completion to false and break. + if ($element->prevent_nextelements($instance)) { + $completemandatory = false; + break; + } + } + } + + // If no mandatory elements were found, set completemandatory to false. + if (!$hasmandatory) { + $completemandatory = false; + } + + return $completemandatory; + } +} diff --git a/classes/event/course_module_instance_list_viewed.php b/classes/event/course_module_instance_list_viewed.php new file mode 100644 index 0000000..574e77c --- /dev/null +++ b/classes/event/course_module_instance_list_viewed.php @@ -0,0 +1,38 @@ +. + +/** + * The mod_contentdesigner instance list viewed event. + * + * @package mod_contentdesigner + * @copyright 2024 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner\event; + +defined('MOODLE_INTERNAL') || die(); + +/** + * The mod_contentdesigner instance list viewed event class. + * + * @package mod_contentdesigner + * @copyright 2024 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { + // No code required here as the parent class handles it all. +} diff --git a/classes/event/course_module_viewed.php b/classes/event/course_module_viewed.php new file mode 100644 index 0000000..dafa7e4 --- /dev/null +++ b/classes/event/course_module_viewed.php @@ -0,0 +1,52 @@ +. + +/** + * The mod_contentdesigner course module viewed event. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner\event; + +/** + * The mod_contentdesigner course module viewed event class. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_module_viewed extends \core\event\course_module_viewed { + + /** + * Init method. + */ + protected function init() { + $this->data['crud'] = 'r'; + $this->data['edulevel'] = self::LEVEL_PARTICIPATING; + $this->data['objecttable'] = 'contentdesigner'; + } + + /** + * Get the object mapping + * @return array + */ + public static function get_objectid_mapping() { + return array('db' => 'contentdesigner', 'restore' => 'contentdesigner'); + } +} diff --git a/classes/form/general_element_form.php b/classes/form/general_element_form.php new file mode 100644 index 0000000..b3c1852 --- /dev/null +++ b/classes/form/general_element_form.php @@ -0,0 +1,205 @@ +. + +/** + * Form for editing a general element. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner\form; + +defined('MOODLE_INTERNAL') || die(); + +use mod_contentdesigner\editor; +use stdClass; + +require_once($CFG->dirroot.'/lib/formslib.php'); + + +/** + * General option form to create elements. + * + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class general_element_form extends \moodleform { + + /** + * Make the custom data as public varaible to access on the elements forms. + * + * @var array + */ + public $_customdata; + + /** + * Define the form. + */ + public function definition() { + global $USER, $CFG, $COURSE, $PAGE, $DB; + $mform = $this->_form; + $element = $this->_customdata['element']; + $instanceid = $this->_customdata['instanceid']; + $cmid = $this->_customdata['cmid']; + $element = \mod_contentdesigner\editor::get_element($element, $cmid); + + $mform->addElement('header', 'elementsettings', + get_string('elementsettings', 'mod_contentdesigner', ucfirst($element->element_name()))); + $element->element_form($mform, $this); + + if ($element->supports_standard_elements()) { + // Extend the elements own options as mform element. + $this->standard_element_settings($mform); + } + + $buttonstr = ''; + if ($instanceid) { + $buttonstr = get_string('elementupdate', 'mod_contentdesigner'); + } else { + $buttonstr = get_string('elementcreate', 'mod_contentdesigner'); + } + + $mform->addElement('hidden', 'chapterid', $this->_customdata['chapterid']); + $mform->setType('chapterid', PARAM_INT); + + $mform->addElement('hidden', 'sesskey', sesskey()); + $mform->setType('sesskey', PARAM_ALPHANUMEXT); + + if (($this->_customdata['element'] == "chapter") && ($record = $DB->get_record('element_chapter', ['id' => $instanceid]))) { + $mform->addElement('hidden', 'position', $record->position); + $mform->setType('position', PARAM_INT); + } else { + $mform->addElement('hidden', 'position', $this->_customdata['position']); + $mform->setType('position', PARAM_ALPHA); + } + + $this->add_action_buttons(true, $buttonstr); + } + + /** + * Defined the standard general options moodle form elements for content designer elements. + * + * @param moodle_form $mform Moodle quick form. + * @return void + */ + public function standard_element_settings($mform) { + global $CFG; + + // Accessibility: "Required" is bad legend text. + $strrequired = get_string('required'); + + // Print the required moodle fields first. + // Title for General element. + $mform->addElement('header', 'generalsettings', get_string('generaltitle', 'mod_contentdesigner')); + + $mform->addElement('text', 'title', get_string('elementtitle', 'mod_contentdesigner'), 'maxlength="100" size="30"'); + $mform->setType('title', PARAM_NOTAGS); + $mform->addHelpButton('title', 'elementtitle', 'mod_contentdesigner'); + + // Visibility for General element. + $visibleoptions = [ + 1 => get_string('visible'), + 0 => get_string('hidden', 'mod_contentdesigner'), + ]; + $mform->addElement('select', 'visible', get_string('visibility', 'mod_contentdesigner'), $visibleoptions); + $mform->addHelpButton('visible', 'visibility', 'mod_contentdesigner'); + + // Margin for General element. + $mform->addElement('text', 'margin', get_string('margin', 'mod_contentdesigner'), 'size="30"'); + $mform->setType('margin', PARAM_RAW); + $mform->addHelpButton('margin', 'margin', 'mod_contentdesigner'); + + // Padding for General element. + $mform->addElement('text', 'padding', get_string('padding', 'mod_contentdesigner'), 'size="30"'); + $mform->setType('padding', PARAM_RAW); + $mform->addHelpButton('padding', 'padding', 'mod_contentdesigner'); + + $mform->addElement('header', 'backgroundsettings', get_string('backgroundtitle', 'mod_contentdesigner')); + + // Background for general element. + $mform->addElement('text', 'abovecolorbg', get_string('abovecolorbg', 'mod_contentdesigner'), + array('placeholder' => 'linear-gradient(#e66465, #9198e5)', 'size' => "60")); + $mform->setType('abovecolorbg', PARAM_RAW); + $mform->addHelpButton('abovecolorbg', 'abovecolorbg', 'mod_contentdesigner'); + + $options = [ + 'accepted_types' => ['image'], + 'maxbytes' => 0, + 'maxfiles' => 1, + 'subdirs' => 0, + ]; + $mform->addElement('filemanager', 'bgimage', get_string('elementbgimage', 'mod_contentdesigner'), null, $options); + $mform->addHelpButton('bgimage', 'elementbgimage', 'mod_contentdesigner'); + + $mform->addElement('text', 'belowcolorbg', get_string('belowcolorbg', 'mod_contentdesigner'), + array('placeholder' => 'linear-gradient(#e66465, #9198e5)', 'size' => "60")); + $mform->setType('belowcolorbg', PARAM_RAW); + $mform->addHelpButton('belowcolorbg', 'belowcolorbg', 'mod_contentdesigner'); + + $mform->addElement('header', 'animationsettings', get_string('animationtitle', 'mod_contentdesigner')); + + // Animation for general element. + $animationtype = [ + 0 => get_string('none'), + 'fadeIn' => get_string('fadein', 'mod_contentdesigner'), + 'slideInRight' => get_string('slidefromright', 'mod_contentdesigner'), + 'slideInLeft' => get_string('slidefromleft', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'animation', get_string('stranimation', 'mod_contentdesigner'), $animationtype); + $mform->addHelpButton('animation', 'stranimation', 'mod_contentdesigner'); + + $durations = [ + 'slow' => get_string('strslow', 'mod_contentdesigner'), + 'normal' => get_string('strnormal', 'mod_contentdesigner'), + 'fast' => get_string('strfast', 'mod_contentdesigner'), + ]; + $mform->addElement('select', 'duration', get_string('strduration', 'mod_contentdesigner'), $durations); + $mform->addHelpButton('duration', 'strduration', 'mod_contentdesigner'); + $mform->addElement('text', 'delay', get_string('strdelay', 'mod_contentdesigner')); + $mform->setType('delay', PARAM_INT); + $mform->addHelpButton('delay', 'strdelay', 'mod_contentdesigner'); + + $mform->addElement('header', 'scrollingsettings', get_string('scrollingeffectstitle', 'mod_contentdesigner')); + + // Animation for general element. + $scrolldirections = [ + 0 => get_string('none'), + 'left' => get_string('toleft', 'mod_contentdesigner'), + 'right' => get_string('toright', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'direction', get_string('strdirection', 'mod_contentdesigner'), $scrolldirections); + $mform->addHelpButton('direction', 'strdirection', 'mod_contentdesigner'); + + $mform->addElement('select', 'speed', get_string('speed', 'mod_contentdesigner'), range(0, 10)); + $mform->addHelpButton('speed', 'speed', 'mod_contentdesigner'); + + $mform->addElement('text', 'viewport', get_string('viewport', 'mod_contentdesigner')); + $mform->setType('viewport', PARAM_INT); + $mform->addHelpButton('viewport', 'viewport', 'mod_contentdesigner'); + + $mform->addElement('header', 'responsivesettings', get_string('responsivetitle', 'mod_contentdesigner')); + + // Responsive for general element. + $mform->addElement('advcheckbox', 'hidedesktop', get_string('hideondesktop', 'mod_contentdesigner')); + $mform->addHelpButton('hidedesktop', 'hideondesktop', 'mod_contentdesigner'); + $mform->addElement('advcheckbox', 'hidetablet', get_string('hideontablet', 'mod_contentdesigner')); + $mform->addHelpButton('hidetablet', 'hideontablet', 'mod_contentdesigner'); + $mform->addElement('advcheckbox', 'hidemobile', get_string('hideonmobile', 'mod_contentdesigner')); + $mform->addHelpButton('hidemobile', 'hideonmobile', 'mod_contentdesigner'); + } +} diff --git a/classes/output/renderer.php b/classes/output/renderer.php new file mode 100644 index 0000000..38482bb --- /dev/null +++ b/classes/output/renderer.php @@ -0,0 +1,134 @@ +. + +/** + * Contentdesigner module base render to update the core components. + * + * @package mod_contentdesigner + * @copyright 2024 bdecent gmbh + * @author http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner\output; + +use core_renderer; +use moodle_url; + +/** + * Contentdesigner render to create custom navigation of previous and next modules. + */ +class renderer extends core_renderer { + + /** + * Activity navigation to fetch the next and previous modules. + * + * * Removed the verification condition of theme is uses the course index. + * + * @copyright Modified version of moodle core output\activity_navigation by 2009 Tim Hunt + * @param int $contentdesigner + * @param \context $context + * + * @return \core_course\output\activity_navigation + */ + public function activity_navigation($contentdesigner=0, $context=null) + { + // First we should check if we want to add navigation. + $context = $context ?: $this->page->context; + if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop') + || $context->contextlevel != CONTEXT_MODULE) { + return ''; + } + + + if ($contentdesigner) { + list($course, $cm) = get_course_and_cm_from_instance($contentdesigner, 'contentdesigner'); + } else { + $course = $this->page->cm->get_course(); + $cm = $this->page->cm; + } + + // If the activity is in stealth mode, show no links. + if ($cm->is_stealth()) { + return ''; + } + + $courseformat = course_get_format($course); + + // If the theme implements course index and the current course format uses course index and the current + // page layout is not 'frametop' (this layout does not support course index), show no links. + /* if ($this->page->theme->usescourseindex && $this->page->pagelayout !== 'frametop') { + return ''; + } */ + + // Get a list of all the activities in the course. + $modules = get_fast_modinfo($course->id)->get_cms(); + + // Put the modules into an array in order by the position they are shown in the course. + $mods = []; + $activitylist = []; + foreach ($modules as $module) { + // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not). + if (!$module->uservisible || $module->is_stealth() || empty($module->url)) { + continue; + } + $mods[$module->id] = $module; + + // No need to add the current module to the list for the activity dropdown menu. + if ($module->id == $this->page->cm->id) { + continue; + } + // Module name. + $modname = $module->get_formatted_name(); + // Display the hidden text if necessary. + if (!$module->visible) { + $modname .= ' ' . get_string('hiddenwithbrackets'); + } + // Module URL. + $linkurl = new moodle_url($module->url, array('forceview' => 1)); + // Add module URL (as key) and name (as value) to the activity list array. + $activitylist[$linkurl->out(false)] = $modname; + } + + $nummods = count($mods); + + // If there is only one mod then do nothing. + if ($nummods == 1) { + return ''; + } + + // Get an array of just the course module ids used to get the cmid value based on their position in the course. + $modids = array_keys($mods); + + // Get the position in the array of the course module we are viewing. + $position = array_search($this->page->cm->id, $modids); + + $prevmod = null; + $nextmod = null; + + // Check if we have a previous mod to show. + if ($position > 0) { + $prevmod = $mods[$modids[$position - 1]]; + } + + // Check if we have a next mod to show. + if ($position < ($nummods - 1)) { + $nextmod = $mods[$modids[$position + 1]]; + } + + $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist); + return $activitynav; + } +} diff --git a/classes/plugininfo/element.php b/classes/plugininfo/element.php new file mode 100644 index 0000000..b671b37 --- /dev/null +++ b/classes/plugininfo/element.php @@ -0,0 +1,106 @@ +. + +/** + * Subplugin info class. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_contentdesigner\plugininfo; + +use core\plugininfo\base; +use part_of_admin_tree; +use admin_settingpage; + +/** + * Element subplugin define classes. + */ +class element extends \core\plugininfo\base { + + /** + * Returns the information about plugin availability + * + * True means that the plugin is enabled. False means that the plugin is + * disabled. Null means that the information is not available, or the + * plugin does not support configurable availability or the availability + * can not be changed. + * + * @return null|bool + */ + public function is_enabled() { + return true; + } + + /** + * Should there be a way to uninstall the plugin via the administration UI. + * + * By default uninstallation is not allowed, plugin developers must enable it explicitly! + * + * @return bool + */ + public function is_uninstall_allowed() { + return true; + } + + /** + * Returns the node name used in admin settings menu for this plugin settings (if applicable) + * + * @return null|string node name or null if plugin does not create settings node (default) + */ + public function get_settings_section_name() { + return 'element'.$this->name.'settings'; + } + /** + * Loads plugin settings to the settings tree + * + * This function usually includes settings.php file in plugins folder. + * Alternatively it can create a link to some settings page (instance of admin_externalpage) + * + * @param \part_of_admin_tree $adminroot + * @param string $parentnodename + * @param bool $hassiteconfig whether the current user has moodle/site:config capability + */ + public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) { + + $ADMIN = $adminroot; // May be used in settings.php. + if (!$this->is_installed_and_upgraded()) { + return; + } + + if (!$hassiteconfig || !file_exists($this->full_path('settings.php'))) { + return; + } + + $section = $this->get_settings_section_name(); + $page = new admin_settingpage($section, $this->displayname, 'moodle/site:config', $this->is_enabled() === false); + include($this->full_path('settings.php')); // This may also set $settings to null. + + if ($page) { + $ADMIN->add($parentnodename, $page); + } + } + + /** + * Pre-uninstall hook. + */ + public function uninstall_cleanup() { + global $CFG; + parent::uninstall_cleanup(); + } + +} diff --git a/classes/privacy/contentdesignerelements_provider.php b/classes/privacy/contentdesignerelements_provider.php new file mode 100644 index 0000000..2e538b2 --- /dev/null +++ b/classes/privacy/contentdesignerelements_provider.php @@ -0,0 +1,40 @@ +. + +/** + * This file contains the contentdesignerelements_provider interface. + * + * Content designer Sub plugins should implement this if they store personal information. + * + * @package mod_contentdesigner + * @copyright 2022, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_contentdesigner\privacy; + +use core_privacy\local\request\contextlist; + +interface contentdesignerelements_provider extends \core_privacy\local\request\plugin\subplugin_provider { + + /** + * Export all relevant user elements information which match the combination of userid. + * + * @param array $contentdesignerids The subcontext within the context to export this information + * @param stdclass $user + */ + public static function export_element_user_data(array $contentdesignerids, \stdclass $user); +} diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php new file mode 100644 index 0000000..056cb03 --- /dev/null +++ b/classes/privacy/provider.php @@ -0,0 +1,359 @@ +. + +/** + * Privacy implementation for contentdesigner module + * + * @package mod_contentdesigner + * @copyright 2022, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_contentdesigner\privacy; + +use stdClass; +use context; + +use core_privacy\local\metadata\collection; +use \core_privacy\local\request\contextlist; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_userlist; +use \core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\helper; +use core_privacy\local\request\transform; +use core_privacy\local\request\writer; +use core_privacy\manager; + +/** + * The contentdesigner module stores user completion and invitation notified details. + */ +class provider implements + \core_privacy\local\metadata\provider, + \core_privacy\local\request\core_userlist_provider, + \core_privacy\local\request\plugin\provider { + + /** + * List of used data fields summary meta key. + * + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection): collection { + + // Module Completion table fields meta summary. + $completionmetadata = [ + 'contentdesignerid' => 'privacy:metadata:completion:contentdesignerid', + 'userid' => 'privacy:metadata:completion:userid', + 'completion' => 'privacy:metadata:completion:completion', + 'timecreated' => 'privacy:metadata:completion:timecreated' + ]; + $collection->add_database_table('contentdesigner_completion', + $completionmetadata, 'privacy:metadata:contentdesignercompletion'); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new \core_privacy\local\request\contextlist(); + // User completions. + $sql = "SELECT c.id + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {contentdesigner} p ON p.id = cm.instance + LEFT JOIN {contentdesigner_completion} pc ON pc.contentdesignerid = p.id + WHERE pc.userid = :userid"; + $params = [ + 'modname' => 'contentdesigner', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $userid + ]; + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'modulename' => 'contentdesigner', + ]; + + // Discussion authors. + $sql = "SELECT d.userid + FROM {course_modules} cm + JOIN {modules} m ON m.id = cm.module AND m.name = :modulename + JOIN {contentdesigner} f ON f.id = cm.instance + JOIN {contentdesigner_completion} d ON d.contentdesignerid = f.id + WHERE cm.id = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + + // Handle the 'contentdesigner' subplugin. + manager::plugintype_class_callback( + 'contentdesignerelements', + contentdesignerelements_provider::class, + 'get_users_in_context', + [$userlist] + ); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + $contentdesigner = $DB->get_record('contentdesigner', ['id' => $cm->instance]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); + $params = array_merge(['contentdesignerid' => $contentdesigner->id], $userinparams); + $sql = "contentdesignerid = :contentdesignerid AND userid {$userinsql}"; + $DB->delete_records_select('contentdesigner_completion', $sql, $params); + + // Handle the 'contentdesigner' subplugin. + manager::plugintype_class_callback( + 'contentdesignerelements', + contentdesignerelements_provider::class, + 'delete_data_for_users', + [$userlist] + ); + + } + + /** + * Delete user completion data for multiple context. + * + * @param approved_contextlist $contextlist The approved context and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $DB->delete_records('contentdesigner_completion', ['contentdesignerid' => $instanceid, 'userid' => $userid]); + } + + // Handle the 'contentdesigner' subplugin. + manager::plugintype_class_callback( + 'contentdesignerelements', + contentdesignerelements_provider::class, + 'delete_data_for_user', + [$contextlist] + ); + } + + /** + * Delete all completion data for all users in the specified context. + * + * @param context $context Context to delete data from. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $cm = get_coursemodule_from_id('contentdesigner', $context->instanceid); + if (!$cm) { + return; + } + $DB->delete_records('contentdesigner_completion', ['contentdesignerid' => $cm->instance]); + + // Handle the 'quizaccess' subplugin. + manager::plugintype_class_callback( + 'contentdesignerelements', + contentdesignerelements_provider::class, + 'delete_subplugin_data_for_all_users_in_context', + [$context] + ); + } + + /** + * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + // Context user. + $user = $contextlist->get_user(); + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT pc.id AS completionid, cm.id AS cmid, c.id AS contextid, + p.id AS pid, p.course AS pcourse, pc.completion AS completion, pc.timecreated AS timecreated, pc.userid AS userid + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {contentdesigner} p ON p.id = cm.instance + INNER JOIN {contentdesigner_completion} pc ON pc.contentdesignerid = p.id AND pc.userid = :userid + WHERE c.id {$contextsql} + ORDER BY cm.id, pc.id ASC"; + + $params = [ + 'modname' => 'contentdesigner', + 'contextlevel' => CONTEXT_MODULE, + 'userid' => $contextlist->get_user()->id, + ]; + $completions = $DB->get_records_sql($sql, $params + $contextparams); + + self::export_contentdesigner_completions( + array_filter( + $completions, + function(stdClass $completion) use ($contextlist) : bool { + return $completion->userid == $contextlist->get_user()->id; + } + ), + $user + ); + + list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); + + $sql = "SELECT cm.id AS cmid, c.id AS contextid, + p.id AS pid, p.course AS pcourse + FROM {context} c + INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel + INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname + INNER JOIN {contentdesigner} p ON p.id = cm.instance + WHERE c.id {$contextsql} + ORDER BY cm.id"; + + $params = [ + 'modname' => 'contentdesigner', + 'contextlevel' => CONTEXT_MODULE, + ]; + $instances = $DB->get_records_sql($sql, $params + $contextparams); + + $contentdesignerids = array_column((array) $instances, 'pid'); + $components = \core_component::get_plugin_list('element'); + $exportparams = [ + $contentdesignerids, + $user, + ]; + foreach (array_keys($components) as $component) { + $classname = manager::get_provider_classname_for_component("element_$component"); + if (class_exists($classname) && is_subclass_of($classname, contentdesignerelements_provider::class)) { + $results = component_class_callback($classname, 'export_element_user_data', $exportparams); + $instances = self::group_by_property($results, 'contentdesignerid'); + + foreach ($instances as $instanceid => $element) { + if (empty($element)) { + continue; + } + $cm = get_coursemodule_from_instance('contentdesigner', $element[0]->contentdesignerid); + $context = \context_module::instance($cm->id); + // Fetch the generic module data for the questionnaire. + $contextdata = helper::get_context_data($context, $user); + unset($element['contentdesignerid']); + $contextdata = (object)array_merge((array)$contextdata, $element); + writer::with_context($context)->export_data( + [get_string('privacy:'.$component, 'element_'.$component)], + $contextdata + ); + } + } + } + } + + /** + * Helper function to export completions. + * + * The array of "completions" is actually the result returned by the SQL in export_user_data. + * It is more of a list of sessions. Which is why it needs to be grouped by context id. + * + * @param array $completions Array of completions to export the logs for. + * @param stdclass $user User record object. + */ + private static function export_contentdesigner_completions(array $completions, $user) { + + $completionsbycontextid = self::group_by_property($completions, 'contextid'); + + foreach ($completionsbycontextid as $contextid => $completion) { + $context = context::instance_by_id($contextid); + $completionsbyid = self::group_by_property($completion, 'completionid'); + foreach ($completionsbyid as $completionid => $completions) { + $completiondata = array_map(function($completion) use ($user) { + return [ + 'completed' => (($completion->completion == 1) ? get_string('yes') : get_string('no')), + 'completedtime' => $completion->timecreated ? transform::datetime($completion->timecreated) : '-', + ]; + + }, $completions); + if (!empty($completiondata)) { + $context = context::instance_by_id($contextid); + // Fetch the generic module data for the questionnaire. + $contextdata = helper::get_context_data($context, $user); + $contextdata = (object)array_merge((array)$contextdata, $completiondata); + writer::with_context($context)->export_data( + [get_string('privacy:completion', 'contentdesigner').' '.$completionid], + $contextdata + ); + } + }; + } + + } + + + + /** + * Helper function to group an array of stdClasses by a common property. + * + * @param array $classes An array of classes to group. + * @param string $property A common property to group the classes by. + * @return array list of element seperated by given property. + */ + private static function group_by_property(array $classes, string $property): array { + return array_reduce( + $classes, + function (array $classes, stdClass $class) use ($property) : array { + $classes[$class->{$property}][] = $class; + return $classes; + }, + [] + ); + } + +} diff --git a/db/access.php b/db/access.php new file mode 100644 index 0000000..ec002b7 --- /dev/null +++ b/db/access.php @@ -0,0 +1,58 @@ +. + +/** + * Capability definitions for the content designer module. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$capabilities = array( + 'mod/contentdesigner:view' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'guest' => CAP_ALLOW, + 'user' => CAP_ALLOW, + ) + ), + + 'mod/contentdesigner:addinstance' => array( + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_COURSE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/course:manageactivities' + ), + + 'mod/contentdesigner:viewcontenteditor' => array( + 'riskbitmask' => RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW, + ) + ), +); + diff --git a/db/install.xml b/db/install.xml new file mode 100644 index 0000000..0737675 --- /dev/null +++ b/db/install.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/db/subplugins.json b/db/subplugins.json new file mode 100644 index 0000000..caae832 --- /dev/null +++ b/db/subplugins.json @@ -0,0 +1,5 @@ +{ + "plugintypes": { + "element": "mod\/contentdesigner\/element" + } +} diff --git a/editor.php b/editor.php new file mode 100644 index 0000000..135bc99 --- /dev/null +++ b/editor.php @@ -0,0 +1,65 @@ +. + +/** + * Content Designer elements add / edit instance form. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot . '/mod/contentdesigner/lib.php'); + +// Course Module ID. +$id = required_param('id', PARAM_INT); + +if (!$cm = get_coursemodule_from_id('contentdesigner', $id)) { + // NOTE this is invalid use of print_error, must be a lang string id. + throw new moodle_exception('invalidcoursemodule'); +} + +$PAGE->set_url('/mod/contentdesigner/editor.php', array('id' => $cm->id, 'sesskey' => sesskey())); + +if (!$course = $DB->get_record('course', array('id' => $cm->course))) { + throw new moodle_exception('invalidcourse'); // NOTE As above. +} +require_course_login($course, false, $cm); + +if (!$data = $DB->get_record('contentdesigner', array('id' => $cm->instance))) { + throw new moodle_exception('course module is incorrect'); // NOTE As above. +} +$context = context_module::instance($cm->id); + +require_sesskey(); + +require_capability('mod/contentdesigner:viewcontenteditor', $context); + +$PAGE->set_title($course->shortname.': '.$data->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_activity_record($data); +$PAGE->add_body_class('limitedwidth'); + +echo $OUTPUT->header(); + +$editor = new mod_contentdesigner\editor($cm, $course); +echo $editor->display(); + +$editor->init_data_forjs(); +$PAGE->requires->js_call_amd('mod_contentdesigner/editor', 'init', + ['contextid' => $context->id, 'cmid' => $cm->id, 'contentdesignerid' => $cm->instance]); +echo $OUTPUT->footer(); diff --git a/element.php b/element.php new file mode 100644 index 0000000..43768f8 --- /dev/null +++ b/element.php @@ -0,0 +1,115 @@ +. + +/** + * Content Designer elements add / edit instance form. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once($CFG->dirroot."/mod/contentdesigner/lib.php"); + +$id = optional_param('id', 0, PARAM_INT); // Element instance id. +$cmid = required_param('cmid', PARAM_INT); // Course module id. +$element = required_param('element', PARAM_ALPHANUM); +$action = optional_param('action', '', PARAM_ALPHA); +$chapter = optional_param('chapter', 0, PARAM_INT); +$position = optional_param('position', 'bottom', PARAM_ALPHA); + +// Check element exist or not. +$elements = contentdesigner_get_element_pluginnames(); +if (!in_array($element, $elements)) { + throw new moodle_exception('invaildelement', 'mod_contentdesigner'); +} + +list ($course, $cm) = get_course_and_cm_from_cmid($cmid, 'contentdesigner'); +$context = context_module::instance($cm->id); + +$elementobj = mod_contentdesigner\editor::get_element($element, $cmid); + +if ($id) { + $elementrecord = $DB->get_record("element_".$element, array('id' => $id)); + if (!$elementrecord) { + throw new moodle_exception('invaildrecord', 'mod_contentdesigner'); + } + $content = $DB->get_record('contentdesigner_content', ['element' => $elementobj->elementid, 'instance' => $id]); + $chapter = isset($content->chapter) ? $content->chapter : 0; +} + + +require_login($course, true, $cm); + +require_sesskey(); + +require_capability('mod/contentdesigner:viewcontenteditor', $context); + +$record = new stdClass(); +$record->course = $course->id; +$record->cmid = $cmid; +$record->element = $element; +$record->contentdesignerid = $cm->instance; + +$urlparams = array( + 'id' => $id, + 'action' => $action, + 'cmid' => $cmid, + 'element' => $element, + 'sesskey' => sesskey() +); +$url = new moodle_url('/mod/contentdesigner/element.php', $urlparams); +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_title($course->shortname.': '.get_string('createnewelement', 'contentdesigner')); + +$mform = new \mod_contentdesigner\form\general_element_form($PAGE->url->out(false), [ + 'element' => $element, + 'context' => $context, + 'instanceid' => $id, + 'cmid' => $cmid, + 'chapterid' => $chapter, + 'position' => $position, +]); + +if ($mform->is_cancelled()) { + redirect(new moodle_url('/mod/contentdesigner/editor.php', ['id' => $cmid, 'sesskey' => sesskey()])); +} else if ($formdata = $mform->get_data()) { + + $formdata->course = $course->id; + $formdata->cmid = $cm->id; + $formdata->element = $elementobj->elementid; // ID of the element in elements table. + $formdata->contextid = $context->id; + $formdata->instanceid = isset($elementrecord) ? $elementrecord->id : 0; + $formdata->contentdesignerid = $cm->instance; + $formdata->elementshortname = $elementobj->shortname; + + $elementobj->update_element($formdata); + $editorurl = new moodle_url('/mod/contentdesigner/editor.php', ['id' => $cmid, 'sesskey' => sesskey()]); + redirect($editorurl, get_string('savechanges'), null, \core\output\notification::NOTIFY_INFO); +} + +$data = (object) $elementobj->prepare_formdata($id); +$data = $elementobj->prepare_standard_file_editor($data); + +$mform->set_data($data); +// PAGE header. +echo $OUTPUT->header(); +// Render and Display the add elemet instance form contents. +echo $mform->display(); +// Page footer. +echo $OUTPUT->footer(); diff --git a/element/chapter/amd/build/chapter.min.js b/element/chapter/amd/build/chapter.min.js new file mode 100644 index 0000000..5b45dc0 --- /dev/null +++ b/element/chapter/amd/build/chapter.min.js @@ -0,0 +1,3 @@ +define("element_chapter/chapter",["jquery","mod_contentdesigner/elements","core/ajax","core/fragment","core/templates","core/loadingicon","core/notification","core/str"],(function($,Elements,AJAX,Fragment,Templates,LoadingIcon,Notification,Str){let completionIcon,completionStr;const completeChapterListener=e=>{var completeCTA=e.target.closest("button.complete-chapter");if(null!=completeCTA){e.preventDefault();var chapter=completeCTA.dataset.chapterid;completeChapter(chapter,completeCTA).done((()=>{updateProgress(),completeCTA.classList.remove("btn-outline-secondary"),completeCTA.classList.add("btn-success"),completeCTA.innerHTML=completionIcon+" "+completionStr,Elements.removeWarning(),Elements.refreshContent()})).catch(Notification.exception)}},stickyProgress=function(){var progressElem=document.querySelector(".contentdesigner-progress"),contentWrapper=document.querySelector(".contentdesigner-content");null!=contentWrapper&&contentWrapper.getBoundingClientRect().top<50?(contentWrapper.classList.add("sticky-progress"),progressElem.classList.add("fixed-top")):(progressElem.classList.remove("fixed-top"),contentWrapper.classList.remove("sticky-progress"))},completeChapter=(chapter,button)=>{var promises=AJAX.call([{methodname:"element_chapter_update_completion",args:{chapter:chapter,cmid:Elements.contentDesignerData().cmid}}]);return LoadingIcon.addIconToContainerRemoveOnCompletion(button,promises[0]),promises[0]},updateProgress=()=>{var params={cmid:Elements.contentDesignerData().cmid};Fragment.loadFragment("element_chapter","update_progressbar",Elements.contentDesignerData().contextid,params).done(((html,js)=>{Templates.replaceNode("div#contentdesigner-progressbar",html,js)})).catch(Notification.exception)};return{init:function(){Templates.renderPix("e/tick","core").done((function(img){completionIcon=img})),Str.get_string("completion_manual:done","course").done((str=>{completionStr=str})),document.body.removeEventListener("click",completeChapterListener),document.body.addEventListener("click",completeChapterListener),document.querySelector("#page").addEventListener("scroll",(()=>{stickyProgress()})),window.addEventListener("scroll",(()=>{stickyProgress()}))}}})); + +//# sourceMappingURL=chapter.min.js.map \ No newline at end of file diff --git a/element/chapter/amd/build/chapter.min.js.map b/element/chapter/amd/build/chapter.min.js.map new file mode 100644 index 0000000..820506e --- /dev/null +++ b/element/chapter/amd/build/chapter.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"chapter.min.js","sources":["../src/chapter.js"],"sourcesContent":["define(['jquery', 'mod_contentdesigner/elements', 'core/ajax', 'core/fragment',\n'core/templates', 'core/loadingicon', 'core/notification', 'core/str'],\nfunction($, Elements, AJAX, Fragment, Templates, LoadingIcon, Notification, Str) {\n\n const chapterCTA = 'button.complete-chapter';\n\n const progressBar = 'div#contentdesigner-progressbar';\n\n let completionIcon, completionStr;\n\n const initEventListeners = () => {\n Templates.renderPix('e/tick', 'core').done(function(img) {\n completionIcon = img;\n });\n Str.get_string('completion_manual:done', 'course').done((str) => {\n completionStr = str;\n });\n // Remove previous eventlisteners on body. to support popup format.\n document.body.removeEventListener('click', completeChapterListener);\n document.body.addEventListener('click', completeChapterListener);\n\n document.querySelector('#page').addEventListener('scroll', () => {\n stickyProgress();\n });\n\n window.addEventListener('scroll', () => {\n stickyProgress();\n });\n };\n\n const completeChapterListener = (e) => {\n var completeCTA = e.target.closest(chapterCTA);\n if (completeCTA != undefined) {\n e.preventDefault();\n var chapter = completeCTA.dataset.chapterid;\n var promise = completeChapter(chapter, completeCTA);\n promise.done(() => {\n updateProgress();\n completeCTA.classList.remove('btn-outline-secondary');\n completeCTA.classList.add('btn-success');\n completeCTA.innerHTML = completionIcon + ' ' + completionStr;\n Elements.removeWarning();\n Elements.refreshContent();\n // TODO: Add a additional function to support loadnext chapter works like replaceonrefresh.\n // Until hide this loadNextchapters().\n // Elements.loadNextChapters(chapter);\n }).catch(Notification.exception);\n }\n };\n\n const stickyProgress = function() {\n var progressElem = document.querySelector('.contentdesigner-progress');\n var contentWrapper = document.querySelector('.contentdesigner-content');\n if (contentWrapper != undefined && contentWrapper.getBoundingClientRect().top < 50) {\n contentWrapper.classList.add('sticky-progress');\n progressElem.classList.add('fixed-top');\n } else {\n progressElem.classList.remove('fixed-top');\n contentWrapper.classList.remove('sticky-progress');\n }\n };\n\n const completeChapter = (chapter, button) => {\n var promises = AJAX.call([{\n methodname: 'element_chapter_update_completion',\n args: {\n chapter: chapter,\n cmid: Elements.contentDesignerData().cmid\n }\n }]);\n LoadingIcon.addIconToContainerRemoveOnCompletion(button, promises[0]);\n\n return promises[0];\n };\n\n const updateProgress = () => {\n var params = {cmid: Elements.contentDesignerData().cmid};\n Fragment.loadFragment('element_chapter', 'update_progressbar', Elements.contentDesignerData().contextid, params).done((html, js) => {\n Templates.replaceNode(progressBar, html, js);\n }).catch(Notification.exception);\n };\n\n return {\n init: function() {\n initEventListeners();\n },\n };\n});\n"],"names":["define","$","Elements","AJAX","Fragment","Templates","LoadingIcon","Notification","Str","completionIcon","completionStr","completeChapterListener","e","completeCTA","target","closest","undefined","preventDefault","chapter","dataset","chapterid","completeChapter","done","updateProgress","classList","remove","add","innerHTML","removeWarning","refreshContent","catch","exception","stickyProgress","progressElem","document","querySelector","contentWrapper","getBoundingClientRect","top","button","promises","call","methodname","args","cmid","contentDesignerData","addIconToContainerRemoveOnCompletion","params","loadFragment","contextid","html","js","replaceNode","init","renderPix","img","get_string","str","body","removeEventListener","addEventListener","window"],"mappings":"AAAAA,iCAAO,CAAC,SAAU,+BAAgC,YAAa,gBAC/D,iBAAkB,mBAAoB,oBAAqB,aAC3D,SAASC,EAAGC,SAAUC,KAAMC,SAAUC,UAAWC,YAAaC,aAAcC,SAMpEC,eAAgBC,oBAsBdC,wBAA2BC,QACzBC,YAAcD,EAAEE,OAAOC,QA3BZ,8BA4BIC,MAAfH,YAA0B,CAC1BD,EAAEK,qBACEC,QAAUL,YAAYM,QAAQC,UACpBC,gBAAgBH,QAASL,aAC/BS,MAAK,KACTC,iBACAV,YAAYW,UAAUC,OAAO,yBAC7BZ,YAAYW,UAAUE,IAAI,eAC1Bb,YAAYc,UAAYlB,eAAiB,IAAMC,cAC/CR,SAAS0B,gBACT1B,SAAS2B,oBAIVC,MAAMvB,aAAawB,aAIxBC,eAAiB,eACfC,aAAeC,SAASC,cAAc,6BACtCC,eAAiBF,SAASC,cAAc,4BACtBnB,MAAlBoB,gBAA+BA,eAAeC,wBAAwBC,IAAM,IAC5EF,eAAeZ,UAAUE,IAAI,mBAC7BO,aAAaT,UAAUE,IAAI,eAE3BO,aAAaT,UAAUC,OAAO,aAC9BW,eAAeZ,UAAUC,OAAO,qBAIlCJ,gBAAkB,CAACH,QAASqB,cAC1BC,SAAWrC,KAAKsC,KAAK,CAAC,CACtBC,WAAY,oCACZC,KAAM,CACFzB,QAASA,QACT0B,KAAM1C,SAAS2C,sBAAsBD,gBAG7CtC,YAAYwC,qCAAqCP,OAAQC,SAAS,IAE3DA,SAAS,IAGdjB,eAAiB,SACfwB,OAAS,CAACH,KAAM1C,SAAS2C,sBAAsBD,MACnDxC,SAAS4C,aAAa,kBAAmB,qBAAsB9C,SAAS2C,sBAAsBI,UAAWF,QAAQzB,MAAK,CAAC4B,KAAMC,MACzH9C,UAAU+C,YAxEE,kCAwEuBF,KAAMC,OAC1CrB,MAAMvB,aAAawB,kBAGnB,CACHsB,KAAM,WAxENhD,UAAUiD,UAAU,SAAU,QAAQhC,MAAK,SAASiC,KAChD9C,eAAiB8C,OAErB/C,IAAIgD,WAAW,yBAA0B,UAAUlC,MAAMmC,MACrD/C,cAAgB+C,OAGpBvB,SAASwB,KAAKC,oBAAoB,QAAShD,yBAC3CuB,SAASwB,KAAKE,iBAAiB,QAASjD,yBAExCuB,SAASC,cAAc,SAASyB,iBAAiB,UAAU,KACvD5B,oBAGJ6B,OAAOD,iBAAiB,UAAU,KAC9B5B"} \ No newline at end of file diff --git a/element/chapter/amd/src/chapter.js b/element/chapter/amd/src/chapter.js new file mode 100644 index 0000000..38ba89b --- /dev/null +++ b/element/chapter/amd/src/chapter.js @@ -0,0 +1,88 @@ +define(['jquery', 'mod_contentdesigner/elements', 'core/ajax', 'core/fragment', +'core/templates', 'core/loadingicon', 'core/notification', 'core/str'], +function($, Elements, AJAX, Fragment, Templates, LoadingIcon, Notification, Str) { + + const chapterCTA = 'button.complete-chapter'; + + const progressBar = 'div#contentdesigner-progressbar'; + + let completionIcon, completionStr; + + const initEventListeners = () => { + Templates.renderPix('e/tick', 'core').done(function(img) { + completionIcon = img; + }); + Str.get_string('completion_manual:done', 'course').done((str) => { + completionStr = str; + }); + // Remove previous eventlisteners on body. to support popup format. + document.body.removeEventListener('click', completeChapterListener); + document.body.addEventListener('click', completeChapterListener); + + document.querySelector('#page').addEventListener('scroll', () => { + stickyProgress(); + }); + + window.addEventListener('scroll', () => { + stickyProgress(); + }); + }; + + const completeChapterListener = (e) => { + var completeCTA = e.target.closest(chapterCTA); + if (completeCTA != undefined) { + e.preventDefault(); + var chapter = completeCTA.dataset.chapterid; + var promise = completeChapter(chapter, completeCTA); + promise.done(() => { + updateProgress(); + completeCTA.classList.remove('btn-outline-secondary'); + completeCTA.classList.add('btn-success'); + completeCTA.innerHTML = completionIcon + ' ' + completionStr; + Elements.removeWarning(); + Elements.refreshContent(); + // TODO: Add a additional function to support loadnext chapter works like replaceonrefresh. + // Until hide this loadNextchapters(). + // Elements.loadNextChapters(chapter); + }).catch(Notification.exception); + } + }; + + const stickyProgress = function() { + var progressElem = document.querySelector('.contentdesigner-progress'); + var contentWrapper = document.querySelector('.contentdesigner-content'); + if (contentWrapper != undefined && contentWrapper.getBoundingClientRect().top < 50) { + contentWrapper.classList.add('sticky-progress'); + progressElem.classList.add('fixed-top'); + } else { + progressElem.classList.remove('fixed-top'); + contentWrapper.classList.remove('sticky-progress'); + } + }; + + const completeChapter = (chapter, button) => { + var promises = AJAX.call([{ + methodname: 'element_chapter_update_completion', + args: { + chapter: chapter, + cmid: Elements.contentDesignerData().cmid + } + }]); + LoadingIcon.addIconToContainerRemoveOnCompletion(button, promises[0]); + + return promises[0]; + }; + + const updateProgress = () => { + var params = {cmid: Elements.contentDesignerData().cmid}; + Fragment.loadFragment('element_chapter', 'update_progressbar', Elements.contentDesignerData().contextid, params).done((html, js) => { + Templates.replaceNode(progressBar, html, js); + }).catch(Notification.exception); + }; + + return { + init: function() { + initEventListeners(); + }, + }; +}); diff --git a/element/chapter/backup/moodle2/backup_element_chapter_subplugin.class.php b/element/chapter/backup/moodle2/backup_element_chapter_subplugin.class.php new file mode 100644 index 0000000..989b19d --- /dev/null +++ b/element/chapter/backup/moodle2/backup_element_chapter_subplugin.class.php @@ -0,0 +1,71 @@ +. + +/** + * This file contains the backup code for the element_chapter plugin. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Provides the information to backup chapter elements. + */ +class backup_element_chapter_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to chapter element + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + $userinfo = $this->get_setting_value('userinfo'); + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_chapter', array('id'), array( + 'contentdesignerid', 'title', 'visible', 'contents', 'position','titlestatus', + 'timecreated', 'timemodified' )); + + $chaptercompletion = new backup_nested_element('elementchapter_completion'); + $chaptercompletionelement = new backup_nested_element('element_chapter_completion', array('id'), array( + 'instance', 'userid', 'completion', 'titlestatus', 'timecreated', 'timemodified' + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + $subplugin->add_child($chaptercompletion); + $chaptercompletion->add_child($chaptercompletionelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_chapter', array('contentdesignerid' => backup::VAR_PARENTID)); + + if ($userinfo) { + $sql = 'SELECT * FROM {element_chapter_completion} WHERE instance IN ( + SELECT id FROM {element_chapter} WHERE contentdesignerid=:contentdesignerid + )'; + $chaptercompletionelement->set_source_sql($sql, array('contentdesignerid' => backup::VAR_PARENTID)); + $chaptercompletionelement->annotate_ids('user', 'userid'); + } + + return $subplugin; + } + +} diff --git a/element/chapter/backup/moodle2/restore_element_chapter_subplugin.class.php b/element/chapter/backup/moodle2/restore_element_chapter_subplugin.class.php new file mode 100644 index 0000000..4e3b60b --- /dev/null +++ b/element/chapter/backup/moodle2/restore_element_chapter_subplugin.class.php @@ -0,0 +1,66 @@ +. + +/** + * This file contains the restore code for the element_chapter plugin. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore chapter element subplugin. + */ +class restore_element_chapter_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_chapter'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes one chapter element instance + * @param mixed $data + */ + public function process_element_chapter_instance($data) { + global $DB; + + $data = (object)$data; + + $oldchapterid = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + // Make the chapter empty, content will be added during the contentdesigner_content restore. + $data->contents = ''; + $data->timemodified = time(); + $newchapterid = $DB->insert_record('element_chapter', $data); + $this->set_mapping('chapterid', $oldchapterid, $newchapterid); + } + +} diff --git a/element/chapter/classes/element.php b/element/chapter/classes/element.php new file mode 100644 index 0000000..deb1616 --- /dev/null +++ b/element/chapter/classes/element.php @@ -0,0 +1,546 @@ +. + +/** + * Extended class of elements for chapter. it contains major part of editor element content + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_chapter; + +use mod_contentdesigner\editor; +use moodle_exception; + +/** + * Definitions of chapter element and it behaviours. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'chapter'; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_chapter'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + return $output->pix_icon('i/folder', get_string('pluginname', 'element_chapter')); + } + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + $strrequired = get_string('required'); + $mform->addElement('text', 'title', get_string('elementtitle', 'mod_contentdesigner'), 'maxlength="100" size="30"'); + $mform->addRule('title', $strrequired, 'required', null, 'client'); + $mform->setType('title', PARAM_NOTAGS); + + // Display title. + $default = get_config('mod_contentdesigner', 'chaptertitlestatus'); + $mform->addElement('checkbox', 'titlestatus', get_string('titlestatus', 'mod_contentdesigner')); + $mform->setDefault('titlestatus', $default ?: 0); + $mform->addHelpButton('titlestatus', 'titlestatus', 'mod_contentdesigner'); + + // Visibility for General element. + $visibleoptions = [ + 1 => get_string('visible'), + 0 => get_string('hidden', 'mod_contentdesigner'), + ]; + $mform->addElement('select', 'visible', get_string('visibility', 'mod_contentdesigner'), $visibleoptions); + $mform->addHelpButton('visible', 'visibility', 'mod_contentdesigner'); + + } + + /** + * Verify the element is supports the content render method. + * + * @return bool + */ + public function supports_content() { + return false; + } + + /** + * Verify the elements the standard general options list. + * + * @return bool + */ + public function supports_standard_elements() { + // By default all the elmenets will supports the standard options. + return false; + } + + /** + * Initiate the element js for the view page. + * + * @return void + */ + public function initiate_js() { + global $PAGE; + $PAGE->requires->js_call_amd('element_chapter/chapter', 'init', []); + } + + /** + * Get the default chapter instance for the current course module. if not found create new one and use it as default one. + * + * @param int $contentdesignerid Content designer instance id. + * @param bool $create + * @return bool + */ + public function get_default($contentdesignerid, $create=false) { + global $DB; + if ($record = $DB->get_record('element_chapter', ['contentdesignerid' => $contentdesignerid], '*', IGNORE_MULTIPLE)) { + return $record->id; + } + + if ($create) { + $id = $this->create_basic_instance($contentdesignerid); + return $id; + } + return false; + } + + /** + * Create the basic instance for the element. Override this function if need to add custom changes. + * + * @param int $contentdesignerid Contnet deisnger instance id. + * @return int Element instance id. + */ + public function create_basic_instance($contentdesignerid) { + global $DB; + + $record = [ + 'contentdesignerid' => $contentdesignerid, + 'timecreated' => time(), + 'timemodified' => time(), + ]; + + if ($this->is_table_exists()) { + $lastelement = (int) $DB->get_field_sql('SELECT max(position) from {element_chapter} + WHERE contentdesignerid = ?', [$this->cm->instance] + ); + $record['position'] = $lastelement ? $lastelement + 1 : 1; + return $DB->insert_record($this->tablename, $record); + } else { + throw new \moodle_exception('tablenotfound', 'contentdesigner'); + } + } + + /** + * Update the element instance. Override the function in elements element class to add custom rules. + * + * @param stdclass $data + * @return void + */ + public function update_instance($data) { + global $DB; + + if ($data->instanceid == false) { + $data->timemodified = time(); + $data->timecreated = time(); + if ($data->chapterid) { + $lastelement = $DB->get_field('element_chapter', 'position', ['id' => $data->chapterid]); + $DB->execute('UPDATE {element_chapter} SET position=position+1 + WHERE position > ? AND contentdesignerid = ?', [$lastelement, $this->cm->instance]); + } else { + $lastelement = (int) $DB->get_field_sql('SELECT max(position) from {element_chapter} + WHERE contentdesignerid = ?', [$this->cm->instance] + ); + } + $data->position = $lastelement ? $lastelement + 1 : 1; + return $DB->insert_record($this->tablename, $data); + } else { + $data->timecreated = time(); + $data->id = $data->instanceid; + $data->titlestatus = $data->titlestatus ?? 0; + if ($data->chapterid) { + $lastelement = $DB->get_field('element_chapter', 'position', ['id' => $data->chapterid]); + $DB->execute('UPDATE {element_chapter} SET position=position+1 + WHERE position > ? AND contentdesignerid = ?', [$lastelement, $this->cm->instance]); + } else { + $lastelement = (int) $DB->get_field_sql('SELECT max(position) from {element_chapter} + WHERE contentdesignerid = ?', [$this->cm->instance] + ); + } + + $data->position = $data->position ?: $lastelement + 1; + if ($DB->update_record($this->tablename, $data)) { + return $data->id; + } + } + } + + + /** + * Set element instance for the given chapter as contents. + * + * @param int $chapterid Chapter id + * @param int $contentid Content id + * @return void + */ + public function set_elements($chapterid, $contentid) { + global $DB; + + if ($chapter = $DB->get_record('element_chapter', ['id' => $chapterid])) { + $contents = isset($chapter->contents) ? array_filter(explode(',', $chapter->contents)) : []; + $contents[] = $contentid; + $DB->set_field('element_chapter', 'contents', implode(',', array_filter($contents)), ['id' => $chapterid]); + $DB->set_field('element_chapter', 'timemodified', time(), ['id' => $chapterid]); + } + return false; + } + + /** + * Render the chapters for the course module. + * + * @param bool $visible Load only visible elements. + * @param bool $render Return the elements with rendered html. + * @param bool $chapterafter It is need to load the chapters after the given chapter. + * @return array + */ + public function get_chapters_data($visible=false, $render=false, $chapterafter=false) { + global $DB, $USER; + if (empty($this->cm)) { + throw new \moodle_exception('coursemoduleidmissing', 'format_levels'); + } + $list = []; // List of chapters. + $condition = ['contentdesignerid' => $this->cm->instance]; + $condition += $visible ? ['visible' => 1] : []; + if ($chapters = $DB->get_records('element_chapter', $condition, 'position ASC')) { + $chapterreached = false; + foreach ($chapters as $chapterid => $chapter) { + // Find the chapter is reached, checks only chapterafter enabled. + if ($chapterafter && !$chapterreached) { + // Set the chapter reached to load the chapters from next chapter. + $chapterreached = ($chapterafter == $chapterid); + continue; + } + $chapter->chaptertitle = $chapter->title; + $chapter->title = $this->title_editable($chapter) ?: $this->info()->name; + list($prevent, $contents) = $this->generate_chapter_content($chapter, $visible, $render); + if ($visible && empty($contents) && !$chapter->titlestatus) { + continue; + } + + $completion = $DB->get_record('element_chapter_completion', ['instance' => $chapter->id, 'userid' => $USER->id]); + $chapterprevent = ($render && $this->prevent_nextelements($chapter)); + + $element = \mod_contentdesigner\editor::get_element('chapter', $this->cmid); + $editurl = new \moodle_url('/mod/contentdesigner/element.php', [ + 'cmid' => $this->cmid, + 'element' => $element->shortname, + 'id' => $chapter->id, + 'sesskey' => sesskey() + ]); + + $list[] = [ + 'instancedata' => $chapter, + 'info' => $this->info(), + 'editurl' => $editurl, + 'contents' => $contents, + 'count' => count($contents), + 'prevent' => $prevent, + 'chapterprevent' => $chapterprevent, + 'chaptercta' => ($render) ?: false, + 'completion' => isset($completion->completion) && $completion->completion ? true : false, + ]; + // Prevent the next chapters when user needs to complete any of activities. + if ($prevent || $chapterprevent) { + break; + } + } + } + return $list; + } + + /** + * Prevent the upcoming elements if the chapter not completed. + * + * @param stdclass $chapter Chapter element instance data. + * @return bool + */ + public function prevent_nextelements($chapter): bool { + global $USER; + + if (has_capability('mod/contentdesigner:viewcontenteditor', $this->context)) { + return false; + } + + return !$this->is_chaptercompleted($chapter->id); + } + + /** + * Find the user is completed the chapter. + * @param int $chapterid Instance data of chapter. + */ + public function is_chaptercompleted($chapterid): bool { + global $USER, $DB; + + if ($record = $DB->get_record('element_chapter_completion', ['instance' => $chapterid, 'userid' => $USER->id])) { + return $record->completion ? true : false; + } + return false; + } + + /** + * Gerneate chapter related elements. + * + * @param stdclass $chapter + * @param bool $visible Fetch only visible elements. + * @param bool $render Render the element instance to student view. + * @return void + */ + public function generate_chapter_content($chapter, $visible=false, $render=false) { + global $DB; + + $list = []; + $prevent = false; + $record = $DB->get_record('contentdesigner', ['id' => $this->cm->instance]); + if (empty($chapter->contents)) { + return [$prevent, $list]; + } + $contents = explode(',', $chapter->contents); + $sql = 'SELECT cc.*, ce.id as elementid, ce.shortname as elementname FROM {contentdesigner_content} cc + JOIN {contentdesigner_elements} ce ON ce.id = cc.element + WHERE cc.chapter = ? ORDER BY position ASC'; + $params = ['chapterid' => $chapter->id]; + + $contents = $DB->get_records_sql($sql, $params); + foreach ($contents as $content) { + $cm = $this->get_cm_from_modinstance($content->contentdesignerid); + $element = \mod_contentdesigner\editor::get_element($content->elementname, $cm->id); + $editor = \mod_contentdesigner\editor::get_editor($this->cmid); + $instance = $element->get_instance($content->instance, $visible); + if ($instance) { + $instance->title = $element->title_editable($instance) ?: $element->info()->name; + //$instance->hideicon = !$element->supports_content(); + $option = $editor->get_option($instance->id, $element->elementid); + // Load the element options classes to instance. + $element->generate_element_classes($instance, $option); + + // Verify this element supports replace on refresh. + $instance->replaceonrefresh = $element->supports_replace_onrefresh(); + + // Use Mutation of instance in render function of element to add element classes. + $contenthtml = ($render) ? $element->render_element($instance) : ''; + $editurl = new \moodle_url('/mod/contentdesigner/element.php', [ + 'cmid' => $this->cmid, + 'element' => $element->shortname, + 'id' => $instance->id, + 'sesskey' => sesskey() + ]); + $list[] = (array) $content + [ + 'info' => $element->info(), + 'instancedata' => $instance, + 'option' => $option, + 'editurl' => $editurl, + 'content' => $contenthtml, + ]; + + // Prevent the elements next to the manatory elements. + if ($render && $element->prevent_nextelements($instance)) { + $prevent = true; + break; + } + } + } + + return [$prevent, $list]; + } + + /** + * Build the progress of the chapter completion. + * + * @return string HTML of the progress bar. + */ + public function build_progress() { + global $DB, $OUTPUT, $USER; + + $sql = "SELECT ec.*, ecc.completion FROM {element_chapter} ec + LEFT JOIN {element_chapter_completion} ecc ON ec.id = ecc.instance AND ecc.userid=:userid + WHERE ec.contentdesignerid=:contentdesignerid AND ec.visible = 1 AND ec.id IN ( + SELECT chapter FROM {contentdesigner_content} cc + ) ORDER BY position ASC"; + $records = $DB->get_records_sql($sql, ['userid' => $USER->id, 'contentdesignerid' => $this->cm->instance]); + + $data = [ + 'chapters' => array_values($records), + 'contentdesignerid' => $this->cm->instance, + 'cmid' => $this->cmid + ]; + return $OUTPUT->render_from_template('element_chapter/progressbar', $data); + } + + /** + * Update the position of the content in chapters. + * + * @param int $chapterid Chapter id. + * @param string $contents Contents in order position. + * @return void + */ + public function update_postion($chapterid, $contents) { + global $DB; + + $instance = $this->get_instance($chapterid); + $instance->contents = $contents; + try { + $transaction = $DB->start_delegated_transaction(); + if (!empty($contents)) { + $list = explode(',', $contents); + $position = 1; + foreach ($list as $item) { + $record = (object) [ + 'id' => $item, + 'position' => $position, + 'chapter' => $chapterid + ]; + $DB->update_record('contentdesigner_content', $record); + $position += 1; + } + } + // Update the chapter contents in element_chapter table. + $this->update_chapter_contents($chapterid); + $transaction->allow_commit(); + } catch (moodle_exception $ex) { + $transaction->rollback($ex); + } + return $DB->update_record('element_chapter', $instance); + } + + /** + * Move the chapter position in the given position. + * + * @param string $chapters Chapters list in the position + * @return void + */ + public function move_chapter($chapters) { + global $DB; + + try { + $transaction = $DB->start_delegated_transaction(); + if (!empty($chapters)) { + $list = explode(',', $chapters); + $position = 1; + foreach ($list as $item) { + $record = (object) [ + 'id' => $item, + 'position' => $position + ]; + $status = $DB->update_record('element_chapter', $record); + $position += 1; + } + } + $transaction->allow_commit(); + return true; + } catch (moodle_exception $ex) { + + $transaction->rollback($ex); + } + } + + /** + * Update contents of chapter. + * + * @param int $chapterid Chapter id. + * @return void + */ + public function update_chapter_contents($chapterid) { + global $DB; + + if ($contents = $DB->get_records('contentdesigner_content', ['chapter' => $chapterid])) { + $contents = array_column($contents, 'id'); + $DB->update_record('element_chapter', ['id' => $chapterid, 'contents' => implode(',', $contents)]); + } + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $instance + * @return void + */ + public function render($instance) { + return false; + } + + /** + * Delete the element settings. + * + * @param int $instanceid + * @return boolean status. + */ + public function delete_element($instanceid) { + global $DB; + if ($this->get_instance_options($instanceid)) { + + try { + $transaction = $DB->start_delegated_transaction(); + // Delete the element settings. + if ($this->get_instance($instanceid)) { + $DB->delete_records($this->tablename(), array('id' => $instanceid)); + } + if ($contents = $DB->get_records('contentdesigner_content', ['chapter' => $instanceid])) { + foreach ($contents as $key => $value) { + $element = editor::get_element($value->element, $this->cmid); + $element->delete_element($value->instance); + } + } + if ($this->get_instance_options($instanceid)) { + // Delete the element general settings. + $DB->delete_records('contentdesigner_options', array('element' => $this->element_id(), + 'instance' => $instanceid)); + } + $transaction->allow_commit(); + } catch (\Exception $e) { + // Extra cleanup steps. + $transaction->rollback($e); // Rethrows exception. + throw new \moodle_exception('chapternotdeleted', 'element_chapter'); + } + return true; + } + return false; + } +} diff --git a/element/chapter/classes/external.php b/element/chapter/classes/external.php new file mode 100644 index 0000000..a7af9b3 --- /dev/null +++ b/element/chapter/classes/external.php @@ -0,0 +1,87 @@ +. + +/** + * Chapter element external werbservice deifintion to manage the chapter completion. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_chapter; + +defined('MOODLE_INTERNAL') || die('No direct access'); + +use external_value; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Chapter external service methods. + */ +class external extends \external_api { + + /** + * Paramters definition for the methos update chapter progress of user. + * + * @return external_function_parameters + */ + public static function update_completion_parameters() { + return new \external_function_parameters( + array( + 'cmid' => new external_value(PARAM_INT, 'Course module id'), + 'chapter' => new external_value(PARAM_INT, 'Chapter element instance id'), + ) + ); + } + + /** + * Update the content designer chapter progress for the current logged in user. + * + * @param int $cmid Coursemodule id. + * @param int $chapter Content designer module instance id. + * @return bool true if everything updated fine, false if not. + */ + public static function update_completion($cmid, $chapter) { + global $DB, $USER; + + $record = $DB->get_record('element_chapter_completion', ['instance' => $chapter, 'userid' => $USER->id]); + $data = new \stdclass(); + $data->instance = $chapter; + $data->userid = $USER->id; + $data->completion = true; + $data->timemodified = time(); + if (isset($record->id)) { + $data->id = $record->id; + return $DB->update_record('element_chapter_completion', $data); + } else { + $data->timecreated = time(); + if (!$DB->insert_record('element_chapter_completion', $data)) { + return false; + } + } + return false; + } + + /** + * Returns the updated result of module completion. + * + * @return external_value True if state updated otherwise returns false.s + */ + public static function update_completion_returns() { + return new external_value(PARAM_BOOL, 'Result of stored user response'); + } +} diff --git a/element/chapter/classes/privacy/provider.php b/element/chapter/classes/privacy/provider.php new file mode 100644 index 0000000..92833cc --- /dev/null +++ b/element/chapter/classes/privacy/provider.php @@ -0,0 +1,188 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_chapter\privacy; + +use \core_privacy\local\metadata\collection; +use core_privacy\local\request\transform; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_userlist; +use \core_privacy\local\request\approved_contextlist; + +/** + * Privacy class for requesting user data. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \mod_contentdesigner\privacy\contentdesignerelements_provider { + + /** + * List of used data fields summary meta key. + * + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection): collection { + + // Module Completion table fields meta summary. + $completionmetadata = [ + 'instance' => 'privacy:metadata:completion:chapterid', + 'userid' => 'privacy:metadata:completion:userid', + 'completion' => 'privacy:metadata:completion:completion', + 'timecreated' => 'privacy:metadata:completion:timecreated' + ]; + $collection->add_database_table('element_chapter_completion', $completionmetadata, + 'privacy:metadata:contentdesignercompletion'); + + return $collection; + } + + /** + * Helper function to export completions. + * + * The array of "completions" is actually the result returned by the SQL in export_user_data. + * It is more of a list of sessions. Which is why it needs to be grouped by context id. + * + * @param array $contentdesignerids Array of completions to export the logs for. + * @param stdclass $user User record object. + */ + public static function export_element_user_data(array $contentdesignerids, \stdclass $user) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($contentdesignerids, SQL_PARAMS_NAMED); + $sql = "SELECT ec.*, ecc.userid AS userid, ecc.completion AS completion, ecc.timecreated AS timecompleted + FROM {element_chapter} ec + INNER JOIN {element_chapter_completion} ecc ON ecc.instance = ec.id AND ecc.userid = :userid + WHERE ec.contentdesignerid {$insql} + ORDER BY ec.id"; + + $params = [ + 'userid' => $user->id, + ]; + $completions = $DB->get_records_sql($sql, $params + $inparams); + foreach ($completions as $chapterid => $completion) { + $data[$chapterid] = (object) [ + 'completed' => (($completion->completion == 1) ? get_string('yes') : get_string('no')), + 'completedtime' => $completion->timecreated ? transform::datetime($completion->timecreated) : '-', + 'title' => $completion->title, + 'contentdesignerid' => $completion->contentdesignerid + ]; + } + return $data; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + ]; + + // Discussion authors. + $sql = "SELECT ecc.userid + FROM {element_chapter} ec + JOIN {element_chapter_completion} ecc ON ecc.instance = ec.id + WHERE ec.contentdesignerid = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + $contentdesigner = $DB->get_record('contentdesigner', ['id' => $cm->instance]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED, 'usr'); + $list = $DB->get_records('element_chapter', ['contentdesignerid' => $contentdesigner->id]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_chapter_completion', "userid {$userinsql} AND instance $insql", + $userinparams + $inparams); + } + + /** + * Delete user completion data for multiple context. + * + * @param approved_contextlist $contextlist The approved context and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $list = $DB->get_records('element_chapter', ['contentdesignerid' => $instanceid]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_chapter_completion', "userid=:userid AND instance $insql", + ['userid' => $userid] + $inparams); + } + } + + /** + * Delete all completion data for all users in the specified context. + * + * @param context $context Context to delete data from. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $cm = get_coursemodule_from_id('contentdesigner', $context->instanceid); + if (!$cm) { + return; + } + $list = $DB->get_records('element_chapter', ['contentdesignerid' => $cm->instance]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_chapter_completion', "instance $insql", $inparams); + + } +} diff --git a/element/chapter/db/install.php b/element/chapter/db/install.php new file mode 100644 index 0000000..0692507 --- /dev/null +++ b/element/chapter/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_chapter_install() { + $shortname = \element_chapter\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/chapter/db/install.xml b/element/chapter/db/install.xml new file mode 100644 index 0000000..2bdd1aa --- /dev/null +++ b/element/chapter/db/install.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
diff --git a/element/chapter/db/services.php b/element/chapter/db/services.php new file mode 100644 index 0000000..dc1eac3 --- /dev/null +++ b/element/chapter/db/services.php @@ -0,0 +1,37 @@ +. + +/** + * Element chapter services defined. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + + 'element_chapter_update_completion' => array( + 'classname' => 'element_chapter\external', + 'methodname' => 'update_completion', + 'description' => 'Store the user completion result of chapter', + 'type' => 'write', + 'capabilities' => 'mod/contentdesigner:view', + 'ajax' => true + ), +); diff --git a/element/chapter/db/upgrade.php b/element/chapter/db/upgrade.php new file mode 100644 index 0000000..f17fefc --- /dev/null +++ b/element/chapter/db/upgrade.php @@ -0,0 +1,51 @@ +. + +/** + * Upgrade script for the chapter element. + * + * @package element_chapter + * @copyright 2024 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Upgrede chapter element. + * + * @param string $oldversion the version we are upgrading from. + */ +function xmldb_element_chapter_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2024110800) { + + // Element chapter table. + $table = new xmldb_table('element_chapter'); + + // Title status. + $titlestatus = new xmldb_field('titlestatus', XMLDB_TYPE_INTEGER, '2', null, null, null, '0', 'position'); + if (!$dbman->field_exists($table, $titlestatus)) { + $dbman->add_field($table, $titlestatus); + } + + upgrade_plugin_savepoint(true, 2024110800, 'element', 'chapter'); + } + + return true; +} diff --git a/element/chapter/lang/en/element_chapter.php b/element/chapter/lang/en/element_chapter.php new file mode 100644 index 0000000..55a8fd7 --- /dev/null +++ b/element/chapter/lang/en/element_chapter.php @@ -0,0 +1,33 @@ +. + +/** + * Element plugin "Chapter" - string file. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Chapter"; +$string['elementdescription'] = 'Add a new chapter to group elements'; +$string['privacy:metadata:completion:chapterid'] = 'Chapter instance id'; +$string['privacy:metadata:completion:userid'] = 'User id'; +$string['privacy:metadata:completion:completion'] = 'Completion Status'; +$string['privacy:metadata:completion:timecreated'] = 'Time completed the chapter'; +$string['privacy:chapter'] = 'Chapter'; diff --git a/element/chapter/lib.php b/element/chapter/lib.php new file mode 100644 index 0000000..4c8eda8 --- /dev/null +++ b/element/chapter/lib.php @@ -0,0 +1,37 @@ +. + +/** + * Chapter element libarary methods defined. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + /** + * Update the completion of chapter and generate the progress bar for the current module contents. + * + * @param array $args list of parameters such as context and chapter details. + * @return string Html of progress bar. + */ +function element_chapter_output_fragment_update_progressbar($args) { + if (isset($args['cmid'])) { + $cmid = $args['cmid']; + $element = mod_contentdesigner\editor::get_element('chapter', $cmid); + return $element->build_progress(); + } +} diff --git a/element/chapter/templates/progressbar.mustache b/element/chapter/templates/progressbar.mustache new file mode 100644 index 0000000..0e8cd7e --- /dev/null +++ b/element/chapter/templates/progressbar.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template element_chapter/progressbar + + Template for the chapter completion progressbar + + Example context (json): + { + "chapters": [{ + "id": 1, + "completion": true + }, { + "id": 2, + "completion": false + }] + } +}} +
+ {{#chapters}} +
+ +
+ {{/chapters}} +
diff --git a/element/chapter/tests/behat/chapter_visibility.feature b/element/chapter/tests/behat/chapter_visibility.feature new file mode 100644 index 0000000..70fac78 --- /dev/null +++ b/element/chapter/tests/behat/chapter_visibility.feature @@ -0,0 +1,41 @@ +@mod @mod_contentdesigner @element_chapter @javascript +Feature: Check content designer chapter element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a chapter element workflow + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=chapter]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Title | First chapter | + And I press "Create element" + Then I should see "First chapter" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 01 | + | Title | Heading 01 | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" diff --git a/element/chapter/version.php b/element/chapter/version.php new file mode 100644 index 0000000..e596f11 --- /dev/null +++ b/element/chapter/version.php @@ -0,0 +1,29 @@ +. + +/** + * element plugin "Chapter" - Version file. + * + * @package element_chapter + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_chapter'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/element/h5p/amd/build/h5p.min.js b/element/h5p/amd/build/h5p.min.js new file mode 100644 index 0000000..152f16d --- /dev/null +++ b/element/h5p/amd/build/h5p.min.js @@ -0,0 +1,3 @@ +define("element_h5p/h5p",["jquery","mod_contentdesigner/elements","core/ajax","core/notification"],(function($,Elements,AJAX,Notification){var interactedInstances=[];const elementH5P=instance=>{document.querySelector('.h5p-element-instance[data-instanceid="'+instance+'"]').querySelector(".h5p-iframe").onload=()=>h5pExternal(instance)},h5pExternal=instance=>{var iframe=document.querySelector('.h5p-element-instance[data-instanceid="'+instance+'"]').querySelector(".h5p-iframe");if(null!=iframe.contentWindow.H5P){var h5p=iframe.contentWindow.H5P;void 0!==h5p.externalDispatcher?h5p.externalDispatcher.on("xAPI",(function(event){if(event&&event.data&&event.data.statement){var statement=event.data.statement;if(statement.verb&&statement.verb.id){var extensionID,isCompleted="http://adlnet.gov/expapi/verbs/answered"===statement.verb.id||"http://adlnet.gov/expapi/verbs/completed"===statement.verb.id,isChild=statement.context&&statement.context.contextActivities&&statement.context.contextActivities.parent&&statement.context.contextActivities.parent[0]&&statement.context.contextActivities.parent[0].id,isInteracted=!1,isResponsed=!1;if("http://adlnet.gov/expapi/verbs/interacted"===statement.verb.id)try{extensionID=statement.object.definition.extensions["http://h5p.org/x-api/h5p-local-content-id"],interactedInstances[extensionID]=!0}catch(err){Notification.alert(err)}else{try{var _interactedInstances$;extensionID=statement.object.definition.extensions["http://h5p.org/x-api/h5p-local-content-id"],isInteracted=null!==(_interactedInstances$=interactedInstances[extensionID])&&void 0!==_interactedInstances$&&_interactedInstances$}catch(err){Notification.alert(err)}if(void 0!==statement.result){if(void 0!==statement.result.response){for(var _statement$result$sco,max=null!==(_statement$result$sco=statement.result.score.max)&&void 0!==_statement$result$sco?_statement$result$sco:0,response=statement.result.response,i=1;i<=max;i++)response=response.replace("[,]","");isResponsed=""!=response}else isResponsed=!0;var isPassed=statement.result.score.max<1||statement.result.score.max==statement.result.score.raw||void 0!==statement.result.success&&1==statement.result.success;if(isCompleted&&!isChild&&isResponsed&&isInteracted&&isPassed){var promises=storeUserResponse(statement,instance);if(!promises)return;promises[0].then((response=>{response&&(removeWarning(),Elements.refreshContent())})).catch(Notification.exception)}}}}}})):setTimeout((()=>elementH5P(instance)),200)}else setTimeout((()=>elementH5P(instance)),200)},removeWarning=()=>{null!==Elements.courseContent().querySelector(".label.label-warning")&&Elements.courseContent().querySelector(".label.label-warning").remove()},storeUserResponse=(statement,instance)=>{var _statement$result$com,_statement$result$suc,_statement$result$dur,_statement$result$res,_statement$result$sco2,_statement$result$sco3,_statement$result$sco4,_statement$result$sco5,params={cmid:Elements.contentDesignerData().cmid,instanceid:instance,result:{completion:null!==(_statement$result$com=statement.result.completion)&&void 0!==_statement$result$com?_statement$result$com:0,success:null!==(_statement$result$suc=statement.result.success)&&void 0!==_statement$result$suc?_statement$result$suc:0,duration:null!==(_statement$result$dur=statement.result.duration)&&void 0!==_statement$result$dur?_statement$result$dur:"",response:null!==(_statement$result$res=statement.result.response)&&void 0!==_statement$result$res?_statement$result$res:"",score:{min:null!==(_statement$result$sco2=statement.result.score.min)&&void 0!==_statement$result$sco2?_statement$result$sco2:0,max:null!==(_statement$result$sco3=statement.result.score.max)&&void 0!==_statement$result$sco3?_statement$result$sco3:0,raw:null!==(_statement$result$sco4=statement.result.score.raw)&&void 0!==_statement$result$sco4?_statement$result$sco4:0,scaled:null!==(_statement$result$sco5=statement.result.score.scaled)&&void 0!==_statement$result$sco5?_statement$result$sco5:0}}};return AJAX.call([{methodname:"element_h5p_store_result",args:params}])};return{init:function(instance){elementH5P(instance)}}})); + +//# sourceMappingURL=h5p.min.js.map \ No newline at end of file diff --git a/element/h5p/amd/build/h5p.min.js.map b/element/h5p/amd/build/h5p.min.js.map new file mode 100644 index 0000000..99ce88c --- /dev/null +++ b/element/h5p/amd/build/h5p.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"h5p.min.js","sources":["../src/h5p.js"],"sourcesContent":["define(['jquery', 'mod_contentdesigner/elements', 'core/ajax', 'core/notification'], function($, Elements, AJAX, Notification) {\n\n var interactedInstances = [];\n\n /**\n * H5P element. Get the user reponse after attempt and send a request to store data in moodle.\n *\n * @param {int} instance\n */\n const elementH5P = (instance) => {\n let instanceElem = document.querySelector('.h5p-element-instance[data-instanceid=\"' + instance + '\"]');\n var iframe = instanceElem.querySelector('.h5p-iframe');\n iframe.onload = () => h5pExternal(instance);\n };\n\n const h5pExternal = (instance) => {\n\n let instanceElem = document.querySelector('.h5p-element-instance[data-instanceid=\"' + instance + '\"]');\n var iframe = instanceElem.querySelector('.h5p-iframe');\n\n if (iframe.contentWindow.H5P == undefined) {\n setTimeout(() => elementH5P(instance), 200);\n return;\n }\n\n var h5p = iframe.contentWindow.H5P;\n\n if (h5p.externalDispatcher === undefined) {\n setTimeout(() => elementH5P(instance), 200);\n return;\n }\n\n\n h5p.externalDispatcher.on('xAPI', function(event) {\n\n // Skip malformed events.\n var hasStatement = event && event.data && event.data.statement;\n if (!hasStatement) {\n return;\n }\n\n var statement = event.data.statement;\n var validVerb = statement.verb && statement.verb.id;\n if (!validVerb) {\n return;\n }\n\n var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered'\n || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed';\n\n var isChild = statement.context && statement.context.contextActivities &&\n statement.context.contextActivities.parent &&\n statement.context.contextActivities.parent[0] &&\n statement.context.contextActivities.parent[0].id;\n // Attempted response only stored.\n var isInteract = statement.verb.id === 'http://adlnet.gov/expapi/verbs/interacted';\n var isInteracted = false;\n var isResponsed = false;\n var extensionID;\n if (isInteract) {\n try {\n extensionID = statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id'];\n interactedInstances[extensionID] = true;\n } catch (err) {\n Notification.alert(err);\n }\n return;\n } else {\n try {\n extensionID = statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id'];\n isInteracted = interactedInstances[extensionID] ?? false;\n } catch (err) {\n Notification.alert(err);\n }\n }\n\n if (statement.result === undefined) {\n return;\n }\n // Remove the separator[,] from response.\n if (statement.result.response !== undefined) {\n var max = statement.result.score.max ?? 0;\n var response = statement.result.response;\n for (var i = 1; i <= max; i++) {\n response = response.replace('[,]', '');\n }\n isResponsed = (response != '');\n } else {\n // Response is not available.\n isResponsed = true;\n }\n\n // If h5p has grade setup then student should pass all.\n var isPassed = (statement.result.score.max < 1\n || (statement.result.score.max == statement.result.score.raw)\n || (statement.result.success !== undefined && statement.result.success == true));\n\n if (isCompleted && !isChild && isResponsed && isInteracted && isPassed) {\n var promises = storeUserResponse(statement, instance);\n if (!promises) {\n return;\n }\n promises[0].then((response) => {\n if (response) {\n // Remove the warning message.\n removeWarning();\n // Update the other elemnets and chapters.\n Elements.refreshContent();\n }\n return;\n }).catch(Notification.exception);\n }\n });\n };\n\n /**\n * Remove the warning from response.\n */\n const removeWarning = () => {\n if (Elements.courseContent().querySelector('.label.label-warning') !== null) {\n Elements.courseContent().querySelector('.label.label-warning').remove();\n }\n };\n\n /**\n * Send the request to store the user h5p response.\n * @param {Object} statement\n * @param {int} instance\n * @returns {Object}\n */\n const storeUserResponse = (statement, instance) => {\n\n var params = {\n cmid: Elements.contentDesignerData().cmid,\n instanceid: instance,\n result: {\n completion: statement.result.completion ?? 0,\n success: statement.result.success ?? 0,\n duration: statement.result.duration ?? '',\n response: statement.result.response ?? '',\n score: {\n min: statement.result.score.min ?? 0,\n max: statement.result.score.max ?? 0,\n raw: statement.result.score.raw ?? 0,\n scaled: statement.result.score.scaled ?? 0\n }\n },\n };\n\n var promises = AJAX.call([{\n methodname: 'element_h5p_store_result',\n args: params\n }]);\n\n return promises;\n };\n\n return {\n init: function(instance) {\n elementH5P(instance);\n }\n };\n});\n"],"names":["define","$","Elements","AJAX","Notification","interactedInstances","elementH5P","instance","document","querySelector","onload","h5pExternal","iframe","undefined","contentWindow","H5P","h5p","externalDispatcher","on","event","data","statement","verb","id","extensionID","isCompleted","isChild","context","contextActivities","parent","isInteracted","isResponsed","object","definition","extensions","err","alert","result","response","max","score","i","replace","isPassed","raw","success","promises","storeUserResponse","then","removeWarning","refreshContent","catch","exception","setTimeout","courseContent","remove","params","cmid","contentDesignerData","instanceid","completion","duration","min","scaled","call","methodname","args","init"],"mappings":"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"} \ No newline at end of file diff --git a/element/h5p/amd/src/h5p.js b/element/h5p/amd/src/h5p.js new file mode 100644 index 0000000..d7f30b2 --- /dev/null +++ b/element/h5p/amd/src/h5p.js @@ -0,0 +1,163 @@ +define(['jquery', 'mod_contentdesigner/elements', 'core/ajax', 'core/notification'], function($, Elements, AJAX, Notification) { + + var interactedInstances = []; + + /** + * H5P element. Get the user reponse after attempt and send a request to store data in moodle. + * + * @param {int} instance + */ + const elementH5P = (instance) => { + let instanceElem = document.querySelector('.h5p-element-instance[data-instanceid="' + instance + '"]'); + var iframe = instanceElem.querySelector('.h5p-iframe'); + iframe.onload = () => h5pExternal(instance); + }; + + const h5pExternal = (instance) => { + + let instanceElem = document.querySelector('.h5p-element-instance[data-instanceid="' + instance + '"]'); + var iframe = instanceElem.querySelector('.h5p-iframe'); + + if (iframe.contentWindow.H5P == undefined) { + setTimeout(() => elementH5P(instance), 200); + return; + } + + var h5p = iframe.contentWindow.H5P; + + if (h5p.externalDispatcher === undefined) { + setTimeout(() => elementH5P(instance), 200); + return; + } + + + h5p.externalDispatcher.on('xAPI', function(event) { + + // Skip malformed events. + var hasStatement = event && event.data && event.data.statement; + if (!hasStatement) { + return; + } + + var statement = event.data.statement; + var validVerb = statement.verb && statement.verb.id; + if (!validVerb) { + return; + } + + var isCompleted = statement.verb.id === 'http://adlnet.gov/expapi/verbs/answered' + || statement.verb.id === 'http://adlnet.gov/expapi/verbs/completed'; + + var isChild = statement.context && statement.context.contextActivities && + statement.context.contextActivities.parent && + statement.context.contextActivities.parent[0] && + statement.context.contextActivities.parent[0].id; + // Attempted response only stored. + var isInteract = statement.verb.id === 'http://adlnet.gov/expapi/verbs/interacted'; + var isInteracted = false; + var isResponsed = false; + var extensionID; + if (isInteract) { + try { + extensionID = statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id']; + interactedInstances[extensionID] = true; + } catch (err) { + Notification.alert(err); + } + return; + } else { + try { + extensionID = statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id']; + isInteracted = interactedInstances[extensionID] ?? false; + } catch (err) { + Notification.alert(err); + } + } + + if (statement.result === undefined) { + return; + } + // Remove the separator[,] from response. + if (statement.result.response !== undefined) { + var max = statement.result.score.max ?? 0; + var response = statement.result.response; + for (var i = 1; i <= max; i++) { + response = response.replace('[,]', ''); + } + isResponsed = (response != ''); + } else { + // Response is not available. + isResponsed = true; + } + + // If h5p has grade setup then student should pass all. + var isPassed = (statement.result.score.max < 1 + || (statement.result.score.max == statement.result.score.raw) + || (statement.result.success !== undefined && statement.result.success == true)); + + if (isCompleted && !isChild && isResponsed && isInteracted && isPassed) { + var promises = storeUserResponse(statement, instance); + if (!promises) { + return; + } + promises[0].then((response) => { + if (response) { + // Remove the warning message. + removeWarning(); + // Update the other elemnets and chapters. + Elements.refreshContent(); + } + return; + }).catch(Notification.exception); + } + }); + }; + + /** + * Remove the warning from response. + */ + const removeWarning = () => { + if (Elements.courseContent().querySelector('.label.label-warning') !== null) { + Elements.courseContent().querySelector('.label.label-warning').remove(); + } + }; + + /** + * Send the request to store the user h5p response. + * @param {Object} statement + * @param {int} instance + * @returns {Object} + */ + const storeUserResponse = (statement, instance) => { + + var params = { + cmid: Elements.contentDesignerData().cmid, + instanceid: instance, + result: { + completion: statement.result.completion ?? 0, + success: statement.result.success ?? 0, + duration: statement.result.duration ?? '', + response: statement.result.response ?? '', + score: { + min: statement.result.score.min ?? 0, + max: statement.result.score.max ?? 0, + raw: statement.result.score.raw ?? 0, + scaled: statement.result.score.scaled ?? 0 + } + }, + }; + + var promises = AJAX.call([{ + methodname: 'element_h5p_store_result', + args: params + }]); + + return promises; + }; + + return { + init: function(instance) { + elementH5P(instance); + } + }; +}); diff --git a/element/h5p/backup/moodle2/backup_element_h5p_subplugin.class.php b/element/h5p/backup/moodle2/backup_element_h5p_subplugin.class.php new file mode 100644 index 0000000..9e9db6b --- /dev/null +++ b/element/h5p/backup/moodle2/backup_element_h5p_subplugin.class.php @@ -0,0 +1,76 @@ +. + +/** + * This file contains the backup code for the element_h5p plugin. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Provides the information to backup feedback files. + * + * This just adds its filearea to the annotations and records the number of files. + */ +class backup_element_h5p_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to h5p element + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + $userinfo = $this->get_setting_value('userinfo'); + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_h5p', array('id'), array( + 'contentdesignerid', 'title', 'visible', 'package', 'mandatory', 'timecreated', 'timemodified' + )); + + $h5pcompletion = new backup_nested_element('elementh5p_completion'); + $h5pcompletionelement = new backup_nested_element('element_h5p_completion', array('id'), array( + 'instance', 'userid', 'completion', 'success', 'score', 'scoredata', 'response', 'timecreated', 'timemodified' + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + $subplugin->add_child($h5pcompletion); + $h5pcompletion->add_child($h5pcompletionelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_h5p', array('contentdesignerid' => backup::VAR_PARENTID)); + + if ($userinfo) { + $sql = 'SELECT * FROM {element_h5p_completion} WHERE instance IN ( + SELECT id FROM {element_h5p} WHERE contentdesignerid=:contentdesignerid + )'; + $h5pcompletionelement->set_source_sql($sql, array('contentdesignerid' => backup::VAR_PARENTID)); + $h5pcompletionelement->annotate_ids('user', 'userid'); + } + + $subpluginelement->annotate_files('element_h5p', 'package', null); + $subpluginelement->annotate_files('mod_contentdesigner', 'h5pelementbg', null); + + return $subplugin; + } + +} diff --git a/element/h5p/backup/moodle2/restore_element_h5p_subplugin.class.php b/element/h5p/backup/moodle2/restore_element_h5p_subplugin.class.php new file mode 100644 index 0000000..12801c5 --- /dev/null +++ b/element/h5p/backup/moodle2/restore_element_h5p_subplugin.class.php @@ -0,0 +1,65 @@ +. + +/** + * This file contains the restore code for the feedback_file plugin. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore element_h5p subplugin. + */ +class restore_element_h5p_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_h5p'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes one element_h5p element + * @param mixed $data + */ + public function process_element_h5p_instance($data) { + global $DB; + + $data = (object)$data; + + $oldinstance = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + $newinstance = $DB->insert_record('element_h5p', $data); + $this->set_mapping('h5p_instanceid', $oldinstance, $newinstance, true); + // Add files need to restore. + $this->add_related_files('element_h5p', 'package', 'h5p_instanceid', null, $oldinstance); + $this->add_related_files('mod_contentdesigner', 'h5pelementbg', 'h5p_instanceid', null, $oldinstance); + } +} diff --git a/element/h5p/classes/element.php b/element/h5p/classes/element.php new file mode 100644 index 0000000..c39e797 --- /dev/null +++ b/element/h5p/classes/element.php @@ -0,0 +1,202 @@ +. + +/** + * Extended class of elements for chapter. it contains major part of editor element content + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_h5p; + +use mod_contentdesigner\editor; + +/** + * Element h5p definition. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'h5p'; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_h5p'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + global $CFG; + return (file_exists($CFG->dirroot.'/mod/h5pactivity/pix/monologo.png')) + ? $output->pix_icon('monologo', '', 'mod_h5pactivity', ['class' => 'icon pluginicon']) + : $output->pix_icon('icon', '', 'mod_h5pactivity', ['class' => 'icon pluginicon']); + } + + /** + * List of areafiles which is used the mod_contentdesigner as component. + * + * @return array + */ + public function areafiles() { + return ['package']; + } + + /** + * Save the area files data after the element instance moodle_form submittted. + * + * @param stdclas $data Submitted moodle_form data. + */ + public function save_areafiles($data) { + parent::save_areafiles($data); + file_save_draft_area_files($data->package, $data->contextid, 'element_h5p', 'package', $data->instance); + } + + /** + * Prepare the form editor elements file data before render the elemnent form. + * + * @param stdclass $formdata + * @return stdclass + */ + public function prepare_standard_file_editor(&$formdata) { + $formdata = parent::prepare_standard_file_editor($formdata); + + if (isset($formdata->instance)) { + $draftitemid = file_get_submitted_draft_itemid('package'); + file_prepare_draft_area($draftitemid, $this->context->id, 'element_h5p', 'package', $formdata->instance, + array('subdirs' => 0, 'maxfiles' => 1)); + $formdata->package = $draftitemid; + } + return $formdata; + } + + /** + * Analyze the H5P is mantory to view upcoming then check the instance is attempted. + * + * @param stdclass $instance Instance data of the element. + * @return bool True if need to stop the next instance Otherwise false if render of next elements. + */ + public function prevent_nextelements($instance): bool { + global $USER, $DB; + if (isset($instance->mandatory) && $instance->mandatory) { + return !$DB->record_exists('element_h5p_completion', [ + 'instance' => $instance->id, 'userid' => $USER->id, 'completion' => true + ]); + } + return false; + } + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + $options = [ + 'accepted_types' => ['.h5p'], + 'maxbytes' => 0, + 'maxfiles' => 1, + 'subdirs' => 0, + ]; + + $mform->addElement('filemanager', 'package', get_string('package', 'mod_h5pactivity'), null, $options); + $mform->addHelpButton('package', 'package', 'mod_h5pactivity'); + $mform->addRule('package', null, 'required'); + + $options = [ + 0 => get_string('no'), + 1 => get_string('yes') + ]; + $mform->addElement('select', 'mandatory', get_string('mandatory', 'mod_contentdesigner'), $options); + $mform->addHelpButton('mandatory', 'mandatory', 'mod_contentdesigner'); + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $data + * @return void + */ + public function render($data) { + global $PAGE; + if (!isset($data->id)) { + return ''; + } + $file = editor::get_editor($data->cmid)->get_element_areafiles('package', $data->id, 'element_h5p'); + $PAGE->requires->js_call_amd('element_h5p/h5p', 'init', ['instance' => $data->instance]); + $completiontable = $this->generate_completion_table($data); + return \html_writer::div( + format_text($file), 'h5p-element-instance', ['data-instanceid' => $data->instance] + ) . $completiontable; + } + + /** + * Generate the result table to display the user atempts to user. It display the highest grade of the user attempt. + * + * @param stdclass $data Instance data of the element. + * @return void + */ + public function generate_completion_table($data) { + global $USER, $DB, $OUTPUT; + $instance = isset($data->instance) ? $data->instance : ''; + $params = ['instance' => $instance]; + if (!has_capability('element/h5p:viewstudentrecords', $this->get_context())) { + $params['userid'] = $USER->id; + } + $results = $DB->get_records('element_h5p_completion', $params); + if (!empty($results)) { + $strings = (array) get_strings(['score', 'maxscore', 'completion'], 'mod_h5pactivity'); + $table = new \html_table(); + $table->head = array_merge(array('#', get_string('date')), $strings, [get_string('success')]); + foreach ($results as $record) { + $table->data[] = array( + $record->id, + userdate($record->timecreated, get_string('strftimedatefullshort', 'core_langconfig')), + $record->score, + json_decode($record->scoredata)->max, + ($record->completion ? $OUTPUT->pix_icon('e/tick', 'core') : $OUTPUT->pix_icon('t/dockclose', 'core')), + ($record->success ? $OUTPUT->pix_icon('e/tick', 'core') : $OUTPUT->pix_icon('t/dockclose', 'core') ), + ); + } + return \html_writer::tag('h3', get_string('highestgrade', 'element_h5p')).\html_writer::table($table); + } + + } + +} diff --git a/element/h5p/classes/external.php b/element/h5p/classes/external.php new file mode 100644 index 0000000..b5d2ae0 --- /dev/null +++ b/element/h5p/classes/external.php @@ -0,0 +1,115 @@ +. + +/** + * Chapter element external werbservice deifintion to manage the chapter completion. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_h5p; + +defined('MOODLE_INTERNAL') || die('No direct access'); + +use external_value; +require_once($CFG->libdir . '/externallib.php'); + +/** + * Chapter external service methods. + */ +class external extends \external_api { + + /** + * Paramters definition for the methos update chapter progress of user. + * + * @return external_function_parameters + */ + public static function store_result_data_parameters() { + return new \external_function_parameters( + array( + 'cmid' => new external_value(PARAM_INT, 'Course module id'), + 'instanceid' => new external_value(PARAM_INT, 'H5P element instance id'), + 'result' => new \external_single_structure( + array( + 'completion' => new external_value(PARAM_BOOL, 'Attempt userid'), + 'success' => new external_value(PARAM_BOOL, 'Attempt userid'), + 'response' => new external_value(PARAM_TEXT, 'Response of the user attempt', VALUE_OPTIONAL), + 'duration' => new external_value(PARAM_TEXT, 'Duration of the user attempt', VALUE_OPTIONAL), + 'score' => new \external_single_structure( + array( + 'max' => new external_value(PARAM_FLOAT, 'Max number of score'), + 'min' => new external_value(PARAM_FLOAT, 'Max number of score'), + 'scaled' => new external_value(PARAM_FLOAT, 'Max number of score'), + 'raw' => new external_value(PARAM_ALPHANUMEXT, 'Max number of score'), + ) + ) + ) + ), + ) + ); + } + + /** + * Store the user response for the H5P. + * + * @param int $cmid Course module id. + * @param int $instanceid Instance id of the H5P. + * @param array $result Result object form the H5P data statement. + * @return bool + */ + public static function store_result_data($cmid, $instanceid, $result) { + global $DB, $USER; + + if ($record = $DB->get_record('element_h5p_completion', ['instance' => $instanceid, 'userid' => $USER->id])) { + // Store only highest scored results only. + if ($record->score > $result['score']['raw']) { + return true; + } + } + + $data = new \stdclass(); + $data->instance = $instanceid; + $data->userid = $USER->id; + $data->completion = $result['completion']; + $data->success = $result['success']; + $data->duration = $result['duration'] ?: ''; + $data->score = $result['score']['raw']; + $data->scoredata = json_encode($result['score']); + $data->response = $result['response']; + $data->timemodified = time(); + if (isset($record->id)) { + $data->id = $record->id; + $DB->update_record('element_h5p_completion', $data); + } else { + $data->timecreated = time(); + if (!$DB->insert_record('element_h5p_completion', $data)) { + return false; + } + } + return true; + } + + /** + * Returns the updated result of store data. + * + * @return external_value True if data updated otherwise returns false. + */ + public static function store_result_data_returns() { + return new external_value(PARAM_BOOL, 'Result of stored user response'); + } +} diff --git a/element/h5p/classes/privacy/provider.php b/element/h5p/classes/privacy/provider.php new file mode 100644 index 0000000..ec01b7e --- /dev/null +++ b/element/h5p/classes/privacy/provider.php @@ -0,0 +1,194 @@ +. + +/** + * Privacy class for requesting user data. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_h5p\privacy; + +use \core_privacy\local\metadata\collection; +use core_privacy\local\request\transform; +use \core_privacy\local\request\userlist; +use \core_privacy\local\request\approved_userlist; +use \core_privacy\local\request\approved_contextlist; + +/** + * Privacy class for requesting user data. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements + \core_privacy\local\metadata\provider, + \mod_contentdesigner\privacy\contentdesignerelements_provider { + + /** + * List of used data fields summary meta key. + * + * @param collection $collection + * @return collection + */ + public static function get_metadata(collection $collection): collection { + + // Module Completion table fields meta summary. + $completionmetadata = [ + 'instance' => 'privacy:metadata:completion:h5pid', + 'userid' => 'privacy:metadata:completion:userid', + 'completion' => 'privacy:metadata:completion:completion', + 'success' => 'privacy:metadata:completion:success', + 'score' => 'privacy:metadata:completion:score', + 'timecreated' => 'privacy:metadata:completion:timecreated' + ]; + $collection->add_database_table('element_h5p_completion', $completionmetadata, + 'privacy:metadata:contentdesignercompletion'); + + return $collection; + } + + /** + * Helper function to export completions. + * + * The array of "completions" is actually the result returned by the SQL in export_user_data. + * It is more of a list of sessions. Which is why it needs to be grouped by context id. + * + * @param array $contentdesignerids Array of completions to export the logs for. + * @param stdclass $user User record object. + */ + public static function export_element_user_data(array $contentdesignerids, \stdclass $user) { + global $DB; + + list($insql, $inparams) = $DB->get_in_or_equal($contentdesignerids, SQL_PARAMS_NAMED); + $sql = "SELECT ec.*, ecc.userid AS userid, ecc.completion AS completion, + ecc.timecreated AS timecompleted, ecc.success, ecc.score + FROM {element_h5p} ec + INNER JOIN {element_h5p_completion} ecc ON ecc.instance = ec.id AND ecc.userid = :userid + WHERE ec.contentdesignerid {$insql} + ORDER BY ec.id"; + + $params = [ + 'userid' => $user->id, + ]; + $completions = $DB->get_records_sql($sql, $params + $inparams); + foreach ($completions as $h5pid => $completion) { + $data[$h5pid] = (object) [ + 'completed' => (($completion->completion == 1) ? get_string('yes') : get_string('no')), + 'completedtime' => $completion->timecreated ? transform::datetime($completion->timecreated) : '-', + 'title' => $completion->title, + 'contentdesignerid' => $completion->contentdesignerid, + 'success' => ($completion->success == 1) ? get_string('yes') : get_string('no'), + 'score' => $completion->score + ]; + } + return $data; + } + + /** + * Get the list of users who have data within a context. + * + * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. + */ + public static function get_users_in_context(userlist $userlist) { + $context = $userlist->get_context(); + + if (!$context instanceof \context_module) { + return; + } + + $params = [ + 'instanceid' => $context->instanceid, + 'modulename' => 'contentdesigner', + ]; + + // Discussion authors. + $sql = "SELECT ecc.userid + FROM {element_h5p} ec + JOIN {element_h5p_completion} ecc ON ecc.instance = ec.id + WHERE ec.contentdesignerid = :instanceid"; + $userlist->add_from_sql('userid', $sql, $params); + } + + /** + * Delete multiple users within a single context. + * + * @param approved_userlist $userlist The approved context and user information to delete information for. + */ + public static function delete_data_for_users(approved_userlist $userlist) { + global $DB; + + $context = $userlist->get_context(); + $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); + $contentdesigner = $DB->get_record('contentdesigner', ['id' => $cm->instance]); + + list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED, 'usr'); + $list = $DB->get_records('element_h5p', ['contentdesignerid' => $contentdesigner->id]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_h5p_completion', "userid {$userinsql} AND instance $insql ", + $userinparams + $inparams ); + } + + /** + * Delete user completion data for multiple context. + * + * @param approved_contextlist $contextlist The approved context and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + if (empty($contextlist->count())) { + return; + } + + $userid = $contextlist->get_user()->id; + foreach ($contextlist->get_contexts() as $context) { + $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); + $list = $DB->get_records('element_h5p', ['contentdesignerid' => $instanceid]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_h5p_completion', "userid=:userid AND instance $insql", + ['userid' => $userid] + $inparams ); + } + } + + /** + * Delete all completion data for all users in the specified context. + * + * @param context $context Context to delete data from. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + if ($context->contextlevel != CONTEXT_MODULE) { + return; + } + + $cm = get_coursemodule_from_id('contentdesigner', $context->instanceid); + if (!$cm) { + return; + } + $list = $DB->get_records('element_h5p', ['contentdesignerid' => $cm->instance]); + $ids = array_column($list, 'id'); + list($insql, $inparams) = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED, 'ch'); + $DB->delete_records_select('element_h5p_completion', "instance $insql", $inparams); + + } +} diff --git a/element/h5p/db/access.php b/element/h5p/db/access.php new file mode 100644 index 0000000..03cc26b --- /dev/null +++ b/element/h5p/db/access.php @@ -0,0 +1,37 @@ +. + +/** + * Define H5P element plugin capabilities. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); + +$capabilities = array( + + 'element/h5p:viewstudentrecords' => array( + 'riskbitmask' => RISK_SPAM, + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ) + ), +); diff --git a/element/h5p/db/install.php b/element/h5p/db/install.php new file mode 100644 index 0000000..e04edc3 --- /dev/null +++ b/element/h5p/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_h5p_install() { + $shortname = \element_h5p\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/h5p/db/install.xml b/element/h5p/db/install.xml new file mode 100644 index 0000000..af09b65 --- /dev/null +++ b/element/h5p/db/install.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
+
diff --git a/element/h5p/db/services.php b/element/h5p/db/services.php new file mode 100644 index 0000000..1b0d98a --- /dev/null +++ b/element/h5p/db/services.php @@ -0,0 +1,37 @@ +. + +/** + * Element h5p services defined. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +$functions = array( + + 'element_h5p_store_result' => array( + 'classname' => 'element_h5p\external', + 'methodname' => 'store_result_data', + 'description' => 'Store the user attempt result of H5P', + 'type' => 'write', + 'capabilities' => 'mod/contentdesigner:view', + 'ajax' => true + ), +); diff --git a/element/h5p/lang/en/element_h5p.php b/element/h5p/lang/en/element_h5p.php new file mode 100644 index 0000000..03eef81 --- /dev/null +++ b/element/h5p/lang/en/element_h5p.php @@ -0,0 +1,37 @@ +. + +/** + * Element plugin "H5P" - string file. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Interactive content (H5P)"; +$string['elementdescription'] = 'Interactive content using H5P'; +$string['highestgrade'] = 'Highest grade'; +$string['privacy:metadata:completion:chapterid'] = 'H5P instance id'; +$string['privacy:metadata:completion:userid'] = 'User id'; +$string['privacy:metadata:completion:completion'] = 'Completion Status'; +$string['privacy:metadata:completion:timecreated'] = 'Time completed the H5P'; +$string['privacy:metadata:completion:score'] = 'Attempt score'; +$string['privacy:metadata:completion:success'] = 'Is attempt success'; +$string['privacy:h5p'] = 'H5P'; +$string['h5p:viewstudentrecords'] = 'View student attempt results'; diff --git a/element/h5p/tests/behat/h5p_visibility.feature b/element/h5p/tests/behat/h5p_visibility.feature new file mode 100644 index 0000000..405ccf5 --- /dev/null +++ b/element/h5p/tests/behat/h5p_visibility.feature @@ -0,0 +1,54 @@ +@mod @mod_contentdesigner @element_h5p @javascript @core_h5p @_file_upload @_switch_iframe +Feature: Check content designer h5p element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/h5p:updatelibraries | Allow | editingteacher | System | | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a h5p element + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=h5p]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Package file | h5p/tests/fixtures/ipsums.h5p | + | Mandatory | No | + | Title | Test H5p | + And I press "Create element" + And I click on ".contentdesigner-addelement[data-position=\"bottom\"] .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 01 | + | Title | Heading 01 | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Heading 01" + And I wait "5" seconds + Then ".h5p-element-instance" "css_element" should exist + And I click on "Content editor" "link" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + And I set the following fields to these values: + | Mandatory | Yes | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should not see "Heading 01" diff --git a/element/h5p/version.php b/element/h5p/version.php new file mode 100644 index 0000000..559298e --- /dev/null +++ b/element/h5p/version.php @@ -0,0 +1,29 @@ +. + +/** + * Element plugin "H5P" - Version file. + * + * @package element_h5p + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_h5p'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/element/heading/backup/moodle2/backup_element_heading_subplugin.class.php b/element/heading/backup/moodle2/backup_element_heading_subplugin.class.php new file mode 100644 index 0000000..3fe4b02 --- /dev/null +++ b/element/heading/backup/moodle2/backup_element_heading_subplugin.class.php @@ -0,0 +1,55 @@ +. + +/** + * This file contains the backup code for the element_heading plugin. + * + * @package element_heading + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Provides the information to backup feedback files. + */ +class backup_element_heading_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to heading element + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_heading', array('id'), array( + 'title', 'visible', 'heading', 'headingurl', 'headingtype', + 'target', 'horizontal', 'vertical', 'timecreated', 'timemodified' + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_heading', array('contentdesignerid' => backup::VAR_PARENTID)); + $subpluginelement->annotate_ids('heading_instanceid', 'id'); + + return $subplugin; + } + +} diff --git a/element/heading/backup/moodle2/restore_element_heading_subplugin.class.php b/element/heading/backup/moodle2/restore_element_heading_subplugin.class.php new file mode 100644 index 0000000..ad59e28 --- /dev/null +++ b/element/heading/backup/moodle2/restore_element_heading_subplugin.class.php @@ -0,0 +1,64 @@ +. + +/** + * This file contains the restore code for the element_heading plugin. + * + * @package element_heading + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore heading element instance. + */ +class restore_element_heading_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_heading'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes heading element instance. + * @param array $data + */ + public function process_element_heading_instance($data) { + global $DB; + + $data = (object)$data; + + $oldinstance = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + $newinstance = $DB->insert_record('element_heading', $data); + $this->set_mapping('heading_instanceid', $oldinstance, $newinstance, true); + $this->add_related_files('mod_contentdesigner', 'headingelementbg', 'heading_instanceid', null, $oldinstance); + } + +} diff --git a/element/heading/classes/element.php b/element/heading/classes/element.php new file mode 100644 index 0000000..5fe9713 --- /dev/null +++ b/element/heading/classes/element.php @@ -0,0 +1,140 @@ +. + +/** + * Heading element instance class. + * + * @package element_heading + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace element_heading; + +use html_writer; + +/** + * Heading element instance class. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'heading'; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_heading'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + return html_writer::tag('i', '', ['class' => 'fa fa-header icon pluginicon']); + } + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + + $mform->addElement( + 'text', 'heading', get_string('headingtext', 'mod_contentdesigner'), 'maxlength="100" size="30"' + ); + $mform->setType('heading', PARAM_NOTAGS); + $mform->addRule('heading', null, 'required'); + $mform->addHelpButton('heading', 'headingtext', 'mod_contentdesigner'); + + $mform->addElement( + 'url', 'headingurl', get_string('headingurl', 'mod_contentdesigner'), + array('size' => '60'), array('usefilepicker' => true) + ); + $mform->setType('headingurl', PARAM_RAW_TRIMMED); + $mform->addHelpButton('headingurl', 'headingurl', 'mod_contentdesigner'); + + $headings = [ + 'h2' => get_string('mainheading', 'mod_contentdesigner'), + 'h3' => get_string('subheading', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'headingtype', get_string('strheading', 'mod_contentdesigner'), $headings); + $mform->addHelpButton('headingtype', 'strheading', 'mod_contentdesigner'); + + $targets = [ + '_blank' => get_string('strblank', 'mod_contentdesigner'), + '_self' => get_string('strself', 'mod_contentdesigner'), + ]; + $mform->addElement('select', 'target', get_string('target', 'mod_contentdesigner'), $targets); + $mform->addHelpButton('target', 'target', 'mod_contentdesigner'); + + $horizontalalign = [ + 'left' => get_string('strleft', 'mod_contentdesigner'), + 'center' => get_string('strcenter', 'mod_contentdesigner'), + 'right' => get_string('strright', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'horizontal', get_string('horizontalalign', 'mod_contentdesigner'), $horizontalalign); + $mform->addHelpButton('horizontal', 'horizontalalign', 'mod_contentdesigner'); + + $verticalalign = [ + 'top' => get_string('strtop', 'mod_contentdesigner'), + 'middle' => get_string('strmiddle', 'mod_contentdesigner'), + 'bottom' => get_string('strbottom', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'vertical', get_string('verticalalign', 'mod_contentdesigner'), $verticalalign); + $mform->addHelpButton('vertical', 'verticalalign', 'mod_contentdesigner'); + + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $instance + * @return void + */ + public function render($instance) { + global $DB; + $content = ''; + if ($instance->visible && $instance->heading && $instance->headingtype) { + $hozclass = "hl-". $instance->horizontal; + $vertclass = "vl-". $instance->vertical; + $heading = html_writer::tag($instance->headingtype, format_string($instance->heading), + ['class' => "element-heading $hozclass $vertclass"]); + $content .= ($instance->headingurl) + ? html_writer::link($instance->headingurl, $heading, ['target' => $instance->target]) : $heading; + } + return $content; + } +} diff --git a/element/heading/db/install.php b/element/heading/db/install.php new file mode 100644 index 0000000..c17cbf3 --- /dev/null +++ b/element/heading/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_heading + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_heading_install() { + $shortname = \element_heading\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/heading/db/install.xml b/element/heading/db/install.xml new file mode 100644 index 0000000..ae6827b --- /dev/null +++ b/element/heading/db/install.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/element/heading/lang/en/element_heading.php b/element/heading/lang/en/element_heading.php new file mode 100644 index 0000000..198fb8a --- /dev/null +++ b/element/heading/lang/en/element_heading.php @@ -0,0 +1,30 @@ +. + +/** + * Element plugin "Heading" - string file. + * + * @package element_heading + * @copyright bdecent GmbH 2021 + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Heading"; +$string['elementdescription'] = 'Add a heading'; + + diff --git a/element/heading/tests/behat/header_visibility.feature b/element/heading/tests/behat/header_visibility.feature new file mode 100644 index 0000000..385dfd1 --- /dev/null +++ b/element/heading/tests/behat/header_visibility.feature @@ -0,0 +1,57 @@ +@mod @mod_contentdesigner @element_heading @javascript +Feature: Check content designer header element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a heading element workflow + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading URL | https://www.example.com | + | Heading | Main heading (h2) | + | Heading text | Heading 01 | + | Title | Heading 01 | + | Target | Open a same window | + | Horizontal Alignment | Right | + | Vertical Alignment | Middle | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" + Then ".chapter-elements-list li.element-item h2.element-heading.hl-right.vl-middle" "css_element" should exist + Then ".chapter-elements-list li.element-item a[target=_self]" "css_element" should exist + And I click on "Content editor" "link" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + And I set the following fields to these values: + | Heading URL | https://www.example.com | + | Heading text | Heading First | + | Title | Heading First | + | Target | Open a new window | + | Horizontal Alignment | Center | + | Vertical Alignment | Top | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should not see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" + Then I should see "Heading First" in the ".chapter-elements-list li.element-item" "css_element" + Then ".chapter-elements-list li.element-item h2.element-heading.hl-center.vl-top" "css_element" should exist + Then ".chapter-elements-list li.element-item a[target=_blank]" "css_element" should exist diff --git a/element/heading/version.php b/element/heading/version.php new file mode 100644 index 0000000..d3aec2e --- /dev/null +++ b/element/heading/version.php @@ -0,0 +1,29 @@ +. + +/** + * element plugin "Heading" - Version file. + * + * @package element_heading + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_heading'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/element/outro/backup/moodle2/backup_element_outro_subplugin.class.php b/element/outro/backup/moodle2/backup_element_outro_subplugin.class.php new file mode 100644 index 0000000..fe041ca --- /dev/null +++ b/element/outro/backup/moodle2/backup_element_outro_subplugin.class.php @@ -0,0 +1,61 @@ +. + +/** + * This file contains the backup code for the element_outro plugin. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Provides the information to backup outro image. + * + * This just adds its filearea to the annotations and records the files. + */ +class backup_element_outro_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to outro element. + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_outro', array('id'), array( + 'contentdesignerid', 'title', 'visible', 'image', 'primarytext', 'primaryurl', + 'secondarytext', 'secondaryurl', 'outrocontent', 'outrocontentformat', 'primarybutton', + 'secondarybutton', 'timecreated', 'timemodified', + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_outro', array('contentdesignerid' => backup::VAR_PARENTID)); + $subpluginelement->annotate_ids('outro_instanceid', 'id'); + + $subpluginelement->annotate_files('mod_contentdesigner', 'element_outro_outroimage', null); + $subpluginelement->annotate_files('mod_contentdesigner', 'element_outro_outrocontent', null); + + return $subplugin; + } + +} diff --git a/element/outro/backup/moodle2/restore_element_outro_subplugin.class.php b/element/outro/backup/moodle2/restore_element_outro_subplugin.class.php new file mode 100644 index 0000000..9575d03 --- /dev/null +++ b/element/outro/backup/moodle2/restore_element_outro_subplugin.class.php @@ -0,0 +1,74 @@ +. + +/** + * This file contains the restore code for the element_outro plugin. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore outro element subplugin. + */ +class restore_element_outro_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_outro'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes outro element instance. + * @param array $data + */ + public function process_element_outro_instance($data) { + global $DB; + + $data = (object)$data; + + $oldinstance = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + $newinstance = $DB->insert_record('element_outro', $data); + $this->set_mapping('outro_instanceid', $oldinstance, $newinstance, true); + + $this->add_related_files('mod_contentdesigner', 'element_outro_outroimage', 'outro_instanceid', null, $oldinstance); + $this->add_related_files('mod_contentdesigner', 'outroelementbg', 'outro_instanceid', null, $oldinstance); + } + + /** + * Restore the editor images after the instance executed. + * + * @return void + */ + public function after_execute_contentdesigner() { + $this->add_related_files('mod_contentdesigner', 'element_outro_outrocontent', 'outro_instanceid'); + } +} diff --git a/element/outro/classes/element.php b/element/outro/classes/element.php new file mode 100644 index 0000000..0976201 --- /dev/null +++ b/element/outro/classes/element.php @@ -0,0 +1,361 @@ +. + +/** + * Extended class of elements for Outro. Only visible when the user reached the end of module content. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_outro; + +use html_writer; +use mod_contentdesigner\editor; +use moodle_url; + +/** + * Outro element instance, inherit the methods from elmenets base class. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'outro'; + + /** + * Outro button disabled option. + */ + const OUTRO_BUTTON_DISABLED = 0; + + /** + * Outro button custom option. + */ + const OUTRO_BUTTON_CUSTOM = 1; + + /** + * Outro button next option. + */ + const OUTRO_BUTTON_NEXT = 2; + + /** + * Outro button back ot course option. + */ + const OUTRO_BUTTON_BACKTOCOURSE = 3; + + /** + * Outro button back ot section option. + */ + const OUTRO_BUTTON_BACKTOSECTION = 4; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_outro'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Outro doesn't supports multiple instance for one course module. It added automatically for module. + * + * @return bool + */ + public function supports_multiple_instance() { + return false; + } + + /** + * List of areafiles which is used the mod_contentdesigner as component. + * + * @return array + */ + public function areafiles() { + return ['outroimage', 'outrocontent']; + } + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + + $options = array('maxfiles' => 1, 'accepted_types' => ['image']); + $mform->addElement('filemanager', 'image', get_string('strimage', 'mod_contentdesigner'), null, $options); + $mform->addHelpButton('image', 'strimage', 'mod_contentdesigner'); + + $editoroptions = $this->editor_options($formobj->_customdata['context']); + $mform->addElement('editor', 'outrocontent_editor', get_string('content', 'mod_contentdesigner'), null, $editoroptions); + $mform->setType('outrocontent_editor', PARAM_RAW); + $mform->addHelpButton('outrocontent_editor', 'content', 'mod_contentdesigner'); + + // Primary Button. + $options = [ + self::OUTRO_BUTTON_DISABLED => get_string('disable'), + self::OUTRO_BUTTON_CUSTOM => get_string('outro:btncustom', 'mod_contentdesigner'), + self::OUTRO_BUTTON_NEXT => get_string('outro:btnnext', 'mod_contentdesigner'), + self::OUTRO_BUTTON_BACKTOCOURSE => get_string('outro:btnbacktocourse', 'mod_contentdesigner'), + self::OUTRO_BUTTON_BACKTOSECTION => get_string('outro:btnbacktosection', 'mod_contentdesigner'), + ]; + $mform->addElement('select', 'primarybutton', get_string('primarybutton', 'mod_contentdesigner'), $options); + $mform->setDefault('primarybutton', self::OUTRO_BUTTON_DISABLED); + $mform->addHelpButton('primarybutton', 'primarybutton', 'mod_contentdesigner'); + + $mform->addElement('text', 'primarytext', + get_string('primarybuttontext', 'mod_contentdesigner'), 'maxlength="100" size="30"'); + $mform->setType('primarytext', PARAM_NOTAGS); + $mform->addHelpButton('primarytext', 'primarybuttontext', 'mod_contentdesigner'); + $mform->hideIf('primarytext', 'primarybutton', 'neq', self::OUTRO_BUTTON_CUSTOM); + + $mform->addElement('text', 'primaryurl', get_string('primarybuttonurl', 'mod_contentdesigner'), ['size' => "60"]); + $mform->setType('primaryurl', PARAM_URL); + $mform->addHelpButton('primaryurl', 'primarybuttonurl', 'mod_contentdesigner'); + $mform->hideIf('primaryurl', 'primarybutton', 'neq', self::OUTRO_BUTTON_CUSTOM); + + // Secondray Button. + $mform->addElement('select', 'secondarybutton', get_string('secondarybutton', 'mod_contentdesigner'), $options); + $mform->setDefault('secondarybutton', self::OUTRO_BUTTON_DISABLED); + $mform->addHelpButton('secondarybutton', 'secondarybutton', 'mod_contentdesigner'); + + $mform->addElement('text', 'secondarytext', + get_string('secondarybuttontext', 'mod_contentdesigner'), 'maxlength="100" size="30"'); + $mform->setType('secondarytext', PARAM_NOTAGS); + $mform->addHelpButton('secondarytext', 'secondarybuttontext', 'mod_contentdesigner'); + $mform->hideIf('secondarytext', 'secondarybutton', 'neq', self::OUTRO_BUTTON_CUSTOM); + + $mform->addElement('text', 'secondaryurl', get_string('secondarybuttonurl', 'mod_contentdesigner'), ['size' => "60"]); + $mform->setType('secondaryurl', PARAM_URL); + $mform->addHelpButton('secondaryurl', 'secondarybuttonurl', 'mod_contentdesigner'); + $mform->hideIf('secondaryurl', 'secondarybutton', 'neq', self::OUTRO_BUTTON_CUSTOM); + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $data + * @return void + */ + public function render($data) { + global $PAGE, $OUTPUT; + + if (!isset($data->id)) { + return ''; + } + $file = editor::get_editor($data->cmid)->get_element_areafiles('element_outro_outroimage', $data->id); + + // Outro Content. + $context = $this->get_context(); + $outrocontent = file_rewrite_pluginfile_urls( + $data->outrocontent, 'pluginfile.php', $context->id, 'mod_contentdesigner', 'element_outro_outrocontent', $data->instance); + $outrocontent = format_text($outrocontent, $data->outrocontentformat, ['context' => $context->id]); + + $html = html_writer::start_div('element-outro'); + $html .= html_writer::div('', 'complete-module', ['id' => 'outro-reached']); + $html .= ($file) ? html_writer::img($file, 'completed', ['class' => 'completion-img img-fluid']) : ''; // Outro image. + + $html .= html_writer::div(html_writer::div($outrocontent, 'outro-content'), 'outro-content-block'); // Outro content block. + + $html .= html_writer::start_div('element-button'); // Outro buttons. + if (!empty($data->primarybutton)) { + list($primarybtntext, $primarybtnurl) = $this->get_button_data($data->primarybutton, 'primary', $data); + $html .= html_writer::link($primarybtnurl, $primarybtntext, ['class' => 'btn btn-primary']); // Primary button. + } + if (!empty($data->secondarybutton)) { + list($secondarybtntext, $secondarybtnurl) = $this->get_button_data($data->secondarybutton, 'secondary', $data); + $html .= html_writer::link($secondarybtnurl, $secondarybtntext, ['class' => 'btn btn-secondary']); // secondary button. + } + $html .= html_writer::end_div(); + $html .= html_writer::end_div(); + return $html; + } + + /** + * Save the area files data after the element instance moodle_form submittted. + * + * @param stdclas $data Submitted moodle_form data. + */ + public function save_areafiles($data) { + global $DB; + parent::save_areafiles($data); + file_save_draft_area_files( + $data->image, $data->contextid, 'mod_contentdesigner', 'element_outro_outroimage', $data->instance + ); + + if (isset($data->contextid)) { + $context = \context::instance_by_id($data->contextid, MUST_EXIST); + } + $editoroptions = $this->editor_options($context); + if (isset($data->instance)) { + $itemid = $data->outrocontent_editor['itemid']; + $data->contentformat = $data->outrocontent_editor['format']; + $data = file_postupdate_standard_editor( + $data, + 'outrocontent', + $editoroptions, + $context, + 'mod_contentdesigner', + 'element_outro_outrocontent', + $data->instance + ); + $updatedata = (object) ['id' => $data->instance, 'outrocontentformat' => $data->outrocontentformat, + 'outrocontent' => $data->outrocontent]; + $DB->update_record('element_outro', $updatedata); + } + } + + /** + * Prepare the form editor elements file data before render the elemnent form. + * + * @param stdclass $formdata + * @return stdclass + */ + public function prepare_standard_file_editor(&$formdata) { + // Always call parent. + $formdata = parent::prepare_standard_file_editor($formdata); + if (isset($formdata->instance)) { + $draftitemid = file_get_submitted_draft_itemid('image'); + file_prepare_draft_area($draftitemid, $this->context->id, 'mod_contentdesigner', 'element_outro_outroimage', + $formdata->instance, array('subdirs' => 0, 'maxfiles' => 1)); + $formdata->image = $draftitemid; + } + + $context = \context_module::instance($this->cmid); + $editoroptions = $this->editor_options($context); + + if (!isset($formdata->id)) { + $formdata->id = null; + $formdata->outrocontentformat = FORMAT_HTML; + $formdata->outrocontent = ''; + } + file_prepare_standard_editor( + $formdata, + 'outrocontent', + $editoroptions, + $context, + 'mod_contentdesigner', + 'element_outro_outrocontent', + $formdata->id + ); + + return $formdata; + } + + /** + * Process the update of element instance and genreal options. + * + * @param stdclass $data Submitted element moodle form data + * @return void + */ + public function update_instance($data) { + global $DB; + $formdata = clone $data; + $formdata->outrocontentformat = $formdata->outrocontent_editor['format']; + $formdata->outrocontent = $formdata->outrocontent_editor['text']; + if ($formdata->instanceid == false) { + $formdata->timemodified = time(); + $formdata->timecreated = time(); + return $DB->insert_record($this->tablename, $formdata); + } else { + $formdata->timecreated = time(); + $formdata->id = $formdata->instanceid; + if ($DB->update_record($this->tablename, $formdata)) { + return $formdata->id; + } + } + } + + /** + * Options used in the editor defined. + * + * @param context_module $context + * @return array Filemanager options. + */ + public function editor_options($context) { + global $CFG; + return [ + 'subdirs' => 1, + 'maxbytes' => $CFG->maxbytes, + 'accepted_types' => '*', + 'context' => $context, + 'maxfiles' => EDITOR_UNLIMITED_FILES + ]; + } + + /** + * Get the pre defined outro buttons data. + * + * @param int $buttontype Button. + * @param int $type Type of the button + * @param int $data instance data. + * + * @return array + */ + public function get_button_data($button, $type, $data) { + global $PAGE, $DB, $CFG; + + $buttontext = ''; + $buttonurl = ''; + $context = $this->get_context(); + + switch ($button) { + case self::OUTRO_BUTTON_NEXT: + $render = $PAGE->get_renderer('mod_contentdesigner'); + $buttontext = get_string('outro:btnnext', 'mod_contentdesigner'); + $buttonurl = $render->activity_navigation($this->cm->instance, $context)->nextlink->url ?? "javascript:void(0);"; + break; + case self::OUTRO_BUTTON_CUSTOM: + if ($type == 'primary') { + $buttontext = !empty($data->primarytext) ? $data->primarytext : ''; + $buttonurl = new moodle_url(!empty($data->primaryurl) ? $data->primaryurl : ''); + } else if ($type == 'secondary') { + $buttontext = !empty($data->secondarytext) ? $data->secondarytext : ''; + $buttonurl = new moodle_url(!empty($data->secondaryurl) ? $data->secondaryurl : ''); + } + break; + case self::OUTRO_BUTTON_BACKTOCOURSE: + $buttontext = get_string('outro:btnbacktocourse', 'mod_contentdesigner'); + $buttonurl = new moodle_url('/course/view.php', ['id' => $this->course->id]); + break; + case self::OUTRO_BUTTON_BACKTOSECTION: + $buttontext = get_string('outro:btnbacktosection', 'mod_contentdesigner'); + $section = $DB->get_record('course_sections', ['id' => $this->cm->section]); + if ($section->id) { + if ($CFG->branch >= 404) { + $buttonurl = new moodle_url('/course/section.php', ['id' => $section->id]); + } else { + $buttonurl = new moodle_url('/course/view.php', ['id' => $this->course->id, 'section' => $section->id]); + } + } + break; + } + return [$buttontext, $buttonurl]; + } +} diff --git a/element/outro/db/install.php b/element/outro/db/install.php new file mode 100644 index 0000000..19a02a8 --- /dev/null +++ b/element/outro/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_outro_install() { + $shortname = \element_outro\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/outro/db/install.xml b/element/outro/db/install.xml new file mode 100644 index 0000000..c23b814 --- /dev/null +++ b/element/outro/db/install.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/element/outro/db/upgrade.php b/element/outro/db/upgrade.php new file mode 100644 index 0000000..090419e --- /dev/null +++ b/element/outro/db/upgrade.php @@ -0,0 +1,68 @@ +. + +/** + * Upgrade script for the outro element. + * + * @package element_outro + * @copyright 2024 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Upgrede outro element. + * + * @param string $oldversion the version we are upgrading from. + */ +function xmldb_element_outro_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + if ($oldversion < 2024110800) { + + // Element outro table. + $table = new xmldb_table('element_outro'); + + // Outrocontent. + $outrocontent = new xmldb_field('outrocontent', XMLDB_TYPE_TEXT, null, null, null, null, null, 'secondaryurl'); + // Outrocontent format. + $outrocontentformat = new xmldb_field('outrocontentformat', XMLDB_TYPE_INTEGER, '2', null, null, null, null, 'outrocontent'); + // Primary button. + $primarybutton = new xmldb_field('primarybutton', XMLDB_TYPE_INTEGER, '9', null, null, null, '0', 'outrocontentformat'); + // Secondary button. + $secondarybutton = new xmldb_field('secondarybutton', XMLDB_TYPE_INTEGER, '9', null, null, null, '0', 'primarybutton'); + + if (!$dbman->field_exists($table, $outrocontent)) { + $dbman->add_field($table, $outrocontent); + } + + if (!$dbman->field_exists($table, $outrocontentformat)) { + $dbman->add_field($table, $outrocontentformat); + } + + if (!$dbman->field_exists($table, $primarybutton)) { + $dbman->add_field($table, $primarybutton); + } + + if (!$dbman->field_exists($table, $secondarybutton)) { + $dbman->add_field($table, $secondarybutton); + } + + upgrade_plugin_savepoint(true, 2024110800, 'element', 'outro'); + } + return true; +} diff --git a/element/outro/lang/en/element_outro.php b/element/outro/lang/en/element_outro.php new file mode 100644 index 0000000..6a831d5 --- /dev/null +++ b/element/outro/lang/en/element_outro.php @@ -0,0 +1,27 @@ +. + +/** + * Element plugin "Outro" - string file. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Outro"; diff --git a/element/outro/tests/behat/assets/c1.jpg b/element/outro/tests/behat/assets/c1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..73f40f1d96649896076cc66119faffbb604b452c GIT binary patch literal 28539 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]s>?~7cXGQU@ovMwOrq7(; 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 literal 0 HcmV?d00001 diff --git a/element/outro/tests/behat/behat_element_outro.php b/element/outro/tests/behat/behat_element_outro.php new file mode 100644 index 0000000..265c53e --- /dev/null +++ b/element/outro/tests/behat/behat_element_outro.php @@ -0,0 +1,54 @@ +. + +/** + * Behat Designer course format steps definitions. + * + * @package element_outro + * @category test + * @copyright 2020 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php'); + + +/** + * Designer course format steps definitions. + * + * @package element_outro + * @category test + * @copyright 2021 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_element_outro extends behat_base { + /** + * Go to editing section layout for specified section number and layout type. + * You need to be in the course page and on editing mode. + * + * @Given /^I check outro image$/ + */ + public function i_check_outro_image() { + $this->execute('behat_general::the_image_at_should_be_identical_to', + [ + "//div[contains(@class, 'element-outro')]//img[contains(@src, 'pluginfile.php') + and contains(@src, '/mod_contentdesigner/element_outro_outroimage/')]", + "xpath_element", + "mod/contentdesigner/element/outro/tests/behat/assets/c1.jpg" + ]); + } + +} diff --git a/element/outro/tests/behat/outro_visibility.feature b/element/outro/tests/behat/outro_visibility.feature new file mode 100644 index 0000000..798a87b --- /dev/null +++ b/element/outro/tests/behat/outro_visibility.feature @@ -0,0 +1,42 @@ +@mod @mod_contentdesigner @element_outro @_file_upload @javascript +Feature: Check content designer outro element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a outro element + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + Then ".course-content-list .item-outro" "css_element" should exist + And I click on ".course-content-list .item-outro .action-item[data-action=edit]" "css_element" + Then I should see "Outro element settings" + And I upload "mod/contentdesigner/element/outro/tests/behat/assets/c1.jpg" file to "Image" filemanager + And I set the following fields to these values: + | primary button text | Button 01 | + | primary button URL | https://www.example.com | + | Secondary button text| Button 02 | + | Secondary button URL | https://www.example.com | + | Title | Demo Outro | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should see "Button 01" + Then I should see "Button 02" + Then I check outro image diff --git a/element/outro/version.php b/element/outro/version.php new file mode 100644 index 0000000..d045fc7 --- /dev/null +++ b/element/outro/version.php @@ -0,0 +1,29 @@ +. + +/** + * element plugin "Outro" - Version file. + * + * @package element_outro + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_outro'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/element/paragraph/backup/moodle2/backup_element_paragraph_subplugin.class.php b/element/paragraph/backup/moodle2/backup_element_paragraph_subplugin.class.php new file mode 100644 index 0000000..6675ae5 --- /dev/null +++ b/element/paragraph/backup/moodle2/backup_element_paragraph_subplugin.class.php @@ -0,0 +1,56 @@ +. + +/** + * This file contains the backup code for the element_paragraph plugin. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Provides the information to backup paragraph contents. + * + * This just adds its filearea to the annotations and records the number of files. + */ +class backup_element_paragraph_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to paragraph element + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_paragraph', array('id'), array( + 'contentdesignerid', 'title', 'visible', 'content', 'horizontal', 'vertical', 'timecreated', 'timemodified', + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_paragraph', array('contentdesignerid' => backup::VAR_PARENTID)); + $subpluginelement->annotate_ids('paragraph_instanceid', 'id'); + + return $subplugin; + } + +} diff --git a/element/paragraph/backup/moodle2/restore_element_paragraph_subplugin.class.php b/element/paragraph/backup/moodle2/restore_element_paragraph_subplugin.class.php new file mode 100644 index 0000000..da7ebae --- /dev/null +++ b/element/paragraph/backup/moodle2/restore_element_paragraph_subplugin.class.php @@ -0,0 +1,67 @@ +. + +/** + * This file contains the restore code for the element_paragraph plugin. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore element_paragraph subplugin. + */ +class restore_element_paragraph_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_paragraph'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes one paragraph element instance + * @param array $data + */ + public function process_element_paragraph_instance($data) { + global $DB; + + $data = (object)$data; + + $oldinstance = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + // The mapping is set in the restore for the paragraph element instance. + $newinstance = $DB->insert_record('element_paragraph', $data); + $this->set_mapping('paragraph_instanceid', $oldinstance, $newinstance, true); + + $this->add_related_files('mod_contentdesigner', 'paragraphelementbg', 'paragraph_instanceid', null, $oldinstance); + + } + +} diff --git a/element/paragraph/classes/element.php b/element/paragraph/classes/element.php new file mode 100644 index 0000000..40e0df9 --- /dev/null +++ b/element/paragraph/classes/element.php @@ -0,0 +1,121 @@ +. + +/** + * Extended class of elements for Paragraph. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace element_paragraph; + +use html_writer; + +/** + * Paragraph element instance extended the base elements. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'paragraph'; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_paragraph'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + return $output->pix_icon('e/styleparagraph', get_string('pluginname', 'element_paragraph')); + } + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + + $mform->addElement('textarea', 'content', get_string('content', 'mod_contentdesigner'), ['rows' => 15, 'cols' => 40]); + $mform->addRule('content', null, 'required'); + $mform->addHelpButton('content', 'content', 'mod_contentdesigner'); + $horizontalalign = [ + 'left' => get_string('strleft', 'mod_contentdesigner'), + 'center' => get_string('strcenter', 'mod_contentdesigner'), + 'right' => get_string('strright', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'horizontal', get_string('horizontalalign', 'mod_contentdesigner'), $horizontalalign); + $mform->addHelpButton('horizontal', 'horizontalalign', 'mod_contentdesigner'); + + $verticalalign = [ + 'top' => get_string('strtop', 'mod_contentdesigner'), + 'middle' => get_string('strmiddle', 'mod_contentdesigner'), + 'bottom' => get_string('strbottom', 'mod_contentdesigner') + ]; + $mform->addElement('select', 'vertical', get_string('verticalalign', 'mod_contentdesigner'), $verticalalign); + $mform->addHelpButton('vertical', 'verticalalign', 'mod_contentdesigner'); + } + + /** + * Load the classes to parent div. + * + * @param stdclass $instance Instance record + * @param stdclass $option General options + * @return void + */ + public function generate_element_classes(&$instance, $option) { + $instance = $this->load_option_classes($instance, $option); + $hozclass = "hl-". $instance->horizontal; + $vertclass = "vl-". $instance->vertical; + $instance->classes .= ' '.$hozclass. ' '. $vertclass; + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $instance + * @return void + */ + public function render($instance) { + global $DB; + + return html_writer::tag('p', format_string($instance->content), ['class' => "element-paragraph"]); + } + +} diff --git a/element/paragraph/db/install.php b/element/paragraph/db/install.php new file mode 100644 index 0000000..29a18f3 --- /dev/null +++ b/element/paragraph/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_paragraph_install() { + $shortname = \element_paragraph\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/paragraph/db/install.xml b/element/paragraph/db/install.xml new file mode 100644 index 0000000..ff1cbd0 --- /dev/null +++ b/element/paragraph/db/install.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/element/paragraph/lang/en/element_paragraph.php b/element/paragraph/lang/en/element_paragraph.php new file mode 100644 index 0000000..1363a06 --- /dev/null +++ b/element/paragraph/lang/en/element_paragraph.php @@ -0,0 +1,29 @@ +. + +/** + * Element plugin "Paragraph" - string file. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Paragraph"; +$string['elementdescription'] = 'Add text'; +$string['content'] = "Content"; diff --git a/element/paragraph/tests/behat/paragraph_visibility.feature b/element/paragraph/tests/behat/paragraph_visibility.feature new file mode 100644 index 0000000..e7b319d --- /dev/null +++ b/element/paragraph/tests/behat/paragraph_visibility.feature @@ -0,0 +1,50 @@ +@mod @mod_contentdesigner @element_paragraph @javascript +Feature: Check content designer paragraph element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a Paragraph element + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=paragraph]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Content | Lorem Ipsum is simply dummy text of the printing and typesetting industry.| + | Horizontal Alignment | Center | + | Vertical Alignment | Middle | + | Title | Paragraph 01 | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Lorem Ipsum" in the ".chapter-elements-list li.element-item" "css_element" + Then ".chapter-elements-list li.element-item .hl-center.vl-middle" "css_element" should exist + Then ".chapter-elements-list li.element-item p.element-paragraph" "css_element" should exist + And I click on "Content editor" "link" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + And I set the following fields to these values: + | Content | Various versions have evolved over the years.| + | Horizontal Alignment | Left | + | Vertical Alignment | Top | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should not see "Lorem Ipsum" in the ".chapter-elements-list li.element-item" "css_element" + Then I should see "Various versions" in the ".chapter-elements-list li.element-item" "css_element" + Then ".chapter-elements-list li.element-item .hl-left.vl-top" "css_element" should exist diff --git a/element/paragraph/version.php b/element/paragraph/version.php new file mode 100644 index 0000000..406e132 --- /dev/null +++ b/element/paragraph/version.php @@ -0,0 +1,29 @@ +. + +/** + * Element plugin "Paragraph" - Version file. + * + * @package element_paragraph + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_paragraph'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/element/richtext/backup/moodle2/backup_element_richtext_subplugin.class.php b/element/richtext/backup/moodle2/backup_element_richtext_subplugin.class.php new file mode 100644 index 0000000..295c390 --- /dev/null +++ b/element/richtext/backup/moodle2/backup_element_richtext_subplugin.class.php @@ -0,0 +1,59 @@ +. + +/** + * This file contains the backup code for the element_richtext plugin. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Provides the information to backup richtext content and files. + * + * This just adds its filearea to the annotations and records the number of files. + */ +class backup_element_richtext_subplugin extends backup_subplugin { + + /** + * Returns the subplugin information to attach to richtext element. + * @return backup_subplugin_element + */ + protected function define_contentdesigner_subplugin_structure() { + + // Create XML elements. + $subplugin = $this->get_subplugin_element(); + $subpluginwrapper = new backup_nested_element($this->get_recommended_name()); + $subpluginelement = new backup_nested_element('element_richtext', array('id'), array( + 'contentdesignerid', 'title', 'visible', 'content', 'contentformat', 'timecreated', 'timemodified' + )); + + // Connect XML elements into the tree. + $subplugin->add_child($subpluginwrapper); + $subpluginwrapper->add_child($subpluginelement); + + // Set source to populate the data. + $subpluginelement->set_source_table('element_richtext', array('contentdesignerid' => backup::VAR_PARENTID)); + $subpluginelement->annotate_ids('richtext_instanceid', 'id'); + + $subpluginelement->annotate_files('mod_contentdesigner', 'element_richtext_content', null); + + return $subplugin; + } + +} diff --git a/element/richtext/backup/moodle2/restore_element_richtext_subplugin.class.php b/element/richtext/backup/moodle2/restore_element_richtext_subplugin.class.php new file mode 100644 index 0000000..91a8336 --- /dev/null +++ b/element/richtext/backup/moodle2/restore_element_richtext_subplugin.class.php @@ -0,0 +1,73 @@ +. + +/** + * This file contains the restore code for the element_richtext plugin. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Restore subplugin class. + * + * Provides the necessary information needed to restore element_richtext. + */ +class restore_element_richtext_subplugin extends restore_subplugin { + + /** + * Returns the paths to be handled by the subplugin. + * @return array + */ + protected function define_contentdesigner_subplugin_structure() { + + $paths = array(); + + $elename = $this->get_namefor('instance'); + // We used get_recommended_name() so this works. + $elepath = $this->get_pathfor('/element_richtext'); + $paths[] = new restore_path_element($elename, $elepath); + + return $paths; + } + + /** + * Processes one element_richtext element + * @param array $data + */ + public function process_element_richtext_instance($data) { + global $DB; + + $data = (object)$data; + + $oldinstance = $data->id; + $data->contentdesignerid = $this->get_new_parentid('contentdesigner'); + + $newinstance = $DB->insert_record('element_richtext', $data); + $this->set_mapping('richtext_instanceid', $oldinstance, $newinstance, true); + $this->add_related_files('mod_contentdesigner', 'richtextelementbg', 'richtext_instanceid', null, $oldinstance); + } + + /** + * Restore the editor images after the instance executed. + * + * @return void + */ + public function after_execute_contentdesigner() { + $this->add_related_files('mod_contentdesigner', 'element_richtext_content', 'richtext_instanceid'); + } +} diff --git a/element/richtext/classes/element.php b/element/richtext/classes/element.php new file mode 100644 index 0000000..6ef5c34 --- /dev/null +++ b/element/richtext/classes/element.php @@ -0,0 +1,203 @@ +. + +/** + * Extended class of elements for Richtext. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace element_richtext; + +use html_writer; + +/** + * Richtext element instance extend the contentdesigner/elements base. + */ +class element extends \mod_contentdesigner\elements { + + /** + * Shortname of the element. + */ + const SHORTNAME = 'richtext'; + + /** + * Element name which is visbile for the users + * + * @return string + */ + public function element_name() { + return get_string('pluginname', 'element_richtext'); + } + + /** + * Element shortname which is used as identical purpose. + * + * @return string + */ + public function element_shortname() { + return self::SHORTNAME; + } + + /** + * Icon of the element. + * + * @param renderer $output + * @return void + */ + public function icon($output) { + return $output->pix_icon('e/source_code', get_string('pluginname', 'element_paragraph')); + } + + /** + * List of areafiles which is used the mod_contentdesigner as component. + * + * @return array + */ + public function areafiles() { + return ['content']; + } + + + /** + * Element form element definition. + * + * @param moodle_form $mform + * @param genreal_element_form $formobj + * @return void + */ + public function element_form(&$mform, $formobj) { + $editoroptions = $this->editor_options($formobj->_customdata['context']); + $mform->addElement('editor', 'content_editor', get_string('richtext', 'mod_contentdesigner'), null, $editoroptions); + $mform->setType('content_editor', PARAM_RAW); + $mform->addRule('content_editor', null, 'required'); + $mform->addHelpButton('content_editor', 'richtext', 'mod_contentdesigner'); + } + + /** + * Render the view of element instance, Which is displayed in the student view. + * + * @param stdclass $data + * @return void + */ + public function render($data) { + $context = $this->get_context(); + $content = file_rewrite_pluginfile_urls( + $data->content, 'pluginfile.php', $context->id, 'mod_contentdesigner', 'element_richtext_content', $data->instance); + $content = format_text($content, $data->contentformat, ['context' => $context->id]); + return html_writer::div(html_writer::div($content, 'richtext-content'), 'richtet-content-block'); + } + + /** + * Process the update of element instance and genreal options. + * + * @param stdclass $data Submitted element moodle form data + * @return void + */ + public function update_instance($data) { + global $DB; + $formdata = clone $data; + $formdata->contentformat = $formdata->content_editor['format']; + $formdata->content = $formdata->content_editor['text']; + if ($formdata->instanceid == false) { + $formdata->timemodified = time(); + $formdata->timecreated = time(); + return $DB->insert_record($this->tablename, $formdata); + } else { + $formdata->timecreated = time(); + $formdata->id = $formdata->instanceid; + if ($DB->update_record($this->tablename, $formdata)) { + return $formdata->id; + } + } + } + + /** + * Save the area files data after the element instance moodle_form submittted. + * + * @param stdclas $data Submitted moodle_form data. + */ + public function save_areafiles($data) { + global $DB; + parent::save_areafiles($data); + if (isset($data->contextid)) { + $context = \context::instance_by_id($data->contextid, MUST_EXIST); + } + $editoroptions = $this->editor_options($context); + if (isset($data->instance)) { + $itemid = $data->content_editor['itemid']; + $data->contentformat = $data->content_editor['format']; + $data = file_postupdate_standard_editor( + $data, + 'content', + $editoroptions, + $context, + 'mod_contentdesigner', + 'element_richtext_content', + $data->instance + ); + $updatedata = (object) ['id' => $data->instance, 'contentformat' => $data->contentformat, 'content' => $data->content]; + $DB->update_record('element_richtext', $updatedata); + } + } + + /** + * Prepare the form editor elements file data before render the elemnent form. + * + * @param stdclass $data + * @return stdclass + */ + public function prepare_standard_file_editor(&$data) { + $data = parent::prepare_standard_file_editor($data); + $context = \context_module::instance($this->cmid); + $editoroptions = $this->editor_options($context); + + if (!isset($data->id)) { + $data->id = null; + $data->contentformat = FORMAT_HTML; + $data->content = ''; + } + file_prepare_standard_editor( + $data, + 'content', + $editoroptions, + $context, + 'mod_contentdesigner', + 'element_richtext_content', + $data->id + ); + return $data; + } + + /** + * Options used in the editor defined. + * + * @param context_module $context + * @return array Filemanager options. + */ + public function editor_options($context) { + global $CFG; + return [ + 'subdirs' => 1, + 'maxbytes' => $CFG->maxbytes, + 'accepted_types' => '*', + 'context' => $context, + 'maxfiles' => EDITOR_UNLIMITED_FILES + ]; + } +} diff --git a/element/richtext/db/install.php b/element/richtext/db/install.php new file mode 100644 index 0000000..4380b2e --- /dev/null +++ b/element/richtext/db/install.php @@ -0,0 +1,34 @@ +. + +/** + * Installation script that inserts the element in the elements list. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Install script, runs during the plugin installtion. + * + * @return bool + */ +function xmldb_element_richtext_install() { + $shortname = \element_richtext\element::SHORTNAME; + $result = \mod_contentdesigner\elements::insertelement($shortname); + return $result ? true : false; +} diff --git a/element/richtext/db/install.xml b/element/richtext/db/install.xml new file mode 100644 index 0000000..716f444 --- /dev/null +++ b/element/richtext/db/install.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + +
+
+
diff --git a/element/richtext/lang/en/element_richtext.php b/element/richtext/lang/en/element_richtext.php new file mode 100644 index 0000000..7456450 --- /dev/null +++ b/element/richtext/lang/en/element_richtext.php @@ -0,0 +1,28 @@ +. + +/** + * Element plugin "Rich text" - string file. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined("MOODLE_INTERNAL") || die(); + +$string['pluginname'] = "Rich text"; +$string['elementdescription'] = 'Add content using the Moodle editor (atto/tinymce)'; diff --git a/element/richtext/tests/behat/richtext_visibility.feature b/element/richtext/tests/behat/richtext_visibility.feature new file mode 100644 index 0000000..cec5a1b --- /dev/null +++ b/element/richtext/tests/behat/richtext_visibility.feature @@ -0,0 +1,36 @@ +@mod @mod_contentdesigner @element_richtext @javascript +Feature: Check content designer richtext element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add a Richtext element + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=richtext]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Rich Text | Hard to read| + | Title | Richtext 01 | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Hard to read" in the ".chapter-elements-list li .richtext-content" "css_element" + Then ".chapter-elements-list li .richtext-content b" "css_element" should exist diff --git a/element/richtext/version.php b/element/richtext/version.php new file mode 100644 index 0000000..8961a59 --- /dev/null +++ b/element/richtext/version.php @@ -0,0 +1,29 @@ +. + +/** + * element plugin "Rich text" - Version file. + * + * @package element_richtext + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'element_richtext'; +$plugin->version = 2024110800; +$plugin->requires = 2020061500; diff --git a/index.php b/index.php new file mode 100644 index 0000000..3261070 --- /dev/null +++ b/index.php @@ -0,0 +1,99 @@ +. + +/** + * List of all contentdesingers in course. + * + * @package mod_contentdesigner + * @copyright 2024 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once("../../config.php"); + +$id = required_param('id', PARAM_INT); // course id +$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); + +require_course_login($course, true); +$PAGE->set_pagelayout('incourse'); + +// Trigger instances list viewed event. +$event = \mod_contentdesigner\event\course_module_instance_list_viewed::create( + ['context' => context_course::instance($course->id)]); +$event->add_record_snapshot('course', $course); +$event->trigger(); + +$strcontentdesigner = get_string('modulename', 'contentdesigner'); +$strcontentdesigners = get_string('modulenameplural', 'contentdesigner'); +$strname = get_string('name'); +$strintro = get_string('moduleintro'); +$strlastmodified = get_string('lastmodified'); + +$PAGE->set_url('/mod/contentdesigner/index.php', ['id' => $id]); +$PAGE->set_title($course->shortname.': '.$strcontentdesigners); +$PAGE->set_heading($course->fullname); +$PAGE->navbar->add($strcontentdesigners); +echo $OUTPUT->header(); +echo $OUTPUT->heading($strcontentdesigners); +if (!$contentdesigners = get_all_instances_in_course('contentdesigner', $course)) { + notice(get_string('thereareno', 'moodle', $strcontentdesigners), "$CFG->wwwroot/course/view.php?id=$course->id"); + exit; +} + +$usesections = course_format_uses_sections($course->format); + +$table = new html_table(); +$table->attributes['class'] = 'generaltable mod_index'; + +if ($usesections) { + $strsectionname = get_string('sectionname', 'format_'.$course->format); + $table->head = [$strsectionname, $strname, $strintro]; + $table->align = ['center', 'left', 'left']; +} else { + $table->head = [$strlastmodified, $strname, $strintro]; + $table->align = ['left', 'left', 'left']; +} + +$modinfo = get_fast_modinfo($course); +$currentsection = ''; +foreach ($contentdesigners as $contentdesigner) { + $cm = $modinfo->cms[$contentdesigner->coursemodule]; + if ($usesections) { + $printsection = ''; + if ($contentdesigner->section !== $currentsection) { + if ($contentdesigner->section) { + $printsection = get_section_name($course, $contentdesigner->section); + } + if ($currentsection !== '') { + $table->data[] = 'hr'; + } + $currentsection = $contentdesigner->section; + } + } else { + $printsection = ''.userdate($contentdesigner->timemodified).""; + } + + $class = $contentdesigner->visible ? '' : 'class="dimmed"'; // hidden modules are dimmed + + $table->data[] = [ + $printsection, + "id\">".format_string($contentdesigner->name)."", + format_module_intro('contentdesigner', $contentdesigner, $cm->id), + ]; +} + +echo html_writer::table($table); +echo $OUTPUT->footer(); diff --git a/lang/en/contentdesigner.php b/lang/en/contentdesigner.php new file mode 100644 index 0000000..e9230bc --- /dev/null +++ b/lang/en/contentdesigner.php @@ -0,0 +1,202 @@ +. + +/** + * Strings for component 'Content designer', language 'en', branch 'MOODLE_20_STABLE' + * + * @package mod_contentdesigner + * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Content Designer'; +$string['modulename'] = 'Content Designer'; +$string['modulenameplural'] = "Content Designers"; +$string['contentdesigner:view'] = 'View content designer activity'; +$string['contentdesigner:addinstance'] = 'Add a new content designer'; +$string['pluginadministration'] = 'Content designer administration'; + +$string['generaltitle'] = "General settings"; +$string['backgroundtitle'] = "Background"; +$string['animationtitle'] = 'Entrance Animation'; +$string['scrollingeffectstitle'] = 'Scrolling Effects'; +$string['responsivetitle'] = 'Responsive settings'; +$string['chaptertitle'] = "Chapter Title"; +$string['visibility'] = "Visibility"; +$string['visibility_help'] = "Show / Hide the element"; +$string['createchapter'] = "Create chapter"; +$string['element:creategeneral'] = "Create a general element"; +$string['element:viewgeneral'] = "View the general element"; +$string['creategeneral'] = 'Create general'; +$string['backbehindelement'] = "Background behind element"; +$string['animationheading'] = "Entrance animation"; +$string['scrolleffectsheading'] = "Scrolling effects (horizontal)"; +$string['responsiveheading'] = "Responsive"; +$string['stranimation'] = "Animation"; +$string['stranimation_help'] = "Choose an entrance animation for the element. This animation will play when the element first appears on the screen as the user scrolls into view or interacts with the content.
Select None if you don't want any animation effect for the element"; +$string['slidefromright'] = "Slide in from right"; +$string['slidefromleft'] = "Slide in from left"; +$string['fadein'] = "Fade in"; +$string['strduration'] = "Duration"; +$string['strduration_help'] = 'Choose the speed at which the entrance animation will occur. This setting determines how quickly or slowly the animation will play, adding different levels of emphasis and pacing.'; +$string['strslow'] = "Slow"; +$string['strnormal'] = "Normal"; +$string['strfast'] = "Fast"; +$string['hideondesktop'] = "Hide on desktop"; +$string['hideondesktop_help'] = "Hide this element when viewed on a desktop screen."; +$string['hideontablet'] = "Hide on tablet"; +$string['hideontablet_help'] = "Hide this element when viewed on a tablet screen."; +$string['hideonmobile'] = "Hide on mobile"; +$string['hideonmobile_help'] = "Hide this element when viewed on a mobile screen."; +$string['hidden'] = "Hidden"; +$string['margin'] = "Margin"; +$string['margin_help'] = 'Margin controls the space outside the element, creating distance between it and other elements. Set a single value to apply the same margin on all sides of the element.
Example: 10px (This will apply a 10px margin on all four sides: top, right, bottom, and left).'; +$string['padding'] = "Padding"; +$string['padding_help'] = 'Padding controls the space inside the element, between the content and its borders. Set a single value to apply the same padding on all sides of the element.
Example: 10px (This will apply a 10px padding on all four sides: top, right, bottom, and left).'; +$string['strdelay'] = "Delay"; +$string['strdelay_help'] = 'Set a delay before the entrance animation begins. This setting defines how much time should pass before the animation starts after the element comes into view.
Example: Set 2 (2 seconds delay).'; +$string['toleft'] = "To left"; +$string['toright'] = "To Right"; +$string['strdirection'] = "Direction"; +$string['strdirection_help'] = 'Apply scrolling effects to your element, making it move as the user scrolls through the page. These effects can help create dynamic interactions and draw attention to key content.
None: No scrolling effect applied. The element will remain stationary as the user scrolls.
To Left: The element will move from right to left as the user scrolls down the page.
To Right: The element will move from left to right as the user scrolls down the page.'; +$string['speed'] = "Speed"; +$string['speed_help'] = 'Set the speed of the scrolling effect. This controls how fast or slow the element will move as the user scrolls the page.
Example: Set 1 for a slower scroll'; +$string['viewport'] = "Viewport"; +$string['viewport_help'] = 'Define when the scrolling effect should start relative to the user\'s viewport. This helps control how soon the effect is triggered based on how much of the element is visible on the screen.'; +$string['invaildelement'] = "Invalid content designer element"; +$string['elementtitle'] = "Title"; +$string['elementtitle_help'] = 'Enter the title for the element. This title will only be displayed on the content editor page to help organize and identify the element within the editor. It will not appear on the final content page.'; +$string['gerneralsettings'] = "General settings"; +$string['abovecolorbg'] = "Background color/gradient (above)"; +$string['abovecolorbg_help'] = 'Apply a gradient as the background for the element. You can create a smooth transition between two or more colors. Use either linear or radial gradients and specify the colors and direction for the gradient.
+Gradient Example:linear-gradient(#ff5733, #f1c40f) (This will create a gradient from red to yellow).
+Color Example: #ff5733 (This will set the background color of the element to a shade of red).'; +$string['elementbgimage'] = "Background image"; +$string['elementbgimage_help'] = 'Set a background image for the element. You can upload an image or provide a URL to an image. The image will be displayed behind the content of the element.'; +$string['belowcolorbg'] = "Background color/gradient (below)"; +$string['belowcolorbg_help'] = 'Set the background color or gradient for the element. You can choose a single color or a gradient that smoothly transitions between two or more colors. The background color or gradient will be applied below the content area of the element.
+Gradient Example:linear-gradient(#ff5733, #f1c40f) (This will create a gradient from red to yellow).
+Color Example: #ff5733 (This will set the background color of the element to a shade of red).'; +$string['completiondetail:reachend'] = 'Reach the end of the contents to complete'; +$string['contentdesigner:viewcontenteditor'] = 'Access the content editor options'; + +$string['elementsettings'] = '{$a} element settings'; +$string['contenteditor'] = "Content editor"; +$string['elementcreate'] = "Create element"; +$string['elementupdate'] = "Update element"; +$string['deletechecktype'] = 'Are you sure that you want to delete this {$a->element} element?'; + +// Heading element. +$string['headingtext'] = "Heading text"; +$string['headingtext_help'] = 'Enter the main text for your heading. This will be prominently displayed to guide users through the content or section.'; +$string['headingurl'] = "Heading URL"; +$string['headingurl_help'] = 'Enter a URL if you want to make the heading clickable. When users click on the heading, they will be redirected to the specified link. Leave blank if no link is needed.'; +$string['mainheading'] = "Main heading (h2)"; +$string['subheading'] = "Sub heading (h3)"; +$string['horizontalalign'] = "Horizontal Alignment"; +$string['horizontalalign_help'] = 'Choose the horizontal position for the text within the element:
+Left: Aligns the text to the left side.
+Center: Centers the text within the element.
+Right: Aligns the text to the right side.
+Selecting the alignment helps control the visual layout of your content.'; +$string['verticalalign'] = "Vertical Alignment"; +$string['verticalalign_help'] = 'Select the vertical position of the text within the element:
+Top: Aligns the text to the top of the element.
+Middle: Centers the text vertically within the element.
+Bottom: Aligns the text to the bottom of the element.
+This setting is useful for fine-tuning the text position within your design layout.'; +$string['strleft'] = "Left"; +$string['strcenter'] = "Center"; +$string['strright'] = "Right"; +$string['strtop'] = "Top"; +$string['strmiddle'] = "Middle"; +$string['strbottom'] = "Bottom"; +$string['strblank'] = "Open a new window"; +$string['strself'] = "Open a same window"; +$string['target'] = "Target"; +$string['target_help'] = 'Select how the link should open if a Heading URL is provided:
+Same Window: Opens the link in the current browser tab, replacing the current page.
+New Window: Opens the link in a new browser tab, keeping the current page open. +Choose ‘New Window’ if you want users to keep this page open while viewing the link.'; +$string['strheading'] = "Heading"; +$string['strheading_help'] = 'Choose the type of heading to display. You have two options:
+Main Heading (H2): A larger, prominent heading typically used for main sections or titles.
+Sub Heading (H3): A slightly smaller heading used for subsections or supporting information within a main section. +Selecting the right heading type helps organize content and improves readability."'; +$string['content'] = "Content"; +$string['content_help'] = 'Enter the main text for this element. This content will be displayed as body text, ideal for providing details, descriptions, or supporting information within your content layout.'; +$string['invaildrecord'] = "Invaild record"; +$string['completioncta'] = 'Complete chapter'; +$string['chapterdone'] = 'Done'; +$string['addelement'] = 'Insert Element'; +$string['createnewelement'] = 'Create new element - Content Designer'; + +// Rich text. +$string['richtext'] = "Rich Text"; +$string['richtext_help'] = "Use the rich text editor to add and format content with full styling options, including text formatting, lists, links, and media. This editor supports file uploads, so you can include images, videos, and other media to enhance your content."; + + +// H5p. +$string['mandatory'] = "Mandatory"; +$string['mandatory_help'] = 'Specify whether completing this element is required to unlock the next one:
+Yes: The user must complete this element before the next one is displayed.
+No: The next element is available regardless of whether this one is completed.
+This setting is useful for creating a sequential flow, guiding users through the content step-by-step.'; +$string['completeprevelement'] = 'Complete the element above to continue.'; + +// Outro. +$string['primarybuttontext'] = "primary button text"; +$string['primarybuttontext_help'] = "Enter the text that will appear on the primary button. This button typically represents the main action, such as 'Continue', 'Next', or 'Submit'."; +$string['primarybuttonurl'] = "primary button URL"; +$string['primarybuttonurl_help'] = 'Enter the URL the primary button will link to when clicked. This can be a link to another page, an external website, or another section within the content.'; +$string['secondarybuttontext'] = "Secondary button text"; +$string['secondarybuttontext_help'] = "Enter the text for the secondary button. This button is typically used for alternative actions, such as 'Cancel', 'Back', or 'Skip'. Choose a label that clearly indicates the secondary action."; +$string['secondarybuttonurl'] = "Secondary button URL"; +$string['secondarybuttonurl_help'] = 'Enter the URL the secondary button will link to when clicked. This could direct users to an alternative action or page.'; +$string['strimage'] = "Image"; +$string['strimage_help'] = 'Upload an image to display in the outro section. This image will appear at the end of the content, adding a visual element to enhance the closing message or theme.'; +$string['outro:btncustom'] = 'Custom'; +$string['outro:btnnext'] = 'Next'; +$string['outro:btnbacktocourse'] = 'Back to course'; +$string['outro:btnbacktosection'] = 'Back to section'; +$string['primarybutton'] = 'Primary button'; +$string['secondarybutton'] = 'Secondary button'; +$string['primarybutton_help'] = 'Primary Button
+Disabled: The primary button is hidden by default.
+Custom: Displays a primary button where you can enter custom text and a URL.
+Next: Displays a "Next" button that links to the next activity in the course sequence.
+Back to Course: Displays a button that redirects to the course overview page.
+Back to Section: Displays a button that links back to the current activity\'s section within the course.'; +$string['secondarybutton_help'] = 'Secondary Button
+Disabled: The secondary button is hidden by default.
+Custom: Displays a secondary button where you can enter custom text and a URL.
+Next: Displays a "Next" button that links to the next activity in the course sequence.
+Back to Course: Displays a button that redirects to the course overview page.
+Back to Section: Displays a button that links back to the current activity\'s section within the course.'; + +$string['privacy:metadata:completion:contentdesignerid'] = 'Content designer instance'; +$string['privacy:metadata:completion:userid'] = 'ID of the user'; +$string['privacy:metadata:completion:completion'] = 'Status of the user completion by self'; +$string['privacy:metadata:completion:timecreated'] = 'Time of completion'; +$string['completionby'] = 'Completed BY'; +$string['privacy:completion'] = 'Completions'; +$string['modulename'] = 'Content designer'; +$string['subplugintype_element'] = 'Element plugin'; +$string['subplugintype_element_plural'] = 'Element plugins'; + +// Chapter. +$string['titlestatus'] = 'Display title'; +$string['titlestatus_help'] = 'Enable this option to display the chapter\'s title to learners. When unchecked, the title will only be used for administrative purposes and will not be visible to learners. Checking this option makes the chapter title visible on the learner\'s view.'; diff --git a/lib.php b/lib.php new file mode 100644 index 0000000..d942541 --- /dev/null +++ b/lib.php @@ -0,0 +1,498 @@ +. + +/** + * Define for lib functions. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_contentdesigner\editor; + +/** + * Chapter element shortname. + */ +define('CONTENTDESIGNER_CHAPTER', 'chapter'); + +/** + * Add contentdesigner instance. + * @param stdClass $data + * @param mod_contentdesigner_mod_form $mform + * @return int instance id + */ +function contentdesigner_add_instance($data, $mform = null) { + global $CFG, $DB, $OUTPUT; + contentdesigner_process_pre_save($data); + $moduleid = $DB->insert_record('contentdesigner', $data); + $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null; + \core_completion\api::update_completion_date_event($data->coursemodule, + 'contentdesigner', $moduleid, $completiontimeexpected); + return $moduleid; +} + + +/** + * Runs any processes that must run before a contentdesigner insert/update + * + * @param object $data content form data + * @return void + **/ +function contentdesigner_process_pre_save(&$data) { + // Whether id exist or not. + if (!isset($data->id)) { + $data->timecreated = time(); + } + $data->timemodified = time(); +} + +/** + * Update page instance. + * + * @param stdClass $data + * @param mod_contentdesigner_mod_form $mform + * @return bool true + */ +function contentdesigner_update_instance($data, $mform) { + global $CFG, $DB; + $data->id = $data->instance; + contentdesigner_process_pre_save($data); + $completiontimeexpected = !empty($data->completionexpected) ? $data->completionexpected : null; + \core_completion\api::update_completion_date_event($data->coursemodule, + 'contentdesigner', $data->id, $completiontimeexpected); + $DB->update_record('contentdesigner', $data); + return true; +} + +/** + * Delete page instance. + * @param int $id + * @return bool true + */ +function contentdesigner_delete_instance($id) { + global $CFG, $DB; + + if (!$record = $DB->get_record('contentdesigner', array('id' => $id))) { + return false; + } + $cm = get_coursemodule_from_instance('contentdesigner', $id); + \core_completion\api::update_completion_date_event($cm->id, 'contentdesigner', $id, null); + $DB->delete_records('contentdesigner', array('id' => $record->id)); + return true; +} + +/** + * List of features supported in contentdesigner module + * @param string $feature FEATURE_xx constant for requested feature + * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. + */ +function contentdesigner_supports($feature) { + + // Add for FEATURE_MOD_PURPOSE. + if (defined('FEATURE_MOD_PURPOSE') && $feature === FEATURE_MOD_PURPOSE) { + return MOD_PURPOSE_CONTENT; + } + + switch($feature) { + case FEATURE_GROUPS: + return false; + case FEATURE_GROUPINGS: + return false; + case FEATURE_MOD_INTRO: + return true; + case FEATURE_COMPLETION_TRACKS_VIEWS: + return true; + case FEATURE_COMPLETION_HAS_RULES: + return true; + case FEATURE_GRADE_HAS_GRADE: + return false; + case FEATURE_GRADE_OUTCOMES: + return false; + case FEATURE_BACKUP_MOODLE2: + return true; + case FEATURE_SHOW_DESCRIPTION: + return true; + default: + return null; + } +} + + +/** + * Mark the activity completed (if required) and trigger the course_module_viewed event. + * + * @param stdClass $data data object + * @param stdClass $course course object + * @param stdClass $cm course module object + * @param stdClass $context context object + * @since Moodle 3.0 + */ +function contentdesigner_view($data, $course, $cm, $context) { + + // Trigger course_module_viewed event. + $params = array( + 'context' => $context, + 'objectid' => $data->id + ); + + $event = \mod_contentdesigner\event\course_module_viewed::create($params); + $event->add_record_snapshot('course_modules', $cm); + $event->add_record_snapshot('course', $course); + $event->add_record_snapshot('contentdesigner', $data); + $event->trigger(); + + // Completion. + $completion = new completion_info($course); + $completion->set_module_viewed($cm); +} + + +/** + * This function receives a calendar event and returns the action associated with it, or null if there is none. + * + * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event + * is not displayed on the block. + * + * @param object $event + * @param object $factory + * @param int $userid + * @return object|null + */ +function mod_contentdesigner_core_calendar_provide_event_action($event, $factory, $userid = 0) { + + global $USER; + if (empty($userid)) { + $userid = $USER->id; + } + + $cm = get_fast_modinfo($event->courseid, $userid)->instances['contentdesigner'][$event->instance]; + + $completion = new \completion_info($cm->get_course()); + + $completiondata = $completion->get_data($cm, false, $userid); + + if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { + return null; + } + + return $factory->create_instance( + get_string('view'), + new \moodle_url('/mod/contentdesigner/view.php', ['id' => $cm->id]), + 1, + true + ); +} + +/** + * This function is used by the reset_course_userdata function in moodlelib. + * @param object $data the data submitted from the reset course. + * @return array status array + */ +function contentdesigner_reset_userdata($data) { + // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. + // See MDL-9367. + return array(); +} + +/** + * List the actions that correspond to a view of this module. + * This is used by the participation report. + * + * Note: This is not used by new logging system. Event with + * crud = 'r' and edulevel = LEVEL_PARTICIPATING will + * be considered as view action. + * + * @return array + */ +function contentdesigner_get_view_actions() { + return array('view', 'view all'); +} + +/** + * List the actions that correspond to a post of this module. + * This is used by the participation report. + * + * Note: This is not used by new logging system. Event with + * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING + * will be considered as post action. + * + * @return array + */ +function contentdesigner_get_post_actions() { + return array('update', 'add'); +} + + + +/** + * Get the element plugins. + * + * @return array elements + */ +function contentdesigner_get_element_pluginnames() { + $plugins = core_plugin_manager::instance()->get_plugins_of_type('element'); + return array_keys($plugins); +} + +/** + * Fragment output to load the list of elements to insert. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_get_elements_list($args) { + if ($args['cmid']) { + return \mod_contentdesigner\editor::get_elements_list($args['cmid']); + } + throw new moodle_exception('invalidcoursemodule', 'contentdesigner'); +} + +/** + * Fragment create instance for element in module content and return the results. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_insert_element($args) { + global $OUTPUT; + list ($course, $cm) = get_course_and_cm_from_cmid($args['cmid'], 'contentdesigner'); + $editor = new mod_contentdesigner\editor($cm, $course); + $chapter = $args['chapter'] ?? 0; + return $editor->insert_element($args['elementID'], $chapter); +} + +/** + * Fragment output to load the rendered fill elemenents. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_load_elements($args) { + list ($course, $cm) = get_course_and_cm_from_cmid($args['cmid'], 'contentdesigner'); + $editor = new mod_contentdesigner\editor($cm, $course); + // $editor->initiate_js(); + + return $editor->render_elements(); +} + +/** + * Prepare the next available chapters to users view after the chapter completed. + * + * @param array $args + * @return void + */ +function contentdesigner_output_fragment_load_next_chapters($args) { + list ($course, $cm) = get_course_and_cm_from_cmid($args['cmid'], 'contentdesigner'); + $completedchapter = $args['chapter']; + $editor = new mod_contentdesigner\editor($cm, $course); + if ($editor->chapter->is_chaptercompleted($completedchapter)) { + return $editor->render_elements($completedchapter); + } + return false; +} + +/** + * Fragment output to load the list of elements to insert. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_edit_element($args) { + global $DB; + $elementid = $args['elementid']; + $instanceid = $args['instanceid']; + $cmid = $args['cmid']; + $elementobj = mod_contentdesigner\editor::get_element($elementid, $cmid); + if ($args['action'] == 'delete') { + return ($elementobj->delete_element($instanceid)) ? "" : false; + } +} + +// TODO: Need to implement the capabilities for all fragment tests. +/** + * Fragment output to load the list of elements to insert. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_move_element($args) { + global $OUTPUT; + + if (isset($args['context']) && !empty($args['chapterid'])) { + $editor = editor::get_editor($args['cmid']); + if ($editor->chapter->update_postion($args['chapterid'], $args['contents'])) { + return $editor->display(); + } + } +} + +/** + * Fragment output to load the list of elements to insert. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_move_chapter($args) { + if (isset($args['context']) && !empty($args['cmid'])) { + $editor = editor::get_editor($args['cmid']); + if ($editor->chapter->move_chapter($args['chapters'])) { + return $editor->display(); + } + } +} + +/** + * Fragment output to load the list of elements to insert. + * + * @param array $args Context and cmid. + * @return string + */ +function contentdesigner_output_fragment_update_visibility($args) { + if (isset($args['context']) && !empty($args['cmid'])) { + $elementobj = editor::get_element($args['element'], $args['cmid']); + $elementobj->update_visibility($args['instance'], $args['status']); + } +} + +/** + * Update the edited title of the elements in the editor page to the respected elements instance. + * + * @param string $itemtype Shortname of the element. + * @param int $itemid Id of the element + * @param string $itemvalue Updated value + * @return string Rendered title. + */ +function mod_contentdesigner_inplace_editable($itemtype, $itemid, $itemvalue) { + global $DB, $PAGE, $CFG; + require_once($CFG->libdir . '/externallib.php'); + + if (strpos($itemtype, 'instance_title') !== false) { + $element = str_replace(']', '', explode('[', $itemtype)[1]); + $instanceid = str_replace(']', '', explode('[', $itemtype)[2]); + + if ($DB->get_manager()->table_exists('element_'.$element)) { + $instance = $DB->get_record('element_'.$element, ['id' => $instanceid]); + $cm = get_coursemodule_from_instance('contentdesigner', $instance->contentdesignerid); + } + if (!isset($cm) || empty($cm)) { + throw new moodle_exception('elementtablenotexists', 'contentdesigner'); + } + + $element = mod_contentdesigner\editor::get_element($element, $cm->id); + $record = $element->get_instance($instanceid); + + $PAGE->set_context(context_module::instance($cm->id)); + + \external_api::validate_context(context_system::instance()); + // TODO: Need to check capability and table exists. + $record->title = clean_param($itemvalue, PARAM_NOTAGS); + $record->timemodified = time(); + $DB->update_record($element->tablename, $record); + + return new \core\output\inplace_editable( + 'mod_contentdesigner', $itemtype, $element->elementid.$record->id, true, + format_string($record->title), $record->title, 'Edit instance title', + 'New value for ' . format_string($record->title) + ); + } +} + +/** + * Adds module specific settings to the settings block + * + * @param settings_navigation $settings The settings navigation object + * @param navigation_node $node The node to add module settings to + */ +function contentdesigner_extend_settings_navigation(settings_navigation $settings, navigation_node $node) { + global $PAGE; + if (has_capability('mod/contentdesigner:viewcontenteditor', $PAGE->cm->context)) { + $url = new moodle_url('/mod/contentdesigner/editor.php', array('id' => $PAGE->cm->id, 'sesskey' => sesskey())); + $node->add( + get_string('contenteditor', 'mod_contentdesigner'), $url, navigation_node::TYPE_SETTING, null, 'editorelement', null + ); + } +} + + +/** + * Serves file from image. + * + * @param mixed $course course or id of the course + * @param mixed $cm course module or id of the course module + * @param context $context Context used in the file. + * @param string $filearea Filearea the file stored + * @param array $args Arguments + * @param bool $forcedownload Force download the file instead of display. + * @param array $options additional options affecting the file serving + * @return bool false if file not found, does not return if found - just send the file + */ +function contentdesigner_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) { + require_login(); + if ($context->contextlevel != CONTEXT_MODULE) { + return false; + } + $areas = contentdesigner_get_element_pluginnames(); + $areas = array_map(function($area) { + return $area . "elementbg"; + }, $areas); + + // Merge sub elements area files. + $areas = array_merge($areas, mod_contentdesigner\editor::get_elements_areafiles($context->instanceid)); + + if (!in_array($filearea, $areas)) { + return false; + } + + $fs = get_file_storage(); + $file = $fs->get_file($context->id, 'mod_contentdesigner', $filearea, $args[0], '/', $args[1]); + if (!$file) { + return false; + } + send_stored_file($file, 0, 0, 0, $options); +} + + +/** + * Add a get_coursemodule_info function in case any pulse type wants to add 'extra' information + * for the course (see resource). + * + * Given a course_module object, this function returns any "extra" information that may be needed + * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. + * + * @param stdClass $coursemodule The coursemodule object (record). + * @return cached_cm_info An object on information that the courses + * will know about (most noticeably, an icon). + */ +function contentdesigner_get_coursemodule_info($coursemodule) { + global $DB; + + $dbparams = ['id' => $coursemodule->instance]; + $fields = 'id, name, intro, introformat, timecreated, timemodified'; + if (!$contentdesigner = $DB->get_record('contentdesigner', $dbparams, $fields)) { + return false; + } + + $result = new cached_cm_info(); + $result->name = $contentdesigner->name; + + if ($coursemodule->showdescription) { + // Convert intro to html. Do not filter cached version, filters run at display time. + $result->content = format_module_intro('contentdesigner', $contentdesigner, $coursemodule->id, false); + } + + return $result; +} diff --git a/mod_form.php b/mod_form.php new file mode 100644 index 0000000..a857040 --- /dev/null +++ b/mod_form.php @@ -0,0 +1,62 @@ +. + +/** + * Content designer module instance add and update form. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/course/moodleform_mod.php'); +require_once($CFG->dirroot.'/mod/contentdesigner/lib.php'); + +/** + * Content designer module form. + */ +class mod_contentdesigner_mod_form extends moodleform_mod { + + /** + * Define the mform elements. + * @return void + */ + public function definition() { + global $CFG, $DB, $OUTPUT; + + $mform =& $this->_form; + + $mform->addElement('header', 'general', get_string('general', 'form')); + + $mform->addElement('text', 'name', get_string('name'), array('size' => '48')); + if (!empty($CFG->formatstringstriptags)) { + $mform->setType('name', PARAM_TEXT); + } else { + $mform->setType('name', PARAM_CLEANHTML); + } + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + + $this->standard_intro_elements(); + + $this->standard_coursemodule_elements(); + + $this->add_action_buttons(); + } + +} diff --git a/pix/icon.png b/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90e31e8a1430d53527e76b22798a4470ff3e521d GIT binary patch literal 859 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$SR%Le#_xB>-7 z0j>}@dao%FC>LK6sJ)@C;zEWv=>QgNSAf?EY? z=r41mG45fO{FRR}>B8Yat2hfhB8wRq_>O=u<5X=vX<$U!iYH_Mh zRPpsWw5h^y^06KBjS~G8eW$p!9rQW+H;3=%%b85~Gey$UKA%WUt>^3WcCBct)|m7< zK~Lma;Gw2h?d!VMPutj0d2sp$A8pY!>q4(zU+u^{l{e~`kob(fo?BSk1bCudW}7MB za+|waPwEO!lbY#+PpPuEmn64ujy)o{@$_lFPg~O87GEn$I+*!!Q~ukFj?oU6ZwV@; z1uvIo+CSknUt{ays*fw;EV>%`mi`RXk@5WOY}NN@-iaORfp?M@?W@<~%kXB8mI?Uy zJLc?zo=wkp?2wT-_TUp!r}i#4H=n&b_a2*jYpPi6SGkkzvnTL39$&6BkG1~U^*H9X zBJuQo{l$|OEWP+eJD^*6-`>NsoE$_REN=h4{o~^LR}cO-7{)EA|My#+dHtWSfB%Ml cV+(xDUi;L*=*#}TYoIjY>FVdQ&MBb@0PVE}kN^Mx literal 0 HcmV?d00001 diff --git a/pix/icon.svg b/pix/icon.svg new file mode 100644 index 0000000..52d5953 --- /dev/null +++ b/pix/icon.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/pix/monologo.png b/pix/monologo.png new file mode 100644 index 0000000000000000000000000000000000000000..90e31e8a1430d53527e76b22798a4470ff3e521d GIT binary patch literal 859 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$SR%Le#_xB>-7 z0j>}@dao%FC>LK6sJ)@C;zEWv=>QgNSAf?EY? z=r41mG45fO{FRR}>B8Yat2hfhB8wRq_>O=u<5X=vX<$U!iYH_Mh zRPpsWw5h^y^06KBjS~G8eW$p!9rQW+H;3=%%b85~Gey$UKA%WUt>^3WcCBct)|m7< zK~Lma;Gw2h?d!VMPutj0d2sp$A8pY!>q4(zU+u^{l{e~`kob(fo?BSk1bCudW}7MB za+|waPwEO!lbY#+PpPuEmn64ujy)o{@$_lFPg~O87GEn$I+*!!Q~ukFj?oU6ZwV@; z1uvIo+CSknUt{ays*fw;EV>%`mi`RXk@5WOY}NN@-iaORfp?M@?W@<~%kXB8mI?Uy zJLc?zo=wkp?2wT-_TUp!r}i#4H=n&b_a2*jYpPi6SGkkzvnTL39$&6BkG1~U^*H9X zBJuQo{l$|OEWP+eJD^*6-`>NsoE$_REN=h4{o~^LR}cO-7{)EA|My#+dHtWSfB%Ml cV+(xDUi;L*=*#}TYoIjY>FVdQ&MBb@0PVE}kN^Mx literal 0 HcmV?d00001 diff --git a/pix/monologo.svg b/pix/monologo.svg new file mode 100644 index 0000000..52d5953 --- /dev/null +++ b/pix/monologo.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/settings.php b/settings.php new file mode 100644 index 0000000..3f80fa5 --- /dev/null +++ b/settings.php @@ -0,0 +1,50 @@ +. + +/** + * Contentdesigner module settings. + * + * @package mod_contentdesigner + * @copyright 2024, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/mod/contentdesigner/lib.php'); + +$ADMIN->add('modsettings', new admin_category('modcontentdesigner', new lang_string('pluginname', 'mod_contentdesigner'))); +$settings = new admin_settingpage('contentdesignergeneralsettings', get_string('gerneralsettings', 'mod_contentdesigner'), + 'moodle/site:config', false); + +if ($ADMIN->fulltree) { + + // Chapter title status. + $settings->add(new admin_setting_configcheckbox('mod_contentdesigner/chaptertitlestatus', + get_string('titlestatus', 'mod_contentdesigner'), get_string('titlestatus_help', 'mod_contentdesigner'), 0 + )); + +} + +$ADMIN->add('modcontentdesigner', $settings); + +$settings = null; // Reset the settings. + +foreach (\core_plugin_manager::instance()->get_plugins_of_type('element') as $plugin) { + // Load all the element plugins settings pages + $plugin->load_settings($ADMIN, 'modcontentdesigner', $hassiteconfig); +} + diff --git a/style/animate.css b/style/animate.css new file mode 100644 index 0000000..56f36b1 --- /dev/null +++ b/style/animate.css @@ -0,0 +1,3447 @@ +@charset "UTF-8";/*! + * animate.css - https://animate.style/ + * Version - 4.1.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2020 Animate.css +/* stylelint-disable declaration-no-important */ +@charset "UTF-8"; + +/*! + * animate.css -http://daneden.me/animate + * Version - 3.5.1 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2016 Daniel Eden + */ + +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.animated.infinite { + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; +} + +.animated.hinge { + -webkit-animation-duration: 2s; + animation-duration: 2s; +} + +.animated.flipOutX, +.animated.flipOutY, +.animated.bounceIn, +.animated.bounceOut { + -webkit-animation-duration: .75s; + animation-duration: .75s; +} + +@-webkit-keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} + +@keyframes bounce { + from, + 20%, + 53%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 40%, + 43% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -30px, 0); + transform: translate3d(0, -30px, 0); + } + + 70% { + -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); + -webkit-transform: translate3d(0, -15px, 0); + transform: translate3d(0, -15px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -4px, 0); + transform: translate3d(0, -4px, 0); + } +} + +.bounce { + -webkit-animation-name: bounce; + animation-name: bounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; +} + +@-webkit-keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} + +@keyframes flash { + from, + 50%, + to { + opacity: 1; + } + + 25%, + 75% { + opacity: 0; + } +} + +.flash { + -webkit-animation-name: flash; + animation-name: flash; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes pulse { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 50% { + -webkit-transform: scale3d(1.05, 1.05, 1.05); + transform: scale3d(1.05, 1.05, 1.05); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.pulse { + -webkit-animation-name: pulse; + animation-name: pulse; +} + +@-webkit-keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(.95, 1.05, 1); + transform: scale3d(.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, .95, 1); + transform: scale3d(1.05, .95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes rubberBand { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 30% { + -webkit-transform: scale3d(1.25, 0.75, 1); + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + -webkit-transform: scale3d(0.75, 1.25, 1); + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + -webkit-transform: scale3d(1.15, 0.85, 1); + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + -webkit-transform: scale3d(.95, 1.05, 1); + transform: scale3d(.95, 1.05, 1); + } + + 75% { + -webkit-transform: scale3d(1.05, .95, 1); + transform: scale3d(1.05, .95, 1); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.rubberBand { + -webkit-animation-name: rubberBand; + animation-name: rubberBand; +} + +@-webkit-keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} + +@keyframes shake { + from, + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + 10%, + 30%, + 50%, + 70%, + 90% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 20%, + 40%, + 60%, + 80% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } +} + +.shake { + -webkit-animation-name: shake; + animation-name: shake; +} + +@-webkit-keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} + +@keyframes headShake { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 6.5% { + -webkit-transform: translateX(-6px) rotateY(-9deg); + transform: translateX(-6px) rotateY(-9deg); + } + + 18.5% { + -webkit-transform: translateX(5px) rotateY(7deg); + transform: translateX(5px) rotateY(7deg); + } + + 31.5% { + -webkit-transform: translateX(-3px) rotateY(-5deg); + transform: translateX(-3px) rotateY(-5deg); + } + + 43.5% { + -webkit-transform: translateX(2px) rotateY(3deg); + transform: translateX(2px) rotateY(3deg); + } + + 50% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} + +.headShake { + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + -webkit-animation-name: headShake; + animation-name: headShake; +} + +@-webkit-keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} + +@keyframes swing { + 20% { + -webkit-transform: rotate3d(0, 0, 1, 15deg); + transform: rotate3d(0, 0, 1, 15deg); + } + + 40% { + -webkit-transform: rotate3d(0, 0, 1, -10deg); + transform: rotate3d(0, 0, 1, -10deg); + } + + 60% { + -webkit-transform: rotate3d(0, 0, 1, 5deg); + transform: rotate3d(0, 0, 1, 5deg); + } + + 80% { + -webkit-transform: rotate3d(0, 0, 1, -5deg); + transform: rotate3d(0, 0, 1, -5deg); + } + + to { + -webkit-transform: rotate3d(0, 0, 1, 0deg); + transform: rotate3d(0, 0, 1, 0deg); + } +} + +.swing { + -webkit-transform-origin: top center; + transform-origin: top center; + -webkit-animation-name: swing; + animation-name: swing; +} + +@-webkit-keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes tada { + from { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + -webkit-transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + transform: scale3d(.9, .9, .9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.tada { + -webkit-animation-name: tada; + animation-name: tada; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes wobble { + from { + -webkit-transform: none; + transform: none; + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +@keyframes wobble { + from { + -webkit-transform: none; + transform: none; + } + + 15% { + -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); + } + + 30% { + -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); + } + + 45% { + -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); + } + + 60% { + -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); + } + + 75% { + -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +.wobble { + -webkit-animation-name: wobble; + animation-name: wobble; +} + +@-webkit-keyframes jello { + from, + 11.1%, + to { + -webkit-transform: none; + transform: none; + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} + +@keyframes jello { + from, + 11.1%, + to { + -webkit-transform: none; + transform: none; + } + + 22.2% { + -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); + transform: skewX(-12.5deg) skewY(-12.5deg); + } + + 33.3% { + -webkit-transform: skewX(6.25deg) skewY(6.25deg); + transform: skewX(6.25deg) skewY(6.25deg); + } + + 44.4% { + -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); + transform: skewX(-3.125deg) skewY(-3.125deg); + } + + 55.5% { + -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); + transform: skewX(1.5625deg) skewY(1.5625deg); + } + + 66.6% { + -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); + transform: skewX(-0.78125deg) skewY(-0.78125deg); + } + + 77.7% { + -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); + transform: skewX(0.390625deg) skewY(0.390625deg); + } + + 88.8% { + -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + transform: skewX(-0.1953125deg) skewY(-0.1953125deg); + } +} + +.jello { + -webkit-animation-name: jello; + animation-name: jello; + -webkit-transform-origin: center; + transform-origin: center; +} + +@-webkit-keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + -webkit-transform: scale3d(.9, .9, .9); + transform: scale3d(.9, .9, .9); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + -webkit-transform: scale3d(.97, .97, .97); + transform: scale3d(.97, .97, .97); + } + + to { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +@keyframes bounceIn { + from, + 20%, + 40%, + 60%, + 80%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + 20% { + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + -webkit-transform: scale3d(.9, .9, .9); + transform: scale3d(.9, .9, .9); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(1.03, 1.03, 1.03); + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + -webkit-transform: scale3d(.97, .97, .97); + transform: scale3d(.97, .97, .97); + } + + to { + opacity: 1; + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + } +} + +.bounceIn { + -webkit-animation-name: bounceIn; + animation-name: bounceIn; +} + +@-webkit-keyframes bounceInDown { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +@keyframes bounceInDown { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(0, -3000px, 0); + transform: translate3d(0, -3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, 25px, 0); + transform: translate3d(0, 25px, 0); + } + + 75% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, 5px, 0); + transform: translate3d(0, 5px, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +.bounceInDown { + -webkit-animation-name: bounceInDown; + animation-name: bounceInDown; +} + +@-webkit-keyframes bounceInLeft { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +@keyframes bounceInLeft { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + 0% { + opacity: 0; + -webkit-transform: translate3d(-3000px, 0, 0); + transform: translate3d(-3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(25px, 0, 0); + transform: translate3d(25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(5px, 0, 0); + transform: translate3d(5px, 0, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +.bounceInLeft { + -webkit-animation-name: bounceInLeft; + animation-name: bounceInLeft; +} + +@-webkit-keyframes bounceInRight { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + from { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +@keyframes bounceInRight { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + from { + opacity: 0; + -webkit-transform: translate3d(3000px, 0, 0); + transform: translate3d(3000px, 0, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(-25px, 0, 0); + transform: translate3d(-25px, 0, 0); + } + + 75% { + -webkit-transform: translate3d(10px, 0, 0); + transform: translate3d(10px, 0, 0); + } + + 90% { + -webkit-transform: translate3d(-5px, 0, 0); + transform: translate3d(-5px, 0, 0); + } + + to { + -webkit-transform: none; + transform: none; + } +} + +.bounceInRight { + -webkit-animation-name: bounceInRight; + animation-name: bounceInRight; +} + +@-webkit-keyframes bounceInUp { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + from { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes bounceInUp { + from, + 60%, + 75%, + 90%, + to { + -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); + } + + from { + opacity: 0; + -webkit-transform: translate3d(0, 3000px, 0); + transform: translate3d(0, 3000px, 0); + } + + 60% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + 75% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 90% { + -webkit-transform: translate3d(0, -5px, 0); + transform: translate3d(0, -5px, 0); + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.bounceInUp { + -webkit-animation-name: bounceInUp; + animation-name: bounceInUp; +} + +@-webkit-keyframes bounceOut { + 20% { + -webkit-transform: scale3d(.9, .9, .9); + transform: scale3d(.9, .9, .9); + } + + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } +} + +@keyframes bounceOut { + 20% { + -webkit-transform: scale3d(.9, .9, .9); + transform: scale3d(.9, .9, .9); + } + + 50%, + 55% { + opacity: 1; + -webkit-transform: scale3d(1.1, 1.1, 1.1); + transform: scale3d(1.1, 1.1, 1.1); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } +} + +.bounceOut { + -webkit-animation-name: bounceOut; + animation-name: bounceOut; +} + +@-webkit-keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +@keyframes bounceOutDown { + 20% { + -webkit-transform: translate3d(0, 10px, 0); + transform: translate3d(0, 10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, -20px, 0); + transform: translate3d(0, -20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +.bounceOutDown { + -webkit-animation-name: bounceOutDown; + animation-name: bounceOutDown; +} + +@-webkit-keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +@keyframes bounceOutLeft { + 20% { + opacity: 1; + -webkit-transform: translate3d(20px, 0, 0); + transform: translate3d(20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +.bounceOutLeft { + -webkit-animation-name: bounceOutLeft; + animation-name: bounceOutLeft; +} + +@-webkit-keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +@keyframes bounceOutRight { + 20% { + opacity: 1; + -webkit-transform: translate3d(-20px, 0, 0); + transform: translate3d(-20px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +.bounceOutRight { + -webkit-animation-name: bounceOutRight; + animation-name: bounceOutRight; +} + +@-webkit-keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +@keyframes bounceOutUp { + 20% { + -webkit-transform: translate3d(0, -10px, 0); + transform: translate3d(0, -10px, 0); + } + + 40%, + 45% { + opacity: 1; + -webkit-transform: translate3d(0, 20px, 0); + transform: translate3d(0, 20px, 0); + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +.bounceOutUp { + -webkit-animation-name: bounceOutUp; + animation-name: bounceOutUp; +} + +@-webkit-keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; +} + +@-webkit-keyframes fadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; +} + +@-webkit-keyframes fadeInDownBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInDownBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInDownBig { + -webkit-animation-name: fadeInDownBig; + animation-name: fadeInDownBig; +} + +@-webkit-keyframes fadeInLeft { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInLeft { + -webkit-animation-name: fadeInLeft; + animation-name: fadeInLeft; +} + +@-webkit-keyframes fadeInLeftBig { + from { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInLeftBig { + from { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInLeftBig { + -webkit-animation-name: fadeInLeftBig; + animation-name: fadeInLeftBig; +} + +@-webkit-keyframes fadeInRight { + from { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInRight { + from { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInRight { + -webkit-animation-name: fadeInRight; + animation-name: fadeInRight; +} + +@-webkit-keyframes fadeInRightBig { + from { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInRightBig { + from { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInRightBig { + -webkit-animation-name: fadeInRightBig; + animation-name: fadeInRightBig; +} + +@-webkit-keyframes fadeInUp { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInUp { + -webkit-animation-name: fadeInUp; + animation-name: fadeInUp; +} + +@-webkit-keyframes fadeInUpBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes fadeInUpBig { + from { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.fadeInUpBig { + -webkit-animation-name: fadeInUpBig; + animation-name: fadeInUpBig; +} + +@-webkit-keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} + +.fadeOut { + -webkit-animation-name: fadeOut; + animation-name: fadeOut; +} + +@-webkit-keyframes fadeOutDown { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes fadeOutDown { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.fadeOutDown { + -webkit-animation-name: fadeOutDown; + animation-name: fadeOutDown; +} + +@-webkit-keyframes fadeOutDownBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +@keyframes fadeOutDownBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, 2000px, 0); + transform: translate3d(0, 2000px, 0); + } +} + +.fadeOutDownBig { + -webkit-animation-name: fadeOutDownBig; + animation-name: fadeOutDownBig; +} + +@-webkit-keyframes fadeOutLeft { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes fadeOutLeft { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.fadeOutLeft { + -webkit-animation-name: fadeOutLeft; + animation-name: fadeOutLeft; +} + +@-webkit-keyframes fadeOutLeftBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +@keyframes fadeOutLeftBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(-2000px, 0, 0); + transform: translate3d(-2000px, 0, 0); + } +} + +.fadeOutLeftBig { + -webkit-animation-name: fadeOutLeftBig; + animation-name: fadeOutLeftBig; +} + +@-webkit-keyframes fadeOutRight { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes fadeOutRight { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.fadeOutRight { + -webkit-animation-name: fadeOutRight; + animation-name: fadeOutRight; +} + +@-webkit-keyframes fadeOutRightBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +@keyframes fadeOutRightBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(2000px, 0, 0); + transform: translate3d(2000px, 0, 0); + } +} + +.fadeOutRightBig { + -webkit-animation-name: fadeOutRightBig; + animation-name: fadeOutRightBig; +} + +@-webkit-keyframes fadeOutUp { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes fadeOutUp { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.fadeOutUp { + -webkit-animation-name: fadeOutUp; + animation-name: fadeOutUp; +} + +@-webkit-keyframes fadeOutUpBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +@keyframes fadeOutUpBig { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(0, -2000px, 0); + transform: translate3d(0, -2000px, 0); + } +} + +.fadeOutUpBig { + -webkit-animation-name: fadeOutUpBig; + animation-name: fadeOutUpBig; +} + +@-webkit-keyframes flip { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 80% { + -webkit-transform: perspective(400px) scale3d(.95, .95, .95); + transform: perspective(400px) scale3d(.95, .95, .95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} + +@keyframes flip { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + transform: perspective(400px) rotate3d(0, 1, 0, -360deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 40% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; + } + + 50% { + -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 80% { + -webkit-transform: perspective(400px) scale3d(.95, .95, .95); + transform: perspective(400px) scale3d(.95, .95, .95); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } +} + +.animated.flip { + -webkit-backface-visibility: visible; + backface-visibility: visible; + -webkit-animation-name: flip; + animation-name: flip; +} + +@-webkit-keyframes flipInX { + from { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +@keyframes flipInX { + from { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +.flipInX { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInX; + animation-name: flipInX; +} + +@-webkit-keyframes flipInY { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +@keyframes flipInY { + from { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + opacity: 0; + } + + 40% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + transform: perspective(400px) rotate3d(0, 1, 0, -20deg); + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; + } + + 60% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + transform: perspective(400px) rotate3d(0, 1, 0, 10deg); + opacity: 1; + } + + 80% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + transform: perspective(400px) rotate3d(0, 1, 0, -5deg); + } + + to { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } +} + +.flipInY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipInY; + animation-name: flipInY; +} + +@-webkit-keyframes flipOutX { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} + +@keyframes flipOutX { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + opacity: 0; + } +} + +.flipOutX { + -webkit-animation-name: flipOutX; + animation-name: flipOutX; + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; +} + +@-webkit-keyframes flipOutY { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} + +@keyframes flipOutY { + from { + -webkit-transform: perspective(400px); + transform: perspective(400px); + } + + 30% { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + transform: perspective(400px) rotate3d(0, 1, 0, -15deg); + opacity: 1; + } + + to { + -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + transform: perspective(400px) rotate3d(0, 1, 0, 90deg); + opacity: 0; + } +} + +.flipOutY { + -webkit-backface-visibility: visible !important; + backface-visibility: visible !important; + -webkit-animation-name: flipOutY; + animation-name: flipOutY; +} + +@-webkit-keyframes lightSpeedIn { + from { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + + to { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes lightSpeedIn { + from { + -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); + transform: translate3d(100%, 0, 0) skewX(-30deg); + opacity: 0; + } + + 60% { + -webkit-transform: skewX(20deg); + transform: skewX(20deg); + opacity: 1; + } + + 80% { + -webkit-transform: skewX(-5deg); + transform: skewX(-5deg); + opacity: 1; + } + + to { + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.lightSpeedIn { + -webkit-animation-name: lightSpeedIn; + animation-name: lightSpeedIn; + -webkit-animation-timing-function: ease-out; + animation-timing-function: ease-out; +} + +@-webkit-keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} + +@keyframes lightSpeedOut { + from { + opacity: 1; + } + + to { + -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); + transform: translate3d(100%, 0, 0) skewX(30deg); + opacity: 0; + } +} + +.lightSpeedOut { + -webkit-animation-name: lightSpeedOut; + animation-name: lightSpeedOut; + -webkit-animation-timing-function: ease-in; + animation-timing-function: ease-in; +} + +@-webkit-keyframes rotateIn { + from { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes rotateIn { + from { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, -200deg); + transform: rotate3d(0, 0, 1, -200deg); + opacity: 0; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.rotateIn { + -webkit-animation-name: rotateIn; + animation-name: rotateIn; +} + +@-webkit-keyframes rotateInDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes rotateInDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.rotateInDownLeft { + -webkit-animation-name: rotateInDownLeft; + animation-name: rotateInDownLeft; +} + +@-webkit-keyframes rotateInDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes rotateInDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.rotateInDownRight { + -webkit-animation-name: rotateInDownRight; + animation-name: rotateInDownRight; +} + +@-webkit-keyframes rotateInUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes rotateInUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.rotateInUpLeft { + -webkit-animation-name: rotateInUpLeft; + animation-name: rotateInUpLeft; +} + +@-webkit-keyframes rotateInUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +@keyframes rotateInUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -90deg); + transform: rotate3d(0, 0, 1, -90deg); + opacity: 0; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: none; + transform: none; + opacity: 1; + } +} + +.rotateInUpRight { + -webkit-animation-name: rotateInUpRight; + animation-name: rotateInUpRight; +} + +@-webkit-keyframes rotateOut { + from { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} + +@keyframes rotateOut { + from { + -webkit-transform-origin: center; + transform-origin: center; + opacity: 1; + } + + to { + -webkit-transform-origin: center; + transform-origin: center; + -webkit-transform: rotate3d(0, 0, 1, 200deg); + transform: rotate3d(0, 0, 1, 200deg); + opacity: 0; + } +} + +.rotateOut { + -webkit-animation-name: rotateOut; + animation-name: rotateOut; +} + +@-webkit-keyframes rotateOutDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} + +@keyframes rotateOutDownLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, 45deg); + transform: rotate3d(0, 0, 1, 45deg); + opacity: 0; + } +} + +.rotateOutDownLeft { + -webkit-animation-name: rotateOutDownLeft; + animation-name: rotateOutDownLeft; +} + +@-webkit-keyframes rotateOutDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +@keyframes rotateOutDownRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +.rotateOutDownRight { + -webkit-animation-name: rotateOutDownRight; + animation-name: rotateOutDownRight; +} + +@-webkit-keyframes rotateOutUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +@keyframes rotateOutUpLeft { + from { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: left bottom; + transform-origin: left bottom; + -webkit-transform: rotate3d(0, 0, 1, -45deg); + transform: rotate3d(0, 0, 1, -45deg); + opacity: 0; + } +} + +.rotateOutUpLeft { + -webkit-animation-name: rotateOutUpLeft; + animation-name: rotateOutUpLeft; +} + +@-webkit-keyframes rotateOutUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} + +@keyframes rotateOutUpRight { + from { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + opacity: 1; + } + + to { + -webkit-transform-origin: right bottom; + transform-origin: right bottom; + -webkit-transform: rotate3d(0, 0, 1, 90deg); + transform: rotate3d(0, 0, 1, 90deg); + opacity: 0; + } +} + +.rotateOutUpRight { + -webkit-animation-name: rotateOutUpRight; + animation-name: rotateOutUpRight; +} + +@-webkit-keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + + to { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} + +@keyframes hinge { + 0% { + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 20%, + 60% { + -webkit-transform: rotate3d(0, 0, 1, 80deg); + transform: rotate3d(0, 0, 1, 80deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + } + + 40%, + 80% { + -webkit-transform: rotate3d(0, 0, 1, 60deg); + transform: rotate3d(0, 0, 1, 60deg); + -webkit-transform-origin: top left; + transform-origin: top left; + -webkit-animation-timing-function: ease-in-out; + animation-timing-function: ease-in-out; + opacity: 1; + } + + to { + -webkit-transform: translate3d(0, 700px, 0); + transform: translate3d(0, 700px, 0); + opacity: 0; + } +} + +.hinge { + -webkit-animation-name: hinge; + animation-name: hinge; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollIn { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +@keyframes rollIn { + from { + opacity: 0; + -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); + } + + to { + opacity: 1; + -webkit-transform: none; + transform: none; + } +} + +.rollIn { + -webkit-animation-name: rollIn; + animation-name: rollIn; +} + +/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ + +@-webkit-keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} + +@keyframes rollOut { + from { + opacity: 1; + } + + to { + opacity: 0; + -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); + } +} + +.rollOut { + -webkit-animation-name: rollOut; + animation-name: rollOut; +} + +@-webkit-keyframes zoomIn { + from { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + 50% { + opacity: 1; + } +} + +@keyframes zoomIn { + from { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + 50% { + opacity: 1; + } +} + +.zoomIn { + -webkit-animation-name: zoomIn; + animation-name: zoomIn; +} + +@-webkit-keyframes zoomInDown { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomInDown { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, -1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomInDown { + -webkit-animation-name: zoomInDown; + animation-name: zoomInDown; +} + +@-webkit-keyframes zoomInLeft { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0); + transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomInLeft { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0); + transform: scale3d(.1, .1, .1) translate3d(-1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomInLeft { + -webkit-animation-name: zoomInLeft; + animation-name: zoomInLeft; +} + +@-webkit-keyframes zoomInRight { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0); + transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomInRight { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0); + transform: scale3d(.1, .1, .1) translate3d(1000px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(-10px, 0, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomInRight { + -webkit-animation-name: zoomInRight; + animation-name: zoomInRight; +} + +@-webkit-keyframes zoomInUp { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomInUp { + from { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, 1000px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + 60% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomInUp { + -webkit-animation-name: zoomInUp; + animation-name: zoomInUp; +} + +@-webkit-keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + to { + opacity: 0; + } +} + +@keyframes zoomOut { + from { + opacity: 1; + } + + 50% { + opacity: 0; + -webkit-transform: scale3d(.3, .3, .3); + transform: scale3d(.3, .3, .3); + } + + to { + opacity: 0; + } +} + +.zoomOut { + -webkit-animation-name: zoomOut; + animation-name: zoomOut; +} + +@-webkit-keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomOutDown { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, -60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, 2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomOutDown { + -webkit-animation-name: zoomOutDown; + animation-name: zoomOutDown; +} + +@-webkit-keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(.1) translate3d(-2000px, 0, 0); + transform: scale(.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} + +@keyframes zoomOutLeft { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(.1) translate3d(-2000px, 0, 0); + transform: scale(.1) translate3d(-2000px, 0, 0); + -webkit-transform-origin: left center; + transform-origin: left center; + } +} + +.zoomOutLeft { + -webkit-animation-name: zoomOutLeft; + animation-name: zoomOutLeft; +} + +@-webkit-keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(.1) translate3d(2000px, 0, 0); + transform: scale(.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} + +@keyframes zoomOutRight { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0); + transform: scale3d(.475, .475, .475) translate3d(-42px, 0, 0); + } + + to { + opacity: 0; + -webkit-transform: scale(.1) translate3d(2000px, 0, 0); + transform: scale(.1) translate3d(2000px, 0, 0); + -webkit-transform-origin: right center; + transform-origin: right center; + } +} + +.zoomOutRight { + -webkit-animation-name: zoomOutRight; + animation-name: zoomOutRight; +} + +@-webkit-keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +@keyframes zoomOutUp { + 40% { + opacity: 1; + -webkit-transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + transform: scale3d(.475, .475, .475) translate3d(0, 60px, 0); + -webkit-animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + animation-timing-function: cubic-bezier(0.550, 0.055, 0.675, 0.190); + } + + to { + opacity: 0; + -webkit-transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0); + transform: scale3d(.1, .1, .1) translate3d(0, -2000px, 0); + -webkit-transform-origin: center bottom; + transform-origin: center bottom; + -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + animation-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); + } +} + +.zoomOutUp { + -webkit-animation-name: zoomOutUp; + animation-name: zoomOutUp; +} + +@-webkit-keyframes slideInDown { + from { + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInDown { + from { + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInDown { + -webkit-animation-name: slideInDown; + animation-name: slideInDown; +} + +@-webkit-keyframes slideInLeft { + from { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInLeft { + from { + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInLeft { + -webkit-animation-name: slideInLeft; + animation-name: slideInLeft; +} + +@-webkit-keyframes slideInRight { + from { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInRight { + from { + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInRight { + -webkit-animation-name: slideInRight; + animation-name: slideInRight; +} + +@-webkit-keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +@keyframes slideInUp { + from { + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + visibility: visible; + } + + to { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.slideInUp { + -webkit-animation-name: slideInUp; + animation-name: slideInUp; +} + +@-webkit-keyframes slideOutDown { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +@keyframes slideOutDown { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); + } +} + +.slideOutDown { + -webkit-animation-name: slideOutDown; + animation-name: slideOutDown; +} + +@-webkit-keyframes slideOutLeft { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +@keyframes slideOutLeft { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } +} + +.slideOutLeft { + -webkit-animation-name: slideOutLeft; + animation-name: slideOutLeft; +} + +@-webkit-keyframes slideOutRight { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +@keyframes slideOutRight { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } +} + +.slideOutRight { + -webkit-animation-name: slideOutRight; + animation-name: slideOutRight; +} + +@-webkit-keyframes slideOutUp { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +@keyframes slideOutUp { + from { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } + + to { + visibility: hidden; + -webkit-transform: translate3d(0, -100%, 0); + transform: translate3d(0, -100%, 0); + } +} + +.slideOutUp { + -webkit-animation-name: slideOutUp; + animation-name: slideOutUp; +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..8a44d46 --- /dev/null +++ b/styles.css @@ -0,0 +1,313 @@ +body#page-mod-contentdesigner-view { + height: auto; + #page { + height: auto; + overflow-y: visible; + } + #region-main { + overflow-x: hidden; + } +} +.path-mod-contentdesigner { + .mform .form-inline .form-control { + &[name="primaryurl"], + &[name="secondaryurl"], + &[name="abovecolorbg"], + &[name="belowcolorbg"] { + max-width: 500px; + width: 100%; + } + } + .modal { + &.modal-dialog { + max-width: 800px; + } + &.modal-content { + .modal-header { + border-bottom: 0; + h5 { + font-weight: normal; + } + } + .modal-body .elements-list { + list-style: none; + padding: 0; + margin: 0; + .element-item { + padding: 5px 0; + display: flex; + align-items: center; + &:first-child { + padding-top: 10px; + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid rgba(102, 102, 102, .2); + } + } + } + .modal-content .modal-body .elements-list .element-item { + i.icon { + margin-right: 10px; + } + .element-name { + width: 85%; + font-weight: bold; + cursor: pointer; + } + .element-description { + width: 100%; + } + } + } + } + &.path-course-view { + .modal-dialog-scrollable .modal-body { + overflow-x: hidden; + .contentdesigner-progress { + position: sticky; + position: -webkit-sticky; + top: -20px; + z-index: 1; + } + } + } +} +.contentdesigner-content { + .contentdesigner-wrapper { + .course-content-list, + .course-content-list li { + padding: 0; + margin: 0; + } + .course-content-list { + padding: 0; + margin: 0; + > li.chapters-list, + > li.element-item { + list-style: none; + } + > li.element-item .element-outro .element-button { + text-align: center; + a { + margin-right: 10px; + } + } + li { + button.btn.complete-chapter { + display: block; + margin-left: auto; + } + &.completed button.btn { + border-color: #28a745; + background-color: #28a745; + } + &.chapters-list { + &:first-child .chapters-content .chapter-elements-list li.element-item:first-child .element-actions li[data-action="moveup"], + &:first-child .chapters-content > .element-item .element-actions li[data-action="moveup"], + &:nth-last-of-type(2) .chapters-content .chapter-elements-list li.element-item:last-child .element-actions li[data-action="movedown"], + &:nth-last-of-type(2) .chapters-content > .element-item .element-actions li[data-action="movedown"], + &:last-child .chapters-content .chapter-element .element-item .element-actions li[data-action="movedown"] { + display: none; + } + } + } + .chapter-elements-list { + padding: 0; + margin: 10px 0 0; + li.element-item { + font-size: 16px; + color: #343a40; + margin-bottom: 20px; + list-style: none; + .element-outro { + text-align: center; + .element-button { + text-align: center; + margin-top: 15px; + a.btn { + margin-right: 10px; + } + } + } + .element-heading { + margin-bottom: 0; + } + a:hover, + a:focus { + text-decoration: none; + } + .hl-left { + text-align: left; + } + .hl-center { + text-align: center; + } + .hl-right { + text-align: right; + } + .vl-top { + vertical-align: top; + } + .vl-middle { + vertical-align: middle; + } + .vl-bottom { + vertical-align: bottom; + } + } + .element-item .general-options { + position: relative; + z-index: 0; + .background-options { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: -1; + .bg-color, + .bg-image, + .bg-overlay { + width: 100%; + height: 100%; + background-size: cover; + background-repeat: no-repeat; + position: absolute; + top: 0; + left: 0; + z-index: 0; + } + } + p { + margin-bottom: 0; + } + a { + margin-bottom: 10px; + } + } + li .element-box { + border: 1px solid #000; + background: none; + } + } + .item-outro { + margin-top: 20px; + } + .chapters-list.no-elements .element-item .element-box { + background-color: #ebebeb; + } + .element-item { + .animation { + opacity: 0; + } + .animated { + opacity: 1; + } + } + } + .contentdesigner-progress { + &.fixed-top { + max-width: 830px; + margin: 0 auto; + top: 57px; + } + #contentdesigner-progressbar { + width: 100%; + display: flex; + .contentdesigner-chapter { + width: 100%; + margin-right: 2px; + label { + width: 100%; + height: 5px; + background-color: #dadada; + } + &.chapter-completed label { + background: #28a745; + } + } + } + } + } + .element-item { + .element-box { + background-color: #ccc; + padding: 15px 10px; + .element-title { + font-weight: bold; + display: inline-block; + } + .element-icon { + display: inline-block; + margin-right: 10px; + } + .element-actions { + float: right; + .actions-list li { + display: inline-block; + margin-right: 5px; + margin-bottom: 0; + &:last-child { + margin: 0; + } + i { + font-size: 15px; + color: #121212; + opacity: .5; + cursor: pointer; + } + } + } + } + } + > .element-item { + list-style: none; + margin-bottom: 20px; + } + .loading-icon { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + background: rgba(255, 255, 255, .9); + z-index: 1; + display: flex; + align-items: center; + i.icon { + font-size: 24px; + margin: 0 auto; + } + } + .contentdesigner-addelement { + text-align: center; + margin: 10px 0; + clear: both; + position: relative; + z-index: 0; + &:before { + content: ''; + width: 97%; + height: 1px; + border: 2px solid #eee; + background-color: #eee; + margin: 0 10px; + position: absolute; + top: 45%; + left: 0; + z-index: -1; + } + span { + color: #fff; + width: 40px; + height: 40px; + font-size: 18px; + line-height: 28px; + border: 6px solid #fff; + border-radius: 50%; + background-color: #eee; + display: inline-block; + cursor: pointer; + } + } +} \ No newline at end of file diff --git a/templates/chapter.mustache b/templates/chapter.mustache new file mode 100644 index 0000000..1f7127a --- /dev/null +++ b/templates/chapter.mustache @@ -0,0 +1,150 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_contentdesigner/chapter + + Template for the chapters and its contents for editor. + + Example context (json): + { + + "instancedata": { + "id": "22", + "contentdesignerid": "1", + "title": "Heading 1", + "visible": "1", + "contents": "38,67,35,240,242,243", + "position": "5", + "timecreated": "1671803153", + "timemodified": "1672664392", + "hideicon": true + }, + "info": { + "elementid": "1", + "name": "Chapter", + "shortname": "chapter", + "icon": "", + "description": "Add a new chapter to group elements" + }, + "contents": [{ + "id": "243", + "contentdesignerid": "1", + "element": "5", + "instance": "62", + "chapter": "22", + "position": "1", + "timecreated": "1672664392", + "timemodified": "1672664392", + "elementid": "5", + "elementname": "paragraph", + "info": { + "elementid": "5", + "name": "Paragraph", + "shortname": "paragraph", + "icon": "", + "description": "Add text" + }, + "instancedata": { + "id": "62", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "contentdesignerid": "1", + "title": "\r\n \r\n \r\n Test - top\r\n \r\n \r\n \r\n \r\n", + "visible": "1", + "content": "Test - top", + "horizontal": "left", + "vertical": "top", + "optionid": "176", + "hideicon": false, + "classes": "animated fadeIn slow hl-left vl-top", + "style": "margin: 0;padding: 0;", + "backimage": "" + }, + "option": { + "id": "176", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "backimage": "" + }, + "editurl": "", + "content": { + "elementcontent": "

Test - top

", + "general": [] + } + }], + "count": 5, + "prevent": false, + "chaptercta": true, + "completion": false + } +}} +
+ {{> mod_contentdesigner/elementbox }} + +
+ +
+
    + {{#contents}} +
  • + {{> mod_contentdesigner/elementbox }} +
  • + {{/contents}} +
+ {{#count}} +
+ +
+ {{/count}} +
+ diff --git a/templates/content.mustache b/templates/content.mustache new file mode 100644 index 0000000..4e7b2de --- /dev/null +++ b/templates/content.mustache @@ -0,0 +1,258 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_contentdesigner/chapter + + Template for the module render view of elements. + + Example context (json): + { + "cm": { + "id": "1377", + "course": "12", + "module": "25", + "instance": "1", + "section": "325", + "name": "Test designer", + "modname": "contentdesigner" + }, + "course": { + "id": "12", + "category": "1", + "fullname": "Demo course", + "shortname": "Demo course", + "enablecompletion": "1" + }, + "progressbar": "", + "chapters": [ + { + "instancedata": { + "id": "22", + "contentdesignerid": "1", + "title": "Heading 1", + "visible": "1", + "contents": "38,67,35,240,242,243", + "position": "5", + "timecreated": "1671803153", + "timemodified": "1672664392", + "hideicon": true + }, + "info": { + "elementid": "1", + "name": "Chapter", + "shortname": "chapter", + "icon": "", + "description": "Add a new chapter to group elements" + }, + "contents": [{ + "id": "243", + "contentdesignerid": "1", + "element": "5", + "instance": "62", + "chapter": "22", + "position": "1", + "timecreated": "1672664392", + "timemodified": "1672664392", + "elementid": "5", + "elementname": "paragraph", + "info": { + "elementid": "5", + "name": "Paragraph", + "shortname": "paragraph", + "icon": "", + "description": "Add text" + }, + "instancedata": { + "id": "62", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "contentdesignerid": "1", + "title": "\r\n \r\n \r\n Test - top\r\n \r\n \r\n \r\n \r\n", + "visible": "1", + "content": "Test - top", + "horizontal": "left", + "vertical": "top", + "optionid": "176", + "hideicon": false, + "classes": "animated fadeIn slow hl-left vl-top", + "style": "margin: 0;padding: 0;", + "backimage": "" + }, + "option": { + "id": "176", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "backimage": "" + }, + "editurl": "", + "content": { + "elementcontent": "

Test - top

", + "general": [] + } + }], + "count": 5, + "prevent": false, + "chaptercta": true, + "completion": false + } + ], + "outro": { + "contents": "", + "instancedata": { + "id": "38", + "contentdesignerid": "1", + "title": "finished content", + "visible": "0", + "image": "565905438", + "primarytext": "Primary ", + "primaryurl": "http://www.example.com/", + "secondarytext": "Secondary", + "secondaryurl": "http://www.example.com/", + "timecreated": "1672374223", + "timemodified": "0" + }, + "element": "4", + "info": { + "elementid": "4", + "name": "Outro", + "shortname": "outro", + "icon": "", + "description": "" + } + }, + "prevent": false + } +}} +
+
+ {{{cmdetails}}} +
+
{{{progressbar}}}
+
    + {{#chapters}} +
  • + {{#instancedata.titlestatus}} +
    +

    {{instancedata.chaptertitle}}

    +
    + {{/instancedata.titlestatus}} +
      + {{#contents}} +
    • +
      + +
      +
      +
      +
      +
      + + + {{{content.elementcontent}}} +
      +
    • + {{/contents}} +
    + {{^prevent}} + {{#chaptercta}} + {{#count}} + {{#completion}} + + {{/completion}} + {{^completion}} + + {{/completion}} + {{/count}} + {{/chaptercta}} + {{/prevent}} +
  • + + {{#prevent}} +
    + {{#pix}} i/caution, core {{/pix}} +
    + {{/prevent}} + {{/chapters}} + + {{^chapterprevent}} + {{^prevent}} +
  • +
    + {{{outro.contents}}} +
    +
  • + {{/prevent}} + {{/chapterprevent}} +
+
+
+
+ diff --git a/templates/editor.mustache b/templates/editor.mustache new file mode 100644 index 0000000..d6127d7 --- /dev/null +++ b/templates/editor.mustache @@ -0,0 +1,173 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_contentdesigner/editor + + Template for the content editor view with list of elements in manage mode. + + Example context (json): + { + "cm": { + "id": "1377", + "course": "12", + "module": "25", + "instance": "1", + "section": "325", + "name": "Test designer", + "modname": "contentdesigner" + }, + "course": { + "id": "12", + "category": "1", + "fullname": "Demo course", + "shortname": "Demo course", + "enablecompletion": "1" + }, + "progressbar": "", + "chapters": [{ + "instancedata": { + "id": "22", + "contentdesignerid": "1", + "title": "Heading 1", + "visible": "1", + "contents": "38,67,35,240,242,243", + "position": "5", + "timecreated": "1671803153", + "timemodified": "1672664392", + "hideicon": true + }, + "info": { + "elementid": "1", + "name": "Chapter", + "shortname": "chapter", + "icon": "", + "description": "Add a new chapter to group elements" + }, + "contents": [{ + "id": "243", + "contentdesignerid": "1", + "element": "5", + "instance": "62", + "chapter": "22", + "position": "1", + "timecreated": "1672664392", + "timemodified": "1672664392", + "elementid": "5", + "elementname": "paragraph", + "info": { + "elementid": "5", + "name": "Paragraph", + "shortname": "paragraph", + "icon": "", + "description": "Add text" + }, + "instancedata": { + "id": "62", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "contentdesignerid": "1", + "title": "\r\n \r\n \r\n Test - top\r\n \r\n \r\n \r\n \r\n", + "visible": "1", + "content": "Test - top", + "horizontal": "left", + "vertical": "top", + "optionid": "176", + "hideicon": false, + "classes": "animated fadeIn slow hl-left vl-top", + "style": "margin: 0;padding: 0;", + "backimage": "" + }, + "option": { + "id": "176", + "element": "5", + "instance": "62", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": null, + "bgimage": "714232411", + "belowcolorbg": "", + "belowgradientbg": null, + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1672664392", + "timemodified": "1672664392", + "backimage": "" + }, + "editurl": "", + "content": { + "elementcontent": "

Test - top

", + "general": [] + } + }], + "count": 5, + "prevent": false, + "chaptercta": true, + "completion": false + }], + "outro": "", + "prevent": false + } + +}} +
+
+
+
    + {{^chapterscount}} +
    + +
    + {{/chapterscount}} + {{#chapters}} +
  • + {{> mod_contentdesigner/chapter}} +
  • + {{/chapters}} +
  • + {{{outro}}} +
  • +
+ +
+
+
diff --git a/templates/elementbox.mustache b/templates/elementbox.mustache new file mode 100644 index 0000000..d907013 --- /dev/null +++ b/templates/elementbox.mustache @@ -0,0 +1,145 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_contentdesigner/chapter + + Template for the elements with edit, delete options for editor page. + + Example context (json): + { + "id": "36", + "contentdesignerid": "1", + "element": "3", + "instance": "34", + "chapter": "21", + "position": "1", + "timecreated": "1671803199", + "timemodified": "1671803199", + "elementid": "3", + "elementname": "heading", + "info": { + "elementid": "3", + "name": "Heading", + "shortname": "heading", + "icon": "", + "description": "Add a heading" + }, + "instancedata": { + "id": "34", + "element": "3", + "instance": "34", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": "", + "bgimage": "221871456", + "belowcolorbg": "", + "belowgradientbg": "", + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1671803199", + "timemodified": "0", + "contentdesignerid": "0", + "title": "\r\n \r\n \r\n second chap heading\r\n \r\n \r\n \r\n \r\n", + "visible": "1", + "heading": "Welcome to Basics", + "headingurl": "https://music.youtube.com/watch?v=fq6egtAzaQM&list=RDAMVMeG5Zx7qq2C4", + "headingtype": "h3", + "target": "_blank", + "horizontal": "left", + "vertical": "top", + "optionid": "51", + "hideicon": false, + "classes": "animated fadeIn slow ", + "style": "margin: 0;padding: 0;", + "backimage": "" + }, + "option": { + "id": "51", + "element": "3", + "instance": "34", + "margin": "0", + "padding": "0", + "abovecolorbg": "", + "abovegradientbg": "", + "bgimage": "221871456", + "belowcolorbg": "", + "belowgradientbg": "", + "animation": "fadeIn", + "duration": "slow", + "delay": "0", + "direction": "left", + "speed": "0", + "viewport": "0", + "hidedesktop": "0", + "hidetablet": "0", + "hidemobile": "0", + "timecreated": "1671803199", + "timemodified": "0", + "backimage": "" + }, + "editurl": "", + "content": "" + } +}} +
+
+
+ {{{info.icon}}} +
+
+ {{{instancedata.title}}} +
+
+
    +
  • + +
  • + {{^hidedelete}} +
  • + {{/hidedelete}} + {{^hidemove}} +
  • +
  • + {{/hidemove}} + {{^hidevisible}} +
  • + {{#instancedata.visible}} + + {{/instancedata.visible}} + {{^instancedata.visible}} + + {{/instancedata.visible}} +
  • + {{/hidevisible}} +
+
+
+
diff --git a/tests/behat/element_appearance.feature b/tests/behat/element_appearance.feature new file mode 100644 index 0000000..ddd3d10 --- /dev/null +++ b/tests/behat/element_appearance.feature @@ -0,0 +1,108 @@ +@mod @mod_contentdesigner @element_appearance @javascript +Feature: Check content designer element options + In order to create content elements of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Add contentdesigner elements. + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + Then I should see "Content editor" + And I click on "Content editor" "link" + Then ".contentdesigner-addelement .fa-plus" "css_element" should exist + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I should see "Insert Element" in the ".modal-header" "css_element" + And I should see "Chapter" in the ".modal-body" "css_element" + And I should see "Heading" in the ".modal-body" "css_element" + And I click on ".elements-list li[data-element=chapter]" "css_element" in the ".modal-body" "css_element" + Then I should see "Chapter element settings" + And I set the following fields to these values: + | Title | First chapter | + And I press "Create element" + Then ".course-content-list .chapters-list" "css_element" should exist + + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + Then I should see "Heading element settings" + And I set the following fields to these values: + | Heading text | Demo Url | + | Heading URL | https://example.com/| + | Title | First heading | + And I press "Create element" + Then I should see "First heading" + Then ".course-content-list .chapters-list .chapter-elements-list .element-item" "css_element" should exist + And I log out + And I am on the "Demo content" "contentdesigner activity" page logged in as student1 + Then ".course-content-list .chapter-elements-list .element-item" "css_element" should exist + Then I should see "Demo Url" + + Scenario: Edit contentdesigner element actions. + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=chapter]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Title | First chapter | + And I press "Create element" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=chapter]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Title | Second chapter | + And I press "Create element" + And I click on ".chapters-list:nth-child(1) .contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 01 | + | Title | Heading 01 | + And I press "Create element" + And I click on ".chapters-list:nth-child(1) .contentdesigner-addelement[data-position=\"bottom\"] .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 02 | + | Title | Heading 02 | + And I press "Create element" + And I click on ".chapters-list:nth-child(2) .contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 03 | + | Title | Heading 03 | + And I press "Create element" + And I click on ".chapters-list:nth-child(2) .contentdesigner-addelement[data-position=\"bottom\"] .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 04 | + | Title | Heading 04 | + And I press "Create element" + And I should see "Heading 01" in the ".course-content-list .chapters-list:nth-child(1) li.element-item:nth-child(1)" "css_element" + And I should see "Heading 02" in the ".course-content-list .chapters-list:nth-child(1) li.element-item:nth-child(2)" "css_element" + And I should see "Heading 03" in the ".course-content-list .chapters-list:nth-child(2) li.element-item:nth-child(1)" "css_element" + And I should see "Heading 04" in the ".course-content-list .chapters-list:nth-child(2) li.element-item:nth-child(2)" "css_element" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + Then I should see "Heading element settings" + And I set the following fields to these values: + | Heading text | Heading one | + | Title | Heading one | + And I click on "Update element" "button" + And I should not see "Heading 01" in the ".chapters-list:nth-child(1) li.element-item:nth-child(1)" "css_element" + And I should see "Heading one" in the ".course-content-list .chapters-list:nth-child(1) li.element-item:nth-child(1)" "css_element" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=delete]" "css_element" + Then I should see "Are you sure that you want to delete this heading element?" in the ".modal-body" "css_element" + Then I click on "Yes" "button" in the ".modal-footer" "css_element" + And I should not see "Heading one" in the ".chapters-list:nth-child(1) li.element-item:nth-child(1)" "css_element" diff --git a/tests/behat/element_generalsettings.feature b/tests/behat/element_generalsettings.feature new file mode 100644 index 0000000..ad57055 --- /dev/null +++ b/tests/behat/element_generalsettings.feature @@ -0,0 +1,62 @@ +@mod @mod_contentdesigner @element_generalsettings @javascript +Feature: Check content designer element settings + In order to content elements settings of multiple responses + As a teacher + I need to add contentdesigner activities to courses + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + When I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + And I add a "Content Designer" to section "1" and I fill the form with: + | Name | Demo content | + | Description | Contentdesigner Description | + And I log out + + Scenario: Check the element general settings. + Given I am on the "Demo content" "contentdesigner activity" page logged in as teacher1 + And I click on "Content editor" "link" + And I click on ".contentdesigner-addelement .fa-plus" "css_element" + And I click on ".elements-list li[data-element=heading]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Heading text | Heading 01 | + | Title | Heading 01 | + And I press "Create element" + And I click on ".contentdesigner-addelement[data-position=\"bottom\"] .fa-plus" "css_element" + And I click on ".elements-list li[data-element=paragraph]" "css_element" in the ".modal-body" "css_element" + And I set the following fields to these values: + | Content | Lorem Ipsum is simply dummy text of the printing and typesetting industry. | + | Title | Paragraph description | + And I press "Create element" + And I click on "Content Designer" "link" + Then I should see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" + And I click on "Content editor" "link" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + And I set the following fields to these values: + | Visibility | Hidden | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should not see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" + And I click on "Content editor" "link" + And I click on ".chapters-list:nth-child(1) li.element-item:nth-child(1) .action-item[data-action=edit]" "css_element" + And I set the following fields to these values: + | Visibility | Visible | + | Margin | 10 | + | Padding | 20 | + | Margin | 10 | + | Animation | Slide in from right| + | Duration | Fast | + | Delay | 1000 | + And I press "Update element" + And I click on "Content Designer" "link" + Then I should see "Heading 01" in the ".chapter-elements-list li.element-item" "css_element" + Then ".chapter-elements-list li.element-item [data-animation=\"slideInRight\"].fast.delay-1000ms" "css_element" should exist diff --git a/tests/generator/lib.php b/tests/generator/lib.php new file mode 100644 index 0000000..4b28af2 --- /dev/null +++ b/tests/generator/lib.php @@ -0,0 +1,51 @@ +. + +/** + * mod_contentdesigner data generator. + * + * @package mod_contentdesigner + * @category test + * @copyright 2013 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * mod_contentdesigner data generator class. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mod_contentdesigner_generator extends testing_module_generator { + + /** + * Create the contentdesigner module instance for testing. + * + * @param stdclass $record + * @param array|null $options + * @return stdclass + */ + public function create_instance($record = null, array $options = null) { + $record = (object)(array)$record; + if (!isset($record->timemodified)) { + $record->timemodified = time(); + } + + return parent::create_instance($record, (array)$options); + } +} diff --git a/tests/generator_test.php b/tests/generator_test.php new file mode 100644 index 0000000..ca48389 --- /dev/null +++ b/tests/generator_test.php @@ -0,0 +1,52 @@ +. + +namespace mod_contentdesigner; + +/** + * Generator tests class. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class generator_test extends \advanced_testcase { + + /** + * Test test_content_designer_create_instance + * @covers ::create_instance + */ + public function test_content_designer_create_instance() { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + + $this->assertFalse($DB->record_exists('contentdesigner', array('course' => $course->id))); + $contentdesigner = $this->getDataGenerator()->create_module('contentdesigner', array('course' => $course->id)); + $this->assertEquals(1, $DB->count_records('contentdesigner', array('course' => $course->id))); + $this->assertTrue($DB->record_exists('contentdesigner', array('course' => $course->id))); + $this->assertTrue($DB->record_exists('contentdesigner', array('id' => $contentdesigner->id))); + + $params = array('course' => $course->id, 'name' => 'One more contentdesigner'); + $contentdesigner = $this->getDataGenerator()->create_module('contentdesigner', $params); + $this->assertEquals(2, $DB->count_records('contentdesigner', array('course' => $course->id))); + $this->assertEquals('One more contentdesigner', $DB->get_field_select('contentdesigner', + 'name', 'id = :id', array('id' => $contentdesigner->id))); + } + +} diff --git a/tests/lib_test.php b/tests/lib_test.php new file mode 100644 index 0000000..22de1fb --- /dev/null +++ b/tests/lib_test.php @@ -0,0 +1,189 @@ +. + +/** + * Unit tests for (some of) mod/book/lib.php. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_contentdesigner; + +defined('MOODLE_INTERNAL') || die(); + +use mod_contentdesigner\editor; +use stdClass; + +global $CFG; + +/** + * Unit tests for (some of) mod/book/lib.php. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class lib_test extends \advanced_testcase { + + /** + * Setup the test. + */ + public function setUp(): void { + global $DB; + $this->resetAfterTest(); + $this->setAdminUser(); + $this->course = $this->getDataGenerator()->create_course(); + $this->user = $this->getDataGenerator()->create_user(); + $this->setUser($this->user); + $this->contentdesigner = $this->getDataGenerator()->create_module('contentdesigner', array('course' => $this->course->id)); + $this->headingelementinfo = $DB->get_record('contentdesigner_elements', array('shortname' => 'heading')); + $this->headingelement = $this->get_element($this->headingelementinfo->id, $this->contentdesigner->cmid); + } + + /** + * Test update_element. + * @covers ::update_element + */ + public function test_element_update_element() { + global $DB; + // Create element. + $this->create_heading_element(); + $this->assertEquals($DB->count_records('element_heading'), 2); + $this->assertEquals($DB->count_records('contentdesigner_options'), 2); + $this->assertEquals($DB->count_records('contentdesigner_content'), 2); + } + + /** + * Create a heading element. + */ + public function create_heading_element() { + global $DB; + $data = new stdClass(); + $data->heading = "Heading 01"; + $data->title = "Heading 01"; + $data->headingtype = "h2"; + $data->course = $this->course->id; + $data->cmid = $this->contentdesigner->cmid; + $data->element = $this->headingelement->element_id(); + $data->instanceid = 0; + $data->contentdesignerid = $this->contentdesigner->id; + $this->headingelement->update_element($data); + $this->assertEquals($DB->count_records('element_heading'), 1); + $data->heading = "Heading 02"; + $data->title = "Heading 02"; + $this->headingelement->update_element($data); + } + + /** + * Test get_instance. + * @covers ::get_instance + */ + public function test_element_get_instance() { + $this->create_heading_element(); + $element = $this->get_heading_element(); + $result = $this->headingelement->get_instance($element->id); + $this->assertEquals('Heading 01', $result->title); + } + + /** + * Test info. + * @covers ::info + */ + public function test_element_info() { + $this->create_heading_element(); + $result = $this->headingelement->info(); + $this->assertEquals($this->headingelementinfo->id, $result->elementid); + $this->assertEquals(get_string('pluginname', 'element_heading'), $result->name); + $this->assertEquals($this->headingelementinfo->shortname, $result->shortname); + } + + /** + * Test get_contentdesigner. + * @covers ::get_contentdesigner + */ + public function test_get_contentdesigner() { + $result = $this->headingelement->get_contentdesigner(); + $this->assertEquals($this->contentdesigner->id, $result->id); + } + + /** + * Test get_cm_from_modinstance. + * @covers ::get_cm_from_modinstance + */ + public function test_get_cm_from_modinstance() { + $result = $this->headingelement->get_cm_from_modinstance($this->contentdesigner->id); + $this->assertEquals($this->contentdesigner->cmid, $result->id); + } + + /** + * Test delete_element. + * @covers ::delete_element + */ + public function test_delete_element() { + global $DB; + $this->create_heading_element(); + $this->assertEquals($DB->count_records('element_heading'), 2); + $element = $this->get_heading_element(); + $instance = $this->headingelement->get_instance($element->id); + $this->headingelement->delete_element($instance->id); + $this->assertEquals($DB->count_records('element_heading'), 1); + $this->assertEquals($DB->count_records('contentdesigner_options'), 1); + $this->assertEquals($DB->count_records('contentdesigner_content'), 1); + } + + /** + * Test update_visibility. + * @covers ::update_visibility + */ + public function test_update_visibility() { + global $DB; + $this->create_heading_element(); + $element = $this->get_heading_element(); + $instance = $this->headingelement->get_instance($element->id); + $this->headingelement->update_visibility($instance->id, 0); + $this->assertEquals(0, $DB->get_field($this->headingelement->tablename(), 'visible', array('id' => $instance->id))); + $this->headingelement->update_visibility($instance->id, 1); + $this->assertEquals(1, $DB->get_field($this->headingelement->tablename(), 'visible', array('id' => $instance->id))); + + } + + /** + * Get the editor. + * @param int $cmid + */ + public function get_editor($cmid) { + return editor::get_editor($cmid); + } + + /** + * Get the element. + * @param int $elementid + * @param int $cmid + */ + public function get_element($elementid, $cmid) { + return editor::get_element($elementid, $cmid); + } + + /** + * Get the heading element. + */ + public function get_heading_element() { + global $DB; + return $DB->get_record('element_heading', array('title' => 'Heading 01')); + } + +} diff --git a/version.php b/version.php new file mode 100644 index 0000000..f464c36 --- /dev/null +++ b/version.php @@ -0,0 +1,29 @@ +. + +/** + * Content designer module version information + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +$plugin->version = 2024110800; // The current module version (Date: YYYYMMDDXX). +$plugin->requires = 2020061500; // Requires this Moodle version. +$plugin->component = 'mod_contentdesigner'; // Full name of the plugin (used for diagnostics). +$plugin->release = 'v1.1'; diff --git a/view.php b/view.php new file mode 100644 index 0000000..c21fd48 --- /dev/null +++ b/view.php @@ -0,0 +1,69 @@ +. + +/** + * Content designer module content view page. + * + * @package mod_contentdesigner + * @copyright 2022 bdecent gmbh + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../../config.php'); +require_once('lib.php'); +require_once(__DIR__.'/lib.php'); + +$id = required_param('id', PARAM_INT); // Course Module ID. + +if (!$cm = get_coursemodule_from_id('contentdesigner', $id)) { + // NOTE this is invalid use of print_error, must be a lang string id. + throw new moodle_exception('invalidcoursemodule'); +} + +$PAGE->set_url('/mod/contentdesigner/view.php', array('id' => $cm->id)); + +if (!$course = $DB->get_record('course', array('id' => $cm->course))) { + // Thorw error if the given moulde coure is exists. + throw new moodle_exception('invalidcourse'); +} +require_course_login($course, false, $cm); +if (!$data = $DB->get_record('contentdesigner', array('id' => $cm->instance))) { + // NOTE As above. + throw new moodle_exception('course module is incorrect'); +} +$context = context_module::instance($cm->id); + +require_capability('mod/contentdesigner:view', $context); +$PAGE->set_title($course->shortname.': '.$data->name); +$PAGE->set_heading($course->fullname); +$PAGE->set_activity_record($data); +$PAGE->add_body_class('limitedwidth'); + +// Add animation of given elements. +$PAGE->requires->css('/mod/contentdesigner/style/animate.css'); + +// Completion and trigger events. +contentdesigner_view($data, $course, $cm, $context); + +echo $OUTPUT->header(); +// Render the page view of the elements. +$editor = new mod_contentdesigner\editor($cm, $course); +$editor->initiate_js(); + +echo $editor->render_elements(); +$PAGE->requires->js_call_amd('mod_contentdesigner/elements', 'animateElements', []); + +echo $OUTPUT->footer(); From 2721cb70d68084c7533844f2e40319272d728eef Mon Sep 17 00:00:00 2001 From: Abinesh LMSACE Date: Mon, 11 Nov 2024 19:07:53 +0530 Subject: [PATCH 2/6] BugFix: content designer editor modal style improved. - CD-264 --- styles.css | 28 +++++++++++++--------------- version.php | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/styles.css b/styles.css index 8a44d46..a6b4fad 100644 --- a/styles.css +++ b/styles.css @@ -19,10 +19,10 @@ body#page-mod-contentdesigner-view { } } .modal { - &.modal-dialog { + .modal-dialog { max-width: 800px; } - &.modal-content { + .modal-content { .modal-header { border-bottom: 0; h5 { @@ -43,19 +43,17 @@ body#page-mod-contentdesigner-view { margin-bottom: 10px; border-bottom: 1px solid rgba(102, 102, 102, .2); } - } - } - .modal-content .modal-body .elements-list .element-item { - i.icon { - margin-right: 10px; - } - .element-name { - width: 85%; - font-weight: bold; - cursor: pointer; - } - .element-description { - width: 100%; + i.icon { + margin-right: 10px; + } + .element-name { + width: 85%; + font-weight: bold; + cursor: pointer; + } + .element-description { + width: 100%; + } } } } diff --git a/version.php b/version.php index f464c36..0fdbd74 100644 --- a/version.php +++ b/version.php @@ -23,7 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024110800; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024110801; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061500; // Requires this Moodle version. $plugin->component = 'mod_contentdesigner'; // Full name of the plugin (used for diagnostics). $plugin->release = 'v1.1'; From 8e5b4205e1ebe65d8b99fe283f6713c66cb458ab Mon Sep 17 00:00:00 2001 From: Abinesh LMSACE Date: Tue, 19 Nov 2024 11:32:17 +0530 Subject: [PATCH 3/6] BugFix: Outro element button improved. - CD-272 --- element/outro/classes/element.php | 34 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/element/outro/classes/element.php b/element/outro/classes/element.php index 0976201..b959b5f 100644 --- a/element/outro/classes/element.php +++ b/element/outro/classes/element.php @@ -108,7 +108,7 @@ public function areafiles() { */ public function element_form(&$mform, $formobj) { - $options = array('maxfiles' => 1, 'accepted_types' => ['image']); + $options = ['maxfiles' => 1, 'accepted_types' => ['image']]; $mform->addElement('filemanager', 'image', get_string('strimage', 'mod_contentdesigner'), null, $options); $mform->addHelpButton('image', 'strimage', 'mod_contentdesigner'); @@ -174,7 +174,8 @@ public function render($data) { // Outro Content. $context = $this->get_context(); $outrocontent = file_rewrite_pluginfile_urls( - $data->outrocontent, 'pluginfile.php', $context->id, 'mod_contentdesigner', 'element_outro_outrocontent', $data->instance); + $data->outrocontent, 'pluginfile.php', $context->id, 'mod_contentdesigner', + 'element_outro_outrocontent', $data->instance); $outrocontent = format_text($outrocontent, $data->outrocontentformat, ['context' => $context->id]); $html = html_writer::start_div('element-outro'); @@ -190,7 +191,7 @@ public function render($data) { } if (!empty($data->secondarybutton)) { list($secondarybtntext, $secondarybtnurl) = $this->get_button_data($data->secondarybutton, 'secondary', $data); - $html .= html_writer::link($secondarybtnurl, $secondarybtntext, ['class' => 'btn btn-secondary']); // secondary button. + $html .= html_writer::link($secondarybtnurl, $secondarybtntext, ['class' => 'btn btn-secondary']); // Secondary button. } $html .= html_writer::end_div(); $html .= html_writer::end_div(); @@ -210,8 +211,8 @@ public function save_areafiles($data) { ); if (isset($data->contextid)) { - $context = \context::instance_by_id($data->contextid, MUST_EXIST); - } + $context = \context::instance_by_id($data->contextid, MUST_EXIST); + } $editoroptions = $this->editor_options($context); if (isset($data->instance)) { $itemid = $data->outrocontent_editor['itemid']; @@ -243,7 +244,7 @@ public function prepare_standard_file_editor(&$formdata) { if (isset($formdata->instance)) { $draftitemid = file_get_submitted_draft_itemid('image'); file_prepare_draft_area($draftitemid, $this->context->id, 'mod_contentdesigner', 'element_outro_outroimage', - $formdata->instance, array('subdirs' => 0, 'maxfiles' => 1)); + $formdata->instance, ['subdirs' => 0, 'maxfiles' => 1]); $formdata->image = $draftitemid; } @@ -255,7 +256,7 @@ public function prepare_standard_file_editor(&$formdata) { $formdata->outrocontentformat = FORMAT_HTML; $formdata->outrocontent = ''; } - file_prepare_standard_editor( + file_prepare_standard_editor( $formdata, 'outrocontent', $editoroptions, @@ -305,16 +306,16 @@ public function editor_options($context) { 'maxbytes' => $CFG->maxbytes, 'accepted_types' => '*', 'context' => $context, - 'maxfiles' => EDITOR_UNLIMITED_FILES + 'maxfiles' => EDITOR_UNLIMITED_FILES, ]; } /** * Get the pre defined outro buttons data. * - * @param int $buttontype Button. - * @param int $type Type of the button - * @param int $data instance data. + * @param int $button Button. + * @param string $type Type of the button + * @param stdclass $data instance data. * * @return array */ @@ -351,7 +352,16 @@ public function get_button_data($button, $type, $data) { if ($CFG->branch >= 404) { $buttonurl = new moodle_url('/course/section.php', ['id' => $section->id]); } else { - $buttonurl = new moodle_url('/course/view.php', ['id' => $this->course->id, 'section' => $section->id]); + $buttonurl = new moodle_url('/course/view.php', ['id' => $this->course->id]); + $sectionno = $section->section; + if ($sectionno != 0) { + $buttonurl->param('section', $sectionno); + } else { + if (empty($CFG->linkcoursesections)) { + return null; + } + $buttonurl->set_anchor('section-'.$sectionno); + } } } break; From 60ba1b4babe57cb344aa0cb190c293639326b534 Mon Sep 17 00:00:00 2001 From: Abinesh LMSACE Date: Wed, 20 Nov 2024 18:57:11 +0530 Subject: [PATCH 4/6] Improve: Content designer privacy provider. - CD-167 --- .../contentdesignerelements_provider.php | 7 + classes/privacy/provider.php | 155 +---------- lang/en/contentdesigner.php | 243 ++++++++---------- styles.css | 90 ++++++- version.php | 2 +- 5 files changed, 213 insertions(+), 284 deletions(-) diff --git a/classes/privacy/contentdesignerelements_provider.php b/classes/privacy/contentdesignerelements_provider.php index 2e538b2..8d234d8 100644 --- a/classes/privacy/contentdesignerelements_provider.php +++ b/classes/privacy/contentdesignerelements_provider.php @@ -28,6 +28,13 @@ use core_privacy\local\request\contextlist; +/** + * Content designer elements privacy provider. + * + * @package mod_contentdesigner + * @copyright 2022, bdecent gmbh bdecent.de + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ interface contentdesignerelements_provider extends \core_privacy\local\request\plugin\subplugin_provider { /** diff --git a/classes/privacy/provider.php b/classes/privacy/provider.php index 056cb03..cf8b2aa 100644 --- a/classes/privacy/provider.php +++ b/classes/privacy/provider.php @@ -27,10 +27,10 @@ use context; use core_privacy\local\metadata\collection; -use \core_privacy\local\request\contextlist; -use \core_privacy\local\request\userlist; -use \core_privacy\local\request\approved_userlist; -use \core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\userlist; +use core_privacy\local\request\approved_userlist; +use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use core_privacy\local\request\writer; @@ -45,23 +45,12 @@ class provider implements \core_privacy\local\request\plugin\provider { /** - * List of used data fields summary meta key. + * Get metadata about this plugin's data usage. * - * @param collection $collection - * @return collection + * @param \core_privacy\local\metadata\collection $collection + * @return \core_privacy\local\metadata\collection */ - public static function get_metadata(collection $collection): collection { - - // Module Completion table fields meta summary. - $completionmetadata = [ - 'contentdesignerid' => 'privacy:metadata:completion:contentdesignerid', - 'userid' => 'privacy:metadata:completion:userid', - 'completion' => 'privacy:metadata:completion:completion', - 'timecreated' => 'privacy:metadata:completion:timecreated' - ]; - $collection->add_database_table('contentdesigner_completion', - $completionmetadata, 'privacy:metadata:contentdesignercompletion'); - + public static function get_metadata(\core_privacy\local\metadata\collection $collection): collection { return $collection; } @@ -71,23 +60,8 @@ public static function get_metadata(collection $collection): collection { * @param int $userid The user to search. * @return contextlist $contextlist The list of contexts used in this plugin. */ - public static function get_contexts_for_userid(int $userid) : contextlist { + public static function get_contexts_for_userid(int $userid): contextlist { $contextlist = new \core_privacy\local\request\contextlist(); - // User completions. - $sql = "SELECT c.id - FROM {context} c - INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel - INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname - INNER JOIN {contentdesigner} p ON p.id = cm.instance - LEFT JOIN {contentdesigner_completion} pc ON pc.contentdesignerid = p.id - WHERE pc.userid = :userid"; - $params = [ - 'modname' => 'contentdesigner', - 'contextlevel' => CONTEXT_MODULE, - 'userid' => $userid - ]; - $contextlist->add_from_sql($sql, $params); - return $contextlist; } @@ -103,20 +77,6 @@ public static function get_users_in_context(userlist $userlist) { return; } - $params = [ - 'instanceid' => $context->instanceid, - 'modulename' => 'contentdesigner', - ]; - - // Discussion authors. - $sql = "SELECT d.userid - FROM {course_modules} cm - JOIN {modules} m ON m.id = cm.module AND m.name = :modulename - JOIN {contentdesigner} f ON f.id = cm.instance - JOIN {contentdesigner_completion} d ON d.contentdesignerid = f.id - WHERE cm.id = :instanceid"; - $userlist->add_from_sql('userid', $sql, $params); - // Handle the 'contentdesigner' subplugin. manager::plugintype_class_callback( 'contentdesignerelements', @@ -132,17 +92,6 @@ public static function get_users_in_context(userlist $userlist) { * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { - global $DB; - - $context = $userlist->get_context(); - $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]); - $contentdesigner = $DB->get_record('contentdesigner', ['id' => $cm->instance]); - - list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); - $params = array_merge(['contentdesignerid' => $contentdesigner->id], $userinparams); - $sql = "contentdesignerid = :contentdesignerid AND userid {$userinsql}"; - $DB->delete_records_select('contentdesigner_completion', $sql, $params); - // Handle the 'contentdesigner' subplugin. manager::plugintype_class_callback( 'contentdesignerelements', @@ -159,18 +108,11 @@ public static function delete_data_for_users(approved_userlist $userlist) { * @param approved_contextlist $contextlist The approved context and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { - global $DB; if (empty($contextlist->count())) { return; } - $userid = $contextlist->get_user()->id; - foreach ($contextlist->get_contexts() as $context) { - $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST); - $DB->delete_records('contentdesigner_completion', ['contentdesignerid' => $instanceid, 'userid' => $userid]); - } - // Handle the 'contentdesigner' subplugin. manager::plugintype_class_callback( 'contentdesignerelements', @@ -186,7 +128,6 @@ public static function delete_data_for_user(approved_contextlist $contextlist) { * @param context $context Context to delete data from. */ public static function delete_data_for_all_users_in_context(\context $context) { - global $DB; if ($context->contextlevel != CONTEXT_MODULE) { return; @@ -196,9 +137,7 @@ public static function delete_data_for_all_users_in_context(\context $context) { if (!$cm) { return; } - $DB->delete_records('contentdesigner_completion', ['contentdesignerid' => $cm->instance]); - - // Handle the 'quizaccess' subplugin. + // Handle the 'contentdesigner' subplugin. manager::plugintype_class_callback( 'contentdesignerelements', contentdesignerelements_provider::class, @@ -210,7 +149,7 @@ public static function delete_data_for_all_users_in_context(\context $context) { /** * Export all user data for the specified user, in the specified contexts, using the supplied exporter instance. * - * @param approved_contextlist $contextlist The approved contexts to export information for. + * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; @@ -222,35 +161,6 @@ public static function export_user_data(approved_contextlist $contextlist) { $user = $contextlist->get_user(); list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); - $sql = "SELECT pc.id AS completionid, cm.id AS cmid, c.id AS contextid, - p.id AS pid, p.course AS pcourse, pc.completion AS completion, pc.timecreated AS timecreated, pc.userid AS userid - FROM {context} c - INNER JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = :contextlevel - INNER JOIN {modules} m ON m.id = cm.module AND m.name = :modname - INNER JOIN {contentdesigner} p ON p.id = cm.instance - INNER JOIN {contentdesigner_completion} pc ON pc.contentdesignerid = p.id AND pc.userid = :userid - WHERE c.id {$contextsql} - ORDER BY cm.id, pc.id ASC"; - - $params = [ - 'modname' => 'contentdesigner', - 'contextlevel' => CONTEXT_MODULE, - 'userid' => $contextlist->get_user()->id, - ]; - $completions = $DB->get_records_sql($sql, $params + $contextparams); - - self::export_contentdesigner_completions( - array_filter( - $completions, - function(stdClass $completion) use ($contextlist) : bool { - return $completion->userid == $contextlist->get_user()->id; - } - ), - $user - ); - - list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED); - $sql = "SELECT cm.id AS cmid, c.id AS contextid, p.id AS pid, p.course AS pcourse FROM {context} c @@ -297,47 +207,6 @@ function(stdClass $completion) use ($contextlist) : bool { } } - /** - * Helper function to export completions. - * - * The array of "completions" is actually the result returned by the SQL in export_user_data. - * It is more of a list of sessions. Which is why it needs to be grouped by context id. - * - * @param array $completions Array of completions to export the logs for. - * @param stdclass $user User record object. - */ - private static function export_contentdesigner_completions(array $completions, $user) { - - $completionsbycontextid = self::group_by_property($completions, 'contextid'); - - foreach ($completionsbycontextid as $contextid => $completion) { - $context = context::instance_by_id($contextid); - $completionsbyid = self::group_by_property($completion, 'completionid'); - foreach ($completionsbyid as $completionid => $completions) { - $completiondata = array_map(function($completion) use ($user) { - return [ - 'completed' => (($completion->completion == 1) ? get_string('yes') : get_string('no')), - 'completedtime' => $completion->timecreated ? transform::datetime($completion->timecreated) : '-', - ]; - - }, $completions); - if (!empty($completiondata)) { - $context = context::instance_by_id($contextid); - // Fetch the generic module data for the questionnaire. - $contextdata = helper::get_context_data($context, $user); - $contextdata = (object)array_merge((array)$contextdata, $completiondata); - writer::with_context($context)->export_data( - [get_string('privacy:completion', 'contentdesigner').' '.$completionid], - $contextdata - ); - } - }; - } - - } - - - /** * Helper function to group an array of stdClasses by a common property. * @@ -348,7 +217,7 @@ private static function export_contentdesigner_completions(array $completions, $ private static function group_by_property(array $classes, string $property): array { return array_reduce( $classes, - function (array $classes, stdClass $class) use ($property) : array { + function (array $classes, stdClass $class) use ($property): array { $classes[$class->{$property}][] = $class; return $classes; }, diff --git a/lang/en/contentdesigner.php b/lang/en/contentdesigner.php index e9230bc..59fd310 100644 --- a/lang/en/contentdesigner.php +++ b/lang/en/contentdesigner.php @@ -18,185 +18,166 @@ * Strings for component 'Content designer', language 'en', branch 'MOODLE_20_STABLE' * * @package mod_contentdesigner - * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} + * @copyright 2024 bdecent gmbh * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['pluginname'] = 'Content Designer'; -$string['modulename'] = 'Content Designer'; -$string['modulenameplural'] = "Content Designers"; -$string['contentdesigner:view'] = 'View content designer activity'; -$string['contentdesigner:addinstance'] = 'Add a new content designer'; -$string['pluginadministration'] = 'Content designer administration'; - -$string['generaltitle'] = "General settings"; -$string['backgroundtitle'] = "Background"; -$string['animationtitle'] = 'Entrance Animation'; -$string['scrollingeffectstitle'] = 'Scrolling Effects'; -$string['responsivetitle'] = 'Responsive settings'; -$string['chaptertitle'] = "Chapter Title"; -$string['visibility'] = "Visibility"; -$string['visibility_help'] = "Show / Hide the element"; -$string['createchapter'] = "Create chapter"; -$string['element:creategeneral'] = "Create a general element"; -$string['element:viewgeneral'] = "View the general element"; -$string['creategeneral'] = 'Create general'; -$string['backbehindelement'] = "Background behind element"; -$string['animationheading'] = "Entrance animation"; -$string['scrolleffectsheading'] = "Scrolling effects (horizontal)"; -$string['responsiveheading'] = "Responsive"; -$string['stranimation'] = "Animation"; -$string['stranimation_help'] = "Choose an entrance animation for the element. This animation will play when the element first appears on the screen as the user scrolls into view or interacts with the content.
Select None if you don't want any animation effect for the element"; -$string['slidefromright'] = "Slide in from right"; -$string['slidefromleft'] = "Slide in from left"; -$string['fadein'] = "Fade in"; -$string['strduration'] = "Duration"; -$string['strduration_help'] = 'Choose the speed at which the entrance animation will occur. This setting determines how quickly or slowly the animation will play, adding different levels of emphasis and pacing.'; -$string['strslow'] = "Slow"; -$string['strnormal'] = "Normal"; -$string['strfast'] = "Fast"; -$string['hideondesktop'] = "Hide on desktop"; -$string['hideondesktop_help'] = "Hide this element when viewed on a desktop screen."; -$string['hideontablet'] = "Hide on tablet"; -$string['hideontablet_help'] = "Hide this element when viewed on a tablet screen."; -$string['hideonmobile'] = "Hide on mobile"; -$string['hideonmobile_help'] = "Hide this element when viewed on a mobile screen."; -$string['hidden'] = "Hidden"; -$string['margin'] = "Margin"; -$string['margin_help'] = 'Margin controls the space outside the element, creating distance between it and other elements. Set a single value to apply the same margin on all sides of the element.
Example: 10px (This will apply a 10px margin on all four sides: top, right, bottom, and left).'; -$string['padding'] = "Padding"; -$string['padding_help'] = 'Padding controls the space inside the element, between the content and its borders. Set a single value to apply the same padding on all sides of the element.
Example: 10px (This will apply a 10px padding on all four sides: top, right, bottom, and left).'; -$string['strdelay'] = "Delay"; -$string['strdelay_help'] = 'Set a delay before the entrance animation begins. This setting defines how much time should pass before the animation starts after the element comes into view.
Example: Set 2 (2 seconds delay).'; -$string['toleft'] = "To left"; -$string['toright'] = "To Right"; -$string['strdirection'] = "Direction"; -$string['strdirection_help'] = 'Apply scrolling effects to your element, making it move as the user scrolls through the page. These effects can help create dynamic interactions and draw attention to key content.
None: No scrolling effect applied. The element will remain stationary as the user scrolls.
To Left: The element will move from right to left as the user scrolls down the page.
To Right: The element will move from left to right as the user scrolls down the page.'; -$string['speed'] = "Speed"; -$string['speed_help'] = 'Set the speed of the scrolling effect. This controls how fast or slow the element will move as the user scrolls the page.
Example: Set 1 for a slower scroll'; -$string['viewport'] = "Viewport"; -$string['viewport_help'] = 'Define when the scrolling effect should start relative to the user\'s viewport. This helps control how soon the effect is triggered based on how much of the element is visible on the screen.'; -$string['invaildelement'] = "Invalid content designer element"; -$string['elementtitle'] = "Title"; -$string['elementtitle_help'] = 'Enter the title for the element. This title will only be displayed on the content editor page to help organize and identify the element within the editor. It will not appear on the final content page.'; -$string['gerneralsettings'] = "General settings"; $string['abovecolorbg'] = "Background color/gradient (above)"; $string['abovecolorbg_help'] = 'Apply a gradient as the background for the element. You can create a smooth transition between two or more colors. Use either linear or radial gradients and specify the colors and direction for the gradient.
Gradient Example:linear-gradient(#ff5733, #f1c40f) (This will create a gradient from red to yellow).
Color Example: #ff5733 (This will set the background color of the element to a shade of red).'; -$string['elementbgimage'] = "Background image"; -$string['elementbgimage_help'] = 'Set a background image for the element. You can upload an image or provide a URL to an image. The image will be displayed behind the content of the element.'; +$string['addelement'] = 'Insert Element'; +$string['animationheading'] = "Entrance animation"; +$string['animationtitle'] = 'Entrance Animation'; +$string['backbehindelement'] = "Background behind element"; +$string['backgroundtitle'] = "Background"; $string['belowcolorbg'] = "Background color/gradient (below)"; $string['belowcolorbg_help'] = 'Set the background color or gradient for the element. You can choose a single color or a gradient that smoothly transitions between two or more colors. The background color or gradient will be applied below the content area of the element.
Gradient Example:linear-gradient(#ff5733, #f1c40f) (This will create a gradient from red to yellow).
Color Example: #ff5733 (This will set the background color of the element to a shade of red).'; +$string['chapterdone'] = 'Done'; +$string['chaptertitle'] = "Chapter Title"; +$string['completeprevelement'] = 'Complete the element above to continue.'; +$string['completionby'] = 'Completed BY'; +$string['completioncta'] = 'Complete chapter'; $string['completiondetail:reachend'] = 'Reach the end of the contents to complete'; +$string['content'] = "Content"; +$string['content_help'] = 'Enter the main text for this element. This content will be displayed as body text, ideal for providing details, descriptions, or supporting information within your content layout.'; +$string['contentdesigner:addinstance'] = 'Add a new content designer'; +$string['contentdesigner:view'] = 'View content designer activity'; $string['contentdesigner:viewcontenteditor'] = 'Access the content editor options'; - -$string['elementsettings'] = '{$a} element settings'; $string['contenteditor'] = "Content editor"; +$string['createchapter'] = "Create chapter"; +$string['creategeneral'] = 'Create general'; +$string['createnewelement'] = 'Create new element - Content Designer'; +$string['deletechecktype'] = 'Are you sure that you want to delete this {$a->element} element?'; +$string['element:creategeneral'] = "Create a general element"; +$string['element:viewgeneral'] = "View the general element"; +$string['elementbgimage'] = "Background image"; +$string['elementbgimage_help'] = 'Set a background image for the element. You can upload an image or provide a URL to an image. The image will be displayed behind the content of the element.'; $string['elementcreate'] = "Create element"; +$string['elementsettings'] = '{$a} element settings'; +$string['elementtitle'] = "Title"; +$string['elementtitle_help'] = 'Enter the title for the element. This title will only be displayed on the content editor page to help organize and identify the element within the editor. It will not appear on the final content page.'; $string['elementupdate'] = "Update element"; -$string['deletechecktype'] = 'Are you sure that you want to delete this {$a->element} element?'; - -// Heading element. +$string['fadein'] = "Fade in"; +$string['generaltitle'] = "General settings"; +$string['gerneralsettings'] = "General settings"; $string['headingtext'] = "Heading text"; $string['headingtext_help'] = 'Enter the main text for your heading. This will be prominently displayed to guide users through the content or section.'; $string['headingurl'] = "Heading URL"; $string['headingurl_help'] = 'Enter a URL if you want to make the heading clickable. When users click on the heading, they will be redirected to the specified link. Leave blank if no link is needed.'; -$string['mainheading'] = "Main heading (h2)"; -$string['subheading'] = "Sub heading (h3)"; +$string['hidden'] = "Hidden"; +$string['hideondesktop'] = "Hide on desktop"; +$string['hideondesktop_help'] = "Hide this element when viewed on a desktop screen."; +$string['hideonmobile'] = "Hide on mobile"; +$string['hideonmobile_help'] = "Hide this element when viewed on a mobile screen."; +$string['hideontablet'] = "Hide on tablet"; +$string['hideontablet_help'] = "Hide this element when viewed on a tablet screen."; $string['horizontalalign'] = "Horizontal Alignment"; $string['horizontalalign_help'] = 'Choose the horizontal position for the text within the element:
Left: Aligns the text to the left side.
Center: Centers the text within the element.
Right: Aligns the text to the right side.
Selecting the alignment helps control the visual layout of your content.'; -$string['verticalalign'] = "Vertical Alignment"; -$string['verticalalign_help'] = 'Select the vertical position of the text within the element:
-Top: Aligns the text to the top of the element.
-Middle: Centers the text vertically within the element.
-Bottom: Aligns the text to the bottom of the element.
-This setting is useful for fine-tuning the text position within your design layout.'; -$string['strleft'] = "Left"; -$string['strcenter'] = "Center"; -$string['strright'] = "Right"; -$string['strtop'] = "Top"; -$string['strmiddle'] = "Middle"; -$string['strbottom'] = "Bottom"; -$string['strblank'] = "Open a new window"; -$string['strself'] = "Open a same window"; -$string['target'] = "Target"; -$string['target_help'] = 'Select how the link should open if a Heading URL is provided:
-Same Window: Opens the link in the current browser tab, replacing the current page.
-New Window: Opens the link in a new browser tab, keeping the current page open. -Choose ‘New Window’ if you want users to keep this page open while viewing the link.'; -$string['strheading'] = "Heading"; -$string['strheading_help'] = 'Choose the type of heading to display. You have two options:
-Main Heading (H2): A larger, prominent heading typically used for main sections or titles.
-Sub Heading (H3): A slightly smaller heading used for subsections or supporting information within a main section. -Selecting the right heading type helps organize content and improves readability."'; -$string['content'] = "Content"; -$string['content_help'] = 'Enter the main text for this element. This content will be displayed as body text, ideal for providing details, descriptions, or supporting information within your content layout.'; +$string['invaildelement'] = "Invalid content designer element"; $string['invaildrecord'] = "Invaild record"; -$string['completioncta'] = 'Complete chapter'; -$string['chapterdone'] = 'Done'; -$string['addelement'] = 'Insert Element'; -$string['createnewelement'] = 'Create new element - Content Designer'; - -// Rich text. -$string['richtext'] = "Rich Text"; -$string['richtext_help'] = "Use the rich text editor to add and format content with full styling options, including text formatting, lists, links, and media. This editor supports file uploads, so you can include images, videos, and other media to enhance your content."; - - -// H5p. +$string['mainheading'] = "Main heading (h2)"; $string['mandatory'] = "Mandatory"; $string['mandatory_help'] = 'Specify whether completing this element is required to unlock the next one:
Yes: The user must complete this element before the next one is displayed.
No: The next element is available regardless of whether this one is completed.
This setting is useful for creating a sequential flow, guiding users through the content step-by-step.'; -$string['completeprevelement'] = 'Complete the element above to continue.'; - -// Outro. -$string['primarybuttontext'] = "primary button text"; -$string['primarybuttontext_help'] = "Enter the text that will appear on the primary button. This button typically represents the main action, such as 'Continue', 'Next', or 'Submit'."; -$string['primarybuttonurl'] = "primary button URL"; -$string['primarybuttonurl_help'] = 'Enter the URL the primary button will link to when clicked. This can be a link to another page, an external website, or another section within the content.'; -$string['secondarybuttontext'] = "Secondary button text"; -$string['secondarybuttontext_help'] = "Enter the text for the secondary button. This button is typically used for alternative actions, such as 'Cancel', 'Back', or 'Skip'. Choose a label that clearly indicates the secondary action."; -$string['secondarybuttonurl'] = "Secondary button URL"; -$string['secondarybuttonurl_help'] = 'Enter the URL the secondary button will link to when clicked. This could direct users to an alternative action or page.'; -$string['strimage'] = "Image"; -$string['strimage_help'] = 'Upload an image to display in the outro section. This image will appear at the end of the content, adding a visual element to enhance the closing message or theme.'; -$string['outro:btncustom'] = 'Custom'; -$string['outro:btnnext'] = 'Next'; +$string['margin'] = "Margin"; +$string['margin_help'] = 'Margin controls the space outside the element, creating distance between it and other elements. Set a single value to apply the same margin on all sides of the element.
Example: 10px (This will apply a 10px margin on all four sides: top, right, bottom, and left).'; +$string['modulename'] = 'Content Designer'; +$string['modulenameplural'] = "Content Designers"; $string['outro:btnbacktocourse'] = 'Back to course'; $string['outro:btnbacktosection'] = 'Back to section'; +$string['outro:btncustom'] = 'Custom'; +$string['outro:btnnext'] = 'Next'; +$string['padding'] = "Padding"; +$string['padding_help'] = 'Padding controls the space inside the element, between the content and its borders. Set a single value to apply the same padding on all sides of the element.
Example: 10px (This will apply a 10px padding on all four sides: top, right, bottom, and left).'; +$string['pluginadministration'] = 'Content designer administration'; +$string['pluginname'] = 'Content Designer'; $string['primarybutton'] = 'Primary button'; -$string['secondarybutton'] = 'Secondary button'; $string['primarybutton_help'] = 'Primary Button
Disabled: The primary button is hidden by default.
Custom: Displays a primary button where you can enter custom text and a URL.
Next: Displays a "Next" button that links to the next activity in the course sequence.
Back to Course: Displays a button that redirects to the course overview page.
Back to Section: Displays a button that links back to the current activity\'s section within the course.'; +$string['primarybuttontext'] = "primary button text"; +$string['primarybuttontext_help'] = "Enter the text that will appear on the primary button. This button typically represents the main action, such as 'Continue', 'Next', or 'Submit'."; +$string['primarybuttonurl'] = "primary button URL"; +$string['primarybuttonurl_help'] = 'Enter the URL the primary button will link to when clicked. This can be a link to another page, an external website, or another section within the content.'; + +$string['responsiveheading'] = "Responsive"; +$string['responsivetitle'] = 'Responsive settings'; +$string['richtext'] = "Rich Text"; +$string['richtext_help'] = "Use the rich text editor to add and format content with full styling options, including text formatting, lists, links, and media. This editor supports file uploads, so you can include images, videos, and other media to enhance your content."; +$string['scrolleffectsheading'] = "Scrolling effects (horizontal)"; +$string['scrollingeffectstitle'] = 'Scrolling Effects'; +$string['secondarybutton'] = 'Secondary button'; $string['secondarybutton_help'] = 'Secondary Button
Disabled: The secondary button is hidden by default.
Custom: Displays a secondary button where you can enter custom text and a URL.
Next: Displays a "Next" button that links to the next activity in the course sequence.
Back to Course: Displays a button that redirects to the course overview page.
Back to Section: Displays a button that links back to the current activity\'s section within the course.'; - -$string['privacy:metadata:completion:contentdesignerid'] = 'Content designer instance'; -$string['privacy:metadata:completion:userid'] = 'ID of the user'; -$string['privacy:metadata:completion:completion'] = 'Status of the user completion by self'; -$string['privacy:metadata:completion:timecreated'] = 'Time of completion'; -$string['completionby'] = 'Completed BY'; -$string['privacy:completion'] = 'Completions'; -$string['modulename'] = 'Content designer'; +$string['secondarybuttontext'] = "Secondary button text"; +$string['secondarybuttontext_help'] = "Enter the text for the secondary button. This button is typically used for alternative actions, such as 'Cancel', 'Back', or 'Skip'. Choose a label that clearly indicates the secondary action."; +$string['secondarybuttonurl'] = "Secondary button URL"; +$string['secondarybuttonurl_help'] = 'Enter the URL the secondary button will link to when clicked. This could direct users to an alternative action or page.'; +$string['slidefromleft'] = "Slide in from left"; +$string['slidefromright'] = "Slide in from right"; +$string['speed'] = "Speed"; +$string['speed_help'] = 'Set the speed of the scrolling effect. This controls how fast or slow the element will move as the user scrolls the page.
Example: Set 1 for a slower scroll'; +$string['stranimation'] = "Animation"; +$string['stranimation_help'] = "Choose an entrance animation for the element. This animation will play when the element first appears on the screen as the user scrolls into view or interacts with the content.
Select None if you don't want any animation effect for the element"; +$string['strblank'] = "Open a new window"; +$string['strbottom'] = "Bottom"; +$string['strcenter'] = "Center"; +$string['strdelay'] = "Delay"; +$string['strdelay_help'] = 'Set a delay before the entrance animation begins. This setting defines how much time should pass before the animation starts after the element comes into view.
Example: Set 2 (2 seconds delay).'; +$string['strdirection'] = "Direction"; +$string['strdirection_help'] = 'Apply scrolling effects to your element, making it move as the user scrolls through the page. These effects can help create dynamic interactions and draw attention to key content.
None: No scrolling effect applied. The element will remain stationary as the user scrolls.
To Left: The element will move from right to left as the user scrolls down the page.
To Right: The element will move from left to right as the user scrolls down the page.'; +$string['strduration'] = "Duration"; +$string['strduration_help'] = 'Choose the speed at which the entrance animation will occur. This setting determines how quickly or slowly the animation will play, adding different levels of emphasis and pacing.'; +$string['strfast'] = "Fast"; +$string['strheading'] = "Heading"; +$string['strheading_help'] = 'Choose the type of heading to display. You have two options:
+Main Heading (H2): A larger, prominent heading typically used for main sections or titles.
+Sub Heading (H3): A slightly smaller heading used for subsections or supporting information within a main section. +Selecting the right heading type helps organize content and improves readability."'; +$string['strimage'] = "Image"; +$string['strimage_help'] = 'Upload an image to display in the outro section. This image will appear at the end of the content, adding a visual element to enhance the closing message or theme.'; +$string['strleft'] = "Left"; +$string['strmiddle'] = "Middle"; +$string['strnormal'] = "Normal"; +$string['strright'] = "Right"; +$string['strself'] = "Open a same window"; +$string['strslow'] = "Slow"; +$string['strtop'] = "Top"; +$string['subheading'] = "Sub heading (h3)"; $string['subplugintype_element'] = 'Element plugin'; $string['subplugintype_element_plural'] = 'Element plugins'; - -// Chapter. +$string['target'] = "Target"; +$string['target_help'] = 'Select how the link should open if a Heading URL is provided:
+Same Window: Opens the link in the current browser tab, replacing the current page.
+New Window: Opens the link in a new browser tab, keeping the current page open. +Choose ‘New Window’ if you want users to keep this page open while viewing the link.'; $string['titlestatus'] = 'Display title'; $string['titlestatus_help'] = 'Enable this option to display the chapter\'s title to learners. When unchecked, the title will only be used for administrative purposes and will not be visible to learners. Checking this option makes the chapter title visible on the learner\'s view.'; +$string['toleft'] = "To left"; +$string['toright'] = "To Right"; +$string['verticalalign'] = "Vertical Alignment"; +$string['verticalalign_help'] = 'Select the vertical position of the text within the element:
+Top: Aligns the text to the top of the element.
+Middle: Centers the text vertically within the element.
+Bottom: Aligns the text to the bottom of the element.
+This setting is useful for fine-tuning the text position within your design layout.'; +$string['viewport'] = "Viewport"; +$string['viewport_help'] = 'Define when the scrolling effect should start relative to the user\'s viewport. This helps control how soon the effect is triggered based on how much of the element is visible on the screen.'; +$string['visibility'] = "Visibility"; +$string['visibility_help'] = "Show / Hide the element"; diff --git a/styles.css b/styles.css index a6b4fad..7465376 100644 --- a/styles.css +++ b/styles.css @@ -1,15 +1,19 @@ body#page-mod-contentdesigner-view { height: auto; + #page { height: auto; overflow-y: visible; } + #region-main { overflow-x: hidden; } } + .path-mod-contentdesigner { .mform .form-inline .form-control { + &[name="primaryurl"], &[name="secondaryurl"], &[name="abovecolorbg"], @@ -18,39 +22,48 @@ body#page-mod-contentdesigner-view { width: 100%; } } + .modal { .modal-dialog { max-width: 800px; } + .modal-content { .modal-header { border-bottom: 0; + h5 { font-weight: normal; } } + .modal-body .elements-list { list-style: none; padding: 0; margin: 0; + .element-item { padding: 5px 0; display: flex; align-items: center; + &:first-child { padding-top: 10px; padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid rgba(102, 102, 102, .2); } + i.icon { margin-right: 10px; } + .element-name { width: 85%; font-weight: bold; cursor: pointer; } + .element-description { width: 100%; } @@ -58,104 +71,135 @@ body#page-mod-contentdesigner-view { } } } + &.path-course-view { .modal-dialog-scrollable .modal-body { overflow-x: hidden; + .contentdesigner-progress { position: sticky; - position: -webkit-sticky; top: -20px; z-index: 1; } } } } + .contentdesigner-content { .contentdesigner-wrapper { + .course-content-list, .course-content-list li { + list-style: none; padding: 0; margin: 0; } + .course-content-list { padding: 0; margin: 0; - > li.chapters-list, - > li.element-item { + + >li.chapters-list, + >li.element-item { list-style: none; } - > li.element-item .element-outro .element-button { + + >li.element-item .element-outro .element-button { text-align: center; + a { margin-right: 10px; } } + li { button.btn.complete-chapter { display: block; margin-left: auto; } + &.completed button.btn { border-color: #28a745; background-color: #28a745; } + &.chapters-list { + padding: 20px; + margin-top: 20px; + border-radius: 15px; + border: 1px solid #efefef; + &:first-child .chapters-content .chapter-elements-list li.element-item:first-child .element-actions li[data-action="moveup"], - &:first-child .chapters-content > .element-item .element-actions li[data-action="moveup"], + &:first-child .chapters-content>.element-item .element-actions li[data-action="moveup"], &:nth-last-of-type(2) .chapters-content .chapter-elements-list li.element-item:last-child .element-actions li[data-action="movedown"], - &:nth-last-of-type(2) .chapters-content > .element-item .element-actions li[data-action="movedown"], + &:nth-last-of-type(2) .chapters-content>.element-item .element-actions li[data-action="movedown"], &:last-child .chapters-content .chapter-element .element-item .element-actions li[data-action="movedown"] { display: none; } } } + .chapter-elements-list { padding: 0; margin: 10px 0 0; + li.element-item { font-size: 16px; color: #343a40; margin-bottom: 20px; list-style: none; + .element-outro { text-align: center; + .element-button { text-align: center; margin-top: 15px; + a.btn { margin-right: 10px; } } } + .element-heading { margin-bottom: 0; } + a:hover, a:focus { text-decoration: none; } + .hl-left { text-align: left; } + .hl-center { text-align: center; } + .hl-right { text-align: right; } + .vl-top { vertical-align: top; } + .vl-middle { vertical-align: middle; } + .vl-bottom { vertical-align: bottom; } } + .element-item .general-options { position: relative; z-index: 0; + .background-options { width: 100%; height: 100%; @@ -163,6 +207,7 @@ body#page-mod-contentdesigner-view { top: 0; left: 0; z-index: -1; + .bg-color, .bg-image, .bg-overlay { @@ -176,50 +221,62 @@ body#page-mod-contentdesigner-view { z-index: 0; } } + p { margin-bottom: 0; } + a { margin-bottom: 10px; } } + li .element-box { border: 1px solid #000; background: none; } } + .item-outro { margin-top: 20px; } + .chapters-list.no-elements .element-item .element-box { background-color: #ebebeb; } + .element-item { .animation { opacity: 0; } + .animated { opacity: 1; } } } + .contentdesigner-progress { &.fixed-top { max-width: 830px; margin: 0 auto; top: 57px; } + #contentdesigner-progressbar { width: 100%; display: flex; + .contentdesigner-chapter { width: 100%; margin-right: 2px; + label { width: 100%; height: 5px; background-color: #dadada; } + &.chapter-completed label { background: #28a745; } @@ -227,27 +284,36 @@ body#page-mod-contentdesigner-view { } } } + .element-item { .element-box { background-color: #ccc; padding: 15px 10px; + display: flex; + .element-title { font-weight: bold; - display: inline-block; + margin-right: 10px; } + .element-icon { display: inline-block; margin-right: 10px; } + .element-actions { - float: right; + text-align: right; + margin-left: auto; + .actions-list li { display: inline-block; margin-right: 5px; margin-bottom: 0; + &:last-child { margin: 0; } + i { font-size: 15px; color: #121212; @@ -258,10 +324,12 @@ body#page-mod-contentdesigner-view { } } } - > .element-item { + + >.element-item { list-style: none; margin-bottom: 20px; } + .loading-icon { width: 100%; height: 100%; @@ -272,17 +340,20 @@ body#page-mod-contentdesigner-view { z-index: 1; display: flex; align-items: center; + i.icon { font-size: 24px; margin: 0 auto; } } + .contentdesigner-addelement { text-align: center; margin: 10px 0; clear: both; position: relative; z-index: 0; + &:before { content: ''; width: 97%; @@ -295,6 +366,7 @@ body#page-mod-contentdesigner-view { left: 0; z-index: -1; } + span { color: #fff; width: 40px; diff --git a/version.php b/version.php index 0fdbd74..5187d01 100644 --- a/version.php +++ b/version.php @@ -23,7 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2024110801; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2024110802; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2020061500; // Requires this Moodle version. $plugin->component = 'mod_contentdesigner'; // Full name of the plugin (used for diagnostics). $plugin->release = 'v1.1'; From a18a456f1e56d423b4f65433f648bf6185fabe5f Mon Sep 17 00:00:00 2001 From: Stefan-Alexander Scholz Date: Mon, 2 Dec 2024 13:29:01 +0100 Subject: [PATCH 5/6] Update README.md --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 464bba1..090a75c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,64 @@ -# moodle-mod_contentdesigner -Content Designer activity for Moodle +# Content Designer + +Create engaging and interactive content, directly in your Moodle course. Content Designer is developed specifically for instructional designers and collaborative content creation. The architecture of Content Designer was inspired by content management systems and built around the workflow of instructional designer: starting with a high-level concept, then broken down into individual elements and then adding content for each part of the concept. Content Designer’s chapter structure allows simultaneous collaborative editing of contents. + +In Content Designer, content is organised in chapters. Chapters then contain elements, such as headings, paragraphs or rich text. Each element comes with customization options for appearance, animations and responsiveness. Elements can be styled globally according to their type to match the organization’s branding guidelines and create a visually appealing look. This architecture makes it much easier to maintain a consistent look and feel. At the same time, content creation is easier and faster. The separation of content, layout and style is perfectly suited for quality management tasks and corporate design compliance. + +At the end of each activity, the outro element visually indicates the end of the activity. This event is also used to trigger activity completion. + +The dedicated h5p element gives teachers a great way to incorporate interactions into their content. For many content types, students can be required to complete the interaction before they can proceed. + + +# Requirements + +This plugin requires Moodle 3.9 + +# Motivation for this plugin + +Content designer simplifies mobile first content authoring, improves productivity with collaborative workflows, saves cost for authoring solutions and speeds up development. + +# Installation + +Install the plugin like any other plugin to folder /mod/contentdesigner +See http://docs.moodle.org/en/Installing_plugins for details on installing Moodle plugins + +# Quick start guide + +No setup up steps are required for administrators. + +Teachers (or users with the permission to edit courses) can add a new content designer activity like any other activity. Then, navigate to the editor tab in the secondary navigation to start adding content. + +# Documentation + +(NOT AVAILABLE YET, WILL BE PUBLISHED BY THE END OF 2024) +Please refer to the documentation for more information: https://github.com/bdecentgmbh/moodle-mod_contentdesigner/wiki + +# Theme support + +This plugin is developed and tested on Moodle Core's Boost theme. It should also work with Boost child themes, including Moodle Core's Classic theme. However, we can't support any other theme than Boost. + +# Plugin repositories + +This plugin will be published and regularly updated in the Moodle plugins repository: https://moodle.org/plugins/mod_contentdesigner +The latest development version can be found on Github: https://github.com/bdecentgmbh/moodle-mod_contentdesigner + +# Bug and problem reports / Support requests + +This plugin is carefully developed and thoroughly tested, but bugs and problems can always appear. Please report bugs and problems on Github: https://github.com/bdecentgmbh/moodle-mod_contentdesigner/issues We will do our best to solve your problems, but please note that due to limited resources we can't always provide per-case support. + +# Feature proposals + +Please issue feature proposals on Github: https://github.com/bdecentgmbh/moodle-mod_contentdesigner/issues Please create pull requests on Github: https://github.com/bdecentgmbh/moodle-mod_contentdesigner/pulls We are always interested to read about your feature proposals or even get a pull request from you, but please accept that we can handle your issues only as feature proposals and not as feature requests. + +# Moodle release support + +This plugin is maintained for the two most recent major releases of Moodle as well as the most recent LTS release of Moodle. If you are running a legacy version of Moodle, but want or need to run the latest version of this plugin, you can get the latest version of the plugin, remove the line starting with $plugin->requires from version.php and use this latest plugin version then on your legacy Moodle. However, please note that you will run this setup completely at your own risk. We can't support this approach in any way and there is an undeniable risk for erratic behavior. + +# Translating this plugin + +This Moodle plugin is shipped with an english language pack only. All translations into other languages must be managed through AMOS (https://lang.moodle.org) by what they will become part of Moodle's official language pack. + +# Copyright + +bdecent gmbh +bdecent.de From fed7989a65476330820a00c792b46b135b588333 Mon Sep 17 00:00:00 2001 From: Stefan-Alexander Scholz Date: Mon, 2 Dec 2024 13:32:16 +0100 Subject: [PATCH 6/6] Update moodle-ci.yml --- .github/workflows/moodle-ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index e57198f..3ba2bb3 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -1,14 +1,14 @@ name: Moodle Plugin CI -on: [push, pull_request] +on: [pull_request] jobs: test: - runs-on: ubuntu-18.04 + runs-on: ubuntu-24.04 services: postgres: - image: postgres:12.7 + image: postgres:13 env: POSTGRES_USER: 'postgres' POSTGRES_HOST_AUTH_METHOD: 'trust' @@ -16,7 +16,7 @@ jobs: - 5432:5432 options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 3 mariadb: - image: mariadb:10.5 + image: mariadb:10.6.7 env: MYSQL_USER: 'root' MYSQL_ALLOW_EMPTY_PASSWORD: "true" @@ -28,18 +28,18 @@ jobs: fail-fast: false matrix: include: - - php: '8.0' - moodle-branch: 'MOODLE_400_STABLE' - database: 'pgsql' - - php: '8.0' - moodle-branch: 'MOODLE_400_STABLE' + - php: '8.3' + moodle-branch: 'MOODLE_405_STABLE' database: 'mariadb' - - php: '7.4' - moodle-branch: 'MOODLE_400_STABLE' + - php: '8.2' + moodle-branch: 'MOODLE_405_STABLE' database: 'pgsql' - - php: '7.4' - moodle-branch: 'MOODLE_400_STABLE' + - php: '8.1' + moodle-branch: 'MOODLE_403_STABLE' database: 'mariadb' + - php: '8.0' + moodle-branch: 'MOODLE_401_STABLE' + database: 'pgsql' steps: - name: Check out repository code