AutorÃa | Ultima modificación | Ver Log |
// 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/>./*** @module moodle-editor_atto-editor* @submodule selection*//*** Selection functions for the Atto editor.** See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.** @namespace M.editor_atto* @class EditorSelection*/function EditorSelection() {}EditorSelection.ATTRS = {};EditorSelection.prototype = {/*** List of saved selections per editor instance.** @property _selections* @private*/_selections: null,/*** A unique identifier for the last selection recorded.** @property _lastSelection* @param lastselection* @type string* @private*/_lastSelection: null,/*** Whether focus came from a click event.** This is used to determine whether to restore the selection or not.** @property _focusFromClick* @type Boolean* @default false* @private*/_focusFromClick: false,/*** Whether if the last gesturemovestart event target was contained in this editor or not.** @property _gesturestartededitor* @type Boolean* @default false* @private*/_gesturestartededitor: false,/*** Set up the watchers for selection save and restoration.** @method setupSelectionWatchers* @chainable*/setupSelectionWatchers: function() {// Save the selection when a change was made.this.on('atto:selectionchanged', this.saveSelection, this);this.editor.on('focus', this.restoreSelection, this);// Do not restore selection when focus is from a click event.this.editor.on('mousedown', function() {this._focusFromClick = true;}, this);// Copy the current value back to the textarea when focus leaves us and save the current selection.this.editor.on('blur', function() {// Clear the _focusFromClick value.this._focusFromClick = false;// Update the original text area.this.updateOriginal();}, this);this.editor.on(['keyup', 'focus'], function(e) {Y.soon(Y.bind(this._hasSelectionChanged, this, e));}, this);Y.one(document.body).on('gesturemovestart', function(e) {if (this._wrapper.contains(e.target._node)) {this._gesturestartededitor = true;} else {this._gesturestartededitor = false;}}, null, this);Y.one(document.body).on('gesturemoveend', function(e) {if (!this._gesturestartededitor) {// Ignore the event if movestart target was not contained in the editor.return;}Y.soon(Y.bind(this._hasSelectionChanged, this, e));}, {// Standalone will make sure all editors receive the end event.standAlone: true}, this);return this;},/*** Work out if the cursor is in the editable area for this editor instance.** @method isActive* @return {boolean}*/isActive: function() {var range = rangy.createRange(),selection = rangy.getSelection();if (!selection.rangeCount) {// If there was no range count, then there is no selection.return false;}// We can't be active if the editor doesn't have focus at the moment.if (!document.activeElement ||!(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {return false;}// Check whether the range intersects the editor selection.range.selectNode(this.editor.getDOMNode());return range.intersectsRange(selection.getRangeAt(0));},/*** Create a cross browser selection object that represents a YUI node.** @method getSelectionFromNode* @param {Node} YUI Node to base the selection upon.* @return {[rangy.Range]}*/getSelectionFromNode: function(node) {var range = rangy.createRange();range.selectNode(node.getDOMNode());return [range];},/*** Save the current selection to an internal property.** This allows more reliable return focus, helping improve keyboard navigation.** Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.** @method saveSelection*/saveSelection: function() {if (this.isActive()) {this._selections = this.getSelection();}},/*** Restore any stored selection when the editor gets focus again.** Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.** @method restoreSelection*/restoreSelection: function() {if (!this._focusFromClick) {if (this._selections) {this.setSelection(this._selections);}}this._focusFromClick = false;},/*** Get the selection object that can be passed back to setSelection.** @method getSelection* @return {array} An array of rangy ranges.*/getSelection: function() {return rangy.getSelection().getAllRanges();},/*** Check that a YUI node it at least partly contained by the current selection.** @method selectionContainsNode* @param {Node} The node to check.* @return {boolean}*/selectionContainsNode: function(node) {return rangy.getSelection().containsNode(node.getDOMNode(), true);},/*** Runs a filter on each node in the selection, and report whether the* supplied selector(s) were found in the supplied Nodes.** By default, all specified nodes must match the selection, but this* can be controlled with the requireall property.** @method selectionFilterMatches* @param {String} selector* @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.* @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.* @return {Boolean}*/selectionFilterMatches: function(selector, selectednodes, requireall) {if (typeof requireall === 'undefined') {requireall = true;}if (!selectednodes) {// Find this because it was not passed as a param.selectednodes = this.getSelectedNodes();}var allmatch = selectednodes.size() > 0,anymatch = false;var editor = this.editor,stopFn = function(node) {// The function getSelectedNodes only returns nodes within the editor, so this test is safe.return node === editor;};// If we do not find at least one match in the editor, no point trying to find them in the selection.if (!editor.one(selector)) {return false;}selectednodes.each(function(node) {// Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.if (requireall) {// Check for at least one failure.if (!allmatch || !node.ancestor(selector, true, stopFn)) {allmatch = false;}} else {// Check for at least one match.if (!anymatch && node.ancestor(selector, true, stopFn)) {anymatch = true;}}}, this);if (requireall) {return allmatch;} else {return anymatch;}},/*** Get the deepest possible list of nodes in the current selection.** @method getSelectedNodes* @return {NodeList}*/getSelectedNodes: function() {var results = new Y.NodeList(),nodes,selection,range,node,i;selection = rangy.getSelection();if (selection.rangeCount) {range = selection.getRangeAt(0);} else {// Empty range.range = rangy.createRange();}if (range.collapsed) {// We do not want to select all the nodes in the editor if we managed to// have a collapsed selection directly in the editor.// It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle// so we must filter that out here too.if (range.commonAncestorContainer !== this.editor.getDOMNode()&& range.commonAncestorContainer !== Y.config.doc) {range = range.cloneRange();range.selectNode(range.commonAncestorContainer);}}nodes = range.getNodes();for (i = 0; i < nodes.length; i++) {node = Y.one(nodes[i]);if (this.editor.contains(node)) {results.push(node);}}return results;},/*** Check whether the current selection has changed since this method was last called.** If the selection has changed, the atto:selectionchanged event is also fired.** @method _hasSelectionChanged* @private* @param {EventFacade} e* @return {Boolean}*/_hasSelectionChanged: function(e) {var selection = rangy.getSelection(),range,changed = false;if (selection.rangeCount) {range = selection.getRangeAt(0);} else {// Empty range.range = rangy.createRange();}if (this._lastSelection) {if (!this._lastSelection.equals(range)) {changed = true;return this._fireSelectionChanged(e);}}this._lastSelection = range;return changed;},/*** Fires the atto:selectionchanged event.** When the selectionchanged event is fired, the following arguments are provided:* - event : the original event that lead to this event being fired.* - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.** @method _fireSelectionChanged* @private* @param {EventFacade} e*/_fireSelectionChanged: function(e) {this.fire('atto:selectionchanged', {event: e,selectedNodes: this.getSelectedNodes()});},/*** Get the DOM node representing the common anscestor of the selection nodes.** @method getSelectionParentNode* @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.*/getSelectionParentNode: function() {var selection = rangy.getSelection();if (selection.rangeCount) {return selection.getRangeAt(0).commonAncestorContainer;}return false;},/*** Set the current selection. Used to restore a selection.** @method selection* @param {array} ranges A list of rangy.range objects in the selection.*/setSelection: function(ranges) {var selection = rangy.getSelection();selection.setRanges(ranges);},/*** Inserts the given HTML into the editable content at the currently focused point.** @method insertContentAtFocusPoint* @param {String} html* @return {Node} The YUI Node object added to the DOM.*/insertContentAtFocusPoint: function(html) {var selection = rangy.getSelection(),range,node = Y.Node.create(html);if (selection.rangeCount) {range = selection.getRangeAt(0);}if (range) {range.deleteContents();range.collapse(false);var currentnode = node.getDOMNode(),last = currentnode.lastChild || currentnode;range.insertNode(currentnode);range.collapseAfter(last);selection.setSingleRange(range);}return node;}};Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);