���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/equation.tar
���ѧ٧ѧ�
styles.css 0000644 00000000335 15152212667 0006611 0 ustar 00 .atto_equation_library button { margin: 0.25%; min-width: 12%; } #page-admin-setting-atto_equation_settings .form-defaultinfo { max-height: 10em; overflow: auto; padding: 5px; min-width: 206px; } db/upgrade.php 0000644 00000004065 15152212667 0007305 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Atto equation plugin upgrade script. * * @package atto_equation * @copyright 2015 Sam Chaffee <sam@moodlerooms.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Run all Atto equation upgrade steps between the current DB version and the current version on disk. * @param int $oldversion The old version of atto equation in the DB. * @return bool */ function xmldb_atto_equation_upgrade($oldversion) { global $CFG; // Automatically generated Moodle v3.9.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.0.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2022110700) { $oldmatrix = '\left| \begin{matrix} a_1 & a_2 \\ a_3 & a_4 \end{matrix} \right|'; $fixedmatrix = '\left| \begin{matrix} a_1 & a_2 \\\\ a_3 & a_4 \end{matrix} \right|'; $config = get_config('atto_equation', 'librarygroup4'); $newdefault = str_replace($oldmatrix, $fixedmatrix, $config); set_config('librarygroup4', $newdefault, 'atto_equation'); // Atto equation savepoint reached. upgrade_plugin_savepoint(true, 2022110700, 'atto', 'equation'); } // Automatically generated Moodle v4.1.0 release upgrade line. // Put any upgrade step following this. return true; } tests/behat/equation.feature 0000644 00000003335 15152212667 0012206 0 ustar 00 @editor @editor_atto @atto @atto_equation @_bug_phantomjs Feature: Atto equation editor To teach maths to students, I need to write equations @javascript Scenario: Create an equation Given I log in as "admin" When I open my profile in edit mode And I set the field "Description" to "<p>Equation test</p>" # Set field on the bottom of page, so equation editor dialogue is visible. And I expand all fieldsets And I set the field "Picture description" to "Test" And I select the text in the "Description" Atto editor And I click on "Show more buttons" "button" And I click on "Equation editor" "button" And the "class" attribute of "Edit equation using" "field" should contain "text-ltr" And I set the field "Edit equation using" to " = 1 \div 0" And I click on "\infty" "button" And I click on "Save equation" "button" And I click on "Update profile" "button" And I follow "Profile" in the user menu Then "\infty" "text" should exist @javascript Scenario: Edit an equation Given I log in as "admin" When I open my profile in edit mode And I set the field "Description" to "<p>\( \pi \)</p>" # Set field on the bottom of page, so equation editor dialogue is visible. And I expand all fieldsets And I set the field "Picture description" to "Test" And I select the text in the "Description" Atto editor And I click on "Show more buttons" "button" And I click on "Equation editor" "button" And the "class" attribute of "Edit equation using" "field" should contain "text-ltr" Then the field "Edit equation using" matches value " \pi " And I click on "Save equation" "button" And the field "Description" matches value "<p>\( \pi \)</p>" ajax.php 0000644 00000003147 15152212667 0006214 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Renders text with the active filters and returns it. Used to create previews of equations * using whatever tex filters are enabled. * * @package atto_equation * @copyright 2014 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require_once(__DIR__ . '/../../../../../config.php'); $contextid = required_param('contextid', PARAM_INT); list($context, $course, $cm) = get_context_info_array($contextid); $PAGE->set_url('/lib/editor/atto/plugins/equation/ajax.php'); $PAGE->set_context($context); require_login($course, false, $cm); require_sesskey(); $action = required_param('action', PARAM_ALPHA); if ($action === 'filtertext') { $text = required_param('text', PARAM_RAW); $result = filter_manager::instance()->filter_text($text, $context); echo $OUTPUT->header(); echo $result; die(); } throw new \moodle_exception('invalidarguments'); settings.php 0000644 00000007374 15152212667 0007137 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Settings that allow configuration of the list of tex examples in the equation editor. * * @package atto_equation * @copyright 2013 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $ADMIN->add('editoratto', new admin_category('atto_equation', new lang_string('pluginname', 'atto_equation'))); $settings = new admin_settingpage('atto_equation_settings', new lang_string('settings', 'atto_equation')); if ($ADMIN->fulltree) { // Group 1 $name = new lang_string('librarygroup1', 'atto_equation'); $desc = new lang_string('librarygroup1_desc', 'atto_equation'); $default = ' \cdot \times \ast \div \diamond \pm \mp \oplus \ominus \otimes \oslash \odot \circ \bullet \asymp \equiv \subseteq \supseteq \leq \geq \preceq \succeq \sim \simeq \approx \subset \supset \ll \gg \prec \succ \infty \in \ni \forall \exists \neq '; $setting = new admin_setting_configtextarea('atto_equation/librarygroup1', $name, $desc, $default); $settings->add($setting); // Group 2 $name = new lang_string('librarygroup2', 'atto_equation'); $desc = new lang_string('librarygroup2_desc', 'atto_equation'); $default = ' \leftarrow \rightarrow \uparrow \downarrow \leftrightarrow \nearrow \searrow \swarrow \nwarrow \Leftarrow \Rightarrow \Uparrow \Downarrow \Leftrightarrow '; $setting = new admin_setting_configtextarea('atto_equation/librarygroup2', $name, $desc, $default); $settings->add($setting); // Group 3 $name = new lang_string('librarygroup3', 'atto_equation'); $desc = new lang_string('librarygroup3_desc', 'atto_equation'); $default = ' \alpha \beta \gamma \delta \epsilon \zeta \eta \theta \iota \kappa \lambda \mu \nu \xi \pi \rho \sigma \tau \upsilon \phi \chi \psi \omega \Gamma \Delta \Theta \Lambda \Xi \Pi \Sigma \Upsilon \Phi \Psi \Omega '; $setting = new admin_setting_configtextarea('atto_equation/librarygroup3', $name, $desc, $default); $settings->add($setting); // Group 4 $name = new lang_string('librarygroup4', 'atto_equation'); $desc = new lang_string('librarygroup4_desc', 'atto_equation'); $default = ' \sum{a,b} \sqrt[a]{b+c} \int_{a}^{b}{c} \iint_{a}^{b}{c} \iiint_{a}^{b}{c} \oint{a} (a) [a] \lbrace{a}\rbrace \left| \begin{matrix} a_1 & a_2 \\\\ a_3 & a_4 \end{matrix} \right| \frac{a}{b+c} \vec{a} \binom {a} {b} {a \brack b} {a \brace b} '; $setting = new admin_setting_configtextarea('atto_equation/librarygroup4', $name, $desc, $default); $settings->add($setting); } lib.php 0000644 00000006334 15152212667 0006040 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Atto text editor integration version file. * * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Get the list of strings for this plugin. * @param string $elementid */ function atto_equation_strings_for_js() { global $PAGE; $PAGE->requires->strings_for_js(array('saveequation', 'editequation', 'preview', 'cursorinfo', 'update', 'librarygroup1', 'librarygroup2', 'librarygroup3', 'librarygroup4'), 'atto_equation'); } /** * Set params for this plugin. * * @param string $elementid * @param stdClass $options - the options for the editor, including the context. * @param stdClass $fpoptions - unused. */ function atto_equation_params_for_js($elementid, $options, $fpoptions) { $texexample = '$$\pi$$'; // Format a string with the active filter set. // If it is modified - we assume that some sort of text filter is working in this context. $result = format_text($texexample, true, $options); $texfilteractive = ($texexample !== $result); $context = $options['context']; if (!$context) { $context = context_system::instance(); } // Tex example librarys. $library = array( 'group1' => array( 'groupname' => 'librarygroup1', 'elements' => get_config('atto_equation', 'librarygroup1'), 'active' => true, ), 'group2' => array( 'groupname' => 'librarygroup2', 'elements' => get_config('atto_equation', 'librarygroup2'), ), 'group3' => array( 'groupname' => 'librarygroup3', 'elements' => get_config('atto_equation', 'librarygroup3'), ), 'group4' => array( 'groupname' => 'librarygroup4', 'elements' => get_config('atto_equation', 'librarygroup4'), )); return array('texfilteractive' => $texfilteractive, 'contextid' => $context->id, 'library' => $library, 'texdocsurl' => get_docs_url('Using_TeX_Notation')); } version.php 0000644 00000002252 15152212667 0006752 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Atto text editor integration version file. * * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2022112800; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022111800; // Requires this Moodle version. $plugin->component = 'atto_equation'; // Full name of the plugin (used for diagnostics). classes/privacy/provider.php 0000644 00000003004 15152212667 0012225 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Privacy Subsystem implementation for block_activity_modules. * * @package atto_equation * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace atto_equation\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for atto_equation implementing null_provider. * * @copyright 2018 Andrew Nicols <andrew@nicols.co.uk> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason() : string { return 'privacy:metadata'; } } yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js 0000644 00000065063 15152212667 0022134 0 ustar 00 YUI.add('moodle-atto_equation-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 <http://www.gnu.org/licenses/>. /** * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Atto text editor equation plugin. */ /** * Atto equation editor. * * @namespace M.atto_equation * @class Button * @extends M.editor_atto.EditorPlugin */ var COMPONENTNAME = 'atto_equation', LOGNAME = 'atto_equation', CSS = { EQUATION_TEXT: 'atto_equation_equation', EQUATION_PREVIEW: 'atto_equation_preview', SUBMIT: 'atto_equation_submit', LIBRARY: 'atto_equation_library', LIBRARY_GROUPS: 'atto_equation_groups', LIBRARY_GROUP_PREFIX: 'atto_equation_group' }, SELECTORS = { LIBRARY: '.' + CSS.LIBRARY, LIBRARY_GROUP: '.' + CSS.LIBRARY_GROUPS + ' > div > div', EQUATION_TEXT: '.' + CSS.EQUATION_TEXT, EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW, SUBMIT: '.' + CSS.SUBMIT, LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button' }, DELIMITERS = { START: '\\(', END: '\\)' }, TEMPLATES = { FORM: '' + '<form class="atto_form">' + '{{{library}}}' + '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' + '<textarea class="fullwidth text-ltr {{CSS.EQUATION_TEXT}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' + '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' + '<div describedby="{{elementid}}_cursorinfo" ' + 'class="border rounded bg-light p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' + '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' + '<div class="mdl-align">' + '<br/>' + '<button class="btn btn-secondary {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' + '</div>' + '</form>', LIBRARY: '' + '<div class="{{CSS.LIBRARY}}">' + '<ul class="root nav nav-tabs mb-1" role="tablist">' + '{{#each library}}' + '<li class="nav-item">' + '<a class="nav-link{{#active}} active{{/active}}" ' + '{{#active}}aria-selected="true"{{/active}}' + '{{^active}}aria-selected="false" tabindex="-1"{{/active}}' + ' href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' + ' data-target="#{{../elementidescaped}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}"' + ' role="tab" data-toggle="tab">' + '{{get_string groupname ../component}}' + '</a>' + '</li>' + '{{/each}}' + '</ul>' + '<div class="tab-content mb-1 {{CSS.LIBRARY_GROUPS}}">' + '{{#each library}}' + '<div data-medium-type="{{CSS.LINK}}" class="tab-pane{{#active}} active{{/active}}" ' + 'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' + '<div role="toolbar">' + '{{#split "\n" elements}}' + '<button class="btn btn-secondary" tabindex="-1" data-tex="{{this}}"' + 'aria-label="{{this}}" title="{{this}}">' + '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' + '</button>' + '{{/split}}' + '</div>' + '</div>' + '{{/each}}' + '</div>' + '</div>' }; Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { /** * The selection object returned by the browser. * * @property _currentSelection * @type Range * @default null * @private */ _currentSelection: null, /** * The cursor position in the equation textarea. * * @property _lastCursorPos * @type Number * @default 0 * @private */ _lastCursorPos: 0, /** * A reference to the dialogue content. * * @property _content * @type Node * @private */ _content: null, /** * The source equation we are editing in the text. * * @property _sourceEquation * @type Object * @private */ _sourceEquation: null, /** * A reference to the tab focus set on each group. * * The keys are the IDs of the group, the value is the Node on which the focus is set. * * @property _groupFocus * @type Object * @private */ _groupFocus: null, /** * Regular Expression patterns used to pick out the equations in a String. * * @property _equationPatterns * @type Array * @private */ _equationPatterns: [ // We use space or not space because . does not match new lines. // $$ blah $$. /\$\$([\S\s]+?)\$\$/, // E.g. "\( blah \)". /\\\(([\S\s]+?)\\\)/, // E.g. "\[ blah \]". /\\\[([\S\s]+?)\\\]/, // E.g. "[tex] blah [/tex]". /\[tex\]([\S\s]+?)\[\/tex\]/ ], initializer: function() { this._groupFocus = {}; // If there is a tex filter active - enable this button. if (this.get('texfilteractive')) { // Add the button to the toolbar. this.addButton({ icon: 'e/math', callback: this._displayDialogue }); // We need custom highlight logic for this button. this.get('host').on('atto:selectionchanged', function() { if (this._resolveEquation()) { this.highlightButtons(); } else { this.unHighlightButtons(); } }, this); // We need to convert these to a non dom node based format. this.editor.all('tex').each(function(texNode) { var replacement = Y.Node.create('<span>' + DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END + '</span>'); texNode.replace(replacement); }); } }, /** * Display the equation editor. * * @method _displayDialogue * @private */ _displayDialogue: function() { this._currentSelection = this.get('host').getSelection(); if (this._currentSelection === false) { return; } // This needs to be done before the dialogue is opened because the focus will shift to the dialogue. var equation = this._resolveEquation(); var dialogue = this.getDialogue({ headerContent: M.util.get_string('pluginname', COMPONENTNAME), focusAfterHide: true, width: 600, focusOnShowSelector: SELECTORS.LIBRARY_BUTTON }); var content = this._getDialogueContent(); dialogue.set('bodyContent', content); dialogue.show(); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode()); }); if (equation) { content.one(SELECTORS.EQUATION_TEXT).set('text', equation); } this._updatePreview(false); }, /** * If there is selected text and it is part of an equation, * extract the equation (and set it in the form). * * @method _resolveEquation * @private * @return {String|Boolean} The equation or false. */ _resolveEquation: function() { // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), selection = this.get('host').getSelection(), text, returnValue = false; // Prevent resolving equations when we don't have focus. if (!this.get('host').isActive()) { return false; } // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. if (!selection || selection.length === 0) { return false; } this.sourceEquation = null; selection = selection[0]; text = Y.one(selectedNode).get('text'); // For each of these patterns we have a RegExp which captures the inner component of the equation but also // includes the delimiters. // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire // equation including delimiters and returning one entry per match of the whole equation. // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the // match results. Y.Array.find(this._equationPatterns, function(pattern) { // For each pattern in turn, find all whole matches (including the delimiters). var patternMatches = text.match(new RegExp(pattern.source, "g")); if (patternMatches && patternMatches.length) { // This pattern matches at least once. See if this pattern matches our current position. // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent // searches which is the required behaviour of this function. return Y.Array.find(patternMatches, function(match) { // Check each occurrence of this match. var startIndex = 0; while (text.indexOf(match, startIndex) !== -1) { // Determine whether the cursor is in the current occurrence of this string. // Note: We do not support a selection exceeding the bounds of an equation. var startOuter = text.indexOf(match, startIndex), endOuter = startOuter + match.length, startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); if (startMatch && endMatch) { // This match is in our current position - fetch the innerMatch data. var innerMatch = match.match(pattern); if (innerMatch && innerMatch.length) { // We need the start and end of the inner match for later. var startInner = text.indexOf(innerMatch[1], startOuter), endInner = startInner + innerMatch[1].length; // We'll be returning the inner match for use in the editor itself. returnValue = innerMatch[1]; // Save all data for later. this.sourceEquation = { // Outer match data. startOuterPosition: startOuter, endOuterPosition: endOuter, outerMatch: match, // Inner match data. startInnerPosition: startInner, endInnerPosition: endInner, innerMatch: innerMatch }; // This breaks out of both Y.Array.find functions. return true; } } // Update the startIndex to match the end of the current match so that we can continue hunting // for further matches. startIndex = endOuter; } }, this); } }, this); // We trim the equation when we load it and then add spaces when we save it. if (returnValue !== false) { returnValue = returnValue.trim(); } return returnValue; }, /** * Handle insertion of a new equation, or update of an existing one. * * @method _setEquation * @param {EventFacade} e * @private */ _setEquation: function(e) { var input, selectedNode, text, value, host, newText; host = this.get('host'); e.preventDefault(); this.getDialogue({ focusAfterHide: null }).hide(); input = e.currentTarget.ancestor('.atto_form').one('textarea'); value = input.get('value'); if (value !== '') { host.setSelection(this._currentSelection); if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); value = ' ' + value + ' '; newText = text.slice(0, this.sourceEquation.startInnerPosition) + value + text.slice(this.sourceEquation.endInnerPosition); selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; host.insertContentAtFocusPoint(value); } // Clean the YUI ids from the HTML. this.markUpdated(); } }, /** * Smart throttle, only call a function every delay milli seconds, * and always run the last call. Y.throttle does not work here, * because it calls the function immediately, the first time, and then * ignores repeated calls within X seconds. This does not guarantee * that the last call will be executed (which is required here). * * @param {function} fn * @param {Number} delay Delay in milliseconds * @method _throttle * @private */ _throttle: function(fn, delay) { var timer = null; return function() { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(context, args); }, delay); }; }, /** * Update the preview div to match the current equation. * * @param {EventFacade} e * @method _updatePreview * @private */ _updatePreview: function(e) { var textarea = this._content.one(SELECTORS.EQUATION_TEXT), equation = textarea.get('value'), url, currentPos = textarea.get('selectionStart'), prefix = '', cursorLatex = '\\Downarrow ', isChar, params; if (e) { e.preventDefault(); } // Move the cursor so it does not break expressions. // Start at the very beginning. if (!currentPos) { currentPos = 0; } // First move back to the beginning of the line. while (equation.charAt(currentPos) === '\\' && currentPos >= 0) { currentPos -= 1; } isChar = /[a-zA-Z\{]/; if (currentPos !== 0) { if (equation.charAt(currentPos - 1) != '{') { // Now match to the end of the line. while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length && isChar.test(equation.charAt(currentPos - 1))) { currentPos += 1; } } } // Save the cursor position - for insertion from the library. this._lastCursorPos = currentPos; equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos); equation = DELIMITERS.START + ' ' + equation + ' ' + DELIMITERS.END; // Make an ajax request to the filter. url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: equation }; Y.io(url, { context: this, data: params, timeout: 500, on: { complete: this._loadPreview } }); }, /** * Load returned preview text into preview * * @param {String} id * @param {EventFacade} e * @method _loadPreview * @private */ _loadPreview: function(id, preview) { var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW); if (preview.status === 200) { previewNode.setHTML(preview.responseText); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(previewNode.getDOMNode()); }); } }, /** * Return the dialogue content for the tool, attaching any required * events. * * @method _getDialogueContent * @return {Node} * @private */ _getDialogueContent: function() { var library = this._getLibraryContent(), throttledUpdate = this._throttle(this._updatePreview, 500), template = Y.Handlebars.compile(TEMPLATES.FORM); this._content = Y.Node.create(template({ elementid: this.get('host').get('elementid'), component: COMPONENTNAME, library: library, texdocsurl: this.get('texdocsurl'), CSS: CSS })); // Sets the default focus. this._content.all(SELECTORS.LIBRARY_GROUP).each(function(group) { // The first button gets the focus. this._setGroupTabFocus(group, group.one('button')); // Sometimes the filter adds an anchor in the button, no tabindex on that. group.all('button a').setAttribute('tabindex', '-1'); }, this); // Keyboard navigation in groups. this._content.delegate('key', this._groupNavigation, 'down:37,39', SELECTORS.LIBRARY_BUTTON, this); this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this); this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', throttledUpdate, this); this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this); return this._content; }, /** * Callback handling the keyboard navigation in the groups of the library. * * @param {EventFacade} e The event. * @method _groupNavigation * @private */ _groupNavigation: function(e) { e.preventDefault(); var current = e.currentTarget, parent = current.get('parentNode'), // This must be the <div> containing all the buttons of the group. buttons = parent.all('button'), direction = e.keyCode !== 37 ? 1 : -1, index = buttons.indexOf(current), nextButton; if (index < 0) { index = 0; } index += direction; if (index < 0) { index = buttons.size() - 1; } else if (index >= buttons.size()) { index = 0; } nextButton = buttons.item(index); this._setGroupTabFocus(parent, nextButton); nextButton.focus(); }, /** * Sets tab focus for the group. * * @method _setGroupTabFocus * @param {Node} button The node that focus should now be set to. * @private */ _setGroupTabFocus: function(parent, button) { var parentId = parent.generateID(); // Unset the previous entry. if (typeof this._groupFocus[parentId] !== 'undefined') { this._groupFocus[parentId].setAttribute('tabindex', '-1'); } // Set on the new entry. this._groupFocus[parentId] = button; button.setAttribute('tabindex', 0); parent.setAttribute('aria-activedescendant', button.generateID()); }, /** * Reponse to button presses in the TeX library panels. * * @method _selectLibraryItem * @param {EventFacade} e * @return {string} * @private */ _selectLibraryItem: function(e) { var tex = e.currentTarget.getAttribute('data-tex'), oldValue, newValue, input, focusPoint = 0; e.preventDefault(); // Set the group focus on the button. this._setGroupTabFocus(e.currentTarget.get('parentNode'), e.currentTarget); input = e.currentTarget.ancestor('.atto_form').one('textarea'); oldValue = input.get('value'); newValue = oldValue.substring(0, this._lastCursorPos); if (newValue.charAt(newValue.length - 1) !== ' ') { newValue += ' '; } newValue += tex; focusPoint = newValue.length; if (oldValue.charAt(this._lastCursorPos) !== ' ') { newValue += ' '; } newValue += oldValue.substring(this._lastCursorPos, oldValue.length); input.set('value', newValue); input.focus(); var realInput = input.getDOMNode(); if (typeof realInput.selectionStart === "number") { // Modern browsers have selectionStart and selectionEnd to control the cursor position. realInput.selectionStart = realInput.selectionEnd = focusPoint; } else if (typeof realInput.createTextRange !== "undefined") { // Legacy browsers (IE<=9) use createTextRange(). var range = realInput.createTextRange(); range.moveToPoint(focusPoint); range.select(); } // Focus must be set before updating the preview for the cursor box to be in the correct location. this._updatePreview(false); }, /** * Return the HTML for rendering the library of predefined buttons. * * @method _getLibraryContent * @return {string} * @private */ _getLibraryContent: function() { var template = Y.Handlebars.compile(TEMPLATES.LIBRARY), library = this.get('library'), content = ''; // Helper to iterate over a newline separated string. Y.Handlebars.registerHelper('split', function(delimiter, str, options) { var parts, current, out; if (typeof delimiter === "undefined" || typeof str === "undefined") { return ''; } out = ''; parts = str.trim().split(delimiter); while (parts.length > 0) { current = parts.shift().trim(); out += options.fn(current); } return out; }); content = template({ elementid: this.get('host').get('elementid'), elementidescaped: this._escapeQuerySelector(this.get('host').get('elementid')), component: COMPONENTNAME, library: library, CSS: CSS, DELIMITERS: DELIMITERS }); var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; var params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: content }; var preview = Y.io(url, { sync: true, data: params, method: 'POST' }); if (preview.status === 200) { content = preview.responseText; } return content; }, /** * Escape special characters in string used as a JS query selector * * @method _excapeQuerySelector * @param {string} selector * @returns {string} */ _escapeQuerySelector: function(selector) { // Bootstrap requires that query selectors have special chars excaped. // See: https://getbootstrap.com/docs/4.2/getting-started/javascript/#selectors return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); } }, { ATTRS: { /** * Whether the TeX filter is currently active. * * @attribute texfilteractive * @type Boolean */ texfilteractive: { value: false }, /** * The contextid to use when generating this preview. * * @attribute contextid * @type String */ contextid: { value: null }, /** * The content of the example library. * * @attribute library * @type object */ library: { value: {} }, /** * The link to the Moodle Docs page about TeX. * * @attribute texdocsurl * @type string */ texdocsurl: { value: null } } }); }, '@VERSION@', { "requires": [ "moodle-editor_atto-plugin", "moodle-core-event", "io", "event-valuechange", "tabview", "array-extras" ] }); yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js 0000644 00000065422 15152212667 0023217 0 ustar 00 YUI.add('moodle-atto_equation-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 <http://www.gnu.org/licenses/>. /** * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Atto text editor equation plugin. */ /** * Atto equation editor. * * @namespace M.atto_equation * @class Button * @extends M.editor_atto.EditorPlugin */ var COMPONENTNAME = 'atto_equation', LOGNAME = 'atto_equation', CSS = { EQUATION_TEXT: 'atto_equation_equation', EQUATION_PREVIEW: 'atto_equation_preview', SUBMIT: 'atto_equation_submit', LIBRARY: 'atto_equation_library', LIBRARY_GROUPS: 'atto_equation_groups', LIBRARY_GROUP_PREFIX: 'atto_equation_group' }, SELECTORS = { LIBRARY: '.' + CSS.LIBRARY, LIBRARY_GROUP: '.' + CSS.LIBRARY_GROUPS + ' > div > div', EQUATION_TEXT: '.' + CSS.EQUATION_TEXT, EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW, SUBMIT: '.' + CSS.SUBMIT, LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button' }, DELIMITERS = { START: '\\(', END: '\\)' }, TEMPLATES = { FORM: '' + '<form class="atto_form">' + '{{{library}}}' + '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' + '<textarea class="fullwidth text-ltr {{CSS.EQUATION_TEXT}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' + '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' + '<div describedby="{{elementid}}_cursorinfo" ' + 'class="border rounded bg-light p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' + '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' + '<div class="mdl-align">' + '<br/>' + '<button class="btn btn-secondary {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' + '</div>' + '</form>', LIBRARY: '' + '<div class="{{CSS.LIBRARY}}">' + '<ul class="root nav nav-tabs mb-1" role="tablist">' + '{{#each library}}' + '<li class="nav-item">' + '<a class="nav-link{{#active}} active{{/active}}" ' + '{{#active}}aria-selected="true"{{/active}}' + '{{^active}}aria-selected="false" tabindex="-1"{{/active}}' + ' href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' + ' data-target="#{{../elementidescaped}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}"' + ' role="tab" data-toggle="tab">' + '{{get_string groupname ../component}}' + '</a>' + '</li>' + '{{/each}}' + '</ul>' + '<div class="tab-content mb-1 {{CSS.LIBRARY_GROUPS}}">' + '{{#each library}}' + '<div data-medium-type="{{CSS.LINK}}" class="tab-pane{{#active}} active{{/active}}" ' + 'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' + '<div role="toolbar">' + '{{#split "\n" elements}}' + '<button class="btn btn-secondary" tabindex="-1" data-tex="{{this}}"' + 'aria-label="{{this}}" title="{{this}}">' + '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' + '</button>' + '{{/split}}' + '</div>' + '</div>' + '{{/each}}' + '</div>' + '</div>' }; Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { /** * The selection object returned by the browser. * * @property _currentSelection * @type Range * @default null * @private */ _currentSelection: null, /** * The cursor position in the equation textarea. * * @property _lastCursorPos * @type Number * @default 0 * @private */ _lastCursorPos: 0, /** * A reference to the dialogue content. * * @property _content * @type Node * @private */ _content: null, /** * The source equation we are editing in the text. * * @property _sourceEquation * @type Object * @private */ _sourceEquation: null, /** * A reference to the tab focus set on each group. * * The keys are the IDs of the group, the value is the Node on which the focus is set. * * @property _groupFocus * @type Object * @private */ _groupFocus: null, /** * Regular Expression patterns used to pick out the equations in a String. * * @property _equationPatterns * @type Array * @private */ _equationPatterns: [ // We use space or not space because . does not match new lines. // $$ blah $$. /\$\$([\S\s]+?)\$\$/, // E.g. "\( blah \)". /\\\(([\S\s]+?)\\\)/, // E.g. "\[ blah \]". /\\\[([\S\s]+?)\\\]/, // E.g. "[tex] blah [/tex]". /\[tex\]([\S\s]+?)\[\/tex\]/ ], initializer: function() { this._groupFocus = {}; // If there is a tex filter active - enable this button. if (this.get('texfilteractive')) { // Add the button to the toolbar. this.addButton({ icon: 'e/math', callback: this._displayDialogue }); // We need custom highlight logic for this button. this.get('host').on('atto:selectionchanged', function() { if (this._resolveEquation()) { this.highlightButtons(); } else { this.unHighlightButtons(); } }, this); // We need to convert these to a non dom node based format. this.editor.all('tex').each(function(texNode) { var replacement = Y.Node.create('<span>' + DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END + '</span>'); texNode.replace(replacement); }); } }, /** * Display the equation editor. * * @method _displayDialogue * @private */ _displayDialogue: function() { this._currentSelection = this.get('host').getSelection(); if (this._currentSelection === false) { return; } // This needs to be done before the dialogue is opened because the focus will shift to the dialogue. var equation = this._resolveEquation(); var dialogue = this.getDialogue({ headerContent: M.util.get_string('pluginname', COMPONENTNAME), focusAfterHide: true, width: 600, focusOnShowSelector: SELECTORS.LIBRARY_BUTTON }); var content = this._getDialogueContent(); dialogue.set('bodyContent', content); dialogue.show(); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode()); }); if (equation) { content.one(SELECTORS.EQUATION_TEXT).set('text', equation); } this._updatePreview(false); }, /** * If there is selected text and it is part of an equation, * extract the equation (and set it in the form). * * @method _resolveEquation * @private * @return {String|Boolean} The equation or false. */ _resolveEquation: function() { // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), selection = this.get('host').getSelection(), text, returnValue = false; // Prevent resolving equations when we don't have focus. if (!this.get('host').isActive()) { return false; } // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. if (!selection || selection.length === 0) { return false; } this.sourceEquation = null; selection = selection[0]; text = Y.one(selectedNode).get('text'); // For each of these patterns we have a RegExp which captures the inner component of the equation but also // includes the delimiters. // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire // equation including delimiters and returning one entry per match of the whole equation. // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the // match results. Y.Array.find(this._equationPatterns, function(pattern) { // For each pattern in turn, find all whole matches (including the delimiters). var patternMatches = text.match(new RegExp(pattern.source, "g")); if (patternMatches && patternMatches.length) { // This pattern matches at least once. See if this pattern matches our current position. // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent // searches which is the required behaviour of this function. return Y.Array.find(patternMatches, function(match) { // Check each occurrence of this match. var startIndex = 0; while (text.indexOf(match, startIndex) !== -1) { // Determine whether the cursor is in the current occurrence of this string. // Note: We do not support a selection exceeding the bounds of an equation. var startOuter = text.indexOf(match, startIndex), endOuter = startOuter + match.length, startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); if (startMatch && endMatch) { // This match is in our current position - fetch the innerMatch data. var innerMatch = match.match(pattern); if (innerMatch && innerMatch.length) { // We need the start and end of the inner match for later. var startInner = text.indexOf(innerMatch[1], startOuter), endInner = startInner + innerMatch[1].length; // We'll be returning the inner match for use in the editor itself. returnValue = innerMatch[1]; // Save all data for later. this.sourceEquation = { // Outer match data. startOuterPosition: startOuter, endOuterPosition: endOuter, outerMatch: match, // Inner match data. startInnerPosition: startInner, endInnerPosition: endInner, innerMatch: innerMatch }; // This breaks out of both Y.Array.find functions. return true; } } // Update the startIndex to match the end of the current match so that we can continue hunting // for further matches. startIndex = endOuter; } }, this); } }, this); // We trim the equation when we load it and then add spaces when we save it. if (returnValue !== false) { returnValue = returnValue.trim(); } return returnValue; }, /** * Handle insertion of a new equation, or update of an existing one. * * @method _setEquation * @param {EventFacade} e * @private */ _setEquation: function(e) { var input, selectedNode, text, value, host, newText; host = this.get('host'); e.preventDefault(); this.getDialogue({ focusAfterHide: null }).hide(); input = e.currentTarget.ancestor('.atto_form').one('textarea'); value = input.get('value'); if (value !== '') { host.setSelection(this._currentSelection); if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); value = ' ' + value + ' '; newText = text.slice(0, this.sourceEquation.startInnerPosition) + value + text.slice(this.sourceEquation.endInnerPosition); selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; host.insertContentAtFocusPoint(value); } // Clean the YUI ids from the HTML. this.markUpdated(); } }, /** * Smart throttle, only call a function every delay milli seconds, * and always run the last call. Y.throttle does not work here, * because it calls the function immediately, the first time, and then * ignores repeated calls within X seconds. This does not guarantee * that the last call will be executed (which is required here). * * @param {function} fn * @param {Number} delay Delay in milliseconds * @method _throttle * @private */ _throttle: function(fn, delay) { var timer = null; return function() { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(context, args); }, delay); }; }, /** * Update the preview div to match the current equation. * * @param {EventFacade} e * @method _updatePreview * @private */ _updatePreview: function(e) { var textarea = this._content.one(SELECTORS.EQUATION_TEXT), equation = textarea.get('value'), url, currentPos = textarea.get('selectionStart'), prefix = '', cursorLatex = '\\Downarrow ', isChar, params; if (e) { e.preventDefault(); } // Move the cursor so it does not break expressions. // Start at the very beginning. if (!currentPos) { currentPos = 0; } // First move back to the beginning of the line. while (equation.charAt(currentPos) === '\\' && currentPos >= 0) { currentPos -= 1; } isChar = /[a-zA-Z\{]/; if (currentPos !== 0) { if (equation.charAt(currentPos - 1) != '{') { // Now match to the end of the line. while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length && isChar.test(equation.charAt(currentPos - 1))) { currentPos += 1; } } } // Save the cursor position - for insertion from the library. this._lastCursorPos = currentPos; equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos); equation = DELIMITERS.START + ' ' + equation + ' ' + DELIMITERS.END; // Make an ajax request to the filter. url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: equation }; Y.io(url, { context: this, data: params, timeout: 500, on: { complete: this._loadPreview } }); }, /** * Load returned preview text into preview * * @param {String} id * @param {EventFacade} e * @method _loadPreview * @private */ _loadPreview: function(id, preview) { var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW); if (preview.status === 200) { previewNode.setHTML(preview.responseText); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(previewNode.getDOMNode()); }); } }, /** * Return the dialogue content for the tool, attaching any required * events. * * @method _getDialogueContent * @return {Node} * @private */ _getDialogueContent: function() { var library = this._getLibraryContent(), throttledUpdate = this._throttle(this._updatePreview, 500), template = Y.Handlebars.compile(TEMPLATES.FORM); this._content = Y.Node.create(template({ elementid: this.get('host').get('elementid'), component: COMPONENTNAME, library: library, texdocsurl: this.get('texdocsurl'), CSS: CSS })); // Sets the default focus. this._content.all(SELECTORS.LIBRARY_GROUP).each(function(group) { // The first button gets the focus. this._setGroupTabFocus(group, group.one('button')); // Sometimes the filter adds an anchor in the button, no tabindex on that. group.all('button a').setAttribute('tabindex', '-1'); }, this); // Keyboard navigation in groups. this._content.delegate('key', this._groupNavigation, 'down:37,39', SELECTORS.LIBRARY_BUTTON, this); this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this); this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', throttledUpdate, this); this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this); return this._content; }, /** * Callback handling the keyboard navigation in the groups of the library. * * @param {EventFacade} e The event. * @method _groupNavigation * @private */ _groupNavigation: function(e) { e.preventDefault(); var current = e.currentTarget, parent = current.get('parentNode'), // This must be the <div> containing all the buttons of the group. buttons = parent.all('button'), direction = e.keyCode !== 37 ? 1 : -1, index = buttons.indexOf(current), nextButton; if (index < 0) { Y.log('Unable to find the current button in the list of buttons', 'debug', LOGNAME); index = 0; } index += direction; if (index < 0) { index = buttons.size() - 1; } else if (index >= buttons.size()) { index = 0; } nextButton = buttons.item(index); this._setGroupTabFocus(parent, nextButton); nextButton.focus(); }, /** * Sets tab focus for the group. * * @method _setGroupTabFocus * @param {Node} button The node that focus should now be set to. * @private */ _setGroupTabFocus: function(parent, button) { var parentId = parent.generateID(); // Unset the previous entry. if (typeof this._groupFocus[parentId] !== 'undefined') { this._groupFocus[parentId].setAttribute('tabindex', '-1'); } // Set on the new entry. this._groupFocus[parentId] = button; button.setAttribute('tabindex', 0); parent.setAttribute('aria-activedescendant', button.generateID()); }, /** * Reponse to button presses in the TeX library panels. * * @method _selectLibraryItem * @param {EventFacade} e * @return {string} * @private */ _selectLibraryItem: function(e) { var tex = e.currentTarget.getAttribute('data-tex'), oldValue, newValue, input, focusPoint = 0; e.preventDefault(); // Set the group focus on the button. this._setGroupTabFocus(e.currentTarget.get('parentNode'), e.currentTarget); input = e.currentTarget.ancestor('.atto_form').one('textarea'); oldValue = input.get('value'); newValue = oldValue.substring(0, this._lastCursorPos); if (newValue.charAt(newValue.length - 1) !== ' ') { newValue += ' '; } newValue += tex; focusPoint = newValue.length; if (oldValue.charAt(this._lastCursorPos) !== ' ') { newValue += ' '; } newValue += oldValue.substring(this._lastCursorPos, oldValue.length); input.set('value', newValue); input.focus(); var realInput = input.getDOMNode(); if (typeof realInput.selectionStart === "number") { // Modern browsers have selectionStart and selectionEnd to control the cursor position. realInput.selectionStart = realInput.selectionEnd = focusPoint; } else if (typeof realInput.createTextRange !== "undefined") { // Legacy browsers (IE<=9) use createTextRange(). var range = realInput.createTextRange(); range.moveToPoint(focusPoint); range.select(); } // Focus must be set before updating the preview for the cursor box to be in the correct location. this._updatePreview(false); }, /** * Return the HTML for rendering the library of predefined buttons. * * @method _getLibraryContent * @return {string} * @private */ _getLibraryContent: function() { var template = Y.Handlebars.compile(TEMPLATES.LIBRARY), library = this.get('library'), content = ''; // Helper to iterate over a newline separated string. Y.Handlebars.registerHelper('split', function(delimiter, str, options) { var parts, current, out; if (typeof delimiter === "undefined" || typeof str === "undefined") { Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button'); return ''; } out = ''; parts = str.trim().split(delimiter); while (parts.length > 0) { current = parts.shift().trim(); out += options.fn(current); } return out; }); content = template({ elementid: this.get('host').get('elementid'), elementidescaped: this._escapeQuerySelector(this.get('host').get('elementid')), component: COMPONENTNAME, library: library, CSS: CSS, DELIMITERS: DELIMITERS }); var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; var params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: content }; var preview = Y.io(url, { sync: true, data: params, method: 'POST' }); if (preview.status === 200) { content = preview.responseText; } return content; }, /** * Escape special characters in string used as a JS query selector * * @method _excapeQuerySelector * @param {string} selector * @returns {string} */ _escapeQuerySelector: function(selector) { // Bootstrap requires that query selectors have special chars excaped. // See: https://getbootstrap.com/docs/4.2/getting-started/javascript/#selectors return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); } }, { ATTRS: { /** * Whether the TeX filter is currently active. * * @attribute texfilteractive * @type Boolean */ texfilteractive: { value: false }, /** * The contextid to use when generating this preview. * * @attribute contextid * @type String */ contextid: { value: null }, /** * The content of the example library. * * @attribute library * @type object */ library: { value: {} }, /** * The link to the Moodle Docs page about TeX. * * @attribute texdocsurl * @type string */ texdocsurl: { value: null } } }); }, '@VERSION@', { "requires": [ "moodle-editor_atto-plugin", "moodle-core-event", "io", "event-valuechange", "tabview", "array-extras" ] }); yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js 0000644 00000020757 15152212667 0022716 0 ustar 00 YUI.add("moodle-atto_equation-button",function(a,t){var n="atto_equation",o={EQUATION_TEXT:"atto_equation_equation",EQUATION_PREVIEW:"atto_equation_preview",SUBMIT:"atto_equation_submit",LIBRARY:"atto_equation_library",LIBRARY_GROUPS:"atto_equation_groups",LIBRARY_GROUP_PREFIX:"atto_equation_group"},s={LIBRARY:"."+o.LIBRARY,LIBRARY_GROUP:"."+o.LIBRARY_GROUPS+" > div > div",EQUATION_TEXT:"."+o.EQUATION_TEXT,EQUATION_PREVIEW:"."+o.EQUATION_PREVIEW,SUBMIT:"."+o.SUBMIT,LIBRARY_BUTTON:"."+o.LIBRARY+" button"},r={START:"\\(",END:"\\)"},u='<form class="atto_form">{{{library}}}<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label><textarea class="fullwidth text-ltr {{CSS.EQUATION_TEXT}}" id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/><label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label><div describedby="{{elementid}}_cursorinfo" class="border rounded bg-light p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div><div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div><div class="mdl-align"><br/><button class="btn btn-secondary {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button></div></form>',l='<div class="{{CSS.LIBRARY}}"><ul class="root nav nav-tabs mb-1" role="tablist">{{#each library}}<li class="nav-item"><a class="nav-link{{#active}} active{{/active}}" {{#active}}aria-selected="true"{{/active}}{{^active}}aria-selected="false" tabindex="-1"{{/active}} href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" data-target="#{{../elementidescaped}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" role="tab" data-toggle="tab">{{get_string groupname ../component}}</a></li>{{/each}}</ul><div class="tab-content mb-1 {{CSS.LIBRARY_GROUPS}}">{{#each library}}<div data-medium-type="{{CSS.LINK}}" class="tab-pane{{#active}} active{{/active}}" id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}"><div role="toolbar">{{#split "\n" elements}}<button class="btn btn-secondary" tabindex="-1" data-tex="{{this}}"aria-label="{{this}}" title="{{this}}">{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}</button>{{/split}}</div></div>{{/each}}</div></div>';a.namespace("M.atto_equation").Button=a.Base.create("button",a.M.editor_atto.EditorPlugin,[],{_currentSelection:null,_lastCursorPos:0,_content:null,_sourceEquation:null,_groupFocus:null,_equationPatterns:[/\$\$([\S\s]+?)\$\$/,/\\\(([\S\s]+?)\\\)/,/\\\[([\S\s]+?)\\\]/,/\[tex\]([\S\s]+?)\[\/tex\]/],initializer:function(){this._groupFocus={},this.get("texfilteractive")&&(this.addButton({icon:"e/math",callback:this._displayDialogue}),this.get("host").on("atto:selectionchanged",function(){this._resolveEquation()?this.highlightButtons():this.unHighlightButtons()},this),this.editor.all("tex").each(function(t){var e=a.Node.create("<span>"+r.START+" "+t.get("text")+" "+r.END+"</span>");t.replace(e)}))},_displayDialogue:function(){var t,e,i;this._currentSelection=this.get("host").getSelection(),!1!==this._currentSelection&&(t=this._resolveEquation(),e=this.getDialogue({headerContent:M.util.get_string("pluginname",n),focusAfterHide:!0,width:600,focusOnShowSelector:s.LIBRARY_BUTTON}),i=this._getDialogueContent(),e.set("bodyContent",i),e.show(),require(["core/event"],function(t){t.notifyFilterContentUpdated(e.get("boundingBox").getDOMNode())}),t&&i.one(s.EQUATION_TEXT).set("text",t),this._updatePreview(!1))},_resolveEquation:function(){var u,t=this.get("host").getSelectionParentNode(),l=this.get("host").getSelection(),c=!1;return!!this.get("host").isActive()&&(!!t&&(!(!l||0===l.length)&&(this.sourceEquation=null,l=l[0],u=a.one(t).get("text"),a.Array.find(this._equationPatterns,function(r){var t=u.match(new RegExp(r.source,"g"));if(t&&t.length)return a.Array.find(t,function(t){for(var e,i,n,o,a,s=0;-1!==u.indexOf(t,s);){if(i=(e=u.indexOf(t,s))+t.length,o=l.startOffset>=e&&l.startOffset<i,a=l.endOffset<=i&&l.endOffset>e,o&&a&&(n=t.match(r))&&n.length)return a=(o=u.indexOf(n[1],e))+n[1].length,c=n[1],this.sourceEquation={startOuterPosition:e,endOuterPosition:i,outerMatch:t,startInnerPosition:o,endInnerPosition:a,innerMatch:n},!0;s=i}},this)},this),c=!1!==c?c.trim():c)))},_setEquation:function(t){var e,i,n=this.get("host");t.preventDefault(),this.getDialogue({focusAfterHide:null}).hide(),""!==(t=t.currentTarget.ancestor(".atto_form").one("textarea").get("value"))&&(n.setSelection(this._currentSelection),this.sourceEquation?(t=" "+t+" ",i=(i=(e=a.one(n.getSelectionParentNode())).get("text")).slice(0,this.sourceEquation.startInnerPosition)+t+i.slice(this.sourceEquation.endInnerPosition),e.set("text",i)):n.insertContentAtFocusPoint(t=r.START+" "+t+" "+r.END),this.markUpdated())},_throttle:function(i,n){var o=null;return function(){var t=this,e=arguments;clearTimeout(o),o=setTimeout(function(){i.apply(t,e)},n)}},_updatePreview:function(t){var e,i=this._content.one(s.EQUATION_TEXT),n=i.get("value"),o=i.get("selectionStart");for(t&&t.preventDefault(),o=o||0;"\\"===n.charAt(o)&&0<=o;)--o;if(e=/[a-zA-Z\{]/,0!==o&&"{"!=n.charAt(o-1))for(;e.test(n.charAt(o))&&o<n.length&&e.test(n.charAt(o-1));)o+=1;this._lastCursorPos=o,n=n.substring(0,o)+"\\Downarrow "+n.substring(o),n=r.START+" "+n+" "+r.END,i=M.cfg.wwwroot+"/lib/editor/atto/plugins/equation/ajax.php",t={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"filtertext",text:n},a.io(i,{context:this,data:t,timeout:500,on:{complete:this._loadPreview}})},_loadPreview:function(t,e){var i=this._content.one(s.EQUATION_PREVIEW);200===e.status&&(i.setHTML(e.responseText),require(["core/event"],function(t){t.notifyFilterContentUpdated(i.getDOMNode())}))},_getDialogueContent:function(){var t=this._getLibraryContent(),e=this._throttle(this._updatePreview,500),i=a.Handlebars.compile(u);return this._content=a.Node.create(i({elementid:this.get("host").get("elementid"),component:n,library:t,texdocsurl:this.get("texdocsurl"),CSS:o})), this._content.all(s.LIBRARY_GROUP).each(function(t){this._setGroupTabFocus(t,t.one("button")),t.all("button a").setAttribute("tabindex","-1")},this),this._content.delegate("key",this._groupNavigation,"down:37,39",s.LIBRARY_BUTTON,this),this._content.one(s.SUBMIT).on("click",this._setEquation,this),this._content.one(s.EQUATION_TEXT).on("valuechange",e,this),this._content.one(s.EQUATION_TEXT).on("mouseup",e,this),this._content.one(s.EQUATION_TEXT).on("keyup",e,this),this._content.delegate("click",this._selectLibraryItem,s.LIBRARY_BUTTON,this),this._content},_groupNavigation:function(t){t.preventDefault();var e=t.currentTarget,i=e.get("parentNode"),n=i.all("button"),t=37!==t.keyCode?1:-1,e=n.indexOf(e);e<0&&(e=0),(e+=t)<0?e=n.size()-1:e>=n.size()&&(e=0),t=n.item(e),this._setGroupTabFocus(i,t),t.focus()},_setGroupTabFocus:function(t,e){var i=t.generateID();"undefined"!=typeof this._groupFocus[i]&&this._groupFocus[i].setAttribute("tabindex","-1"),(this._groupFocus[i]=e).setAttribute("tabindex",0),t.setAttribute("aria-activedescendant",e.generateID())},_selectLibraryItem:function(t){var e,i,n=t.currentTarget.getAttribute("data-tex");t.preventDefault(),this._setGroupTabFocus(t.currentTarget.get("parentNode"),t.currentTarget)," "!==(i=(e=(t=t.currentTarget.ancestor(".atto_form").one("textarea")).get("value")).substring(0,this._lastCursorPos)).charAt(i.length-1)&&(i+=" "),n=(i+=n).length," "!==e.charAt(this._lastCursorPos)&&(i+=" "),i+=e.substring(this._lastCursorPos,e.length),t.set("value",i),t.focus(),"number"==typeof(e=t.getDOMNode()).selectionStart?e.selectionStart=e.selectionEnd=n:"undefined"!=typeof e.createTextRange&&((i=e.createTextRange()).moveToPoint(n),i.select()),this._updatePreview(!1)},_getLibraryContent:function(){var t=a.Handlebars.compile(l),e=this.get("library"),i="";return a.Handlebars.registerHelper("split",function(t,e,i){var n,o,a;if(void 0===t||void 0===e)return"";for(a="",n=e.trim().split(t);0<n.length;)o=n.shift().trim(),a+=i.fn(o);return a}),i=t({elementid:this.get("host").get("elementid"),elementidescaped:this._escapeQuerySelector(this.get("host").get("elementid")),component:n,library:e,CSS:o,DELIMITERS:r}),e=M.cfg.wwwroot+"/lib/editor/atto/plugins/equation/ajax.php",t={sesskey:M.cfg.sesskey,contextid:this.get("contextid"),action:"filtertext",text:i},i=200===(e=a.io(e,{sync:!0,data:t,method:"POST"})).status?e.responseText:i},_escapeQuerySelector:function(t){return t.replace(/(:|\.|\[|\]|,|=|@)/g,"\\$1")}},{ATTRS:{texfilteractive:{value:!1},contextid:{value:null},library:{value:{}},texdocsurl:{value:null}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin","moodle-core-event","io","event-valuechange","tabview","array-extras"]}); yui/src/button/meta/button.json 0000644 00000000376 15152212667 0012625 0 ustar 00 { "moodle-atto_equation-button": { "requires": [ "moodle-editor_atto-plugin", "moodle-core-event", "io", "event-valuechange", "tabview", "array-extras" ] } } yui/src/button/build.json 0000644 00000000231 15152212667 0011451 0 ustar 00 { "name": "moodle-atto_equation-button", "builds": { "moodle-atto_equation-button": { "jsfiles": [ "button.js" ] } } } yui/src/button/js/button.js 0000644 00000065016 15152212667 0011760 0 ustar 00 // 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 <http://www.gnu.org/licenses/>. /** * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Atto text editor equation plugin. */ /** * Atto equation editor. * * @namespace M.atto_equation * @class Button * @extends M.editor_atto.EditorPlugin */ var COMPONENTNAME = 'atto_equation', LOGNAME = 'atto_equation', CSS = { EQUATION_TEXT: 'atto_equation_equation', EQUATION_PREVIEW: 'atto_equation_preview', SUBMIT: 'atto_equation_submit', LIBRARY: 'atto_equation_library', LIBRARY_GROUPS: 'atto_equation_groups', LIBRARY_GROUP_PREFIX: 'atto_equation_group' }, SELECTORS = { LIBRARY: '.' + CSS.LIBRARY, LIBRARY_GROUP: '.' + CSS.LIBRARY_GROUPS + ' > div > div', EQUATION_TEXT: '.' + CSS.EQUATION_TEXT, EQUATION_PREVIEW: '.' + CSS.EQUATION_PREVIEW, SUBMIT: '.' + CSS.SUBMIT, LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button' }, DELIMITERS = { START: '\\(', END: '\\)' }, TEMPLATES = { FORM: '' + '<form class="atto_form">' + '{{{library}}}' + '<label for="{{elementid}}_{{CSS.EQUATION_TEXT}}">{{{get_string "editequation" component texdocsurl}}}</label>' + '<textarea class="fullwidth text-ltr {{CSS.EQUATION_TEXT}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' + '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' + '<div describedby="{{elementid}}_cursorinfo" ' + 'class="border rounded bg-light p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' + 'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' + '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' + '<div class="mdl-align">' + '<br/>' + '<button class="btn btn-secondary {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' + '</div>' + '</form>', LIBRARY: '' + '<div class="{{CSS.LIBRARY}}">' + '<ul class="root nav nav-tabs mb-1" role="tablist">' + '{{#each library}}' + '<li class="nav-item">' + '<a class="nav-link{{#active}} active{{/active}}" ' + '{{#active}}aria-selected="true"{{/active}}' + '{{^active}}aria-selected="false" tabindex="-1"{{/active}}' + ' href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' + ' data-target="#{{../elementidescaped}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}"' + ' role="tab" data-toggle="tab">' + '{{get_string groupname ../component}}' + '</a>' + '</li>' + '{{/each}}' + '</ul>' + '<div class="tab-content mb-1 {{CSS.LIBRARY_GROUPS}}">' + '{{#each library}}' + '<div data-medium-type="{{CSS.LINK}}" class="tab-pane{{#active}} active{{/active}}" ' + 'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' + '<div role="toolbar">' + '{{#split "\n" elements}}' + '<button class="btn btn-secondary" tabindex="-1" data-tex="{{this}}"' + 'aria-label="{{this}}" title="{{this}}">' + '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' + '</button>' + '{{/split}}' + '</div>' + '</div>' + '{{/each}}' + '</div>' + '</div>' }; Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { /** * The selection object returned by the browser. * * @property _currentSelection * @type Range * @default null * @private */ _currentSelection: null, /** * The cursor position in the equation textarea. * * @property _lastCursorPos * @type Number * @default 0 * @private */ _lastCursorPos: 0, /** * A reference to the dialogue content. * * @property _content * @type Node * @private */ _content: null, /** * The source equation we are editing in the text. * * @property _sourceEquation * @type Object * @private */ _sourceEquation: null, /** * A reference to the tab focus set on each group. * * The keys are the IDs of the group, the value is the Node on which the focus is set. * * @property _groupFocus * @type Object * @private */ _groupFocus: null, /** * Regular Expression patterns used to pick out the equations in a String. * * @property _equationPatterns * @type Array * @private */ _equationPatterns: [ // We use space or not space because . does not match new lines. // $$ blah $$. /\$\$([\S\s]+?)\$\$/, // E.g. "\( blah \)". /\\\(([\S\s]+?)\\\)/, // E.g. "\[ blah \]". /\\\[([\S\s]+?)\\\]/, // E.g. "[tex] blah [/tex]". /\[tex\]([\S\s]+?)\[\/tex\]/ ], initializer: function() { this._groupFocus = {}; // If there is a tex filter active - enable this button. if (this.get('texfilteractive')) { // Add the button to the toolbar. this.addButton({ icon: 'e/math', callback: this._displayDialogue }); // We need custom highlight logic for this button. this.get('host').on('atto:selectionchanged', function() { if (this._resolveEquation()) { this.highlightButtons(); } else { this.unHighlightButtons(); } }, this); // We need to convert these to a non dom node based format. this.editor.all('tex').each(function(texNode) { var replacement = Y.Node.create('<span>' + DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END + '</span>'); texNode.replace(replacement); }); } }, /** * Display the equation editor. * * @method _displayDialogue * @private */ _displayDialogue: function() { this._currentSelection = this.get('host').getSelection(); if (this._currentSelection === false) { return; } // This needs to be done before the dialogue is opened because the focus will shift to the dialogue. var equation = this._resolveEquation(); var dialogue = this.getDialogue({ headerContent: M.util.get_string('pluginname', COMPONENTNAME), focusAfterHide: true, width: 600, focusOnShowSelector: SELECTORS.LIBRARY_BUTTON }); var content = this._getDialogueContent(); dialogue.set('bodyContent', content); dialogue.show(); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(dialogue.get('boundingBox').getDOMNode()); }); if (equation) { content.one(SELECTORS.EQUATION_TEXT).set('text', equation); } this._updatePreview(false); }, /** * If there is selected text and it is part of an equation, * extract the equation (and set it in the form). * * @method _resolveEquation * @private * @return {String|Boolean} The equation or false. */ _resolveEquation: function() { // Find the equation in the surrounding text. var selectedNode = this.get('host').getSelectionParentNode(), selection = this.get('host').getSelection(), text, returnValue = false; // Prevent resolving equations when we don't have focus. if (!this.get('host').isActive()) { return false; } // Note this is a document fragment and YUI doesn't like them. if (!selectedNode) { return false; } // We don't yet have a cursor selection somehow so we can't possible be resolving an equation that has selection. if (!selection || selection.length === 0) { return false; } this.sourceEquation = null; selection = selection[0]; text = Y.one(selectedNode).get('text'); // For each of these patterns we have a RegExp which captures the inner component of the equation but also // includes the delimiters. // We first run the RegExp adding the global flag ("g"). This ignores the capture, instead matching the entire // equation including delimiters and returning one entry per match of the whole equation. // We have to deal with multiple occurences of the same equation in a String so must be able to loop on the // match results. Y.Array.find(this._equationPatterns, function(pattern) { // For each pattern in turn, find all whole matches (including the delimiters). var patternMatches = text.match(new RegExp(pattern.source, "g")); if (patternMatches && patternMatches.length) { // This pattern matches at least once. See if this pattern matches our current position. // Note: We return here to break the Y.Array.find loop - any truthy return will stop any subsequent // searches which is the required behaviour of this function. return Y.Array.find(patternMatches, function(match) { // Check each occurrence of this match. var startIndex = 0; while (text.indexOf(match, startIndex) !== -1) { // Determine whether the cursor is in the current occurrence of this string. // Note: We do not support a selection exceeding the bounds of an equation. var startOuter = text.indexOf(match, startIndex), endOuter = startOuter + match.length, startMatch = (selection.startOffset >= startOuter && selection.startOffset < endOuter), endMatch = (selection.endOffset <= endOuter && selection.endOffset > startOuter); if (startMatch && endMatch) { // This match is in our current position - fetch the innerMatch data. var innerMatch = match.match(pattern); if (innerMatch && innerMatch.length) { // We need the start and end of the inner match for later. var startInner = text.indexOf(innerMatch[1], startOuter), endInner = startInner + innerMatch[1].length; // We'll be returning the inner match for use in the editor itself. returnValue = innerMatch[1]; // Save all data for later. this.sourceEquation = { // Outer match data. startOuterPosition: startOuter, endOuterPosition: endOuter, outerMatch: match, // Inner match data. startInnerPosition: startInner, endInnerPosition: endInner, innerMatch: innerMatch }; // This breaks out of both Y.Array.find functions. return true; } } // Update the startIndex to match the end of the current match so that we can continue hunting // for further matches. startIndex = endOuter; } }, this); } }, this); // We trim the equation when we load it and then add spaces when we save it. if (returnValue !== false) { returnValue = returnValue.trim(); } return returnValue; }, /** * Handle insertion of a new equation, or update of an existing one. * * @method _setEquation * @param {EventFacade} e * @private */ _setEquation: function(e) { var input, selectedNode, text, value, host, newText; host = this.get('host'); e.preventDefault(); this.getDialogue({ focusAfterHide: null }).hide(); input = e.currentTarget.ancestor('.atto_form').one('textarea'); value = input.get('value'); if (value !== '') { host.setSelection(this._currentSelection); if (this.sourceEquation) { // Replace the equation. selectedNode = Y.one(host.getSelectionParentNode()); text = selectedNode.get('text'); value = ' ' + value + ' '; newText = text.slice(0, this.sourceEquation.startInnerPosition) + value + text.slice(this.sourceEquation.endInnerPosition); selectedNode.set('text', newText); } else { // Insert the new equation. value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END; host.insertContentAtFocusPoint(value); } // Clean the YUI ids from the HTML. this.markUpdated(); } }, /** * Smart throttle, only call a function every delay milli seconds, * and always run the last call. Y.throttle does not work here, * because it calls the function immediately, the first time, and then * ignores repeated calls within X seconds. This does not guarantee * that the last call will be executed (which is required here). * * @param {function} fn * @param {Number} delay Delay in milliseconds * @method _throttle * @private */ _throttle: function(fn, delay) { var timer = null; return function() { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function() { fn.apply(context, args); }, delay); }; }, /** * Update the preview div to match the current equation. * * @param {EventFacade} e * @method _updatePreview * @private */ _updatePreview: function(e) { var textarea = this._content.one(SELECTORS.EQUATION_TEXT), equation = textarea.get('value'), url, currentPos = textarea.get('selectionStart'), prefix = '', cursorLatex = '\\Downarrow ', isChar, params; if (e) { e.preventDefault(); } // Move the cursor so it does not break expressions. // Start at the very beginning. if (!currentPos) { currentPos = 0; } // First move back to the beginning of the line. while (equation.charAt(currentPos) === '\\' && currentPos >= 0) { currentPos -= 1; } isChar = /[a-zA-Z\{]/; if (currentPos !== 0) { if (equation.charAt(currentPos - 1) != '{') { // Now match to the end of the line. while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length && isChar.test(equation.charAt(currentPos - 1))) { currentPos += 1; } } } // Save the cursor position - for insertion from the library. this._lastCursorPos = currentPos; equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos); equation = DELIMITERS.START + ' ' + equation + ' ' + DELIMITERS.END; // Make an ajax request to the filter. url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: equation }; Y.io(url, { context: this, data: params, timeout: 500, on: { complete: this._loadPreview } }); }, /** * Load returned preview text into preview * * @param {String} id * @param {EventFacade} e * @method _loadPreview * @private */ _loadPreview: function(id, preview) { var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW); if (preview.status === 200) { previewNode.setHTML(preview.responseText); // Notify the filters about the modified nodes. require(['core/event'], function(event) { event.notifyFilterContentUpdated(previewNode.getDOMNode()); }); } }, /** * Return the dialogue content for the tool, attaching any required * events. * * @method _getDialogueContent * @return {Node} * @private */ _getDialogueContent: function() { var library = this._getLibraryContent(), throttledUpdate = this._throttle(this._updatePreview, 500), template = Y.Handlebars.compile(TEMPLATES.FORM); this._content = Y.Node.create(template({ elementid: this.get('host').get('elementid'), component: COMPONENTNAME, library: library, texdocsurl: this.get('texdocsurl'), CSS: CSS })); // Sets the default focus. this._content.all(SELECTORS.LIBRARY_GROUP).each(function(group) { // The first button gets the focus. this._setGroupTabFocus(group, group.one('button')); // Sometimes the filter adds an anchor in the button, no tabindex on that. group.all('button a').setAttribute('tabindex', '-1'); }, this); // Keyboard navigation in groups. this._content.delegate('key', this._groupNavigation, 'down:37,39', SELECTORS.LIBRARY_BUTTON, this); this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this); this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', throttledUpdate, this); this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', throttledUpdate, this); this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this); return this._content; }, /** * Callback handling the keyboard navigation in the groups of the library. * * @param {EventFacade} e The event. * @method _groupNavigation * @private */ _groupNavigation: function(e) { e.preventDefault(); var current = e.currentTarget, parent = current.get('parentNode'), // This must be the <div> containing all the buttons of the group. buttons = parent.all('button'), direction = e.keyCode !== 37 ? 1 : -1, index = buttons.indexOf(current), nextButton; if (index < 0) { Y.log('Unable to find the current button in the list of buttons', 'debug', LOGNAME); index = 0; } index += direction; if (index < 0) { index = buttons.size() - 1; } else if (index >= buttons.size()) { index = 0; } nextButton = buttons.item(index); this._setGroupTabFocus(parent, nextButton); nextButton.focus(); }, /** * Sets tab focus for the group. * * @method _setGroupTabFocus * @param {Node} button The node that focus should now be set to. * @private */ _setGroupTabFocus: function(parent, button) { var parentId = parent.generateID(); // Unset the previous entry. if (typeof this._groupFocus[parentId] !== 'undefined') { this._groupFocus[parentId].setAttribute('tabindex', '-1'); } // Set on the new entry. this._groupFocus[parentId] = button; button.setAttribute('tabindex', 0); parent.setAttribute('aria-activedescendant', button.generateID()); }, /** * Reponse to button presses in the TeX library panels. * * @method _selectLibraryItem * @param {EventFacade} e * @return {string} * @private */ _selectLibraryItem: function(e) { var tex = e.currentTarget.getAttribute('data-tex'), oldValue, newValue, input, focusPoint = 0; e.preventDefault(); // Set the group focus on the button. this._setGroupTabFocus(e.currentTarget.get('parentNode'), e.currentTarget); input = e.currentTarget.ancestor('.atto_form').one('textarea'); oldValue = input.get('value'); newValue = oldValue.substring(0, this._lastCursorPos); if (newValue.charAt(newValue.length - 1) !== ' ') { newValue += ' '; } newValue += tex; focusPoint = newValue.length; if (oldValue.charAt(this._lastCursorPos) !== ' ') { newValue += ' '; } newValue += oldValue.substring(this._lastCursorPos, oldValue.length); input.set('value', newValue); input.focus(); var realInput = input.getDOMNode(); if (typeof realInput.selectionStart === "number") { // Modern browsers have selectionStart and selectionEnd to control the cursor position. realInput.selectionStart = realInput.selectionEnd = focusPoint; } else if (typeof realInput.createTextRange !== "undefined") { // Legacy browsers (IE<=9) use createTextRange(). var range = realInput.createTextRange(); range.moveToPoint(focusPoint); range.select(); } // Focus must be set before updating the preview for the cursor box to be in the correct location. this._updatePreview(false); }, /** * Return the HTML for rendering the library of predefined buttons. * * @method _getLibraryContent * @return {string} * @private */ _getLibraryContent: function() { var template = Y.Handlebars.compile(TEMPLATES.LIBRARY), library = this.get('library'), content = ''; // Helper to iterate over a newline separated string. Y.Handlebars.registerHelper('split', function(delimiter, str, options) { var parts, current, out; if (typeof delimiter === "undefined" || typeof str === "undefined") { Y.log('Handlebars split helper: String and delimiter are required.', 'debug', 'moodle-atto_equation-button'); return ''; } out = ''; parts = str.trim().split(delimiter); while (parts.length > 0) { current = parts.shift().trim(); out += options.fn(current); } return out; }); content = template({ elementid: this.get('host').get('elementid'), elementidescaped: this._escapeQuerySelector(this.get('host').get('elementid')), component: COMPONENTNAME, library: library, CSS: CSS, DELIMITERS: DELIMITERS }); var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php'; var params = { sesskey: M.cfg.sesskey, contextid: this.get('contextid'), action: 'filtertext', text: content }; var preview = Y.io(url, { sync: true, data: params, method: 'POST' }); if (preview.status === 200) { content = preview.responseText; } return content; }, /** * Escape special characters in string used as a JS query selector * * @method _excapeQuerySelector * @param {string} selector * @returns {string} */ _escapeQuerySelector: function(selector) { // Bootstrap requires that query selectors have special chars excaped. // See: https://getbootstrap.com/docs/4.2/getting-started/javascript/#selectors return selector.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); } }, { ATTRS: { /** * Whether the TeX filter is currently active. * * @attribute texfilteractive * @type Boolean */ texfilteractive: { value: false }, /** * The contextid to use when generating this preview. * * @attribute contextid * @type String */ contextid: { value: null }, /** * The content of the example library. * * @attribute library * @type object */ library: { value: {} }, /** * The link to the Moodle Docs page about TeX. * * @attribute texdocsurl * @type string */ texdocsurl: { value: null } } }); lang/en/atto_equation.php 0000644 00000003541 15152212667 0011466 0 ustar 00 <?php // 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 <http://www.gnu.org/licenses/>. /** * Strings for component 'atto_equation', language 'en'. * * @package atto_equation * @copyright 2013 Damyon Wiese <damyon@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['editequation'] = 'Edit equation using <a href="{$a}" target="_blank">TeX</a>'; $string['librarygroup1'] = 'Operators'; $string['librarygroup1_desc'] = 'TeX commands listed on the operators tab.'; $string['librarygroup2'] = 'Arrows'; $string['librarygroup2_desc'] = 'TeX commands listed on the arrows tab.'; $string['librarygroup3'] = 'Greek symbols'; $string['librarygroup3_desc'] = 'TeX commands listed on the Greek symbols tab.'; $string['librarygroup4'] = 'Advanced'; $string['librarygroup4_desc'] = 'TeX commands listed on the advanced tab.'; $string['pluginname'] = 'Equation editor'; $string['preview'] = 'Equation preview'; $string['cursorinfo'] = 'An arrow indicates the position that new elements from the element library will be inserted.'; $string['saveequation'] = 'Save equation'; $string['settings'] = 'Equation editor settings'; $string['update'] = 'Update'; $string['privacy:metadata'] = 'The atto_equation plugin does not store any personal data.';
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�