define("dijit/_editor/plugins/FontChoice", [ "dojo/_base/array", // array.indexOf array.map "dojo/_base/declare", // declare "dojo/dom-construct", // domConstruct.place "dojo/i18n", // i18n.getLocalization "dojo/_base/lang", // lang.delegate lang.hitch lang.isString "dojo/store/Memory", // MemoryStore "dojo/_base/window", // win.withGlobal "../../registry", // registry.getUniqueId "../../_Widget", "../../_TemplatedMixin", "../../_WidgetsInTemplateMixin", "../../form/FilteringSelect", "../_Plugin", "../range", "../selection", "dojo/i18n!../nls/FontChoice" ], function(array, declare, domConstruct, i18n, lang, MemoryStore, win, registry, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, FilteringSelect, _Plugin, rangeapi, selectionapi){ /*===== var _Plugin = dijit._editor._Plugin; var _Widget = dijit._Widget; var _TemplatedMixin = dijit._TemplatedMixin; var _WidgetsInTemplateMixin = dijit._WidgetsInTemplateMixin; var FilteringSelect = dijit.form.FilteringSelect; =====*/ // module: // dijit/_editor/plugins/FontChoice // summary: // fontchoice, fontsize, and formatblock editor plugins var _FontDropDown = declare("dijit._editor.plugins._FontDropDown", [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], { // summary: // Base class for widgets that contains a label (like "Font:") // and a FilteringSelect drop down to pick a value. // Used as Toolbar entry. // label: [public] String // The label to apply to this particular FontDropDown. label: "", // plainText: [public] boolean // Flag to indicate that the returned label should be plain text // instead of an example. plainText: false, // templateString: [public] String // The template used to construct the labeled dropdown. templateString: "" + "" + "" + "", postMixInProperties: function(){ // summary: // Over-ride to set specific properties. this.inherited(arguments); this.strings = i18n.getLocalization("dijit._editor", "FontChoice"); // Set some substitution variables used in the template this.label = this.strings[this.command]; this.id = registry.getUniqueId(this.declaredClass.replace(/\./g,"_")); // TODO: unneeded?? this.selectId = this.id + "_select"; // used in template this.inherited(arguments); }, postCreate: function(){ // summary: // Over-ride for the default postCreate action // This establishes the filtering selects and the like. // Initialize the list of items in the drop down by creating data store with items like: // {value: 1, name: "xx-small", label: "xx-small" } this.select.set("store", new MemoryStore({ idProperty: "value", data: array.map(this.values, function(value){ var name = this.strings[value] || value; return { label: this.getLabel(value, name), name: name, value: value }; }, this) })); this.select.set("value", "", false); this.disabled = this.select.get("disabled"); }, _setValueAttr: function(value, priorityChange){ // summary: // Over-ride for the default action of setting the // widget value, maps the input to known values // value: Object|String // The value to set in the select. // priorityChange: // Optional parameter used to tell the select whether or not to fire // onChange event. // if the value is not a permitted value, just set empty string to prevent showing the warning icon priorityChange = priorityChange !== false; this.select.set('value', array.indexOf(this.values,value) < 0 ? "" : value, priorityChange); if(!priorityChange){ // Clear the last state in case of updateState calls. Ref: #10466 this.select._lastValueReported=null; } }, _getValueAttr: function(){ // summary: // Allow retrieving the value from the composite select on // call to button.get("value"); return this.select.get('value'); }, focus: function(){ // summary: // Over-ride for focus control of this widget. Delegates focus down to the // filtering select. this.select.focus(); }, _setDisabledAttr: function(value){ // summary: // Over-ride for the button's 'disabled' attribute so that it can be // disabled programmatically. // Save off ths disabled state so the get retrieves it correctly //without needing to have a function proxy it. this.disabled = value; this.select.set("disabled", value); } }); var _FontNameDropDown = declare("dijit._editor.plugins._FontNameDropDown", _FontDropDown, { // summary: // Dropdown to select a font; goes in editor toolbar. // generic: Boolean // Use generic (web standard) font names generic: false, // command: [public] String // The editor 'command' implemented by this plugin. command: "fontName", postMixInProperties: function(){ // summary: // Over-ride for the default posr mixin control if(!this.values){ this.values = this.generic ? ["serif", "sans-serif", "monospace", "cursive", "fantasy"] : // CSS font-family generics ["Arial", "Times New Roman", "Comic Sans MS", "Courier New"]; } this.inherited(arguments); }, getLabel: function(value, name){ // summary: // Function used to generate the labels of the format dropdown // will return a formatted, or plain label based on the value // of the plainText option. // value: String // The 'insert value' associated with a name // name: String // The text name of the value if(this.plainText){ return name; }else{ return "
" + name + "
"; } }, _setValueAttr: function(value, priorityChange){ // summary: // Over-ride for the default action of setting the // widget value, maps the input to known values priorityChange = priorityChange !== false; if(this.generic){ var map = { "Arial": "sans-serif", "Helvetica": "sans-serif", "Myriad": "sans-serif", "Times": "serif", "Times New Roman": "serif", "Comic Sans MS": "cursive", "Apple Chancery": "cursive", "Courier": "monospace", "Courier New": "monospace", "Papyrus": "fantasy", "Estrangelo Edessa": "cursive", // Windows 7 "Gabriola": "fantasy" // Windows 7 }; value = map[value] || value; } this.inherited(arguments, [value, priorityChange]); } }); var _FontSizeDropDown = declare("dijit._editor.plugins._FontSizeDropDown", _FontDropDown, { // summary: // Dropdown to select a font size; goes in editor toolbar. // command: [public] String // The editor 'command' implemented by this plugin. command: "fontSize", // values: [public] Number[] // The HTML font size values supported by this plugin values: [1,2,3,4,5,6,7], // sizes according to the old HTML FONT SIZE getLabel: function(value, name){ // summary: // Function used to generate the labels of the format dropdown // will return a formatted, or plain label based on the value // of the plainText option. // We're stuck using the deprecated FONT tag to correspond // with the size measurements used by the editor // value: String // The 'insert value' associated with a name // name: String // The text name of the value if(this.plainText){ return name; }else{ return "" + name + ""; } }, _setValueAttr: function(value, priorityChange){ // summary: // Over-ride for the default action of setting the // widget value, maps the input to known values priorityChange = priorityChange !== false; if(value.indexOf && value.indexOf("px") != -1){ var pixels = parseInt(value, 10); value = {10:1, 13:2, 16:3, 18:4, 24:5, 32:6, 48:7}[pixels] || value; } this.inherited(arguments, [value, priorityChange]); } }); var _FormatBlockDropDown = declare("dijit._editor.plugins._FormatBlockDropDown", _FontDropDown, { // summary: // Dropdown to select a format (like paragraph or heading); goes in editor toolbar. // command: [public] String // The editor 'command' implemented by this plugin. command: "formatBlock", // values: [public] Array // The HTML format tags supported by this plugin values: ["noFormat", "p", "h1", "h2", "h3", "pre"], postCreate: function(){ // Init and set the default value to no formatting. Update state will adjust it // as needed. this.inherited(arguments); this.set("value", "noFormat", false); }, getLabel: function(value, name){ // summary: // Function used to generate the labels of the format dropdown // will return a formatted, or plain label based on the value // of the plainText option. // value: String // The 'insert value' associated with a name // name: String // The text name of the value if(this.plainText || value == "noFormat"){ return name; }else{ return "<" + value + ">" + name + ""; } }, _execCommand: function(editor, command, choice){ // summary: // Over-ride for default exec-command label. // Allows us to treat 'none' as special. if(choice === "noFormat"){ var start; var end; var sel = rangeapi.getSelection(editor.window); if(sel && sel.rangeCount > 0){ var range = sel.getRangeAt(0); var node, tag; if(range){ start = range.startContainer; end = range.endContainer; // find containing nodes of start/end. while(start && start !== editor.editNode && start !== editor.document.body && start.nodeType !== 1){ start = start.parentNode; } while(end && end !== editor.editNode && end !== editor.document.body && end.nodeType !== 1){ end = end.parentNode; } var processChildren = lang.hitch(this, function(node, ary){ if(node.childNodes && node.childNodes.length){ var i; for(i = 0; i < node.childNodes.length; i++){ var c = node.childNodes[i]; if(c.nodeType == 1){ if(win.withGlobal(editor.window, "inSelection", selectionapi, [c])){ var tag = c.tagName? c.tagName.toLowerCase(): ""; if(array.indexOf(this.values, tag) !== -1){ ary.push(c); } processChildren(c, ary); } } } } }); var unformatNodes = lang.hitch(this, function(nodes){ // summary: // Internal function to clear format nodes. // nodes: // The array of nodes to strip formatting from. if(nodes && nodes.length){ editor.beginEditing(); while(nodes.length){ this._removeFormat(editor, nodes.pop()); } editor.endEditing(); } }); var clearNodes = []; if(start == end){ //Contained within the same block, may be collapsed, but who cares, see if we // have a block element to remove. var block; node = start; while(node && node !== editor.editNode && node !== editor.document.body){ if(node.nodeType == 1){ tag = node.tagName? node.tagName.toLowerCase(): ""; if(array.indexOf(this.values, tag) !== -1){ block = node; break; } } node = node.parentNode; } //Also look for all child nodes in the selection that may need to be //cleared of formatting processChildren(start, clearNodes); if(block){ clearNodes = [block].concat(clearNodes); } unformatNodes(clearNodes); }else{ // Probably a multi select, so we have to process it. Whee. node = start; while(win.withGlobal(editor.window, "inSelection", selectionapi, [node])){ if(node.nodeType == 1){ tag = node.tagName? node.tagName.toLowerCase(): ""; if(array.indexOf(this.values, tag) !== -1){ clearNodes.push(node); } processChildren(node,clearNodes); } node = node.nextSibling; } unformatNodes(clearNodes); } editor.onDisplayChanged(); } } }else{ editor.execCommand(command, choice); } }, _removeFormat: function(editor, node){ // summary: // function to remove the block format node. // node: // The block format node to remove (and leave the contents behind) if(editor.customUndo){ // So of course IE doesn't work right with paste-overs. // We have to do this manually, which is okay since IE already uses // customUndo and we turned it on for WebKit. WebKit pasted funny, // so couldn't use the execCommand approach while(node.firstChild){ domConstruct.place(node.firstChild, node, "before"); } node.parentNode.removeChild(node); }else{ // Everyone else works fine this way, a paste-over and is native // undo friendly. win.withGlobal(editor.window, "selectElementChildren", selectionapi, [node]); var html = win.withGlobal(editor.window, "getSelectedHtml", selectionapi, [null]); win.withGlobal(editor.window, "selectElement", selectionapi, [node]); editor.execCommand("inserthtml", html||""); } } }); // TODO: for 2.0, split into FontChoice plugin into three separate classes, // one for each command (and change registry below) var FontChoice = declare("dijit._editor.plugins.FontChoice", _Plugin,{ // summary: // This plugin provides three drop downs for setting style in the editor // (font, font size, and format block), as controlled by command. // // description: // The commands provided by this plugin are: // // * fontName // | Provides a drop down to select from a list of font names // * fontSize // | Provides a drop down to select from a list of font sizes // * formatBlock // | Provides a drop down to select from a list of block styles // | // // which can easily be added to an editor by including one or more of the above commands // in the `plugins` attribute as follows: // // | plugins="['fontName','fontSize',...]" // // It is possible to override the default dropdown list by providing an Array for the `custom` property when // instantiating this plugin, e.g. // // | plugins="[{name:'dijit._editor.plugins.FontChoice', command:'fontName', custom:['Verdana','Myriad','Garamond']},...]" // // Alternatively, for `fontName` only, `generic:true` may be specified to provide a dropdown with // [CSS generic font families](http://www.w3.org/TR/REC-CSS2/fonts.html#generic-font-families) // // Note that the editor is often unable to properly handle font styling information defined outside // the context of the current editor instance, such as pre-populated HTML. // useDefaultCommand: [protected] Boolean // Override _Plugin.useDefaultCommand... // processing is handled by this plugin, not by dijit.Editor. useDefaultCommand: false, _initButton: function(){ // summary: // Overrides _Plugin._initButton(), to initialize the FilteringSelect+label in toolbar, // rather than a simple button. // tags: // protected // Create the widget to go into the toolbar (the so-called "button") var clazz = { fontName: _FontNameDropDown, fontSize: _FontSizeDropDown, formatBlock: _FormatBlockDropDown }[this.command], params = this.params; // For back-compat reasons support setting custom values via "custom" parameter // rather than "values" parameter if(this.params.custom){ params.values = this.params.custom; } var editor = this.editor; this.button = new clazz(lang.delegate({dir: editor.dir, lang: editor.lang}, params)); // Reflect changes to the drop down in the editor this.connect(this.button.select, "onChange", function(choice){ // User invoked change, since all internal updates set priorityChange to false and will // not trigger an onChange event. this.editor.focus(); if(this.command == "fontName" && choice.indexOf(" ") != -1){ choice = "'" + choice + "'"; } // Invoke, the editor already normalizes commands called through its // execCommand. if(this.button._execCommand){ this.button._execCommand(this.editor, this.command, choice); }else{ this.editor.execCommand(this.command, choice); } }); }, updateState: function(){ // summary: // Overrides _Plugin.updateState(). This controls updating the menu // options to the right values on state changes in the document (that trigger a // test of the actions.) // It set value of drop down in toolbar to reflect font/font size/format block // of text at current caret position. // tags: // protected var _e = this.editor; var _c = this.command; if(!_e || !_e.isLoaded || !_c.length){ return; } if(this.button){ var disabled = this.get("disabled"); this.button.set("disabled", disabled); if(disabled){ return; } var value; try{ value = _e.queryCommandValue(_c) || ""; }catch(e){ //Firefox may throw error above if the editor is just loaded, ignore it value = ""; } // strip off single quotes, if any var quoted = lang.isString(value) && value.match(/'([^']*)'/); if(quoted){ value = quoted[1]; } if(_c === "formatBlock"){ if(!value || value == "p"){ // Some browsers (WebKit) doesn't actually get the tag info right. // and IE returns paragraph when in a DIV!, so incorrect a lot, // so we have double-check it. value = null; var elem; // Try to find the current element where the caret is. var sel = rangeapi.getSelection(this.editor.window); if(sel && sel.rangeCount > 0){ var range = sel.getRangeAt(0); if(range){ elem = range.endContainer; } } // Okay, now see if we can find one of the formatting types we're in. while(elem && elem !== _e.editNode && elem !== _e.document){ var tg = elem.tagName?elem.tagName.toLowerCase():""; if(tg && array.indexOf(this.button.values, tg) > -1){ value = tg; break; } elem = elem.parentNode; } if(!value){ // Still no value, so lets select 'none'. value = "noFormat"; } }else{ // Check that the block format is one allowed, if not, // null it so that it gets set to empty. if(array.indexOf(this.button.values, value) < 0){ value = "noFormat"; } } } if(value !== this.button.get("value")){ // Set the value, but denote it is not a priority change, so no // onchange fires. this.button.set('value', value, false); } } } }); // Register these plugins array.forEach(["fontName", "fontSize", "formatBlock"], function(name){ _Plugin.registry[name] = function(args){ return new FontChoice({ command: name, plainText: args.plainText }); }; }); });