FontChoice.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. define("dijit/_editor/plugins/FontChoice", [
  2. "dojo/_base/array", // array.indexOf array.map
  3. "dojo/_base/declare", // declare
  4. "dojo/dom-construct", // domConstruct.place
  5. "dojo/i18n", // i18n.getLocalization
  6. "dojo/_base/lang", // lang.delegate lang.hitch lang.isString
  7. "dojo/store/Memory", // MemoryStore
  8. "dojo/_base/window", // win.withGlobal
  9. "../../registry", // registry.getUniqueId
  10. "../../_Widget",
  11. "../../_TemplatedMixin",
  12. "../../_WidgetsInTemplateMixin",
  13. "../../form/FilteringSelect",
  14. "../_Plugin",
  15. "../range",
  16. "../selection",
  17. "dojo/i18n!../nls/FontChoice"
  18. ], function(array, declare, domConstruct, i18n, lang, MemoryStore, win,
  19. registry, _Widget, _TemplatedMixin, _WidgetsInTemplateMixin, FilteringSelect, _Plugin, rangeapi, selectionapi){
  20. /*=====
  21. var _Plugin = dijit._editor._Plugin;
  22. var _Widget = dijit._Widget;
  23. var _TemplatedMixin = dijit._TemplatedMixin;
  24. var _WidgetsInTemplateMixin = dijit._WidgetsInTemplateMixin;
  25. var FilteringSelect = dijit.form.FilteringSelect;
  26. =====*/
  27. // module:
  28. // dijit/_editor/plugins/FontChoice
  29. // summary:
  30. // fontchoice, fontsize, and formatblock editor plugins
  31. var _FontDropDown = declare("dijit._editor.plugins._FontDropDown",
  32. [_Widget, _TemplatedMixin, _WidgetsInTemplateMixin], {
  33. // summary:
  34. // Base class for widgets that contains a label (like "Font:")
  35. // and a FilteringSelect drop down to pick a value.
  36. // Used as Toolbar entry.
  37. // label: [public] String
  38. // The label to apply to this particular FontDropDown.
  39. label: "",
  40. // plainText: [public] boolean
  41. // Flag to indicate that the returned label should be plain text
  42. // instead of an example.
  43. plainText: false,
  44. // templateString: [public] String
  45. // The template used to construct the labeled dropdown.
  46. templateString:
  47. "<span style='white-space: nowrap' class='dijit dijitReset dijitInline'>" +
  48. "<label class='dijitLeft dijitInline' for='${selectId}'>${label}</label>" +
  49. "<input data-dojo-type='dijit.form.FilteringSelect' required='false' " +
  50. "data-dojo-props='labelType:\"html\", labelAttr:\"label\", searchAttr:\"name\"' " +
  51. "tabIndex='-1' id='${selectId}' data-dojo-attach-point='select' value=''/>" +
  52. "</span>",
  53. postMixInProperties: function(){
  54. // summary:
  55. // Over-ride to set specific properties.
  56. this.inherited(arguments);
  57. this.strings = i18n.getLocalization("dijit._editor", "FontChoice");
  58. // Set some substitution variables used in the template
  59. this.label = this.strings[this.command];
  60. this.id = registry.getUniqueId(this.declaredClass.replace(/\./g,"_")); // TODO: unneeded??
  61. this.selectId = this.id + "_select"; // used in template
  62. this.inherited(arguments);
  63. },
  64. postCreate: function(){
  65. // summary:
  66. // Over-ride for the default postCreate action
  67. // This establishes the filtering selects and the like.
  68. // Initialize the list of items in the drop down by creating data store with items like:
  69. // {value: 1, name: "xx-small", label: "<font size=1>xx-small</font-size>" }
  70. this.select.set("store", new MemoryStore({
  71. idProperty: "value",
  72. data: array.map(this.values, function(value){
  73. var name = this.strings[value] || value;
  74. return {
  75. label: this.getLabel(value, name),
  76. name: name,
  77. value: value
  78. };
  79. }, this)
  80. }));
  81. this.select.set("value", "", false);
  82. this.disabled = this.select.get("disabled");
  83. },
  84. _setValueAttr: function(value, priorityChange){
  85. // summary:
  86. // Over-ride for the default action of setting the
  87. // widget value, maps the input to known values
  88. // value: Object|String
  89. // The value to set in the select.
  90. // priorityChange:
  91. // Optional parameter used to tell the select whether or not to fire
  92. // onChange event.
  93. // if the value is not a permitted value, just set empty string to prevent showing the warning icon
  94. priorityChange = priorityChange !== false;
  95. this.select.set('value', array.indexOf(this.values,value) < 0 ? "" : value, priorityChange);
  96. if(!priorityChange){
  97. // Clear the last state in case of updateState calls. Ref: #10466
  98. this.select._lastValueReported=null;
  99. }
  100. },
  101. _getValueAttr: function(){
  102. // summary:
  103. // Allow retrieving the value from the composite select on
  104. // call to button.get("value");
  105. return this.select.get('value');
  106. },
  107. focus: function(){
  108. // summary:
  109. // Over-ride for focus control of this widget. Delegates focus down to the
  110. // filtering select.
  111. this.select.focus();
  112. },
  113. _setDisabledAttr: function(value){
  114. // summary:
  115. // Over-ride for the button's 'disabled' attribute so that it can be
  116. // disabled programmatically.
  117. // Save off ths disabled state so the get retrieves it correctly
  118. //without needing to have a function proxy it.
  119. this.disabled = value;
  120. this.select.set("disabled", value);
  121. }
  122. });
  123. var _FontNameDropDown = declare("dijit._editor.plugins._FontNameDropDown", _FontDropDown, {
  124. // summary:
  125. // Dropdown to select a font; goes in editor toolbar.
  126. // generic: Boolean
  127. // Use generic (web standard) font names
  128. generic: false,
  129. // command: [public] String
  130. // The editor 'command' implemented by this plugin.
  131. command: "fontName",
  132. postMixInProperties: function(){
  133. // summary:
  134. // Over-ride for the default posr mixin control
  135. if(!this.values){
  136. this.values = this.generic ?
  137. ["serif", "sans-serif", "monospace", "cursive", "fantasy"] : // CSS font-family generics
  138. ["Arial", "Times New Roman", "Comic Sans MS", "Courier New"];
  139. }
  140. this.inherited(arguments);
  141. },
  142. getLabel: function(value, name){
  143. // summary:
  144. // Function used to generate the labels of the format dropdown
  145. // will return a formatted, or plain label based on the value
  146. // of the plainText option.
  147. // value: String
  148. // The 'insert value' associated with a name
  149. // name: String
  150. // The text name of the value
  151. if(this.plainText){
  152. return name;
  153. }else{
  154. return "<div style='font-family: "+value+"'>" + name + "</div>";
  155. }
  156. },
  157. _setValueAttr: function(value, priorityChange){
  158. // summary:
  159. // Over-ride for the default action of setting the
  160. // widget value, maps the input to known values
  161. priorityChange = priorityChange !== false;
  162. if(this.generic){
  163. var map = {
  164. "Arial": "sans-serif",
  165. "Helvetica": "sans-serif",
  166. "Myriad": "sans-serif",
  167. "Times": "serif",
  168. "Times New Roman": "serif",
  169. "Comic Sans MS": "cursive",
  170. "Apple Chancery": "cursive",
  171. "Courier": "monospace",
  172. "Courier New": "monospace",
  173. "Papyrus": "fantasy",
  174. "Estrangelo Edessa": "cursive", // Windows 7
  175. "Gabriola": "fantasy" // Windows 7
  176. };
  177. value = map[value] || value;
  178. }
  179. this.inherited(arguments, [value, priorityChange]);
  180. }
  181. });
  182. var _FontSizeDropDown = declare("dijit._editor.plugins._FontSizeDropDown", _FontDropDown, {
  183. // summary:
  184. // Dropdown to select a font size; goes in editor toolbar.
  185. // command: [public] String
  186. // The editor 'command' implemented by this plugin.
  187. command: "fontSize",
  188. // values: [public] Number[]
  189. // The HTML font size values supported by this plugin
  190. values: [1,2,3,4,5,6,7], // sizes according to the old HTML FONT SIZE
  191. getLabel: function(value, name){
  192. // summary:
  193. // Function used to generate the labels of the format dropdown
  194. // will return a formatted, or plain label based on the value
  195. // of the plainText option.
  196. // We're stuck using the deprecated FONT tag to correspond
  197. // with the size measurements used by the editor
  198. // value: String
  199. // The 'insert value' associated with a name
  200. // name: String
  201. // The text name of the value
  202. if(this.plainText){
  203. return name;
  204. }else{
  205. return "<font size=" + value + "'>" + name + "</font>";
  206. }
  207. },
  208. _setValueAttr: function(value, priorityChange){
  209. // summary:
  210. // Over-ride for the default action of setting the
  211. // widget value, maps the input to known values
  212. priorityChange = priorityChange !== false;
  213. if(value.indexOf && value.indexOf("px") != -1){
  214. var pixels = parseInt(value, 10);
  215. value = {10:1, 13:2, 16:3, 18:4, 24:5, 32:6, 48:7}[pixels] || value;
  216. }
  217. this.inherited(arguments, [value, priorityChange]);
  218. }
  219. });
  220. var _FormatBlockDropDown = declare("dijit._editor.plugins._FormatBlockDropDown", _FontDropDown, {
  221. // summary:
  222. // Dropdown to select a format (like paragraph or heading); goes in editor toolbar.
  223. // command: [public] String
  224. // The editor 'command' implemented by this plugin.
  225. command: "formatBlock",
  226. // values: [public] Array
  227. // The HTML format tags supported by this plugin
  228. values: ["noFormat", "p", "h1", "h2", "h3", "pre"],
  229. postCreate: function(){
  230. // Init and set the default value to no formatting. Update state will adjust it
  231. // as needed.
  232. this.inherited(arguments);
  233. this.set("value", "noFormat", false);
  234. },
  235. getLabel: function(value, name){
  236. // summary:
  237. // Function used to generate the labels of the format dropdown
  238. // will return a formatted, or plain label based on the value
  239. // of the plainText option.
  240. // value: String
  241. // The 'insert value' associated with a name
  242. // name: String
  243. // The text name of the value
  244. if(this.plainText || value == "noFormat"){
  245. return name;
  246. }else{
  247. return "<" + value + ">" + name + "</" + value + ">";
  248. }
  249. },
  250. _execCommand: function(editor, command, choice){
  251. // summary:
  252. // Over-ride for default exec-command label.
  253. // Allows us to treat 'none' as special.
  254. if(choice === "noFormat"){
  255. var start;
  256. var end;
  257. var sel = rangeapi.getSelection(editor.window);
  258. if(sel && sel.rangeCount > 0){
  259. var range = sel.getRangeAt(0);
  260. var node, tag;
  261. if(range){
  262. start = range.startContainer;
  263. end = range.endContainer;
  264. // find containing nodes of start/end.
  265. while(start && start !== editor.editNode &&
  266. start !== editor.document.body &&
  267. start.nodeType !== 1){
  268. start = start.parentNode;
  269. }
  270. while(end && end !== editor.editNode &&
  271. end !== editor.document.body &&
  272. end.nodeType !== 1){
  273. end = end.parentNode;
  274. }
  275. var processChildren = lang.hitch(this, function(node, ary){
  276. if(node.childNodes && node.childNodes.length){
  277. var i;
  278. for(i = 0; i < node.childNodes.length; i++){
  279. var c = node.childNodes[i];
  280. if(c.nodeType == 1){
  281. if(win.withGlobal(editor.window, "inSelection", selectionapi, [c])){
  282. var tag = c.tagName? c.tagName.toLowerCase(): "";
  283. if(array.indexOf(this.values, tag) !== -1){
  284. ary.push(c);
  285. }
  286. processChildren(c, ary);
  287. }
  288. }
  289. }
  290. }
  291. });
  292. var unformatNodes = lang.hitch(this, function(nodes){
  293. // summary:
  294. // Internal function to clear format nodes.
  295. // nodes:
  296. // The array of nodes to strip formatting from.
  297. if(nodes && nodes.length){
  298. editor.beginEditing();
  299. while(nodes.length){
  300. this._removeFormat(editor, nodes.pop());
  301. }
  302. editor.endEditing();
  303. }
  304. });
  305. var clearNodes = [];
  306. if(start == end){
  307. //Contained within the same block, may be collapsed, but who cares, see if we
  308. // have a block element to remove.
  309. var block;
  310. node = start;
  311. while(node && node !== editor.editNode && node !== editor.document.body){
  312. if(node.nodeType == 1){
  313. tag = node.tagName? node.tagName.toLowerCase(): "";
  314. if(array.indexOf(this.values, tag) !== -1){
  315. block = node;
  316. break;
  317. }
  318. }
  319. node = node.parentNode;
  320. }
  321. //Also look for all child nodes in the selection that may need to be
  322. //cleared of formatting
  323. processChildren(start, clearNodes);
  324. if(block){ clearNodes = [block].concat(clearNodes); }
  325. unformatNodes(clearNodes);
  326. }else{
  327. // Probably a multi select, so we have to process it. Whee.
  328. node = start;
  329. while(win.withGlobal(editor.window, "inSelection", selectionapi, [node])){
  330. if(node.nodeType == 1){
  331. tag = node.tagName? node.tagName.toLowerCase(): "";
  332. if(array.indexOf(this.values, tag) !== -1){
  333. clearNodes.push(node);
  334. }
  335. processChildren(node,clearNodes);
  336. }
  337. node = node.nextSibling;
  338. }
  339. unformatNodes(clearNodes);
  340. }
  341. editor.onDisplayChanged();
  342. }
  343. }
  344. }else{
  345. editor.execCommand(command, choice);
  346. }
  347. },
  348. _removeFormat: function(editor, node){
  349. // summary:
  350. // function to remove the block format node.
  351. // node:
  352. // The block format node to remove (and leave the contents behind)
  353. if(editor.customUndo){
  354. // So of course IE doesn't work right with paste-overs.
  355. // We have to do this manually, which is okay since IE already uses
  356. // customUndo and we turned it on for WebKit. WebKit pasted funny,
  357. // so couldn't use the execCommand approach
  358. while(node.firstChild){
  359. domConstruct.place(node.firstChild, node, "before");
  360. }
  361. node.parentNode.removeChild(node);
  362. }else{
  363. // Everyone else works fine this way, a paste-over and is native
  364. // undo friendly.
  365. win.withGlobal(editor.window,
  366. "selectElementChildren", selectionapi, [node]);
  367. var html = win.withGlobal(editor.window,
  368. "getSelectedHtml", selectionapi, [null]);
  369. win.withGlobal(editor.window,
  370. "selectElement", selectionapi, [node]);
  371. editor.execCommand("inserthtml", html||"");
  372. }
  373. }
  374. });
  375. // TODO: for 2.0, split into FontChoice plugin into three separate classes,
  376. // one for each command (and change registry below)
  377. var FontChoice = declare("dijit._editor.plugins.FontChoice", _Plugin,{
  378. // summary:
  379. // This plugin provides three drop downs for setting style in the editor
  380. // (font, font size, and format block), as controlled by command.
  381. //
  382. // description:
  383. // The commands provided by this plugin are:
  384. //
  385. // * fontName
  386. // | Provides a drop down to select from a list of font names
  387. // * fontSize
  388. // | Provides a drop down to select from a list of font sizes
  389. // * formatBlock
  390. // | Provides a drop down to select from a list of block styles
  391. // |
  392. //
  393. // which can easily be added to an editor by including one or more of the above commands
  394. // in the `plugins` attribute as follows:
  395. //
  396. // | plugins="['fontName','fontSize',...]"
  397. //
  398. // It is possible to override the default dropdown list by providing an Array for the `custom` property when
  399. // instantiating this plugin, e.g.
  400. //
  401. // | plugins="[{name:'dijit._editor.plugins.FontChoice', command:'fontName', custom:['Verdana','Myriad','Garamond']},...]"
  402. //
  403. // Alternatively, for `fontName` only, `generic:true` may be specified to provide a dropdown with
  404. // [CSS generic font families](http://www.w3.org/TR/REC-CSS2/fonts.html#generic-font-families)
  405. //
  406. // Note that the editor is often unable to properly handle font styling information defined outside
  407. // the context of the current editor instance, such as pre-populated HTML.
  408. // useDefaultCommand: [protected] Boolean
  409. // Override _Plugin.useDefaultCommand...
  410. // processing is handled by this plugin, not by dijit.Editor.
  411. useDefaultCommand: false,
  412. _initButton: function(){
  413. // summary:
  414. // Overrides _Plugin._initButton(), to initialize the FilteringSelect+label in toolbar,
  415. // rather than a simple button.
  416. // tags:
  417. // protected
  418. // Create the widget to go into the toolbar (the so-called "button")
  419. var clazz = {
  420. fontName: _FontNameDropDown,
  421. fontSize: _FontSizeDropDown,
  422. formatBlock: _FormatBlockDropDown
  423. }[this.command],
  424. params = this.params;
  425. // For back-compat reasons support setting custom values via "custom" parameter
  426. // rather than "values" parameter
  427. if(this.params.custom){
  428. params.values = this.params.custom;
  429. }
  430. var editor = this.editor;
  431. this.button = new clazz(lang.delegate({dir: editor.dir, lang: editor.lang}, params));
  432. // Reflect changes to the drop down in the editor
  433. this.connect(this.button.select, "onChange", function(choice){
  434. // User invoked change, since all internal updates set priorityChange to false and will
  435. // not trigger an onChange event.
  436. if(this.editor.focused){
  437. // put focus back in the iframe, unless focus has somehow been shifted out of the editor completely
  438. this.editor.focus();
  439. }
  440. if(this.command == "fontName" && choice.indexOf(" ") != -1){ choice = "'" + choice + "'"; }
  441. // Invoke, the editor already normalizes commands called through its
  442. // execCommand.
  443. if(this.button._execCommand){
  444. this.button._execCommand(this.editor, this.command, choice);
  445. }else{
  446. this.editor.execCommand(this.command, choice);
  447. }
  448. });
  449. },
  450. updateState: function(){
  451. // summary:
  452. // Overrides _Plugin.updateState(). This controls updating the menu
  453. // options to the right values on state changes in the document (that trigger a
  454. // test of the actions.)
  455. // It set value of drop down in toolbar to reflect font/font size/format block
  456. // of text at current caret position.
  457. // tags:
  458. // protected
  459. var _e = this.editor;
  460. var _c = this.command;
  461. if(!_e || !_e.isLoaded || !_c.length){ return; }
  462. if(this.button){
  463. var disabled = this.get("disabled");
  464. this.button.set("disabled", disabled);
  465. if(disabled){ return; }
  466. var value;
  467. try{
  468. value = _e.queryCommandValue(_c) || "";
  469. }catch(e){
  470. //Firefox may throw error above if the editor is just loaded, ignore it
  471. value = "";
  472. }
  473. // strip off single quotes, if any
  474. var quoted = lang.isString(value) && (value.match(/'([^']*)'/) || value.match(/"([^"]*)"/));
  475. if(quoted){ value = quoted[1]; }
  476. if(_c === "formatBlock"){
  477. if(!value || value == "p"){
  478. // Some browsers (WebKit) doesn't actually get the tag info right.
  479. // and IE returns paragraph when in a DIV!, so incorrect a lot,
  480. // so we have double-check it.
  481. value = null;
  482. var elem;
  483. // Try to find the current element where the caret is.
  484. var sel = rangeapi.getSelection(this.editor.window);
  485. if(sel && sel.rangeCount > 0){
  486. var range = sel.getRangeAt(0);
  487. if(range){
  488. elem = range.endContainer;
  489. }
  490. }
  491. // Okay, now see if we can find one of the formatting types we're in.
  492. while(elem && elem !== _e.editNode && elem !== _e.document){
  493. var tg = elem.tagName?elem.tagName.toLowerCase():"";
  494. if(tg && array.indexOf(this.button.values, tg) > -1){
  495. value = tg;
  496. break;
  497. }
  498. elem = elem.parentNode;
  499. }
  500. if(!value){
  501. // Still no value, so lets select 'none'.
  502. value = "noFormat";
  503. }
  504. }else{
  505. // Check that the block format is one allowed, if not,
  506. // null it so that it gets set to empty.
  507. if(array.indexOf(this.button.values, value) < 0){
  508. value = "noFormat";
  509. }
  510. }
  511. }
  512. if(value !== this.button.get("value")){
  513. // Set the value, but denote it is not a priority change, so no
  514. // onchange fires.
  515. this.button.set('value', value, false);
  516. }
  517. }
  518. }
  519. });
  520. // Register these plugins
  521. array.forEach(["fontName", "fontSize", "formatBlock"], function(name){
  522. _Plugin.registry[name] = function(args){
  523. return new FontChoice({
  524. command: name,
  525. plainText: args.plainText
  526. });
  527. };
  528. });
  529. });