InsertAnchor.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. define("dojox/editor/plugins/InsertAnchor", [
  2. "dojo",
  3. "dijit",
  4. "dojox",
  5. "dijit/_base/manager", // TODO: change to dijit/registry, and change dijit.byId to registry.byId
  6. "dijit/_editor/range",
  7. "dijit/_Templated",
  8. "dijit/TooltipDialog",
  9. "dijit/form/ValidationTextBox",
  10. "dijit/form/Select",
  11. "dijit/form/Button",
  12. "dijit/form/DropDownButton",
  13. "dijit/_editor/range",
  14. "dijit/_editor/selection",
  15. "dijit/_editor/_Plugin",
  16. "dojo/_base/connect",
  17. "dojo/_base/declare",
  18. "dojo/i18n",
  19. "dojo/string",
  20. "dojox/editor/plugins/ToolbarLineBreak",
  21. "dojo/i18n!dojox/editor/plugins/nls/InsertAnchor",
  22. "dojo/i18n!dijit/nls/common"
  23. ], function(dojo, dijit, dojox) {
  24. dojo.declare("dojox.editor.plugins.InsertAnchor", dijit._editor._Plugin, {
  25. // summary:
  26. // This plugin provides the basis for an insert anchor dialog for the
  27. // dijit.Editor
  28. //
  29. // description:
  30. // The command provided by this plugin is:
  31. // * insertAnchor
  32. // htmlTemplate: [protected] String
  33. // String used for templating the HTML to insert at the desired point.
  34. htmlTemplate: "<a name=\"${anchorInput}\" class=\"dijitEditorPluginInsertAnchorStyle\">${textInput}</a>",
  35. // iconClassPrefix: [const] String
  36. // The CSS class name for the button node icon.
  37. iconClassPrefix: "dijitAdditionalEditorIcon",
  38. // linkDialogTemplate: [private] String
  39. // Template for contents of TooltipDialog to pick URL
  40. _template: [
  41. "<table><tr><td>",
  42. "<label for='${id}_anchorInput'>${anchor}</label>",
  43. "</td><td>",
  44. "<input dojoType='dijit.form.ValidationTextBox' required='true' " +
  45. "id='${id}_anchorInput' name='anchorInput' intermediateChanges='true'>",
  46. "</td></tr><tr><td>",
  47. "<label for='${id}_textInput'>${text}</label>",
  48. "</td><td>",
  49. "<input dojoType='dijit.form.ValidationTextBox' required='true' id='${id}_textInput' " +
  50. "name='textInput' intermediateChanges='true'>",
  51. "</td></tr>",
  52. "<tr><td colspan='2'>",
  53. "<button dojoType='dijit.form.Button' type='submit' id='${id}_setButton'>${set}</button>",
  54. "<button dojoType='dijit.form.Button' type='button' id='${id}_cancelButton'>${cancel}</button>",
  55. "</td></tr></table>"
  56. ].join(""),
  57. _initButton: function(){
  58. // Override _Plugin._initButton() to initialize DropDownButton and TooltipDialog.
  59. var _this = this;
  60. var messages = dojo.i18n.getLocalization("dojox.editor.plugins", "InsertAnchor", this.lang);
  61. // Build the dropdown dialog we'll use for the button
  62. var dropDown = (this.dropDown = new dijit.TooltipDialog({
  63. title: messages["title"],
  64. execute: dojo.hitch(this, "setValue"),
  65. onOpen: function(){
  66. _this._onOpenDialog();
  67. dijit.TooltipDialog.prototype.onOpen.apply(this, arguments);
  68. },
  69. onCancel: function(){
  70. setTimeout(dojo.hitch(_this, "_onCloseDialog"),0);
  71. }
  72. }));
  73. this.button = new dijit.form.DropDownButton({
  74. label: messages["insertAnchor"],
  75. showLabel: false,
  76. iconClass: this.iconClassPrefix + " " + this.iconClassPrefix + "InsertAnchor",
  77. tabIndex: "-1",
  78. dropDown: this.dropDown
  79. });
  80. messages.id = dijit.getUniqueId(this.editor.id);
  81. this._uniqueId = messages.id;
  82. this.dropDown.set('content', dropDown.title +
  83. "<div style='border-bottom: 1px black solid;padding-bottom:2pt;margin-bottom:4pt'></div>" +
  84. dojo.string.substitute(this._template, messages));
  85. dropDown.startup();
  86. this._anchorInput = dijit.byId(this._uniqueId + "_anchorInput");
  87. this._textInput = dijit.byId(this._uniqueId + "_textInput");
  88. this._setButton = dijit.byId(this._uniqueId + "_setButton");
  89. this.connect(dijit.byId(this._uniqueId + "_cancelButton"), "onClick", function(){
  90. this.dropDown.onCancel();
  91. });
  92. if(this._anchorInput){
  93. this.connect(this._anchorInput, "onChange", "_checkInput");
  94. }
  95. if(this._textInput){
  96. this.connect(this._anchorInput, "onChange", "_checkInput");
  97. }
  98. //Register some filters to handle setting/removing the class tags on anchors.
  99. this.editor.contentDomPreFilters.push(dojo.hitch(this, this._preDomFilter));
  100. this.editor.contentDomPostFilters.push(dojo.hitch(this, this._postDomFilter));
  101. this._setup();
  102. },
  103. updateState: function(){
  104. // summary:
  105. // Over-ride for button state control for disabled to work.
  106. this.button.set("disabled", this.get("disabled"));
  107. },
  108. setEditor: function(editor){
  109. // summary:
  110. // Over-ride for the setting of the editor.
  111. // editor: Object
  112. // The editor to configure for this plugin to use.
  113. this.editor = editor;
  114. this._initButton();
  115. },
  116. _checkInput: function(){
  117. // summary:
  118. // Function to check the input to the dialog is valid
  119. // and enable/disable set button
  120. // tags:
  121. // private
  122. var disable = true;
  123. if(this._anchorInput.isValid()){
  124. disable = false;
  125. }
  126. this._setButton.set("disabled", disable);
  127. },
  128. _setup: function(){
  129. // summary:
  130. // Over-ridable function that connects tag specific events.
  131. this.editor.onLoadDeferred.addCallback(dojo.hitch(this, function(){
  132. this.connect(this.editor.editNode, "ondblclick", this._onDblClick);
  133. setTimeout(dojo.hitch(this, function() {
  134. this._applyStyles();
  135. }), 100);
  136. }));
  137. },
  138. getAnchorStyle: function(){
  139. // summary:
  140. // Over-ridable function for getting the style to apply to the anchor.
  141. // The default is a dashed border with an anchor symbol.
  142. // tags:
  143. // public
  144. var style = "@media screen {\n" +
  145. "\t.dijitEditorPluginInsertAnchorStyle {\n" +
  146. "\t\tbackground-image: url({MODURL}/images/anchor.gif);\n" +
  147. "\t\tbackground-repeat: no-repeat;\n" +
  148. "\t\tbackground-position: top left;\n" +
  149. "\t\tborder-width: 1px;\n" +
  150. "\t\tborder-style: dashed;\n" +
  151. "\t\tborder-color: #D0D0D0;\n" +
  152. "\t\tpadding-left: 20px;\n" +
  153. "\t}\n" +
  154. "}\n";
  155. //Finally associate in the image locations based off the module url.
  156. var modurl = dojo.moduleUrl(dojox._scopeName, "editor/plugins/resources").toString();
  157. if(!(modurl.match(/^https?:\/\//i)) &&
  158. !(modurl.match(/^file:\/\//i))){
  159. // We have to root it to the page location on webkit for some nutball reason.
  160. // Probably has to do with how iframe was loaded.
  161. var bUrl;
  162. if(modurl.charAt(0) === "/"){
  163. //Absolute path on the server, so lets handle...
  164. var proto = dojo.doc.location.protocol;
  165. var hostn = dojo.doc.location.host;
  166. bUrl = proto + "//" + hostn;
  167. }else{
  168. bUrl = this._calcBaseUrl(dojo.global.location.href);
  169. }
  170. if(bUrl[bUrl.length - 1] !== "/" && modurl.charAt(0) !== "/"){
  171. bUrl += "/";
  172. }
  173. modurl = bUrl + modurl;
  174. }
  175. return style.replace(/\{MODURL\}/gi, modurl);
  176. },
  177. _applyStyles: function(){
  178. // summary:
  179. // Function to apply a style to inserted anchor tags so that
  180. // they are obviously anchors.
  181. if(!this._styled){
  182. try{
  183. //Attempt to inject our specialized style rules for doing this.
  184. this._styled = true;
  185. var doc = this.editor.document;
  186. var style = this.getAnchorStyle();
  187. if(!dojo.isIE){
  188. var sNode = doc.createElement("style");
  189. sNode.appendChild(doc.createTextNode(style));
  190. doc.getElementsByTagName("head")[0].appendChild(sNode);
  191. }else{
  192. var ss = doc.createStyleSheet("");
  193. ss.cssText = style;
  194. }
  195. }catch(e){ /* Squelch */ }
  196. }
  197. },
  198. _calcBaseUrl: function(fullUrl) {
  199. // summary:
  200. // Internal function used to figure out the full root url (no relatives)
  201. // for loading images in the styles in the iframe.
  202. // fullUrl: String
  203. // The full url to tear down to the base.
  204. // tags:
  205. // private
  206. var baseUrl = null;
  207. if (fullUrl !== null) {
  208. // Check to see if we need to strip off any query parameters from the Url.
  209. var index = fullUrl.indexOf("?");
  210. if (index != -1) {
  211. fullUrl = fullUrl.substring(0,index);
  212. }
  213. // Now we need to trim if necessary. If it ends in /, then we don't
  214. // have a filename to trim off so we can return.
  215. index = fullUrl.lastIndexOf("/");
  216. if (index > 0 && index < fullUrl.length) {
  217. baseUrl = fullUrl.substring(0,index);
  218. }else{
  219. baseUrl = fullUrl;
  220. }
  221. }
  222. return baseUrl; //String
  223. },
  224. _checkValues: function(args){
  225. // summary:
  226. // Function to check the values in args and 'fix' them up as needed.
  227. // args: Object
  228. // Content being set.
  229. // tags:
  230. // protected
  231. if(args){
  232. if(args.anchorInput){
  233. args.anchorInput = args.anchorInput.replace(/"/g, "&quot;");
  234. }
  235. if(!args.textInput){
  236. // WebKit doesn't work with double-click select unless there's
  237. // a space in the anchor text, so put a in the case of
  238. // empty desc.
  239. args.textInput = "&nbsp;";
  240. }
  241. }
  242. return args;
  243. },
  244. setValue: function(args){
  245. // summary:
  246. // Callback from the dialog when user presses "set" button.
  247. // tags:
  248. // private
  249. this._onCloseDialog();
  250. if(!this.editor.window.getSelection){
  251. // IE check without using user agent string.
  252. var sel = dijit.range.getSelection(this.editor.window);
  253. var range = sel.getRangeAt(0);
  254. var a = range.endContainer;
  255. if(a.nodeType === 3){
  256. // Text node, may be the link contents, so check parent.
  257. // This plugin doesn't really support nested HTML elements
  258. // in the link, it assumes all link content is text.
  259. a = a.parentNode;
  260. }
  261. if(a && (a.nodeName && a.nodeName.toLowerCase() !== "a")){
  262. // Stll nothing, one last thing to try on IE, as it might be 'img'
  263. // and thus considered a control.
  264. a = dojo.withGlobal(this.editor.window,
  265. "getSelectedElement", dijit._editor.selection, ["a"]);
  266. }
  267. if(a && (a.nodeName && a.nodeName.toLowerCase() === "a")){
  268. // Okay, we do have a match. IE, for some reason, sometimes pastes before
  269. // instead of removing the targetted paste-over element, so we unlink the
  270. // old one first. If we do not the <a> tag remains, but it has no content,
  271. // so isn't readily visible (but is wrong for the action).
  272. if(this.editor.queryCommandEnabled("unlink")){
  273. // Select all the link childent, then unlink. The following insert will
  274. // then replace the selected text.
  275. dojo.withGlobal(this.editor.window,
  276. "selectElementChildren", dijit._editor.selection, [a]);
  277. this.editor.execCommand("unlink");
  278. }
  279. }
  280. }
  281. // make sure values are properly escaped, etc.
  282. args = this._checkValues(args);
  283. this.editor.execCommand('inserthtml',
  284. dojo.string.substitute(this.htmlTemplate, args));
  285. },
  286. _onCloseDialog: function(){
  287. // summary:
  288. // Handler for close event on the dialog
  289. this.editor.focus();
  290. },
  291. _getCurrentValues: function(a){
  292. // summary:
  293. // Over-ride for getting the values to set in the dropdown.
  294. // a:
  295. // The anchor/link to process for data for the dropdown.
  296. // tags:
  297. // protected
  298. var anchor, text;
  299. if(a && a.tagName.toLowerCase() === "a" && dojo.attr(a, "name")){
  300. anchor = dojo.attr(a, "name");
  301. text = a.textContent || a.innerText;
  302. dojo.withGlobal(this.editor.window, "selectElement", dijit._editor.selection, [a, true]);
  303. }else{
  304. text = dojo.withGlobal(this.editor.window, dijit._editor.selection.getSelectedText);
  305. }
  306. return {anchorInput: anchor || '', textInput: text || ''}; //Object;
  307. },
  308. _onOpenDialog: function(){
  309. // summary:
  310. // Handler for when the dialog is opened.
  311. // If the caret is currently in a URL then populate the URL's info into the dialog.
  312. var a;
  313. if(!this.editor.window.getSelection){
  314. // IE is difficult to select the element in, using the range unified
  315. // API seems to work reasonably well.
  316. var sel = dijit.range.getSelection(this.editor.window);
  317. var range = sel.getRangeAt(0);
  318. a = range.endContainer;
  319. if(a.nodeType === 3){
  320. // Text node, may be the link contents, so check parent.
  321. // This plugin doesn't really support nested HTML elements
  322. // in the link, it assumes all link content is text.
  323. a = a.parentNode;
  324. }
  325. if(a && (a.nodeName && a.nodeName.toLowerCase() !== "a")){
  326. // Stll nothing, one last thing to try on IE, as it might be 'img'
  327. // and thus considered a control.
  328. a = dojo.withGlobal(this.editor.window,
  329. "getSelectedElement", dijit._editor.selection, ["a"]);
  330. }
  331. }else{
  332. a = dojo.withGlobal(this.editor.window,
  333. "getAncestorElement", dijit._editor.selection, ["a"]);
  334. }
  335. this.dropDown.reset();
  336. this._setButton.set("disabled", true);
  337. this.dropDown.set("value", this._getCurrentValues(a));
  338. },
  339. _onDblClick: function(e){
  340. // summary:
  341. // Function to define a behavior on double clicks on the element
  342. // type this dialog edits to select it and pop up the editor
  343. // dialog.
  344. // e: Object
  345. // The double-click event.
  346. // tags:
  347. // protected.
  348. if(e && e.target){
  349. var t = e.target;
  350. var tg = t.tagName? t.tagName.toLowerCase() : "";
  351. if(tg === "a" && dojo.attr(t, "name")){
  352. this.editor.onDisplayChanged();
  353. dojo.withGlobal(this.editor.window,
  354. "selectElement",
  355. dijit._editor.selection, [t]);
  356. setTimeout(dojo.hitch(this, function(){
  357. // Focus shift outside the event handler.
  358. // IE doesn't like focus changes in event handles.
  359. this.button.set("disabled", false);
  360. this.button.openDropDown();
  361. if(this.button.dropDown.focus){
  362. this.button.dropDown.focus();
  363. }
  364. }), 10);
  365. }
  366. }
  367. },
  368. _preDomFilter: function(node){
  369. // summary:
  370. // A filter to identify the 'a' tags and if they're anchors,
  371. // apply the right style to them.
  372. // node:
  373. // The node to search from.
  374. // tags:
  375. // private
  376. var ed = this.editor;
  377. dojo.withGlobal(ed.window, function(){
  378. dojo.query("a", ed.editNode).forEach(function(a){
  379. if(dojo.attr(a, "name") && !dojo.attr(a, "href")){
  380. if(!dojo.hasClass(a,"dijitEditorPluginInsertAnchorStyle")){
  381. dojo.addClass(a, "dijitEditorPluginInsertAnchorStyle");
  382. }
  383. }
  384. });
  385. });
  386. },
  387. _postDomFilter: function(node){
  388. // summary:
  389. // A filter to identify the 'a' tags and if they're anchors,
  390. // remove the class style that shows up in the editor from
  391. // them.
  392. // node:
  393. // The node to search from.
  394. // tags:
  395. // private
  396. var ed = this.editor;
  397. dojo.withGlobal(ed.window, function(){
  398. dojo.query("a", node).forEach(function(a){
  399. if(dojo.attr(a, "name") && !dojo.attr(a, "href")){
  400. if(dojo.hasClass(a,"dijitEditorPluginInsertAnchorStyle")){
  401. dojo.removeClass(a, "dijitEditorPluginInsertAnchorStyle");
  402. }
  403. }
  404. });
  405. });
  406. return node;
  407. }
  408. });
  409. // Register this plugin.
  410. dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){
  411. if(o.plugin){ return; }
  412. var name = o.args.name;
  413. if(name) { name = name.toLowerCase(); }
  414. if(name === "insertanchor"){
  415. o.plugin = new dojox.editor.plugins.InsertAnchor();
  416. }
  417. });
  418. return dojox.editor.plugins.InsertAnchor;
  419. });