Apply prettier to whole project

This commit is contained in:
Ramon Wenger 2023-01-12 15:58:59 +01:00
parent 647e684469
commit 9a91aaf47c
443 changed files with 19003 additions and 17334 deletions

View File

@ -1,14 +1,15 @@
{ {
"presets": [ "presets": [
"@babel/preset-typescript", "@babel/preset-typescript",
["@babel/preset-env", { [
"useBuiltIns": false, "@babel/preset-env",
"targets": { {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"] "useBuiltIns": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
} }
}] ]
], ],
"plugins": [ "plugins": ["@babel/plugin-transform-runtime"]
"@babel/plugin-transform-runtime"
]
} }

View File

@ -10,7 +10,7 @@ module.exports = {
browser: true, browser: true,
}, },
globals: { globals: {
process: "readonly" process: 'readonly',
}, },
extends: [ extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
@ -21,69 +21,77 @@ module.exports = {
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
], ],
// required to lint *.vue files // required to lint *.vue files
plugins: [ plugins: ['vue', '@typescript-eslint'],
'vue', overrides: [
'@typescript-eslint' {
files: ['*.ts', '*.tsx'],
rules: {
'no-unused-vars': 'off',
},
},
], ],
overrides: [{
files: ['*.ts','*.tsx'],
rules: {
'no-unused-vars': 'off'
}
}],
// add your custom rules here // add your custom rules here
rules: { rules: {
// allow async-await // allow async-await
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
// allow debugger during development // allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'indent': 'off', indent: 'off',
'semi': ['error', 'always'], semi: ['error', 'always'],
'space-before-function-paren': 'off', 'space-before-function-paren': 'off',
'comma-dangle': 'off', 'comma-dangle': 'off',
// vue rules // vue rules
'vue/require-prop-types': 'off', //todo: should we do this? 'vue/require-prop-types': 'off', //todo: should we do this?
'vue/require-default-prop': 'off', //todo: should we do this? 'vue/require-default-prop': 'off', //todo: should we do this?
'vue/attributes-order': ['error', { 'vue/attributes-order': [
'order': [ 'error',
'OTHER_ATTR', {
'DEFINITION', order: [
'LIST_RENDERING', 'OTHER_ATTR',
'CONDITIONALS', 'DEFINITION',
'RENDER_MODIFIERS', 'LIST_RENDERING',
'GLOBAL', 'CONDITIONALS',
'UNIQUE', 'RENDER_MODIFIERS',
'TWO_WAY_BINDING', 'GLOBAL',
'OTHER_DIRECTIVES', 'UNIQUE',
'EVENTS', 'TWO_WAY_BINDING',
'CONTENT' 'OTHER_DIRECTIVES',
] 'EVENTS',
}], 'CONTENT',
"vue/multi-word-component-names": ["off", { ],
"ignores": [] },
}], ],
'vue/order-in-components': ['error', { 'vue/multi-word-component-names': [
'order': [ 'off',
'el', {
'name', ignores: [],
'parent', },
'functional', ],
['delimiters', 'comments'], 'vue/order-in-components': [
['props', 'propsData'], 'error',
'mixins', {
['components', 'directives', 'filters'], order: [
'data', 'el',
'extends', 'name',
'inheritAttrs', 'parent',
'model', 'functional',
'computed', ['delimiters', 'comments'],
'watch', ['props', 'propsData'],
'LIFECYCLE_HOOKS', 'mixins',
'methods', ['components', 'directives', 'filters'],
['template', 'render'], 'data',
'renderError' 'extends',
] 'inheritAttrs',
}] 'model',
} 'computed',
'watch',
'LIFECYCLE_HOOKS',
'methods',
['template', 'render'],
'renderError',
],
},
],
},
}; };

View File

@ -1,10 +1,10 @@
// https://github.com/michael-ciniawsky/postcss-load-config // https://github.com/michael-ciniawsky/postcss-load-config
module.exports = { module.exports = {
"plugins": { plugins: {
"postcss-import": {}, 'postcss-import': {},
"postcss-url": {}, 'postcss-url': {},
// to edit target browsers: use "browserslist" field in package.json // to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {} autoprefixer: {},
} },
} };

View File

@ -1,41 +1,45 @@
'use strict' 'use strict';
require('./check-versions')() require('./check-versions')();
process.env.NODE_ENV = 'production' process.env.NODE_ENV = 'production';
const ora = require('ora') const ora = require('ora');
const rm = require('rimraf') const rm = require('rimraf');
const path = require('path') const path = require('path');
const chalk = require('chalk') const chalk = require('chalk');
const webpack = require('webpack') const webpack = require('webpack');
const config = require('../config') const config = require('../config');
const webpackConfig = require('./webpack.prod.conf') const webpackConfig = require('./webpack.prod.conf');
const spinner = ora('building for production...') const spinner = ora('building for production...');
spinner.start() spinner.start();
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), (err) => {
if (err) throw err if (err) throw err;
webpack(webpackConfig, (err, stats) => { webpack(webpackConfig, (err, stats) => {
spinner.succeed() spinner.succeed();
if (err) throw err if (err) throw err;
process.stdout.write(stats.toString({ process.stdout.write(
colors: true, stats.toString({
modules: false, colors: true,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. modules: false,
chunks: false, children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunkModules: false chunks: false,
}) + '\n\n') chunkModules: false,
}) + '\n\n'
);
if (stats.hasErrors()) { if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n')) console.log(chalk.red(' Build failed with errors.\n'));
process.exit(1) process.exit(1);
} }
console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.cyan(' Build complete.\n'));
console.log(chalk.yellow( console.log(
' Tip: built files are meant to be served over an HTTP server.\n' + chalk.yellow(
' Opening index.html over file:// won\'t work.\n' ' Tip: built files are meant to be served over an HTTP server.\n' +
)) " Opening index.html over file:// won't work.\n"
}) )
}) );
});
});

View File

@ -1,54 +1,53 @@
'use strict' 'use strict';
const chalk = require('chalk') const chalk = require('chalk');
const semver = require('semver') const semver = require('semver');
const packageConfig = require('../package.json') const packageConfig = require('../package.json');
const shell = require('shelljs') const shell = require('shelljs');
function exec (cmd) { function exec(cmd) {
return require('child_process').execSync(cmd).toString().trim() return require('child_process').execSync(cmd).toString().trim();
} }
const versionRequirements = [ const versionRequirements = [
{ {
name: 'node', name: 'node',
currentVersion: semver.clean(process.version), currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node versionRequirement: packageConfig.engines.node,
} },
] ];
if (shell.which('npm')) { if (shell.which('npm')) {
versionRequirements.push({ versionRequirements.push({
name: 'npm', name: 'npm',
currentVersion: exec('npm --version'), currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm versionRequirement: packageConfig.engines.npm,
}) });
} }
module.exports = function () { module.exports = function () {
const warnings = [] const warnings = [];
for (let i = 0; i < versionRequirements.length; i++) { for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i] const mod = versionRequirements[i];
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' + warnings.push(
chalk.red(mod.currentVersion) + ' should be ' + mod.name + ': ' + chalk.red(mod.currentVersion) + ' should be ' + chalk.green(mod.versionRequirement)
chalk.green(mod.versionRequirement) );
)
} }
} }
if (warnings.length) { if (warnings.length) {
console.log('') console.log('');
console.log(chalk.yellow('To use this template, you must update following to modules:')) console.log(chalk.yellow('To use this template, you must update following to modules:'));
console.log() console.log();
for (let i = 0; i < warnings.length; i++) { for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i] const warning = warnings[i];
console.log(' ' + warning) console.log(' ' + warning);
} }
console.log() console.log();
process.exit(1) process.exit(1);
} }
} };

View File

@ -1,20 +1,17 @@
'use strict' 'use strict';
const path = require('path') const path = require('path');
const config = require('../config') const config = require('../config');
const packageConfig = require('../package.json') const packageConfig = require('../package.json');
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
const assetsPath = (_path) => { const assetsPath = (_path) => {
const assetsSubDirectory = isDev const assetsSubDirectory = isDev ? config.dev.assetsSubDirectory : config.build.assetsSubDirectory;
? config.dev.assetsSubDirectory
: config.build.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
return path.posix.join(assetsSubDirectory, _path);
};
module.exports = { module.exports = {
isDev, isDev,
assetsPath assetsPath,
} };

View File

@ -3,10 +3,10 @@ const path = require('path');
const config = require('../config'); const config = require('../config');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {VueLoaderPlugin} = require('vue-loader'); const { VueLoaderPlugin } = require('vue-loader');
const ESLintPlugin = require('eslint-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin');
const {isDev, assetsPath} = require('./utils'); const { isDev, assetsPath } = require('./utils');
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, '..', dir); return path.join(__dirname, '..', dir);
@ -15,7 +15,7 @@ function resolve(dir) {
const eslintOptions = { const eslintOptions = {
formatter: require('eslint-formatter-friendly'), formatter: require('eslint-formatter-friendly'),
emitWarning: !config.dev.showEslintErrorsInOverlay, emitWarning: !config.dev.showEslintErrorsInOverlay,
extensions: ['js', 'ts', 'vue'] extensions: ['js', 'ts', 'vue'],
}; };
//todo: mini-css-extract-plugin? upgrade to webpack 4, then use this //todo: mini-css-extract-plugin? upgrade to webpack 4, then use this
@ -29,9 +29,7 @@ module.exports = {
output: { output: {
path: config.build.assetsRoot, path: config.build.assetsRoot,
filename: '[name].js', filename: '[name].js',
publicPath: isDev publicPath: isDev ? config.dev.assetsPublicPath : config.build.assetsPublicPath,
? config.dev.assetsPublicPath
: config.build.assetsPublicPath,
}, },
optimization: { optimization: {
splitChunks: { splitChunks: {
@ -131,10 +129,7 @@ module.exports = {
// styleRule(true), // sass rule // styleRule(true), // sass rule
], ],
}, },
plugins: [ plugins: [new VueLoaderPlugin(), new ESLintPlugin(eslintOptions)],
new VueLoaderPlugin(),
new ESLintPlugin(eslintOptions),
],
// node: { // node: {
// // prevent webpack from injecting useless setImmediate polyfill because Vue // // prevent webpack from injecting useless setImmediate polyfill because Vue

View File

@ -8,7 +8,7 @@ const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const portfinder = require('portfinder'); const portfinder = require('portfinder');
const {merge} = require('webpack-merge'); const { merge } = require('webpack-merge');
const HOST = process.env.HOST; const HOST = process.env.HOST;
const PORT = process.env.PORT && Number(process.env.PORT); const PORT = process.env.PORT && Number(process.env.PORT);
@ -21,15 +21,12 @@ const devWebpackConfig = merge(baseWebpackConfig, {
devServer: { devServer: {
client: { client: {
logging: 'warn', logging: 'warn',
overlay: config.dev.errorOverlay ? {errors: true, warnings: false} : false, overlay: config.dev.errorOverlay ? { errors: true, warnings: false } : false,
progress: true, progress: true,
}, },
historyApiFallback: { historyApiFallback: {
rewrites: [ rewrites: [{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }],
{from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html')},
],
}, },
hot: true, hot: true,
compress: true, compress: true,
@ -66,8 +63,8 @@ const devWebpackConfig = merge(baseWebpackConfig, {
], ],
}), }),
new BundleAnalyzerPlugin({ new BundleAnalyzerPlugin({
analyzerMode: 'disabled' // do nothing by default, but be able to generate stats with --profile analyzerMode: 'disabled', // do nothing by default, but be able to generate stats with --profile
}) }),
], ],
}); });

View File

@ -3,11 +3,11 @@ const path = require('path');
const utils = require('./utils'); const utils = require('./utils');
const webpack = require('webpack'); const webpack = require('webpack');
const config = require('../config'); const config = require('../config');
const {merge} = require('webpack-merge'); const { merge } = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf'); const baseWebpackConfig = require('./webpack.base.conf');
const CopyWebpackPlugin = require('copy-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const env = require('../config/prod.env'); const env = require('../config/prod.env');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@ -21,9 +21,7 @@ const webpackConfig = merge(baseWebpackConfig, {
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'), chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
}, },
optimization: { optimization: {
minimizer: [ minimizer: [new CssMinimizerPlugin()],
new CssMinimizerPlugin()
]
}, },
plugins: [ plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html // http://vuejs.github.io/vue-loader/en/workflow/production.html
@ -44,7 +42,8 @@ const webpackConfig = merge(baseWebpackConfig, {
filename: config.build.index, filename: config.build.index,
template: 'index.html', template: 'index.html',
...require('../config/prod.env'), ...require('../config/prod.env'),
minify: { // defaults from https://github.com/jantimon/html-webpack-plugin#minification minify: {
// defaults from https://github.com/jantimon/html-webpack-plugin#minification
collapseWhitespace: true, collapseWhitespace: true,
keepClosingSlash: true, keepClosingSlash: true,
removeComments: true, removeComments: true,
@ -112,14 +111,10 @@ if (config.build.productionGzip) {
new CompressionWebpackPlugin({ new CompressionWebpackPlugin({
asset: '[path].gz[query]', asset: '[path].gz[query]',
algorithm: 'gzip', algorithm: 'gzip',
test: new RegExp( test: new RegExp('\\.(' + config.build.productionGzipExtensions.join('|') + ')$'),
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$',
),
threshold: 10240, threshold: 10240,
minRatio: 0.8, minRatio: 0.8,
}), })
); );
} }

View File

@ -1,8 +1,8 @@
'use strict' 'use strict';
const {merge} = require('webpack-merge') const { merge } = require('webpack-merge');
const prodEnv = require('./prod.env') const prodEnv = require('./prod.env');
module.exports = merge(prodEnv, { module.exports = merge(prodEnv, {
NODE_ENV: '"development"', NODE_ENV: '"development"',
VUE_APP_ENABLE_SPELLCHECK: 'true' VUE_APP_ENABLE_SPELLCHECK: 'true',
}); });

View File

@ -1,12 +1,11 @@
'use strict' 'use strict';
// Template version: 1.3.1 // Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation. // see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path') const path = require('path');
module.exports = { module.exports = {
dev: { dev: {
// Paths // Paths
assetsSubDirectory: 'static', assetsSubDirectory: 'static',
assetsPublicPath: '/', assetsPublicPath: '/',
@ -41,7 +40,7 @@ module.exports = {
// https://vue-loader.vuejs.org/en/options.html#cachebusting // https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true, cacheBusting: true,
cssSourceMap: true cssSourceMap: true,
}, },
build: { build: {
@ -71,6 +70,6 @@ module.exports = {
// View the bundle analyzer report after build finishes: // View the bundle analyzer report after build finishes:
// `npm run build --report` // `npm run build --report`
// Set to `true` or `false` to always turn it on or off // Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report bundleAnalyzerReport: process.env.npm_config_report,
} },
} };

View File

@ -1,9 +1,9 @@
'use strict' 'use strict';
module.exports = { module.exports = {
/* /*
* ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes * ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes
* around strings * around strings
*/ */
VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL, VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL,
/* /*
@ -12,6 +12,6 @@ module.exports = {
// vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv // vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv
VUE_APP_FAVICON_32: 'https://skillbox-my-detailhandel-dha-prod.s3.eu-central-1.amazonaws.com/myDHA-favicon.png', VUE_APP_FAVICON_32: 'https://skillbox-my-detailhandel-dha-prod.s3.eu-central-1.amazonaws.com/myDHA-favicon.png',
VUE_APP_FAVICON_16: 'https://skillbox-my-detailhandel-dha-prod.s3.eu-central-1.amazonaws.com/myDHA-favicon.png', VUE_APP_FAVICON_16: 'https://skillbox-my-detailhandel-dha-prod.s3.eu-central-1.amazonaws.com/myDHA-favicon.png',
VUE_APP_TITLE: 'myDHA' VUE_APP_TITLE: 'myDHA',
// ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^ // ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^
} };

View File

@ -1,9 +1,9 @@
'use strict' 'use strict';
module.exports = { module.exports = {
/* /*
* ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes * ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes
* around strings * around strings
*/ */
VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL, VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL,
/* /*
@ -12,6 +12,6 @@ module.exports = {
// vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv // vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv
VUE_APP_FAVICON_32: 'https://skillbox-my-detailhandel-dhf-prod.s3.eu-central-1.amazonaws.com/myDHF-favicon.png', VUE_APP_FAVICON_32: 'https://skillbox-my-detailhandel-dhf-prod.s3.eu-central-1.amazonaws.com/myDHF-favicon.png',
VUE_APP_FAVICON_16: 'https://skillbox-my-detailhandel-dhf-prod.s3.eu-central-1.amazonaws.com/myDHF-favicon.png', VUE_APP_FAVICON_16: 'https://skillbox-my-detailhandel-dhf-prod.s3.eu-central-1.amazonaws.com/myDHF-favicon.png',
VUE_APP_TITLE: 'myDHF' VUE_APP_TITLE: 'myDHF',
// ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^ // ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^
} };

View File

@ -1,9 +1,9 @@
'use strict' 'use strict';
module.exports = { module.exports = {
/* /*
* ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes * ENV variables used in JS code need to be stringyfied, as they will be replaced in the code, and JS needs quotes
* around strings * around strings
*/ */
VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL, VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL,
/* /*
@ -12,6 +12,6 @@ module.exports = {
// vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv // vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv
VUE_APP_FAVICON_32: 'https://skillbox-my-kv-prod.s3-eu-west-1.amazonaws.com/mykv-favicon.png', VUE_APP_FAVICON_32: 'https://skillbox-my-kv-prod.s3-eu-west-1.amazonaws.com/mykv-favicon.png',
VUE_APP_FAVICON_16: 'https://skillbox-my-kv-prod.s3-eu-west-1.amazonaws.com/mykv-favicon.png', VUE_APP_FAVICON_16: 'https://skillbox-my-kv-prod.s3-eu-west-1.amazonaws.com/mykv-favicon.png',
VUE_APP_TITLE: 'myKV' VUE_APP_TITLE: 'myKV',
// ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^ // ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^
} };

View File

@ -1,5 +1,5 @@
'use strict' 'use strict';
const { merge } = require('webpack-merge') const { merge } = require('webpack-merge');
const values = { const values = {
NODE_ENV: '"production"', NODE_ENV: '"production"',
@ -9,10 +9,10 @@ const values = {
LOGOUT_REDIRECT_URL: JSON.stringify(process.env.LOGOUT_REDIRECT_URL), LOGOUT_REDIRECT_URL: JSON.stringify(process.env.LOGOUT_REDIRECT_URL),
VUE_APP_FLAVOR: JSON.stringify(process.env.APP_FLAVOR), VUE_APP_FLAVOR: JSON.stringify(process.env.APP_FLAVOR),
/* /*
* ENV variables used in JS code need to be stringyfied, as they will be replaced (in place) in the code, * ENV variables used in JS code need to be stringyfied, as they will be replaced (in place) in the code,
* and JS needs quotes around strings * and JS needs quotes around strings
* see https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code * see https://cli.vuejs.org/guide/mode-and-env.html#using-env-variables-in-client-side-code
*/ */
VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL, VUE_APP_ENABLE_SPELLCHECK: !!process.env.TASKBASE_BASEURL,
/* /*
@ -21,9 +21,9 @@ const values = {
// vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv // vvvv HTML PROPERTIES FROM HERE, NOT STRINGIFIED vvvv
VUE_APP_FAVICON_32: '/static/favicon-32x32.png', VUE_APP_FAVICON_32: '/static/favicon-32x32.png',
VUE_APP_FAVICON_16: '/static/favicon-16x16.png', VUE_APP_FAVICON_16: '/static/favicon-16x16.png',
VUE_APP_TITLE: 'mySkillbox' VUE_APP_TITLE: 'mySkillbox',
// ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^ // ^^^^ HTML PROPERTIES TO HERE, NOT STRINGIFIED ^^^^
} };
switch (process.env.APP_FLAVOR) { switch (process.env.APP_FLAVOR) {
case 'my-kv': case 'my-kv':
@ -39,4 +39,3 @@ switch (process.env.APP_FLAVOR) {
// we are on the skillbox APP_FLAVOR // we are on the skillbox APP_FLAVOR
module.exports = values; module.exports = values;
} }

View File

@ -1,27 +1,24 @@
import {defineConfig} from 'cypress'; import { defineConfig } from 'cypress';
import {readFileSync} from "fs"; import { readFileSync } from 'fs';
import {resolve} from "path"; import { resolve } from 'path';
export default defineConfig( { export default defineConfig({
e2e: { e2e: {
"baseUrl": "http://localhost:8000", baseUrl: 'http://localhost:8000',
specPattern: 'cypress/e2e/e2e/**/*.{spec,cy}.{js,ts}', specPattern: 'cypress/e2e/e2e/**/*.{spec,cy}.{js,ts}',
supportFile: 'cypress/support/e2e.ts', supportFile: 'cypress/support/e2e.ts',
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
on('task', { on('task', {
getSchema() { getSchema() {
return readFileSync( return readFileSync(resolve(__dirname, '../server/schema.graphql'), 'utf8');
resolve(__dirname, '../server/schema.graphql'), },
'utf8'
);
}
}); });
}, },
}, },
"videoUploadOnPasses": false, videoUploadOnPasses: false,
"reporter": "junit", reporter: 'junit',
"reporterOptions": { reporterOptions: {
"mochaFile": "cypress/test-reports/e2e/cypress-results-[hash].xml", mochaFile: 'cypress/test-reports/e2e/cypress-results-[hash].xml',
"toConsole": true toConsole: true,
}, },
"projectId": "msk-ee", projectId: 'msk-ee',
}); });

View File

@ -1,34 +1,29 @@
import {defineConfig} from 'cypress'; import { defineConfig } from 'cypress';
import {readFileSync} from "fs"; import { readFileSync } from 'fs';
import {resolve} from "path"; import { resolve } from 'path';
export default defineConfig({ export default defineConfig({
chromeWebSecurity: false, chromeWebSecurity: false,
e2e: { e2e: {
baseUrl: "http://localhost:8080", baseUrl: 'http://localhost:8080',
specPattern: 'cypress/e2e/frontend/**/*.{cy,spec}.{js,ts}', specPattern: 'cypress/e2e/frontend/**/*.{cy,spec}.{js,ts}',
supportFile: 'cypress/support/e2e.ts', supportFile: 'cypress/support/e2e.ts',
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
on('task', { on('task', {
getSchema() { getSchema() {
return readFileSync( return readFileSync(resolve(__dirname, '../server/schema.graphql'), 'utf8');
resolve(__dirname, '../server/schema.graphql'), },
'utf8'
);
}
}); });
}, },
}, },
videoUploadOnPasses: false, videoUploadOnPasses: false,
reporter: "junit", reporter: 'junit',
reporterOptions: { reporterOptions: {
mochaFile: "cypress/test-reports/frontend/cypress-results-[hash].xml", mochaFile: 'cypress/test-reports/frontend/cypress-results-[hash].xml',
toConsole: true toConsole: true,
}, },
"projectId": "msk-fe", projectId: 'msk-fe',
retries: { retries: {
runMode: 3 runMode: 3,
} },
}); });

View File

@ -1,43 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
<title><%= htmlWebpackPlugin.options.VUE_APP_TITLE %></title> <title><%= htmlWebpackPlugin.options.VUE_APP_TITLE %></title>
<link href='https://fonts.googleapis.com/css?family=Material+Icons' rel="stylesheet" type="text/css"> <link href="https://fonts.googleapis.com/css?family=Material+Icons" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,600,800" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Montserrat:400,600,800" rel="stylesheet" />
<link href="https://use.typekit.net/tck7ptw.css" rel="stylesheet"> <link href="https://use.typekit.net/tck7ptw.css" rel="stylesheet" />
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="<%- htmlWebpackPlugin.options.VUE_APP_FAVICON_32 %>"> <link rel="icon" type="image/png" sizes="32x32" href="<%- htmlWebpackPlugin.options.VUE_APP_FAVICON_32 %>" />
<link rel="icon" type="image/png" sizes="16x16" href="<%- htmlWebpackPlugin.options.VUE_APP_FAVICON_16 %>"> <link rel="icon" type="image/png" sizes="16x16" href="<%- htmlWebpackPlugin.options.VUE_APP_FAVICON_16 %>" />
<link rel="manifest" href="/static/site.webmanifest"> <link rel="manifest" href="/static/site.webmanifest" />
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c"> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff" />
<script> <script>
window.UPLOADCARE_PUBLIC_KEY = '78212ff39934a59775ac'; window.UPLOADCARE_PUBLIC_KEY = '78212ff39934a59775ac';
window.UPLOADCARE_LOCALE = 'de'; window.UPLOADCARE_LOCALE = 'de';
window.UPLOADCARE_LOCALE_TRANSLATIONS = { window.UPLOADCARE_LOCALE_TRANSLATIONS = {
dialog: { dialog: {
tabs: { tabs: {
file: { file: {
drag: 'Ziehen Sie ein Bild hier hinein', drag: 'Ziehen Sie ein Bild hier hinein',
button: 'Wählen Sie ein lokales Bild', button: 'Wählen Sie ein lokales Bild',
} },
} },
} },
}; };
</script> </script>
</head>
</head> <body>
<body> <div id="app">
<div id="app"> <div class="center"></div>
<div class="center"> </div>
</div> <!-- built files will be auto injected -->
</div> </body>
<!-- built files will be auto injected -->
</body>
</html> </html>

View File

@ -1,11 +1,5 @@
module.exports = { module.exports = {
moduleFileExtensions: [ moduleFileExtensions: ['js', 'jsx', 'ts', 'json', 'vue'],
'js',
'jsx',
'ts',
'json',
'vue',
],
transform: { transform: {
'\\.(gql|graphql)$': 'jest-transform-graphql', '\\.(gql|graphql)$': 'jest-transform-graphql',
'^.+\\.js$': 'babel-jest', '^.+\\.js$': 'babel-jest',
@ -13,27 +7,15 @@ module.exports = {
'^.+\\.vue$': '@vue/vue2-jest', '^.+\\.vue$': '@vue/vue2-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
}, },
modulePaths: [ modulePaths: ['<rootDir>/src', '<rootDir>/node_modules'],
'<rootDir>/src', transformIgnorePatterns: ['/node_modules/'],
'<rootDir>/node_modules',
],
transformIgnorePatterns: [
'/node_modules/',
],
moduleNameMapper: { moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', '^@/(.*)$': '<rootDir>/src/$1',
'^gql/(.*)$': '<rootDir>/src/graphql/gql/$1', '^gql/(.*)$': '<rootDir>/src/graphql/gql/$1',
}, },
snapshotSerializers: [ snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
'<rootDir>/node_modules/jest-serializer-vue',
],
testEnvironment: 'jsdom', testEnvironment: 'jsdom',
testMatch: [ testMatch: ['**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)',
],
testURL: 'http://localhost/', testURL: 'http://localhost/',
watchPlugins: [ watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
}; };

View File

@ -1,29 +1,26 @@
const {readFileSync} = require('fs'); const { readFileSync } = require('fs');
const {resolve} = require('path'); const { resolve } = require('path');
const { addMocksToSchema} = require('@graphql-tools/mock'); const { addMocksToSchema } = require('@graphql-tools/mock');
const { makeExecutableSchema } = require('@graphql-tools/schema'); const { makeExecutableSchema } = require('@graphql-tools/schema');
const { graphql } = require('graphql'); const { graphql } = require('graphql');
const schemaString = readFileSync( const schemaString = readFileSync(resolve(__dirname, '../server/schema.graphql'), 'utf8');
resolve(__dirname,'../server/schema.graphql'),
'utf8'
);
// Make a GraphQL schema with no resolvers // Make a GraphQL schema with no resolvers
const schema = makeExecutableSchema({ typeDefs: schemaString }) const schema = makeExecutableSchema({ typeDefs: schemaString });
// Create a new schema with mocks // Create a new schema with mocks
const schemaWithMocks = addMocksToSchema({ schema }) const schemaWithMocks = addMocksToSchema({ schema });
const query = /* GraphQL */ ` const query = /* GraphQL */ `
query MeQuery { query MeQuery {
me { me {
firstName firstName
} }
} }
` `;
graphql({ graphql({
schema: schemaWithMocks, schema: schemaWithMocks,
source: query, source: query,
}).then(result => console.log('Got result', result)) }).then((result) => console.log('Got result', result));

View File

@ -1,12 +1,12 @@
declare module '*.graphql' { declare module '*.graphql' {
import {DocumentNode} from "graphql"; import { DocumentNode } from 'graphql';
const Schema: DocumentNode; const Schema: DocumentNode;
export = Schema; export = Schema;
} }
declare module '*.gql' { declare module '*.gql' {
import {DocumentNode} from "graphql"; import { DocumentNode } from 'graphql';
const content: DocumentNode; const content: DocumentNode;
export default content; export default content;
} }

View File

@ -11,7 +11,7 @@ export interface ContentBlock {
} }
export interface ActionOptions { export interface ActionOptions {
up?: boolean, up?: boolean;
down?: boolean, down?: boolean;
extended?: boolean extended?: boolean;
} }

View File

@ -1,109 +1,107 @@
<template> <template>
<div <div :class="{ 'no-scroll': showModal || showMobileNavigation }" class="app" id="app">
:class="{'no-scroll': showModal || showMobileNavigation}"
class="app"
id="app"
>
<read-only-banner /> <read-only-banner />
<scroll-up /> <scroll-up />
<component <component :is="showModalDeprecated" v-if="showModalDeprecated" />
:is="showModalDeprecated" <component :is="showModal" v-if="showModal" />
v-if="showModalDeprecated"
/>
<component
:is="showModal"
v-if="showModal"
/>
<component :is="layout" /> <component :is="layout" />
</div> </div>
</template> </template>
<script> <script>
import {mapGetters} from 'vuex'; import { mapGetters } from 'vuex';
import ScrollUp from '@/components/ScrollUp'; import ScrollUp from '@/components/ScrollUp';
import ReadOnlyBanner from '@/components/ReadOnlyBanner'; import ReadOnlyBanner from '@/components/ReadOnlyBanner';
import modals from '@/components/modals'; import modals from '@/components/modals';
const NewContentBlockWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/NewContentBlockWizard'); const NewContentBlockWizard = () =>
const EditContentBlockWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/EditContentBlockWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/NewContentBlockWizard');
const EditRoomEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/rooms/room-entries/EditRoomEntryWizard'); const EditContentBlockWizard = () =>
const NewProjectEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/portfolio/NewProjectEntryWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/EditContentBlockWizard');
const EditProjectEntryWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/portfolio/EditProjectEntryWizard'); const EditRoomEntryWizard = () =>
const NewObjectiveWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/objective-groups/NewObjectiveWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/rooms/room-entries/EditRoomEntryWizard');
const NewNoteWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/notes/NewNoteWizard'); const NewProjectEntryWizard = () =>
const EditNoteWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/notes/EditNoteWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/NewProjectEntryWizard');
const EditClassNameWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/school-class/EditClassNameWizard'); const EditProjectEntryWizard = () =>
const EditTeamNameWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/profile/EditTeamNameWizard'); import(/* webpackChunkName: "content-forms" */ '@/components/portfolio/EditProjectEntryWizard');
const EditSnapshotTitleWizard = () => import(/* webpackChunkName: "content-forms" */'@/components/snapshots/EditSnapshotTitleWizard'); const NewObjectiveWizard = () =>
const DefaultLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/DefaultLayout'); import(/* webpackChunkName: "content-forms" */ '@/components/objective-groups/NewObjectiveWizard');
const SimpleLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/SimpleLayout'); const NewNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/NewNoteWizard');
const FullScreenLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/FullScreenLayout'); const EditNoteWizard = () => import(/* webpackChunkName: "content-forms" */ '@/components/notes/EditNoteWizard');
const PublicLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/PublicLayout'); const EditClassNameWizard = () =>
const BlankLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/BlankLayout'); import(/* webpackChunkName: "content-forms" */ '@/components/school-class/EditClassNameWizard');
const SplitLayout = () => import(/* webpackChunkName: "layouts" */'@/layouts/SplitLayout'); const EditTeamNameWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/profile/EditTeamNameWizard');
const EditSnapshotTitleWizard = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/snapshots/EditSnapshotTitleWizard');
const DefaultLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/DefaultLayout');
const SimpleLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SimpleLayout');
const FullScreenLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/FullScreenLayout');
const PublicLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/PublicLayout');
const BlankLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/BlankLayout');
const SplitLayout = () => import(/* webpackChunkName: "layouts" */ '@/layouts/SplitLayout');
export default { export default {
name: 'App', name: 'App',
components: { components: {
ReadOnlyBanner, ReadOnlyBanner,
ScrollUp, ScrollUp,
DefaultLayout, DefaultLayout,
SimpleLayout, SimpleLayout,
FullScreenLayout, FullScreenLayout,
PublicLayout, PublicLayout,
BlankLayout, BlankLayout,
SplitLayout, SplitLayout,
NewContentBlockWizard, NewContentBlockWizard,
EditContentBlockWizard, EditContentBlockWizard,
EditRoomEntryWizard, EditRoomEntryWizard,
NewProjectEntryWizard, NewProjectEntryWizard,
EditProjectEntryWizard, EditProjectEntryWizard,
NewObjectiveWizard, NewObjectiveWizard,
NewNoteWizard, NewNoteWizard,
EditNoteWizard, EditNoteWizard,
EditClassNameWizard, EditClassNameWizard,
EditTeamNameWizard, EditTeamNameWizard,
EditSnapshotTitleWizard, EditSnapshotTitleWizard,
...modals ...modals,
},
computed: {
layout() {
return (this.$route.meta.layout || 'default') + '-layout';
}, },
...mapGetters({
computed: { showModalDeprecated: 'showModal', // don't use this any more todo: remove this
layout() { showMobileNavigation: 'showMobileNavigation',
return (this.$route.meta.layout || 'default') + '-layout'; }),
}, showModal() {
...mapGetters({ return this.$modal.state.component;
showModalDeprecated: 'showModal', // don't use this any more todo: remove this
showMobileNavigation: 'showMobileNavigation',
}),
showModal() {
return this.$modal.state.component;
},
}, },
}; },
};
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~styles/main.scss"; @import '~styles/main.scss';
@import "~styles/helpers"; @import '~styles/helpers';
body { body {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
height: 100vh; height: 100vh;
} }
.app { .app {
/*overflow-y: auto;*/ /*overflow-y: auto;*/
min-height: 100vh; min-height: 100vh;
/*for IE10+*/ /*for IE10+*/
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.no-scroll {
overflow-y: hidden;
}
.no-scroll {
overflow-y: hidden;
}
</style> </style>

View File

@ -1,104 +1,98 @@
<template> <template>
<div class="add-content"> <div class="add-content">
<a <a class="add-content__button" @click="addContent">
class="add-content__button"
@click="addContent"
>
<add-pointer class="add-content__icon" /> <add-pointer class="add-content__icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import { import { CREATE_CONTENT_BLOCK_AFTER_PAGE, CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE } from '@/router/module.names';
CREATE_CONTENT_BLOCK_AFTER_PAGE,
CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
} from '@/router/module.names';
const AddPointer = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddPointer'); const AddPointer = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddPointer');
export default { export default {
props: { props: {
where: { where: {
type: Object, type: Object,
validator(prop) { validator(prop) {
return Object.prototype.hasOwnProperty.call(prop, 'after' ) return (
|| Object.prototype.hasOwnProperty.call(prop, 'parent'); Object.prototype.hasOwnProperty.call(prop, 'after') || Object.prototype.hasOwnProperty.call(prop, 'parent')
} );
}, },
}, },
},
components: { components: {
AddPointer AddPointer,
},
computed: {
parent() {
return this.where.parent;
}, },
after() {
computed: { return this.where.after;
parent() {
return this.where.parent;
},
after() {
return this.where.after;
},
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
slug() {
return this.$route.params.slug;
}
}, },
isObjectiveGroup() {
return this.parent && this.parent.__typename === 'ObjectiveGroupNode';
},
slug() {
return this.$route.params.slug;
},
},
methods: {
methods: { addContent() {
addContent() { if (this.isObjectiveGroup) {
if (this.isObjectiveGroup) { this.$modal.open('new-objective-wizard', { parent: this.parent.id });
this.$modal.open('new-objective-wizard', {parent: this.parent.id}); } else {
let route;
if (this.after && this.after.id) {
route = {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE,
params: {
after: this.after.id,
slug: this.slug,
},
};
} else { } else {
let route; route = {
if (this.after && this.after.id) { name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
route = { params: {
name: CREATE_CONTENT_BLOCK_AFTER_PAGE, parent: this.parent.id,
params: { },
after: this.after.id, };
slug: this.slug
}
};
} else {
route = {
name: CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
params: {
parent: this.parent.id
}
};
}
this.$router.push(route);
} }
this.$router.push(route);
} }
} },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.add-content { .add-content {
display: none; display: none;
position: relative; position: relative;
@include desktop { @include desktop {
display: flex; display: flex;
}
z-index: 1;
justify-content: flex-end;
&__button {
margin-right: -85px;
cursor: pointer;
display: inline-grid;
}
&__icon {
width: 40px;
fill: $color-silver-dark;
}
} }
z-index: 1;
justify-content: flex-end;
&__button {
margin-right: -85px;
cursor: pointer;
display: inline-grid;
}
&__icon {
width: 40px;
fill: $color-silver-dark;
}
}
</style> </style>

View File

@ -1,41 +1,38 @@
<template> <template>
<div <div class="add-content-element" @click="$emit('add-element', index)">
class="add-content-element"
@click="$emit('add-element', index)"
>
<add-icon class="add-content-element__icon" /> <add-icon class="add-content-element__icon" />
</div> </div>
</template> </template>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon'); const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon');
export default { export default {
props: ['index'], props: ['index'],
components: { components: {
AddIcon AddIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.add-content-element { .add-content-element {
display: flex; display: flex;
justify-content: center; justify-content: center;
border-bottom: 2px solid $color-silver-dark; border-bottom: 2px solid $color-silver-dark;
margin-bottom: 21px + 25px; margin-bottom: 21px + 25px;
cursor: pointer; cursor: pointer;
&__icon { &__icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
fill: $color-silver-dark; fill: $color-silver-dark;
margin-bottom: -21px; margin-bottom: -21px;
background-color: $color-white; background-color: $color-white;
border-radius: 50px; border-radius: 50px;
}
} }
}
</style> </style>

View File

@ -11,68 +11,71 @@
</template> </template>
<script> <script>
const AddIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/AddIcon.vue'); const AddIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/AddIcon.vue');
export default { export default {
props: { props: {
route: { route: {
type: String, type: String,
default: null default: null,
},
reverse: { // use reverse colors
type: Boolean,
default: false
},
click: {
type: Function,
default: null
}
}, },
reverse: {
// use reverse colors
type: Boolean,
default: false,
},
click: {
type: Function,
default: null,
},
},
components: { components: {
AddIcon AddIcon,
}, },
computed: { computed: {
component() { component() {
// only use the router link if the route prop is provided, otherwise render a normal anchor tag // only use the router link if the route prop is provided, otherwise render a normal anchor tag
return this.route ? 'router-link' : 'a'; return this.route ? 'router-link' : 'a';
},
properties() {
return this.route ? {
to: this.route,
tag: 'div'
} : {};
}
}, },
}; properties() {
return this.route
? {
to: this.route,
tag: 'div',
}
: {};
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.add-widget { .add-widget {
display: none; display: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@include widget-shadow; @include widget-shadow;
cursor: pointer; cursor: pointer;
@include desktop { @include desktop {
display: flex; display: flex;
}
&__add {
width: 80px;
fill: $color-silver-dark;
}
&--reverse {
@include widget-shadow-reverse;
}
&--reverse &__add {
fill: white;
}
} }
&__add {
width: 80px;
fill: $color-silver-dark;
}
&--reverse {
@include widget-shadow-reverse;
}
&--reverse &__add {
fill: white;
}
}
</style> </style>

View File

@ -1,53 +1,31 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="assignment-with-submissions"> <div class="assignment-with-submissions">
<p <p class="assignment-with-submissions__text" data-cy="assignment-main-text" v-html="assignment.assignment" />
class="assignment-with-submissions__text"
data-cy="assignment-main-text"
v-html="assignment.assignment"
/>
<div> <div>
<a <a class="button button--primary submissions-page__back" @click="$emit('back')"
class="button button--primary submissions-page__back" >Aufgabe im {{ $flavor.textModule }} anzeigen</a
@click="$emit('back')" >
>Aufgabe im {{ $flavor.textModule }} anzeigen</a>
</div> </div>
<div <div class="assignment-with-submissions__solution" v-if="assignment.solution">
class="assignment-with-submissions__solution" <h4 class="assignment-with-submissions__heading">Lösung</h4>
v-if="assignment.solution"
>
<h4 class="assignment-with-submissions__heading">
Lösung
</h4>
<p <p
class="assignment-with-submissions__solution-text" class="assignment-with-submissions__solution-text"
data-cy="assignment-solution" data-cy="assignment-solution"
v-html="assignment.solution" v-html="assignment.solution"
/> />
</div> </div>
<p <p class="assignment-with-submissions__no-submissions" v-if="!assignment.submissions.length">
class="assignment-with-submissions__no-submissions"
v-if="!assignment.submissions.length"
>
Zu diesem Auftrag sind noch keine Ergebnisse vorhanden. Zu diesem Auftrag sind noch keine Ergebnisse vorhanden.
</p> </p>
<div <div class="assignment-with-submissions__submissions submissions" v-if="assignment.submissions.length">
class="assignment-with-submissions__submissions submissions"
v-if="assignment.submissions.length"
>
<div class="submissions__header student-submission-row submission-header"> <div class="submissions__header student-submission-row submission-header">
<p class="submission-header__title"> <p class="submission-header__title">Lernende</p>
Lernende <p class="submission-header__title">Ergebnisse</p>
</p> <p class="submission-header__title">Feedback</p>
<p class="submission-header__title">
Ergebnisse
</p>
<p class="submission-header__title">
Feedback
</p>
</div> </div>
<router-link <router-link
:to="submissionLink(submission)" :to="submissionLink(submission)"
@ -55,116 +33,110 @@
v-for="submission in submissions" v-for="submission in submissions"
:key="submission.id" :key="submission.id"
> >
<student-submission <student-submission :submission="submission" class="assignment-with-submissions__submission" />
:submission="submission"
class="assignment-with-submissions__submission"
/>
</router-link> </router-link>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import StudentSubmission from '@/components/StudentSubmission'; import StudentSubmission from '@/components/StudentSubmission';
import { meQuery } from '@/graphql/queries'; import { meQuery } from '@/graphql/queries';
export default { export default {
props: ['assignment'], props: ['assignment'],
components: { components: {
StudentSubmission StudentSubmission,
},
data() {
return {
me: {},
};
},
computed: {
submissions() {
return this.assignment.submissions.filter((submission) => {
return this.belongsToSchool(submission);
});
}, },
currentFilter() {
data() { return this.me.selectedClass;
return {
me: {}
};
}, },
},
computed: { methods: {
submissions() { submissionLink(submission) {
return this.assignment.submissions.filter(submission => { return `/submission/${submission.id}`;
return this.belongsToSchool(submission);
});
},
currentFilter() {
return this.me.selectedClass;
},
}, },
belongsToSchool(submission) {
methods: { if (this.currentFilter.id === '') {
submissionLink(submission) { return true;
return `/submission/${submission.id}`;
},
belongsToSchool(submission) {
if (this.currentFilter.id === '') {
return true;
}
return submission.student.schoolClasses.some(schoolClass => schoolClass .id === this.currentFilter.id);
} }
return submission.student.schoolClasses.some((schoolClass) => schoolClass.id === this.currentFilter.id);
}, },
},
apollo: { apollo: {
me: meQuery me: meQuery,
} },
};
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.assignment-with-submissions {
&__title {
font-size: toRem(35px);
}
&__text {
font-size: toRem(26px);
margin-bottom: 1rem;
}
&__solution {
margin-bottom: 1rem;
}
&__heading {
font-size: toRem(17px);
font-weight: 800;
margin-bottom: 1rem;
}
&__link {
display: block;
}
&__submissions {
margin-top: 3rem;
}
&__no-submissions {
margin-top: $large-spacing;
}
:deep(ul) {
@include list-parent;
}
:deep(li) {
@include list-child;
}
.assignment-with-submissions {
&__title {
font-size: toRem(35px);
} }
.submissions { &__text {
width: 100%; font-size: toRem(26px);
margin-bottom: 1rem;
} }
.submission-header { &__solution {
&__title { margin-bottom: 1rem;
color: $color-silver-dark;
font-family: $sans-serif-font-family;
}
} }
&__heading {
font-size: toRem(17px);
font-weight: 800;
margin-bottom: 1rem;
}
&__link {
display: block;
}
&__submissions {
margin-top: 3rem;
}
&__no-submissions {
margin-top: $large-spacing;
}
:deep(ul) {
@include list-parent;
}
:deep(li) {
@include list-child;
}
}
.submissions {
width: 100%;
}
.submission-header {
&__title {
color: $color-silver-dark;
font-family: $sans-serif-font-family;
}
}
</style> </style>

View File

@ -1,72 +1,68 @@
<template> <template>
<router-link <router-link :to="to" data-cy="back-link" class="sub-navigation-item back-link">
:to="to"
data-cy="back-link"
class="sub-navigation-item back-link"
>
<chevron-left class="back-link__icon sub-navigation-item__icon" /> <chevron-left class="back-link__icon sub-navigation-item__icon" />
{{ fullTitle }} {{ fullTitle }}
</router-link> </router-link>
</template> </template>
<script> <script>
import { MODULE_PAGE } from '@/router/module.names'; import { MODULE_PAGE } from '@/router/module.names';
import { ROOMS_PAGE } from '@/router/room.names'; import { ROOMS_PAGE } from '@/router/room.names';
import { PROJECTS_PAGE } from '@/router/portfolio.names'; import { PROJECTS_PAGE } from '@/router/portfolio.names';
const ChevronLeft = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronLeft'); const ChevronLeft = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronLeft');
export default { export default {
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: '',
},
type: {
type: String,
default: 'topic',
},
slug: {
type: String,
default: '',
},
}, },
type: {
components: { type: String,
ChevronLeft, default: 'topic',
}, },
slug: {
computed: { type: String,
to() { default: '',
switch (this.type) {
case 'topic':
return {name: 'topic', params: {topicSlug: this.slug}};
case 'module':
return {name: MODULE_PAGE};
case 'portfolio':
return {name: PROJECTS_PAGE};
default:
return {name: ROOMS_PAGE};
}
},
fullTitle() {
switch (this.type) {
case 'topic':
return `${this.$flavor.textTopic}: ${this.title}`;
case 'module':
return `${this.$flavor.textModule}: ${this.title}`;
default:
return this.title;
}
},
}, },
}; },
components: {
ChevronLeft,
},
computed: {
to() {
switch (this.type) {
case 'topic':
return { name: 'topic', params: { topicSlug: this.slug } };
case 'module':
return { name: MODULE_PAGE };
case 'portfolio':
return { name: PROJECTS_PAGE };
default:
return { name: ROOMS_PAGE };
}
},
fullTitle() {
switch (this.type) {
case 'topic':
return `${this.$flavor.textTopic}: ${this.title}`;
case 'module':
return `${this.$flavor.textModule}: ${this.title}`;
default:
return this.title;
}
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.back-link { .back-link {
@include regular-text; @include regular-text;
} }
</style> </style>

View File

@ -1,26 +1,12 @@
<template> <template>
<div <div :data-scrollto="chapter.id" class="chapter" data-cy="chapter">
:data-scrollto="chapter.id" <div :class="{ 'hideable-element--greyed-out': titleGreyedOut }" class="hideable-element" v-if="!titleHidden">
class="chapter" <h3 :id="'chapter-' + index">
data-cy="chapter"
>
<div
:class="{'hideable-element--greyed-out': titleGreyedOut}"
class="hideable-element"
v-if="!titleHidden"
>
<h3
:id="'chapter-' + index"
>
{{ chapter.title }} {{ chapter.title }}
</h3> </h3>
</div> </div>
<visibility-action <visibility-action :block="chapter" type="chapter-title" v-if="editMode" />
:block="chapter"
type="chapter-title"
v-if="editMode"
/>
<bookmark-actions <bookmark-actions
:bookmarked="!!chapter.bookmark" :bookmarked="!!chapter.bookmark"
@ -33,27 +19,17 @@
@bookmark="bookmark(!chapter.bookmark)" @bookmark="bookmark(!chapter.bookmark)"
/> />
<div <div
:class="{'hideable-element--greyed-out': descriptionGreyedOut}" :class="{ 'hideable-element--greyed-out': descriptionGreyedOut }"
class="chapter__intro intro hideable-element" class="chapter__intro intro hideable-element"
v-if="!descriptionHidden" v-if="!descriptionHidden"
> >
<visibility-action <visibility-action :block="chapter" :chapter="true" type="chapter-description" v-if="editMode" />
:block="chapter" <p class="chapter__description">
:chapter="true"
type="chapter-description"
v-if="editMode"
/>
<p
class="chapter__description"
>
{{ chapter.description }} {{ chapter.description }}
</p> </p>
</div> </div>
<add-content-button <add-content-button :where="{ parent: chapter }" v-if="editMode" />
:where="{parent: chapter}"
v-if="editMode"
/>
<content-block <content-block
:content-block="contentBlock" :content-block="contentBlock"
@ -66,177 +42,179 @@
</template> </template>
<script> <script>
import ContentBlock from '@/components/ContentBlock'; import ContentBlock from '@/components/ContentBlock';
import AddContentButton from '@/components/AddContentButton'; import AddContentButton from '@/components/AddContentButton';
import BookmarkActions from '@/components/notes/BookmarkActions'; import BookmarkActions from '@/components/notes/BookmarkActions';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import {hidden} from '@/helpers/visibility'; import { hidden } from '@/helpers/visibility';
import {CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE} from '@/consts/types'; import { CHAPTER_DESCRIPTION_TYPE, CHAPTER_TITLE_TYPE, CONTENT_TYPE } from '@/consts/types';
import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql'; import UPDATE_CHAPTER_BOOKMARK_MUTATION from '@/graphql/gql/mutations/updateChapterBookmark.gql';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import me from '@/mixins/me'; import me from '@/mixins/me';
export default { export default {
props: { props: {
chapter: { chapter: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
index: { index: {
type: Number, type: Number,
default: 0 default: 0,
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false default: false,
},
},
mixins: [me],
components: {
BookmarkActions,
VisibilityAction,
ContentBlock,
AddContentButton,
},
computed: {
filteredContentBlocks() {
if (!(this.chapter && this.chapter.contentBlocks)) {
return [];
} }
if (this.editMode) {
return this.chapter.contentBlocks;
}
return this.chapter.contentBlocks.filter(
(contentBlock) =>
!hidden({
block: contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE,
})
);
}, },
note() {
mixins: [me], if (this.chapter && this.chapter.bookmark) {
return this.chapter.bookmark.note;
components: { }
BookmarkActions, return false;
VisibilityAction,
ContentBlock,
AddContentButton,
}, },
titleGreyedOut() {
computed: { return this.textHidden(CHAPTER_TITLE_TYPE) && this.editMode;
filteredContentBlocks() {
if (!(this.chapter && this.chapter.contentBlocks)) {
return [];
}
if (this.editMode) {
return this.chapter.contentBlocks;
}
return this.chapter.contentBlocks.filter(contentBlock => !hidden({
block: contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE,
}));
},
note() {
if (this.chapter && this.chapter.bookmark) {
return this.chapter.bookmark.note;
}
return false;
},
titleGreyedOut() {
return this.textHidden(CHAPTER_TITLE_TYPE) && this.editMode;
},
// never hidden when editing the module
titleHidden() {
if (this.chapter.titleHidden === true) {
return true;
}
return this.textHidden(CHAPTER_TITLE_TYPE) && !this.editMode;
},
descriptionGreyedOut() {
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && this.editMode;
},
// never hidden when editing the module
descriptionHidden() {
if (this.chapter.descriptionHidden === true) {
return true;
}
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && !this.editMode;
},
}, },
// never hidden when editing the module
titleHidden() {
if (this.chapter.titleHidden === true) {
return true;
}
return this.textHidden(CHAPTER_TITLE_TYPE) && !this.editMode;
},
descriptionGreyedOut() {
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && this.editMode;
},
// never hidden when editing the module
descriptionHidden() {
if (this.chapter.descriptionHidden === true) {
return true;
}
return this.textHidden(CHAPTER_DESCRIPTION_TYPE) && !this.editMode;
},
},
methods: { methods: {
bookmark(bookmarked) { bookmark(bookmarked) {
const id = this.chapter.id; const id = this.chapter.id;
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_CHAPTER_BOOKMARK_MUTATION, mutation: UPDATE_CHAPTER_BOOKMARK_MUTATION,
variables: { variables: {
input: { input: {
chapter: id, chapter: id,
bookmarked, bookmarked,
},
}, },
update: (store) => { },
const query = CHAPTER_QUERY; update: (store) => {
const variables = {id}; const query = CHAPTER_QUERY;
const {chapter} = store.readQuery({ const variables = { id };
query, const { chapter } = store.readQuery({
variables, query,
}); variables,
});
let bookmark; let bookmark;
if (bookmarked) { if (bookmarked) {
bookmark = { bookmark = {
__typename: 'ChapterBookmarkNode', __typename: 'ChapterBookmarkNode',
note: null, note: null,
};
} else {
bookmark = null;
}
const data = {
chapter: {
...chapter,
bookmark
}
}; };
} else {
bookmark = null;
}
store.writeQuery({ const data = {
data, chapter: {
query, ...chapter,
variables, bookmark,
});
},
optimisticResponse: {
__typename: 'Mutation',
updateChapterBookmark: {
__typename: 'UpdateChapterBookmarkPayload',
success: true,
}, },
}, };
});
},
addNote(id) {
this.$store.dispatch('addNote', {
content: id,
parent: this.chapter.id,
});
},
editNote() {
this.$store.dispatch('editNote', this.chapter.bookmark.note);
},
textHidden(type) {
return hidden({
block: this.chapter,
schoolClass: this.schoolClass,
type,
});
},
},
}; store.writeQuery({
data,
query,
variables,
});
},
optimisticResponse: {
__typename: 'Mutation',
updateChapterBookmark: {
__typename: 'UpdateChapterBookmarkPayload',
success: true,
},
},
});
},
addNote(id) {
this.$store.dispatch('addNote', {
content: id,
parent: this.chapter.id,
});
},
editNote() {
this.$store.dispatch('editNote', this.chapter.bookmark.note);
},
textHidden(type) {
return hidden({
block: this.chapter,
schoolClass: this.schoolClass,
type,
});
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.chapter { .chapter {
position: relative; position: relative;
&__bookmark-actions { &__bookmark-actions {
margin-top: 3px; margin-top: 3px;
}
&__intro {
position: relative;
}
&__description {
@include lead-paragraph;
margin-bottom: $large-spacing;
}
} }
&__intro {
position: relative;
}
&__description {
@include lead-paragraph;
margin-bottom: $large-spacing;
}
}
</style> </style>

View File

@ -1,92 +1,86 @@
<template> <template>
<div class="color-chooser"> <div class="color-chooser">
<div <div
:class="{'color-chooser__color-wrapper--selected': selectedColor === color.name}" :class="{ 'color-chooser__color-wrapper--selected': selectedColor === color.name }"
class="color-chooser__color-wrapper" class="color-chooser__color-wrapper"
data-cy="color-select" data-cy="color-select"
v-for="(color, index) in colors" v-for="(color, index) in colors"
:key="index" :key="index"
@click="$emit('input', color.name)" @click="$emit('input', color.name)"
> >
<div <div :class="'color-chooser__color--' + color.name" class="color-chooser__color">
:class="'color-chooser__color--' + color.name" <tick class="color-chooser__selected-icon" v-if="selectedColor === color.name" />
class="color-chooser__color"
>
<tick
class="color-chooser__selected-icon"
v-if="selectedColor === color.name"
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const Tick = () => import(/* webpackChunkName: "icons" */'@/components/icons/Tick'); const Tick = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Tick');
export default { export default {
props: ['selectedColor'], props: ['selectedColor'],
components: { components: {
Tick Tick,
}, },
data() { data() {
return { return {
colors: [ colors: [
{ {
name: 'yellow' name: 'yellow',
}, },
{ {
name: 'blue' name: 'blue',
}, },
{ {
name: 'red' name: 'red',
}, },
{ {
name: 'green' name: 'green',
} },
] ],
}; };
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.color-chooser { .color-chooser {
display: flex; display: flex;
&__color-wrapper { &__color-wrapper {
margin-right: 10px; margin-right: 10px;
border-radius: 50px; border-radius: 50px;
padding: 10px; padding: 10px;
&--selected { &--selected {
border: 1px solid $color-charcoal-dark; border: 1px solid $color-charcoal-dark;
}
}
&__selected-icon {
width: 17px;
fill: $color-charcoal-dark;
}
&__color {
width: 46px;
height: 46px;
border-radius: 23px;
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid
}
justify-items: center;
align-items: center;
@include skillbox-colors;
} }
} }
&__selected-icon {
width: 17px;
fill: $color-charcoal-dark;
}
&__color {
width: 46px;
height: 46px;
border-radius: 23px;
display: flex;
justify-content: center;
@supports (display: grid) {
display: grid;
}
justify-items: center;
align-items: center;
@include skillbox-colors;
}
}
</style> </style>

View File

@ -1,38 +1,20 @@
<template> <template>
<div <div
:class="{'hideable-element--greyed-out': hidden}" :class="{ 'hideable-element--greyed-out': hidden }"
class="content-block__container hideable-element content-list__parent" class="content-block__container hideable-element content-list__parent"
> >
<div <div :class="specialClass" :style="instrumentStyle" class="content-block" data-cy="content-block">
:class="specialClass" <div class="block-actions" v-if="canEditModule && !isInstrumentBlock">
:style="instrumentStyle" <user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget" v-if="isMine" />
class="content-block"
data-cy="content-block"
>
<div
class="block-actions"
v-if="canEditModule && !isInstrumentBlock"
>
<user-widget
v-bind="me"
class="block-actions__user-widget content-block__user-widget"
v-if="isMine"
/>
<more-options-widget> <more-options-widget>
<li <li class="popover-links__link" v-if="!isInstrumentBlock">
class="popover-links__link"
v-if="!isInstrumentBlock"
>
<popover-link <popover-link
data-cy="duplicate-content-block-link" data-cy="duplicate-content-block-link"
text="Duplizieren" text="Duplizieren"
@link-action="duplicateContentBlock(contentBlock)" @link-action="duplicateContentBlock(contentBlock)"
/> />
</li> </li>
<li <li class="popover-links__link" v-if="isMine">
class="popover-links__link"
v-if="isMine"
>
<popover-link <popover-link
data-cy="delete-content-block-link" data-cy="delete-content-block-link"
text="Löschen" text="Löschen"
@ -40,22 +22,13 @@
/> />
</li> </li>
<li <li class="popover-links__link" v-if="isMine">
class="popover-links__link" <popover-link text="Bearbeiten" @link-action="editContentBlock(contentBlock)" />
v-if="isMine"
>
<popover-link
text="Bearbeiten"
@link-action="editContentBlock(contentBlock)"
/>
</li> </li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility"> <div class="content-block__visibility">
<visibility-action <visibility-action :block="contentBlock" v-if="canEditModule" />
:block="contentBlock"
v-if="canEditModule"
/>
</div> </div>
<h3 <h3
@ -66,10 +39,7 @@
> >
{{ instrumentLabel }} {{ instrumentLabel }}
</h3> </h3>
<h4 <h4 class="content-block__title" v-if="!contentBlock.indent">
class="content-block__title"
v-if="!contentBlock.indent"
>
{{ contentBlock.title }} {{ contentBlock.title }}
</h4> </h4>
@ -85,109 +55,107 @@
/> />
</div> </div>
<add-content-button <add-content-button :where="{ after: contentBlock }" v-if="canEditModule" />
:where="{after: contentBlock}"
v-if="canEditModule"
/>
</div> </div>
</template> </template>
<script> <script>
import AddContentButton from '@/components/AddContentButton'; import AddContentButton from '@/components/AddContentButton';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql'; import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql';
import me from '@/mixins/me'; import me from '@/mixins/me';
import {hidden} from '@/helpers/visibility'; import { hidden } from '@/helpers/visibility';
import {CONTENT_TYPE} from '@/consts/types'; import { CONTENT_TYPE } from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink'; import PopoverLink from '@/components/ui/PopoverLink';
import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations'; import { insertAtIndex, removeAtIndex } from '@/graphql/immutable-operations';
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names'; import { EDIT_CONTENT_BLOCK_PAGE } from '@/router/module.names';
import {instrumentCategory} from '@/helpers/instrumentType'; import { instrumentCategory } from '@/helpers/instrumentType';
const ContentComponent = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'); const ContentComponent = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent');
export default {
export default { name: 'ContentBlock',
name: 'ContentBlock', props: {
props: { contentBlock: {
contentBlock: { type: Object,
type: Object, default: () => ({}),
default: () => ({}),
},
parent: {
type: Object,
default: () => ({}),
},
editMode: {
type: Boolean,
default: true,
},
}, },
parent: {
mixins: [me], type: Object,
default: () => ({}),
components: {
PopoverLink,
ContentComponent,
AddContentButton,
VisibilityAction,
MoreOptionsWidget,
UserWidget,
}, },
editMode: {
type: Boolean,
default: true,
},
},
computed: { mixins: [me],
canEditModule() {
return !this.contentBlock.indent && this.editMode; components: {
}, PopoverLink,
specialClass() { ContentComponent,
return `content-block--${this.contentBlock.type.toLowerCase()}`; AddContentButton,
}, VisibilityAction,
isInstrumentBlock() { MoreOptionsWidget,
return !!this.contentBlock.instrumentCategory; UserWidget,
}, },
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
instrumentStyle() { computed: {
if (this.isInstrumentBlock) { canEditModule() {
return { return !this.contentBlock.indent && this.editMode;
backgroundColor: this.contentBlock.instrumentCategory.background },
}; specialClass() {
} return `content-block--${this.contentBlock.type.toLowerCase()}`;
return {}; },
}, isInstrumentBlock() {
instrumentLabel() { return !!this.contentBlock.instrumentCategory;
const contentType = this.contentBlock.type.toLowerCase(); },
if (contentType.startsWith('base')) { // all legacy instruments start with `base` // todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
return instrumentCategory(contentType); instrumentStyle() {
} if (this.isInstrumentBlock) {
if (this.isInstrumentBlock) { return {
return instrumentCategory(this.contentBlock.instrumentCategory.name); backgroundColor: this.contentBlock.instrumentCategory.background,
} };
return ''; }
}, return {};
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css },
instrumentLabelStyle() { instrumentLabel() {
if (this.isInstrumentBlock) { const contentType = this.contentBlock.type.toLowerCase();
return { if (contentType.startsWith('base')) {
color: this.contentBlock.instrumentCategory.foreground // all legacy instruments start with `base`
}; return instrumentCategory(contentType);
} }
return {}; if (this.isInstrumentBlock) {
}, return instrumentCategory(this.contentBlock.instrumentCategory.name);
canEditContentBlock() { }
return this.isMine && !this.contentBlock.indent; return '';
}, },
isMine() { // todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
return this.contentBlock.mine; instrumentLabelStyle() {
}, if (this.isInstrumentBlock) {
contentBlocksWithContentLists() { return {
/* color: this.contentBlock.instrumentCategory.foreground,
};
}
return {};
},
canEditContentBlock() {
return this.isMine && !this.contentBlock.indent;
},
isMine() {
return this.contentBlock.mine;
},
contentBlocksWithContentLists() {
/*
collects all content_list_items in content_lists: collects all content_list_items in content_lists:
{ {
text_block, text_block,
@ -201,221 +169,238 @@
text_block text_block
} }
*/ */
let contentList = []; let contentList = [];
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => { let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
// collect content_list_items // collect content_list_items
if (content.type === 'content_list_item') { if (content.type === 'content_list_item') {
contentList = [...contentList, content]; contentList = [...contentList, content];
if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array if (index === this.contentBlock.contents.length - 1) {
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)]; // content is last element of contents array
return updatedContent; let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)];
} return updatedContent;
}
return newContents;
} else {
// handle all other items and reset current content_list if necessary
if (contentList.length !== 0) {
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
contentList = [];
return newContents; return newContents;
} else { } else {
// handle all other items and reset current content_list if necessary return [...newContents, content];
if (contentList.length !== 0) {
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
contentList = [];
return newContents;
} else {
return [...newContents, content];
}
} }
}, []); }
return Object.assign({}, this.contentBlock, { }, []);
contents: newContent, return Object.assign({}, this.contentBlock, {
}); contents: newContent,
}, });
hidden() {
return hidden({
block: this.contentBlock,
schoolClass: this.schoolClass,
type: CONTENT_TYPE,
});
},
root() {
// we need the root content block id, not the generated content block if inside a content list block
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
},
}, },
methods: { hidden() {
duplicateContentBlock({id}) { return hidden({
const parent = this.parent; block: this.contentBlock,
this.$apollo.mutate({ schoolClass: this.schoolClass,
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION, type: CONTENT_TYPE,
variables: { });
input: { },
id, root() {
// we need the root content block id, not the generated content block if inside a content list block
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
},
},
methods: {
duplicateContentBlock({ id }) {
const parent = this.parent;
this.$apollo.mutate({
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
},
},
update(
store,
{
data: {
duplicateContentBlock: { contentBlock },
}, },
}
) {
if (contentBlock) {
const query = CHAPTER_QUERY;
const variables = {
id: parent.id,
};
const { chapter } = store.readQuery({ query, variables });
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
const data = {
chapter: {
...chapter,
contentBlocks,
},
};
store.writeQuery({ query, variables, data });
}
},
});
},
editContentBlock(contentBlock) {
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
params: {
id: contentBlock.id,
},
};
this.$router.push(route);
},
deleteContentBlock(contentBlock) {
this.$modal
.open('confirm')
.then(() => {
this.doDeleteContentBlock(contentBlock);
})
.catch();
},
doDeleteContentBlock(contentBlock) {
const parent = this.parent;
const id = contentBlock.id;
this.$apollo.mutate({
mutation: DELETE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
}, },
update(store, {data: {duplicateContentBlock: {contentBlock}}}) { },
if (contentBlock) { update(
const query = CHAPTER_QUERY; store,
const variables = { {
id: parent.id, data: {
}; deleteContentBlock: { success },
const {chapter} = store.readQuery({query, variables});
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
const data = {
chapter: {
...chapter,
contentBlocks,
},
};
store.writeQuery({query, variables, data});
}
},
});
},
editContentBlock(contentBlock) {
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
params: {
id: contentBlock.id,
},
};
this.$router.push(route);
},
deleteContentBlock(contentBlock) {
this.$modal.open('confirm').then(() => {
this.doDeleteContentBlock(contentBlock);
})
.catch();
},
doDeleteContentBlock(contentBlock) {
const parent = this.parent;
const id = contentBlock.id;
this.$apollo.mutate({
mutation: DELETE_CONTENT_BLOCK_MUTATION,
variables: {
input: {
id,
}, },
}, }
update(store, {data: {deleteContentBlock: {success}}}) { ) {
if (success) { if (success) {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = { const variables = {
id: parent.id, id: parent.id,
}; };
const {chapter} = store.readQuery({query, variables}); const { chapter } = store.readQuery({ query, variables });
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id); const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
const contentBlocks = removeAtIndex(chapter.contentBlocks, index); const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
const data = { const data = {
chapter: { chapter: {
...chapter, ...chapter,
contentBlocks, contentBlocks,
}, },
}; };
store.writeQuery({query, variables, data}); store.writeQuery({ query, variables, data });
} }
}, },
}); });
}, },
createContentListOrBlocks(contentList) { createContentListOrBlocks(contentList) {
return [{ return [
{
type: 'content_list', type: 'content_list',
contents: contentList, contents: contentList,
id: contentList[0].id, id: contentList[0].id,
}]; },
}, ];
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.content-block { .content-block {
margin-bottom: $section-spacing; margin-bottom: $section-spacing;
position: relative;
&__container {
position: relative; position: relative;
}
&__container { &__title {
position: relative; line-height: 1.5;
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
}
&__instrument-label {
margin-bottom: $medium-spacing;
@include regular-text();
}
&__action-button {
cursor: pointer;
}
&__user-widget {
margin-right: 0;
}
&--base_communication {
@include content-box($color-accent-1-list);
.content-block__instrument-label {
color: $color-accent-1-dark;
} }
}
&__title { &--task {
line-height: 1.5; @include light-border(bottom);
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
}
&__instrument-label { .content-block__title {
margin-bottom: $medium-spacing; color: $color-brand;
@include regular-text(); margin-top: $default-padding;
} margin-bottom: $large-spacing;
&__action-button {
cursor: pointer;
}
&__user-widget {
margin-right: 0;
}
&--base_communication {
@include content-box($color-accent-1-list);
.content-block__instrument-label {
color: $color-accent-1-dark;
}
}
&--task {
@include light-border(bottom); @include light-border(bottom);
.content-block__title { @include desktop {
color: $color-brand; margin-top: 0;
margin-top: $default-padding;
margin-bottom: $large-spacing;
@include light-border(bottom);
@include desktop {
margin-top: 0;
}
} }
} }
&--base_society {
@include content-box($color-accent-2-list);
.content-block__instrument-label {
color: $color-accent-2-dark;
}
}
&--base_interdisciplinary {
@include content-box($color-accent-4-list);
.content-block__instrument-label {
color: $color-accent-4-dark;
}
}
&--instrument {
@include content-box-base;
}
:deep(p) {
line-height: 1.5;
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
:deep(.text-block) {
ul {
@include list-parent;
}
li {
@include list-child;
line-height: 1.5;
}
}
} }
&--base_society {
@include content-box($color-accent-2-list);
.content-block__instrument-label {
color: $color-accent-2-dark;
}
}
&--base_interdisciplinary {
@include content-box($color-accent-4-list);
.content-block__instrument-label {
color: $color-accent-4-dark;
}
}
&--instrument {
@include content-box-base;
}
:deep(p) {
line-height: 1.5;
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
:deep(.text-block) {
ul {
@include list-parent;
}
li {
@include list-child;
line-height: 1.5;
}
}
}
</style> </style>

View File

@ -1,38 +1,31 @@
<template> <template>
<modal <modal :hide-header="true" :fullscreen="true" class="fullscreen-image">
:hide-header="true" <img :src="imageUrl" class="fullscreen-image__image" />
:fullscreen="true"
class="fullscreen-image"
>
<img
:src="imageUrl"
class="fullscreen-image__image"
>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
export default { export default {
components: { components: {
Modal Modal,
},
computed: {
imageUrl() {
return this.$store.state.imageUrl;
}, },
},
computed: { };
imageUrl() {
return this.$store.state.imageUrl;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.fullscreen-image { .fullscreen-image {
&__image { &__image {
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
vertical-align: bottom; vertical-align: bottom;
}
} }
}
</style> </style>

View File

@ -1,36 +1,35 @@
<template> <template>
<modal :fullscreen="true"> <modal :fullscreen="true">
<component <component :value="value" :is="type" />
:value="value"
:is="type"
/>
</modal> </modal>
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
const InfogramBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock'); const InfogramBlock = () =>
const GeniallyBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InfogramBlock');
const GeniallyBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/GeniallyBlock');
export default { export default {
components: { components: {
Modal, Modal,
InfogramBlock, InfogramBlock,
GeniallyBlock GeniallyBlock,
},
computed: {
id() {
return this.$store.state.infographic.id;
}, },
type() {
computed: { return this.$store.state.infographic.type;
id() { },
return this.$store.state.infographic.id; value() {
}, return {
type() { id: this.id,
return this.$store.state.infographic.type; };
}, },
value() { },
return { };
id: this.id
};
}
}
};
</script> </script>

View File

@ -1,9 +1,5 @@
<template> <template>
<modal <modal :hide-header="true" :fullscreen="true" class="fullscreen-video">
:hide-header="true"
:fullscreen="true"
class="fullscreen-video"
>
<iframe <iframe
:src="src" :src="src"
width="2000" width="2000"
@ -18,31 +14,31 @@
</template> </template>
<script> <script>
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
export default { export default {
components: { components: {
Modal Modal,
},
computed: {
vimeoId() {
return this.$store.state.vimeoId;
}, },
src() {
computed: { return `https://player.vimeo.com/video/${this.vimeoId}`;
vimeoId() { },
return this.$store.state.vimeoId; },
}, };
src() {
return `https://player.vimeo.com/video/${this.vimeoId}`;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.fullscreen-video { .fullscreen-video {
&__embed { &__embed {
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
height: 95vh; height: 95vh;
vertical-align: bottom; vertical-align: bottom;
}
} }
}
</style> </style>

View File

@ -1,139 +1,126 @@
<template> <template>
<header class="header-bar"> <header class="header-bar">
<a <a class="header-bar__sidebar-link" data-cy="open-sidebar-link" @click.stop="openSidebar('navigation')">
class="header-bar__sidebar-link"
data-cy="open-sidebar-link"
@click.stop="openSidebar('navigation')"
>
<hamburger class="header-bar__sidebar-icon" /> <hamburger class="header-bar__sidebar-icon" />
</a> </a>
<content-navigation class="header-bar__content-navigation" /> <content-navigation class="header-bar__content-navigation" />
<div class="user-header"> <div class="user-header">
<a <a class="user-header__sidebar-link">
class="user-header__sidebar-link" <current-class class="user-header__current-class" @click.native.stop="openSidebar('profile')" />
>
<current-class
class="user-header__current-class"
@click.native.stop="openSidebar('profile')"
/>
</a> </a>
<user-widget <user-widget v-bind="me" data-cy="header-user-widget" @click.native.stop="openSidebar('profile')" />
v-bind="me"
data-cy="header-user-widget"
@click.native.stop="openSidebar('profile')"
/>
</div> </div>
</header> </header>
</template> </template>
<script> <script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue'; import ContentNavigation from '@/components/book-navigation/ContentNavigation.vue';
import UserWidget from '@/components/UserWidget.vue'; import UserWidget from '@/components/UserWidget.vue';
import CurrentClass from '@/components/school-class/CurrentClass'; import CurrentClass from '@/components/school-class/CurrentClass';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
import me from '@/mixins/me'; import me from '@/mixins/me';
const Hamburger = () => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger'); const Hamburger = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger');
export default { export default {
mixins: [openSidebar, me], mixins: [openSidebar, me],
components: { components: {
ContentNavigation, ContentNavigation,
UserWidget, UserWidget,
CurrentClass, CurrentClass,
Hamburger, Hamburger,
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.header-bar { .header-bar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;
} }
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: $color-white; background-color: $color-white;
grid-auto-rows: 50px; grid-auto-rows: 50px;
width: 100%; width: 100%;
max-width: 100vw; max-width: 100vw;
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
@include desktop { @include desktop {
grid-template-columns: 50px 1fr auto; grid-template-columns: 50px 1fr auto;
grid-template-rows: 50px; grid-template-rows: 50px;
grid-auto-rows: auto; grid-auto-rows: auto;
} }
/* /*
* For IE10+ * For IE10+
*/ */
-ms-grid-columns: 1fr 1fr 1fr; -ms-grid-columns: 1fr 1fr 1fr;
-ms-grid-rows: 50px 50px; -ms-grid-rows: 50px 50px;
/* /*
* For IE10+ * For IE10+
*/ */
& > :nth-child(1) { & > :nth-child(1) {
-ms-grid-column: 1; -ms-grid-column: 1;
-ms-grid-row-align: center; -ms-grid-row-align: center;
} }
&__content-navigation { &__content-navigation {
grid-column: 2; grid-column: 2;
justify-content: space-between; justify-content: space-between;
} }
&__sidebar-link { &__sidebar-link {
padding: $small-spacing; padding: $small-spacing;
cursor: pointer; cursor: pointer;
} }
&__sidebar-icon { &__sidebar-icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
/* /*
* For IE10+ * For IE10+
*/ */
& > :nth-child(3) { & > :nth-child(3) {
-ms-grid-column: 3; -ms-grid-column: 3;
-ms-grid-row-align: center; -ms-grid-row-align: center;
-ms-grid-column-align: end; -ms-grid-column-align: end;
justify-self: end; justify-self: end;
}
& > :nth-child(4) {
-ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
} }
.user-header { & > :nth-child(4) {
display: flex; -ms-grid-row: 2;
-ms-grid-column: 1;
-ms-grid-column-span: 3;
}
}
&__current-class { .user-header {
margin-right: $large-spacing; display: flex;
}
&__sidebar-link { &__current-class {
cursor: pointer; margin-right: $large-spacing;
display: none; }
@include desktop { &__sidebar-link {
display: flex; cursor: pointer;
} display: none;
@include desktop {
display: flex;
} }
} }
}
</style> </style>

View File

@ -10,66 +10,65 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'); const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon');
export default { export default {
props: ['text'], props: ['text'],
components: { components: {
InfoIcon InfoIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.helpful-tooltip { .helpful-tooltip {
position: relative; position: relative;
&__icon { &__icon {
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $color-silver-dark; fill: $color-silver-dark;
} }
&__tooltip { &__tooltip {
visibility: hidden; visibility: hidden;
position: absolute;
left: 30px;
top: 0px;
width: 400px;
}
&__text {
display: inline-table;
width: auto;
background-color: $color-white;
border: 1px solid $color-silver-dark;
border-radius: 5px;
padding: $small-spacing;
@include small-text;
&::before {
content: '';
position: absolute; position: absolute;
left: 30px; left: 0;
top: 0px; top: 18px;
width: 400px; margin-left: -1px;
} border-left: 1px solid $color-silver-dark;
border-top: 1px solid $color-silver-dark;
&__text {
display: inline-table;
width: auto;
background-color: $color-white; background-color: $color-white;
border: 1px solid $color-silver-dark; width: 10px;
border-radius: 5px; height: 10px;
padding: $small-spacing; transform: rotate(-45deg) translateY(-50%);
@include small-text;
&::before {
content: '';
position: absolute;
left: 0;
top: 18px;
margin-left: -1px;
border-left: 1px solid $color-silver-dark;
border-top: 1px solid $color-silver-dark;
background-color: $color-white;
width: 10px;
height: 10px;
transform: rotate(-45deg) translateY(-50%);
}
}
&:hover &__tooltip {
visibility: visible;
} }
} }
&:hover &__tooltip {
visibility: visible;
}
}
</style> </style>

View File

@ -1,57 +1,51 @@
<template> <template>
<button <button :disabled="loading || disabled" class="loading-button button button--primary button--big">
:disabled="loading || disabled"
class="loading-button button button--primary button--big"
>
<template v-if="!loading"> <template v-if="!loading">
{{ label }} {{ label }}
</template> </template>
<loading-icon <loading-icon class="loading-button__icon" v-else />
class="loading-button__icon"
v-else
/>
</button> </button>
</template> </template>
<script> <script>
const LoadingIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon'); const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon');
export default { export default {
props: { props: {
loading: { loading: {
type: Boolean, type: Boolean,
default: false default: false,
},
disabled: {
type: Boolean,
default: false
},
label: {
type: String,
default: ''
}
}, },
components: { disabled: {
LoadingIcon type: Boolean,
} default: false,
}; },
label: {
type: String,
default: '',
},
},
components: {
LoadingIcon,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.loading-button { .loading-button {
height: 52px; height: 52px;
min-width: 100px; min-width: 100px;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
&__icon { &__icon {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin: 0 auto; margin: 0 auto;
@include spin; @include spin;
fill: $color-brand; fill: $color-brand;
}
} }
}
</style> </style>

View File

@ -1,40 +1,40 @@
<template> <template>
<div class="logout-widget"> <div class="logout-widget">
<a <a class="logout-widget__logout" data-cy="logout" @click="logout()">Abmelden</a>
class="logout-widget__logout"
data-cy="logout"
@click="logout()"
>Abmelden</a>
</div> </div>
</template> </template>
<script> <script>
import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql'; import LOGOUT_MUTATION from '@/graphql/gql/mutations/logoutUser.gql';
export default { export default {
methods: { methods: {
logout() { logout() {
this.$apollo.mutate({ this.$apollo
.mutate({
mutation: LOGOUT_MUTATION, mutation: LOGOUT_MUTATION,
}).then(({data}) => { })
if (data.logout.success) { location.replace('/logout'); } .then(({ data }) => {
if (data.logout.success) {
location.replace('/logout');
}
}); });
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.logout-widget { .logout-widget {
display: flex; display: flex;
align-items: center; align-items: center;
&__logout { &__logout {
@include default-link; @include default-link;
cursor: pointer; cursor: pointer;
}
} }
}
</style> </style>

View File

@ -4,65 +4,59 @@
<hamburger class="mobile-header__hamburger" /> <hamburger class="mobile-header__hamburger" />
</a> </a>
<router-link <router-link to="/" data-cy="mobile-home-link">
to="/"
data-cy="mobile-home-link"
>
<logo /> <logo />
</router-link> </router-link>
<user-widget <user-widget v-bind="me" @click.native.stop="openSidebar('profile')" />
v-bind="me"
@click.native.stop="openSidebar('profile')"
/>
</div> </div>
</template> </template>
<script> <script>
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import me from '@/mixins/me'; import me from '@/mixins/me';
import openSidebar from '@/mixins/open-sidebar'; import openSidebar from '@/mixins/open-sidebar';
const Logo = () => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'); const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo');
const Hamburger = () => import(/* webpackChunkName: "icons" */'@/components/icons/Hamburger'); const Hamburger = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Hamburger');
export default { export default {
mixins: [me, openSidebar], mixins: [me, openSidebar],
components: { components: {
Logo, Logo,
Hamburger, Hamburger,
UserWidget, UserWidget,
},
methods: {
showMobileNavigation() {
this.$store.dispatch('showMobileNavigation', true);
}, },
},
methods: { };
showMobileNavigation() {
this.$store.dispatch('showMobileNavigation', true);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.mobile-header { .mobile-header {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
display: flex; display: flex;
@include desktop { @include desktop {
display: none; display: none;
}
padding: 0 $medium-spacing;
&__hamburger {
width: 30px;
height: 30px;
fill: $color-silver-dark;
}
} }
padding: 0 $medium-spacing;
&__hamburger {
width: 30px;
height: 30px;
fill: $color-silver-dark;
}
}
</style> </style>

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="modal__backdrop"> <div class="modal__backdrop">
<div <div
:class="{'modal--hide-header': hideHeader || fullscreen, 'modal--fullscreen': fullscreen, 'modal--small': small}" :class="{
'modal--hide-header': hideHeader || fullscreen,
'modal--fullscreen': fullscreen,
'modal--small': small,
}"
class="modal" class="modal"
> >
<div class="modal__header"> <div class="modal__header">
@ -9,20 +13,14 @@
</div> </div>
<div class="modal__body"> <div class="modal__body">
<slot /> <slot />
<div <div class="modal__close-button" @click="hideModal">
class="modal__close-button"
@click="hideModal"
>
<cross class="modal__close-icon" /> <cross class="modal__close-icon" />
</div> </div>
</div> </div>
<div class="modal__footer"> <div class="modal__footer">
<slot name="footer"> <slot name="footer">
<!--<a class="button button&#45;&#45;active">Speichern</a>--> <!--<a class="button button&#45;&#45;active">Speichern</a>-->
<a <a class="button" @click="hideModal">Abbrechen</a>
class="button"
@click="hideModal"
>Abbrechen</a>
</slot> </slot>
</div> </div>
</div> </div>
@ -30,159 +28,158 @@
</template> </template>
<script> <script>
const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'); const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon');
export default { export default {
props: { props: {
hideHeader: { hideHeader: {
type: Boolean, type: Boolean,
default: false default: false,
},
fullscreen: {
type: Boolean,
default: false
},
small: {
type: Boolean,
default: false
}
}, },
fullscreen: {
components: { type: Boolean,
Cross default: false,
}, },
small: {
type: Boolean,
default: false,
},
},
methods: { components: {
hideModal() { Cross,
this.$store.dispatch('hideModal'); },
}
} methods: {
}; hideModal() {
this.$store.dispatch('hideModal');
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.modal { .modal {
align-self: center; align-self: center;
justify-self: center; justify-self: center;
width: 700px; width: 700px;
height: 80vh; height: 80vh;
background-color: $color-white; background-color: $color-white;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
border: 1px solid $color-silver-light; border: 1px solid $color-silver-light;
display: -ms-grid; display: -ms-grid;
@supports (display: grid) {
display: grid;
}
grid-template-rows: auto 1fr 65px;
grid-template-areas: 'header' 'body' 'footer';
-ms-grid-rows: auto 1fr 65px;
position: relative;
&__backdrop {
display: flex;
justify-content: center;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;
} }
grid-template-rows: auto 1fr 65px; position: fixed;
grid-template-areas: "header" "body" "footer"; top: 0;
-ms-grid-rows: auto 1fr 65px; left: 0;
position: relative; bottom: 0;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__backdrop { &__header {
display: flex; grid-area: header;
justify-content: center; -ms-grid-row: 1;
@supports (display: grid) { padding: 10px $modal-lateral-padding;
display: grid; border-bottom: 1px solid $color-silver-light;
} }
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba($color-white, 0.8);
z-index: 90;
}
&__header { &__body {
grid-area: header; grid-area: body;
-ms-grid-row: 1; -ms-grid-row: 2;
padding: 10px $modal-lateral-padding; padding: 10px $modal-lateral-padding;
border-bottom: 1px solid $color-silver-light; overflow: auto;
} box-sizing: border-box;
min-height: 30vh;
}
&__body { &__close-button {
grid-area: body; display: none;
-ms-grid-row: 2; cursor: pointer;
padding: 10px $modal-lateral-padding; position: absolute;
overflow: auto; right: 15px;
box-sizing: border-box; top: 15px;
min-height: 30vh; background: rgba($color-white, 0.5);
} border-radius: 40px;
padding: 10px;
align-content: center;
}
&__close-button { &__footer {
grid-area: footer;
-ms-grid-row: 3;
border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-padding;
}
$parent: &;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: 'body' 'footer';
#{$parent}__header {
display: none; display: none;
cursor: pointer;
position: absolute;
right: 15px;
top: 15px;
background: rgba($color-white, 0.5);
border-radius: 40px;
padding: 10px;
align-content: center;
} }
&__footer { #{$parent}__body {
grid-area: footer; padding: $default-padding;
-ms-grid-row: 3;
border-top: 1px solid $color-silver-light;
padding: 16px $modal-lateral-padding;
}
$parent: &;
&--hide-header {
grid-template-rows: 1fr 65px;
grid-template-areas: "body" "footer";
#{$parent}__header {
display: none;
}
#{$parent}__body {
padding: $default-padding;
}
}
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: "body";
overflow: hidden;
#{$parent}__footer {
display: none;
}
#{$parent}__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
#{$parent}__close-button {
display: flex;
}
}
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
} }
} }
&--fullscreen {
width: 95vw;
height: auto;
grid-template-rows: 1fr;
-ms-grid-rows: 1fr;
grid-template-areas: 'body';
overflow: hidden;
#{$parent}__footer {
display: none;
}
#{$parent}__body {
padding: 0;
scrollbar-width: none;
margin-right: -5px;
height: auto;
max-height: 95vh;
&::-webkit-scrollbar {
display: none;
}
}
#{$parent}__close-button {
display: flex;
}
}
&--small {
height: auto;
#{$parent}__body {
min-height: 0;
}
}
}
</style> </style>

View File

@ -2,40 +2,35 @@
<div class="modal-input"> <div class="modal-input">
<input <input
:placeholder="placeholder" :placeholder="placeholder"
:class="{'skillbox-input--error': error}" :class="{ 'skillbox-input--error': error }"
:value="value" :value="value"
class="modal-input__inputfield skillbox-input" class="modal-input__inputfield skillbox-input"
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
> />
<div <div class="modal-input__error" v-if="error">Für Inhaltsblöcke muss zwingend ein Titel erfasst werden.</div>
class="modal-input__error"
v-if="error"
>
Für Inhaltsblöcke muss zwingend ein Titel erfasst werden.
</div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value', 'error', 'placeholder'] props: ['value', 'error', 'placeholder'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_functions.scss"; @import '@/styles/_functions.scss';
.modal-input { .modal-input {
&__inputfield { &__inputfield {
width: $modal-input-width; width: $modal-input-width;
}
&__error {
font-family: sans-serif;
font-size: toRem(14px);
color: $color-accent-3-dark;
padding: 10px 0;
}
} }
&__error {
font-family: sans-serif;
font-size: toRem(14px);
color: $color-accent-3-dark;
padding: 10px 0;
}
}
</style> </style>

View File

@ -1,67 +1,59 @@
<template> <template>
<div class="more-options"> <div class="more-options">
<a <a class="more-options__more-link" data-cy="more-options-link" @click.stop="showMenu = !showMenu">
class="more-options__more-link"
data-cy="more-options-link"
@click.stop="showMenu = !showMenu"
>
<ellipses class="more-options__ellipses" /> <ellipses class="more-options__ellipses" />
</a> </a>
<widget-popover <widget-popover class="more-options__popover" v-if="showMenu" @hide-me="showMenu = false">
class="more-options__popover"
v-if="showMenu"
@hide-me="showMenu = false"
>
<slot /> <slot />
</widget-popover> </widget-popover>
</div> </div>
</template> </template>
<script> <script>
import WidgetPopover from '@/components/ui/WidgetPopover'; import WidgetPopover from '@/components/ui/WidgetPopover';
const Ellipses = () => import(/* webpackChunkName: "icons" */'@/components/icons/Ellipses.vue'); const Ellipses = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Ellipses.vue');
export default { export default {
components: { components: {
WidgetPopover, WidgetPopover,
Ellipses Ellipses,
}, },
data() { data() {
return { return {
showMenu: false showMenu: false,
}; };
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.more-options { .more-options {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
&__ellipses { &__ellipses {
width: 30px; width: 30px;
height: 30px; height: 30px;
fill: $color-charcoal-dark; fill: $color-charcoal-dark;
margin-top: -7px; margin-top: -7px;
}
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__popover {
min-width: 200px;
@include popover-defaults();
}
} }
&__more-link {
background-color: rgba($color-white, 0.9);
width: 35px;
height: 15px;
border-radius: 15px;
display: flex;
justify-content: center;
}
&__popover {
min-width: 200px;
@include popover-defaults();
}
}
</style> </style>

View File

@ -1,33 +1,27 @@
<template> <template>
<base-input <base-input :label="label" :checked="checked" :item="item" :type="'radiobutton'" @input="passOn" />
:label="label"
:checked="checked"
:item="item"
:type="'radiobutton'"
@input="passOn"
/>
</template> </template>
<script> <script>
import BaseInput from '@/components/ui/BaseInput'; import BaseInput from '@/components/ui/BaseInput';
export default { export default {
props: { props: {
label: String, label: String,
checked: { checked: {
type: Boolean type: Boolean,
},
item: Object
}, },
item: Object,
},
components: { components: {
BaseInput BaseInput,
},
methods: {
passOn() {
this.$emit('input', ...arguments);
}, },
},
methods: { };
passOn() {
this.$emit('input', ...arguments);
}
}
};
</script> </script>

View File

@ -1,14 +1,7 @@
<template> <template>
<div <div class="read-only-banner" data-cy="read-only-banner" v-if="me.readOnly || me.selectedClass.readOnly">
class="read-only-banner"
data-cy="read-only-banner"
v-if="me.readOnly || me.selectedClass.readOnly"
>
<div class="read-only-banner__content"> <div class="read-only-banner__content">
<p class="read-only-banner__text"> <p class="read-only-banner__text">{{ readOnlyText }} Sie können Inhalte lesen, aber nicht bearbeiten.</p>
{{ readOnlyText }} Sie können Inhalte lesen, aber nicht
bearbeiten.
</p>
<div class="read-only-banner__buttons"> <div class="read-only-banner__buttons">
<router-link <router-link
:to="licenseActivationLink" :to="licenseActivationLink"
@ -18,96 +11,93 @@
> >
Neuen Lizenzcode eingeben Neuen Lizenzcode eingeben
</router-link> </router-link>
<a <a target="_blank" href="https://myskillbox.ch/lesemodus" class="button button--secondary"
target="_blank" >Mehr Informationen zum Lesemodus</a
href="https://myskillbox.ch/lesemodus" >
class="button button--secondary"
>Mehr Informationen zum Lesemodus</a>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import {LICENSE_ACTIVATION} from '@/router/auth.names'; import { LICENSE_ACTIVATION } from '@/router/auth.names';
export default { export default {
data() { data() {
return { return {
me: {
readOnly: false,
selectedClass: {
readOnly: false,
},
},
licenseActivationLink: {
name: LICENSE_ACTIVATION,
},
};
},
apollo: {
me: { me: {
query: gql` readOnly: false,
selectedClass: {
readOnly: false,
},
},
licenseActivationLink: {
name: LICENSE_ACTIVATION,
},
};
},
apollo: {
me: {
query: gql`
query { query {
me { me {
readOnly readOnly
selectedClass { selectedClass {
readOnly readOnly
} }
} }
} }
`, `,
fetchPolicy: 'cache-only', fetchPolicy: 'cache-only',
update({me}) { update({ me }) {
if (!me) { if (!me) {
return { return {
readOnly: false,
selectedClass: {
readOnly: false, readOnly: false,
selectedClass: { },
readOnly: false, };
}, }
}; return me;
}
return me;
},
}, },
}, },
},
computed: { computed: {
readOnlyText() { readOnlyText() {
return this.me.readOnly ? 'Sie besitzen keine aktive Lizenz.' : 'Sie sind in dieser Klasse nicht mehr aktiv.'; return this.me.readOnly ? 'Sie besitzen keine aktive Lizenz.' : 'Sie sind in dieser Klasse nicht mehr aktiv.';
},
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.read-only-banner { .read-only-banner {
background-color: $color-brand-light; background-color: $color-brand-light;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: $small-spacing 0; padding: $small-spacing 0;
&__content { &__content {
max-width: $screen-width; max-width: $screen-width;
}
&__text {
padding: 0;
@include regular-text;
margin-bottom: $small-spacing;
}
&__buttons {
}
&__link {
@include default-link;
margin-right: $small-spacing;
}
} }
&__text {
padding: 0;
@include regular-text;
margin-bottom: $small-spacing;
}
&__buttons {
}
&__link {
@include default-link;
margin-right: $small-spacing;
}
}
</style> </style>

View File

@ -1,78 +1,73 @@
<template> <template>
<transition name="fade"> <transition name="fade">
<a <a class="scroll-up" v-if="scroll > 200" @click="scrollTop">
class="scroll-up"
v-if="scroll>200"
@click="scrollTop"
>
<arrow-up class="scroll-up__icon" /> <arrow-up class="scroll-up__icon" />
</a> </a>
</transition> </transition>
</template> </template>
<script> <script>
const ArrowUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ArrowUp'); const ArrowUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ArrowUp');
export default { export default {
components: { components: {
ArrowUp ArrowUp,
}, },
data() { data() {
return { return {
scroll: 0 scroll: 0,
}; };
}, },
mounted() { mounted() {
let html = document.scrollingElement; let html = document.scrollingElement;
document.body.onscroll = () => { document.body.onscroll = () => {
this.scroll = html.scrollTop; this.scroll = html.scrollTop;
}; };
}, },
destroyed() { destroyed() {
document.body.onscroll = null; document.body.onscroll = null;
}, },
methods: { methods: {
scrollTop() { scrollTop() {
document.scrollingElement.scrollTop = 0; document.scrollingElement.scrollTop = 0;
}
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import '@/styles/_variables.scss';
@import '@/styles/_mixins.scss'; @import '@/styles/_mixins.scss';
.scroll-up { .scroll-up {
position: fixed; position: fixed;
right: $large-spacing; right: $large-spacing;
bottom: $large-spacing; bottom: $large-spacing;
padding: $medium-spacing; padding: $medium-spacing;
border-radius: 100px; border-radius: 100px;
@include default-box-shadow; @include default-box-shadow;
cursor: pointer; cursor: pointer;
background-color: $color-white; background-color: $color-white;
border: 1px solid $color-silver; border: 1px solid $color-silver;
z-index: 2; z-index: 2;
&__icon {
width: 50px;
height: 50px;
fill: $color-brand;
}
&__icon {
width: 50px;
height: 50px;
fill: $color-brand;
} }
}
.fade-enter-active, .fade-leave-active { .fade-enter-active,
transition: opacity .3s; .fade-leave-active {
} transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
{ opacity: 0;
opacity: 0; }
}
</style> </style>

View File

@ -1,18 +1,10 @@
<template> <template>
<div class="section-block"> <div class="section-block">
<div <div :class="{ 'section-block--navigatable': route }" class="section-block__illustration" @click="navigate()">
:class="{'section-block--navigatable': route}"
class="section-block__illustration"
@click="navigate()"
>
<slot /> <slot />
</div> </div>
<div <div :class="{ 'section-block--navigatable': route }" class="section-block__title block-title" @click="navigate()">
:class="{'section-block--navigatable': route}"
class="section-block__title block-title"
@click="navigate()"
>
<h2 class="block-title__title"> <h2 class="block-title__title">
{{ title }} {{ title }}
</h2> </h2>
@ -23,91 +15,88 @@
<div class="section-block__content section-content"> <div class="section-block__content section-content">
<div class="section-content__subsection subsection"> <div class="section-content__subsection subsection">
<a <a
:class="{'section-block--navigatable': route}" :class="{ 'section-block--navigatable': route }"
class="subsection__content button button--primary" class="subsection__content button button--primary"
v-if="route" v-if="route"
@click="navigate()" @click="navigate()"
> >
{{ linkText }} {{ linkText }}
</a> </a>
<span <span class="subsection__content subsection__content--disabled" v-if="!route">Noch nicht verfügbar</span>
class="subsection__content subsection__content--disabled"
v-if="!route"
>Noch nicht verfügbar</span>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['title', 'subtitle', 'route', 'linkText'], props: ['title', 'subtitle', 'route', 'linkText'],
methods: { methods: {
navigate() { navigate() {
if (this.route) { if (this.route) {
this.$router.push(this.route); this.$router.push(this.route);
}
} }
} },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_functions.scss"; @import '@/styles/_functions.scss';
.section-block { .section-block {
border-radius: $default-border-radius; border-radius: $default-border-radius;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
&--navigatable { &--navigatable {
cursor: pointer; cursor: pointer;
}
&__title {
padding: 20px;
}
} }
.block-title { &__title {
&__title, &__subtitle { padding: 20px;
color: $color-charcoal-dark; }
font-family: $sans-serif-font-family; }
}
&__title { .block-title {
font-size: toRem(29px); &__title,
text-transform: uppercase; &__subtitle {
margin-bottom: 8px; color: $color-charcoal-dark;
font-weight: 600; font-family: $sans-serif-font-family;
}
&__subtitle {
font-size: toRem(14px);
font-weight: $font-weight-regular;
min-height: 2rem;
}
} }
.section-content { &__title {
padding: 15px 15px 15px; font-size: toRem(29px);
text-transform: uppercase;
&__subsection { margin-bottom: 8px;
padding-bottom: 15px; font-weight: 600;
}
} }
.subsection { &__subtitle {
&__content { font-size: toRem(14px);
font-family: $sans-serif-font-family; font-weight: $font-weight-regular;
font-weight: 600; min-height: 2rem;
}
}
&--disabled { .section-content {
color: $color-silver-dark; padding: 15px 15px 15px;
}
&__subsection {
padding-bottom: 15px;
}
}
.subsection {
&__content {
font-family: $sans-serif-font-family;
font-weight: 600;
&--disabled {
color: $color-silver-dark;
} }
} }
}
</style> </style>

View File

@ -5,24 +5,12 @@
</div> </div>
<div class="student-submission__entry entry"> <div class="student-submission__entry entry">
<p>{{ submission.text | trimToLength(50) }}</p> <p>{{ submission.text | trimToLength(50) }}</p>
<p <p class="entry__document" v-if="submission.document && submission.document.length > 0">
class="entry__document" <student-submission-document :document="submission.document" class="entry-document" />
v-if="submission.document && submission.document.length > 0"
>
<student-submission-document
:document="submission.document"
class="entry-document"
/>
</p> </p>
</div> </div>
<div <div class="student-submission__feedback entry" v-if="submission.submissionFeedback">
class="student-submission__feedback entry" <p :class="{ 'entry__text--final': submission.submissionFeedback.final }" class="entry__text">
v-if="submission.submissionFeedback"
>
<p
:class="{'entry__text--final': submission.submissionFeedback.final}"
class="entry__text"
>
{{ submission.submissionFeedback.text | trimToLength(50) }} {{ submission.submissionFeedback.text | trimToLength(50) }}
</p> </p>
</div> </div>
@ -30,66 +18,67 @@
</template> </template>
<script> <script>
import StudentSubmissionDocument from '@/components/StudentSubmissionDocument'; import StudentSubmissionDocument from '@/components/StudentSubmissionDocument';
export default { export default {
props: ['submission'], props: ['submission'],
components: { components: {
StudentSubmissionDocument StudentSubmissionDocument,
}, },
filters: { filters: {
trimToLength: function(text, numberOfChars) { trimToLength: function (text, numberOfChars) {
if (!text) { if (!text) {
return ''; return '';
}
if (text.length <= numberOfChars) {
return text;
}
const index = text.indexOf(' ', numberOfChars - 1);
if (index === -1) {
return text;
}
return `${text.substring(0, index)}`;
} }
if (text.length <= numberOfChars) {
return text;
}
const index = text.indexOf(' ', numberOfChars - 1);
if (index === -1) {
return text;
}
return `${text.substring(0, index)}`;
}, },
},
computed: { computed: {
name() { name() {
return this.submission && this.submission.student return this.submission && this.submission.student
? `${this.submission.student.firstName} ${this.submission.student.lastName}` : ''; ? `${this.submission.student.firstName} ${this.submission.student.lastName}`
}, : '';
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.student-submission { .student-submission {
@include table-row; @include table-row;
&__student-name { &__student-name {
font-size: toRem(17px); font-size: toRem(17px);
font-weight: 800; font-weight: 800;
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
}
&__entry {
font-size: toRem(14px);
font-family: $sans-serif-font-family;
}
.entry-document {
margin-top: 1rem;
}
} }
.entry { &__entry {
&__text { font-size: toRem(14px);
color: $color-silver-dark; font-family: $sans-serif-font-family;
&--final { }
color: $color-charcoal-dark;
} .entry-document {
margin-top: 1rem;
}
}
.entry {
&__text {
color: $color-silver-dark;
&--final {
color: $color-charcoal-dark;
} }
} }
}
</style> </style>

View File

@ -1,44 +1,40 @@
<template> <template>
<div class="submission-document"> <div class="submission-document">
<p <p class="submission-document__content content" v-if="document && document.length > 0">
class="submission-document__content content"
v-if="document && document.length > 0"
>
<document-icon class="content__icon" /><span class="content__text">{{ filename }}</span> <document-icon class="content__icon" /><span class="content__text">{{ filename }}</span>
</p> </p>
</div> </div>
</template> </template>
<script> <script>
import filenameFromUrl from '@/helpers/urls';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
import filenameFromUrl from '@/helpers/urls'; export default {
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); name: 'StudentSubmissionDocument',
props: ['document'],
components: { DocumentIcon },
export default { computed: {
name: 'StudentSubmissionDocument', filename() {
props: ['document'], return filenameFromUrl(this.document);
components: { DocumentIcon },
computed: {
filename() {
return filenameFromUrl(this.document);
}
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.content { .content {
display: flex; display: flex;
&__icon { &__icon {
width: 25px; width: 25px;
align-self: center; align-self: center;
}
&__text {
align-self: center;
padding-left: 5px;
}
} }
&__text {
align-self: center;
padding-left: 5px;
}
}
</style> </style>

View File

@ -4,53 +4,50 @@
<avatar :avatar-url="avatarUrl" /> <avatar :avatar-url="avatarUrl" />
</div> </div>
<span class="user-widget__name">{{ firstName }} {{ lastName }}</span> <span class="user-widget__name">{{ firstName }} {{ lastName }}</span>
<span <span class="user-widget__date" v-if="date">{{ date }}</span>
class="user-widget__date"
v-if="date"
>{{ date }}</span>
</div> </div>
</template> </template>
<script> <script>
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
export default { export default {
props: ['firstName', 'lastName', 'avatarUrl', 'date'], props: ['firstName', 'lastName', 'avatarUrl', 'date'],
components: { components: {
Avatar Avatar,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.user-widget { .user-widget {
color: $color-silver-dark;
display: flex;
align-items: center;
&__name {
padding: 0px 10px;
color: $color-silver-dark; color: $color-silver-dark;
display: flex; font-family: $sans-serif-font-family;
align-items: center; }
&__name { &__date {
padding: 0px 10px; font-family: $sans-serif-font-family;
color: $color-silver-dark; }
font-family: $sans-serif-font-family;
}
&__date { &__avatar {
font-family: $sans-serif-font-family; width: 30px;
} height: 30px;
fill: $color-silver-dark;
}
&__avatar { &--is-profile {
width: 30px; & > span {
height: 30px; color: $color-brand;
fill: $color-silver-dark;
}
&--is-profile {
& > span {
color: $color-brand;
}
} }
} }
}
</style> </style>

View File

@ -1,79 +1,70 @@
<template> <template>
<div <div :class="{ 'user-widget--is-profile': isProfile }" class="user-widget">
:class="{'user-widget--is-profile': isProfile}" <div class="user-widget__avatar" data-cy="user-widget-avatar">
class="user-widget" <avatar :avatar-url="avatarUrl" :icon-highlighted="isProfile" />
>
<div
class="user-widget__avatar"
data-cy="user-widget-avatar"
>
<avatar
:avatar-url="avatarUrl"
:icon-highlighted="isProfile"
/>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
export default { export default {
props: { props: {
avatarUrl: { avatarUrl: {
type: String type: String,
}
}, },
},
components: { components: {
Avatar Avatar,
},
computed: {
isProfile() {
return this.$route.meta.isProfile;
}, },
computed: { },
isProfile() { };
return this.$route.meta.isProfile;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.user-widget { .user-widget {
color: $color-silver-dark;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
// todo: do we need the margin right always? just do it where needed --> content block actions and objecives override this
margin-right: $medium-spacing;
&__popover {
top: 40px;
white-space: nowrap;
}
&__name {
padding: 0px $small-spacing;
color: $color-silver-dark; color: $color-silver-dark;
display: flex; font-family: $sans-serif-font-family;
justify-content: space-between; }
align-items: center;
position: relative;
// todo: do we need the margin right always? just do it where needed --> content block actions and objecives override this
margin-right: $medium-spacing;
&__popover { &__date {
top: 40px; font-family: $sans-serif-font-family;
white-space: nowrap; }
}
&__name { &__avatar {
padding: 0px $small-spacing; width: 30px;
color: $color-silver-dark; height: 30px;
font-family: $sans-serif-font-family; fill: $color-silver-dark;
} cursor: pointer;
}
&__date { &--is-profile {
font-family: $sans-serif-font-family; & > span {
} color: $color-brand;
&__avatar {
width: 30px;
height: 30px;
fill: $color-silver-dark;
cursor: pointer;
}
&--is-profile {
& > span {
color: $color-brand;
}
} }
} }
}
</style> </style>

View File

@ -1,12 +1,9 @@
<template> <template>
<nav <nav :class="{ 'content-navigation--sidebar': isSidebar }" class="content-navigation">
:class="{'content-navigation--sidebar': isSidebar}"
class="content-navigation"
>
<div class="content-navigation__primary"> <div class="content-navigation__primary">
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
:class="{'content-navigation__link--active': isActive('book')}" :class="{ 'content-navigation__link--active': isActive('book') }"
:to="topicRoute" :to="topicRoute"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
@ -15,9 +12,7 @@
{{ $flavor.textTopics }} {{ $flavor.textTopics }}
</router-link> </router-link>
<topic-navigation <topic-navigation v-if="isSidebar" />
v-if="isSidebar"
/>
</div> </div>
<div class="content-navigation__item"> <div class="content-navigation__item">
@ -33,7 +28,7 @@
<div class="content-navigation__item"> <div class="content-navigation__item">
<router-link <router-link
:to="{name: 'news'}" :to="{ name: 'news' }"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link" class="content-navigation__link"
data-cy="news-navigation-link" data-cy="news-navigation-link"
@ -45,19 +40,14 @@
</div> </div>
</div> </div>
<router-link <router-link to="/" class="content-navigation__logo" data-cy="home-link" v-if="!isSidebar">
to="/"
class="content-navigation__logo"
data-cy="home-link"
v-if="!isSidebar"
>
<logo class="content-navigation__logo-icon" /> <logo class="content-navigation__logo-icon" />
</router-link> </router-link>
<div class="content-navigation__secondary"> <div class="content-navigation__secondary">
<div class="content-navigation__item content-navigation__item--secondary"> <div class="content-navigation__item content-navigation__item--secondary">
<router-link <router-link
:class="{'content-navigation__link--active': isRoomUrl()}" :class="{ 'content-navigation__link--active': isRoomUrl() }"
to="/rooms" to="/rooms"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@ -67,10 +57,7 @@
</router-link> </router-link>
</div> </div>
<div <div class="content-navigation__item content-navigation__item--secondary" v-if="showPortfolio">
class="content-navigation__item content-navigation__item--secondary"
v-if="showPortfolio"
>
<router-link <router-link
to="/portfolio" to="/portfolio"
active-class="content-navigation__link--active" active-class="content-navigation__link--active"
@ -80,16 +67,13 @@
Portfolio Portfolio
</router-link> </router-link>
</div> </div>
<div <div class="content-navigation__item content-navigation__item--secondary" v-if="isSidebar">
class="content-navigation__item content-navigation__item--secondary"
v-if="isSidebar"
>
<a <a
:href="$flavor.supportLink" :href="$flavor.supportLink"
target="_blank" target="_blank"
class="content-navigation__link content-navigation__link--secondary" class="content-navigation__link content-navigation__link--secondary"
@click="close" @click="close"
>Support >Support
</a> </a>
</div> </div>
</div> </div>
@ -97,141 +81,142 @@
</template> </template>
<script> <script>
import TopicNavigation from '@/components/book-navigation/TopicNavigation'; import TopicNavigation from '@/components/book-navigation/TopicNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
import meMixin from '@/mixins/me'; import meMixin from '@/mixins/me';
const Logo = () => import(/* webpackChunkName: "icons" */'@/components/icons/Logo'); const Logo = () => import(/* webpackChunkName: "icons" */ '@/components/icons/Logo');
export default { export default {
props: { props: {
isSidebar: { isSidebar: {
default: false default: false,
}
}, },
},
mixins: [sidebarMixin, meMixin], mixins: [sidebarMixin, meMixin],
components: { components: {
TopicNavigation, TopicNavigation,
Logo Logo,
},
computed: {
showPortfolio() {
return this.$flavor.showPortfolio;
}, },
},
computed: { methods: {
showPortfolio() { isActive(linkName) {
return this.$flavor.showPortfolio; return linkName === 'book' && this.$route.path.indexOf('module') > -1;
}
}, },
isRoomUrl() {
methods: { return this.$route.path.indexOf('room') > -1;
isActive(linkName) { },
return linkName === 'book' && this.$route.path.indexOf('module') > -1; close() {
}, this.closeSidebar('navigation');
isRoomUrl() { },
return this.$route.path.indexOf('room') > -1; },
}, };
close() {
this.closeSidebar('navigation');
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.content-navigation { .content-navigation {
display: flex; display: flex;
align-items: center; align-items: center;
&__link { &__link {
padding: 0 24px; padding: 0 24px;
@include navigation-link; @include navigation-link;
} }
&__primary, &__secondary { &__primary,
display: none; &__secondary {
flex-direction: row; display: none;
flex-direction: row;
@include desktop { @include desktop {
display: flex;
}
}
&__logo {
color: #17A887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex; display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__logo-icon {
width: auto;
height: 31px;
}
&__link {
&--secondary {
@include regular-text;
}
&--active {
color: $color-brand;
}
}
$parent: &;
&--sidebar {
flex-direction: column;
#{$parent}__primary, #{$parent}__secondary {
display: flex;
flex-direction: column;
width: 100%;
}
#{$parent}__link {
@include heading-4;
line-height: 2.5em;
padding: 0;
display: block;
margin-bottom: 0.5*$small-spacing;
&:only-child {
margin-bottom: 0;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
}
} }
} }
&__logo {
color: #17a887;
font-size: 36px;
font-weight: 800;
font-family: $sans-serif-font-family;
display: flex;
justify-self: center;
/*
* For IE10+
*/
-ms-grid-column: 2;
-ms-grid-row-align: center;
-ms-grid-column-align: center;
}
&__logo-icon {
width: auto;
height: 31px;
}
&__link {
&--secondary {
@include regular-text;
}
&--active {
color: $color-brand;
}
}
$parent: &;
&--sidebar {
flex-direction: column;
#{$parent}__primary,
#{$parent}__secondary {
display: flex;
flex-direction: column;
width: 100%;
}
#{$parent}__link {
@include heading-4;
line-height: 2.5em;
padding: 0;
display: block;
margin-bottom: 0.5 * $small-spacing;
&:only-child {
margin-bottom: 0;
}
}
#{$parent}__item {
width: 100%;
//border-bottom: 1px solid $color-white;
/*&:nth-child(1) {*/
/* order: 3;*/
/* border-bottom: 0;*/
/*}*/
/*&:nth-child(2) {*/
/* order: 1;*/
/*}*/
/*&:nth-child(3) {*/
/* order: 2;*/
/*}*/
}
}
}
</style> </style>

View File

@ -1,18 +1,8 @@
<template> <template>
<transition name="slide"> <transition name="slide">
<div <div class="navigation-sidebar" v-if="sidebar.navigation" v-click-outside="close">
class="navigation-sidebar" <content-navigation :is-sidebar="true" class="navigation-sidebar__main" />
v-if="sidebar.navigation" <div class="navigation-sidebar__close-button" @click="close">
v-click-outside="close"
>
<content-navigation
:is-sidebar="true"
class="navigation-sidebar__main"
/>
<div
class="navigation-sidebar__close-button"
@click="close"
>
<cross class="navigation-sidebar__close-icon" /> <cross class="navigation-sidebar__close-icon" />
</div> </div>
</div> </div>
@ -20,93 +10,94 @@
</template> </template>
<script> <script>
import ContentNavigation from '@/components/book-navigation/ContentNavigation'; import ContentNavigation from '@/components/book-navigation/ContentNavigation';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
const Cross = () => import(/* webpackChunkName: "icons" */'@/components/icons/CrossIcon'); const Cross = () => import(/* webpackChunkName: "icons" */ '@/components/icons/CrossIcon');
export default { export default {
mixins: [sidebarMixin], mixins: [sidebarMixin],
components: { components: {
ContentNavigation, ContentNavigation,
Cross Cross,
},
methods: {
close() {
this.closeSidebar('navigation');
}, },
},
methods: { };
close() {
this.closeSidebar('navigation');
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
$desktop-width: 285px; $desktop-width: 285px;
.navigation-sidebar { .navigation-sidebar {
position: fixed; position: fixed;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
top: 0; top: 0;
background-color: white; background-color: white;
z-index: 20; z-index: 20;
@include desktop { @include desktop {
box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.12); box-shadow: 0px 2px 9px rgba(0, 0, 0, 0.12);
}
display: grid;
grid-template-columns: 1fr 50px;
grid-template-rows: 50px max-content auto 100px;
grid-template-areas: "m m" "m m" "s s" "s s";
&--with-subnavigation {
grid-template-areas: "m m" "m m" "sub sub" "s s";
}
height: 100vh;
overflow-y: auto;
@include desktop {
width: $desktop-width;
}
&__main {
padding: $medium-spacing;
grid-area: m;
}
&__main-link {
}
&__close-button {
grid-row: 1;
grid-column: 2;
align-self: center;
justify-self: center;
cursor: pointer;
}
} }
.slide { display: grid;
&-enter-active, &-leave-active {
transition: left 0.2s;
}
&-enter, &-leave-to { grid-template-columns: 1fr 50px;
left: -100vw; grid-template-rows: 50px max-content auto 100px;
@include desktop {
left: -$desktop-width; grid-template-areas: 'm m' 'm m' 's s' 's s';
}
&--with-subnavigation {
grid-template-areas: 'm m' 'm m' 'sub sub' 's s';
}
height: 100vh;
overflow-y: auto;
@include desktop {
width: $desktop-width;
}
&__main {
padding: $medium-spacing;
grid-area: m;
}
&__main-link {
}
&__close-button {
grid-row: 1;
grid-column: 2;
align-self: center;
justify-self: center;
cursor: pointer;
}
}
.slide {
&-enter-active,
&-leave-active {
transition: left 0.2s;
}
&-enter,
&-leave-to {
left: -100vw;
@include desktop {
left: -$desktop-width;
} }
} }
}
</style> </style>

View File

@ -1,54 +1,44 @@
<template> <template>
<div <div :class="{ 'sub-navigation-item--active': show }" class="sub-navigation-item" v-click-outside="close">
:class="{ 'sub-navigation-item--active': show}" <div class="sub-navigation-item__title" @click="show = !show">
class="sub-navigation-item"
v-click-outside="close"
>
<div
class="sub-navigation-item__title"
@click="show = !show"
>
{{ title }} {{ title }}
<chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" /> <chevron-down class="sub-navigation-item__icon sub-navigation-item__chevron-down" />
<chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" /> <chevron-up class="sub-navigation-item__icon sub-navigation-item__chevron-up" />
</div> </div>
<div <div class="sub-navigation-item__nav-items book-subnavigation" v-if="show">
class="sub-navigation-item__nav-items book-subnavigation"
v-if="show"
>
<slot /> <slot />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const ChevronDown = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronDown'); const ChevronDown = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronDown');
const ChevronUp = () => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronUp'); const ChevronUp = () => import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronUp');
export default { export default {
props: ['title'], props: ['title'],
components: { components: {
ChevronDown, ChevronDown,
ChevronUp ChevronUp,
},
data() {
return {
show: false,
};
},
watch: {
$route() {
this.show = false;
}, },
},
data() { methods: {
return { close() {
show: false this.show = false;
};
}, },
},
watch: { };
$route() {
this.show = false;
}
},
methods: {
close() {
this.show = false;
}
}
};
</script> </script>

View File

@ -1,8 +1,8 @@
<template> <template>
<nav class="topic-navigation"> <nav class="topic-navigation">
<router-link <router-link
:to="{name: 'topic', params: {topicSlug: topic.slug}}" :to="{ name: 'topic', params: { topicSlug: topic.slug } }"
:class="{'topic-navigation__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile}" :class="{ 'topic-navigation__topic--active': topic.active, 'book-subnavigation__item--mobile': mobile }"
tag="div" tag="div"
active-class="book-subnavigation__item--active" active-class="book-subnavigation__item--active"
class="topic-navigation__topic book-subnavigation__item" class="topic-navigation__topic book-subnavigation__item"
@ -17,49 +17,49 @@
</template> </template>
<script> <script>
import ALL_TOPICS_QUERY from '@/graphql/gql/queries/allTopicsQuery.gql'; import ALL_TOPICS_QUERY from '@/graphql/gql/queries/allTopicsQuery.gql';
import sidebarMixin from '@/mixins/sidebar'; import sidebarMixin from '@/mixins/sidebar';
export default { export default {
props: { props: {
mobile: { mobile: {
default: false default: false,
}
}, },
},
mixins: [sidebarMixin], mixins: [sidebarMixin],
data() { data() {
return { return {
topics: [] topics: [],
}; };
},
methods: {
topicId(id) {
return atob(id);
}, },
},
methods: { apollo: {
topicId(id) { topics: {
return atob(id); query: ALL_TOPICS_QUERY,
} manual: true,
}, result({ data, loading }) {
if (!loading) {
apollo: { this.topics = this.$getRidOfEdges(data).topics;
topics: {
query: ALL_TOPICS_QUERY,
manual: true,
result({data, loading}) {
if (!loading) {
this.topics = this.$getRidOfEdges(data).topics;
}
} }
} },
} },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.topic-navigation { .topic-navigation {
&__topic { &__topic {
}
} }
}
</style> </style>

View File

@ -1,39 +1,37 @@
<template> <template>
<a <a class="add-content-link" data-cy="add-content-link" @click="$emit('click')"
class="add-content-link" ><plus-icon class="add-content-link__icon" /> <span class="add-content-link__text">Inhalt hinzufügen</span></a
data-cy="add-content-link" >
@click="$emit('click')"
><plus-icon class="add-content-link__icon" /> <span class="add-content-link__text">Inhalt hinzufügen</span></a>
</template> </template>
<script> <script>
import PlusIcon from '@/components/icons/PlusIcon'; import PlusIcon from '@/components/icons/PlusIcon';
export default { export default {
components: { PlusIcon } components: { PlusIcon },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
$color: $color-silver-dark; $color: $color-silver-dark;
$icon-size: 14px; $icon-size: 14px;
.add-content-link { .add-content-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
cursor: pointer; cursor: pointer;
&__icon { &__icon {
width: $icon-size; width: $icon-size;
height: $icon-size; height: $icon-size;
margin-right: $small-spacing; margin-right: $small-spacing;
fill: $color; fill: $color;
} }
&__text { &__text {
// custom style, because the view needs this // custom style, because the view needs this
@include large-link; @include large-link;
color: $color; color: $color;
} }
} }
</style> </style>

View File

@ -1,10 +1,7 @@
<template> <template>
<div class="content-block-form content-list__parent"> <div class="content-block-form content-list__parent">
<div class="content-block-form__content"> <div class="content-block-form__content">
<h1 <h1 class="heading-1 content-block-form__heading" data-cy="content-block-form-heading">
class="heading-1 content-block-form__heading"
data-cy="content-block-form-heading"
>
{{ title }} {{ title }}
</h1> </h1>
@ -15,26 +12,21 @@
class="content-block-form__task-toggle" class="content-block-form__task-toggle"
label="Inhaltsblock als Auftrag formatieren" label="Inhaltsblock als Auftrag formatieren"
v-if="hasDefaultFeatures" v-if="hasDefaultFeatures"
@input="localContentBlock.isAssignment=$event" @input="localContentBlock.isAssignment = $event"
/> />
<!-- Form for title of content block --> <!-- Form for title of content block -->
<content-form-section <content-form-section data-cy="content-form-title-section" title="Titel (Pflichtfeld)">
data-cy="content-form-title-section"
title="Titel (Pflichtfeld)"
>
<input-with-label <input-with-label
:value="localContentBlock.title" :value="localContentBlock.title"
data-cy="content-block-title" data-cy="content-block-title"
placeholder="z.B. Auftrag 3" placeholder="z.B. Auftrag 3"
@input="localContentBlock.title=$event" @input="localContentBlock.title = $event"
/> />
</content-form-section> </content-form-section>
<!-- Add content at top of content block --> <!-- Add content at top of content block -->
<add-content-link <add-content-link @click="addBlock(-1)" />
@click="addBlock(-1)"
/>
<!-- Loop for outer contents layer --> <!-- Loop for outer contents layer -->
<div <div
@ -43,32 +35,21 @@
:key="block.id" :key="block.id"
> >
<!-- If the block is a content list --> <!-- If the block is a content list -->
<div <div class="content-block-form__segment" data-cy="content-list" v-if="block.type === 'content_list_item'">
class="content-block-form__segment"
data-cy="content-list"
v-if="block.type === 'content_list_item'"
>
<content-element-actions <content-element-actions
class="content-block-form__actions" class="content-block-form__actions"
:actions="{extended: true, up: outer>0, down: outer<localContentBlock.contents.length }" :actions="{ extended: true, up: outer > 0, down: outer < localContentBlock.contents.length }"
@remove="remove(outer)" @remove="remove(outer)"
@move-up="up(outer)" @move-up="up(outer)"
@move-down="down(outer)" @move-down="down(outer)"
@move-top="top(outer)" @move-top="top(outer)"
@move-bottom="bottom(outer)" @move-bottom="bottom(outer)"
/> />
<ol <ol class="content-list__item" data-cy="content-list-item">
class="content-list__item" <li class="content-block-form__segment" v-for="(content, index) in block.contents" :key="content.id">
data-cy="content-list-item"
>
<li
class="content-block-form__segment"
v-for="(content, index) in block.contents"
:key="content.id"
>
<content-element <content-element
:first-element="index===0" :first-element="index === 0"
:last-element="index===block.contents.length-1" :last-element="index === block.contents.length - 1"
:element="content" :element="content"
class="content-block-form__segment" class="content-block-form__segment"
:top-level="false" :top-level="false"
@ -80,10 +61,7 @@
@bottom="bottom(outer, index)" @bottom="bottom(outer, index)"
/> />
<add-content-link <add-content-link class="content-block-form__add-button" @click="addBlock(outer, index)" />
class="content-block-form__add-button"
@click="addBlock(outer, index)"
/>
</li> </li>
</ol> </ol>
</div> </div>
@ -93,8 +71,8 @@
:element="block" :element="block"
class="content-block-form__segment" class="content-block-form__segment"
:top-level="true" :top-level="true"
:first-element="outer===0" :first-element="outer === 0"
:last-element="outer===localContentBlock.contents.length-1" :last-element="outer === localContentBlock.contents.length - 1"
v-else v-else
@update="update(outer, $event)" @update="update(outer, $event)"
@remove="remove(outer, undefined, $event)" @remove="remove(outer, undefined, $event)"
@ -105,11 +83,10 @@
/> />
<!-- Add element after the looped item --> <!-- Add element after the looped item -->
<add-content-link <add-content-link @click="addBlock(outer)" />
@click="addBlock(outer)"
/>
</div> </div>
</div><!-- --> </div>
<!-- -->
<!-- Save and Cancel buttons --> <!-- Save and Cancel buttons -->
<footer class="content-block-form__footer"> <footer class="content-block-form__footer">
<div class="content-block-form__buttons"> <div class="content-block-form__buttons">
@ -121,309 +98,307 @@
> >
Speichern Speichern
</button> </button>
<a <a class="button" @click="$emit('back')">Abbrechen</a>
class="button"
@click="$emit('back')"
>Abbrechen</a>
</div> </div>
</footer> </footer>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, {PropType} from 'vue'; import Vue, { PropType } from 'vue';
import Toggle from '@/components/ui/Toggle.vue'; import Toggle from '@/components/ui/Toggle.vue';
import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue'; import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue';
import InputWithLabel from '@/components/ui/InputWithLabel.vue'; import InputWithLabel from '@/components/ui/InputWithLabel.vue';
import AddContentLink from '@/components/content-block-form/AddContentLink.vue'; import AddContentLink from '@/components/content-block-form/AddContentLink.vue';
import ContentElement from '@/components/content-block-form/ContentElement.vue'; import ContentElement from '@/components/content-block-form/ContentElement.vue';
import { import {
insertAtIndex, insertAtIndex,
moveToIndex, moveToIndex,
removeAtIndex, removeAtIndex,
replaceAtIndex, replaceAtIndex,
swapElements swapElements,
} from '@/graphql/immutable-operations'; } from '@/graphql/immutable-operations';
import {CHOOSER, transformInnerContents} from '@/components/content-block-form/helpers.js'; import { CHOOSER, transformInnerContents } from '@/components/content-block-form/helpers.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue'; import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
import {ContentBlock, numberOrUndefined} from "@/@types"; import { ContentBlock, numberOrUndefined } from '@/@types';
import {DEFAULT_FEATURE_SET} from "@/consts/features.consts"; import { DEFAULT_FEATURE_SET } from '@/consts/features.consts';
// TODO: refactor this file, it's huuuuuge! // TODO: refactor this file, it's huuuuuge!
interface ContentBlockFormData { interface ContentBlockFormData {
localContentBlock: any; localContentBlock: any;
} }
export default Vue.extend({ export default Vue.extend({
props: { props: {
title: { title: {
type: String, type: String,
default: '', default: '',
},
contentBlock: {
type: Object as PropType<ContentBlock>,
required: true,
},
features: {
type: String,
default: DEFAULT_FEATURE_SET
}
}, },
provide(): object { contentBlock: {
return { type: Object as PropType<ContentBlock>,
features: this.features required: true,
};
}, },
components: { features: {
ContentElementActions, type: String,
ContentElement, default: DEFAULT_FEATURE_SET,
AddContentLink,
InputWithLabel,
ContentFormSection,
Toggle,
}, },
data(): ContentBlockFormData { },
return { provide(): object {
localContentBlock: Object.assign({}, { return {
features: this.features,
};
},
components: {
ContentElementActions,
ContentElement,
AddContentLink,
InputWithLabel,
ContentFormSection,
Toggle,
},
data(): ContentBlockFormData {
return {
localContentBlock: Object.assign(
{},
{
title: this.contentBlock.title, title: this.contentBlock.title,
// contents: [...this.contentBlock.contents], // contents: [...this.contentBlock.contents],
contents: transformInnerContents([...this.contentBlock.contents]), contents: transformInnerContents([...this.contentBlock.contents]),
id: this.contentBlock.id || undefined, id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task', isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
}), }
}; ),
};
},
computed: {
isValid(): boolean {
return this.localContentBlock.title > '';
}, },
computed: { hasDefaultFeatures(): boolean {
isValid(): boolean { return this.features === DEFAULT_FEATURE_SET;
return this.localContentBlock.title > ''; },
}, },
hasDefaultFeatures(): boolean { methods: {
return this.features === DEFAULT_FEATURE_SET; update(index: number, element: any, parent?: number) {
if (parent === undefined) {
// element is top level
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, index, element);
} else {
const parentBlock = this.localContentBlock.contents[parent];
const newElementContents = replaceAtIndex(parentBlock.contents, index, element);
const newBlock = {
...parentBlock,
contents: newElementContents,
};
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, parent, newBlock);
} }
}, },
methods: { addBlock(afterOuterIndex: number, innerIndex?: number) {
update(index: number, element: any, parent?: number) { if (innerIndex !== undefined) {
if (parent === undefined) { const block = this.localContentBlock.contents[afterOuterIndex];
// element is top level const element = {
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, index, element); ...block,
} else { contents: insertAtIndex(block.contents, innerIndex + 1, {
const parentBlock = this.localContentBlock.contents[parent];
const newElementContents = replaceAtIndex(parentBlock.contents, index, element);
const newBlock = {
...parentBlock,
contents: newElementContents,
};
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, parent, newBlock);
}
},
addBlock(afterOuterIndex: number, innerIndex?: number) {
if (innerIndex !== undefined) {
const block = this.localContentBlock.contents[afterOuterIndex];
const element = {
...block,
contents: insertAtIndex(block.contents, innerIndex + 1, {
id: -1,
type: CHOOSER,
}),
};
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, afterOuterIndex, element);
} else {
const element = {
id: -1, id: -1,
type: CHOOSER, type: CHOOSER,
includeListOption: true, }),
}; };
this.localContentBlock.contents = insertAtIndex(this.localContentBlock.contents, afterOuterIndex + 1, element); this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, afterOuterIndex, element);
} } else {
}, const element = {
remove(outer: number, inner?: number, askForConfirmation = true) { id: -1,
if (askForConfirmation) { type: CHOOSER,
this.$modal.open('confirm') includeListOption: true,
.then(() => { };
this.executeRemoval(outer, inner);
})
.catch(() => {
}); this.localContentBlock.contents = insertAtIndex(this.localContentBlock.contents, afterOuterIndex + 1, element);
} else { }
this.executeRemoval(outer, inner); },
} remove(outer: number, inner?: number, askForConfirmation = true) {
}, if (askForConfirmation) {
shift(outer: number, inner: numberOrUndefined = undefined, distance: number) { this.$modal
if (inner === undefined) { .open('confirm')
this.localContentBlock.contents = swapElements(this.localContentBlock.contents, outer, outer + distance); .then(() => {
} else { this.executeRemoval(outer, inner);
const {contents} = this.localContentBlock; })
const outerElement = contents[outer]; .catch(() => {});
const newOuterElement = { } else {
...outerElement, this.executeRemoval(outer, inner);
contents: swapElements(outerElement.contents, inner, inner + distance), }
}; },
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement); shift(outer: number, inner: numberOrUndefined = undefined, distance: number) {
} if (inner === undefined) {
}, this.localContentBlock.contents = swapElements(this.localContentBlock.contents, outer, outer + distance);
top(outer: number, inner: numberOrUndefined = undefined) { } else {
if (inner === undefined) { const { contents } = this.localContentBlock;
this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, 0); const outerElement = contents[outer];
} else { const newOuterElement = {
const {contents} = this.localContentBlock; ...outerElement,
const outerElement = contents[outer]; contents: swapElements(outerElement.contents, inner, inner + distance),
const newOuterElement = { };
...outerElement, this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
contents: moveToIndex(outerElement.contents, inner, 0), }
}; },
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement); top(outer: number, inner: numberOrUndefined = undefined) {
} if (inner === undefined) {
}, this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, 0);
up(outer: number, inner: numberOrUndefined = undefined) { } else {
this.shift(outer, inner, -1); const { contents } = this.localContentBlock;
}, const outerElement = contents[outer];
down(outer: number, inner: numberOrUndefined = undefined) { const newOuterElement = {
this.shift(outer, inner, 1); ...outerElement,
}, contents: moveToIndex(outerElement.contents, inner, 0),
bottom(outer: number, inner: numberOrUndefined = undefined) { };
if (inner === undefined) { this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
const maxIndex = this.localContentBlock.contents.length - 1; }
this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, maxIndex); },
} else { up(outer: number, inner: numberOrUndefined = undefined) {
const {contents} = this.localContentBlock; this.shift(outer, inner, -1);
const outerElement = contents[outer]; },
const maxIndex = outerElement.contents.length - 1; down(outer: number, inner: numberOrUndefined = undefined) {
const newOuterElement = { this.shift(outer, inner, 1);
...outerElement, },
contents: moveToIndex(outerElement.contents, inner, maxIndex), bottom(outer: number, inner: numberOrUndefined = undefined) {
}; if (inner === undefined) {
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement); const maxIndex = this.localContentBlock.contents.length - 1;
} this.localContentBlock.contents = moveToIndex(this.localContentBlock.contents, outer, maxIndex);
}, } else {
executeRemoval(outer: number, inner: numberOrUndefined = undefined) { const { contents } = this.localContentBlock;
if (inner === undefined) { const outerElement = contents[outer];
// not a list item container, just remove the element from the outer array const maxIndex = outerElement.contents.length - 1;
this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer); const newOuterElement = {
} else { ...outerElement,
let prevInnerContents = this.localContentBlock.contents[outer].contents; contents: moveToIndex(outerElement.contents, inner, maxIndex),
let innerContents = removeAtIndex(prevInnerContents, inner); };
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
}
},
executeRemoval(outer: number, inner: numberOrUndefined = undefined) {
if (inner === undefined) {
// not a list item container, just remove the element from the outer array
this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer);
} else {
let prevInnerContents = this.localContentBlock.contents[outer].contents;
let innerContents = removeAtIndex(prevInnerContents, inner);
if (innerContents.length) { if (innerContents.length) {
/* /*
there is still an element inside the outer element after removal, there is still an element inside the outer element after removal,
so we replace the previous element in the outer array with the new one with fewer contents so we replace the previous element in the outer array with the new one with fewer contents
*/ */
let element = { let element = {
...this.localContentBlock.contents[outer], ...this.localContentBlock.contents[outer],
contents: innerContents, contents: innerContents,
}; };
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, outer, element); this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, outer, element);
} else { } else {
// inner contents is now empty, remove the whole element from the outer array // inner contents is now empty, remove the whole element from the outer array
this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer); this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer);
}
} }
}, }
save(contentBlock: ContentBlock) {
this.$emit('save', contentBlock);
},
}, },
save(contentBlock: ContentBlock) {
}); this.$emit('save', contentBlock);
},
},
});
</script> </script>
<style lang="scss"> <style lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
// override parent page properties // override parent page properties
.layout--no-scroll { .layout--no-scroll {
padding-bottom: 20px; padding-bottom: 20px;
height: 100vh; height: 100vh;
overflow-y: hidden; overflow-y: hidden;
box-sizing: border-box; box-sizing: border-box;
.module-page { .module-page {
height: 100vh; height: 100vh;
grid-template-rows: 60px 1fr; grid-template-rows: 60px 1fr;
padding-bottom: $small-spacing; padding-bottom: $small-spacing;
box-sizing: border-box; box-sizing: border-box;
}
} }
}
</style> </style>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-block-form { .content-block-form {
max-width: 100%; max-width: 100%;
display: grid; display: grid;
grid-template-columns: 100vw; grid-template-columns: 100vw;
grid-template-rows: 85vh 80px; grid-template-rows: 85vh 80px;
height: 100vh; height: 100vh;
grid-template-areas: grid-template-areas:
'content' 'content'
'footer'; 'footer';
@media (-webkit-min-device-pixel-ratio: 1.25) { @media (-webkit-min-device-pixel-ratio: 1.25) {
grid-template-rows: 80vh auto; grid-template-rows: 80vh auto;
height: auto; height: auto;
} }
&__heading {
@include heading-1;
}
&__heading { &__task-toggle {
@include heading-1; margin-bottom: $large-spacing;
} }
&__task-toggle { &__add-button {
margin-bottom: $large-spacing; }
}
&__add-button { &__segment {
} display: flex;
flex-direction: column;
align-items: stretch;
&__segment { margin-bottom: $large-spacing;
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: $large-spacing; :last-child {
margin-bottom: 0;
:last-child {
margin-bottom: 0;
}
}
&__actions {
align-self: flex-end;
}
&__content {
grid-area: content;
overflow-x: visible;
overflow-y: auto;
padding: 10px;
align-items: center;
display: flex;
flex-direction: column;
& > * { // we make an exception and use a wildcard here
width: 800px;
max-width: 100vw;
box-sizing: border-box;
}
}
&__footer {
padding: $medium-spacing 0;
border-top: 1px solid $color-silver;
margin-top: auto;
grid-area: footer;
justify-content: center;
display: flex;
}
&__buttons {
width: 800px;
} }
} }
&__actions {
align-self: flex-end;
}
&__content {
grid-area: content;
overflow-x: visible;
overflow-y: auto;
padding: 10px;
align-items: center;
display: flex;
flex-direction: column;
& > * {
// we make an exception and use a wildcard here
width: 800px;
max-width: 100vw;
box-sizing: border-box;
}
}
&__footer {
padding: $medium-spacing 0;
border-top: 1px solid $color-silver;
margin-top: auto;
grid-area: footer;
justify-content: center;
display: flex;
}
&__buttons {
width: 800px;
}
}
</style> </style>

View File

@ -9,7 +9,6 @@
@remove="$emit('remove', false)" @remove="$emit('remove', false)"
/> />
<!-- Content Forms --> <!-- Content Forms -->
<content-form-section <content-form-section
:title="title" :title="title"
@ -27,14 +26,10 @@
:class="['content-element__component']" :class="['content-element__component']"
v-bind="element" v-bind="element"
:is="component" :is="component"
@change-text="changeText" @change-text="changeText"
@link-change-url="changeUrl" @link-change-url="changeUrl"
@change-url="changeUrl" @change-url="changeUrl"
@switch-to-document="switchToDocument" @switch-to-document="switchToDocument"
@assignment-change-title="changeAssignmentTitle" @assignment-change-title="changeAssignmentTitle"
@assignment-change-assignment="changeAssignmentAssignment" @assignment-change-assignment="changeAssignmentAssignment"
/> />
@ -44,312 +39,321 @@
</template> </template>
<script> <script>
import ContentFormSection from '@/components/content-block-form/ContentFormSection'; import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import ContentElementActions from '@/components/content-block-form/ContentElementActions'; import ContentElementActions from '@/components/content-block-form/ContentElementActions';
const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'); const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
const ContentBlockElementChooserWidget = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ContentBlockElementChooserWidget'); const ContentBlockElementChooserWidget = () =>
const LinkForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/LinkForm'); import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ContentBlockElementChooserWidget');
const VideoForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/VideoForm'); const LinkForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/LinkForm');
const ImageForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/ImageForm'); const VideoForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/VideoForm');
const DocumentForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/DocumentForm'); const ImageForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/ImageForm');
const AssignmentForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/AssignmentForm'); const DocumentForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/DocumentForm');
const TextForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/TipTap.vue'); const AssignmentForm = () =>
const SubtitleForm = () => import(/* webpackChunkName: "content-forms" */'@/components/content-forms/SubtitleForm'); import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/AssignmentForm');
// readonly blocks const TextForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/TipTap.vue');
const Assignment = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/assignment/Assignment'); const SubtitleForm = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/SubtitleForm');
const SurveyBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/SurveyBlock'); // readonly blocks
const Solution = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Solution'); const Assignment = () =>
const ImageBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ImageBlock'); import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/assignment/Assignment');
const Instruction = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/Instruction'); const SurveyBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/SurveyBlock');
const ModuleRoomSlug = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ModuleRoomSlug'); const Solution = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Solution');
const CmsDocumentBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/CmsDocumentBlock'); const ImageBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ImageBlock');
const ThinglinkBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/ThinglinkBlock'); const Instruction = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/Instruction');
const InfogramBlock = () => import(/* webpackChunkName: "content-forms" */'@/components/content-blocks/InfogramBlock'); const ModuleRoomSlug = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ModuleRoomSlug');
const CmsDocumentBlock = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/CmsDocumentBlock');
const ThinglinkBlock = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/ThinglinkBlock');
const InfogramBlock = () => import(/* webpackChunkName: "content-forms" */ '@/components/content-blocks/InfogramBlock');
const CHOOSER = 'content-block-element-chooser-widget'; const CHOOSER = 'content-block-element-chooser-widget';
export default { export default {
props: { props: {
element: { element: {
type: Object, type: Object,
default: null, default: null,
},
// is this element at the top level, or is it nested? we assume top level
topLevel: {
type: Boolean,
default: true,
},
firstElement: {
type: Boolean,
required: true,
},
lastElement: {
type: Boolean,
required: true,
},
}, },
// is this element at the top level, or is it nested? we assume top level
components: { topLevel: {
ContentElementActions, type: Boolean,
ContentFormSection, default: true,
TrashIcon,
ContentBlockElementChooserWidget,
LinkForm,
VideoForm,
ImageForm,
DocumentForm,
AssignmentForm,
TextForm,
SubtitleForm,
SurveyBlock,
Solution,
ImageBlock,
Instruction,
ModuleRoomSlug,
CmsDocumentBlock,
InfogramBlock,
ThinglinkBlock,
Assignment
}, },
firstElement: {
computed: { type: Boolean,
actions() { required: true,
return {
up: !this.firstElement,
down: !this.lastElement,
extended: this.topLevel,
};
},
isChooser() {
return this.component === CHOOSER;
},
type() {
return this.getType(this.element);
},
component() {
return this.type.component;
},
title() {
return this.type.title;
},
icon() {
return this.type.icon;
},
}, },
lastElement: {
type: Boolean,
required: true,
},
},
methods: { components: {
getType(element) { ContentElementActions,
switch (element.type) { ContentFormSection,
case 'subtitle': TrashIcon,
return { ContentBlockElementChooserWidget,
component: 'subtitle-form', LinkForm,
title: 'Untertitel', VideoForm,
icon: 'title-icon', ImageForm,
}; DocumentForm,
case 'link_block': AssignmentForm,
return { TextForm,
component: 'link-form', SubtitleForm,
title: 'Link', SurveyBlock,
icon: 'link-icon', Solution,
}; ImageBlock,
case 'video_block': Instruction,
return { ModuleRoomSlug,
component: 'video-form', CmsDocumentBlock,
title: 'Video', InfogramBlock,
icon: 'video-icon', ThinglinkBlock,
}; Assignment,
case 'image_url_block': },
return {
component: 'image-form',
title: 'Bild',
icon: 'image-icon',
};
case 'text_block':
return {
component: 'text-form',
title: 'Text',
icon: 'text-icon',
};
case 'assignment':
return {
component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon',
};
case 'document_block':
return {
component: 'document-form',
title: 'Dokument',
icon: 'document-icon',
};
case 'survey':
return {
component: 'survey-block',
title: 'Übung',
};
case 'solution':
return {
component: 'solution',
title: 'Lösung',
};
case 'image_block':
return {
component: 'image-block',
title: 'Bild',
};
case 'instruction':
return {
component: 'instruction',
title: 'Instruktion',
};
case 'module_room_slug':
return {
component: 'module-room-slug',
title: 'Raum',
};
case 'cms_document_block':
return {
component: 'cms-document-block',
title: 'Dokument',
};
case 'thinglink_block':
return {
component: 'thinglink-block',
title: 'Interaktive Grafik'
};
case 'infogram_block':
return {
component: 'infogram-block',
title: 'Interaktive Grafik'
};
}
return {
component: CHOOSER,
title: '',
icon: '',
};
},
_updateProperty(value, key) {
// const content = this.localContentBlock.contents[index];
const content = this.element;
this.update({
...content,
value: {
...content.value,
[key]: value,
},
});
},
changeUrl(value) {
this._updateProperty(value, 'url');
},
changeText(value) {
this._updateProperty(value, 'text');
},
changeAssignmentTitle(value) {
this._updateProperty(value, 'title');
},
changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment');
},
changeType({type, convertToList}, value) {
let el = {
type: type,
value: Object.assign({}, value),
};
switch (type) {
case 'subtitle':
el = {
...el,
value: {
text: '',
},
};
break;
case 'text_block':
el = {
...el,
value: {
text: '',
},
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign({
url: '',
}, value),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) { computed: {
el = { actions() {
type: 'content_list_item', return {
contents: [el], up: !this.firstElement,
down: !this.lastElement,
extended: this.topLevel,
};
},
isChooser() {
return this.component === CHOOSER;
},
type() {
return this.getType(this.element);
},
component() {
return this.type.component;
},
title() {
return this.type.title;
},
icon() {
return this.type.icon;
},
},
methods: {
getType(element) {
switch (element.type) {
case 'subtitle':
return {
component: 'subtitle-form',
title: 'Untertitel',
icon: 'title-icon',
}; };
} case 'link_block':
this.update(el); return {
}, component: 'link-form',
update(element) { title: 'Link',
this.$emit('update', element); icon: 'link-icon',
}, };
switchToDocument(value) { case 'video_block':
this.changeType('document_block', value); return {
}, component: 'video-form',
title: 'Video',
icon: 'video-icon',
};
case 'image_url_block':
return {
component: 'image-form',
title: 'Bild',
icon: 'image-icon',
};
case 'text_block':
return {
component: 'text-form',
title: 'Text',
icon: 'text-icon',
};
case 'assignment':
return {
component: element.id ? 'assignment' : 'assignment-form', // prevent editing of existing assignments
title: 'Aufgabe & Ergebnis',
icon: 'speech-bubble-icon',
};
case 'document_block':
return {
component: 'document-form',
title: 'Dokument',
icon: 'document-icon',
};
case 'survey':
return {
component: 'survey-block',
title: 'Übung',
};
case 'solution':
return {
component: 'solution',
title: 'Lösung',
};
case 'image_block':
return {
component: 'image-block',
title: 'Bild',
};
case 'instruction':
return {
component: 'instruction',
title: 'Instruktion',
};
case 'module_room_slug':
return {
component: 'module-room-slug',
title: 'Raum',
};
case 'cms_document_block':
return {
component: 'cms-document-block',
title: 'Dokument',
};
case 'thinglink_block':
return {
component: 'thinglink-block',
title: 'Interaktive Grafik',
};
case 'infogram_block':
return {
component: 'infogram-block',
title: 'Interaktive Grafik',
};
}
return {
component: CHOOSER,
title: '',
icon: '',
};
}, },
}; _updateProperty(value, key) {
// const content = this.localContentBlock.contents[index];
const content = this.element;
this.update({
...content,
value: {
...content.value,
[key]: value,
},
});
},
changeUrl(value) {
this._updateProperty(value, 'url');
},
changeText(value) {
this._updateProperty(value, 'text');
},
changeAssignmentTitle(value) {
this._updateProperty(value, 'title');
},
changeAssignmentAssignment(value) {
this._updateProperty(value, 'assignment');
},
changeType({ type, convertToList }, value) {
let el = {
type: type,
value: Object.assign({}, value),
};
switch (type) {
case 'subtitle':
el = {
...el,
value: {
text: '',
},
};
break;
case 'text_block':
el = {
...el,
value: {
text: '',
},
};
break;
case 'link_block':
el = {
...el,
value: {
text: '',
url: '',
},
};
break;
case 'video_block':
el = {
...el,
value: {
url: '',
},
};
break;
case 'document_block':
el = {
...el,
value: Object.assign(
{
url: '',
},
value
),
};
break;
case 'image_url_block':
el = {
...el,
value: {
url: '',
},
};
break;
}
if (convertToList) {
el = {
type: 'content_list_item',
contents: [el],
};
}
this.update(el);
},
update(element) {
this.$emit('update', element);
},
switchToDocument(value) {
this.changeType('document_block', value);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-element { .content-element {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&__actions { &__actions {
display: inline-flex; display: inline-flex;
justify-self: flex-end; justify-self: flex-end;
align-self: flex-end; align-self: flex-end;
}
&__section {
display: grid;
//grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__chooser {
grid-column: 1 / span 2;
}
} }
&__section {
display: grid;
//grid-template-columns: 1fr 50px;
grid-auto-rows: auto;
/*width: 95%; // reserve space for scrollbar*/
}
&__chooser {
grid-column: 1 / span 2;
}
}
</style> </style>

View File

@ -1,17 +1,9 @@
<template> <template>
<div class="content-element-actions"> <div class="content-element-actions">
<button <button class="icon-button" @click.stop="toggle(true)">
class="icon-button"
@click.stop="toggle(true)"
>
<ellipses class="icon-button__icon" /> <ellipses class="icon-button__icon" />
</button> </button>
<widget-popover <widget-popover class="content-element-actions__popover" :no-padding="true" v-if="show" @hide-me="toggle(false)">
class="content-element-actions__popover"
:no-padding="true"
v-if="show"
@hide-me="toggle(false)"
>
<section class="content-element-actions__section"> <section class="content-element-actions__section">
<button-with-icon-and-text <button-with-icon-and-text
class="content-element-actions__button" class="content-element-actions__button"
@ -63,84 +55,82 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import WidgetPopover from '@/components/ui/WidgetPopover.vue'; import WidgetPopover from '@/components/ui/WidgetPopover.vue';
import Ellipses from '@/components/icons/Ellipses.vue'; import Ellipses from '@/components/icons/Ellipses.vue';
import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText.vue'; import ButtonWithIconAndText from '@/components/ui/ButtonWithIconAndText.vue';
import {ActionOptions} from "@/@types"; import { ActionOptions } from '@/@types';
interface Data { interface Data {
show: boolean; show: boolean;
} }
export default Vue.extend({ export default Vue.extend({
props: { props: {
actions: { actions: {
type: Object as () => ActionOptions type: Object as () => ActionOptions,
},
}, },
components: {ButtonWithIconAndText, Ellipses, WidgetPopover}, },
components: { ButtonWithIconAndText, Ellipses, WidgetPopover },
data: (): Data => ({ data: (): Data => ({
show: false, show: false,
}), }),
methods: { methods: {
toggle(show: boolean) { toggle(show: boolean) {
this.show = show; this.show = show;
},
close() {
this.show = false;
},
up() {
this.$emit('move-up');
this.close();
},
down() {
this.$emit('move-down');
this.close();
},
top() {
this.$emit('move-top');
this.close();
},
bottom() {
this.$emit('move-bottom');
this.close();
},
emitAndClose(event: string) {
this.$emit(event);
this.close();
}
}, },
}); close() {
this.show = false;
},
up() {
this.$emit('move-up');
this.close();
},
down() {
this.$emit('move-down');
this.close();
},
top() {
this.$emit('move-top');
this.close();
},
bottom() {
this.$emit('move-bottom');
this.close();
},
emitAndClose(event: string) {
this.$emit(event);
this.close();
},
},
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-element-actions { .content-element-actions {
position: relative; position: relative;
&__popover { &__popover {
white-space: nowrap; white-space: nowrap;
top: 100%; top: 100%;
transform: translateY($small-spacing); transform: translateY($small-spacing);
}
} &__section {
border-bottom: 1px solid $color-silver-dark;
padding: $medium-spacing $small-spacing;
&__section { &:last-child {
border-bottom: 1px solid $color-silver-dark; border-bottom: 0;
padding: $medium-spacing $small-spacing;
&:last-child {
border-bottom: 0;
}
}
&__button {
margin-bottom: $medium-spacing;
} }
} }
&__button {
margin-bottom: $medium-spacing;
}
}
</style> </style>

View File

@ -1,13 +1,8 @@
<template> <template>
<div class="content-form-section"> <div class="content-form-section">
<h2 class="content-form-section__heading"> <h2 class="content-form-section__heading">
<component <component class="content-form-section__icon" :is="icon" />
class="content-form-section__icon" <span class="content-form-section__title" data-cy="content-form-section-title">{{ title }}</span>
:is="icon"
/> <span
class="content-form-section__title"
data-cy="content-form-section-title"
>{{ title }}</span>
</h2> </h2>
<content-element-actions <content-element-actions
@ -27,74 +22,73 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import formElementIcons from '@/components/ui/form-element-icons.js'; import formElementIcons from '@/components/ui/form-element-icons.js';
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue'; import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
import {ActionOptions} from "@/@types"; import { ActionOptions } from '@/@types';
export default {
export default { props: {
props: { title: {
title: { type: String,
type: String, default: '',
default: ''
},
icon: {
type: String,
default: ''
},
actions: {
type: Object as () => ActionOptions,
default: () => {}
}
}, },
components: { icon: {
ContentElementActions, type: String,
...formElementIcons default: '',
} },
}; actions: {
type: Object as () => ActionOptions,
default: () => {},
},
},
components: {
ContentElementActions,
...formElementIcons,
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.content-form-section { .content-form-section {
@include default-box-shadow; @include default-box-shadow;
border-radius: $default-border-radius; border-radius: $default-border-radius;
padding: $small-spacing $medium-spacing; padding: $small-spacing $medium-spacing;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-template-rows: auto; grid-template-rows: auto;
grid-template-areas: 'h a' 'c c'; grid-template-areas: 'h a' 'c c';
align-items: center;
grid-row-gap: $medium-spacing;
&__heading {
display: flex;
align-items: center; align-items: center;
grid-row-gap: $medium-spacing; grid-area: h;
margin-bottom: 0;
&__heading {
display: flex;
align-items: center;
grid-area: h;
margin-bottom: 0;
}
&__actions {
grid-area: a;
justify-self: end;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__content {
grid-area: c;
}
} }
&__actions {
grid-area: a;
justify-self: end;
}
&__title {
@include heading-3;
margin-bottom: 0;
}
&__icon {
width: 28px;
height: 28px;
margin-right: $small-spacing;
}
&__content {
grid-area: c;
}
}
</style> </style>

View File

@ -18,166 +18,150 @@
/> />
</template> </template>
<add-content-element <add-content-element :index="-1" class="contents-form__add" @add-element="addElement" />
:index="-1" <div class="contents-form__element" v-for="(element, index) in localContentBlock.contents" :key="index">
class="contents-form__add" <content-element :element="element" @update="update(index, $event)" @remove="remove(index)" />
@add-element="addElement"
/>
<div
class="contents-form__element"
v-for="(element, index) in localContentBlock.contents"
:key="index"
>
<content-element
:element="element"
@update="update(index, $event)"
@remove="remove(index)"
/>
<add-content-element <add-content-element :index="index" class="contents-form__add" @add-element="addElement" />
:index="index"
class="contents-form__add"
@add-element="addElement"
/>
</div> </div>
<template #footer> <template #footer>
<div> <div>
<a <a
:class="{'button--disabled': disableSave}" :class="{ 'button--disabled': disableSave }"
class="button button--primary" class="button button--primary"
data-cy="modal-save-button" data-cy="modal-save-button"
@click="save" @click="save"
>Speichern</a> >Speichern</a
<a >
class="button" <a class="button" @click="$emit('hide')">Abbrechen</a>
@click="$emit('hide')"
>Abbrechen</a>
</div> </div>
</template> </template>
</modal> </modal>
</template> </template>
<script> <script>
import {meQuery} from '@/graphql/queries'; import { meQuery } from '@/graphql/queries';
const ModalInput = () => import(/* webpackChunkName: "content-forms" */'@/components/ModalInput'); const ModalInput = () => import(/* webpackChunkName: "content-forms" */ '@/components/ModalInput');
const AddContentElement = () => import(/* webpackChunkName: "content-forms" */'@/components/AddContentElement'); const AddContentElement = () => import(/* webpackChunkName: "content-forms" */ '@/components/AddContentElement');
const ContentElement = () => import(/* webpackChunkName: "content-forms" */'@/components/content-block-form/ContentElement'); const ContentElement = () =>
import(/* webpackChunkName: "content-forms" */ '@/components/content-block-form/ContentElement');
const Modal = () => import('@/components/Modal.vue'); const Modal = () => import('@/components/Modal.vue');
const Checkbox = () => import('@/components/ui/Checkbox.vue'); const Checkbox = () => import('@/components/ui/Checkbox.vue');
export default { export default {
props: { props: {
contentBlock: Object, contentBlock: Object,
blockType: { blockType: {
type: String, type: String,
default: 'ContentBlock', default: 'ContentBlock',
},
showTaskSelection: {
type: Boolean,
default: false,
},
disableSave: {
type: Boolean,
default: false,
},
}, },
showTaskSelection: {
components: { type: Boolean,
ContentElement, default: false,
Modal,
ModalInput,
AddContentElement,
Checkbox,
}, },
disableSave: {
type: Boolean,
default: false,
},
},
data() { components: {
return { ContentElement,
error: false, Modal,
localContentBlock: Object.assign({}, { ModalInput,
AddContentElement,
Checkbox,
},
data() {
return {
error: false,
localContentBlock: Object.assign(
{},
{
title: this.contentBlock.title, title: this.contentBlock.title,
contents: [...this.contentBlock.contents], contents: [...this.contentBlock.contents],
id: this.contentBlock.id || undefined, id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task', isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
}),
me: {},
};
},
apollo: {
me: meQuery,
},
computed: {
titlePlaceholder() {
return this.blockType === 'RoomEntry' ? 'Titel für Raumeintrag erfassen' : 'Titel für Inhaltsblock erfassen';
},
taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
},
},
methods: {
setContentBlockType(checked) {
this.localContentBlock.isAssignment = checked;
},
update(index, element) {
this.localContentBlock.contents.splice(index, 1, element);
},
save() {
if (!this.disableSave) {
if (!this.localContentBlock.title) {
this.error = true;
return false;
}
this.$emit('save', this.localContentBlock);
} }
}, ),
updateTitle(title) { me: {},
this.localContentBlock.title = title; };
this.error = false; },
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
apollo: {
me: meQuery,
},
computed: {
titlePlaceholder() {
return this.blockType === 'RoomEntry' ? 'Titel für Raumeintrag erfassen' : 'Titel für Inhaltsblock erfassen';
}, },
}; taskSelection() {
return this.showTaskSelection && this.me.permissions.includes('users.can_manage_school_class_content');
},
},
methods: {
setContentBlockType(checked) {
this.localContentBlock.isAssignment = checked;
},
update(index, element) {
this.localContentBlock.contents.splice(index, 1, element);
},
save() {
if (!this.disableSave) {
if (!this.localContentBlock.title) {
this.error = true;
return false;
}
this.$emit('save', this.localContentBlock);
}
},
updateTitle(title) {
this.localContentBlock.title = title;
this.error = false;
},
addElement(index) {
this.localContentBlock.contents.splice(index + 1, 0, {
hideAssignment: this.blockType !== 'ContentBlock',
});
},
remove(index) {
this.localContentBlock.contents.splice(index, 1);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.contents-form { .contents-form {
/* top level does not exist, because of the modal */ /* top level does not exist, because of the modal */
&__element { &__element {
}
&__element-component {
margin-bottom: 25px;
}
&__remove {
}
&__trash-icon {
}
&__add {
grid-column: 1 / span 2;
}
&__task {
margin: 15px 0 10px;
}
} }
&__element-component {
margin-bottom: 25px;
}
&__remove {
}
&__trash-icon {
}
&__add {
grid-column: 1 / span 2;
}
&__task {
margin: 15px 0 10px;
}
}
</style> </style>

View File

@ -1,77 +1,75 @@
<template> <template>
<contents-form <contents-form :content-block="contentBlock" :show-task-selection="true" @save="saveContentBlock" @hide="hideModal" />
:content-block="contentBlock"
:show-task-selection="true"
@save="saveContentBlock"
@hide="hideModal"
/>
</template> </template>
<script> <script>
import ContentsForm from '@/components/content-block-form/ContentsForm'; import ContentsForm from '@/components/content-block-form/ContentsForm';
import store from '@/store/index'; import store from '@/store/index';
import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql'; import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql'; import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import { setUserBlockType } from '@/helpers/content-block'; import { setUserBlockType } from '@/helpers/content-block';
export default { export default {
components: { components: {
ContentsForm ContentsForm,
},
data() {
return {
contentBlock: {},
};
},
created() {
// debugger;
},
methods: {
hideModal() {
this.$store.dispatch('resetCurrentNoteBlock');
this.$store.dispatch('hideModal');
}, },
saveContentBlock(contentBlock) {
data() { this.$apollo
return { .mutate({
contentBlock: {}
};
},
created() {
// debugger;
},
methods: {
hideModal() {
this.$store.dispatch('resetCurrentNoteBlock');
this.$store.dispatch('hideModal');
},
saveContentBlock(contentBlock) {
this.$apollo.mutate({
mutation: EDIT_CONTENT_BLOCK_MUTATION, mutation: EDIT_CONTENT_BLOCK_MUTATION,
variables: { variables: {
input: { input: {
contentBlock: { contentBlock: {
title: contentBlock.title, title: contentBlock.title,
contents: contentBlock.contents.filter(value => Object.keys(value).length > 0), contents: contentBlock.contents.filter((value) => Object.keys(value).length > 0),
type: setUserBlockType(contentBlock.isAssignment) type: setUserBlockType(contentBlock.isAssignment),
}, },
id: contentBlock.id id: contentBlock.id,
} },
}, },
refetchQueries: [{ refetchQueries: [
query: MODULE_DETAILS_QUERY, {
variables: { query: MODULE_DETAILS_QUERY,
slug: this.$route.params.slug variables: {
} slug: this.$route.params.slug,
}] },
}).then(() => { },
],
})
.then(() => {
this.hideModal(); this.hideModal();
}); });
}
}, },
},
apollo: { apollo: {
contentBlock() { contentBlock() {
return { return {
query: CONTENT_BLOCK_QUERY, query: CONTENT_BLOCK_QUERY,
variables: { variables: {
id: store.state.currentNoteBlock id: store.state.currentNoteBlock,
} },
}; };
} },
} },
};
};
</script> </script>

View File

@ -9,60 +9,62 @@
</template> </template>
<script> <script>
import ContentsForm from '@/components/content-block-form/ContentsForm'; import ContentsForm from '@/components/content-block-form/ContentsForm';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql'; import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {setUserBlockType} from '@/helpers/content-block'; import { setUserBlockType } from '@/helpers/content-block';
export default { export default {
components: { components: {
ContentsForm ContentsForm,
}, },
data() { data() {
return { return {
contentBlock: { contentBlock: {
title: '', title: '',
contents: [ contents: [{}],
{}
]
},
saving: false
};
},
methods: {
hideModal() {
this.$store.dispatch('resetContentBlockPosition');
this.$store.dispatch('hideModal');
}, },
saveContentBlock(contentBlock) { saving: false,
this.saving = true; };
this.$apollo.mutate({ },
methods: {
hideModal() {
this.$store.dispatch('resetContentBlockPosition');
this.$store.dispatch('hideModal');
},
saveContentBlock(contentBlock) {
this.saving = true;
this.$apollo
.mutate({
mutation: NEW_CONTENT_BLOCK_MUTATION, mutation: NEW_CONTENT_BLOCK_MUTATION,
variables: { variables: {
input: { input: {
contentBlock: { contentBlock: {
title: contentBlock.title, title: contentBlock.title,
contents: contentBlock.contents.filter(value => Object.keys(value).length > 0), contents: contentBlock.contents.filter((value) => Object.keys(value).length > 0),
type: setUserBlockType(contentBlock.isAssignment) type: setUserBlockType(contentBlock.isAssignment),
}, },
after: this.$store.state.contentBlockPosition.after, after: this.$store.state.contentBlockPosition.after,
parent: this.$store.state.contentBlockPosition.parent parent: this.$store.state.contentBlockPosition.parent,
} },
}, },
refetchQueries: [{ refetchQueries: [
query: MODULE_DETAILS_QUERY, {
variables: { query: MODULE_DETAILS_QUERY,
slug: this.$route.params.slug variables: {
} slug: this.$route.params.slug,
}] },
}).then(() => { },
],
})
.then(() => {
this.saving = false; this.saving = false;
this.hideModal(); this.hideModal();
}); });
}
}, },
}; },
};
</script> </script>

View File

@ -1,39 +1,46 @@
export const CHOOSER = 'content-block-element-chooser-widget'; export const CHOOSER = 'content-block-element-chooser-widget';
export const chooserFilter = value => value.type !== CHOOSER; export const chooserFilter = (value) => value.type !== CHOOSER;
export const USER_CONTENT_TYPES = ['subtitle', 'link_block', 'video_block', 'image_url_block', 'text_block', 'assignment', 'document_block']; export const USER_CONTENT_TYPES = [
'subtitle',
'link_block',
'video_block',
'image_url_block',
'text_block',
'assignment',
'document_block',
];
/* /*
Users can only edit certain types of contents, the rest can only be re-ordered. We only care about their id, we won't Users can only edit certain types of contents, the rest can only be re-ordered. We only care about their id, we won't
send anything else to the server about them send anything else to the server about them
*/ */
export const simplifyContents = (contents) => { export const simplifyContents = (contents) => {
return contents.map(c => { return contents.map((c) => {
if (USER_CONTENT_TYPES.includes(c.type)) { if (USER_CONTENT_TYPES.includes(c.type)) {
return c; return c;
} }
if (c.type === 'content_list_item') { if (c.type === 'content_list_item') {
return { return {
...c, ...c,
contents: simplifyContents(c.contents) contents: simplifyContents(c.contents),
}; };
} }
return { return {
id: c.id, id: c.id,
type: 'readonly' type: 'readonly',
}; };
}); });
}; };
export const cleanUpContents = (contents) => { export const cleanUpContents = (contents) => {
let filteredContents = contents let filteredContents = contents.filter(chooserFilter); // only use items that are not chooser elements
.filter(chooserFilter); // only use items that are not chooser elements
return filteredContents.map(content => { return filteredContents.map((content) => {
// if the element has a contents property, it's a list of contents, filter them // if the element has a contents property, it's a list of contents, filter them
if (content.contents) { if (content.contents) {
return { return {
...content, ...content,
contents: content.contents.filter(chooserFilter) contents: content.contents.filter(chooserFilter),
}; };
} }
// else just return it // else just return it
@ -44,16 +51,16 @@ export const cleanUpContents = (contents) => {
// transform value prop to contents, to better handle the input type on the server // transform value prop to contents, to better handle the input type on the server
export const transformInnerContents = (contents) => { export const transformInnerContents = (contents) => {
let ret = []; let ret = [];
for (let content of contents) { for (let content of contents) {
if (Array.isArray(content.value)) { if (Array.isArray(content.value)) {
const {value, ...contentWithoutValue} = content; const { value, ...contentWithoutValue } = content;
ret.push({ ret.push({
...contentWithoutValue, ...contentWithoutValue,
contents: value contents: value,
}); });
} else { } else {
ret.push(content); ret.push(content);
} }
} }
return ret; return ret;
}; };

View File

@ -1,66 +1,57 @@
<template> <template>
<div <div :class="{ 'cms-document-block--solution': solution }" class="cms-document-block">
:class="{'cms-document-block--solution': solution}"
class="cms-document-block"
>
<document-icon class="cms-document-block__icon" /> <document-icon class="cms-document-block__icon" />
<a <a :href="value.url" class="cms-document-block__link" target="_blank">{{ value.display_text }}</a>
:href="value.url"
class="cms-document-block__link"
target="_blank"
>{{ value.display_text }}</a>
</div> </div>
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
export default { export default {
props: { props: {
value: Object, value: Object,
solution: { solution: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
},
components: { components: {
DocumentIcon, DocumentIcon,
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.cms-document-block { .cms-document-block {
display: grid; display: grid;
grid-template-columns: 50px 1fr 50px; grid-template-columns: 50px 1fr 50px;
align-items: center; align-items: center;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
&__icon { &__icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
}
&__link {
text-decoration: underline;
}
$parent: &;
&--solution {
margin-bottom: $small-spacing;
#{$parent}__link {
color: $color-silver-dark;
} }
#{$parent}__icon {
fill: $color-silver-dark;
&__link {
text-decoration: underline;
}
$parent: &;
&--solution {
margin-bottom: $small-spacing;
#{$parent}__link {
color: $color-silver-dark;
}
#{$parent}__icon {
fill: $color-silver-dark;
}
} }
} }
}
</style> </style>

View File

@ -1,9 +1,5 @@
<template> <template>
<div <div :class="componentClass" :data-scrollto="component.id" data-cy="content-component">
:class="componentClass"
:data-scrollto="component.id"
data-cy="content-component"
>
<bookmark-actions <bookmark-actions
:bookmarked="bookmarked" :bookmarked="bookmarked"
:note="note" :note="note"
@ -13,96 +9,105 @@
@edit-note="editNote" @edit-note="editNote"
@bookmark="bookmarkContent(component.id, !bookmarked)" @bookmark="bookmarkContent(component.id, !bookmarked)"
/> />
<component <component v-bind="component" :parent="parent" :is="component.type" />
v-bind="component"
:parent="parent"
:is="component.type"
/>
</div> </div>
</template> </template>
<script> <script>
import {constructContentComponentBookmarkMutation} from '@/helpers/update-content-bookmark-mutation'; import { constructContentComponentBookmarkMutation } from '@/helpers/update-content-bookmark-mutation';
const TextBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/TextBlock'); const TextBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/TextBlock');
const InstrumentWidget = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InstrumentWidget'); const InstrumentWidget = () =>
const ImageBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InstrumentWidget');
const ImageUrlBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ImageUrlBlock'); const ImageBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ImageBlock');
const VideoBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/VideoBlock'); const ImageUrlBlock = () =>
const LinkBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/LinkBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ImageUrlBlock');
const DocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'); const VideoBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/VideoBlock');
const CmsDocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/CmsDocumentBlock'); const LinkBlock = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/LinkBlock');
const InfogramBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/InfogramBlock'); const DocumentBlock = () =>
const ThinglinkBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ThinglinkBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock');
const GeniallyBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/GeniallyBlock'); const CmsDocumentBlock = () =>
const SubtitleBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SubtitleBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/CmsDocumentBlock');
const SectionTitleBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SectionTitleBlock'); const InfogramBlock = () =>
const ContentListBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentListBlock'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/InfogramBlock');
const ModuleRoomSlug = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ModuleRoomSlug'); const ThinglinkBlock = () =>
const Assignment = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/Assignment'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ThinglinkBlock');
const Survey = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/SurveyBlock'); const GeniallyBlock = () =>
const Solution = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/GeniallyBlock');
const Instruction = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Instruction'); const SubtitleBlock = () =>
const BookmarkActions = () => import(/* webpackChunkName: "content-components" */'@/components/notes/BookmarkActions'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SubtitleBlock');
const SectionTitleBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SectionTitleBlock');
const ContentListBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentListBlock');
const ModuleRoomSlug = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ModuleRoomSlug');
const Assignment = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/Assignment');
const Survey = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/SurveyBlock');
const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution');
const Instruction = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Instruction');
const BookmarkActions = () => import(/* webpackChunkName: "content-components" */ '@/components/notes/BookmarkActions');
export default { export default {
props: { props: {
component: { component: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
parent: { parent: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
bookmarks: { bookmarks: {
type: Array, type: Array,
default: () => ([]) default: () => [],
}, },
notes: { notes: {
type: Array, type: Array,
default: () => ([]) default: () => [],
}, },
root: { root: {
type: String, type: String,
default: '' default: '',
}, },
editMode: { editMode: {
type: Boolean, type: Boolean,
default: false default: false,
} },
}, },
components: { components: {
'text_block': TextBlock, text_block: TextBlock,
'basic_knowledge': InstrumentWidget, // for legacy basic_knowledge: InstrumentWidget, // for legacy
'instrument': InstrumentWidget, instrument: InstrumentWidget,
'image_block': ImageBlock, image_block: ImageBlock,
'image_url_block': ImageUrlBlock, image_url_block: ImageUrlBlock,
'video_block': VideoBlock, video_block: VideoBlock,
'link_block': LinkBlock, link_block: LinkBlock,
'document_block': DocumentBlock, document_block: DocumentBlock,
'infogram_block': InfogramBlock, infogram_block: InfogramBlock,
'genially_block': GeniallyBlock, genially_block: GeniallyBlock,
'subtitle': SubtitleBlock, subtitle: SubtitleBlock,
'section_title': SectionTitleBlock, section_title: SectionTitleBlock,
'content_list': ContentListBlock, content_list: ContentListBlock,
'module_room_slug': ModuleRoomSlug, module_room_slug: ModuleRoomSlug,
'thinglink_block': ThinglinkBlock, thinglink_block: ThinglinkBlock,
'cms_document_block': CmsDocumentBlock, cms_document_block: CmsDocumentBlock,
Survey, Survey,
Solution, Solution,
Instruction, Instruction,
Assignment, Assignment,
BookmarkActions BookmarkActions,
}, },
computed: { computed: {
bookmarked() { bookmarked() {
return this.bookmarks && !!this.bookmarks.find(bookmark => bookmark.uuid === this.component.id); return this.bookmarks && !!this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id);
}, },
note() { note() {
const bookmark = this.bookmarks && this.bookmarks.find(bookmark => bookmark.uuid === this.component.id); const bookmark = this.bookmarks && this.bookmarks.find((bookmark) => bookmark.uuid === this.component.id);
return bookmark && bookmark.note; return bookmark && bookmark.note;
}, },
showBookmarkActions() { showBookmarkActions() {
@ -114,18 +119,19 @@ export default {
classes.push('content-component--bookmarked'); classes.push('content-component--bookmarked');
} }
return classes; return classes;
} },
}, },
methods: { methods: {
addNote(id) { addNote(id) {
const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename') const type = Object.prototype.hasOwnProperty.call(this.parent, '__typename')
? this.parent.__typename : 'ContentBlockNode'; ? this.parent.__typename
: 'ContentBlockNode';
this.$store.dispatch('addNote', { this.$store.dispatch('addNote', {
content: id, content: id,
type, type,
block: this.root block: this.root,
}); });
}, },
editNote() { editNote() {
@ -133,37 +139,36 @@ export default {
}, },
bookmarkContent(uuid, bookmarked) { bookmarkContent(uuid, bookmarked) {
this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root)); this.$apollo.mutate(constructContentComponentBookmarkMutation(uuid, bookmarked, this.parent, this.root));
} },
} },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~styles/helpers"; @import '~styles/helpers';
.content-component { .content-component {
position: relative; position: relative;
&--bookmarked { &--bookmarked {
}
&--subtitle {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--section_title {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--text_block {
margin-bottom: $large-spacing;
}
&--document_block {
margin-bottom: $large-spacing;
}
} }
&--subtitle {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--section_title {
margin-top: $section-spacing;
margin-bottom: $large-spacing;
}
&--text_block {
margin-bottom: $large-spacing;
}
&--document_block {
margin-bottom: $large-spacing;
}
}
</style> </style>

View File

@ -1,14 +1,7 @@
<template> <template>
<ol class="content-list"> <ol class="content-list">
<li <li class="content-list__item" v-for="(item, index) in items" :key="item.id">
class="content-list__item" <slot :item="item" :index="index">
v-for="(item, index) in items"
:key="item.id"
>
<slot
:item="item"
:index="index"
>
{{ item.id }} {{ item.id }}
</slot> </slot>
</li> </li>
@ -16,13 +9,13 @@
</template> </template>
<script> <script>
export default { export default {
// //
props: { props: {
items: { items: {
type: Array, type: Array,
default: () => ([]) default: () => [],
}
}, },
}; },
};
</script> </script>

View File

@ -1,47 +1,40 @@
<template> <template>
<content-list <content-list :items="contentBlocks">
:items="contentBlocks"
>
<template #default="{ item }"> <template #default="{ item }">
<content-block <content-block :content-block="item" :parent="parent" />
:content-block="item"
:parent="parent"
/>
</template> </template>
</content-list> </content-list>
</template> </template>
<script> <script>
import ContentList from '@/components/content-blocks/ContentList.vue'; import ContentList from '@/components/content-blocks/ContentList.vue';
export default { export default {
name: 'ContentBlockList', name: 'ContentBlockList',
props: ['contents', 'parent'], props: ['contents', 'parent'],
components: { components: {
ContentList, ContentList,
// https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components // https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
ContentBlock: () => import('@/components/ContentBlock.vue') ContentBlock: () => import('@/components/ContentBlock.vue'),
}, },
computed: { computed: {
contentBlocks() { contentBlocks() {
return this.contents.map(contentBlock => { return this.contents.map((contentBlock) => {
const contents = contentBlock.value ? [...contentBlock.value] : []; const contents = contentBlock.value ? [...contentBlock.value] : [];
return Object.assign({}, contentBlock, { return Object.assign({}, contentBlock, {
contents, contents,
indent: true, indent: true,
bookmarks: this.parent.bookmarks, bookmarks: this.parent.bookmarks,
notes: this.parent.notes, notes: this.parent.notes,
root: this.parent.id root: this.parent.id,
});
}); });
} });
}, },
},
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
</style> </style>

View File

@ -1,79 +1,71 @@
<template> <template>
<div class="document-block"> <div class="document-block">
<document-icon class="document-block__icon" /> <document-icon class="document-block__icon" />
<a <a :href="value.url" class="document-block__link" target="_blank">{{ urlName }}</a>
:href="value.url" <a class="document-block__remove" v-if="showTrashIcon" @click="$emit('trash')">
class="document-block__link"
target="_blank"
>{{ urlName }}</a>
<a
class="document-block__remove"
v-if="showTrashIcon"
@click="$emit('trash')"
>
<trash-icon class="document-block__trash-icon" /> <trash-icon class="document-block__trash-icon" />
</a> </a>
</div> </div>
</template> </template>
<script> <script>
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
const TrashIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TrashIcon'); const TrashIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TrashIcon');
export default { export default {
props: { props: {
value: Object, value: Object,
showTrashIcon: Boolean, showTrashIcon: Boolean,
}, },
components: { components: {
DocumentIcon, DocumentIcon,
TrashIcon, TrashIcon,
}, },
computed: { computed: {
urlName: function() { urlName: function () {
if (this.value && this.value.url) { if (this.value && this.value.url) {
const parts = this.value.url.split('/'); const parts = this.value.url.split('/');
return parts[parts.length - 1]; return parts[parts.length - 1];
}
return null;
} }
} return null;
}; },
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.document-block { .document-block {
display: grid; display: grid;
grid-template-columns: 50px 1fr 50px; grid-template-columns: 50px 1fr 50px;
align-items: center; align-items: center;
&__icon { &__icon {
width: 30px; width: 30px;
height: 30px; height: 30px;
}
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
}
} }
&__link {
text-decoration: underline;
}
&__remove {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 50px;
}
&__trash-icon {
width: 25px;
height: 25px;
fill: $color-silver-dark;
cursor: pointer;
justify-self: center;
}
}
</style> </style>

View File

@ -18,51 +18,51 @@
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
computed: { computed: {
src() { src() {
return `https://view.genial.ly/${this.value.id}`; return `https://view.genial.ly/${this.value.id}`;
}
}, },
},
methods: { methods: {
openFullscreen() { openFullscreen() {
this.$store.dispatch('showFullscreenInfographic', { this.$store.dispatch('showFullscreenInfographic', {
id: this.value.id, id: this.value.id,
type: 'genially-block' type: 'genially-block',
}); });
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
// Styling and structure taken from original iframe // Styling and structure taken from original iframe
.genially-block { .genially-block {
width: 100%; width: 100%;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
&__wrapper { &__wrapper {
position: relative; position: relative;
padding-bottom: 75%; padding-bottom: 75%;
padding-top: 0; padding-top: 0;
height: 0; height: 0;
}
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__link {
cursor: pointer;
}
} }
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__link {
cursor: pointer;
}
}
</style> </style>

View File

@ -1,31 +1,25 @@
<template> <template>
<img <img :src="value.path" alt="" class="image-block" @click="openFullscreen" />
:src="value.path"
alt=""
class="image-block"
@click="openFullscreen"
>
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
methods: { methods: {
openFullscreen() { openFullscreen() {
this.$store.dispatch('showFullscreenImage', this.value.path); this.$store.dispatch('showFullscreenImage', this.value.path);
} },
} },
};
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.image-block { .image-block {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border-radius: 13px; border-radius: 13px;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
} }
</style> </style>

View File

@ -1,30 +1,24 @@
<template> <template>
<img <img :src="value.url" alt="" class="image-block" @click="openFullscreen" />
:src="value.url"
alt=""
class="image-block"
@click="openFullscreen"
>
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
methods: { methods: {
openFullscreen() { openFullscreen() {
this.$store.dispatch('showFullscreenImage', this.value.url); this.$store.dispatch('showFullscreenImage', this.value.url);
} },
} },
};
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.image-block { .image-block {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
border-radius: 13px; border-radius: 13px;
margin-bottom: 30px; margin-bottom: 30px;
} }
</style> </style>

View File

@ -7,79 +7,79 @@
class="infogram-block__iframe" class="infogram-block__iframe"
scrolling="no" scrolling="no"
frameborder="0" frameborder="0"
style="border:none;" style="border: none"
/> />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
data() { data() {
return { return {
height: 1 height: 1,
}; };
},
computed: {
src() {
return `https://e.infogram.com/${this.id}?src=embed`;
}, },
href() {
computed: { return `https://infogram.com/${this.id}`;
src() {
return `https://e.infogram.com/${this.id}?src=embed`;
},
href() {
return `https://infogram.com/${this.id}`;
},
id() {
return this.value.id;
},
title() {
return this.value.title || 'Infografik';
}
}, },
id() {
return this.value.id;
},
title() {
return this.value.title || 'Infografik';
},
},
mounted() { mounted() {
// from https://developers.infogr.am/oembed/ // from https://developers.infogr.am/oembed/
window.addEventListener('message', event => { window.addEventListener('message', (event) => {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.context === 'iframe.resize' && this.parseId(data.src) === this.id) { if (data.context === 'iframe.resize' && this.parseId(data.src) === this.id) {
this.height = data.height; this.height = data.height;
}
} catch (e) {
return false;
} }
} catch (e) {
return false;
}
});
},
methods: {
parseId(src) {
// src will be in the format of something like https://e.infogram.com/0ccf86bc-1afe-4026-b313-1f1b5992452b?src=embed
let last = src.split('/').pop();
return last.substring(0, last.indexOf('?')); // we're only interested in the id part before the '?'
},
openFullscreen() {
this.$store.dispatch('showFullscreenInfographic', {
id: this.value.id,
type: 'infogram-block',
}); });
}, },
},
methods: { };
parseId(src) {
// src will be in the format of something like https://e.infogram.com/0ccf86bc-1afe-4026-b313-1f1b5992452b?src=embed
let last = src.split('/').pop();
return last.substring(0, last.indexOf('?')); // we're only interested in the id part before the '?'
},
openFullscreen() {
this.$store.dispatch('showFullscreenInfographic', {
id: this.value.id,
type: 'infogram-block'
});
}
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.infogram-block { .infogram-block {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
&__link { &__link {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
}
&__iframe {
width: 100%;
}
} }
&__iframe {
width: 100%;
}
}
</style> </style>

View File

@ -1,56 +1,50 @@
<template> <template>
<div <div class="instruction" v-if="me.isTeacher">
class="instruction"
v-if="me.isTeacher"
>
<bulb-icon class="instruction__icon" /> <bulb-icon class="instruction__icon" />
<a <a :href="url" class="instruction__link">{{ text }}</a>
:href="url"
class="instruction__link"
>{{ text }}</a>
</div> </div>
</template> </template>
<script> <script>
import me from '@/mixins/me'; import me from '@/mixins/me';
const BulbIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/BulbIcon'); const BulbIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/BulbIcon');
export default { export default {
props: ['value'], props: ['value'],
mixins: [me], mixins: [me],
components: { components: {
BulbIcon BulbIcon,
},
computed: {
text() {
return this.value.text ? this.value.text : 'Anweisungen';
}, },
url() {
computed: { return this.value.document ? this.value.document.url : this.value.url;
text() { },
return this.value.text ? this.value.text : 'Anweisungen'; },
}, };
url() {
return this.value.document ? this.value.document.url : this.value.url;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.instruction { .instruction {
margin-bottom: 1rem; margin-bottom: 1rem;
display: flex; display: flex;
align-items: center; align-items: center;
&__icon { &__icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin-right: $small-spacing; margin-right: $small-spacing;
}
&__link {
@include heading-3;
}
} }
&__link {
@include heading-3;
}
}
</style> </style>

View File

@ -1,15 +1,12 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="instrument-widget"> <div class="instrument-widget">
<div <div class="instrument-widget__description" v-html="value.description" />
class="instrument-widget__description"
v-html="value.description"
/>
<router-link <router-link
:to="{name: 'instrument', params: { slug: value.slug }}" :to="{ name: 'instrument', params: { slug: value.slug } }"
class="instrument-widget__button button" class="instrument-widget__button button"
:style="{ :style="{
borderColor: value.foreground borderColor: value.foreground,
}" }"
> >
{{ $flavor.textInstrument }} anzeigen {{ $flavor.textInstrument }} anzeigen
@ -18,24 +15,23 @@
</template> </template>
<script> <script>
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css // todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
export default { export default {
props: ['value'], props: ['value'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/_variables.scss"; @import '~styles/_variables.scss';
.instrument-widget { .instrument-widget {
margin-bottom: $small-spacing; margin-bottom: $small-spacing;
&__description { &__description {
margin-bottom: 25px; margin-bottom: 25px;
}
&__button {
}
} }
&__button {
}
}
</style> </style>

View File

@ -1,59 +1,52 @@
<template> <template>
<div <div :class="{ 'link-block--no-margin': noMargin }" class="link-block">
:class="{ 'link-block--no-margin': noMargin}"
class="link-block"
>
<link-icon class="link-block__icon" /> <link-icon class="link-block__icon" />
<a <a :href="href" class="link-block__link" target="_blank">{{ value.text }}</a>
:href="href"
class="link-block__link"
target="_blank"
>{{ value.text }}</a>
</div> </div>
</template> </template>
<script> <script>
const LinkIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/LinkIcon'); const LinkIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LinkIcon');
export default { export default {
props: { props: {
value: Object, value: Object,
noMargin: { noMargin: {
default: false default: false,
}
}, },
},
components: { components: {
LinkIcon LinkIcon,
},
computed: {
href() {
const url = this.value.url;
return url.startsWith('http') ? this.value.url : `http://${this.value.url}`;
}, },
},
computed: { };
href() {
const url = this.value.url;
return url.startsWith('http') ? this.value.url : `http://${this.value.url}`;
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.link-block { .link-block {
margin-bottom: 30px; margin-bottom: 30px;
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-columns: 50px 1fr;
align-items: center; align-items: center;
&--no-margin { &--no-margin {
margin-bottom: 0; margin-bottom: 0;
}
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
} }
&__icon {
width: 30px;
height: 30px;
}
&__link {
text-decoration: underline;
}
}
</style> </style>

View File

@ -1,24 +1,21 @@
<template> <template>
<div class="module-slug"> <div class="module-slug">
<router-link <router-link :to="{ name: 'moduleRoom', params: { slug: value.slug } }" class="button button--primary">
:to="{name: 'moduleRoom', params: { slug: value.slug }}"
class="button button--primary"
>
Raum anzeigen Raum anzeigen
</router-link> </router-link>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value'] props: ['value'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.module-slug { .module-slug {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
} }
</style> </style>

View File

@ -1,23 +1,20 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<h4 <h4 class="section-title" v-html="value.text" />
class="section-title"
v-html="value.text"
/>
</template> </template>
<script> <script>
export default { export default {
props: ['value'] props: ['value'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.section-title { .section-title {
margin-bottom: 30px; margin-bottom: 30px;
@include heading-line-height; @include heading-line-height;
} }
</style> </style>

View File

@ -1,114 +1,95 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div class="solution" data-cy="solution">
class="solution" <a class="solution__toggle" data-cy="show-solution" @click="toggle"
data-cy="solution" >Lösung
>
<a
class="solution__toggle"
data-cy="show-solution"
@click="toggle"
>Lösung
<template v-if="!visible">anzeigen</template> <template v-if="!visible">anzeigen</template>
<template v-else>ausblenden</template> <template v-else>ausblenden</template>
</a> </a>
<transition name="fade"> <transition name="fade">
<div <div class="solution__hidden fade" v-if="visible">
class="solution__hidden fade" <p class="solution__text solution-text" data-cy="solution-text" v-html="sanitizedText" />
v-if="visible" <cms-document-block :solution="true" class="solution__document" :value="value.document" v-if="value.document" />
>
<p
class="solution__text solution-text"
data-cy="solution-text"
v-html="sanitizedText"
/>
<cms-document-block
:solution="true"
class="solution__document"
:value="value.document"
v-if="value.document"
/>
</div> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script>
import {sanitizeAsHtml} from '@/helpers/text'; import { sanitizeAsHtml } from '@/helpers/text';
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock'; import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
export default { export default {
props: ['value'], props: ['value'],
components: {CmsDocumentBlock}, components: { CmsDocumentBlock },
data() { data() {
return { return {
visible: false, visible: false,
}; };
},
computed: {
sanitizedText() {
return sanitizeAsHtml(this.value.text);
}, },
},
computed: { methods: {
sanitizedText() { toggle() {
return sanitizeAsHtml(this.value.text); this.visible = !this.visible;
},
}, },
},
methods: { };
toggle() {
this.visible = !this.visible;
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.solution { .solution {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: 15px; grid-row-gap: 15px;
margin-bottom: 1rem; margin-bottom: 1rem;
&__toggle { &__toggle {
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
color: $color-silver-dark; color: $color-silver-dark;
font-size: toRem(15px); font-size: toRem(15px);
/*margin-bottom: 15px;*/ /*margin-bottom: 15px;*/
display: block; display: block;
cursor: pointer; cursor: pointer;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
} }
&__text { &__text {
font-size: toRem(18px);
color: $color-silver-dark;
:deep(p) {
font-size: toRem(18px); font-size: toRem(18px);
color: $color-silver-dark; color: $color-silver-dark;
}
:deep(p) { :deep(ul) {
font-size: toRem(18px); padding-left: $medium-spacing;
> li {
list-style: disc outside none;
color: $color-silver-dark; color: $color-silver-dark;
} }
:deep(ul) {
padding-left: $medium-spacing;
> li {
list-style: disc outside none;
color: $color-silver-dark;
}
}
} }
} }
}
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity .3s; transition: opacity 0.3s;
} }
.fade-enter, .fade-enter,
.fade-leave-active { .fade-leave-active {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -1,33 +1,29 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<h5 <h5 class="subtitle" data-cy="subtitle-block" v-html="sanitizedText" />
class="subtitle"
data-cy="subtitle-block"
v-html="sanitizedText"
/>
</template> </template>
<script> <script>
import {sanitize} from '@/helpers/text'; import { sanitize } from '@/helpers/text';
export default { export default {
props: ['value'], props: ['value'],
computed: { computed: {
sanitizedText() { sanitizedText() {
return sanitize(this.value.text); return sanitize(this.value.text);
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.subtitle { .subtitle {
padding-top: 1px; padding-top: 1px;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
margin-bottom: calc(#{$large-spacing} - 0.1rem); margin-bottom: calc(#{$large-spacing} - 0.1rem);
@include heading-line-height; @include heading-line-height;
} }
</style> </style>

View File

@ -1,27 +1,21 @@
<template> <template>
<div <div :data-scrollto="value.id" class="survey-block">
:data-scrollto="value.id" <router-link :to="{ name: 'survey', params: { id: value.id } }" class="button button--primary">
class="survey-block"
>
<router-link
:to="{name: 'survey', params: {id:value.id}}"
class="button button--primary"
>
Übung anzeigen Übung anzeigen
</router-link> </router-link>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.survey-block { .survey-block {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
} }
</style> </style>

View File

@ -1,26 +1,23 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div class="task"> <div class="task">
<div <div class="task__text" v-html="value.text" />
class="task__text"
v-html="value.text"
/>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value'] props: ['value'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
.task { .task {
margin-bottom: 30px; margin-bottom: 30px;
align-items: start; align-items: start;
background-color: $color-brand-light; background-color: $color-brand-light;
border-radius: $default-border-radius; border-radius: $default-border-radius;
} }
</style> </style>

View File

@ -1,39 +1,35 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div class="text-block" data-cy="text-block" v-html="sanitizedText" />
class="text-block"
data-cy="text-block"
v-html="sanitizedText"
/>
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
computed: { computed: {
sanitizedText() { sanitizedText() {
// don't need to sanitize the input, server does this // don't need to sanitize the input, server does this
return this.value.text; return this.value.text;
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.text-block { .text-block {
margin-bottom: $medium-spacing; // if calc is not supported margin-bottom: $medium-spacing; // if calc is not supported
margin-bottom: calc(#{$large-spacing} - 0.25rem); // to offset the 1.5 line height, which leaves a padding margin-bottom: calc(#{$large-spacing} - 0.25rem); // to offset the 1.5 line height, which leaves a padding
@include regular-paragraph; @include regular-paragraph;
:deep(ul) { :deep(ul) {
@include list-parent; @include list-parent;
}
:deep(li) {
@include list-child;
}
} }
:deep(li) {
@include list-child;
}
}
</style> </style>

View File

@ -20,51 +20,51 @@
</template> </template>
<script> <script>
export default { export default {
props: ['value'], props: ['value'],
computed: { computed: {
src() { src() {
return `https://www.thinglink.com/card/${this.value.id}`; return `https://www.thinglink.com/card/${this.value.id}`;
}
}, },
},
methods: { methods: {
openFullscreen() { openFullscreen() {
this.$store.dispatch('showFullscreenInfographic', { this.$store.dispatch('showFullscreenInfographic', {
id: this.value.id, id: this.value.id,
type: 'thinglink-block' type: 'thinglink-block',
}); });
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
// Styling and structure taken from original iframe // Styling and structure taken from original iframe
.thinglink-block { .thinglink-block {
width: 100%; width: 100%;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
&__wrapper { &__wrapper {
position: relative; position: relative;
padding-bottom: 75%; padding-bottom: 75%;
padding-top: 0; padding-top: 0;
height: 0; height: 0;
}
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__link {
cursor: pointer;
}
} }
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&__link {
cursor: pointer;
}
}
</style> </style>

View File

@ -1,51 +1,42 @@
<template> <template>
<div class="video-block"> <div class="video-block">
<youtube-embed <youtube-embed :url="value.url" v-if="isYoutube" />
:url="value.url" <vimeo-embed :url="value.url" v-if="isVimeo" />
v-if="isYoutube" <srf-embed :url="value.url" v-if="isSrf" />
/>
<vimeo-embed
:url="value.url"
v-if="isVimeo"
/>
<srf-embed
:url="value.url"
v-if="isSrf"
/>
</div> </div>
</template> </template>
<script> <script>
import YoutubeEmbed from '@/components/videos/YoutubeEmbed'; import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed'; import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed'; import SrfEmbed from '@/components/videos/SrfEmbed';
import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video'; import { isVimeoUrl, isYoutubeUrl, isSrfUrl } from '@/helpers/video';
export default { export default {
props: ['value'], props: ['value'],
components: { components: {
YoutubeEmbed, YoutubeEmbed,
VimeoEmbed, VimeoEmbed,
SrfEmbed SrfEmbed,
},
computed: {
isYoutube() {
return isYoutubeUrl(this.value.url);
}, },
isVimeo() {
computed: { return isVimeoUrl(this.value.url);
isYoutube() { },
return isYoutubeUrl(this.value.url); isSrf() {
}, return isSrfUrl(this.value.url);
isVimeo() { },
return isVimeoUrl(this.value.url); },
}, };
isSrf() {
return isSrfUrl(this.value.url);
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.video-block { .video-block {
margin-bottom: 30px; margin-bottom: 30px;
} }
</style> </style>

View File

@ -1,19 +1,9 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div :data-scrollto="value.id" class="assignment">
:data-scrollto="value.id" <p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" />
class="assignment"
>
<p
class="assignment__main-text"
data-cy="assignment-main-text"
v-html="assignment.assignment"
/>
<solution <solution :value="solution" v-if="assignment.solution" />
:value="solution"
v-if="assignment.solution"
/>
<template v-if="isStudent"> <template v-if="isStudent">
<submission-form <submission-form
@ -33,100 +23,92 @@
@spellcheck="spellcheck" @spellcheck="spellcheck"
/> />
<spell-check <spell-check :corrections="corrections" :text="submission.text" />
:corrections="corrections"
:text="submission.text"
/>
<p <p class="assignment__feedback" v-if="assignment.submission.submissionFeedback" v-html="feedbackText" />
class="assignment__feedback"
v-if="assignment.submission.submissionFeedback"
v-html="feedbackText"
/>
</template> </template>
<template v-if="!isStudent"> <template v-if="!isStudent">
<router-link <router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary">
:to="{name: 'submissions', params: { id: assignment.id }}" Zu den Ergebnissen
class="button button--primary"
>
Zu den
Ergebnissen
</router-link> </router-link>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import {mapActions, mapGetters} from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql'; import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql'; import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql'; import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql'; import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import {sanitize} from '@/helpers/text'; import { sanitize } from '@/helpers/text';
const SubmissionForm = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm'); const SubmissionForm = () =>
const Solution = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm');
const SpellCheck = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck'); const Solution = () => import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution');
const SpellCheck = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SpellCheck');
export default { export default {
props: ['value'], props: ['value'],
components: { components: {
Solution, Solution,
SubmissionForm, SubmissionForm,
SpellCheck, SpellCheck,
},
data() {
return {
assignment: {
submission: this.initialSubmission(),
},
me: {
permissions: [],
},
inputType: 'text',
unsaved: false,
saving: 0,
corrections: '',
spellcheckLoading: false,
};
},
computed: {
...mapGetters(['scrollToAssignmentId']),
final() {
return !!this.submission && this.submission.final;
}, },
submission() {
data() { return this.assignment.submission ? this.assignment.submission : {};
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
return { return {
assignment: { text: this.assignment.solution,
submission: this.initialSubmission(),
},
me: {
permissions: [],
},
inputType: 'text',
unsaved: false,
saving: 0,
corrections: '',
spellcheckLoading: false,
}; };
}, },
id() {
computed: { return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
...mapGetters(['scrollToAssignmentId']),
final() {
return !!this.submission && this.submission.final;
},
submission() {
return this.assignment.submission ? this.assignment.submission : {};
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
return {
text: this.assignment.solution,
};
},
id() {
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
},
feedbackText() {
let feedback = this.assignment.submission.submissionFeedback;
let sanitizedFeedbackText = sanitize(feedback.text);
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
},
}, },
feedbackText() {
let feedback = this.assignment.submission.submissionFeedback;
let sanitizedFeedbackText = sanitize(feedback.text);
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
},
},
methods: { methods: {
...mapActions(['scrollToAssignmentReady']), ...mapActions(['scrollToAssignmentReady']),
_save: debounce(function (submission) { _save: debounce(function (submission) {
this.saving++; this.saving++;
this.$apollo.mutate({ this.$apollo
.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS, mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS,
variables: { variables: {
input: { input: {
@ -137,7 +119,14 @@
}, },
}, },
}, },
update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) { update(
store,
{
data: {
updateAssignment: { successful, updatedAssignment },
},
}
) {
try { try {
if (successful) { if (successful) {
const query = ASSIGNMENT_QUERY; const query = ASSIGNMENT_QUERY;
@ -148,80 +137,82 @@
submission, submission,
}); });
const data = { const data = {
assignment assignment,
}; };
store.writeQuery({query, variables, data}); store.writeQuery({ query, variables, data });
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
} }
}, },
}).then(() => { })
.then(() => {
this.saving--; this.saving--;
if (this.saving === 0) { if (this.saving === 0) {
this.unsaved = false; this.unsaved = false;
} }
}); });
}, 500), }, 500),
saveInput: function (answer) { saveInput: function (answer) {
// reset corrections on input // reset corrections on input
this.corrections = ''; this.corrections = '';
this.unsaved = true; this.unsaved = true;
/* /*
We update the assignment on this component, so the changes are reflected on it. The server does not return We update the assignment on this component, so the changes are reflected on it. The server does not return
the updated entity, to prevent the UI to update when the user is entering his input the updated entity, to prevent the UI to update when the user is entering his input
*/ */
this.assignment.submission.text = answer; this.assignment.submission.text = answer;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
changeDocumentUrl(documentUrl) { changeDocumentUrl(documentUrl) {
this.assignment.submission.document = documentUrl; this.assignment.submission.document = documentUrl;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
turnIn() { turnIn() {
// reset corrections on turn in // reset corrections on turn in
this.corrections = ''; this.corrections = '';
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION, mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: { variables: {
input: { input: {
assignment: { assignment: {
id: this.assignment.id, id: this.assignment.id,
answer: this.assignment.submission.text, answer: this.assignment.submission.text,
document: this.assignment.submission.document, document: this.assignment.submission.document,
final: true, final: true,
},
}, },
}, },
}); },
}, });
reopen() { },
this.$apollo.mutate({ reopen() {
mutation: UPDATE_ASSIGNMENT_MUTATION, this.$apollo.mutate({
variables: { mutation: UPDATE_ASSIGNMENT_MUTATION,
input: { variables: {
assignment: { input: {
id: this.assignment.id, assignment: {
answer: this.assignment.submission.text, id: this.assignment.id,
document: this.assignment.submission.document, answer: this.assignment.submission.text,
final: false, document: this.assignment.submission.document,
}, final: false,
}, },
}, },
}); },
}, });
initialSubmission() { },
return { initialSubmission() {
text: '', return {
document: '', text: '',
final: false, document: '',
}; final: false,
}, };
spellcheck() { },
let self = this; spellcheck() {
this.spellcheckLoading = true; let self = this;
this.$apollo.mutate({ this.spellcheckLoading = true;
this.$apollo
.mutate({
mutation: SPELL_CHECK_MUTATION, mutation: SPELL_CHECK_MUTATION,
variables: { variables: {
input: { input: {
@ -229,90 +220,96 @@
text: this.assignment.submission.text, text: this.assignment.submission.text,
}, },
}, },
update(store, {data: {spellCheck: {results}}}) { update(
store,
{
data: {
spellCheck: { results },
},
}
) {
self.corrections = results; self.corrections = results;
}, },
}).then(() => { })
.then(() => {
this.spellcheckLoading = false; this.spellcheckLoading = false;
}); });
},
}, },
},
apollo: { apollo: {
assignment: { assignment: {
query: ASSIGNMENT_QUERY, query: ASSIGNMENT_QUERY,
variables() { variables() {
return { return {
id: this.value.id, id: this.value.id,
}; };
},
result(response) {
const data = response.data;
this.assignment = cloneDeep(data.assignment);
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
this.$nextTick(() => this.scrollToAssignmentReady(true));
}
},
}, },
me: { result(response) {
query: ME_QUERY, const data = response.data;
this.assignment = cloneDeep(data.assignment);
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
this.$nextTick(() => this.scrollToAssignmentReady(true));
}
}, },
}, },
}; me: {
query: ME_QUERY,
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import '@/styles/_variables.scss';
@import '@/styles/_functions.scss'; @import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss'; @import '@/styles/_mixins.scss';
.assignment { .assignment {
margin-bottom: 3rem; margin-bottom: 3rem;
position: relative; position: relative;
&__title {
font-size: toRem(17px);
margin-bottom: 1rem;
}
&__main-text {
:deep(ul){
@include list-parent
}
:deep(li) {
@include list-child;
}
}
&__toggle-input-container {
display: flex;
margin-bottom: 15px;
}
&__toggle-input {
border: 0;
font-family: $sans-serif-font-family;
background: transparent;
font-size: toRem(14px);
padding: 5px 0;
margin-right: 15px;
outline: 0;
color: $color-silver-dark;
cursor: pointer;
border-bottom: 2px solid transparent;
&--active {
border-bottom-color: $color-charcoal-dark;
color: $color-charcoal-dark;
}
}
&__feedback {
@include regular-text;
}
&__title {
font-size: toRem(17px);
margin-bottom: 1rem;
} }
&__main-text {
:deep(ul) {
@include list-parent;
}
:deep(li) {
@include list-child;
}
}
&__toggle-input-container {
display: flex;
margin-bottom: 15px;
}
&__toggle-input {
border: 0;
font-family: $sans-serif-font-family;
background: transparent;
font-size: toRem(14px);
padding: 5px 0;
margin-right: 15px;
outline: 0;
color: $color-silver-dark;
cursor: pointer;
border-bottom: 2px solid transparent;
&--active {
border-bottom-color: $color-charcoal-dark;
color: $color-charcoal-dark;
}
}
&__feedback {
@include regular-text;
}
}
</style> </style>

View File

@ -1,108 +1,99 @@
<template> <template>
<div <div class="final-submission" data-cy="final-submission">
class="final-submission" <document-block :value="{ url: userInput.document }" class="final-submission__document" v-if="userInput.document" />
data-cy="final-submission"
>
<document-block
:value="{url: userInput.document}"
class="final-submission__document"
v-if="userInput.document"
/>
<div class="final-submission__explanation"> <div class="final-submission__explanation">
<info-icon class="final-submission__explanation-icon" /> <info-icon class="final-submission__explanation-icon" />
<span class="final-submission__explanation-text">{{ sharedMsg }}</span> <span class="final-submission__explanation-text">{{ sharedMsg }}</span>
<a <a class="final-submission__reopen" data-cy="final-submission-reopen" v-if="showReopen" @click="$emit('reopen')"
class="final-submission__reopen" >Bearbeiten</a
data-cy="final-submission-reopen" >
v-if="showReopen"
@click="$emit('reopen')"
>Bearbeiten</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {newLineToParagraph} from '@/helpers/text'; import { newLineToParagraph } from '@/helpers/text';
const DocumentBlock = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/DocumentBlock'); const DocumentBlock = () =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/DocumentBlock');
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'); const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon');
export default { export default {
props: { props: {
userInput: { userInput: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
},
showReopen: {
type: Boolean,
default: true
},
sharedMsg: {
type: String,
default: ''
}
}, },
showReopen: {
components: { type: Boolean,
InfoIcon, default: true,
DocumentBlock,
}, },
sharedMsg: {
type: String,
default: '',
},
},
computed: { components: {
text() { InfoIcon,
return newLineToParagraph(this.userInput.text); DocumentBlock,
} },
}
}; computed: {
text() {
return newLineToParagraph(this.userInput.text);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.final-submission { .final-submission {
&__text { &__text {
background-color: $color-white; background-color: $color-white;
@include input-box-shadow; @include input-box-shadow;
border-radius: $input-border-radius; border-radius: $input-border-radius;
padding: 15px; padding: 15px;
font-size: toRem(17px); font-size: toRem(17px);
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
margin-bottom: 20px; margin-bottom: 20px;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
hyphens: auto; hyphens: auto;
word-break: break-word; word-break: break-word;
}
&__document {
margin-bottom: $small-spacing;
}
&__explanation {
display: flex;
align-items: center;
}
&__explanation-icon {
width: 40px;
height: 40px;
fill: $color-brand;
margin-right: 8px;
}
&__explanation-text {
color: $color-brand;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
margin-right: $medium-spacing;
}
&__reopen {
@include small-text;
cursor: pointer;
color: $color-charcoal-light;
}
} }
&__document {
margin-bottom: $small-spacing;
}
&__explanation {
display: flex;
align-items: center;
}
&__explanation-icon {
width: 40px;
height: 40px;
fill: $color-brand;
margin-right: 8px;
}
&__explanation-text {
color: $color-brand;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
margin-right: $medium-spacing;
}
&__reopen {
@include small-text;
cursor: pointer;
color: $color-charcoal-light;
}
}
</style> </style>

View File

@ -1,63 +1,63 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<p <p class="spellcheck" v-if="corrections">
class="spellcheck"
v-if="corrections"
>
<span class="inline-title">Rechtschreibung:</span> <span v-html="highlightedText" /> <span class="inline-title">Rechtschreibung:</span> <span v-html="highlightedText" />
</p> </p>
</template> </template>
<script> <script>
export default { export default {
props: ['corrections', 'text'], props: ['corrections', 'text'],
computed: { computed: {
highlightedText() { highlightedText() {
if (!this.corrections) { if (!this.corrections) {
return ''; return '';
}
let parts = [];
let index = 0;
[...this.corrections] // no side effects, as sort changes the source array
.sort((e1, e2) => (e1.offset + e1.sentenceOffset) - (e2.offset + e2.sentenceOffset))
.forEach(current => {
let realOffset = current.offset + current.sentenceOffset;
parts.push({
correct: true,
text: this.text.substring(index, realOffset)
}, {
correct: false,
text: this.text.substring(realOffset, realOffset + current.length)
});
index = realOffset + current.length;
});
parts.push({
correct: true,
text: this.text.substring(index, this.text.length + 1)
});
return parts
.filter(part => part.text.length)
.reduce((previous, part) => {
if (part.correct) {
return `${previous}${part.text}`;
} else {
return `${previous}<span data-cy="spellcheck-correction" class="spellcheck__correction">${part.text}</span>`;
}
}, '');
} }
} let parts = [];
}; let index = 0;
[...this.corrections] // no side effects, as sort changes the source array
.sort((e1, e2) => e1.offset + e1.sentenceOffset - (e2.offset + e2.sentenceOffset))
.forEach((current) => {
let realOffset = current.offset + current.sentenceOffset;
parts.push(
{
correct: true,
text: this.text.substring(index, realOffset),
},
{
correct: false,
text: this.text.substring(realOffset, realOffset + current.length),
}
);
index = realOffset + current.length;
});
parts.push({
correct: true,
text: this.text.substring(index, this.text.length + 1),
});
return parts
.filter((part) => part.text.length)
.reduce((previous, part) => {
if (part.correct) {
return `${previous}${part.text}`;
} else {
return `${previous}<span data-cy="spellcheck-correction" class="spellcheck__correction">${part.text}</span>`;
}
}, '');
},
},
};
</script> </script>
<style lang="scss"> <style lang="scss">
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.spellcheck { .spellcheck {
@include regular-text; @include regular-text;
&__correction { &__correction {
background: yellow; background: yellow;
}
} }
}
</style> </style>

View File

@ -10,10 +10,7 @@
/> />
</div> </div>
<div <div class="submission-form-container__actions" v-if="!isFinalOrReadOnly">
class="submission-form-container__actions"
v-if="!isFinalOrReadOnly"
>
<button <button
class="submission-form-container__submit button button--primary button--white-bg" class="submission-form-container__submit button button--primary button--white-bg"
data-cy="submission-form-submit" data-cy="submission-form-submit"
@ -29,11 +26,7 @@
> >
{{ spellcheckText }} {{ spellcheckText }}
</button> </button>
<file-upload <file-upload :document="userInput.document" v-if="allowsDocuments" @change-document-url="changeDocumentUrl" />
:document="userInput.document"
v-if="allowsDocuments"
@change-document-url="changeDocumentUrl"
/>
<slot /> <slot />
</div> </div>
@ -48,115 +41,112 @@
</template> </template>
<script> <script>
const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput.vue'); const SubmissionInput = () => import('@/components/content-blocks/assignment/SubmissionInput.vue');
const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission.vue'); const FinalSubmission = () => import('@/components/content-blocks/assignment/FinalSubmission.vue');
const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue'); const FileUpload = () => import('@/components/ui/file-upload/FileUpload.vue');
export default {
export default { props: {
props: { userInput: Object,
userInput: Object, saved: Boolean,
saved: Boolean, placeholder: String,
placeholder: String, action: String,
action: String, reopen: Function,
reopen: Function, document: String,
document: String, readOnly: {
readOnly: { type: Boolean,
type: Boolean, default: false,
default: false,
},
spellcheck: {
type: Boolean,
default: false,
},
spellcheckLoading: {
type: Boolean,
default: false,
},
sharedMsg: String,
}, },
spellcheck: {
components: { type: Boolean,
FileUpload, default: false,
SubmissionInput,
FinalSubmission,
}, },
spellcheckLoading: {
computed: { type: Boolean,
final() { default: false,
return !!this.userInput && this.userInput.final;
},
isFinalOrReadOnly() {
return this.final || this.readOnly;
},
allowsDocuments() {
return 'document' in this.userInput;
},
showSpellcheckButton() {
return this.spellcheck && process.env.VUE_APP_ENABLE_SPELLCHECK;
},
spellcheckText() {
if (!this.spellcheckLoading) {
return 'Rechtschreibung prüfen';
} else {
return 'Wird geprüft...';
}
},
}, },
sharedMsg: String,
},
methods: { components: {
reopenSubmission() { FileUpload,
this.$emit('reopen'); SubmissionInput,
}, FinalSubmission,
saveInput(input) { },
this.$emit('saveInput', input);
}, computed: {
changeDocumentUrl(documentUrl) { final() {
this.$emit('changeDocumentUrl', documentUrl); return !!this.userInput && this.userInput.final;
},
}, },
isFinalOrReadOnly() {
return this.final || this.readOnly;
},
allowsDocuments() {
return 'document' in this.userInput;
},
showSpellcheckButton() {
return this.spellcheck && process.env.VUE_APP_ENABLE_SPELLCHECK;
},
spellcheckText() {
if (!this.spellcheckLoading) {
return 'Rechtschreibung prüfen';
} else {
return 'Wird geprüft...';
}
},
},
}; methods: {
reopenSubmission() {
this.$emit('reopen');
},
saveInput(input) {
this.$emit('saveInput', input);
},
changeDocumentUrl(documentUrl) {
this.$emit('changeDocumentUrl', documentUrl);
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.submission-form-container { .submission-form-container {
@include form-with-border; @include form-with-border;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
display: none; display: none;
@include desktop { @include desktop {
display: block; display: block;
} }
&__inputs { &__inputs {
margin-bottom: 12px; margin-bottom: 12px;
} }
&__submit { &__submit {
margin-right: $medium-spacing; margin-right: $medium-spacing;
} }
&__actions { &__actions {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&__document { &__document {
&:hover { &:hover {
cursor: pointer; cursor: pointer;
}
}
&__spellcheck {
/* so the button does not change size when changing the text */
width: 235px;
text-align: center;
display: inline-block;
} }
} }
&__spellcheck {
/* so the button does not change size when changing the text */
width: 235px;
text-align: center;
display: inline-block;
}
}
</style> </style>

View File

@ -4,74 +4,68 @@
:placeholder="placeholder" :placeholder="placeholder"
:readonly="readonly" :readonly="readonly"
:value="inputText" :value="inputText"
:class="{'submission-form__textarea--readonly': readonly}" :class="{ 'submission-form__textarea--readonly': readonly }"
data-cy="submission-textarea" data-cy="submission-textarea"
rows="1" rows="1"
class="submission-form__textarea" class="submission-form__textarea"
v-auto-grow v-auto-grow
@input="$emit('input', $event.target.value)" @input="$emit('input', $event.target.value)"
/> />
<div <div class="submission-form__save-status submission-form__save-status--saved" v-if="saved">
class="submission-form__save-status submission-form__save-status--saved"
v-if="saved"
>
<tick-circle-icon class="submission-form__save-status-icon" /> <tick-circle-icon class="submission-form__save-status-icon" />
</div> </div>
<div <div class="submission-form__save-status submission-form__save-status--unsaved" v-if="!saved">
class="submission-form__save-status submission-form__save-status--unsaved"
v-if="!saved"
>
<loading-icon class="submission-form__save-status-icon submission-form__saving-icon" /> <loading-icon class="submission-form__save-status-icon submission-form__saving-icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const TickCircleIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/TickCircleIcon'); const TickCircleIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/TickCircleIcon');
const LoadingIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/LoadingIcon'); const LoadingIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/LoadingIcon');
export default { export default {
props: { props: {
inputText: String, inputText: String,
saved: Boolean, saved: Boolean,
readonly: Boolean, readonly: Boolean,
placeholder: { placeholder: {
type: String, type: String,
default: 'Ergebnis erfassen' default: 'Ergebnis erfassen',
}
}, },
components: { },
TickCircleIcon, components: {
LoadingIcon TickCircleIcon,
} LoadingIcon,
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.submission-form { .submission-form {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
&__textarea { &__textarea {
@include borderless-textarea; @include borderless-textarea;
}
&__save-status {
position: relative;
align-items: center;
}
&__save-status-icon {
width: 22px;
height: 22px;
fill: $color-silver-dark;
}
&__saving-icon {
@include spin;
}
} }
&__save-status {
position: relative;
align-items: center;
}
&__save-status-icon {
width: 22px;
height: 22px;
fill: $color-silver-dark;
}
&__saving-icon {
@include spin;
}
}
</style> </style>

View File

@ -5,7 +5,7 @@
class="assignment-form__title skillbox-input" class="assignment-form__title skillbox-input"
placeholder="Aufgabentitel" placeholder="Aufgabentitel"
@input="$emit('assignment-change-title', $event.target.value, index)" @input="$emit('assignment-change-title', $event.target.value, index)"
> />
<textarea <textarea
:value="value.assignment" :value="value.assignment"
class="assignment-form__exercise-text skillbox-textarea" class="assignment-form__exercise-text skillbox-textarea"
@ -20,43 +20,43 @@
</template> </template>
<script> <script>
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'); const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon');
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
InfoIcon InfoIcon,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.assignment-form { .assignment-form {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: 13px; grid-row-gap: 13px;
grid-template-columns: 40px 1fr; grid-template-columns: 40px 1fr;
grid-column-gap: 16px; grid-column-gap: 16px;
align-items: center; align-items: center;
&__title { &__title {
width: $modal-input-width; width: $modal-input-width;
grid-column: span 2; grid-column: span 2;
}
&__exercise-text {
width: $modal-input-width;
grid-column: span 2;
}
&__help-icon {
width: 40px;
height: 40px;
}
&__help-description {
}
} }
&__exercise-text {
width: $modal-input-width;
grid-column: span 2;
}
&__help-icon {
width: 40px;
height: 40px;
}
&__help-description {
}
}
</style> </style>

View File

@ -1,13 +1,6 @@
<template> <template>
<div <div :class="['chooser-element', subclass]" :data-cy="cy" @click="$emit('select')">
:class="['chooser-element', subclass]" <component class="chooser-element__icon" :is="icon" />
:data-cy="cy"
@click="$emit('select')"
>
<component
class="chooser-element__icon"
:is="icon"
/>
<div class="chooser-element__title"> <div class="chooser-element__title">
{{ title }} {{ title }}
</div> </div>
@ -15,62 +8,61 @@
</template> </template>
<script> <script>
import formElementIcons from '@/components/ui/form-element-icons'; import formElementIcons from '@/components/ui/form-element-icons';
export default { export default {
props: { props: {
type: { type: {
type: String, type: String,
default: '', default: '',
}, },
icon: { icon: {
type: String, type: String,
default() { default() {
return `${this.type}-icon`; return `${this.type}-icon`;
},
},
title: {
type: String,
default() {
return this.type.replace(/^\w/, c => c.toUpperCase());
},
}, },
}, },
title: {
components: { type: String,
...formElementIcons, default() {
return this.type.replace(/^\w/, (c) => c.toUpperCase());
},
}, },
},
data() { components: {
return { ...formElementIcons,
subclass: `chooser-element--${this.type}`, },
cy: `choose-${this.type}-widget`,
}; data() {
}, return {
}; subclass: `chooser-element--${this.type}`,
cy: `choose-${this.type}-widget`,
};
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.chooser-element { .chooser-element {
cursor: pointer; cursor: pointer;
border: 1px solid $color-silver; border: 1px solid $color-silver;
border-radius: 4px; border-radius: 4px;
height: 105px; height: 105px;
width: 105px; width: 105px;
box-sizing: border-box; box-sizing: border-box;
display: grid; display: grid;
grid-template-rows: 1fr 45px; grid-template-rows: 1fr 45px;
justify-content: center; justify-content: center;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
&__icon { &__icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
align-self: end; align-self: end;
}
} }
}
</style> </style>

View File

@ -1,31 +1,21 @@
<template> <template>
<div class="content-block-element-chooser-widget__wrapper"> <div class="content-block-element-chooser-widget__wrapper">
<button <button class="content-block-element-chooser-widget__remove-button icon-button" @click="remove">
class="content-block-element-chooser-widget__remove-button icon-button"
@click="remove"
>
<cross-icon class="icon-button__icon" /> <cross-icon class="icon-button__icon" />
</button> </button>
<h3 <h3 class="content-block-element-chooser-widget__heading" data-cy="chooser-heading">Neuer Inhalt</h3>
class="content-block-element-chooser-widget__heading" <template v-if="includeListOption && hasDefaultFeatures">
data-cy="chooser-heading"
>
Neuer Inhalt
</h3>
<template
v-if="includeListOption && hasDefaultFeatures"
>
<checkbox <checkbox
class="content-block-element-chooser-widget__list-toggle" class="content-block-element-chooser-widget__list-toggle"
:checked="convertToList" :checked="convertToList"
data-cy="convert-to-list-checkbox" data-cy="convert-to-list-checkbox"
label="Aufzählungszeichen hinzufügen" label="Aufzählungszeichen hinzufügen"
@input="convertToList=$event" @input="convertToList = $event"
/> />
</template> </template>
<div <div
:class="{'content-block-element-chooser-widget--no-assignment': hideAssignment}" :class="{ 'content-block-element-chooser-widget--no-assignment': hideAssignment }"
class="content-block-element-chooser-widget" class="content-block-element-chooser-widget"
> >
<chooser-element <chooser-element
@ -41,182 +31,177 @@
</template> </template>
<script> <script>
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import formElementIcons from '@/components/ui/form-element-icons'; import formElementIcons from '@/components/ui/form-element-icons';
import CrossIcon from '@/components/icons/CrossIcon'; import CrossIcon from '@/components/icons/CrossIcon';
import ChooserElement from '@/components/content-forms/ChooserElement'; import ChooserElement from '@/components/content-forms/ChooserElement';
import {DEFAULT_FEATURE_SET} from '@/consts/features.consts'; import { DEFAULT_FEATURE_SET } from '@/consts/features.consts';
export default {
export default { props: {
props: { element: {},
element: {}, index: {},
index: {}, hideAssignment: {
hideAssignment: { type: Boolean,
type: Boolean, default: false,
default: false,
},
includeListOption: {
type: Boolean,
default: false,
},
}, },
includeListOption: {
inject: ['features'], type: Boolean,
default: false,
components: {
ChooserElement,
CrossIcon,
Checkbox,
...formElementIcons,
}, },
},
data() { inject: ['features'],
const hasDefaultFeatures = this.features === DEFAULT_FEATURE_SET;
return {
convertToList: false,
chooserTypes: [
{
type: 'subtitle',
block: 'subtitle',
title: 'Untertitel',
icon: 'title-icon',
},
{
type: 'link',
block: 'link_block',
title: 'Link',
icon: 'link-icon',
},
{
type: 'video',
block: 'video_block',
},
{
type: 'image',
block: 'image_url_block',
title: 'Bild',
},
{
type: 'text',
block: 'text_block',
},
{
type: 'assignment',
block: 'assignment',
icon: 'speech-bubble-icon',
title: 'Aufgabe & Ergebnis',
show: !this.hideAssignment && hasDefaultFeatures
},
{
type: 'document',
block: 'document_block',
title: 'Dokument',
show: hasDefaultFeatures
},
components: {
ChooserElement,
CrossIcon,
Checkbox,
...formElementIcons,
},
], data() {
}; const hasDefaultFeatures = this.features === DEFAULT_FEATURE_SET;
return {
convertToList: false,
chooserTypes: [
{
type: 'subtitle',
block: 'subtitle',
title: 'Untertitel',
icon: 'title-icon',
},
{
type: 'link',
block: 'link_block',
title: 'Link',
icon: 'link-icon',
},
{
type: 'video',
block: 'video_block',
},
{
type: 'image',
block: 'image_url_block',
title: 'Bild',
},
{
type: 'text',
block: 'text_block',
},
{
type: 'assignment',
block: 'assignment',
icon: 'speech-bubble-icon',
title: 'Aufgabe & Ergebnis',
show: !this.hideAssignment && hasDefaultFeatures,
},
{
type: 'document',
block: 'document_block',
title: 'Dokument',
show: hasDefaultFeatures,
},
],
};
},
computed: {
filteredChooserTypes() {
return this.chooserTypes.filter((type) => !('show' in type) || type.show); // display element if `show` is not set or if `show` evaluates to true
}, },
hasDefaultFeatures() {
computed: { return this.features === DEFAULT_FEATURE_SET;
filteredChooserTypes() {
return this.chooserTypes.filter(type => !("show" in type) || type.show ); // display element if `show` is not set or if `show` evaluates to true
},
hasDefaultFeatures() {
return this.features === DEFAULT_FEATURE_SET;
}
}, },
},
methods: { methods: {
changeType(type) { changeType(type) {
this.$emit('change-type', { this.$emit('change-type', {
type, type,
convertToList: this.convertToList, convertToList: this.convertToList,
}); });
},
remove() {
this.$emit('remove');
},
}, },
}; remove() {
this.$emit('remove');
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.content-block-element-chooser-widget {
display: -ms-grid;
@supports (display: grid) {
display: grid;
}
grid-template-columns: repeat(7, 1fr);
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-column-gap: 0;
font-family: $sans-serif-font-family;
text-align: center;
position: relative;
// grid position in wrapper element
grid-column: 1 / -1;
/*IE10+*/
& > :nth-child(1) {
-ms-grid-column: 1;
}
& > :nth-child(2) {
-ms-grid-column: 2;
}
& > :nth-child(3) {
-ms-grid-column: 3;
}
& > :nth-child(4) {
-ms-grid-column: 4;
}
& > :nth-child(5) {
-ms-grid-column: 5;
}
& > :nth-child(6) {
-ms-grid-column: 6;
}
&--no-assignment {
grid-template-columns: repeat(5, 1fr);
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr;
}
&__wrapper {
display: grid;
grid-template-columns: 180px 1fr 50px;
align-items: center;
row-gap: $small-spacing;
}
&__remove-button {
grid-column: 3;
}
&__heading {
@include heading-3;
margin-bottom: 0;
grid-column: 1;
grid-row: 1;
}
&__list-toggle {
margin-bottom: 0;
grid-column: 2;
grid-row: 1;
}
.content-block-element-chooser-widget {
display: -ms-grid;
@supports (display: grid) {
display: grid;
} }
grid-template-columns: repeat(7, 1fr);
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-column-gap: 0;
font-family: $sans-serif-font-family;
text-align: center;
position: relative;
// grid position in wrapper element
grid-column: 1 / -1;
/*IE10+*/
& > :nth-child(1) {
-ms-grid-column: 1;
}
& > :nth-child(2) {
-ms-grid-column: 2;
}
& > :nth-child(3) {
-ms-grid-column: 3;
}
& > :nth-child(4) {
-ms-grid-column: 4;
}
& > :nth-child(5) {
-ms-grid-column: 5;
}
& > :nth-child(6) {
-ms-grid-column: 6;
}
&--no-assignment {
grid-template-columns: repeat(5, 1fr);
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr;
}
&__wrapper {
display: grid;
grid-template-columns: 180px 1fr 50px;
align-items: center;
row-gap: $small-spacing;
}
&__remove-button {
grid-column: 3;
}
&__heading {
@include heading-3;
margin-bottom: 0;
grid-column: 1;
grid-row: 1;
}
&__list-toggle {
margin-bottom: 0;
grid-column: 2;
grid-row: 1;
}
}
</style> </style>

View File

@ -1,132 +1,119 @@
<template> <template>
<div <div class="document-form" ref="documentform">
class="document-form" <div v-if="!value.url" ref="uploadcare-panel" />
ref="documentform" <div class="document-form__spinner" v-if="loading">
>
<div
v-if="!value.url"
ref="uploadcare-panel"
/>
<div
class="document-form__spinner"
v-if="loading"
>
<loading-icon class="document-form__loading-icon" /> <loading-icon class="document-form__loading-icon" />
</div> </div>
<div <div class="document-form__uploaded" v-if="value.url">
class="document-form__uploaded"
v-if="value.url"
>
<document-icon class="document-form__icon" /> <document-icon class="document-form__icon" />
<a <a :href="previewUrl" class="document-form__link" target="_blank">{{ previewLink }}</a>
:href="previewUrl"
class="document-form__link"
target="_blank"
>{{ previewLink }}</a>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {uploadcare} from '@/helpers/uploadcare'; import { uploadcare } from '@/helpers/uploadcare';
import LoadingIcon from '@/components/icons/LoadingIcon'; import LoadingIcon from '@/components/icons/LoadingIcon';
const DocumentIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'); const DocumentIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon');
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
LoadingIcon, LoadingIcon,
DocumentIcon, DocumentIcon,
},
data() {
return {
loading: false,
};
},
computed: {
previewUrl() {
if (this.value && this.value.url) {
return this.value.url;
}
return null;
}, },
previewLink() {
data() { if (this.value && this.value.url) {
return { const parts = this.value.url.split('/');
loading: false, return parts[parts.length - 1];
}; }
return '';
}, },
},
computed: { mounted() {
previewUrl() { uploadcare(
if (this.value && this.value.url) { this,
return this.value.url; (url) => {
}
return null;
},
previewLink() {
if (this.value && this.value.url) {
const parts = this.value.url.split('/');
return parts[parts.length - 1];
}
return '';
},
},
mounted() {
uploadcare(this, url => {
this.$emit('change-url', url, this.index); this.$emit('change-url', url, this.index);
this.loading = false; this.loading = false;
}, () => { },
() => {
this.loading = true; this.loading = true;
}
}); );
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.document-form { .document-form {
&__uploaded { &__uploaded {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&__link { &__link {
text-decoration: underline; text-decoration: underline;
} }
&__spinner { &__spinner {
width: 100%; width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
&__loading-icon {
@include spin;
fill: $color-silver-dark;
}
&__icon {
width: 30px;
height: 30px;
margin-right: $small-spacing;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px; height: 150px;
display: flex; display: flex;
align-items: center; width: 100%;
justify-content: center; justify-content: center;
} align-items: center;
font-family: $sans-serif-font-family;
&__loading-icon { font-weight: $font-weight-regular;
@include spin; text-decoration: underline;
fill: $color-silver-dark;
}
&__icon {
width: 30px;
height: 30px;
margin-right: $small-spacing;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px;
display: flex;
width: 100%;
justify-content: center;
align-items: center;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
text-decoration: underline;
}
} }
} }
}
</style> </style>

View File

@ -1,149 +1,134 @@
<template> <template>
<div class="image-form"> <div class="image-form">
<div <div class="image-form__error" v-if="hadError">
class="image-form__error" Ups, das scheint kein Bild zu sein. Bitte versuche es nochmal mit einer anderen Datei, oder lade die Datei als
v-if="hadError" <a class="image-form__link" @click="switchToDocument">Dokument</a> hoch.
>
Ups, das scheint kein Bild zu sein. Bitte versuche es nochmal mit einer anderen Datei, oder lade die Datei als <a
class="image-form__link"
@click="switchToDocument"
>Dokument</a> hoch.
</div> </div>
<div <div class="image-form__spinner" v-if="loading">
class="image-form__spinner"
v-if="loading"
>
<loading-icon class="image-form__loading-icon" /> <loading-icon class="image-form__loading-icon" />
</div> </div>
<div <div v-if="!value.url || hadError" ref="uploadcare-panel" />
v-if="!value.url || hadError"
ref="uploadcare-panel"
/>
<div v-if="value.url && !hadError"> <div v-if="value.url && !hadError">
<img <img alt="" :src="previewUrl" @error="error" />
alt=""
:src="previewUrl"
@error="error"
>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import uploadcare from 'uploadcare-widget'; import uploadcare from 'uploadcare-widget';
import LoadingIcon from '@/components/icons/LoadingIcon'; import LoadingIcon from '@/components/icons/LoadingIcon';
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: {LoadingIcon}, components: { LoadingIcon },
data() { data() {
return { return {
hadError: false, hadError: false,
uploadcarePanel: null, uploadcarePanel: null,
url: '', url: '',
filename: '', filename: '',
loading: false, loading: false,
}; };
},
computed: {
previewUrl: function () {
if (this.value && this.value.url) {
return this.value.url + '-/preview/200x200/';
}
return null;
}, },
computed: { },
previewUrl: function () { mounted() {
if (this.value && this.value.url) { this.mountUploadcare();
return this.value.url + '-/preview/200x200/'; },
} methods: {
return null; error() {
}, this.hadError = true;
},
mounted() {
this.mountUploadcare();
},
methods: {
error() {
this.hadError = true;
setTimeout(() => { setTimeout(() => {
this.mountUploadcare(); this.mountUploadcare();
}, 0); }, 0);
}, },
mountUploadcare() { mountUploadcare() {
this.uploadcarePanel = uploadcare.openPanel(this.$refs['uploadcare-panel'], null, { this.uploadcarePanel = uploadcare.openPanel(this.$refs['uploadcare-panel'], null, {
tabs: ['file'], tabs: ['file'],
});
this.uploadcarePanel.done((panelResult) => {
this.loading = true;
panelResult.done((fileInfo) => {
this.hadError = false;
this.loading = false;
this.url = fileInfo.cdnUrl;
this.filename = fileInfo.name;
this.$emit('change-url', fileInfo.cdnUrl, this.index);
}); });
this.uploadcarePanel.done(panelResult => { // the api also provides these methods
this.loading = true; // panelResult.progress(p => {});
panelResult.done(fileInfo => { // panelResult.fail(uploadResult => {});
this.hadError = false; });
this.loading = false;
this.url = fileInfo.cdnUrl;
this.filename = fileInfo.name;
this.$emit('change-url', fileInfo.cdnUrl, this.index);
});
// the api also provides these methods
// panelResult.progress(p => {});
// panelResult.fail(uploadResult => {});
});
},
switchToDocument() {
this.$emit('switch-to-document', this.index, {
url: `${this.url}${this.filename}`,
});
},
}, },
}; switchToDocument() {
this.$emit('switch-to-document', this.index, {
url: `${this.url}${this.filename}`,
});
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.image-form { .image-form {
&__error { &__error {
@include regular-text; @include regular-text;
margin-bottom: $medium-spacing; margin-bottom: $medium-spacing;
line-height: 1.5; line-height: 1.5;
} }
&__spinner { &__spinner {
width: 100%; width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
}
&__loading-icon {
@include spin;
fill: $color-silver-dark;
}
&__link {
text-decoration: underline;
@include regular-text;
cursor: pointer;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px; height: 150px;
display: flex; display: flex;
align-items: center; width: 100%;
justify-content: center; justify-content: center;
} align-items: center;
font-family: $sans-serif-font-family;
&__loading-icon { font-weight: $font-weight-regular;
@include spin;
fill: $color-silver-dark;
}
&__link {
text-decoration: underline; text-decoration: underline;
@include regular-text;
cursor: pointer;
}
&__file-input {
width: 0.1px;
height: 0.1px;
overflow: hidden;
opacity: 0;
position: absolute;
z-index: -1;
& + label {
cursor: pointer;
background-color: $color-silver-light;
height: 150px;
display: flex;
width: 100%;
justify-content: center;
align-items: center;
font-family: $sans-serif-font-family;
font-weight: $font-weight-regular;
text-decoration: underline;
}
} }
} }
}
</style> </style>

View File

@ -5,34 +5,34 @@
placeholder="Name erfassen..." placeholder="Name erfassen..."
class="link-form__text skillbox-input" class="link-form__text skillbox-input"
@input="$emit('change-text', $event.target.value, index)" @input="$emit('change-text', $event.target.value, index)"
> />
<input <input
:value="value.url" :value="value.url"
placeholder="URL einfügen..." placeholder="URL einfügen..."
class="link-form__url skillbox-input" class="link-form__url skillbox-input"
@input="$emit('change-url', $event.target.value, index)" @input="$emit('change-url', $event.target.value, index)"
> />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ['value', 'index'] props: ['value', 'index'],
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.link-form { .link-form {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: 11px; grid-row-gap: 11px;
&__text, &__text,
&__url { &__url {
width: $modal-input-width; width: $modal-input-width;
}
} }
}
</style> </style>

View File

@ -12,38 +12,38 @@
</template> </template>
<script> <script>
import InputWithLabel from '@/components/ui/InputWithLabel'; import InputWithLabel from '@/components/ui/InputWithLabel';
export default { export default {
props: { props: {
value: { value: {
type: Object, type: Object,
default: null, default: null,
validator(value) { validator(value) {
return Object.prototype.hasOwnProperty.call(value, 'text'); return Object.prototype.hasOwnProperty.call(value, 'text');
}
}, },
index: {
type: Number,
default: -1
}
}, },
components: {InputWithLabel}, index: {
type: Number,
default: -1,
},
},
components: { InputWithLabel },
computed: { computed: {
text() { text() {
return this.value.text; return this.value.text;
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.subtitle-form { .subtitle-form {
&__input { &__input {
width: 100%; width: 100%;
}
} }
}
</style> </style>

View File

@ -11,35 +11,35 @@
</template> </template>
<script> <script>
export default { export default {
props: { props: {
value: { value: {
type: Object, type: Object,
default: null, default: null,
validator(value) { validator(value) {
return Object.prototype.hasOwnProperty.call(value, 'text'); return Object.prototype.hasOwnProperty.call(value, 'text');
}
}, },
index: { },
type: Number, index: {
default: -1 type: Number,
} default: -1,
},
}, },
computed: { computed: {
text() { text() {
return this.value.text ? this.value.text.replace(/<br(\/)?>/, '\n').replace(/(<([^>]+)>)/ig, '') : ''; return this.value.text ? this.value.text.replace(/<br(\/)?>/, '\n').replace(/(<([^>]+)>)/gi, '') : '';
} },
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.text-form { .text-form {
&__input { &__input {
width: 100%; width: 100%;
}
} }
}
</style> </style>

View File

@ -4,53 +4,50 @@
<span class="text-form-with-help-text__title">{{ title }}</span> <span class="text-form-with-help-text__title">{{ title }}</span>
<helpful-tooltip :text="helpText" /> <helpful-tooltip :text="helpText" />
</h3> </h3>
<text-form <text-form :value="v" @change-text="$emit('change', $event)" />
:value="v"
@change-text="$emit('change', $event)"
/>
</div> </div>
</template> </template>
<script> <script>
import TextForm from '@/components/content-forms/TextForm'; import TextForm from '@/components/content-forms/TextForm';
import HelpfulTooltip from '@/components/HelpfulTooltip'; import HelpfulTooltip from '@/components/HelpfulTooltip';
export default { export default {
props: ['title', 'value', 'helpText'], props: ['title', 'value', 'helpText'],
components: { components: {
TextForm, TextForm,
HelpfulTooltip HelpfulTooltip,
},
computed: {
v() {
return {
text: this.value,
};
}, },
},
computed: { };
v() {
return {
text: this.value
};
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_functions.scss"; @import '@/styles/_functions.scss';
.text-form-with-help-text { .text-form-with-help-text {
margin-bottom: 30px; margin-bottom: 30px;
&__heading { &__heading {
margin-bottom: 15px; margin-bottom: 15px;
display: flex; display: flex;
justify-items: center; justify-items: center;
align-items: center; align-items: center;
}
&__title {
font-size: toRem(22px);
font-weight: 600;
margin-right: 8px;
}
} }
&__title {
font-size: toRem(22px);
font-weight: 600;
margin-right: 8px;
}
}
</style> </style>

View File

@ -1,146 +1,130 @@
<template> <template>
<div class="tip-tap"> <div class="tip-tap">
<editor-content <editor-content class="tip-tap__editor-wrapper" :editor="editor" />
class="tip-tap__editor-wrapper"
:editor="editor"
/>
<toggle <toggle :bordered="false" :checked="isList" label="Als Liste formatieren" @input="toggleList" />
:bordered="false"
:checked="isList"
label="Als Liste formatieren"
@input="toggleList"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, {PropType} from 'vue'; import Vue, { PropType } from 'vue';
import {Editor, EditorContent} from "@tiptap/vue-2"; import { Editor, EditorContent } from '@tiptap/vue-2';
import Document from '@tiptap/extension-document'; import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph'; import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text'; import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list'; import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item'; import ListItem from '@tiptap/extension-list-item';
import Toggle from "@/components/ui/Toggle.vue"; import Toggle from '@/components/ui/Toggle.vue';
interface Data { interface Data {
editor: Editor | undefined; editor: Editor | undefined;
} }
interface Value { interface Value {
text: string; text: string;
} }
export default Vue.extend({ export default Vue.extend({
props: { props: {
value: { value: {
type: Object as PropType<Value>, type: Object as PropType<Value>,
validator(value: Value) { validator(value: Value) {
return Object.prototype.hasOwnProperty.call(value, 'text'); return Object.prototype.hasOwnProperty.call(value, 'text');
},
},
},
components: {
Toggle,
EditorContent,
},
data(): Data {
return {
editor: undefined,
};
},
computed: {
isList(): boolean {
return this.editor?.isActive('bulletList') || false;
},
text(): string {
return this.value.text;
},
},
watch: {
value({ text }: Value) {
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
const isSame = editor.getHTML() === text;
if (isSame) {
return;
}
editor.commands.setContent(text, false);
},
},
mounted() {
this.editor = new Editor({
editorProps: {
attributes: {
class: 'tip-tap__editor',
}, },
}, },
}, content: this.text,
extensions: [Document, Paragraph, Text, BulletList, ListItem],
components: { onUpdate: () => {
Toggle, const text = (this.editor as Editor).getHTML();
EditorContent this.$emit('input', text);
}, this.$emit('change-text', text);
data(): Data {
return {
editor: undefined,
};
},
computed: {
isList(): boolean {
return this.editor?.isActive('bulletList') || false;
}, },
text(): string { });
return this.value.text; },
}
beforeDestroy() {
this.editor?.destroy();
},
methods: {
toggleList() {
const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run();
}, },
},
watch: { });
value({text}: Value) {
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
const isSame = editor.getHTML() === text;
if (isSame) {
return;
}
editor.commands.setContent(text, false);
}
},
mounted() {
this.editor = new Editor({
editorProps: {
attributes: {
class: 'tip-tap__editor'
}
},
content: this.text,
extensions: [
Document,
Paragraph,
Text,
BulletList,
ListItem
],
onUpdate: () => {
const text=(this.editor as Editor).getHTML();
this.$emit('input', text);
this.$emit('change-text', text);
}
});
},
beforeDestroy() {
this.editor?.destroy();
},
methods: {
toggleList() {
const editor = this.editor as Editor;
editor.chain().selectAll().toggleBulletList().run();
},
}
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.tip-tap {
.tip-tap { &__editor-wrapper {
margin-bottom: $medium-spacing;
&__editor-wrapper {
margin-bottom: $medium-spacing;
}
:deep(.tip-tap__editor) {
@include inputstyle;
flex-direction: column;
min-height: 150px;
}
:deep(ul) {
padding-left: $medium-spacing;
list-style: initial;
}
:deep(li) {
@include inputfont;
}
:deep(div) {
@include inputfont;
}
:deep(p) {
@include inputfont;
}
} }
:deep(.tip-tap__editor) {
@include inputstyle;
flex-direction: column;
min-height: 150px;
}
:deep(ul) {
padding-left: $medium-spacing;
list-style: initial;
}
:deep(li) {
@include inputfont;
}
:deep(div) {
@include inputfont;
}
:deep(p) {
@include inputfont;
}
}
</style> </style>

View File

@ -1,21 +1,11 @@
<template> <template>
<div> <div>
<div <div class="video-form" v-if="!isVimeo && !isYoutube && !isSrf">
class="video-form"
v-if="!isVimeo && !isYoutube && !isSrf"
>
<info-icon class="video-form__help-icon help-text__icon" /> <info-icon class="video-form__help-icon help-text__icon" />
<p class="video-form__help-description help-text__description"> <p class="video-form__help-description help-text__description">
Sie können Videos auf <a Sie können Videos auf
class="video-form__platform-link help-text__link" <a class="video-form__platform-link help-text__link" href="https://youtube.com/" target="_blank">Youtube</a>
href="https://youtube.com/" oder <a class="video-form__platform-link help-text__link" href="https://vimeo.com/" target="_blank">Vimeo</a>
target="_blank"
>Youtube</a>
oder <a
class="video-form__platform-link help-text__link"
href="https://vimeo.com/"
target="_blank"
>Vimeo</a>
hochladen und anschliessen einen Link hier einfügen. hochladen und anschliessen einen Link hier einfügen.
</p> </p>
@ -24,7 +14,7 @@
class="video-form__video-link skillbox-input" class="video-form__video-link skillbox-input"
placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ" placeholder="Bsp: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
@input="$emit('change-url', $event.target.value, index)" @input="$emit('change-url', $event.target.value, index)"
> />
</div> </div>
<div v-if="isYoutube"> <div v-if="isYoutube">
@ -40,66 +30,64 @@
</template> </template>
<script> <script>
import YoutubeEmbed from '@/components/videos/YoutubeEmbed'; import YoutubeEmbed from '@/components/videos/YoutubeEmbed';
import VimeoEmbed from '@/components/videos/VimeoEmbed'; import VimeoEmbed from '@/components/videos/VimeoEmbed';
import SrfEmbed from '@/components/videos/SrfEmbed'; import SrfEmbed from '@/components/videos/SrfEmbed';
import {isVimeoUrl, isYoutubeUrl, isSrfUrl} from '@/helpers/video'; import { isVimeoUrl, isYoutubeUrl, isSrfUrl } from '@/helpers/video';
const InfoIcon = () => import(/* webpackChunkName: "icons" */'@/components/icons/InfoIcon'); const InfoIcon = () => import(/* webpackChunkName: "icons" */ '@/components/icons/InfoIcon');
export default { export default {
props: ['value', 'index'], props: ['value', 'index'],
components: { components: {
InfoIcon, InfoIcon,
YoutubeEmbed, YoutubeEmbed,
VimeoEmbed, VimeoEmbed,
SrfEmbed SrfEmbed,
},
computed: {
isYoutube() {
return isYoutubeUrl(this.value.url);
}, },
isVimeo() {
computed: { return isVimeoUrl(this.value.url);
isYoutube() { },
return isYoutubeUrl(this.value.url); isSrf() {
}, return isSrfUrl(this.value.url);
isVimeo() { },
return isVimeoUrl(this.value.url); },
}, };
isSrf() {
return isSrfUrl(this.value.url);
}
}
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_functions.scss"; @import '@/styles/_functions.scss';
.video-form { .video-form {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-template-columns: 40px 1fr; grid-template-columns: 40px 1fr;
grid-column-gap: 16px; grid-column-gap: 16px;
grid-row-gap: 20px; grid-row-gap: 20px;
align-items: center; align-items: center;
&__help-icon { &__help-icon {
}
&__help-description {
}
&__platform-link {
font-family: $sans-serif-font-family;
text-decoration: underline;
font-weight: $font-weight-regular;
font-size: toRem(17px);
}
&__video-link {
grid-column: 1 / span 2;
width: $modal-input-width
}
} }
&__help-description {
}
&__platform-link {
font-family: $sans-serif-font-family;
text-decoration: underline;
font-weight: $font-weight-regular;
font-size: toRem(17px);
}
&__video-link {
grid-column: 1 / span 2;
width: $modal-input-width;
}
}
</style> </style>

Some files were not shown because too many files have changed in this diff Show More