diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..5f2302e --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,403 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . +/* jshint node: true, browser: false */ +/* eslint-env node */ + +/** + * @copyright 2014 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Grunt configuration + */ + +module.exports = function(grunt) { + var path = require('path'), + tasks = {}, + cwd = process.env.PWD || process.cwd(), + async = require('async'), + DOMParser = require('xmldom').DOMParser, + xpath = require('xpath'), + semver = require('semver'); + + // Verify the node version is new enough. + var expected = semver.validRange(grunt.file.readJSON('package.json').engines.node); + var actual = semver.valid(process.version); + if (!semver.satisfies(actual, expected)) { + grunt.fail.fatal('Node version too old. Require ' + expected + ', version installed: ' + actual); + } + + // Windows users can't run grunt in a subdirectory, so allow them to set + // the root by passing --root=path/to/dir. + if (grunt.option('root')) { + var root = grunt.option('root'); + if (grunt.file.exists(__dirname, root)) { + cwd = path.join(__dirname, root); + grunt.log.ok('Setting root to ' + cwd); + } else { + grunt.fail.fatal('Setting root to ' + root + ' failed - path does not exist'); + } + } + + var inAMD = path.basename(cwd) == 'amd'; + + // Globbing pattern for matching all AMD JS source files. + var amdSrc = [inAMD ? cwd + '/src/*.js' : '**/amd/src/*.js']; + + /** + * Function to generate the destination for the uglify task + * (e.g. build/file.min.js). This function will be passed to + * the rename property of files array when building dynamically: + * http://gruntjs.com/configuring-tasks#building-the-files-object-dynamically + * + * @param {String} destPath the current destination + * @param {String} srcPath the matched src path + * @return {String} The rewritten destination path. + */ + var uglifyRename = function(destPath, srcPath) { + destPath = srcPath.replace('src', 'build'); + destPath = destPath.replace('.js', '.min.js'); + destPath = path.resolve(cwd, destPath); + return destPath; + }; + + /** + * Find thirdpartylibs.xml and generate an array of paths contained within + * them (used to generate ignore files and so on). + * + * @return {array} The list of thirdparty paths. + */ + var getThirdPartyPathsFromXML = function() { + var thirdpartyfiles = grunt.file.expand('*/**/thirdpartylibs.xml'); + var libs = ['node_modules/', 'vendor/']; + + thirdpartyfiles.forEach(function(file) { + var dirname = path.dirname(file); + + var doc = new DOMParser().parseFromString(grunt.file.read(file)); + var nodes = xpath.select("/libraries/library/location/text()", doc); + + nodes.forEach(function(node) { + var lib = path.join(dirname, node.toString()); + if (grunt.file.isDir(lib)) { + // Ensure trailing slash on dirs. + lib = lib.replace(/\/?$/, '/'); + } + + // Look for duplicate paths before adding to array. + if (libs.indexOf(lib) === -1) { + libs.push(lib); + } + }); + }); + return libs; + }; + + // Project configuration. + grunt.initConfig({ + eslint: { + // Even though warnings dont stop the build we don't display warnings by default because + // at this moment we've got too many core warnings. + options: {quiet: !grunt.option('show-lint-warnings')}, + amd: { + src: amdSrc, + // Check AMD with some slightly stricter rules. + rules: { + 'no-unused-vars': 'error', + 'no-implicit-globals': 'error' + } + }, + // Check YUI module source files. + yui: { + src: ['**/yui/src/**/*.js', '!*/**/yui/src/*/meta/*.js'], + options: { + // Disable some rules which we can't safely define for YUI rollups. + rules: { + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'no-unused-expressions': 'off' + } + } + } + }, + uglify: { + amd: { + files: [{ + expand: true, + src: amdSrc, + rename: uglifyRename + }], + options: {report: 'none'} + } + }, + less: { + bootstrapbase: { + files: { + "theme/bootstrapbase/style/moodle.css": "theme/bootstrapbase/less/moodle.less", + "theme/bootstrapbase/style/editor.css": "theme/bootstrapbase/less/editor.less", + }, + options: { + compress: false // We must not compress to keep the comments. + } + } + }, + watch: { + options: { + nospawn: true // We need not to spawn so config can be changed dynamically. + }, + amd: { + files: ['**/amd/src/**/*.js'], + tasks: ['amd'] + }, + bootstrapbase: { + files: ["theme/bootstrapbase/less/**/*.less"], + tasks: ["css"] + }, + yui: { + files: ['**/yui/src/**/*.js'], + tasks: ['yui'] + }, + gherkinlint: { + files: ['**/tests/behat/*.feature'], + tasks: ['gherkinlint'] + } + }, + shifter: { + options: { + recursive: true, + paths: [cwd] + } + }, + gherkinlint: { + options: { + files: ['**/tests/behat/*.feature'], + } + }, + stylelint: { + less: { + options: { + syntax: 'less', + configOverrides: { + rules: { + // These rules have to be disabled in .stylelintrc for scss compat. + "at-rule-no-unknown": true, + "no-browser-hacks": [true, {"severity": "warning"}] + } + } + }, + src: ['theme/**/*.less'] + }, + scss: { + options: {syntax: 'scss'}, + src: ['*/**/*.scss'] + }, + css: { + src: ['*/**/*.css'], + options: { + configOverrides: { + rules: { + // These rules have to be disabled in .stylelintrc for scss compat. + "at-rule-no-unknown": true, + "no-browser-hacks": [true, {"severity": "warning"}] + } + } + } + } + } + }); + + /** + * Generate ignore files (utilising thirdpartylibs.xml data) + */ + tasks.ignorefiles = function() { + // An array of paths to third party directories. + var thirdPartyPaths = getThirdPartyPathsFromXML(); + // Generate .eslintignore. + var eslintIgnores = ['# Generated by "grunt ignorefiles"', '*/**/yui/src/*/meta/', '*/**/build/'].concat(thirdPartyPaths); + grunt.file.write('.eslintignore', eslintIgnores.join('\n')); + // Generate .stylelintignore. + var stylelintIgnores = [ + '# Generated by "grunt ignorefiles"', + 'theme/bootstrapbase/style/', + 'theme/clean/style/custom.css', + 'theme/more/style/custom.css' + ].concat(thirdPartyPaths); + grunt.file.write('.stylelintignore', stylelintIgnores.join('\n')); + }; + + /** + * Shifter task. Is configured with a path to a specific file or a directory, + * in the case of a specific file it will work out the right module to be built. + * + * Note that this task runs the invidiaul shifter jobs async (becase it spawns + * so be careful to to call done(). + */ + tasks.shifter = function() { + var done = this.async(), + options = grunt.config('shifter.options'); + + // Run the shifter processes one at a time to avoid confusing output. + async.eachSeries(options.paths, function(src, filedone) { + var args = []; + args.push(path.normalize(__dirname + '/node_modules/shifter/bin/shifter')); + + // Always ignore the node_modules directory. + args.push('--excludes', 'node_modules'); + + // Determine the most appropriate options to run with based upon the current location. + if (grunt.file.isMatch('**/yui/**/*.js', src)) { + // When passed a JS file, build our containing module (this happen with + // watch). + grunt.log.debug('Shifter passed a specific JS file'); + src = path.dirname(path.dirname(src)); + options.recursive = false; + } else if (grunt.file.isMatch('**/yui/src', src)) { + // When in a src directory --walk all modules. + grunt.log.debug('In a src directory'); + args.push('--walk'); + options.recursive = false; + } else if (grunt.file.isMatch('**/yui/src/*', src)) { + // When in module, only build our module. + grunt.log.debug('In a module directory'); + options.recursive = false; + } else if (grunt.file.isMatch('**/yui/src/*/js', src)) { + // When in module src, only build our module. + grunt.log.debug('In a source directory'); + src = path.dirname(src); + options.recursive = false; + } + + if (grunt.option('watch')) { + grunt.fail.fatal('The --watch option has been removed, please use `grunt watch` instead'); + } + + // Add the stderr option if appropriate + if (grunt.option('verbose')) { + args.push('--lint-stderr'); + } + + if (grunt.option('no-color')) { + args.push('--color=false'); + } + + var execShifter = function() { + + grunt.log.ok("Running shifter on " + src); + grunt.util.spawn({ + cmd: "node", + args: args, + opts: {cwd: src, stdio: 'inherit', env: process.env} + }, function(error, result, code) { + if (code) { + grunt.fail.fatal('Shifter failed with code: ' + code); + } else { + grunt.log.ok('Shifter build complete.'); + filedone(); + } + }); + }; + + // Actually run shifter. + if (!options.recursive) { + execShifter(); + } else { + // Check that there are yui modules otherwise shifter ends with exit code 1. + if (grunt.file.expand({cwd: src}, '**/yui/src/**/*.js').length > 0) { + args.push('--recursive'); + execShifter(); + } else { + grunt.log.ok('No YUI modules to build.'); + filedone(); + } + } + }, done); + }; + + tasks.gherkinlint = function() { + var done = this.async(), + options = grunt.config('gherkinlint.options'); + + var args = grunt.file.expand(options.files); + args.unshift(path.normalize(__dirname + '/node_modules/.bin/gherkin-lint')); + grunt.util.spawn({ + cmd: 'node', + args: args, + opts: {stdio: 'inherit', env: process.env} + }, function(error, result, code) { + // Propagate the exit code. + done(code === 0); + }); + }; + + tasks.startup = function() { + // Are we in a YUI directory? + if (path.basename(path.resolve(cwd, '../../')) == 'yui') { + grunt.task.run('yui'); + // Are we in an AMD directory? + } else if (inAMD) { + grunt.task.run('amd'); + } else { + // Run them all!. + grunt.task.run('css'); + grunt.task.run('js'); + grunt.task.run('gherkinlint'); + } + }; + + // On watch, we dynamically modify config to build only affected files. This + // method is slightly complicated to deal with multiple changed files at once (copied + // from the grunt-contrib-watch readme). + var changedFiles = Object.create(null); + var onChange = grunt.util._.debounce(function() { + var files = Object.keys(changedFiles); + grunt.config('eslint.amd.src', files); + grunt.config('eslint.yui.src', files); + grunt.config('uglify.amd.files', [{expand: true, src: files, rename: uglifyRename}]); + grunt.config('shifter.options.paths', files); + grunt.config('stylelint.less.src', files); + grunt.config('gherkinlint.options.files', files); + changedFiles = Object.create(null); + }, 200); + + grunt.event.on('watch', function(action, filepath) { + changedFiles[filepath] = action; + onChange(); + }); + + // Register NPM tasks. + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-eslint'); + grunt.loadNpmTasks('grunt-stylelint'); + + // Register JS tasks. + grunt.registerTask('shifter', 'Run Shifter against the current directory', tasks.shifter); + grunt.registerTask('gherkinlint', 'Run gherkinlint against the current directory', tasks.gherkinlint); + grunt.registerTask('ignorefiles', 'Generate ignore files for linters', tasks.ignorefiles); + grunt.registerTask('yui', ['eslint:yui', 'shifter']); + grunt.registerTask('amd', ['eslint:amd', 'uglify']); + grunt.registerTask('js', ['amd', 'yui']); + + // Register CSS taks. + grunt.registerTask('css', ['stylelint:scss', 'stylelint:less', 'less:bootstrapbase', 'stylelint:css']); + + // Register the startup task. + grunt.registerTask('startup', 'Run the correct tasks for the current directory', tasks.startup); + + // Register the default task. + grunt.registerTask('default', ['startup']); +}; diff --git a/README.md b/README.md index 19c9fd4..16fc65e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@ Atto multilanguage plugin ========================= -![Release](https://img.shields.io/badge/release-v1.6-blue.svg) ![Supported](https://img.shields.io/badge/supported-2.9%2C%203.0%2C%203.1-green.svg) +![Release](https://img.shields.io/badge/release-v1.9-blue.svg) ![Supported](https://img.shields.io/badge/supported-2.9%2C%203.0%2C%203.1%2C%203.2%2C%203.3%2C%203.4-green.svg) This plugin will make the creation of multilingual contents on Moodle much more easier with Atto editor. The plugin is developed to work with [Iñaki Arenaza's multilang2 filter](https://github.com/iarenaza/moodle-filter_multilang2), and the idea is based on [his plugin for TinyMCE editor](https://github.com/iarenaza/moodle-tinymce_moodlelang2). ## Current version -The latest release is the v1.6 (build 2016042800) for Moodle 2.9, 3.0 and 3.1. Checkout [v2.9.1.6](https://github.com/julenpardo/moodle-atto_multilang2/releases/tag/v2.9.1.6), [v3.0.1.6](https://github.com/julenpardo/moodle-atto_multilang2/releases/tag/v3.0.1.6), and [v3.1.1.6](https://github.com/julenpardo/moodle-atto_multilang2/releases/tag/v3.1.1.6) releases, respectively. -## Changes from v1.5 - - Fix issue "Filepicker not loading when grading assignment" (see [issue 17](https://github.com/julenpardo/moodle-atto_multilang2/issues/17)). +The latest release is v1.9 (build 2017102700) for Moodle 2.9, 3.0, 3.1, 3.2, 3.4 and 3.4 (Checkout [v2.9.1.9](https://github.com/iarenaza/moodle-atto_multilang2/releases/tag/v2.9.1.9), [v3.0.1.9](https://github.com/iarenaza/moodle-atto_multilang2/releases/tag/v3.0.1.9), [v3.1.1.9](https://github.com/iarenaza/moodle-atto_multilang2/releases/tag/v3.1.1.9), [v3.2.1.9](https://github.com/iarenaza/moodle-atto_multilang2/releases/tag/v3.2.1.9) and [v3.3.1.9](https://github.com/iarenaza/moodle-atto_multilang2/releases/tag/v3.3.1.9) releases, respectively. + +## Changes from v1.8 + - Added Grunt support files. No we can check Moodle Javascript coding guidelines and minify the plugin code without installing the plugins. + - Minor Javascript coding guidelines fix (signalled by the minifier, but ignored by the linter!) + +## Changes from v1.7 + - Multiple bug fixes, especially when there were more than one Atto editor in the same page. Closes issues #2, #18, #21. + - Multilang tag highlighting 's are now removed/added when switching to/from HTML view, reducing clutter when editing the HTML code. + - Cleaned the code to conform to PHP, PHPDoc and Javascript Moodle coding guidelines (all checks pass!) ## Requirements As mentioned before, [filter_multilang2](https://github.com/iarenaza/moodle-filter_multilang2) is required. @@ -19,11 +26,8 @@ As mentioned before, [filter_multilang2](https://github.com/iarenaza/moodle-filt ## Installation - Copy repository content in *moodleroot*/lib/editor/atto/plugins. The following can be omitted: - - moodle-javascript_style_checker/ - tests/ (if you're not going to test it with Behat) - .gitmodules - build.xml - - Install it from Moodle. - - Go to Site administration/Plugins/Text - editors/Atto HTML editor/Atto toolbar settings, and add *multilang2* - to the Toolbar config where you prefer. E.g. `multilang2 = multilang2` + - Install the plugin from Moodle. + - Go to "Site administration" >> "Plugins" >> "Text editors" >> "Atto HTML editor" >> "Atto toolbar settings", and add *multilang2* to the Toolbar config where you prefer. E.g. `multilang2 = multilang2` (see [Text editor - Toolbar settings](https://docs.moodle.org/en/Text_editor#Toolbar_settings) and [Text editor - Adding extra buttons](https://docs.moodle.org/en/Text_editor#Adding_extra_buttons) for instructions on how to add a plugin button to Atto toolbar. diff --git a/db/access.php b/db/access.php index 2e9bb23..459e75c 100644 --- a/db/access.php +++ b/db/access.php @@ -14,12 +14,22 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . +/** + * Atto text editor multilanguage plugin lib. + * + * @package atto_multilang2 + * @copyright 2015 onwards Julen Pardo & Mondragon Unibertsitatea + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + defined('MOODLE_INTERNAL') || die(); -// Plugin for Moodle 'Multilingual content' drop down menu. -// @package atto_multilang2 -// @copyright 2016 onwards Julen Pardo & Mondragon Unibertsitatea -// @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. +/* + * Plugin for Moodle 'Multilingual content' drop down menu. + * @package atto_multilang2 + * @copyright 2016 onwards Julen Pardo & Mondragon Unibertsitatea + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. + */ $capabilities = array( 'atto/multilang2:viewlanguagemenu' => array( diff --git a/default-css.php b/default-css.php index 5fdc0e5..0b5f0b0 100644 --- a/default-css.php +++ b/default-css.php @@ -22,12 +22,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$multilang2_default_css = <<< EOF +defined('MOODLE_INTERNAL') || die(); +$multilang2defaultcss = " outline: 1px dotted; padding: 0.1em; margin: 0em 0.1em; background-color: #ffffaa; - -EOF -; +"; diff --git a/lang/en/atto_multilang2.php b/lang/en/atto_multilang2.php index d073098..a7d90ff 100644 --- a/lang/en/atto_multilang2.php +++ b/lang/en/atto_multilang2.php @@ -28,3 +28,4 @@ $string['customcss'] = 'CSS for delimiters'; $string['customcss_desc'] = "
CSS used to highlight the multi-language content delimiters.

Attention: just put the CSS BODY RULES, excluding the selectors, just as it appears in the default value.

"; +$string['multilang2:viewlanguagemenu'] = 'View language menu in the editor'; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2a7d142 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "Moodle", + "private": true, + "description": "Moodle", + "devDependencies": { + "async": "1.5.2", + "eslint": "3.7.1", + "gherkin-lint": "1.1.3", + "grunt": "1.0.1", + "grunt-contrib-less": "1.3.0", + "grunt-contrib-uglify": "1.0.1", + "grunt-contrib-watch": "1.0.0", + "grunt-eslint": "19.0.0", + "grunt-stylelint": "0.6.0", + "semver": "5.3.0", + "shifter": "0.5.0", + "stylelint": "7.4.1", + "stylelint-checkstyle-formatter": "0.1.0", + "xmldom": "0.1.22", + "xpath": "0.0.23" + }, + "engines": { + "node": ">=4" + } +} diff --git a/settings.php b/settings.php index ef0f539..d38e26e 100644 --- a/settings.php +++ b/settings.php @@ -30,4 +30,4 @@ get_string('highlight', 'atto_multilang2'), get_string('highlight_desc', 'atto_multilang2'), 1)); $settings->add(new admin_setting_configtextarea('atto_multilang2/customcss', get_string('customcss', 'atto_multilang2'), get_string('customcss_desc', 'atto_multilang2'), - $multilang2_default_css, PARAM_RAW)); + $multilang2defaultcss, PARAM_RAW)); diff --git a/version.php b/version.php index d48e22d..12b9d84 100644 --- a/version.php +++ b/version.php @@ -24,9 +24,9 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016052600; // The current plugin version (Date: YYYYMMDDXX). -$plugin->release = 'v2.9.1.6 (version 1.6 for Moodle 2.9) (Build 2016052600)'; -$plugin->requires = 2015051100; // Required Moodle version. +$plugin->version = 2017102700; // The current plugin version (Date: YYYYMMDDXX). +$plugin->release = 'v2.9.1.9 (version 1.9 for Moodle 2.9) (Build 2017102700)'; +$plugin->requires = 2015051100; // Required Moodle version. $plugin->component = 'atto_multilang2'; // Full name of the plugin (used for diagnostics). $plugin->maturity = MATURITY_STABLE; $plugin->dependencies = array( diff --git a/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-coverage.js b/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-coverage.js index 8aa9f76..3eb8e65 100644 --- a/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-coverage.js +++ b/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-coverage.js @@ -1,6 +1,6 @@ if (typeof __coverage__ === 'undefined') { __coverage__ = {}; } if (!__coverage__['build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js']) { - __coverage__['build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js'] = {"path":"build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js","s":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":0,"68":0,"69":0,"70":0,"71":0,"72":0,"73":0,"74":0,"75":0,"76":0,"77":0,"78":0,"79":0,"80":0,"81":0,"82":0,"83":0,"84":0,"85":0,"86":0,"87":0,"88":0,"89":0,"90":0,"91":0,"92":0,"93":0,"94":0,"95":0,"96":0,"97":0,"98":0,"99":0,"100":0,"101":0,"102":0,"103":0,"104":0,"105":0,"106":0,"107":0,"108":0,"109":0,"110":0,"111":0,"112":0,"113":0,"114":0,"115":0,"116":0,"117":0,"118":0,"119":0,"120":0,"121":0,"122":0,"123":0,"124":0,"125":0,"126":0,"127":0,"128":0,"129":0,"130":0,"131":0,"132":0},"b":{"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0],"16":[0,0],"17":[0,0],"18":[0,0],"19":[0,0],"20":[0,0],"21":[0,0]},"f":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0},"fnMap":{"1":{"name":"(anonymous_1)","line":1,"loc":{"start":{"line":1,"column":41},"end":{"line":1,"column":60}}},"2":{"name":"(anonymous_2)","line":73,"loc":{"start":{"line":73,"column":17},"end":{"line":73,"column":28}}},"3":{"name":"(anonymous_3)","line":107,"loc":{"start":{"line":107,"column":22},"end":{"line":107,"column":33}}},"4":{"name":"(anonymous_4)","line":126,"loc":{"start":{"line":126,"column":29},"end":{"line":126,"column":40}}},"5":{"name":"(anonymous_5)","line":161,"loc":{"start":{"line":161,"column":14},"end":{"line":161,"column":40}}},"6":{"name":"(anonymous_6)","line":188,"loc":{"start":{"line":188,"column":23},"end":{"line":188,"column":34}}},"7":{"name":"(anonymous_7)","line":222,"loc":{"start":{"line":222,"column":27},"end":{"line":222,"column":38}}},"8":{"name":"(anonymous_8)","line":245,"loc":{"start":{"line":245,"column":25},"end":{"line":245,"column":36}}},"9":{"name":"(anonymous_9)","line":260,"loc":{"start":{"line":260,"column":33},"end":{"line":260,"column":54}}},"10":{"name":"(anonymous_10)","line":294,"loc":{"start":{"line":294,"column":24},"end":{"line":294,"column":54}}},"11":{"name":"(anonymous_11)","line":323,"loc":{"start":{"line":323,"column":27},"end":{"line":323,"column":38}}},"12":{"name":"(anonymous_12)","line":388,"loc":{"start":{"line":388,"column":26},"end":{"line":388,"column":37}}},"13":{"name":"(anonymous_13)","line":457,"loc":{"start":{"line":457,"column":25},"end":{"line":457,"column":36}}},"14":{"name":"(anonymous_14)","line":504,"loc":{"start":{"line":504,"column":29},"end":{"line":504,"column":40}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":573,"column":61}},"2":{"start":{"line":28,"column":0},"end":{"line":52,"column":56}},"3":{"start":{"line":62,"column":0},"end":{"line":570,"column":3}},"4":{"start":{"line":74,"column":8},"end":{"line":75,"column":30}},"5":{"start":{"line":77,"column":8},"end":{"line":98,"column":9}},"6":{"start":{"line":78,"column":12},"end":{"line":78,"column":58}},"7":{"start":{"line":79,"column":12},"end":{"line":79,"column":55}},"8":{"start":{"line":81,"column":12},"end":{"line":88,"column":15}},"9":{"start":{"line":90,"column":12},"end":{"line":90,"column":91}},"10":{"start":{"line":92,"column":12},"end":{"line":92,"column":36}},"11":{"start":{"line":94,"column":12},"end":{"line":97,"column":13}},"12":{"start":{"line":95,"column":16},"end":{"line":95,"column":43}},"13":{"start":{"line":96,"column":16},"end":{"line":96,"column":43}},"14":{"start":{"line":108,"column":8},"end":{"line":109,"column":18}},"15":{"start":{"line":111,"column":8},"end":{"line":111,"column":48}},"16":{"start":{"line":112,"column":8},"end":{"line":112,"column":32}},"17":{"start":{"line":113,"column":8},"end":{"line":113,"column":30}},"18":{"start":{"line":115,"column":8},"end":{"line":115,"column":41}},"19":{"start":{"line":127,"column":8},"end":{"line":129,"column":21}},"20":{"start":{"line":131,"column":8},"end":{"line":131,"column":57}},"21":{"start":{"line":133,"column":8},"end":{"line":140,"column":9}},"22":{"start":{"line":134,"column":12},"end":{"line":139,"column":13}},"23":{"start":{"line":135,"column":16},"end":{"line":138,"column":19}},"24":{"start":{"line":142,"column":8},"end":{"line":142,"column":28}},"25":{"start":{"line":162,"column":8},"end":{"line":165,"column":20}},"26":{"start":{"line":167,"column":8},"end":{"line":167,"column":84}},"27":{"start":{"line":169,"column":8},"end":{"line":169,"column":45}},"28":{"start":{"line":170,"column":8},"end":{"line":170,"column":87}},"29":{"start":{"line":172,"column":8},"end":{"line":172,"column":71}},"30":{"start":{"line":173,"column":8},"end":{"line":173,"column":73}},"31":{"start":{"line":175,"column":8},"end":{"line":175,"column":54}},"32":{"start":{"line":177,"column":8},"end":{"line":177,"column":27}},"33":{"start":{"line":189,"column":8},"end":{"line":193,"column":19}},"34":{"start":{"line":195,"column":8},"end":{"line":210,"column":9}},"35":{"start":{"line":196,"column":12},"end":{"line":196,"column":46}},"36":{"start":{"line":198,"column":12},"end":{"line":204,"column":13}},"37":{"start":{"line":199,"column":16},"end":{"line":199,"column":58}},"38":{"start":{"line":200,"column":16},"end":{"line":202,"column":17}},"39":{"start":{"line":201,"column":20},"end":{"line":201,"column":87}},"40":{"start":{"line":203,"column":16},"end":{"line":203,"column":43}},"41":{"start":{"line":206,"column":15},"end":{"line":210,"column":9}},"42":{"start":{"line":207,"column":12},"end":{"line":209,"column":13}},"43":{"start":{"line":208,"column":16},"end":{"line":208,"column":65}},"44":{"start":{"line":212,"column":8},"end":{"line":212,"column":20}},"45":{"start":{"line":223,"column":8},"end":{"line":227,"column":22}},"46":{"start":{"line":229,"column":8},"end":{"line":229,"column":67}},"47":{"start":{"line":230,"column":8},"end":{"line":230,"column":63}},"48":{"start":{"line":232,"column":8},"end":{"line":234,"column":9}},"49":{"start":{"line":233,"column":12},"end":{"line":233,"column":70}},"50":{"start":{"line":246,"column":8},"end":{"line":246,"column":56}},"51":{"start":{"line":248,"column":8},"end":{"line":248,"column":67}},"52":{"start":{"line":261,"column":8},"end":{"line":265,"column":28}},"53":{"start":{"line":267,"column":8},"end":{"line":267,"column":69}},"54":{"start":{"line":269,"column":8},"end":{"line":279,"column":9}},"55":{"start":{"line":270,"column":12},"end":{"line":270,"column":47}},"56":{"start":{"line":271,"column":12},"end":{"line":271,"column":62}},"57":{"start":{"line":273,"column":12},"end":{"line":273,"column":70}},"58":{"start":{"line":274,"column":12},"end":{"line":274,"column":77}},"59":{"start":{"line":276,"column":12},"end":{"line":278,"column":13}},"60":{"start":{"line":277,"column":16},"end":{"line":277,"column":82}},"61":{"start":{"line":295,"column":8},"end":{"line":295,"column":31}},"62":{"start":{"line":297,"column":8},"end":{"line":297,"column":37}},"63":{"start":{"line":298,"column":8},"end":{"line":298,"column":35}},"64":{"start":{"line":300,"column":8},"end":{"line":300,"column":62}},"65":{"start":{"line":301,"column":8},"end":{"line":301,"column":39}},"66":{"start":{"line":324,"column":8},"end":{"line":332,"column":30}},"67":{"start":{"line":334,"column":8},"end":{"line":334,"column":78}},"68":{"start":{"line":336,"column":8},"end":{"line":340,"column":9}},"69":{"start":{"line":337,"column":12},"end":{"line":337,"column":33}},"70":{"start":{"line":338,"column":12},"end":{"line":338,"column":27}},"71":{"start":{"line":339,"column":12},"end":{"line":339,"column":36}},"72":{"start":{"line":342,"column":8},"end":{"line":364,"column":9}},"73":{"start":{"line":343,"column":12},"end":{"line":343,"column":58}},"74":{"start":{"line":344,"column":12},"end":{"line":344,"column":64}},"75":{"start":{"line":346,"column":12},"end":{"line":346,"column":50}},"76":{"start":{"line":348,"column":12},"end":{"line":348,"column":65}},"77":{"start":{"line":350,"column":12},"end":{"line":352,"column":13}},"78":{"start":{"line":351,"column":16},"end":{"line":351,"column":25}},"79":{"start":{"line":354,"column":12},"end":{"line":361,"column":13}},"80":{"start":{"line":355,"column":16},"end":{"line":355,"column":56}},"81":{"start":{"line":356,"column":16},"end":{"line":356,"column":73}},"82":{"start":{"line":358,"column":16},"end":{"line":358,"column":69}},"83":{"start":{"line":360,"column":16},"end":{"line":360,"column":77}},"84":{"start":{"line":363,"column":12},"end":{"line":363,"column":49}},"85":{"start":{"line":366,"column":8},"end":{"line":366,"column":27}},"86":{"start":{"line":389,"column":8},"end":{"line":399,"column":21}},"87":{"start":{"line":401,"column":8},"end":{"line":401,"column":81}},"88":{"start":{"line":402,"column":8},"end":{"line":402,"column":89}},"89":{"start":{"line":404,"column":8},"end":{"line":408,"column":9}},"90":{"start":{"line":405,"column":12},"end":{"line":405,"column":33}},"91":{"start":{"line":406,"column":12},"end":{"line":406,"column":27}},"92":{"start":{"line":407,"column":12},"end":{"line":407,"column":36}},"93":{"start":{"line":410,"column":8},"end":{"line":435,"column":9}},"94":{"start":{"line":411,"column":12},"end":{"line":411,"column":58}},"95":{"start":{"line":412,"column":12},"end":{"line":412,"column":64}},"96":{"start":{"line":414,"column":12},"end":{"line":414,"column":50}},"97":{"start":{"line":416,"column":12},"end":{"line":416,"column":73}},"98":{"start":{"line":418,"column":12},"end":{"line":420,"column":13}},"99":{"start":{"line":419,"column":16},"end":{"line":419,"column":25}},"100":{"start":{"line":422,"column":12},"end":{"line":430,"column":13}},"101":{"start":{"line":423,"column":16},"end":{"line":423,"column":64}},"102":{"start":{"line":424,"column":16},"end":{"line":424,"column":68}},"103":{"start":{"line":426,"column":16},"end":{"line":426,"column":84}},"104":{"start":{"line":427,"column":16},"end":{"line":427,"column":69}},"105":{"start":{"line":429,"column":16},"end":{"line":429,"column":77}},"106":{"start":{"line":432,"column":12},"end":{"line":432,"column":49}},"107":{"start":{"line":434,"column":12},"end":{"line":434,"column":31}},"108":{"start":{"line":458,"column":8},"end":{"line":466,"column":27}},"109":{"start":{"line":468,"column":8},"end":{"line":468,"column":51}},"110":{"start":{"line":470,"column":8},"end":{"line":470,"column":58}},"111":{"start":{"line":471,"column":8},"end":{"line":471,"column":55}},"112":{"start":{"line":473,"column":8},"end":{"line":490,"column":9}},"113":{"start":{"line":474,"column":12},"end":{"line":487,"column":13}},"114":{"start":{"line":475,"column":16},"end":{"line":475,"column":44}},"115":{"start":{"line":477,"column":16},"end":{"line":477,"column":75}},"116":{"start":{"line":479,"column":16},"end":{"line":486,"column":17}},"117":{"start":{"line":480,"column":20},"end":{"line":480,"column":52}},"118":{"start":{"line":482,"column":20},"end":{"line":482,"column":76}},"119":{"start":{"line":483,"column":20},"end":{"line":483,"column":66}},"120":{"start":{"line":485,"column":20},"end":{"line":485,"column":88}},"121":{"start":{"line":489,"column":12},"end":{"line":489,"column":49}},"122":{"start":{"line":505,"column":8},"end":{"line":512,"column":18}},"123":{"start":{"line":514,"column":8},"end":{"line":514,"column":55}},"124":{"start":{"line":515,"column":8},"end":{"line":515,"column":57}},"125":{"start":{"line":516,"column":8},"end":{"line":516,"column":61}},"126":{"start":{"line":518,"column":8},"end":{"line":527,"column":9}},"127":{"start":{"line":519,"column":12},"end":{"line":526,"column":13}},"128":{"start":{"line":520,"column":16},"end":{"line":520,"column":56}},"129":{"start":{"line":522,"column":16},"end":{"line":522,"column":73}},"130":{"start":{"line":523,"column":16},"end":{"line":523,"column":69}},"131":{"start":{"line":525,"column":16},"end":{"line":525,"column":77}},"132":{"start":{"line":529,"column":8},"end":{"line":529,"column":25}}},"branchMap":{"1":{"line":77,"type":"if","locations":[{"start":{"line":77,"column":8},"end":{"line":77,"column":8}},{"start":{"line":77,"column":8},"end":{"line":77,"column":8}}]},"2":{"line":94,"type":"if","locations":[{"start":{"line":94,"column":12},"end":{"line":94,"column":12}},{"start":{"line":94,"column":12},"end":{"line":94,"column":12}}]},"3":{"line":134,"type":"if","locations":[{"start":{"line":134,"column":12},"end":{"line":134,"column":12}},{"start":{"line":134,"column":12},"end":{"line":134,"column":12}}]},"4":{"line":167,"type":"cond-expr","locations":[{"start":{"line":167,"column":44},"end":{"line":167,"column":60}},{"start":{"line":167,"column":63},"end":{"line":167,"column":83}}]},"5":{"line":170,"type":"cond-expr","locations":[{"start":{"line":170,"column":66},"end":{"line":170,"column":74}},{"start":{"line":170,"column":77},"end":{"line":170,"column":86}}]},"6":{"line":195,"type":"if","locations":[{"start":{"line":195,"column":8},"end":{"line":195,"column":8}},{"start":{"line":195,"column":8},"end":{"line":195,"column":8}}]},"7":{"line":198,"type":"if","locations":[{"start":{"line":198,"column":12},"end":{"line":198,"column":12}},{"start":{"line":198,"column":12},"end":{"line":198,"column":12}}]},"8":{"line":206,"type":"if","locations":[{"start":{"line":206,"column":15},"end":{"line":206,"column":15}},{"start":{"line":206,"column":15},"end":{"line":206,"column":15}}]},"9":{"line":207,"type":"if","locations":[{"start":{"line":207,"column":12},"end":{"line":207,"column":12}},{"start":{"line":207,"column":12},"end":{"line":207,"column":12}}]},"10":{"line":232,"type":"if","locations":[{"start":{"line":232,"column":8},"end":{"line":232,"column":8}},{"start":{"line":232,"column":8},"end":{"line":232,"column":8}}]},"11":{"line":232,"type":"binary-expr","locations":[{"start":{"line":232,"column":12},"end":{"line":232,"column":22}},{"start":{"line":232,"column":26},"end":{"line":232,"column":35}}]},"12":{"line":269,"type":"if","locations":[{"start":{"line":269,"column":8},"end":{"line":269,"column":8}},{"start":{"line":269,"column":8},"end":{"line":269,"column":8}}]},"13":{"line":276,"type":"if","locations":[{"start":{"line":276,"column":12},"end":{"line":276,"column":12}},{"start":{"line":276,"column":12},"end":{"line":276,"column":12}}]},"14":{"line":276,"type":"binary-expr","locations":[{"start":{"line":276,"column":16},"end":{"line":276,"column":31}},{"start":{"line":276,"column":35},"end":{"line":276,"column":50}}]},"15":{"line":336,"type":"if","locations":[{"start":{"line":336,"column":8},"end":{"line":336,"column":8}},{"start":{"line":336,"column":8},"end":{"line":336,"column":8}}]},"16":{"line":350,"type":"if","locations":[{"start":{"line":350,"column":12},"end":{"line":350,"column":12}},{"start":{"line":350,"column":12},"end":{"line":350,"column":12}}]},"17":{"line":404,"type":"if","locations":[{"start":{"line":404,"column":8},"end":{"line":404,"column":8}},{"start":{"line":404,"column":8},"end":{"line":404,"column":8}}]},"18":{"line":418,"type":"if","locations":[{"start":{"line":418,"column":12},"end":{"line":418,"column":12}},{"start":{"line":418,"column":12},"end":{"line":418,"column":12}}]},"19":{"line":473,"type":"if","locations":[{"start":{"line":473,"column":8},"end":{"line":473,"column":8}},{"start":{"line":473,"column":8},"end":{"line":473,"column":8}}]},"20":{"line":479,"type":"if","locations":[{"start":{"line":479,"column":16},"end":{"line":479,"column":16}},{"start":{"line":479,"column":16},"end":{"line":479,"column":16}}]},"21":{"line":518,"type":"if","locations":[{"start":{"line":518,"column":8},"end":{"line":518,"column":8}},{"start":{"line":518,"column":8},"end":{"line":518,"column":8}}]}},"code":["(function () { YUI.add('moodle-atto_multilang2-button', function (Y, NAME) {","","// This file is part of Moodle - http://moodle.org/","//","// Moodle is free software: you can redistribute it and/or modify","// it under the terms of the GNU General Public License as published by","// the Free Software Foundation, either version 3 of the License, or","// (at your option) any later version.","//","// Moodle is distributed in the hope that it will be useful,","// but WITHOUT ANY WARRANTY; without even the implied warranty of","// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the","// GNU General Public License for more details.","//","// You should have received a copy of the GNU General Public License","// along with Moodle. If not, see .","","/**"," * @package atto_multilang2"," * @copyright 2015 onwards Julen Pardo & Mondragon Unibertsitatea"," * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later"," */","","/**"," * @module moodle-atto_multilang2-button"," */","","var CLASSES = {"," TAG: 'filter-multilang-tag'"," },",""," LANG_WILDCARD = '%lang',"," CONTENT_WILDCARD = '%content',"," ATTR_LANGUAGES = 'languages',"," ATTR_CAPABILITY = 'capability',"," ATTR_HIGHLIGHT = 'highlight',"," ATTR_CSS = 'css',"," DEFAULT_LANGUAGE = '{\"en\":\"English (en)\"}',"," DEFAULT_CAPABILITY = true,"," DEFAULT_HIGHLIGHT = true,"," DEFAULT_CSS = 'outline: 1px dotted;' +"," 'padding: 0.1em;' +"," 'margin: 0em 0.1em;' +"," 'background-color: #ffffaa;',"," TEMPLATES = {"," SPANED: ' {mlang ' + LANG_WILDCARD + '}' +"," CONTENT_WILDCARD +"," '{mlang} ',",""," NOT_SPANED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}'"," },"," OPENING_SPAN = '';","","/**"," * Atto text editor multilanguage plugin."," *"," * @namespace M.atto_multilang2"," * @class button"," * @extends M.editor_atto.EditorPlugin"," */","","Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {",""," /**"," * If the {mlang} tags have to be highlighted or not. Received as parameter from lib.php."," *"," * @property _highlight"," * @type boolean"," * @private"," */"," _highlight: true,",""," initializer: function() {"," var hascapability = this.get(ATTR_CAPABILITY),"," toolbarItems = [];",""," if (hascapability) {"," toolbarItems = this._initializeToolbarItems();"," this._highlight = this.get(ATTR_HIGHLIGHT);",""," this.addToolbarMenu({"," globalItemConfig: {"," callback: this._addTags"," },"," icon: 'icon',"," iconComponent: 'atto_multilang2',"," items: toolbarItems"," });",""," this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this);",""," this._addDelimiterCss();",""," if (this._highlight) {"," this._decorateTagsOnInit();"," this._setSubmitListeners();"," }"," }"," },",""," /**"," * Adds the CSS rules for the delimiters, received as parameter from lib.php."," *"," * @method _addDelimiterCss"," * @private"," */"," _addDelimiterCss: function() {"," var css = '.' + CLASSES.TAG + '{' + this.get(ATTR_CSS) + '}',"," style;",""," style = document.createElement('style');"," style.type = 'text/css';"," style.innerHTML = css;",""," document.head.appendChild(style);"," },",""," /**"," * Initializes the toolbar items, which will be the installed languages,"," * received as parameter."," *"," * @method _initializeToolbarItems"," * @private"," * @return {Array} installed language strings"," */"," _initializeToolbarItems: function() {"," var toolbarItems = [],"," languages,"," langCode;",""," languages = JSON.parse(this.get(ATTR_LANGUAGES));",""," for (langCode in languages) {"," if (languages.hasOwnProperty(langCode)) {"," toolbarItems.push({"," text: languages[langCode],"," callbackArgs: langCode"," });"," }"," }",""," return toolbarItems;"," },",""," /**"," * Retrieves the selected text, wraps it with the multilang tags,"," * and replaces the selected text in the editor with with it."," *"," * If the 'highlight' setting is checked, the {mlang} will be wrapped between"," * the tags with the class for the CSS highlight; if not, they will not"," * be wrapped."," *"," * If there is no content selected, a \" \" will be inserted; otherwhise,"," * it's impossible to place the cursor inside the {mlang} tags."," *"," * @method _addTags"," * @param {EventFacade} event"," * @param {string} langCode the language code"," * @private"," */"," _addTags: function(event, langCode) {"," var selection,"," host = this.get('host'),"," taggedContent,"," content;",""," taggedContent = (this._highlight) ? TEMPLATES.SPANED : TEMPLATES.NOT_SPANED;",""," selection = this._getSelectionHTML();"," content = (host.getSelection().toString().length === 0) ? ' ' : selection;",""," taggedContent = taggedContent.replace(LANG_WILDCARD, langCode);"," taggedContent = taggedContent.replace(CONTENT_WILDCARD, content);",""," host.insertContentAtFocusPoint(taggedContent);",""," this.markUpdated();"," },",""," /**"," * Retrieves selected text with its HTML."," * Took from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234"," *"," * @method _getSelectionHTML"," * @private"," * @return {string} selected text's html; empty if nothing selected"," */"," _getSelectionHTML: function() {"," var html = '',"," selection,"," container,"," index,"," lenght;",""," if (typeof window.getSelection !== 'undefined') {"," selection = window.getSelection();",""," if (selection.rangeCount) {"," container = document.createElement('div');"," for (index = 0, lenght = selection.rangeCount; index < lenght; ++index) {"," container.appendChild(selection.getRangeAt(index).cloneContents());"," }"," html = container.innerHTML;"," }",""," } else if (typeof document.selection !== 'undefined') {"," if (document.selection.type === 'Text') {"," html = document.selection.createRange().htmlText;"," }"," }",""," return html;"," },",""," /**"," * Listens to every change of the text cursor in the text area. If the"," * cursor is placed within a multilang tag, the whole tag is selected."," *"," * @method _checkSelectionChange"," * @private"," */"," _checkSelectionChange: function() {"," var host = this.get('host'),"," node = host.getSelectionParentNode(),"," nodeValue = Y.one(node).get('text'),"," isTextNode,"," isLangTag;",""," isTextNode = Y.one(node).toString().indexOf('#text') > - 1;"," isLangTag = (nodeValue.match(/\\{mlang/g).length === 1);",""," if (isTextNode && isLangTag) {"," host.setSelection(host.getSelectionFromNode(Y.one(node)));"," }"," },",""," /**"," * Retrieves the inputs of type submit, and, for each element, calls the function"," * that sets the submit listener. Is not made in this function because there is"," * not any (apparent) way to access class scope from YUI closure."," *"," * @method _setSubmitListeners"," * @private"," */"," _setSubmitListeners: function() {"," var submitButtons = Y.all('input[type=submit]');",""," submitButtons.each(this._addListenerToSubmitButtons, this);"," },",""," /**"," * Adds the clean tags submit listener of each input[type=\"submit\"], but only if"," * it's not 'cancel' type, and if its parent form is of 'mform' class, because there"," * may be any other submit type (such us administrator's search button)."," *"," * @method _addListenerToSubmitButtons"," * @param {Node} buttonNode"," * @private"," */"," _addListenerToSubmitButtons: function(buttonNode) {"," var buttonObject,"," className,"," parentFormClassName,"," notCancelButton,"," notSearchButton;",""," buttonObject = document.getElementById(buttonNode.get('id'));",""," if (buttonObject !== null) {"," className = buttonObject.className;"," parentFormClassName = buttonObject.form.className;",""," notCancelButton = className.match(/btn-cancel/g) === null;"," notSearchButton = parentFormClassName.match(/mform/g).length > 0;",""," if (notCancelButton && notSearchButton) {"," buttonNode.on('click', this._cleanTagsOnSubmit, this, buttonNode);"," }"," }"," },",""," /**"," * When submit button clicked, this function is invoked. It has to stop the submission,"," * in order to process the textarea to clean the tags."," *"," * Once the textarea is cleaned, detaches this submit listener, i.e., it sets as default,"," * an then simulates the click, to submit the form."," *"," * @method _cleanTagsOnSubmit"," * @param {EventFacade} event"," * @param {Node} submitButton"," * @private"," */"," _cleanTagsOnSubmit: function(event, submitButton) {"," event.preventDefault();",""," this._cleanTagsWithNoYuiId();"," this._cleanTagsWithYuiId();",""," submitButton.detach('click', this._cleanTagsOnSubmit);"," submitButton.simulate('click');"," },",""," /**"," * Cleans the tags around the {mlang} tags when the form is submitted,"," * that do not have \"id\" attribute."," * The cleanup with \"id\" attribute and without it is made separately, to avoid an evil"," * regular expression."," *"," * There may be more than one atto editor textarea in the page. So, we have to retrieve"," * the textareas by the class name. If there is only one, the object will be only the"," * reference, but, if there are more, we will have an array. So, the easiest way is to"," * check if what we have is an array, and if it not, create it manually, and iterate it"," * later."," *"," * issue #15: the textareas are now retrieved passing to YUI selector the whole element,"," * instead of the id string, due to problems with special characters."," * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217"," *"," * @method _cleanTagsWithNoYuiId"," * @private"," */"," _cleanTagsWithNoYuiId: function() {"," var textareas = Y.all('.editor_atto_content'),"," textarea,"," textareaIndex,"," innerHTML,"," spanedmlangtags,"," spanedmlangtag,"," index,"," cleanmlangtag,"," regularExpression;",""," regularExpression = new RegExp(OPENING_SPAN + '.*?' + '', 'g');",""," if (!textareas instanceof Array) {"," textarea = textareas;"," textareas = [];"," textareas[0] = textarea;"," }",""," for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) {"," textarea = textareas._nodes[textareaIndex].id;"," textarea = Y.one(document.getElementById(textarea));",""," innerHTML = textarea.get('innerHTML');",""," spanedmlangtags = innerHTML.match(regularExpression);",""," if (spanedmlangtags === null) {"," continue;"," }"," "," for (index = 0; index < spanedmlangtags.length; index++) {"," spanedmlangtag = spanedmlangtags[index];"," cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, '');",""," cleanmlangtag = cleanmlangtag.replace('', '');",""," innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag);"," }",""," textarea.set('innerHTML', innerHTML);"," }",""," this.markUpdated();"," },",""," /**"," * Cleans the tags around the {mlang} tags when the form is submitted,"," * that have \"id\" attribute, generated by YUI, when the cursor is placed on the tags."," * The cleanup with \"id\" attribute and without it is made separately, to avoid an evil"," * regular expression."," *"," * There may be more than one atto editor textarea in the page. So, we have to retrieve"," * the textareas by the class name. If there is only one, the object will be only the"," * reference, but, if there are more, we will have an array. So, the easiest way is to"," * check if what we have is an array, and if it not, create it manually, and iterate it"," * later."," *"," * issue #15: the textareas are now retrieved passing to YUI selector the whole element,"," * instead of the id string, due to problems with special characters."," * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217"," *"," * @method anTagsWithYuiId"," * @private"," */"," _cleanTagsWithYuiId: function() {"," var textareas = Y.all('.editor_atto_content'),"," textarea,"," textareaIndex,"," innerHTML,"," spanedmlangtag,"," index,"," cleanmlangtag,"," regularExpression,"," openingspanwithyui,"," spanedmlangtagsdwithyui,"," mlangtag;",""," openingspanwithyui = OPENING_SPAN.replace('', 'g');",""," if (!textareas instanceof Array) {"," textarea = textareas;"," textareas = [];"," textareas[0] = textarea;"," }"," "," for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) {"," textarea = textareas._nodes[textareaIndex].id;"," textarea = Y.one(document.getElementById(textarea));",""," innerHTML = textarea.get('innerHTML');",""," spanedmlangtagsdwithyui = innerHTML.match(regularExpression);",""," if (spanedmlangtagsdwithyui === null) {"," continue;"," }"," "," for (index = 0; index < spanedmlangtagsdwithyui.length; index++) {"," spanedmlangtag = spanedmlangtagsdwithyui[index];"," mlangtag = spanedmlangtag.match(/\\{mlang.*?\\}/g)[0];",""," cleanmlangtag = spanedmlangtag.replace(regularExpression, mlangtag);"," cleanmlangtag = cleanmlangtag.replace('', '');",""," innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag);"," }",""," textarea.set('innerHTML', innerHTML);",""," this.markUpdated();"," }"," },",""," /**"," * Adds the tags to the {mlang} tags when the editor is loaded."," * In this case, we DON'T HAVE TO CALL TO markUpdated(). Why? Honestly,"," * I don't know. But, if we call it after setting the HTML, the {mlang}"," * tags flicker with the decoration, and returns to their original state."," *"," * Instead of taking the HTML directly from the textarea, we have to"," * retrieve it, first, without the tags that can be stored"," * in database, due to a bug in version 2015120501 that stores the"," * {mlang} tags in database, with the tags."," * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8"," *"," * Every different {mlang} tag has to be replaced only once, otherwise,"," * nested s will be created in every repeated replacement. So, we"," * have to have a track of which replacements have been made."," *"," * @method _decorateTagsOnInit"," * @private"," */"," _decorateTagsOnInit: function() {"," var textarea = Y.all('.editor_atto_content'),"," innerHTML,"," regularExpression,"," mlangtags,"," mlangtag,"," index,"," decoratedmlangtag,"," replacementsmade = [],"," notreplacedyet;",""," innerHTML = this._getHTMLwithCleanedTags();",""," regularExpression = new RegExp('{mlang.*?}', 'g');"," mlangtags = innerHTML.match(regularExpression);",""," if (mlangtags !== null) {"," for (index = 0; index < mlangtags.length; index++) {"," mlangtag = mlangtags[index];",""," notreplacedyet = replacementsmade.indexOf(mlangtag) === -1;",""," if (notreplacedyet) {"," replacementsmade.push(mlangtag);",""," decoratedmlangtag = OPENING_SPAN + mlangtag + '';"," regularExpression = new RegExp(mlangtag, 'g');",""," innerHTML = innerHTML.replace(regularExpression, decoratedmlangtag);"," }"," }",""," textarea.set('innerHTML', innerHTML);"," }",""," },",""," /**"," * This function returns the HTML as it is in the textarea, but cleaning every"," * tag around the {mlang} tags. This is necessary for decorating tags on"," * init, because it could happen that in database are stored the {mlang} tags with"," * their tags, due to a bug in version 2015120501."," * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8"," *"," * @method _getHTMLwithCleanedTags"," * @return {string} HTML in textarea, without any around {mlang} tags"," */"," _getHTMLwithCleanedTags: function() {"," var host = this.get('host'),"," innerHTML = host.getCleanHTML(),"," regexString,"," regularExpression,"," spanedmlangtags,"," spanedmlangtag,"," cleanmlangtag,"," index;",""," regexString = OPENING_SPAN + '.*?' + '';"," regularExpression = new RegExp(regexString, 'g');"," spanedmlangtags = innerHTML.match(regularExpression);",""," if (spanedmlangtags !== null) {"," for (index = 0; index < spanedmlangtags.length; index++) {"," spanedmlangtag = spanedmlangtags[index];",""," cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, '');"," cleanmlangtag = cleanmlangtag.replace('', '');",""," innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag);"," }"," }",""," return innerHTML;"," }","","}, {"," ATTRS: {"," /**"," * The list of installed languages."," *"," * @attribute languages"," * @type array"," * @default {\"en\":\"English (en)\"}"," */"," languages: DEFAULT_LANGUAGE,",""," /**"," * If the current user has the capability to use the plugin."," *"," * @attribute capability"," * @type boolean"," * @default true"," */"," capability: DEFAULT_CAPABILITY,",""," /**"," * If the {mlang} tags have to be highlighted or not."," *"," * @property highlight"," * @type boolean"," * @default true"," */"," highlight: DEFAULT_HIGHLIGHT,",""," /**"," * The CSS for delimiters."," *"," * @property css"," * @type string"," * @default DEFAULT_CSS"," */"," css: DEFAULT_CSS"," }","});","","","}, '@VERSION@', {\"requires\": [\"moodle-editor_atto-plugin\"]});","","}());"]}; + __coverage__['build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js'] = {"path":"build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js","s":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0,"18":0,"19":0,"20":0,"21":0,"22":0,"23":0,"24":0,"25":0,"26":0,"27":0,"28":0,"29":0,"30":0,"31":0,"32":0,"33":0,"34":0,"35":0,"36":0,"37":0,"38":0,"39":0,"40":0,"41":0,"42":0,"43":0,"44":0,"45":0,"46":0,"47":0,"48":0,"49":0,"50":0,"51":0,"52":0,"53":0,"54":0,"55":0,"56":0,"57":0,"58":0,"59":0,"60":0,"61":0,"62":0,"63":0,"64":0,"65":0,"66":0,"67":0,"68":0,"69":0,"70":0,"71":0,"72":0,"73":0,"74":0,"75":0,"76":0,"77":0,"78":0,"79":0,"80":0,"81":0,"82":0,"83":0,"84":0,"85":0,"86":0,"87":0,"88":0,"89":0,"90":0,"91":0,"92":0,"93":0,"94":0,"95":0,"96":0,"97":0,"98":0,"99":0,"100":0,"101":0,"102":0,"103":0},"b":{"1":[0,0],"2":[0,0],"3":[0,0],"4":[0,0],"5":[0,0],"6":[0,0],"7":[0,0],"8":[0,0],"9":[0,0],"10":[0,0],"11":[0,0],"12":[0,0],"13":[0,0],"14":[0,0],"15":[0,0,0,0,0],"16":[0,0],"17":[0,0],"18":[0,0,0,0,0],"19":[0,0],"20":[0,0],"21":[0,0],"22":[0,0],"23":[0,0]},"f":{"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0,"16":0,"17":0},"fnMap":{"1":{"name":"(anonymous_1)","line":1,"loc":{"start":{"line":1,"column":41},"end":{"line":1,"column":60}}},"2":{"name":"(anonymous_2)","line":74,"loc":{"start":{"line":74,"column":17},"end":{"line":74,"column":28}}},"3":{"name":"(anonymous_3)","line":130,"loc":{"start":{"line":130,"column":29},"end":{"line":130,"column":40}}},"4":{"name":"(anonymous_4)","line":154,"loc":{"start":{"line":154,"column":25},"end":{"line":154,"column":36}}},"5":{"name":"(anonymous_5)","line":176,"loc":{"start":{"line":176,"column":25},"end":{"line":176,"column":36}}},"6":{"name":"(anonymous_6)","line":180,"loc":{"start":{"line":180,"column":31},"end":{"line":180,"column":42}}},"7":{"name":"(anonymous_7)","line":182,"loc":{"start":{"line":182,"column":19},"end":{"line":182,"column":30}}},"8":{"name":"(anonymous_8)","line":202,"loc":{"start":{"line":202,"column":29},"end":{"line":202,"column":40}}},"9":{"name":"(anonymous_9)","line":206,"loc":{"start":{"line":206,"column":35},"end":{"line":206,"column":46}}},"10":{"name":"(anonymous_10)","line":208,"loc":{"start":{"line":208,"column":19},"end":{"line":208,"column":30}}},"11":{"name":"(anonymous_11)","line":234,"loc":{"start":{"line":234,"column":14},"end":{"line":234,"column":40}}},"12":{"name":"(anonymous_12)","line":261,"loc":{"start":{"line":261,"column":23},"end":{"line":261,"column":34}}},"13":{"name":"(anonymous_13)","line":295,"loc":{"start":{"line":295,"column":27},"end":{"line":295,"column":38}}},"14":{"name":"(anonymous_14)","line":323,"loc":{"start":{"line":323,"column":21},"end":{"line":323,"column":32}}},"15":{"name":"(anonymous_15)","line":346,"loc":{"start":{"line":346,"column":25},"end":{"line":346,"column":36}}},"16":{"name":"(anonymous_16)","line":392,"loc":{"start":{"line":392,"column":29},"end":{"line":392,"column":47}}},"17":{"name":"(anonymous_17)","line":404,"loc":{"start":{"line":404,"column":25},"end":{"line":404,"column":40}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":460,"column":61}},"2":{"start":{"line":37,"column":0},"end":{"line":60,"column":6}},"3":{"start":{"line":63,"column":0},"end":{"line":457,"column":3}},"4":{"start":{"line":75,"column":8},"end":{"line":78,"column":17}},"5":{"start":{"line":80,"column":8},"end":{"line":119,"column":9}},"6":{"start":{"line":81,"column":12},"end":{"line":81,"column":58}},"7":{"start":{"line":83,"column":12},"end":{"line":90,"column":15}},"8":{"start":{"line":92,"column":12},"end":{"line":92,"column":54}},"9":{"start":{"line":94,"column":12},"end":{"line":94,"column":55}},"10":{"start":{"line":95,"column":12},"end":{"line":118,"column":13}},"11":{"start":{"line":96,"column":16},"end":{"line":96,"column":54}},"12":{"start":{"line":100,"column":16},"end":{"line":100,"column":40}},"13":{"start":{"line":101,"column":16},"end":{"line":101,"column":54}},"14":{"start":{"line":102,"column":16},"end":{"line":104,"column":17}},"15":{"start":{"line":103,"column":20},"end":{"line":103,"column":66}},"16":{"start":{"line":108,"column":16},"end":{"line":108,"column":95}},"17":{"start":{"line":111,"column":16},"end":{"line":111,"column":85}},"18":{"start":{"line":112,"column":16},"end":{"line":112,"column":85}},"19":{"start":{"line":116,"column":16},"end":{"line":116,"column":43}},"20":{"start":{"line":117,"column":16},"end":{"line":117,"column":47}},"21":{"start":{"line":131,"column":8},"end":{"line":133,"column":21}},"22":{"start":{"line":135,"column":8},"end":{"line":135,"column":57}},"23":{"start":{"line":136,"column":8},"end":{"line":143,"column":9}},"24":{"start":{"line":137,"column":12},"end":{"line":142,"column":13}},"25":{"start":{"line":138,"column":16},"end":{"line":141,"column":19}},"26":{"start":{"line":145,"column":8},"end":{"line":145,"column":28}},"27":{"start":{"line":155,"column":8},"end":{"line":156,"column":18}},"28":{"start":{"line":158,"column":8},"end":{"line":158,"column":48}},"29":{"start":{"line":159,"column":8},"end":{"line":159,"column":32}},"30":{"start":{"line":160,"column":8},"end":{"line":160,"column":30}},"31":{"start":{"line":162,"column":8},"end":{"line":162,"column":41}},"32":{"start":{"line":177,"column":8},"end":{"line":178,"column":35}},"33":{"start":{"line":180,"column":8},"end":{"line":188,"column":13}},"34":{"start":{"line":181,"column":12},"end":{"line":181,"column":54}},"35":{"start":{"line":182,"column":12},"end":{"line":187,"column":14}},"36":{"start":{"line":183,"column":16},"end":{"line":185,"column":17}},"37":{"start":{"line":184,"column":20},"end":{"line":184,"column":126}},"38":{"start":{"line":186,"column":16},"end":{"line":186,"column":62}},"39":{"start":{"line":203,"column":8},"end":{"line":204,"column":35}},"40":{"start":{"line":206,"column":8},"end":{"line":215,"column":13}},"41":{"start":{"line":207,"column":12},"end":{"line":207,"column":62}},"42":{"start":{"line":208,"column":12},"end":{"line":214,"column":14}},"43":{"start":{"line":209,"column":16},"end":{"line":209,"column":69}},"44":{"start":{"line":210,"column":16},"end":{"line":212,"column":17}},"45":{"start":{"line":211,"column":20},"end":{"line":211,"column":58}},"46":{"start":{"line":213,"column":16},"end":{"line":213,"column":27}},"47":{"start":{"line":235,"column":8},"end":{"line":238,"column":20}},"48":{"start":{"line":240,"column":8},"end":{"line":240,"column":42}},"49":{"start":{"line":242,"column":8},"end":{"line":242,"column":45}},"50":{"start":{"line":243,"column":8},"end":{"line":243,"column":87}},"51":{"start":{"line":245,"column":8},"end":{"line":245,"column":71}},"52":{"start":{"line":246,"column":8},"end":{"line":246,"column":73}},"53":{"start":{"line":248,"column":8},"end":{"line":248,"column":54}},"54":{"start":{"line":250,"column":8},"end":{"line":250,"column":27}},"55":{"start":{"line":262,"column":8},"end":{"line":266,"column":19}},"56":{"start":{"line":268,"column":8},"end":{"line":283,"column":9}},"57":{"start":{"line":269,"column":12},"end":{"line":269,"column":46}},"58":{"start":{"line":271,"column":12},"end":{"line":277,"column":13}},"59":{"start":{"line":272,"column":16},"end":{"line":272,"column":58}},"60":{"start":{"line":273,"column":16},"end":{"line":275,"column":17}},"61":{"start":{"line":274,"column":20},"end":{"line":274,"column":87}},"62":{"start":{"line":276,"column":16},"end":{"line":276,"column":43}},"63":{"start":{"line":279,"column":15},"end":{"line":283,"column":9}},"64":{"start":{"line":280,"column":12},"end":{"line":282,"column":13}},"65":{"start":{"line":281,"column":16},"end":{"line":281,"column":65}},"66":{"start":{"line":285,"column":8},"end":{"line":285,"column":20}},"67":{"start":{"line":296,"column":8},"end":{"line":300,"column":22}},"68":{"start":{"line":303,"column":8},"end":{"line":306,"column":9}},"69":{"start":{"line":305,"column":12},"end":{"line":305,"column":19}},"70":{"start":{"line":308,"column":8},"end":{"line":308,"column":50}},"71":{"start":{"line":309,"column":8},"end":{"line":309,"column":105}},"72":{"start":{"line":310,"column":8},"end":{"line":314,"column":9}},"73":{"start":{"line":312,"column":12},"end":{"line":312,"column":63}},"74":{"start":{"line":313,"column":12},"end":{"line":313,"column":41}},"75":{"start":{"line":324,"column":8},"end":{"line":327,"column":9}},"76":{"start":{"line":325,"column":12},"end":{"line":325,"column":85}},"77":{"start":{"line":326,"column":12},"end":{"line":326,"column":31}},"78":{"start":{"line":347,"column":8},"end":{"line":354,"column":27}},"79":{"start":{"line":355,"column":8},"end":{"line":377,"column":9}},"80":{"start":{"line":356,"column":12},"end":{"line":356,"column":77}},"81":{"start":{"line":358,"column":12},"end":{"line":358,"column":62}},"82":{"start":{"line":359,"column":12},"end":{"line":359,"column":60}},"83":{"start":{"line":360,"column":12},"end":{"line":374,"column":13}},"84":{"start":{"line":361,"column":16},"end":{"line":371,"column":17}},"85":{"start":{"line":362,"column":20},"end":{"line":362,"column":48}},"86":{"start":{"line":364,"column":20},"end":{"line":364,"column":79}},"87":{"start":{"line":365,"column":20},"end":{"line":370,"column":21}},"88":{"start":{"line":366,"column":24},"end":{"line":366,"column":56}},"89":{"start":{"line":367,"column":24},"end":{"line":367,"column":85}},"90":{"start":{"line":368,"column":24},"end":{"line":368,"column":70}},"91":{"start":{"line":369,"column":24},"end":{"line":369,"column":96}},"92":{"start":{"line":373,"column":16},"end":{"line":373,"column":48}},"93":{"start":{"line":376,"column":12},"end":{"line":376,"column":31}},"94":{"start":{"line":394,"column":8},"end":{"line":396,"column":21}},"95":{"start":{"line":398,"column":8},"end":{"line":398,"column":35}},"96":{"start":{"line":399,"column":8},"end":{"line":399,"column":52}},"97":{"start":{"line":402,"column":8},"end":{"line":402,"column":56}},"98":{"start":{"line":404,"column":8},"end":{"line":414,"column":11}},"99":{"start":{"line":405,"column":12},"end":{"line":413,"column":13}},"100":{"start":{"line":407,"column":16},"end":{"line":409,"column":17}},"101":{"start":{"line":408,"column":20},"end":{"line":408,"column":72}},"102":{"start":{"line":412,"column":16},"end":{"line":412,"column":50}},"103":{"start":{"line":416,"column":8},"end":{"line":416,"column":32}}},"branchMap":{"1":{"line":80,"type":"if","locations":[{"start":{"line":80,"column":8},"end":{"line":80,"column":8}},{"start":{"line":80,"column":8},"end":{"line":80,"column":8}}]},"2":{"line":95,"type":"if","locations":[{"start":{"line":95,"column":12},"end":{"line":95,"column":12}},{"start":{"line":95,"column":12},"end":{"line":95,"column":12}}]},"3":{"line":102,"type":"if","locations":[{"start":{"line":102,"column":16},"end":{"line":102,"column":16}},{"start":{"line":102,"column":16},"end":{"line":102,"column":16}}]},"4":{"line":137,"type":"if","locations":[{"start":{"line":137,"column":12},"end":{"line":137,"column":12}},{"start":{"line":137,"column":12},"end":{"line":137,"column":12}}]},"5":{"line":183,"type":"if","locations":[{"start":{"line":183,"column":16},"end":{"line":183,"column":16}},{"start":{"line":183,"column":16},"end":{"line":183,"column":16}}]},"6":{"line":183,"type":"binary-expr","locations":[{"start":{"line":183,"column":20},"end":{"line":183,"column":46}},{"start":{"line":183,"column":51},"end":{"line":183,"column":109}}]},"7":{"line":210,"type":"if","locations":[{"start":{"line":210,"column":16},"end":{"line":210,"column":16}},{"start":{"line":210,"column":16},"end":{"line":210,"column":16}}]},"8":{"line":210,"type":"binary-expr","locations":[{"start":{"line":210,"column":20},"end":{"line":210,"column":46}},{"start":{"line":210,"column":51},"end":{"line":210,"column":113}}]},"9":{"line":243,"type":"cond-expr","locations":[{"start":{"line":243,"column":66},"end":{"line":243,"column":74}},{"start":{"line":243,"column":77},"end":{"line":243,"column":86}}]},"10":{"line":268,"type":"if","locations":[{"start":{"line":268,"column":8},"end":{"line":268,"column":8}},{"start":{"line":268,"column":8},"end":{"line":268,"column":8}}]},"11":{"line":271,"type":"if","locations":[{"start":{"line":271,"column":12},"end":{"line":271,"column":12}},{"start":{"line":271,"column":12},"end":{"line":271,"column":12}}]},"12":{"line":279,"type":"if","locations":[{"start":{"line":279,"column":15},"end":{"line":279,"column":15}},{"start":{"line":279,"column":15},"end":{"line":279,"column":15}}]},"13":{"line":280,"type":"if","locations":[{"start":{"line":280,"column":12},"end":{"line":280,"column":12}},{"start":{"line":280,"column":12},"end":{"line":280,"column":12}}]},"14":{"line":303,"type":"if","locations":[{"start":{"line":303,"column":8},"end":{"line":303,"column":8}},{"start":{"line":303,"column":8},"end":{"line":303,"column":8}}]},"15":{"line":303,"type":"binary-expr","locations":[{"start":{"line":303,"column":13},"end":{"line":303,"column":40}},{"start":{"line":303,"column":46},"end":{"line":303,"column":59}},{"start":{"line":303,"column":65},"end":{"line":303,"column":79}},{"start":{"line":304,"column":17},"end":{"line":304,"column":55}},{"start":{"line":304,"column":61},"end":{"line":304,"column":85}}]},"16":{"line":309,"type":"cond-expr","locations":[{"start":{"line":309,"column":62},"end":{"line":309,"column":99}},{"start":{"line":309,"column":102},"end":{"line":309,"column":104}}]},"17":{"line":310,"type":"if","locations":[{"start":{"line":310,"column":8},"end":{"line":310,"column":8}},{"start":{"line":310,"column":8},"end":{"line":310,"column":8}}]},"18":{"line":310,"type":"binary-expr","locations":[{"start":{"line":310,"column":13},"end":{"line":310,"column":50}},{"start":{"line":310,"column":56},"end":{"line":310,"column":79}},{"start":{"line":310,"column":85},"end":{"line":310,"column":103}},{"start":{"line":311,"column":17},"end":{"line":311,"column":42}},{"start":{"line":311,"column":48},"end":{"line":311,"column":87}}]},"19":{"line":324,"type":"if","locations":[{"start":{"line":324,"column":8},"end":{"line":324,"column":8}},{"start":{"line":324,"column":8},"end":{"line":324,"column":8}}]},"20":{"line":355,"type":"if","locations":[{"start":{"line":355,"column":8},"end":{"line":355,"column":8}},{"start":{"line":355,"column":8},"end":{"line":355,"column":8}}]},"21":{"line":360,"type":"if","locations":[{"start":{"line":360,"column":12},"end":{"line":360,"column":12}},{"start":{"line":360,"column":12},"end":{"line":360,"column":12}}]},"22":{"line":365,"type":"if","locations":[{"start":{"line":365,"column":20},"end":{"line":365,"column":20}},{"start":{"line":365,"column":20},"end":{"line":365,"column":20}}]},"23":{"line":405,"type":"if","locations":[{"start":{"line":405,"column":12},"end":{"line":405,"column":12}},{"start":{"line":405,"column":12},"end":{"line":405,"column":12}}]}},"code":["(function () { YUI.add('moodle-atto_multilang2-button', function (Y, NAME) {","","// This file is part of Moodle - http://moodle.org/","//","// Moodle is free software: you can redistribute it and/or modify","// it under the terms of the GNU General Public License as published by","// the Free Software Foundation, either version 3 of the License, or","// (at your option) any later version.","//","// Moodle is distributed in the hope that it will be useful,","// but WITHOUT ANY WARRANTY; without even the implied warranty of","// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the","// GNU General Public License for more details.","//","// You should have received a copy of the GNU General Public License","// along with Moodle. If not, see .","","/**"," * @package atto_multilang2"," * @copyright 2015 onwards Julen Pardo & Mondragon Unibertsitatea"," * @copyright 2017 onwards Iñaki Arenaza & Mondragon Unibertsitatea"," * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later"," */","","/**"," * @module moodle-atto_multilang2-button"," */","","/**"," * Atto text editor multilanguage plugin."," *"," * @namespace M.atto_multilang2"," * @class button"," * @extends M.editor_atto.EditorPlugin."," */","","var CLASSES = {"," TAG: 'filter-multilang-tag'"," },",""," LANG_WILDCARD = '%lang',"," CONTENT_WILDCARD = '%content',"," ATTR_LANGUAGES = 'languages',"," ATTR_CAPABILITY = 'capability',"," ATTR_HIGHLIGHT = 'highlight',"," ATTR_CSS = 'css',"," DEFAULT_LANGUAGE = '{\"en\":\"English (en)\"}',"," DEFAULT_CAPABILITY = true,"," DEFAULT_HIGHLIGHT = true,"," DEFAULT_CSS = 'outline: 1px dotted;' +"," 'padding: 0.1em;' +"," 'margin: 0em 0.1em;' +"," 'background-color: #ffffaa;',"," OPENING_SPAN = '',"," CLOSING_SPAN = '',"," TEMPLATES = {"," SPANNED: ' ' + OPENING_SPAN + '{mlang ' + LANG_WILDCARD + '}' + CLOSING_SPAN +"," CONTENT_WILDCARD + OPENING_SPAN + '{mlang}' + CLOSING_SPAN + ' ',"," NOT_SPANNED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}'"," };","","","Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {",""," /**"," * If the {mlang} tags have to be highlighted or not. Received as parameter from lib.php."," *"," * @property _highlight"," * @type boolean"," * @private"," */"," _highlight: true,",""," initializer: function() {"," var hascapability = this.get(ATTR_CAPABILITY),"," toolbarItems,"," host,"," form;",""," if (hascapability) {"," toolbarItems = this._initializeToolbarItems();",""," this.addToolbarMenu({"," globalItemConfig: {"," callback: this._addTags"," },"," icon: 'icon',"," iconComponent: 'atto_multilang2',"," items: toolbarItems"," });",""," this._tagTemplate = TEMPLATES.NOT_SPANNED;",""," this._highlight = this.get(ATTR_HIGHLIGHT);"," if (this._highlight) {"," this._tagTemplate = TEMPLATES.SPANNED;",""," // Attach a submit listener to the form, so we can remove"," // the highlighting html before sending content to Moodle."," host = this.get('host');"," form = host.textarea.ancestor('form');"," if (form) {"," form.on('submit', this._cleanMlangTags, this);"," }",""," // Listen to every change of the text cursor in the text area, to see if"," // the cursor is placed within a multilang tag."," this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this);",""," // Highlight the multilang tags once everything is loaded."," this.get('host').on('pluginsloaded', this._addHighlightingCss, this);"," this.get('host').on('pluginsloaded', this._highlightMlangTags, this);",""," // Hook into host.updateOriginal() and host.updateFromTextArea()"," // so we can add/remove highlighting when we switch to/from HTML view."," this._hookUpdateOriginal();"," this._hookUpdateFromTextArea();"," }"," }"," },",""," /**"," * Initializes the toolbar items, which will be the installed languages,"," * received as parameter."," *"," * @method _initializeToolbarItems"," * @private"," * @return {Array} installed language strings"," */"," _initializeToolbarItems: function() {"," var toolbarItems = [],"," languages,"," langCode;",""," languages = JSON.parse(this.get(ATTR_LANGUAGES));"," for (langCode in languages) {"," if (languages.hasOwnProperty(langCode)) {"," toolbarItems.push({"," text: languages[langCode],"," callbackArgs: langCode"," });"," }"," }",""," return toolbarItems;"," },",""," /**"," * Adds the CSS rules for the delimiters, received as parameter from lib.php."," *"," * @method _addHighlightingCss"," * @private"," */"," _addHighlightingCss: function() {"," var css = '.' + CLASSES.TAG + ' {' + this.get(ATTR_CSS) + '}',"," style;",""," style = document.createElement('style');"," style.type = 'text/css';"," style.innerHTML = css;",""," document.head.appendChild(style);"," },",""," /**"," * Hook the host.updateOriginal() method to allow us to remove the highlighting html when"," * switching to HTML view. As the HTML view plugin doesn't provide a hook or fire an event"," * to notify about the switch to HTML view, we need to hijack host.updateOriginal and look"," * for the caller. Once we've cleaned up the highlighting, we need to execute the original"," * host.updateOriginal() method."," * Inspired by https://stackoverflow.com/a/16580937"," *"," * @method _hookUpdateOriginal"," * @private"," */"," _hookUpdateOriginal: function() {"," var host = this.get('host'),"," multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _removeTags().",""," host.updateOriginal = (function() {"," var _updateOriginal = host.updateOriginal;"," return function() {"," if (multilangplugin._highlight && (this.updateOriginal.caller === host.plugins.html._showHTML)) {"," multilangplugin.editor.setHTML(multilangplugin._getHTMLwithCleanedTags(multilangplugin.editor.getHTML()));"," }"," return _updateOriginal.apply(this, arguments);"," };"," })();"," },",""," /**"," * Hook the host.updateFromTextAreal() method to allow us to re-add the highlighting"," * html when switching from HTML view. As the HTML view plugin doesn't provide a hook"," * or fire an event to notify about the switch from HTML view, we need to hijack"," * host.updateFromTextArea and look for the caller. Once we've executed the original"," * host.updateFromTextArea() method, we re-added the highlighting."," * Inspired by https://stackoverflow.com/a/16580937"," *"," * @method _hookUpdateFromTextArea"," * @private"," */"," _hookUpdateFromTextArea: function() {"," var host = this.get('host'),"," multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _highlightMlangTags().",""," host.updateFromTextArea = (function() {"," var _updateFromTextArea = host.updateFromTextArea;"," return function() {"," var ret = _updateFromTextArea.apply(this, arguments);"," if (multilangplugin._highlight && (this.updateFromTextArea.caller === host.plugins.html._showHTML)) {"," multilangplugin._highlightMlangTags();"," }"," return ret;"," };"," })();"," },",""," /**"," * Retrieves the selected text, wraps it with the multilang tags,"," * and replaces the selected text in the editor with with it."," *"," * If the 'highlight' setting is checked, the {mlang} will be wrapped between"," * the tags with the class for the CSS highlight; if not, they will not"," * be wrapped."," *"," * If there is no content selected, a \" \" will be inserted; otherwhise,"," * it's impossible to place the cursor inside the {mlang} tags."," *"," * @method _addTags"," * @param {EventFacade} event"," * @param {string} langCode the language code"," * @private"," */"," _addTags: function(event, langCode) {"," var selection,"," host = this.get('host'),"," taggedContent,"," content;",""," taggedContent = this._tagTemplate;",""," selection = this._getSelectionHTML();"," content = (host.getSelection().toString().length === 0) ? ' ' : selection;",""," taggedContent = taggedContent.replace(LANG_WILDCARD, langCode);"," taggedContent = taggedContent.replace(CONTENT_WILDCARD, content);",""," host.insertContentAtFocusPoint(taggedContent);",""," this.markUpdated();"," },",""," /**"," * Retrieves selected text with its HTML."," * Taken from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234"," *"," * @method _getSelectionHTML"," * @private"," * @return {string} selected text's html; empty if nothing selected"," */"," _getSelectionHTML: function() {"," var html = '',"," selection,"," container,"," index,"," length;",""," if (typeof window.getSelection !== 'undefined') {"," selection = window.getSelection();",""," if (selection.rangeCount) {"," container = document.createElement('div');"," for (index = 0, length = selection.rangeCount; index < length; ++index) {"," container.appendChild(selection.getRangeAt(index).cloneContents());"," }"," html = container.innerHTML;"," }",""," } else if (typeof document.selection !== 'undefined') {"," if (document.selection.type === 'Text') {"," html = document.selection.createRange().htmlText;"," }"," }",""," return html;"," },",""," /**"," * Listens to every change of the text cursor in the text area. If the"," * cursor is placed within a highlighted multilang tag, the whole tag is selected."," *"," * @method _checkSelectionChange"," * @private"," */"," _checkSelectionChange: function() {"," var host = this.get('host'),"," node = host.getSelectionParentNode(),"," parentNodeName,"," parentClass,"," selection;",""," // If the event fires without a parent node for the selection, ignore the whole thing."," if ((typeof node === 'undefined') || (node === null) || (node === false) ||"," (typeof node.parentNode === 'undefined') || (node.parentNode === null)) {"," return;"," }",""," parentNodeName = node.parentNode.nodeName;"," parentClass = node.parentNode.hasAttribute('class') ? node.parentNode.getAttribute('class') : '';"," if ((typeof parentNodeName !== 'undefined') && (parentNodeName !== null) && (parentClass !== '') &&"," (parentNodeName === 'SPAN') && (parentClass.indexOf(CLASSES.TAG) !== -1)) {"," selection = host.getSelectionFromNode(Y.one(node));"," host.setSelection(selection);"," }"," },",""," /**"," * When submitting the form, this function is invoked to clean the highlighting html code."," *"," * @method _cleanMlangTags"," * @private"," */"," _cleanMlangTags: function() {"," if (this._highlight) {"," this.editor.setHTML(this._getHTMLwithCleanedTags(this.editor.getHTML()));"," this.markUpdated();"," }"," },",""," /**"," * Adds the tags to the {mlang} tags if highlighting is enable."," *"," * Instead of taking the HTML directly from the textarea, we have to"," * retrieve it, first, without the tags that can be stored"," * in database, due to a bug in version 2015120501 that stores the"," * {mlang} tags in database, with the tags."," * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8"," *"," * Every different {mlang} tag has to be replaced only once, otherwise,"," * nested s will be created in every repeated replacement. So, we"," * need to track which replacements have been made."," *"," * @method _highlightMlangTags"," * @private"," */"," _highlightMlangTags: function() {"," var editorHTML,"," regularExpression,"," mlangtags,"," mlangtag,"," index,"," highlightedmlangtag,"," replacementsmade = [],"," notreplacedyet;"," if (this._highlight) {"," editorHTML = this._getHTMLwithCleanedTags(this.editor.getHTML());",""," regularExpression = new RegExp('{mlang.*?}', 'g');"," mlangtags = editorHTML.match(regularExpression);"," if (mlangtags !== null) {"," for (index = 0; index < mlangtags.length; index++) {"," mlangtag = mlangtags[index];",""," notreplacedyet = replacementsmade.indexOf(mlangtag) === -1;"," if (notreplacedyet) {"," replacementsmade.push(mlangtag);"," highlightedmlangtag = OPENING_SPAN + mlangtag + CLOSING_SPAN;"," regularExpression = new RegExp(mlangtag, 'g');"," editorHTML = editorHTML.replace(regularExpression, highlightedmlangtag);"," }"," }",""," this.editor.setHTML(editorHTML);"," }",""," this.markUpdated();"," }"," },",""," /**"," * This function returns the HTML passed in as parameter, but cleaning every multilang"," * tag around the {mlang} tags. This is necessary for decorating tags on"," * init, because it could happen that in database are stored the {mlang} tags with"," * their tags, due to a bug in version 2015120501."," * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8"," * Implementation based on code from EditorClean._clearSpans()"," *"," * @method _getHTMLwithCleanedTags"," * @param {string} content The to be cleaned."," * @return {string} HTML in editor, without any around {mlang} tags."," */"," _getHTMLwithCleanedTags: function(content) {"," // This is better to run detached from the DOM, so the browser doesn't try to update on each change."," var holder = document.createElement('div'),"," spans,"," spansarr;",""," holder.innerHTML = content;"," spans = holder.getElementsByTagName('span');",""," // Since we will be removing elements from the list, we should copy it to an array, making it static."," spansarr = Array.prototype.slice.call(spans, 0);",""," spansarr.forEach(function(span) {"," if (span.className.indexOf(CLASSES.TAG) !== -1) {"," // Move each child (if they exist) to the parent in place of this span."," while (span.firstChild) {"," span.parentNode.insertBefore(span.firstChild, span);"," }",""," // Remove the now empty span."," span.parentNode.removeChild(span);"," }"," });",""," return holder.innerHTML;"," }","","}, {"," ATTRS: {"," /**"," * The list of installed languages."," *"," * @attribute languages"," * @type array"," * @default {\"en\":\"English (en)\"}"," */"," languages: DEFAULT_LANGUAGE,",""," /**"," * If the current user has the capability to use the plugin."," *"," * @attribute capability"," * @type boolean"," * @default true"," */"," capability: DEFAULT_CAPABILITY,",""," /**"," * If the {mlang} tags have to be highlighted or not."," *"," * @property highlight"," * @type boolean"," * @default true"," */"," highlight: DEFAULT_HIGHLIGHT,",""," /**"," * The CSS for delimiters."," *"," * @property css"," * @type string"," * @default DEFAULT_CSS"," */"," css: DEFAULT_CSS"," }","});","","","}, '@VERSION@', {\"requires\": [\"moodle-editor_atto-plugin\"]});","","}());"]}; } var __cov_xlcucJyua6odXhLRctX6xg = __coverage__['build/moodle-atto_multilang2-button/moodle-atto_multilang2-button.js']; -__cov_xlcucJyua6odXhLRctX6xg.s['1']++;YUI.add('moodle-atto_multilang2-button',function(Y,NAME){__cov_xlcucJyua6odXhLRctX6xg.f['1']++;__cov_xlcucJyua6odXhLRctX6xg.s['2']++;var CLASSES={TAG:'filter-multilang-tag'},LANG_WILDCARD='%lang',CONTENT_WILDCARD='%content',ATTR_LANGUAGES='languages',ATTR_CAPABILITY='capability',ATTR_HIGHLIGHT='highlight',ATTR_CSS='css',DEFAULT_LANGUAGE='{"en":"English (en)"}',DEFAULT_CAPABILITY=true,DEFAULT_HIGHLIGHT=true,DEFAULT_CSS='outline: 1px dotted;'+'padding: 0.1em;'+'margin: 0em 0.1em;'+'background-color: #ffffaa;',TEMPLATES={SPANED:' {mlang '+LANG_WILDCARD+'}'+CONTENT_WILDCARD+'{mlang} ',NOT_SPANED:'{mlang '+LANG_WILDCARD+'}'+CONTENT_WILDCARD+'{mlang}'},OPENING_SPAN='';__cov_xlcucJyua6odXhLRctX6xg.s['3']++;Y.namespace('M.atto_multilang2').Button=Y.Base.create('button',Y.M.editor_atto.EditorPlugin,[],{_highlight:true,initializer:function(){__cov_xlcucJyua6odXhLRctX6xg.f['2']++;__cov_xlcucJyua6odXhLRctX6xg.s['4']++;var hascapability=this.get(ATTR_CAPABILITY),toolbarItems=[];__cov_xlcucJyua6odXhLRctX6xg.s['5']++;if(hascapability){__cov_xlcucJyua6odXhLRctX6xg.b['1'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['6']++;toolbarItems=this._initializeToolbarItems();__cov_xlcucJyua6odXhLRctX6xg.s['7']++;this._highlight=this.get(ATTR_HIGHLIGHT);__cov_xlcucJyua6odXhLRctX6xg.s['8']++;this.addToolbarMenu({globalItemConfig:{callback:this._addTags},icon:'icon',iconComponent:'atto_multilang2',items:toolbarItems});__cov_xlcucJyua6odXhLRctX6xg.s['9']++;this.get('host').on('atto:selectionchanged',this._checkSelectionChange,this);__cov_xlcucJyua6odXhLRctX6xg.s['10']++;this._addDelimiterCss();__cov_xlcucJyua6odXhLRctX6xg.s['11']++;if(this._highlight){__cov_xlcucJyua6odXhLRctX6xg.b['2'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['12']++;this._decorateTagsOnInit();__cov_xlcucJyua6odXhLRctX6xg.s['13']++;this._setSubmitListeners();}else{__cov_xlcucJyua6odXhLRctX6xg.b['2'][1]++;}}else{__cov_xlcucJyua6odXhLRctX6xg.b['1'][1]++;}},_addDelimiterCss:function(){__cov_xlcucJyua6odXhLRctX6xg.f['3']++;__cov_xlcucJyua6odXhLRctX6xg.s['14']++;var css='.'+CLASSES.TAG+'{'+this.get(ATTR_CSS)+'}',style;__cov_xlcucJyua6odXhLRctX6xg.s['15']++;style=document.createElement('style');__cov_xlcucJyua6odXhLRctX6xg.s['16']++;style.type='text/css';__cov_xlcucJyua6odXhLRctX6xg.s['17']++;style.innerHTML=css;__cov_xlcucJyua6odXhLRctX6xg.s['18']++;document.head.appendChild(style);},_initializeToolbarItems:function(){__cov_xlcucJyua6odXhLRctX6xg.f['4']++;__cov_xlcucJyua6odXhLRctX6xg.s['19']++;var toolbarItems=[],languages,langCode;__cov_xlcucJyua6odXhLRctX6xg.s['20']++;languages=JSON.parse(this.get(ATTR_LANGUAGES));__cov_xlcucJyua6odXhLRctX6xg.s['21']++;for(langCode in languages){__cov_xlcucJyua6odXhLRctX6xg.s['22']++;if(languages.hasOwnProperty(langCode)){__cov_xlcucJyua6odXhLRctX6xg.b['3'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['23']++;toolbarItems.push({text:languages[langCode],callbackArgs:langCode});}else{__cov_xlcucJyua6odXhLRctX6xg.b['3'][1]++;}}__cov_xlcucJyua6odXhLRctX6xg.s['24']++;return toolbarItems;},_addTags:function(event,langCode){__cov_xlcucJyua6odXhLRctX6xg.f['5']++;__cov_xlcucJyua6odXhLRctX6xg.s['25']++;var selection,host=this.get('host'),taggedContent,content;__cov_xlcucJyua6odXhLRctX6xg.s['26']++;taggedContent=this._highlight?(__cov_xlcucJyua6odXhLRctX6xg.b['4'][0]++,TEMPLATES.SPANED):(__cov_xlcucJyua6odXhLRctX6xg.b['4'][1]++,TEMPLATES.NOT_SPANED);__cov_xlcucJyua6odXhLRctX6xg.s['27']++;selection=this._getSelectionHTML();__cov_xlcucJyua6odXhLRctX6xg.s['28']++;content=host.getSelection().toString().length===0?(__cov_xlcucJyua6odXhLRctX6xg.b['5'][0]++,' '):(__cov_xlcucJyua6odXhLRctX6xg.b['5'][1]++,selection);__cov_xlcucJyua6odXhLRctX6xg.s['29']++;taggedContent=taggedContent.replace(LANG_WILDCARD,langCode);__cov_xlcucJyua6odXhLRctX6xg.s['30']++;taggedContent=taggedContent.replace(CONTENT_WILDCARD,content);__cov_xlcucJyua6odXhLRctX6xg.s['31']++;host.insertContentAtFocusPoint(taggedContent);__cov_xlcucJyua6odXhLRctX6xg.s['32']++;this.markUpdated();},_getSelectionHTML:function(){__cov_xlcucJyua6odXhLRctX6xg.f['6']++;__cov_xlcucJyua6odXhLRctX6xg.s['33']++;var html='',selection,container,index,lenght;__cov_xlcucJyua6odXhLRctX6xg.s['34']++;if(typeof window.getSelection!=='undefined'){__cov_xlcucJyua6odXhLRctX6xg.b['6'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['35']++;selection=window.getSelection();__cov_xlcucJyua6odXhLRctX6xg.s['36']++;if(selection.rangeCount){__cov_xlcucJyua6odXhLRctX6xg.b['7'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['37']++;container=document.createElement('div');__cov_xlcucJyua6odXhLRctX6xg.s['38']++;for(index=0,lenght=selection.rangeCount;index-1;__cov_xlcucJyua6odXhLRctX6xg.s['47']++;isLangTag=nodeValue.match(/\{mlang/g).length===1;__cov_xlcucJyua6odXhLRctX6xg.s['48']++;if((__cov_xlcucJyua6odXhLRctX6xg.b['11'][0]++,isTextNode)&&(__cov_xlcucJyua6odXhLRctX6xg.b['11'][1]++,isLangTag)){__cov_xlcucJyua6odXhLRctX6xg.b['10'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['49']++;host.setSelection(host.getSelectionFromNode(Y.one(node)));}else{__cov_xlcucJyua6odXhLRctX6xg.b['10'][1]++;}},_setSubmitListeners:function(){__cov_xlcucJyua6odXhLRctX6xg.f['8']++;__cov_xlcucJyua6odXhLRctX6xg.s['50']++;var submitButtons=Y.all('input[type=submit]');__cov_xlcucJyua6odXhLRctX6xg.s['51']++;submitButtons.each(this._addListenerToSubmitButtons,this);},_addListenerToSubmitButtons:function(buttonNode){__cov_xlcucJyua6odXhLRctX6xg.f['9']++;__cov_xlcucJyua6odXhLRctX6xg.s['52']++;var buttonObject,className,parentFormClassName,notCancelButton,notSearchButton;__cov_xlcucJyua6odXhLRctX6xg.s['53']++;buttonObject=document.getElementById(buttonNode.get('id'));__cov_xlcucJyua6odXhLRctX6xg.s['54']++;if(buttonObject!==null){__cov_xlcucJyua6odXhLRctX6xg.b['12'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['55']++;className=buttonObject.className;__cov_xlcucJyua6odXhLRctX6xg.s['56']++;parentFormClassName=buttonObject.form.className;__cov_xlcucJyua6odXhLRctX6xg.s['57']++;notCancelButton=className.match(/btn-cancel/g)===null;__cov_xlcucJyua6odXhLRctX6xg.s['58']++;notSearchButton=parentFormClassName.match(/mform/g).length>0;__cov_xlcucJyua6odXhLRctX6xg.s['59']++;if((__cov_xlcucJyua6odXhLRctX6xg.b['14'][0]++,notCancelButton)&&(__cov_xlcucJyua6odXhLRctX6xg.b['14'][1]++,notSearchButton)){__cov_xlcucJyua6odXhLRctX6xg.b['13'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['60']++;buttonNode.on('click',this._cleanTagsOnSubmit,this,buttonNode);}else{__cov_xlcucJyua6odXhLRctX6xg.b['13'][1]++;}}else{__cov_xlcucJyua6odXhLRctX6xg.b['12'][1]++;}},_cleanTagsOnSubmit:function(event,submitButton){__cov_xlcucJyua6odXhLRctX6xg.f['10']++;__cov_xlcucJyua6odXhLRctX6xg.s['61']++;event.preventDefault();__cov_xlcucJyua6odXhLRctX6xg.s['62']++;this._cleanTagsWithNoYuiId();__cov_xlcucJyua6odXhLRctX6xg.s['63']++;this._cleanTagsWithYuiId();__cov_xlcucJyua6odXhLRctX6xg.s['64']++;submitButton.detach('click',this._cleanTagsOnSubmit);__cov_xlcucJyua6odXhLRctX6xg.s['65']++;submitButton.simulate('click');},_cleanTagsWithNoYuiId:function(){__cov_xlcucJyua6odXhLRctX6xg.f['11']++;__cov_xlcucJyua6odXhLRctX6xg.s['66']++;var textareas=Y.all('.editor_atto_content'),textarea,textareaIndex,innerHTML,spanedmlangtags,spanedmlangtag,index,cleanmlangtag,regularExpression;__cov_xlcucJyua6odXhLRctX6xg.s['67']++;regularExpression=new RegExp(OPENING_SPAN+'.*?'+'','g');__cov_xlcucJyua6odXhLRctX6xg.s['68']++;if(!textareas instanceof Array){__cov_xlcucJyua6odXhLRctX6xg.b['15'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['69']++;textarea=textareas;__cov_xlcucJyua6odXhLRctX6xg.s['70']++;textareas=[];__cov_xlcucJyua6odXhLRctX6xg.s['71']++;textareas[0]=textarea;}else{__cov_xlcucJyua6odXhLRctX6xg.b['15'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['72']++;for(textareaIndex=0;textareaIndex','');__cov_xlcucJyua6odXhLRctX6xg.s['83']++;innerHTML=innerHTML.replace(spanedmlangtag,cleanmlangtag);}__cov_xlcucJyua6odXhLRctX6xg.s['84']++;textarea.set('innerHTML',innerHTML);}__cov_xlcucJyua6odXhLRctX6xg.s['85']++;this.markUpdated();},_cleanTagsWithYuiId:function(){__cov_xlcucJyua6odXhLRctX6xg.f['12']++;__cov_xlcucJyua6odXhLRctX6xg.s['86']++;var textareas=Y.all('.editor_atto_content'),textarea,textareaIndex,innerHTML,spanedmlangtag,index,cleanmlangtag,regularExpression,openingspanwithyui,spanedmlangtagsdwithyui,mlangtag;__cov_xlcucJyua6odXhLRctX6xg.s['87']++;openingspanwithyui=OPENING_SPAN.replace('','g');__cov_xlcucJyua6odXhLRctX6xg.s['89']++;if(!textareas instanceof Array){__cov_xlcucJyua6odXhLRctX6xg.b['17'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['90']++;textarea=textareas;__cov_xlcucJyua6odXhLRctX6xg.s['91']++;textareas=[];__cov_xlcucJyua6odXhLRctX6xg.s['92']++;textareas[0]=textarea;}else{__cov_xlcucJyua6odXhLRctX6xg.b['17'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['93']++;for(textareaIndex=0;textareaIndex','');__cov_xlcucJyua6odXhLRctX6xg.s['105']++;innerHTML=innerHTML.replace(spanedmlangtag,cleanmlangtag);}__cov_xlcucJyua6odXhLRctX6xg.s['106']++;textarea.set('innerHTML',innerHTML);__cov_xlcucJyua6odXhLRctX6xg.s['107']++;this.markUpdated();}},_decorateTagsOnInit:function(){__cov_xlcucJyua6odXhLRctX6xg.f['13']++;__cov_xlcucJyua6odXhLRctX6xg.s['108']++;var textarea=Y.all('.editor_atto_content'),innerHTML,regularExpression,mlangtags,mlangtag,index,decoratedmlangtag,replacementsmade=[],notreplacedyet;__cov_xlcucJyua6odXhLRctX6xg.s['109']++;innerHTML=this._getHTMLwithCleanedTags();__cov_xlcucJyua6odXhLRctX6xg.s['110']++;regularExpression=new RegExp('{mlang.*?}','g');__cov_xlcucJyua6odXhLRctX6xg.s['111']++;mlangtags=innerHTML.match(regularExpression);__cov_xlcucJyua6odXhLRctX6xg.s['112']++;if(mlangtags!==null){__cov_xlcucJyua6odXhLRctX6xg.b['19'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['113']++;for(index=0;index';__cov_xlcucJyua6odXhLRctX6xg.s['119']++;regularExpression=new RegExp(mlangtag,'g');__cov_xlcucJyua6odXhLRctX6xg.s['120']++;innerHTML=innerHTML.replace(regularExpression,decoratedmlangtag);}else{__cov_xlcucJyua6odXhLRctX6xg.b['20'][1]++;}}__cov_xlcucJyua6odXhLRctX6xg.s['121']++;textarea.set('innerHTML',innerHTML);}else{__cov_xlcucJyua6odXhLRctX6xg.b['19'][1]++;}},_getHTMLwithCleanedTags:function(){__cov_xlcucJyua6odXhLRctX6xg.f['14']++;__cov_xlcucJyua6odXhLRctX6xg.s['122']++;var host=this.get('host'),innerHTML=host.getCleanHTML(),regexString,regularExpression,spanedmlangtags,spanedmlangtag,cleanmlangtag,index;__cov_xlcucJyua6odXhLRctX6xg.s['123']++;regexString=OPENING_SPAN+'.*?'+'';__cov_xlcucJyua6odXhLRctX6xg.s['124']++;regularExpression=new RegExp(regexString,'g');__cov_xlcucJyua6odXhLRctX6xg.s['125']++;spanedmlangtags=innerHTML.match(regularExpression);__cov_xlcucJyua6odXhLRctX6xg.s['126']++;if(spanedmlangtags!==null){__cov_xlcucJyua6odXhLRctX6xg.b['21'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['127']++;for(index=0;index','');__cov_xlcucJyua6odXhLRctX6xg.s['131']++;innerHTML=innerHTML.replace(spanedmlangtag,cleanmlangtag);}}else{__cov_xlcucJyua6odXhLRctX6xg.b['21'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['132']++;return innerHTML;}},{ATTRS:{languages:DEFAULT_LANGUAGE,capability:DEFAULT_CAPABILITY,highlight:DEFAULT_HIGHLIGHT,css:DEFAULT_CSS}});},'@VERSION@',{'requires':['moodle-editor_atto-plugin']}); +__cov_xlcucJyua6odXhLRctX6xg.s['1']++;YUI.add('moodle-atto_multilang2-button',function(Y,NAME){__cov_xlcucJyua6odXhLRctX6xg.f['1']++;__cov_xlcucJyua6odXhLRctX6xg.s['2']++;var CLASSES={TAG:'filter-multilang-tag'},LANG_WILDCARD='%lang',CONTENT_WILDCARD='%content',ATTR_LANGUAGES='languages',ATTR_CAPABILITY='capability',ATTR_HIGHLIGHT='highlight',ATTR_CSS='css',DEFAULT_LANGUAGE='{"en":"English (en)"}',DEFAULT_CAPABILITY=true,DEFAULT_HIGHLIGHT=true,DEFAULT_CSS='outline: 1px dotted;'+'padding: 0.1em;'+'margin: 0em 0.1em;'+'background-color: #ffffaa;',OPENING_SPAN='',CLOSING_SPAN='',TEMPLATES={SPANNED:' '+OPENING_SPAN+'{mlang '+LANG_WILDCARD+'}'+CLOSING_SPAN+CONTENT_WILDCARD+OPENING_SPAN+'{mlang}'+CLOSING_SPAN+' ',NOT_SPANNED:'{mlang '+LANG_WILDCARD+'}'+CONTENT_WILDCARD+'{mlang}'};__cov_xlcucJyua6odXhLRctX6xg.s['3']++;Y.namespace('M.atto_multilang2').Button=Y.Base.create('button',Y.M.editor_atto.EditorPlugin,[],{_highlight:true,initializer:function(){__cov_xlcucJyua6odXhLRctX6xg.f['2']++;__cov_xlcucJyua6odXhLRctX6xg.s['4']++;var hascapability=this.get(ATTR_CAPABILITY),toolbarItems,host,form;__cov_xlcucJyua6odXhLRctX6xg.s['5']++;if(hascapability){__cov_xlcucJyua6odXhLRctX6xg.b['1'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['6']++;toolbarItems=this._initializeToolbarItems();__cov_xlcucJyua6odXhLRctX6xg.s['7']++;this.addToolbarMenu({globalItemConfig:{callback:this._addTags},icon:'icon',iconComponent:'atto_multilang2',items:toolbarItems});__cov_xlcucJyua6odXhLRctX6xg.s['8']++;this._tagTemplate=TEMPLATES.NOT_SPANNED;__cov_xlcucJyua6odXhLRctX6xg.s['9']++;this._highlight=this.get(ATTR_HIGHLIGHT);__cov_xlcucJyua6odXhLRctX6xg.s['10']++;if(this._highlight){__cov_xlcucJyua6odXhLRctX6xg.b['2'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['11']++;this._tagTemplate=TEMPLATES.SPANNED;__cov_xlcucJyua6odXhLRctX6xg.s['12']++;host=this.get('host');__cov_xlcucJyua6odXhLRctX6xg.s['13']++;form=host.textarea.ancestor('form');__cov_xlcucJyua6odXhLRctX6xg.s['14']++;if(form){__cov_xlcucJyua6odXhLRctX6xg.b['3'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['15']++;form.on('submit',this._cleanMlangTags,this);}else{__cov_xlcucJyua6odXhLRctX6xg.b['3'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['16']++;this.get('host').on('atto:selectionchanged',this._checkSelectionChange,this);__cov_xlcucJyua6odXhLRctX6xg.s['17']++;this.get('host').on('pluginsloaded',this._addHighlightingCss,this);__cov_xlcucJyua6odXhLRctX6xg.s['18']++;this.get('host').on('pluginsloaded',this._highlightMlangTags,this);__cov_xlcucJyua6odXhLRctX6xg.s['19']++;this._hookUpdateOriginal();__cov_xlcucJyua6odXhLRctX6xg.s['20']++;this._hookUpdateFromTextArea();}else{__cov_xlcucJyua6odXhLRctX6xg.b['2'][1]++;}}else{__cov_xlcucJyua6odXhLRctX6xg.b['1'][1]++;}},_initializeToolbarItems:function(){__cov_xlcucJyua6odXhLRctX6xg.f['3']++;__cov_xlcucJyua6odXhLRctX6xg.s['21']++;var toolbarItems=[],languages,langCode;__cov_xlcucJyua6odXhLRctX6xg.s['22']++;languages=JSON.parse(this.get(ATTR_LANGUAGES));__cov_xlcucJyua6odXhLRctX6xg.s['23']++;for(langCode in languages){__cov_xlcucJyua6odXhLRctX6xg.s['24']++;if(languages.hasOwnProperty(langCode)){__cov_xlcucJyua6odXhLRctX6xg.b['4'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['25']++;toolbarItems.push({text:languages[langCode],callbackArgs:langCode});}else{__cov_xlcucJyua6odXhLRctX6xg.b['4'][1]++;}}__cov_xlcucJyua6odXhLRctX6xg.s['26']++;return toolbarItems;},_addHighlightingCss:function(){__cov_xlcucJyua6odXhLRctX6xg.f['4']++;__cov_xlcucJyua6odXhLRctX6xg.s['27']++;var css='.'+CLASSES.TAG+' {'+this.get(ATTR_CSS)+'}',style;__cov_xlcucJyua6odXhLRctX6xg.s['28']++;style=document.createElement('style');__cov_xlcucJyua6odXhLRctX6xg.s['29']++;style.type='text/css';__cov_xlcucJyua6odXhLRctX6xg.s['30']++;style.innerHTML=css;__cov_xlcucJyua6odXhLRctX6xg.s['31']++;document.head.appendChild(style);},_hookUpdateOriginal:function(){__cov_xlcucJyua6odXhLRctX6xg.f['5']++;__cov_xlcucJyua6odXhLRctX6xg.s['32']++;var host=this.get('host'),multilangplugin=this;__cov_xlcucJyua6odXhLRctX6xg.s['33']++;host.updateOriginal=function(){__cov_xlcucJyua6odXhLRctX6xg.f['6']++;__cov_xlcucJyua6odXhLRctX6xg.s['34']++;var _updateOriginal=host.updateOriginal;__cov_xlcucJyua6odXhLRctX6xg.s['35']++;return function(){__cov_xlcucJyua6odXhLRctX6xg.f['7']++;__cov_xlcucJyua6odXhLRctX6xg.s['36']++;if((__cov_xlcucJyua6odXhLRctX6xg.b['6'][0]++,multilangplugin._highlight)&&(__cov_xlcucJyua6odXhLRctX6xg.b['6'][1]++,this.updateOriginal.caller===host.plugins.html._showHTML)){__cov_xlcucJyua6odXhLRctX6xg.b['5'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['37']++;multilangplugin.editor.setHTML(multilangplugin._getHTMLwithCleanedTags(multilangplugin.editor.getHTML()));}else{__cov_xlcucJyua6odXhLRctX6xg.b['5'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['38']++;return _updateOriginal.apply(this,arguments);};}();},_hookUpdateFromTextArea:function(){__cov_xlcucJyua6odXhLRctX6xg.f['8']++;__cov_xlcucJyua6odXhLRctX6xg.s['39']++;var host=this.get('host'),multilangplugin=this;__cov_xlcucJyua6odXhLRctX6xg.s['40']++;host.updateFromTextArea=function(){__cov_xlcucJyua6odXhLRctX6xg.f['9']++;__cov_xlcucJyua6odXhLRctX6xg.s['41']++;var _updateFromTextArea=host.updateFromTextArea;__cov_xlcucJyua6odXhLRctX6xg.s['42']++;return function(){__cov_xlcucJyua6odXhLRctX6xg.f['10']++;__cov_xlcucJyua6odXhLRctX6xg.s['43']++;var ret=_updateFromTextArea.apply(this,arguments);__cov_xlcucJyua6odXhLRctX6xg.s['44']++;if((__cov_xlcucJyua6odXhLRctX6xg.b['8'][0]++,multilangplugin._highlight)&&(__cov_xlcucJyua6odXhLRctX6xg.b['8'][1]++,this.updateFromTextArea.caller===host.plugins.html._showHTML)){__cov_xlcucJyua6odXhLRctX6xg.b['7'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['45']++;multilangplugin._highlightMlangTags();}else{__cov_xlcucJyua6odXhLRctX6xg.b['7'][1]++;}__cov_xlcucJyua6odXhLRctX6xg.s['46']++;return ret;};}();},_addTags:function(event,langCode){__cov_xlcucJyua6odXhLRctX6xg.f['11']++;__cov_xlcucJyua6odXhLRctX6xg.s['47']++;var selection,host=this.get('host'),taggedContent,content;__cov_xlcucJyua6odXhLRctX6xg.s['48']++;taggedContent=this._tagTemplate;__cov_xlcucJyua6odXhLRctX6xg.s['49']++;selection=this._getSelectionHTML();__cov_xlcucJyua6odXhLRctX6xg.s['50']++;content=host.getSelection().toString().length===0?(__cov_xlcucJyua6odXhLRctX6xg.b['9'][0]++,' '):(__cov_xlcucJyua6odXhLRctX6xg.b['9'][1]++,selection);__cov_xlcucJyua6odXhLRctX6xg.s['51']++;taggedContent=taggedContent.replace(LANG_WILDCARD,langCode);__cov_xlcucJyua6odXhLRctX6xg.s['52']++;taggedContent=taggedContent.replace(CONTENT_WILDCARD,content);__cov_xlcucJyua6odXhLRctX6xg.s['53']++;host.insertContentAtFocusPoint(taggedContent);__cov_xlcucJyua6odXhLRctX6xg.s['54']++;this.markUpdated();},_getSelectionHTML:function(){__cov_xlcucJyua6odXhLRctX6xg.f['12']++;__cov_xlcucJyua6odXhLRctX6xg.s['55']++;var html='',selection,container,index,length;__cov_xlcucJyua6odXhLRctX6xg.s['56']++;if(typeof window.getSelection!=='undefined'){__cov_xlcucJyua6odXhLRctX6xg.b['10'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['57']++;selection=window.getSelection();__cov_xlcucJyua6odXhLRctX6xg.s['58']++;if(selection.rangeCount){__cov_xlcucJyua6odXhLRctX6xg.b['11'][0]++;__cov_xlcucJyua6odXhLRctX6xg.s['59']++;container=document.createElement('div');__cov_xlcucJyua6odXhLRctX6xg.s['60']++;for(index=0,length=selection.rangeCount;index', + CLOSING_SPAN = '', TEMPLATES = { - SPANED: ' {mlang ' + LANG_WILDCARD + '}' + - CONTENT_WILDCARD + - '{mlang} ', + SPANNED: ' ' + OPENING_SPAN + '{mlang ' + LANG_WILDCARD + '}' + CLOSING_SPAN + + CONTENT_WILDCARD + OPENING_SPAN + '{mlang}' + CLOSING_SPAN + ' ', + NOT_SPANNED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' + }; - NOT_SPANED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' - }, - OPENING_SPAN = ''; - -/** - * Atto text editor multilanguage plugin. - * - * @namespace M.atto_multilang2 - * @class button - * @extends M.editor_atto.EditorPlugin - */ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { @@ -72,11 +73,12 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att initializer: function() { var hascapability = this.get(ATTR_CAPABILITY), - toolbarItems = []; + toolbarItems, + host, + form; if (hascapability) { toolbarItems = this._initializeToolbarItems(); - this._highlight = this.get(ATTR_HIGHLIGHT); this.addToolbarMenu({ globalItemConfig: { @@ -87,32 +89,34 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att items: toolbarItems }); - this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - - this._addDelimiterCss(); + this._tagTemplate = TEMPLATES.NOT_SPANNED; + this._highlight = this.get(ATTR_HIGHLIGHT); if (this._highlight) { - this._decorateTagsOnInit(); - this._setSubmitListeners(); - } - } - }, + this._tagTemplate = TEMPLATES.SPANNED; + + // Attach a submit listener to the form, so we can remove + // the highlighting html before sending content to Moodle. + host = this.get('host'); + form = host.textarea.ancestor('form'); + if (form) { + form.on('submit', this._cleanMlangTags, this); + } - /** - * Adds the CSS rules for the delimiters, received as parameter from lib.php. - * - * @method _addDelimiterCss - * @private - */ - _addDelimiterCss: function() { - var css = '.' + CLASSES.TAG + '{' + this.get(ATTR_CSS) + '}', - style; + // Listen to every change of the text cursor in the text area, to see if + // the cursor is placed within a multilang tag. + this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = css; + // Highlight the multilang tags once everything is loaded. + this.get('host').on('pluginsloaded', this._addHighlightingCss, this); + this.get('host').on('pluginsloaded', this._highlightMlangTags, this); - document.head.appendChild(style); + // Hook into host.updateOriginal() and host.updateFromTextArea() + // so we can add/remove highlighting when we switch to/from HTML view. + this._hookUpdateOriginal(); + this._hookUpdateFromTextArea(); + } + } }, /** @@ -129,7 +133,6 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att langCode; languages = JSON.parse(this.get(ATTR_LANGUAGES)); - for (langCode in languages) { if (languages.hasOwnProperty(langCode)) { toolbarItems.push({ @@ -142,6 +145,76 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att return toolbarItems; }, + /** + * Adds the CSS rules for the delimiters, received as parameter from lib.php. + * + * @method _addHighlightingCss + * @private + */ + _addHighlightingCss: function() { + var css = '.' + CLASSES.TAG + ' {' + this.get(ATTR_CSS) + '}', + style; + + style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = css; + + document.head.appendChild(style); + }, + + /** + * Hook the host.updateOriginal() method to allow us to remove the highlighting html when + * switching to HTML view. As the HTML view plugin doesn't provide a hook or fire an event + * to notify about the switch to HTML view, we need to hijack host.updateOriginal and look + * for the caller. Once we've cleaned up the highlighting, we need to execute the original + * host.updateOriginal() method. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateOriginal + * @private + */ + _hookUpdateOriginal: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _removeTags(). + + host.updateOriginal = (function() { + var _updateOriginal = host.updateOriginal; + return function() { + if (multilangplugin._highlight && (this.updateOriginal.caller === host.plugins.html._showHTML)) { + multilangplugin.editor.setHTML(multilangplugin._getHTMLwithCleanedTags(multilangplugin.editor.getHTML())); + } + return _updateOriginal.apply(this, arguments); + }; + })(); + }, + + /** + * Hook the host.updateFromTextAreal() method to allow us to re-add the highlighting + * html when switching from HTML view. As the HTML view plugin doesn't provide a hook + * or fire an event to notify about the switch from HTML view, we need to hijack + * host.updateFromTextArea and look for the caller. Once we've executed the original + * host.updateFromTextArea() method, we re-added the highlighting. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateFromTextArea + * @private + */ + _hookUpdateFromTextArea: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _highlightMlangTags(). + + host.updateFromTextArea = (function() { + var _updateFromTextArea = host.updateFromTextArea; + return function() { + var ret = _updateFromTextArea.apply(this, arguments); + if (multilangplugin._highlight && (this.updateFromTextArea.caller === host.plugins.html._showHTML)) { + multilangplugin._highlightMlangTags(); + } + return ret; + }; + })(); + }, + /** * Retrieves the selected text, wraps it with the multilang tags, * and replaces the selected text in the editor with with it. @@ -164,7 +237,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att taggedContent, content; - taggedContent = (this._highlight) ? TEMPLATES.SPANED : TEMPLATES.NOT_SPANED; + taggedContent = this._tagTemplate; selection = this._getSelectionHTML(); content = (host.getSelection().toString().length === 0) ? ' ' : selection; @@ -179,7 +252,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Retrieves selected text with its HTML. - * Took from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 + * Taken from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 * * @method _getSelectionHTML * @private @@ -190,14 +263,14 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att selection, container, index, - lenght; + length; if (typeof window.getSelection !== 'undefined') { selection = window.getSelection(); if (selection.rangeCount) { container = document.createElement('div'); - for (index = 0, lenght = selection.rangeCount; index < lenght; ++index) { + for (index = 0, length = selection.rangeCount; index < length; ++index) { container.appendChild(selection.getRangeAt(index).cloneContents()); } html = container.innerHTML; @@ -214,7 +287,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Listens to every change of the text cursor in the text area. If the - * cursor is placed within a multilang tag, the whole tag is selected. + * cursor is placed within a highlighted multilang tag, the whole tag is selected. * * @method _checkSelectionChange * @private @@ -222,224 +295,40 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att _checkSelectionChange: function() { var host = this.get('host'), node = host.getSelectionParentNode(), - nodeValue = Y.one(node).get('text'), - isTextNode, - isLangTag; - - isTextNode = Y.one(node).toString().indexOf('#text') > - 1; - isLangTag = (nodeValue.match(/\{mlang/g).length === 1); - - if (isTextNode && isLangTag) { - host.setSelection(host.getSelectionFromNode(Y.one(node))); + parentNodeName, + parentClass, + selection; + + // If the event fires without a parent node for the selection, ignore the whole thing. + if ((typeof node === 'undefined') || (node === null) || (node === false) || + (typeof node.parentNode === 'undefined') || (node.parentNode === null)) { + return; } - }, - - /** - * Retrieves the inputs of type submit, and, for each element, calls the function - * that sets the submit listener. Is not made in this function because there is - * not any (apparent) way to access class scope from YUI closure. - * - * @method _setSubmitListeners - * @private - */ - _setSubmitListeners: function() { - var submitButtons = Y.all('input[type=submit]'); - - submitButtons.each(this._addListenerToSubmitButtons, this); - }, - /** - * Adds the clean tags submit listener of each input[type="submit"], but only if - * it's not 'cancel' type, and if its parent form is of 'mform' class, because there - * may be any other submit type (such us administrator's search button). - * - * @method _addListenerToSubmitButtons - * @param {Node} buttonNode - * @private - */ - _addListenerToSubmitButtons: function(buttonNode) { - var buttonObject, - className, - parentFormClassName, - notCancelButton, - notSearchButton; - - buttonObject = document.getElementById(buttonNode.get('id')); - - if (buttonObject !== null) { - className = buttonObject.className; - parentFormClassName = buttonObject.form.className; - - notCancelButton = className.match(/btn-cancel/g) === null; - notSearchButton = parentFormClassName.match(/mform/g).length > 0; - - if (notCancelButton && notSearchButton) { - buttonNode.on('click', this._cleanTagsOnSubmit, this, buttonNode); - } + parentNodeName = node.parentNode.nodeName; + parentClass = node.parentNode.hasAttribute('class') ? node.parentNode.getAttribute('class') : ''; + if ((typeof parentNodeName !== 'undefined') && (parentNodeName !== null) && (parentClass !== '') && + (parentNodeName === 'SPAN') && (parentClass.indexOf(CLASSES.TAG) !== -1)) { + selection = host.getSelectionFromNode(Y.one(node)); + host.setSelection(selection); } }, /** - * When submit button clicked, this function is invoked. It has to stop the submission, - * in order to process the textarea to clean the tags. - * - * Once the textarea is cleaned, detaches this submit listener, i.e., it sets as default, - * an then simulates the click, to submit the form. - * - * @method _cleanTagsOnSubmit - * @param {EventFacade} event - * @param {Node} submitButton - * @private - */ - _cleanTagsOnSubmit: function(event, submitButton) { - event.preventDefault(); - - this._cleanTagsWithNoYuiId(); - this._cleanTagsWithYuiId(); - - submitButton.detach('click', this._cleanTagsOnSubmit); - submitButton.simulate('click'); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that do not have "id" attribute. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. - * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method _cleanTagsWithNoYuiId - * @private - */ - _cleanTagsWithNoYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtags, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression; - - regularExpression = new RegExp(OPENING_SPAN + '.*?' + '', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags === null) { - continue; - } - - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - } - - this.markUpdated(); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that have "id" attribute, generated by YUI, when the cursor is placed on the tags. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. + * When submitting the form, this function is invoked to clean the highlighting html code. * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method anTagsWithYuiId + * @method _cleanMlangTags * @private */ - _cleanTagsWithYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression, - openingspanwithyui, - spanedmlangtagsdwithyui, - mlangtag; - - openingspanwithyui = OPENING_SPAN.replace('', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtagsdwithyui = innerHTML.match(regularExpression); - - if (spanedmlangtagsdwithyui === null) { - continue; - } - - for (index = 0; index < spanedmlangtagsdwithyui.length; index++) { - spanedmlangtag = spanedmlangtagsdwithyui[index]; - mlangtag = spanedmlangtag.match(/\{mlang.*?\}/g)[0]; - - cleanmlangtag = spanedmlangtag.replace(regularExpression, mlangtag); - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - + _cleanMlangTags: function() { + if (this._highlight) { + this.editor.setHTML(this._getHTMLwithCleanedTags(this.editor.getHTML())); this.markUpdated(); } }, /** - * Adds the tags to the {mlang} tags when the editor is loaded. - * In this case, we DON'T HAVE TO CALL TO markUpdated(). Why? Honestly, - * I don't know. But, if we call it after setting the HTML, the {mlang} - * tags flicker with the decoration, and returns to their original state. + * Adds the tags to the {mlang} tags if highlighting is enable. * * Instead of taking the HTML directly from the textarea, we have to * retrieve it, first, without the tags that can be stored @@ -449,84 +338,82 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att * * Every different {mlang} tag has to be replaced only once, otherwise, * nested s will be created in every repeated replacement. So, we - * have to have a track of which replacements have been made. + * need to track which replacements have been made. * - * @method _decorateTagsOnInit + * @method _highlightMlangTags * @private */ - _decorateTagsOnInit: function() { - var textarea = Y.all('.editor_atto_content'), - innerHTML, + _highlightMlangTags: function() { + var editorHTML, regularExpression, mlangtags, mlangtag, index, - decoratedmlangtag, + highlightedmlangtag, replacementsmade = [], notreplacedyet; - - innerHTML = this._getHTMLwithCleanedTags(); - - regularExpression = new RegExp('{mlang.*?}', 'g'); - mlangtags = innerHTML.match(regularExpression); - - if (mlangtags !== null) { - for (index = 0; index < mlangtags.length; index++) { - mlangtag = mlangtags[index]; - - notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; - - if (notreplacedyet) { - replacementsmade.push(mlangtag); - - decoratedmlangtag = OPENING_SPAN + mlangtag + ''; - regularExpression = new RegExp(mlangtag, 'g'); - - innerHTML = innerHTML.replace(regularExpression, decoratedmlangtag); + if (this._highlight) { + editorHTML = this._getHTMLwithCleanedTags(this.editor.getHTML()); + + regularExpression = new RegExp('{mlang.*?}', 'g'); + mlangtags = editorHTML.match(regularExpression); + if (mlangtags !== null) { + for (index = 0; index < mlangtags.length; index++) { + mlangtag = mlangtags[index]; + + notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; + if (notreplacedyet) { + replacementsmade.push(mlangtag); + highlightedmlangtag = OPENING_SPAN + mlangtag + CLOSING_SPAN; + regularExpression = new RegExp(mlangtag, 'g'); + editorHTML = editorHTML.replace(regularExpression, highlightedmlangtag); + } } + + this.editor.setHTML(editorHTML); } - textarea.set('innerHTML', innerHTML); + this.markUpdated(); } - }, /** - * This function returns the HTML as it is in the textarea, but cleaning every + * This function returns the HTML passed in as parameter, but cleaning every multilang * tag around the {mlang} tags. This is necessary for decorating tags on * init, because it could happen that in database are stored the {mlang} tags with * their tags, due to a bug in version 2015120501. * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8 + * Implementation based on code from EditorClean._clearSpans() * * @method _getHTMLwithCleanedTags - * @return {string} HTML in textarea, without any around {mlang} tags + * @param {string} content The to be cleaned. + * @return {string} HTML in editor, without any around {mlang} tags. */ - _getHTMLwithCleanedTags: function() { - var host = this.get('host'), - innerHTML = host.getCleanHTML(), - regexString, - regularExpression, - spanedmlangtags, - spanedmlangtag, - cleanmlangtag, - index; - - regexString = OPENING_SPAN + '.*?' + ''; - regularExpression = new RegExp(regexString, 'g'); - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags !== null) { - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - cleanmlangtag = cleanmlangtag.replace('', ''); + _getHTMLwithCleanedTags: function(content) { + // This is better to run detached from the DOM, so the browser doesn't try to update on each change. + var holder = document.createElement('div'), + spans, + spansarr; + + holder.innerHTML = content; + spans = holder.getElementsByTagName('span'); + + // Since we will be removing elements from the list, we should copy it to an array, making it static. + spansarr = Array.prototype.slice.call(spans, 0); + + spansarr.forEach(function(span) { + if (span.className.indexOf(CLASSES.TAG) !== -1) { + // Move each child (if they exist) to the parent in place of this span. + while (span.firstChild) { + span.parentNode.insertBefore(span.firstChild, span); + } - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); + // Remove the now empty span. + span.parentNode.removeChild(span); } - } + }); - return innerHTML; + return holder.innerHTML; } }, { diff --git a/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-min.js b/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-min.js index f0d313e..5b98511 100644 --- a/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-min.js +++ b/yui/build/moodle-atto_multilang2-button/moodle-atto_multilang2-button-min.js @@ -1 +1 @@ -YUI.add("moodle-atto_multilang2-button",function(e,t){var n={TAG:"filter-multilang-tag"},r="%lang",i="%content",s="languages",o="capability",u="highlight",a="css",f='{"en":"English (en)"}',l=!0,c=!0,h="outline: 1px dotted;padding: 0.1em;margin: 0em 0.1em;background-color: #ffffaa;",p={SPANED:' {mlang '+r+"}"+i+'{mlang} ',NOT_SPANED:"{mlang "+r+"}"+i+"{mlang}"},d='';e.namespace("M.atto_multilang2").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_highlight:!0,initializer:function(){var e=this.get(o),t=[];e&&(t=this._initializeToolbarItems(),this._highlight=this.get(u),this.addToolbarMenu({globalItemConfig:{callback:this._addTags},icon:"icon",iconComponent:"atto_multilang2",items:t}),this.get("host").on("atto:selectionchanged",this._checkSelectionChange,this),this._addDelimiterCss(),this._highlight&&(this._decorateTagsOnInit(),this._setSubmitListeners()))},_addDelimiterCss:function(){var e="."+n.TAG+"{"+this.get(a)+"}",t;t=document.createElement("style"),t.type="text/css",t.innerHTML=e,document.head.appendChild(t)},_initializeToolbarItems:function(){var e=[],t,n;t=JSON.parse(this.get(s));for(n in t)t.hasOwnProperty(n)&&e.push({text:t[n],callbackArgs:n});return e},_addTags:function(e,t){var n,s=this.get("host"),o,u;o=this._highlight?p.SPANED:p.NOT_SPANED,n=this._getSelectionHTML(),u=s.getSelection().toString().length===0?" ":n,o=o.replace(r,t),o=o.replace(i,u),s.insertContentAtFocusPoint(o),this.markUpdated()},_getSelectionHTML:function(){var e="",t,n,r,i;if(typeof window.getSelection!="undefined"){t=window.getSelection();if(t.rangeCount){n=document.createElement("div");for(r=0,i=t.rangeCount;r-1,s=r.match(/\{mlang/g).length===1,i&&s&&t.setSelection(t.getSelectionFromNode(e.one(n)))},_setSubmitListeners:function(){var t=e.all("input[type=submit]");t.each(this._addListenerToSubmitButtons,this)},_addListenerToSubmitButtons:function(e){var t,n,r,i,s;t=document.getElementById(e.get("id")),t!==null&&(n=t.className,r=t.form.className,i=n.match(/btn-cancel/g)===null,s=r.match(/mform/g).length>0,i&&s&&e.on("click",this._cleanTagsOnSubmit,this,e))},_cleanTagsOnSubmit:function(e,t){e.preventDefault(),this._cleanTagsWithNoYuiId(),this._cleanTagsWithYuiId(),t.detach("click",this._cleanTagsOnSubmit),t.simulate("click")},_cleanTagsWithNoYuiId:function(){var t=e.all(".editor_atto_content"),n,r,i,s,o,u,a,f;f=new RegExp(d+".*?"+"","g"),!t instanceof Array&&(n=t,t=[],t[0]=n);for(r=0;r",""),i=i.replace(o,a);n.set("innerHTML",i)}this.markUpdated()},_cleanTagsWithYuiId:function(){var t=e.all(".editor_atto_content"),n,r,i,s,o,u,a,f,l,c;f=d.replace("","g"),!t instanceof Array&&(n=t,t=[],t[0]=n);for(r=0;r",""),i=i.replace(s,u);n.set("innerHTML",i),this.markUpdated()}},_decorateTagsOnInit:function(){var t=e.all(".editor_atto_content"),n,r,i,s,o,u,a=[],f;n=this._getHTMLwithCleanedTags(),r=new RegExp("{mlang.*?}","g"),i=n.match(r);if(i!==null){for(o=0;o",r=new RegExp(s,"g"),n=n.replace(r,u));t.set("innerHTML",n)}},_getHTMLwithCleanedTags:function(){var e=this.get("host"),t=e.getCleanHTML(),n,r,i,s,o,u;n=d+".*?"+"",r=new RegExp(n,"g"),i=t.match(r);if(i!==null)for(u=0;u",""),t=t.replace(s,o);return t}},{ATTRS:{languages:f,capability:l,highlight:c,css:h}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]}); +YUI.add("moodle-atto_multilang2-button",function(e,t){var n={TAG:"filter-multilang-tag"},r="%lang",i="%content",s="languages",o="capability",u="highlight",a="css",f='{"en":"English (en)"}',l=!0,c=!0,h="outline: 1px dotted;padding: 0.1em;margin: 0em 0.1em;background-color: #ffffaa;",p='',d="",v={SPANNED:" "+p+"{mlang "+r+"}"+d+i+p+"{mlang}"+d+" ",NOT_SPANNED:"{mlang "+r+"}"+i+"{mlang}"};e.namespace("M.atto_multilang2").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_highlight:!0,initializer:function(){var e=this.get(o),t,n,r;e&&(t=this._initializeToolbarItems(),this.addToolbarMenu({globalItemConfig:{callback:this._addTags},icon:"icon",iconComponent:"atto_multilang2",items:t}),this._tagTemplate=v.NOT_SPANNED,this._highlight=this.get(u),this._highlight&&(this._tagTemplate=v.SPANNED,n=this.get("host"),r=n.textarea.ancestor("form"),r&&r.on("submit",this._cleanMlangTags,this),this.get("host").on("atto:selectionchanged",this._checkSelectionChange,this),this.get("host").on("pluginsloaded",this._addHighlightingCss,this),this.get("host").on("pluginsloaded",this._highlightMlangTags,this),this._hookUpdateOriginal(),this._hookUpdateFromTextArea()))},_initializeToolbarItems:function(){var e=[],t,n;t=JSON.parse(this.get(s));for(n in t)t.hasOwnProperty(n)&&e.push({text:t[n],callbackArgs:n});return e},_addHighlightingCss:function(){var e="."+n.TAG+" {"+this.get(a)+"}",t;t=document.createElement("style"),t.type="text/css",t.innerHTML=e,document.head.appendChild(t)},_hookUpdateOriginal:function(){var e=this.get("host"),t=this;e.updateOriginal=function(){var n=e.updateOriginal;return function(){return t._highlight&&this.updateOriginal.caller===e.plugins.html._showHTML&&t.editor.setHTML(t._getHTMLwithCleanedTags(t.editor.getHTML())),n.apply(this,arguments)}}()},_hookUpdateFromTextArea:function(){var e=this.get("host"),t=this;e.updateFromTextArea=function(){var n=e.updateFromTextArea;return function(){var r=n.apply(this,arguments);return t._highlight&&this.updateFromTextArea.caller===e.plugins.html._showHTML&&t._highlightMlangTags(),r}}()},_addTags:function(e,t){var n,s=this.get("host"),o,u;o=this._tagTemplate,n=this._getSelectionHTML(),u=s.getSelection().toString().length===0?" ":n,o=o.replace(r,t),o=o.replace(i,u),s.insertContentAtFocusPoint(o),this.markUpdated()},_getSelectionHTML:function(){var e="",t,n,r,i;if(typeof window.getSelection!="undefined"){t=window.getSelection();if(t.rangeCount){n=document.createElement("div");for(r=0,i=t.rangeCount;r', + CLOSING_SPAN = '', TEMPLATES = { - SPANED: ' {mlang ' + LANG_WILDCARD + '}' + - CONTENT_WILDCARD + - '{mlang} ', + SPANNED: ' ' + OPENING_SPAN + '{mlang ' + LANG_WILDCARD + '}' + CLOSING_SPAN + + CONTENT_WILDCARD + OPENING_SPAN + '{mlang}' + CLOSING_SPAN + ' ', + NOT_SPANNED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' + }; - NOT_SPANED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' - }, - OPENING_SPAN = ''; - -/** - * Atto text editor multilanguage plugin. - * - * @namespace M.atto_multilang2 - * @class button - * @extends M.editor_atto.EditorPlugin - */ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { @@ -72,11 +73,12 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att initializer: function() { var hascapability = this.get(ATTR_CAPABILITY), - toolbarItems = []; + toolbarItems, + host, + form; if (hascapability) { toolbarItems = this._initializeToolbarItems(); - this._highlight = this.get(ATTR_HIGHLIGHT); this.addToolbarMenu({ globalItemConfig: { @@ -87,32 +89,34 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att items: toolbarItems }); - this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - - this._addDelimiterCss(); + this._tagTemplate = TEMPLATES.NOT_SPANNED; + this._highlight = this.get(ATTR_HIGHLIGHT); if (this._highlight) { - this._decorateTagsOnInit(); - this._setSubmitListeners(); - } - } - }, + this._tagTemplate = TEMPLATES.SPANNED; + + // Attach a submit listener to the form, so we can remove + // the highlighting html before sending content to Moodle. + host = this.get('host'); + form = host.textarea.ancestor('form'); + if (form) { + form.on('submit', this._cleanMlangTags, this); + } - /** - * Adds the CSS rules for the delimiters, received as parameter from lib.php. - * - * @method _addDelimiterCss - * @private - */ - _addDelimiterCss: function() { - var css = '.' + CLASSES.TAG + '{' + this.get(ATTR_CSS) + '}', - style; + // Listen to every change of the text cursor in the text area, to see if + // the cursor is placed within a multilang tag. + this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = css; + // Highlight the multilang tags once everything is loaded. + this.get('host').on('pluginsloaded', this._addHighlightingCss, this); + this.get('host').on('pluginsloaded', this._highlightMlangTags, this); - document.head.appendChild(style); + // Hook into host.updateOriginal() and host.updateFromTextArea() + // so we can add/remove highlighting when we switch to/from HTML view. + this._hookUpdateOriginal(); + this._hookUpdateFromTextArea(); + } + } }, /** @@ -129,7 +133,6 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att langCode; languages = JSON.parse(this.get(ATTR_LANGUAGES)); - for (langCode in languages) { if (languages.hasOwnProperty(langCode)) { toolbarItems.push({ @@ -142,6 +145,76 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att return toolbarItems; }, + /** + * Adds the CSS rules for the delimiters, received as parameter from lib.php. + * + * @method _addHighlightingCss + * @private + */ + _addHighlightingCss: function() { + var css = '.' + CLASSES.TAG + ' {' + this.get(ATTR_CSS) + '}', + style; + + style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = css; + + document.head.appendChild(style); + }, + + /** + * Hook the host.updateOriginal() method to allow us to remove the highlighting html when + * switching to HTML view. As the HTML view plugin doesn't provide a hook or fire an event + * to notify about the switch to HTML view, we need to hijack host.updateOriginal and look + * for the caller. Once we've cleaned up the highlighting, we need to execute the original + * host.updateOriginal() method. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateOriginal + * @private + */ + _hookUpdateOriginal: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _removeTags(). + + host.updateOriginal = (function() { + var _updateOriginal = host.updateOriginal; + return function() { + if (multilangplugin._highlight && (this.updateOriginal.caller === host.plugins.html._showHTML)) { + multilangplugin.editor.setHTML(multilangplugin._getHTMLwithCleanedTags(multilangplugin.editor.getHTML())); + } + return _updateOriginal.apply(this, arguments); + }; + })(); + }, + + /** + * Hook the host.updateFromTextAreal() method to allow us to re-add the highlighting + * html when switching from HTML view. As the HTML view plugin doesn't provide a hook + * or fire an event to notify about the switch from HTML view, we need to hijack + * host.updateFromTextArea and look for the caller. Once we've executed the original + * host.updateFromTextArea() method, we re-added the highlighting. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateFromTextArea + * @private + */ + _hookUpdateFromTextArea: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _highlightMlangTags(). + + host.updateFromTextArea = (function() { + var _updateFromTextArea = host.updateFromTextArea; + return function() { + var ret = _updateFromTextArea.apply(this, arguments); + if (multilangplugin._highlight && (this.updateFromTextArea.caller === host.plugins.html._showHTML)) { + multilangplugin._highlightMlangTags(); + } + return ret; + }; + })(); + }, + /** * Retrieves the selected text, wraps it with the multilang tags, * and replaces the selected text in the editor with with it. @@ -164,7 +237,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att taggedContent, content; - taggedContent = (this._highlight) ? TEMPLATES.SPANED : TEMPLATES.NOT_SPANED; + taggedContent = this._tagTemplate; selection = this._getSelectionHTML(); content = (host.getSelection().toString().length === 0) ? ' ' : selection; @@ -179,7 +252,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Retrieves selected text with its HTML. - * Took from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 + * Taken from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 * * @method _getSelectionHTML * @private @@ -190,14 +263,14 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att selection, container, index, - lenght; + length; if (typeof window.getSelection !== 'undefined') { selection = window.getSelection(); if (selection.rangeCount) { container = document.createElement('div'); - for (index = 0, lenght = selection.rangeCount; index < lenght; ++index) { + for (index = 0, length = selection.rangeCount; index < length; ++index) { container.appendChild(selection.getRangeAt(index).cloneContents()); } html = container.innerHTML; @@ -214,7 +287,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Listens to every change of the text cursor in the text area. If the - * cursor is placed within a multilang tag, the whole tag is selected. + * cursor is placed within a highlighted multilang tag, the whole tag is selected. * * @method _checkSelectionChange * @private @@ -222,224 +295,40 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att _checkSelectionChange: function() { var host = this.get('host'), node = host.getSelectionParentNode(), - nodeValue = Y.one(node).get('text'), - isTextNode, - isLangTag; - - isTextNode = Y.one(node).toString().indexOf('#text') > - 1; - isLangTag = (nodeValue.match(/\{mlang/g).length === 1); - - if (isTextNode && isLangTag) { - host.setSelection(host.getSelectionFromNode(Y.one(node))); + parentNodeName, + parentClass, + selection; + + // If the event fires without a parent node for the selection, ignore the whole thing. + if ((typeof node === 'undefined') || (node === null) || (node === false) || + (typeof node.parentNode === 'undefined') || (node.parentNode === null)) { + return; } - }, - - /** - * Retrieves the inputs of type submit, and, for each element, calls the function - * that sets the submit listener. Is not made in this function because there is - * not any (apparent) way to access class scope from YUI closure. - * - * @method _setSubmitListeners - * @private - */ - _setSubmitListeners: function() { - var submitButtons = Y.all('input[type=submit]'); - - submitButtons.each(this._addListenerToSubmitButtons, this); - }, - /** - * Adds the clean tags submit listener of each input[type="submit"], but only if - * it's not 'cancel' type, and if its parent form is of 'mform' class, because there - * may be any other submit type (such us administrator's search button). - * - * @method _addListenerToSubmitButtons - * @param {Node} buttonNode - * @private - */ - _addListenerToSubmitButtons: function(buttonNode) { - var buttonObject, - className, - parentFormClassName, - notCancelButton, - notSearchButton; - - buttonObject = document.getElementById(buttonNode.get('id')); - - if (buttonObject !== null) { - className = buttonObject.className; - parentFormClassName = buttonObject.form.className; - - notCancelButton = className.match(/btn-cancel/g) === null; - notSearchButton = parentFormClassName.match(/mform/g).length > 0; - - if (notCancelButton && notSearchButton) { - buttonNode.on('click', this._cleanTagsOnSubmit, this, buttonNode); - } + parentNodeName = node.parentNode.nodeName; + parentClass = node.parentNode.hasAttribute('class') ? node.parentNode.getAttribute('class') : ''; + if ((typeof parentNodeName !== 'undefined') && (parentNodeName !== null) && (parentClass !== '') && + (parentNodeName === 'SPAN') && (parentClass.indexOf(CLASSES.TAG) !== -1)) { + selection = host.getSelectionFromNode(Y.one(node)); + host.setSelection(selection); } }, /** - * When submit button clicked, this function is invoked. It has to stop the submission, - * in order to process the textarea to clean the tags. - * - * Once the textarea is cleaned, detaches this submit listener, i.e., it sets as default, - * an then simulates the click, to submit the form. - * - * @method _cleanTagsOnSubmit - * @param {EventFacade} event - * @param {Node} submitButton - * @private - */ - _cleanTagsOnSubmit: function(event, submitButton) { - event.preventDefault(); - - this._cleanTagsWithNoYuiId(); - this._cleanTagsWithYuiId(); - - submitButton.detach('click', this._cleanTagsOnSubmit); - submitButton.simulate('click'); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that do not have "id" attribute. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. - * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method _cleanTagsWithNoYuiId - * @private - */ - _cleanTagsWithNoYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtags, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression; - - regularExpression = new RegExp(OPENING_SPAN + '.*?' + '', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags === null) { - continue; - } - - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - } - - this.markUpdated(); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that have "id" attribute, generated by YUI, when the cursor is placed on the tags. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. + * When submitting the form, this function is invoked to clean the highlighting html code. * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method anTagsWithYuiId + * @method _cleanMlangTags * @private */ - _cleanTagsWithYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression, - openingspanwithyui, - spanedmlangtagsdwithyui, - mlangtag; - - openingspanwithyui = OPENING_SPAN.replace('', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtagsdwithyui = innerHTML.match(regularExpression); - - if (spanedmlangtagsdwithyui === null) { - continue; - } - - for (index = 0; index < spanedmlangtagsdwithyui.length; index++) { - spanedmlangtag = spanedmlangtagsdwithyui[index]; - mlangtag = spanedmlangtag.match(/\{mlang.*?\}/g)[0]; - - cleanmlangtag = spanedmlangtag.replace(regularExpression, mlangtag); - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - + _cleanMlangTags: function() { + if (this._highlight) { + this.editor.setHTML(this._getHTMLwithCleanedTags(this.editor.getHTML())); this.markUpdated(); } }, /** - * Adds the tags to the {mlang} tags when the editor is loaded. - * In this case, we DON'T HAVE TO CALL TO markUpdated(). Why? Honestly, - * I don't know. But, if we call it after setting the HTML, the {mlang} - * tags flicker with the decoration, and returns to their original state. + * Adds the tags to the {mlang} tags if highlighting is enable. * * Instead of taking the HTML directly from the textarea, we have to * retrieve it, first, without the tags that can be stored @@ -449,84 +338,82 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att * * Every different {mlang} tag has to be replaced only once, otherwise, * nested s will be created in every repeated replacement. So, we - * have to have a track of which replacements have been made. + * need to track which replacements have been made. * - * @method _decorateTagsOnInit + * @method _highlightMlangTags * @private */ - _decorateTagsOnInit: function() { - var textarea = Y.all('.editor_atto_content'), - innerHTML, + _highlightMlangTags: function() { + var editorHTML, regularExpression, mlangtags, mlangtag, index, - decoratedmlangtag, + highlightedmlangtag, replacementsmade = [], notreplacedyet; - - innerHTML = this._getHTMLwithCleanedTags(); - - regularExpression = new RegExp('{mlang.*?}', 'g'); - mlangtags = innerHTML.match(regularExpression); - - if (mlangtags !== null) { - for (index = 0; index < mlangtags.length; index++) { - mlangtag = mlangtags[index]; - - notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; - - if (notreplacedyet) { - replacementsmade.push(mlangtag); - - decoratedmlangtag = OPENING_SPAN + mlangtag + ''; - regularExpression = new RegExp(mlangtag, 'g'); - - innerHTML = innerHTML.replace(regularExpression, decoratedmlangtag); + if (this._highlight) { + editorHTML = this._getHTMLwithCleanedTags(this.editor.getHTML()); + + regularExpression = new RegExp('{mlang.*?}', 'g'); + mlangtags = editorHTML.match(regularExpression); + if (mlangtags !== null) { + for (index = 0; index < mlangtags.length; index++) { + mlangtag = mlangtags[index]; + + notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; + if (notreplacedyet) { + replacementsmade.push(mlangtag); + highlightedmlangtag = OPENING_SPAN + mlangtag + CLOSING_SPAN; + regularExpression = new RegExp(mlangtag, 'g'); + editorHTML = editorHTML.replace(regularExpression, highlightedmlangtag); + } } + + this.editor.setHTML(editorHTML); } - textarea.set('innerHTML', innerHTML); + this.markUpdated(); } - }, /** - * This function returns the HTML as it is in the textarea, but cleaning every + * This function returns the HTML passed in as parameter, but cleaning every multilang * tag around the {mlang} tags. This is necessary for decorating tags on * init, because it could happen that in database are stored the {mlang} tags with * their tags, due to a bug in version 2015120501. * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8 + * Implementation based on code from EditorClean._clearSpans() * * @method _getHTMLwithCleanedTags - * @return {string} HTML in textarea, without any around {mlang} tags + * @param {string} content The to be cleaned. + * @return {string} HTML in editor, without any around {mlang} tags. */ - _getHTMLwithCleanedTags: function() { - var host = this.get('host'), - innerHTML = host.getCleanHTML(), - regexString, - regularExpression, - spanedmlangtags, - spanedmlangtag, - cleanmlangtag, - index; - - regexString = OPENING_SPAN + '.*?' + ''; - regularExpression = new RegExp(regexString, 'g'); - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags !== null) { - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - cleanmlangtag = cleanmlangtag.replace('', ''); + _getHTMLwithCleanedTags: function(content) { + // This is better to run detached from the DOM, so the browser doesn't try to update on each change. + var holder = document.createElement('div'), + spans, + spansarr; + + holder.innerHTML = content; + spans = holder.getElementsByTagName('span'); + + // Since we will be removing elements from the list, we should copy it to an array, making it static. + spansarr = Array.prototype.slice.call(spans, 0); + + spansarr.forEach(function(span) { + if (span.className.indexOf(CLASSES.TAG) !== -1) { + // Move each child (if they exist) to the parent in place of this span. + while (span.firstChild) { + span.parentNode.insertBefore(span.firstChild, span); + } - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); + // Remove the now empty span. + span.parentNode.removeChild(span); } - } + }); - return innerHTML; + return holder.innerHTML; } }, { diff --git a/yui/src/button/js/button.js b/yui/src/button/js/button.js index dca595a..634724f 100644 --- a/yui/src/button/js/button.js +++ b/yui/src/button/js/button.js @@ -16,6 +16,7 @@ /** * @package atto_multilang2 * @copyright 2015 onwards Julen Pardo & Mondragon Unibertsitatea + * @copyright 2017 onwards Iñaki Arenaza & Mondragon Unibertsitatea * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -23,6 +24,14 @@ * @module moodle-atto_multilang2-button */ +/** + * Atto text editor multilanguage plugin. + * + * @namespace M.atto_multilang2 + * @class button + * @extends M.editor_atto.EditorPlugin. + */ + var CLASSES = { TAG: 'filter-multilang-tag' }, @@ -36,26 +45,18 @@ var CLASSES = { DEFAULT_LANGUAGE = '{"en":"English (en)"}', DEFAULT_CAPABILITY = true, DEFAULT_HIGHLIGHT = true, - DEFAULT_CSS = 'outline: 1px dotted;' + - 'padding: 0.1em;' + - 'margin: 0em 0.1em;' + - 'background-color: #ffffaa;', + DEFAULT_CSS = 'outline: 1px dotted;' + + 'padding: 0.1em;' + + 'margin: 0em 0.1em;' + + 'background-color: #ffffaa;', + OPENING_SPAN = '', + CLOSING_SPAN = '', TEMPLATES = { - SPANED: ' {mlang ' + LANG_WILDCARD + '}' + - CONTENT_WILDCARD + - '{mlang} ', + SPANNED: ' ' + OPENING_SPAN + '{mlang ' + LANG_WILDCARD + '}' + CLOSING_SPAN + + CONTENT_WILDCARD + OPENING_SPAN + '{mlang}' + CLOSING_SPAN + ' ', + NOT_SPANNED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' + }; - NOT_SPANED: '{mlang ' + LANG_WILDCARD + '}' + CONTENT_WILDCARD + '{mlang}' - }, - OPENING_SPAN = ''; - -/** - * Atto text editor multilanguage plugin. - * - * @namespace M.atto_multilang2 - * @class button - * @extends M.editor_atto.EditorPlugin - */ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { @@ -70,11 +71,12 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att initializer: function() { var hascapability = this.get(ATTR_CAPABILITY), - toolbarItems = []; + toolbarItems, + host, + form; if (hascapability) { toolbarItems = this._initializeToolbarItems(); - this._highlight = this.get(ATTR_HIGHLIGHT); this.addToolbarMenu({ globalItemConfig: { @@ -85,32 +87,34 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att items: toolbarItems }); - this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - - this._addDelimiterCss(); + this._tagTemplate = TEMPLATES.NOT_SPANNED; + this._highlight = this.get(ATTR_HIGHLIGHT); if (this._highlight) { - this._decorateTagsOnInit(); - this._setSubmitListeners(); - } - } - }, + this._tagTemplate = TEMPLATES.SPANNED; + + // Attach a submit listener to the form, so we can remove + // the highlighting html before sending content to Moodle. + host = this.get('host'); + form = host.textarea.ancestor('form'); + if (form) { + form.on('submit', this._cleanMlangTags, this); + } - /** - * Adds the CSS rules for the delimiters, received as parameter from lib.php. - * - * @method _addDelimiterCss - * @private - */ - _addDelimiterCss: function() { - var css = '.' + CLASSES.TAG + '{' + this.get(ATTR_CSS) + '}', - style; + // Listen to every change of the text cursor in the text area, to see if + // the cursor is placed within a multilang tag. + this.get('host').on('atto:selectionchanged', this._checkSelectionChange, this); - style = document.createElement('style'); - style.type = 'text/css'; - style.innerHTML = css; + // Highlight the multilang tags once everything is loaded. + this.get('host').on('pluginsloaded', this._addHighlightingCss, this); + this.get('host').on('pluginsloaded', this._highlightMlangTags, this); - document.head.appendChild(style); + // Hook into host.updateOriginal() and host.updateFromTextArea() + // so we can add/remove highlighting when we switch to/from HTML view. + this._hookUpdateOriginal(); + this._hookUpdateFromTextArea(); + } + } }, /** @@ -127,7 +131,6 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att langCode; languages = JSON.parse(this.get(ATTR_LANGUAGES)); - for (langCode in languages) { if (languages.hasOwnProperty(langCode)) { toolbarItems.push({ @@ -140,6 +143,76 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att return toolbarItems; }, + /** + * Adds the CSS rules for the delimiters, received as parameter from lib.php. + * + * @method _addHighlightingCss + * @private + */ + _addHighlightingCss: function() { + var css = '.' + CLASSES.TAG + ' {' + this.get(ATTR_CSS) + '}', + style; + + style = document.createElement('style'); + style.type = 'text/css'; + style.innerHTML = css; + + document.head.appendChild(style); + }, + + /** + * Hook the host.updateOriginal() method to allow us to remove the highlighting html when + * switching to HTML view. As the HTML view plugin doesn't provide a hook or fire an event + * to notify about the switch to HTML view, we need to hijack host.updateOriginal and look + * for the caller. Once we've cleaned up the highlighting, we need to execute the original + * host.updateOriginal() method. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateOriginal + * @private + */ + _hookUpdateOriginal: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _removeTags(). + + host.updateOriginal = (function() { + var _updateOriginal = host.updateOriginal; + return function() { + if (multilangplugin._highlight && (this.updateOriginal.caller === host.plugins.html._showHTML)) { + multilangplugin.editor.setHTML(multilangplugin._getHTMLwithCleanedTags(multilangplugin.editor.getHTML())); + } + return _updateOriginal.apply(this, arguments); + }; + })(); + }, + + /** + * Hook the host.updateFromTextAreal() method to allow us to re-add the highlighting + * html when switching from HTML view. As the HTML view plugin doesn't provide a hook + * or fire an event to notify about the switch from HTML view, we need to hijack + * host.updateFromTextArea and look for the caller. Once we've executed the original + * host.updateFromTextArea() method, we re-added the highlighting. + * Inspired by https://stackoverflow.com/a/16580937 + * + * @method _hookUpdateFromTextArea + * @private + */ + _hookUpdateFromTextArea: function() { + var host = this.get('host'), + multilangplugin = this; // Capture the plugin in the closure below, so we can invoke _highlightMlangTags(). + + host.updateFromTextArea = (function() { + var _updateFromTextArea = host.updateFromTextArea; + return function() { + var ret = _updateFromTextArea.apply(this, arguments); + if (multilangplugin._highlight && (this.updateFromTextArea.caller === host.plugins.html._showHTML)) { + multilangplugin._highlightMlangTags(); + } + return ret; + }; + })(); + }, + /** * Retrieves the selected text, wraps it with the multilang tags, * and replaces the selected text in the editor with with it. @@ -162,7 +235,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att taggedContent, content; - taggedContent = (this._highlight) ? TEMPLATES.SPANED : TEMPLATES.NOT_SPANED; + taggedContent = this._tagTemplate; selection = this._getSelectionHTML(); content = (host.getSelection().toString().length === 0) ? ' ' : selection; @@ -177,7 +250,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Retrieves selected text with its HTML. - * Took from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 + * Taken from: http://stackoverflow.com/questions/4176923/html-of-selected-text/4177234#4177234 * * @method _getSelectionHTML * @private @@ -188,14 +261,14 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att selection, container, index, - lenght; + length; if (typeof window.getSelection !== 'undefined') { selection = window.getSelection(); if (selection.rangeCount) { container = document.createElement('div'); - for (index = 0, lenght = selection.rangeCount; index < lenght; ++index) { + for (index = 0, length = selection.rangeCount; index < length; ++index) { container.appendChild(selection.getRangeAt(index).cloneContents()); } html = container.innerHTML; @@ -212,7 +285,7 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att /** * Listens to every change of the text cursor in the text area. If the - * cursor is placed within a multilang tag, the whole tag is selected. + * cursor is placed within a highlighted multilang tag, the whole tag is selected. * * @method _checkSelectionChange * @private @@ -220,224 +293,40 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att _checkSelectionChange: function() { var host = this.get('host'), node = host.getSelectionParentNode(), - nodeValue = Y.one(node).get('text'), - isTextNode, - isLangTag; - - isTextNode = Y.one(node).toString().indexOf('#text') > - 1; - isLangTag = (nodeValue.match(/\{mlang/g).length === 1); - - if (isTextNode && isLangTag) { - host.setSelection(host.getSelectionFromNode(Y.one(node))); + parentNodeName, + parentClass, + selection; + + // If the event fires without a parent node for the selection, ignore the whole thing. + if ((typeof node === 'undefined') || (node === null) || (node === false) || + (typeof node.parentNode === 'undefined') || (node.parentNode === null)) { + return; } - }, - - /** - * Retrieves the inputs of type submit, and, for each element, calls the function - * that sets the submit listener. Is not made in this function because there is - * not any (apparent) way to access class scope from YUI closure. - * - * @method _setSubmitListeners - * @private - */ - _setSubmitListeners: function() { - var submitButtons = Y.all('input[type=submit]'); - - submitButtons.each(this._addListenerToSubmitButtons, this); - }, - /** - * Adds the clean tags submit listener of each input[type="submit"], but only if - * it's not 'cancel' type, and if its parent form is of 'mform' class, because there - * may be any other submit type (such us administrator's search button). - * - * @method _addListenerToSubmitButtons - * @param {Node} buttonNode - * @private - */ - _addListenerToSubmitButtons: function(buttonNode) { - var buttonObject, - className, - parentFormClassName, - notCancelButton, - notSearchButton; - - buttonObject = document.getElementById(buttonNode.get('id')); - - if (buttonObject !== null) { - className = buttonObject.className; - parentFormClassName = buttonObject.form.className; - - notCancelButton = className.match(/btn-cancel/g) === null; - notSearchButton = parentFormClassName.match(/mform/g).length > 0; - - if (notCancelButton && notSearchButton) { - buttonNode.on('click', this._cleanTagsOnSubmit, this, buttonNode); - } + parentNodeName = node.parentNode.nodeName; + parentClass = node.parentNode.hasAttribute('class') ? node.parentNode.getAttribute('class') : ''; + if ((typeof parentNodeName !== 'undefined') && (parentNodeName !== null) && (parentClass !== '') && + (parentNodeName === 'SPAN') && (parentClass.indexOf(CLASSES.TAG) !== -1)) { + selection = host.getSelectionFromNode(Y.one(node)); + host.setSelection(selection); } }, /** - * When submit button clicked, this function is invoked. It has to stop the submission, - * in order to process the textarea to clean the tags. - * - * Once the textarea is cleaned, detaches this submit listener, i.e., it sets as default, - * an then simulates the click, to submit the form. - * - * @method _cleanTagsOnSubmit - * @param {EventFacade} event - * @param {Node} submitButton - * @private - */ - _cleanTagsOnSubmit: function(event, submitButton) { - event.preventDefault(); - - this._cleanTagsWithNoYuiId(); - this._cleanTagsWithYuiId(); - - submitButton.detach('click', this._cleanTagsOnSubmit); - submitButton.simulate('click'); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that do not have "id" attribute. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. - * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method _cleanTagsWithNoYuiId - * @private - */ - _cleanTagsWithNoYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtags, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression; - - regularExpression = new RegExp(OPENING_SPAN + '.*?' + '', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags === null) { - continue; - } - - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - } - - this.markUpdated(); - }, - - /** - * Cleans the tags around the {mlang} tags when the form is submitted, - * that have "id" attribute, generated by YUI, when the cursor is placed on the tags. - * The cleanup with "id" attribute and without it is made separately, to avoid an evil - * regular expression. + * When submitting the form, this function is invoked to clean the highlighting html code. * - * There may be more than one atto editor textarea in the page. So, we have to retrieve - * the textareas by the class name. If there is only one, the object will be only the - * reference, but, if there are more, we will have an array. So, the easiest way is to - * check if what we have is an array, and if it not, create it manually, and iterate it - * later. - * - * issue #15: the textareas are now retrieved passing to YUI selector the whole element, - * instead of the id string, due to problems with special characters. - * See discussion: https://moodle.org/mod/forum/discuss.php?d=332217 - * - * @method anTagsWithYuiId + * @method _cleanMlangTags * @private */ - _cleanTagsWithYuiId: function() { - var textareas = Y.all('.editor_atto_content'), - textarea, - textareaIndex, - innerHTML, - spanedmlangtag, - index, - cleanmlangtag, - regularExpression, - openingspanwithyui, - spanedmlangtagsdwithyui, - mlangtag; - - openingspanwithyui = OPENING_SPAN.replace('', 'g'); - - if (!textareas instanceof Array) { - textarea = textareas; - textareas = []; - textareas[0] = textarea; - } - - for (textareaIndex = 0; textareaIndex < textareas._nodes.length; textareaIndex++) { - textarea = textareas._nodes[textareaIndex].id; - textarea = Y.one(document.getElementById(textarea)); - - innerHTML = textarea.get('innerHTML'); - - spanedmlangtagsdwithyui = innerHTML.match(regularExpression); - - if (spanedmlangtagsdwithyui === null) { - continue; - } - - for (index = 0; index < spanedmlangtagsdwithyui.length; index++) { - spanedmlangtag = spanedmlangtagsdwithyui[index]; - mlangtag = spanedmlangtag.match(/\{mlang.*?\}/g)[0]; - - cleanmlangtag = spanedmlangtag.replace(regularExpression, mlangtag); - cleanmlangtag = cleanmlangtag.replace('', ''); - - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); - } - - textarea.set('innerHTML', innerHTML); - + _cleanMlangTags: function() { + if (this._highlight) { + this.editor.setHTML(this._getHTMLwithCleanedTags(this.editor.getHTML())); this.markUpdated(); } }, /** - * Adds the tags to the {mlang} tags when the editor is loaded. - * In this case, we DON'T HAVE TO CALL TO markUpdated(). Why? Honestly, - * I don't know. But, if we call it after setting the HTML, the {mlang} - * tags flicker with the decoration, and returns to their original state. + * Adds the tags to the {mlang} tags if highlighting is enable. * * Instead of taking the HTML directly from the textarea, we have to * retrieve it, first, without the tags that can be stored @@ -447,84 +336,82 @@ Y.namespace('M.atto_multilang2').Button = Y.Base.create('button', Y.M.editor_att * * Every different {mlang} tag has to be replaced only once, otherwise, * nested s will be created in every repeated replacement. So, we - * have to have a track of which replacements have been made. + * need to track which replacements have been made. * - * @method _decorateTagsOnInit + * @method _highlightMlangTags * @private */ - _decorateTagsOnInit: function() { - var textarea = Y.all('.editor_atto_content'), - innerHTML, + _highlightMlangTags: function() { + var editorHTML, regularExpression, mlangtags, mlangtag, index, - decoratedmlangtag, + highlightedmlangtag, replacementsmade = [], notreplacedyet; - - innerHTML = this._getHTMLwithCleanedTags(); - - regularExpression = new RegExp('{mlang.*?}', 'g'); - mlangtags = innerHTML.match(regularExpression); - - if (mlangtags !== null) { - for (index = 0; index < mlangtags.length; index++) { - mlangtag = mlangtags[index]; - - notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; - - if (notreplacedyet) { - replacementsmade.push(mlangtag); - - decoratedmlangtag = OPENING_SPAN + mlangtag + ''; - regularExpression = new RegExp(mlangtag, 'g'); - - innerHTML = innerHTML.replace(regularExpression, decoratedmlangtag); + if (this._highlight) { + editorHTML = this._getHTMLwithCleanedTags(this.editor.getHTML()); + + regularExpression = new RegExp('{mlang.*?}', 'g'); + mlangtags = editorHTML.match(regularExpression); + if (mlangtags !== null) { + for (index = 0; index < mlangtags.length; index++) { + mlangtag = mlangtags[index]; + + notreplacedyet = replacementsmade.indexOf(mlangtag) === -1; + if (notreplacedyet) { + replacementsmade.push(mlangtag); + highlightedmlangtag = OPENING_SPAN + mlangtag + CLOSING_SPAN; + regularExpression = new RegExp(mlangtag, 'g'); + editorHTML = editorHTML.replace(regularExpression, highlightedmlangtag); + } } + + this.editor.setHTML(editorHTML); } - textarea.set('innerHTML', innerHTML); + this.markUpdated(); } - }, /** - * This function returns the HTML as it is in the textarea, but cleaning every + * This function returns the HTML passed in as parameter, but cleaning every multilang * tag around the {mlang} tags. This is necessary for decorating tags on * init, because it could happen that in database are stored the {mlang} tags with * their tags, due to a bug in version 2015120501. * More info about this bug: https://github.com/julenpardo/moodle-atto_multilang2/issues/8 + * Implementation based on code from EditorClean._clearSpans() * * @method _getHTMLwithCleanedTags - * @return {string} HTML in textarea, without any around {mlang} tags + * @param {string} content The to be cleaned. + * @return {string} HTML in editor, without any around {mlang} tags. */ - _getHTMLwithCleanedTags: function() { - var host = this.get('host'), - innerHTML = host.getCleanHTML(), - regexString, - regularExpression, - spanedmlangtags, - spanedmlangtag, - cleanmlangtag, - index; - - regexString = OPENING_SPAN + '.*?' + ''; - regularExpression = new RegExp(regexString, 'g'); - spanedmlangtags = innerHTML.match(regularExpression); - - if (spanedmlangtags !== null) { - for (index = 0; index < spanedmlangtags.length; index++) { - spanedmlangtag = spanedmlangtags[index]; - - cleanmlangtag = spanedmlangtag.replace(OPENING_SPAN, ''); - cleanmlangtag = cleanmlangtag.replace('', ''); + _getHTMLwithCleanedTags: function(content) { + // This is better to run detached from the DOM, so the browser doesn't try to update on each change. + var holder = document.createElement('div'), + spans, + spansarr; + + holder.innerHTML = content; + spans = holder.getElementsByTagName('span'); + + // Since we will be removing elements from the list, we should copy it to an array, making it static. + spansarr = Array.prototype.slice.call(spans, 0); + + spansarr.forEach(function(span) { + if (span.className.indexOf(CLASSES.TAG) !== -1) { + // Move each child (if they exist) to the parent in place of this span. + while (span.firstChild) { + span.parentNode.insertBefore(span.firstChild, span); + } - innerHTML = innerHTML.replace(spanedmlangtag, cleanmlangtag); + // Remove the now empty span. + span.parentNode.removeChild(span); } - } + }); - return innerHTML; + return holder.innerHTML; } }, {