-
-
Save digideskio/14ff80699b9b6908f8d509e51e027069 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 editor main class. | |
* Common functions required by editor plugins. | |
* | |
* @package editor-atto | |
* @copyright 2013 Damyon Wiese <[email protected]> | |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
*/ | |
M.editor_atto = M.editor_atto || { | |
/** | |
* Classes constants. | |
*/ | |
CSS: { | |
CONTENT: 'editor_atto_content', | |
CONTENTWRAPPER: 'editor_atto_content_wrap', | |
TOOLBAR: 'editor_atto_toolbar', | |
WRAPPER: 'editor_atto' | |
}, | |
/** | |
* List of attached button handlers to prevent duplicates. | |
*/ | |
buttonhandlers : {}, | |
/** | |
* List of YUI overlays for custom menus. | |
*/ | |
menus : {}, | |
/** | |
* List of attached menu handlers to prevent duplicates. | |
*/ | |
menuhandlers : {}, | |
/** | |
* List of file picker options for specific editor instances. | |
*/ | |
filepickeroptions : {}, | |
/** | |
* List of buttons and menus that have been added to the toolbar. | |
*/ | |
widgets : {}, | |
/** | |
* Toggle a menu. | |
* @param event e | |
*/ | |
showhide_menu_handler : function(e) { | |
e.preventDefault(); | |
var disabled = this.getAttribute('disabled'); | |
var overlayid = this.getAttribute('data-menu'); | |
var overlay = M.editor_atto.menus[overlayid]; | |
var menu = overlay.get('bodyContent'); | |
if (overlay.get('visible') || disabled) { | |
overlay.hide(); | |
menu.detach('clickoutside'); | |
} else { | |
menu.on('clickoutside', function(ev) { | |
if ((ev.target.ancestor() !== this) && (ev.target !== this)) { | |
if (overlay.get('visible')) { | |
menu.detach('clickoutside'); | |
overlay.hide(); | |
} | |
} | |
}, this); | |
overlay.show(); | |
} | |
}, | |
/** | |
* Handle clicks on editor buttons. | |
* @param event e | |
*/ | |
buttonclicked_handler : function(e) { | |
var elementid = this.getAttribute('data-editor'); | |
var plugin = this.getAttribute('data-plugin'); | |
var handler = this.getAttribute('data-handler'); | |
var overlay = M.editor_atto.menus[plugin + '_' + elementid]; | |
if (overlay) { | |
overlay.hide(); | |
} | |
if (M.editor_atto.is_enabled(elementid, plugin)) { | |
// Pass it on. | |
handler = M.editor_atto.buttonhandlers[handler]; | |
return handler(e, elementid); | |
} | |
}, | |
/** | |
* Determine if the specified toolbar button/menu is enabled. | |
* @param string elementid, the element id of this editor. | |
* @param string plugin, the plugin that created the button/menu. | |
*/ | |
is_enabled : function(elementid, plugin) { | |
var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button'); | |
return !element.hasAttribute('disabled'); | |
}, | |
/** | |
* Disable all buttons and menus in the toolbar. | |
* @param string elementid, the element id of this editor. | |
*/ | |
disable_all_widgets : function(elementid) { | |
var plugin, element; | |
for (plugin in M.editor_atto.widgets) { | |
element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button'); | |
if (element) { | |
element.setAttribute('disabled', 'true'); | |
} | |
} | |
}, | |
/** | |
* Enable a single widget in the toolbar. | |
* @param string elementid, the element id of this editor. | |
* @param string plugin, the name of the plugin that created the widget. | |
*/ | |
enable_widget : function(elementid, plugin) { | |
var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button'); | |
if (element) { | |
element.removeAttribute('disabled'); | |
} | |
}, | |
/** | |
* Enable all buttons and menus in the toolbar. | |
* @param string elementid, the element id of this editor. | |
*/ | |
enable_all_widgets : function(elementid) { | |
var plugin, element; | |
for (plugin in M.editor_atto.widgets) { | |
element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button'); | |
if (element) { | |
element.removeAttribute('disabled'); | |
} | |
} | |
}, | |
/** | |
* Add a button to the toolbar belonging to the editor for element with id "elementid". | |
* @param string elementid - the id of the textarea we created this editor from. | |
* @param string plugin - the plugin defining the button | |
* @param string icon - the html used for the content of the button | |
* @param string groupname - the group the button should be appended to. | |
* @handler function handler- A function to call when the button is clicked. | |
*/ | |
add_toolbar_menu : function(elementid, plugin, icon, groupname, entries) { | |
var toolbar = Y.one('#' + elementid + '_toolbar'), | |
group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'), | |
currentfocus, | |
button; | |
if (!group) { | |
group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>'); | |
toolbar.append(group); | |
} | |
button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' + | |
'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
'tabindex="-1" ' + | |
'data-menu="' + plugin + '_' + elementid + '" >' + | |
icon + | |
'</button>'); | |
group.append(button); | |
currentfocus = toolbar.getAttribute('aria-activedescendant'); | |
if (!currentfocus) { | |
button.setAttribute('tabindex', '0'); | |
toolbar.setAttribute('aria-activedescendant', button.generateID()); | |
} | |
// Save the name of the plugin. | |
M.editor_atto.widgets[plugin] = plugin; | |
var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' + | |
' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>'); | |
var i = 0, entry = {}; | |
for (i = 0; i < entries.length; i++) { | |
entry = entries[i]; | |
menu.append(Y.Node.create('<div class="atto_menuentry">' + | |
'<a href="#" class="atto_' + plugin + '_action_' + i + '" ' + | |
'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
'data-plugin="' + Y.Escape.html(plugin) + '" ' + | |
'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' + | |
entry.text + | |
'</a>' + | |
'</div>')); | |
if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) { | |
Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i); | |
M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler; | |
} | |
} | |
if (!M.editor_atto.buttonhandlers[plugin]) { | |
Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button'); | |
M.editor_atto.buttonhandlers[plugin] = true; | |
} | |
var overlay = new M.core.dialogue({ | |
bodyContent : menu, | |
visible : false, | |
width: '14em', | |
zindex: 100, | |
lightbox: false, | |
closeButton: false, | |
centered : false, | |
align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]} | |
}); | |
M.editor_atto.menus[plugin + '_' + elementid] = overlay; | |
overlay.render(); | |
overlay.hide(); | |
overlay.headerNode.hide(); | |
}, | |
/** | |
* Add a button to the toolbar belonging to the editor for element with id "elementid". | |
* @param string elementid - the id of the textarea we created this editor from. | |
* @param string plugin - the plugin defining the button. | |
* @param string icon - the html used for the content of the button. | |
* @param string groupname - the group the button should be appended to. | |
* @handler function handler- A function to call when the button is clicked. | |
*/ | |
add_toolbar_button : function(elementid, plugin, icon, groupname, handler) { | |
var toolbar = Y.one('#' + elementid + '_toolbar'), | |
group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'), | |
button, | |
currentfocus; | |
if (!group) { | |
group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>'); | |
toolbar.append(group); | |
} | |
button = Y.Node.create('<button class="atto_' + plugin + '_button" ' + | |
'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
'data-plugin="' + Y.Escape.html(plugin) + '" ' + | |
'tabindex="-1" ' + | |
'data-handler="' + Y.Escape.html(plugin) + '">' + | |
icon + | |
'</button>'); | |
group.append(button); | |
currentfocus = toolbar.getAttribute('aria-activedescendant'); | |
if (!currentfocus) { | |
button.setAttribute('tabindex', '0'); | |
toolbar.setAttribute('aria-activedescendant', button.generateID()); | |
} | |
// We only need to attach this once. | |
if (!M.editor_atto.buttonhandlers[plugin]) { | |
Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button'); | |
M.editor_atto.buttonhandlers[plugin] = handler; | |
} | |
// Save the name of the plugin. | |
M.editor_atto.widgets[plugin] = plugin; | |
}, | |
/** | |
* Work out if the cursor is in the editable area for this editor instance. | |
* @param string elementid of this editor | |
* @return bool | |
*/ | |
is_active : function(elementid) { | |
var selection = M.editor_atto.get_selection(); | |
if (selection.length) { | |
selection = selection.pop(); | |
} | |
var node = null; | |
if (selection.parentElement) { | |
node = Y.one(selection.parentElement()); | |
} else { | |
node = Y.one(selection.startContainer); | |
} | |
return node && node.ancestor('#' + elementid + 'editable') !== null; | |
}, | |
/** | |
* Focus on the editable area for this editor. | |
* @param string elementid of this editor | |
*/ | |
focus : function(elementid) { | |
Y.one('#' + elementid + 'editable').focus(); | |
}, | |
/** | |
* Initialise the editor | |
* @param object params for this editor instance. | |
*/ | |
init : function(params) { | |
var textarea = Y.one('#' +params.elementid); | |
var wrapper = Y.Node.create('<div class="' + this.CSS.WRAPPER + '" />'); | |
var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' + | |
'contenteditable="true" ' + | |
'spellcheck="true" ' + | |
'class="' + this.CSS.CONTENT + '" />'); | |
var cssfont = ''; | |
var toolbar = Y.Node.create('<div class="' + this.CSS.TOOLBAR + '" id="' + params.elementid + '_toolbar" role="toolbar"/>'); | |
// Editable content wrapper. | |
var content = Y.Node.create('<div class="' + this.CSS.CONTENTWRAPPER + '" />'); | |
content.appendChild(atto); | |
// Add everything to the wrapper. | |
wrapper.appendChild(toolbar); | |
wrapper.appendChild(content); | |
// Bleh - why are we sent a url and not the css to apply directly? | |
var css = Y.io(params.content_css, { sync: true }); | |
var pos = css.responseText.indexOf('font:'); | |
if (pos) { | |
cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1); | |
atto.setStyle('font', cssfont); | |
} | |
atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em'); | |
// Copy text to editable div. | |
atto.append(textarea.get('value')); | |
// Add the toolbar and editable zone to the page. | |
textarea.get('parentNode').insert(wrapper, textarea); | |
atto.setStyle('color', textarea.getStyle('color')); | |
atto.setStyle('lineHeight', textarea.getStyle('lineHeight')); | |
atto.setStyle('fontSize', textarea.getStyle('fontSize')); | |
// Hide the old textarea. | |
textarea.hide(); | |
// Copy the current value back to the textarea when focus leaves us. | |
atto.on('blur', function() { | |
textarea.set('value', atto.getHTML()); | |
}); | |
// Listen for Arrow left and Arrow right keys. | |
Y.one(Y.config.doc.body).delegate('key', | |
this.keyboard_navigation, | |
'down:37,39', | |
'#' + params.elementid + '_toolbar', | |
this, | |
params.elementid); | |
// Save the file picker options for later. | |
M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions; | |
}, | |
/** | |
* Implement arrow key navigation for the buttons in the toolbar. | |
* @param Event e - the keyboard event. | |
* @param string elementid - the id of the textarea we created this editor from. | |
*/ | |
keyboard_navigation : function(e, elementid) { | |
var buttons, | |
current, | |
currentid, | |
currentindex; | |
e.preventDefault(); | |
buttons = Y.all('#' + elementid + '_toolbar button'); | |
currentid = Y.one('#' + elementid + '_toolbar').getAttribute('aria-activedescendant'); | |
if (!currentid) { | |
return; | |
} | |
current = Y.one('#' + currentid); | |
current.setAttribute('tabindex', '-1'); | |
currentindex = buttons.indexOf(current); | |
if (e.keyCode === 37) { | |
// Left | |
currentindex--; | |
if (currentindex < 0) { | |
currentindex = buttons.size()-1; | |
} | |
} else { | |
// Right | |
currentindex++; | |
if (currentindex >= buttons.size()) { | |
currentindex = 0; | |
} | |
} | |
current = buttons.item(currentindex); | |
current.setAttribute('tabindex', '0'); | |
current.focus(); | |
Y.one('#' + elementid + '_toolbar').setAttribute('aria-activedescendant', current.generateID()); | |
}, | |
/** | |
* Show the filepicker. | |
* @param string elementid for this editor instance. | |
* @param string type The media type for the file picker | |
* @param function callback | |
*/ | |
show_filepicker : function(elementid, type, callback) { | |
Y.use('core_filepicker', function (Y) { | |
var options = M.editor_atto.filepickeroptions[elementid][type]; | |
options.formcallback = callback; | |
options.editor_target = Y.one(elementid); | |
M.core_filepicker.show(Y, options); | |
}); | |
}, | |
/** | |
* Create a cross browser selection object that represents a yui node. | |
* @param Node yui node for the selection | |
* @return range (browser dependent) | |
*/ | |
get_selection_from_node: function(node) { | |
var range; | |
if (window.getSelection) { | |
range = document.createRange(); | |
range.setStartBefore(node.getDOMNode()); | |
range.setEndAfter(node.getDOMNode()); | |
return [range]; | |
} else if (document.selection) { | |
range = document.body.createTextRange(); | |
range.moveToElementText(node.getDOMNode()); | |
return range; | |
} | |
return false; | |
}, | |
/** | |
* Get the selection object that can be passed back to set_selection. | |
* @return range (browser dependent) | |
*/ | |
get_selection : function() { | |
if (window.getSelection) { | |
var sel = window.getSelection(); | |
var ranges = [], i = 0; | |
for (i = 0; i < sel.rangeCount; i++) { | |
ranges.push(sel.getRangeAt(i)); | |
} | |
return ranges; | |
} else if (document.selection) { | |
// IE < 9 | |
if (document.selection.createRange) { | |
return document.selection.createRange(); | |
} | |
} | |
return false; | |
}, | |
/** | |
* Get the dom node representing the common anscestor of the selection nodes. | |
* @return DOMNode | |
*/ | |
get_selection_parent_node : function() { | |
var selection = M.editor_atto.get_selection(); | |
if (selection.length > 0) { | |
return selection[0].commonAncestorContainer; | |
} | |
}, | |
/** | |
* Get the list of child nodes of the selection. | |
* @return DOMNode[] | |
*/ | |
get_selection_text : function() { | |
var selection = M.editor_atto.get_selection(); | |
if (selection.length > 0 && selection[0].cloneContents) { | |
return selection[0].cloneContents(); | |
} | |
}, | |
/** | |
* Set the current selection. Used to restore a selection. | |
*/ | |
set_selection : function(selection) { | |
var sel, i; | |
if (window.getSelection) { | |
sel = window.getSelection(); | |
sel.removeAllRanges(); | |
for (i = 0; i < selection.length; i++) { | |
sel.addRange(selection[i]); | |
} | |
} else if (document.selection) { | |
// IE < 9 | |
if (selection.select) { | |
selection.select(); | |
} | |
} | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment