theme.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /**
  2. * theme.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /*global tinymce:true */
  11. tinymce.ThemeManager.add('modern', function(editor) {
  12. var self = this, settings = editor.settings, Factory = tinymce.ui.Factory, each = tinymce.each, DOM = tinymce.DOM;
  13. // Default menus
  14. var defaultMenus = {
  15. file: {title: 'File', items: 'newdocument'},
  16. edit: {title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall'},
  17. insert: {title: 'Insert', items: '|'},
  18. view: {title: 'View', items: 'visualaid |'},
  19. format: {title: 'Format', items: 'bold italic underline strikethrough superscript subscript | formats | removeformat'},
  20. table: {title: 'Table'},
  21. tools: {title: 'Tools'}
  22. };
  23. var defaultToolbar = "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | " +
  24. "bullist numlist outdent indent | link image";
  25. /**
  26. * Creates the toolbars from config and returns a toolbar array.
  27. *
  28. * @return {Array} Array with toolbars.
  29. */
  30. function createToolbars() {
  31. var toolbars = [];
  32. function addToolbar(items) {
  33. var toolbarItems = [], buttonGroup;
  34. if (!items) {
  35. return;
  36. }
  37. each(items.split(/[ ,]/), function(item) {
  38. var itemName;
  39. function bindSelectorChanged() {
  40. var selection = editor.selection;
  41. if (itemName == "bullist") {
  42. selection.selectorChanged('ul > li', function(state, args) {
  43. var nodeName, i = args.parents.length;
  44. while (i--) {
  45. nodeName = args.parents[i].nodeName;
  46. if (nodeName == "OL" || nodeName == "UL") {
  47. break;
  48. }
  49. }
  50. item.active(state && nodeName == "UL");
  51. });
  52. }
  53. if (itemName == "numlist") {
  54. selection.selectorChanged('ol > li', function(state, args) {
  55. var nodeName, i = args.parents.length;
  56. while (i--) {
  57. nodeName = args.parents[i].nodeName;
  58. if (nodeName == "OL" || nodeName == "UL") {
  59. break;
  60. }
  61. }
  62. item.active(state && nodeName == "OL");
  63. });
  64. }
  65. if (item.settings.stateSelector) {
  66. selection.selectorChanged(item.settings.stateSelector, function(state) {
  67. item.active(state);
  68. }, true);
  69. }
  70. if (item.settings.disabledStateSelector) {
  71. selection.selectorChanged(item.settings.disabledStateSelector, function(state) {
  72. item.disabled(state);
  73. });
  74. }
  75. }
  76. if (item == "|") {
  77. buttonGroup = null;
  78. } else {
  79. if (Factory.has(item)) {
  80. item = {type: item};
  81. if (settings.toolbar_items_size) {
  82. item.size = settings.toolbar_items_size;
  83. }
  84. toolbarItems.push(item);
  85. buttonGroup = null;
  86. } else {
  87. if (!buttonGroup) {
  88. buttonGroup = {type: 'buttongroup', items: []};
  89. toolbarItems.push(buttonGroup);
  90. }
  91. if (editor.buttons[item]) {
  92. // TODO: Move control creation to some UI class
  93. itemName = item;
  94. item = editor.buttons[itemName];
  95. if (typeof(item) == "function") {
  96. item = item();
  97. }
  98. item.type = item.type || 'button';
  99. if (settings.toolbar_items_size) {
  100. item.size = settings.toolbar_items_size;
  101. }
  102. item = Factory.create(item);
  103. buttonGroup.items.push(item);
  104. if (editor.initialized) {
  105. bindSelectorChanged();
  106. } else {
  107. editor.on('init', bindSelectorChanged);
  108. }
  109. }
  110. }
  111. }
  112. });
  113. toolbars.push({type: 'toolbar', layout: 'flow', items: toolbarItems});
  114. return true;
  115. }
  116. // Convert toolbar array to multiple options
  117. if (tinymce.isArray(settings.toolbar)) {
  118. // Empty toolbar array is the same as a disabled toolbar
  119. if (settings.toolbar.length === 0) {
  120. return;
  121. }
  122. tinymce.each(settings.toolbar, function(toolbar, i) {
  123. settings["toolbar" + (i + 1)] = toolbar;
  124. });
  125. delete settings.toolbar;
  126. }
  127. // Generate toolbar<n>
  128. for (var i = 1; i < 10; i++) {
  129. if (!addToolbar(settings["toolbar" + i])) {
  130. break;
  131. }
  132. }
  133. // Generate toolbar or default toolbar unless it's disabled
  134. if (!toolbars.length && settings.toolbar !== false) {
  135. addToolbar(settings.toolbar || defaultToolbar);
  136. }
  137. if (toolbars.length) {
  138. return {
  139. type: 'panel',
  140. layout: 'stack',
  141. classes: "toolbar-grp",
  142. ariaRoot: true,
  143. ariaRemember: true,
  144. items: toolbars
  145. };
  146. }
  147. }
  148. /**
  149. * Creates the menu buttons based on config.
  150. *
  151. * @return {Array} Menu buttons array.
  152. */
  153. function createMenuButtons() {
  154. var name, menuButtons = [];
  155. function createMenuItem(name) {
  156. var menuItem;
  157. if (name == '|') {
  158. return {text: '|'};
  159. }
  160. menuItem = editor.menuItems[name];
  161. return menuItem;
  162. }
  163. function createMenu(context) {
  164. var menuButton, menu, menuItems, isUserDefined, removedMenuItems;
  165. removedMenuItems = tinymce.makeMap((settings.removed_menuitems || '').split(/[ ,]/));
  166. // User defined menu
  167. if (settings.menu) {
  168. menu = settings.menu[context];
  169. isUserDefined = true;
  170. } else {
  171. menu = defaultMenus[context];
  172. }
  173. if (menu) {
  174. menuButton = {text: menu.title};
  175. menuItems = [];
  176. // Default/user defined items
  177. each((menu.items || '').split(/[ ,]/), function(item) {
  178. var menuItem = createMenuItem(item);
  179. if (menuItem && !removedMenuItems[item]) {
  180. menuItems.push(createMenuItem(item));
  181. }
  182. });
  183. // Added though context
  184. if (!isUserDefined) {
  185. each(editor.menuItems, function(menuItem) {
  186. if (menuItem.context == context) {
  187. if (menuItem.separator == 'before') {
  188. menuItems.push({text: '|'});
  189. }
  190. if (menuItem.prependToContext) {
  191. menuItems.unshift(menuItem);
  192. } else {
  193. menuItems.push(menuItem);
  194. }
  195. if (menuItem.separator == 'after') {
  196. menuItems.push({text: '|'});
  197. }
  198. }
  199. });
  200. }
  201. for (var i = 0; i < menuItems.length; i++) {
  202. if (menuItems[i].text == '|') {
  203. if (i === 0 || i == menuItems.length - 1) {
  204. menuItems.splice(i, 1);
  205. }
  206. }
  207. }
  208. menuButton.menu = menuItems;
  209. if (!menuButton.menu.length) {
  210. return null;
  211. }
  212. }
  213. return menuButton;
  214. }
  215. var defaultMenuBar = [];
  216. if (settings.menu) {
  217. for (name in settings.menu) {
  218. defaultMenuBar.push(name);
  219. }
  220. } else {
  221. for (name in defaultMenus) {
  222. defaultMenuBar.push(name);
  223. }
  224. }
  225. var enabledMenuNames = typeof(settings.menubar) == "string" ? settings.menubar.split(/[ ,]/) : defaultMenuBar;
  226. for (var i = 0; i < enabledMenuNames.length; i++) {
  227. var menu = enabledMenuNames[i];
  228. menu = createMenu(menu);
  229. if (menu) {
  230. menuButtons.push(menu);
  231. }
  232. }
  233. return menuButtons;
  234. }
  235. /**
  236. * Adds accessibility shortcut keys to panel.
  237. *
  238. * @param {tinymce.ui.Panel} panel Panel to add focus to.
  239. */
  240. function addAccessibilityKeys(panel) {
  241. function focus(type) {
  242. var item = panel.find(type)[0];
  243. if (item) {
  244. item.focus(true);
  245. }
  246. }
  247. editor.shortcuts.add('Alt+F9', '', function() {
  248. focus('menubar');
  249. });
  250. editor.shortcuts.add('Alt+F10', '', function() {
  251. focus('toolbar');
  252. });
  253. editor.shortcuts.add('Alt+F11', '', function() {
  254. focus('elementpath');
  255. });
  256. panel.on('cancel', function() {
  257. editor.focus();
  258. });
  259. }
  260. /**
  261. * Resizes the editor to the specified width, height.
  262. */
  263. function resizeTo(width, height) {
  264. var containerElm, iframeElm, containerSize, iframeSize;
  265. function getSize(elm) {
  266. return {
  267. width: elm.clientWidth,
  268. height: elm.clientHeight
  269. };
  270. }
  271. containerElm = editor.getContainer();
  272. iframeElm = editor.getContentAreaContainer().firstChild;
  273. containerSize = getSize(containerElm);
  274. iframeSize = getSize(iframeElm);
  275. if (width !== null) {
  276. width = Math.max(settings.min_width || 100, width);
  277. width = Math.min(settings.max_width || 0xFFFF, width);
  278. DOM.css(containerElm, 'width', width + (containerSize.width - iframeSize.width));
  279. DOM.css(iframeElm, 'width', width);
  280. }
  281. height = Math.max(settings.min_height || 100, height);
  282. height = Math.min(settings.max_height || 0xFFFF, height);
  283. DOM.css(iframeElm, 'height', height);
  284. editor.fire('ResizeEditor');
  285. }
  286. function resizeBy(dw, dh) {
  287. var elm = editor.getContentAreaContainer();
  288. self.resizeTo(elm.clientWidth + dw, elm.clientHeight + dh);
  289. }
  290. /**
  291. * Renders the inline editor UI.
  292. *
  293. * @return {Object} Name/value object with theme data.
  294. */
  295. function renderInlineUI(args) {
  296. var panel, inlineToolbarContainer;
  297. if (settings.fixed_toolbar_container) {
  298. inlineToolbarContainer = DOM.select(settings.fixed_toolbar_container)[0];
  299. }
  300. function reposition() {
  301. if (panel && panel.moveRel && panel.visible() && !panel._fixed) {
  302. // TODO: This is kind of ugly and doesn't handle multiple scrollable elements
  303. var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody();
  304. var deltaX = 0, deltaY = 0;
  305. if (scrollContainer) {
  306. var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer);
  307. deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x);
  308. deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y);
  309. }
  310. panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl']).moveBy(deltaX, deltaY);
  311. }
  312. }
  313. function show() {
  314. if (panel) {
  315. panel.show();
  316. reposition();
  317. DOM.addClass(editor.getBody(), 'mce-edit-focus');
  318. }
  319. }
  320. function hide() {
  321. if (panel) {
  322. panel.hide();
  323. DOM.removeClass(editor.getBody(), 'mce-edit-focus');
  324. }
  325. }
  326. function render() {
  327. if (panel) {
  328. if (!panel.visible()) {
  329. show();
  330. }
  331. return;
  332. }
  333. // Render a plain panel inside the inlineToolbarContainer if it's defined
  334. panel = self.panel = Factory.create({
  335. type: inlineToolbarContainer ? 'panel' : 'floatpanel',
  336. role: 'application',
  337. classes: 'tinymce tinymce-inline',
  338. layout: 'flex',
  339. direction: 'column',
  340. align: 'stretch',
  341. autohide: false,
  342. autofix: true,
  343. fixed: !!inlineToolbarContainer,
  344. border: 1,
  345. items: [
  346. settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
  347. createToolbars()
  348. ]
  349. });
  350. // Add statusbar
  351. /*if (settings.statusbar !== false) {
  352. panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [
  353. {type: 'elementpath'}
  354. ]});
  355. }*/
  356. editor.fire('BeforeRenderUI');
  357. panel.renderTo(inlineToolbarContainer || document.body).reflow();
  358. addAccessibilityKeys(panel);
  359. show();
  360. editor.on('nodeChange', reposition);
  361. editor.on('activate', show);
  362. editor.on('deactivate', hide);
  363. editor.nodeChanged();
  364. }
  365. settings.content_editable = true;
  366. editor.on('focus', function() {
  367. // Render only when the CSS file has been loaded
  368. if (args.skinUiCss) {
  369. tinymce.DOM.styleSheetLoader.load(args.skinUiCss, render, render);
  370. } else {
  371. render();
  372. }
  373. });
  374. editor.on('blur', hide);
  375. // Remove the panel when the editor is removed
  376. editor.on('remove', function() {
  377. if (panel) {
  378. panel.remove();
  379. panel = null;
  380. }
  381. });
  382. // Preload skin css
  383. if (args.skinUiCss) {
  384. tinymce.DOM.styleSheetLoader.load(args.skinUiCss);
  385. }
  386. return {};
  387. }
  388. /**
  389. * Renders the iframe editor UI.
  390. *
  391. * @param {Object} args Details about target element etc.
  392. * @return {Object} Name/value object with theme data.
  393. */
  394. function renderIframeUI(args) {
  395. var panel, resizeHandleCtrl, startSize;
  396. if (args.skinUiCss) {
  397. tinymce.DOM.loadCSS(args.skinUiCss);
  398. }
  399. // Basic UI layout
  400. panel = self.panel = Factory.create({
  401. type: 'panel',
  402. role: 'application',
  403. classes: 'tinymce',
  404. style: 'visibility: hidden',
  405. layout: 'stack',
  406. border: 1,
  407. items: [
  408. settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
  409. createToolbars(),
  410. {type: 'panel', name: 'iframe', layout: 'stack', classes: 'edit-area', html: '', border: '1 0 0 0'}
  411. ]
  412. });
  413. if (settings.resize !== false) {
  414. resizeHandleCtrl = {
  415. type: 'resizehandle',
  416. direction: settings.resize,
  417. onResizeStart: function() {
  418. var elm = editor.getContentAreaContainer().firstChild;
  419. startSize = {
  420. width: elm.clientWidth,
  421. height: elm.clientHeight
  422. };
  423. },
  424. onResize: function(e) {
  425. if (settings.resize == 'both') {
  426. resizeTo(startSize.width + e.deltaX, startSize.height + e.deltaY);
  427. } else {
  428. resizeTo(null, startSize.height + e.deltaY);
  429. }
  430. }
  431. };
  432. }
  433. // Add statusbar if needed
  434. if (settings.statusbar !== false) {
  435. panel.add({type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [
  436. {type: 'elementpath'},
  437. resizeHandleCtrl
  438. ]});
  439. }
  440. if (settings.readonly) {
  441. panel.find('*').disabled(true);
  442. }
  443. editor.fire('BeforeRenderUI');
  444. panel.renderBefore(args.targetNode).reflow();
  445. if (settings.width) {
  446. tinymce.DOM.setStyle(panel.getEl(), 'width', settings.width);
  447. }
  448. // Remove the panel when the editor is removed
  449. editor.on('remove', function() {
  450. panel.remove();
  451. panel = null;
  452. });
  453. // Add accesibility shortkuts
  454. addAccessibilityKeys(panel);
  455. return {
  456. iframeContainer: panel.find('#iframe')[0].getEl(),
  457. editorContainer: panel.getEl()
  458. };
  459. }
  460. /**
  461. * Renders the UI for the theme. This gets called by the editor.
  462. *
  463. * @param {Object} args Details about target element etc.
  464. * @return {Object} Theme UI data items.
  465. */
  466. self.renderUI = function(args) {
  467. var skin = settings.skin !== false ? settings.skin || 'lightgray' : false;
  468. if (skin) {
  469. var skinUrl = settings.skin_url;
  470. if (skinUrl) {
  471. skinUrl = editor.documentBaseURI.toAbsolute(skinUrl);
  472. } else {
  473. skinUrl = tinymce.baseURL + '/skins/' + skin;
  474. }
  475. // Load special skin for IE7
  476. // TODO: Remove this when we drop IE7 support
  477. if (tinymce.Env.documentMode <= 7) {
  478. args.skinUiCss = skinUrl + '/skin.ie7.min.css';
  479. } else {
  480. args.skinUiCss = skinUrl + '/skin.min.css';
  481. }
  482. // Load content.min.css or content.inline.min.css
  483. editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css');
  484. }
  485. // Handle editor setProgressState change
  486. editor.on('ProgressState', function(e) {
  487. self.throbber = self.throbber || new tinymce.ui.Throbber(self.panel.getEl('body'));
  488. if (e.state) {
  489. self.throbber.show(e.time);
  490. } else {
  491. self.throbber.hide();
  492. }
  493. });
  494. if (settings.inline) {
  495. return renderInlineUI(args);
  496. }
  497. return renderIframeUI(args);
  498. };
  499. self.resizeTo = resizeTo;
  500. self.resizeBy = resizeBy;
  501. });