4 * Released under LGPL License.
5 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
11 /*global tinymce:true */
13 tinymce.ThemeManager.add('modern', function(editor) {
14 var self = this, settings = editor.settings, Factory = tinymce.ui.Factory,
15 each = tinymce.each, DOM = tinymce.DOM, Rect = tinymce.ui.Rect, FloatPanel = tinymce.ui.FloatPanel;
19 file: {title: 'File', items: 'newdocument'},
20 edit: {title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall'},
21 insert: {title: 'Insert', items: '|'},
22 view: {title: 'View', items: 'visualaid |'},
23 format: {title: 'Format', items: 'bold italic underline strikethrough superscript subscript | formats | removeformat'},
24 table: {title: 'Table'},
25 tools: {title: 'Tools'}
28 var defaultToolbar = "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | " +
29 "bullist numlist outdent indent | link image";
31 function createToolbar(items, size) {
32 var toolbarItems = [], buttonGroup;
38 each(items.split(/[ ,]/), function(item) {
41 function bindSelectorChanged() {
42 var selection = editor.selection;
44 if (itemName == "bullist") {
45 selection.selectorChanged('ul > li', function(state, args) {
46 var nodeName, i = args.parents.length;
49 nodeName = args.parents[i].nodeName;
50 if (nodeName == "OL" || nodeName == "UL") {
55 item.active(state && nodeName == "UL");
59 if (itemName == "numlist") {
60 selection.selectorChanged('ol > li', function(state, args) {
61 var nodeName, i = args.parents.length;
64 nodeName = args.parents[i].nodeName;
65 if (nodeName == "OL" || nodeName == "UL") {
70 item.active(state && nodeName == "OL");
74 if (item.settings.stateSelector) {
75 selection.selectorChanged(item.settings.stateSelector, function(state) {
80 if (item.settings.disabledStateSelector) {
81 selection.selectorChanged(item.settings.disabledStateSelector, function(state) {
90 if (Factory.has(item)) {
91 item = {type: item, size: size};
92 toolbarItems.push(item);
96 buttonGroup = {type: 'buttongroup', items: []};
97 toolbarItems.push(buttonGroup);
100 if (editor.buttons[item]) {
101 // TODO: Move control creation to some UI class
103 item = editor.buttons[itemName];
105 if (typeof item == "function") {
109 item.type = item.type || 'button';
112 item = Factory.create(item);
113 buttonGroup.items.push(item);
115 if (editor.initialized) {
116 bindSelectorChanged();
118 editor.on('init', bindSelectorChanged);
133 * Creates the toolbars from config and returns a toolbar array.
135 * @param {String} size Optional toolbar item size.
136 * @return {Array} Array with toolbars.
138 function createToolbars(size) {
141 function addToolbar(items) {
143 toolbars.push(createToolbar(items, size));
148 // Convert toolbar array to multiple options
149 if (tinymce.isArray(settings.toolbar)) {
150 // Empty toolbar array is the same as a disabled toolbar
151 if (settings.toolbar.length === 0) {
155 tinymce.each(settings.toolbar, function(toolbar, i) {
156 settings["toolbar" + (i + 1)] = toolbar;
159 delete settings.toolbar;
162 // Generate toolbar<n>
163 for (var i = 1; i < 10; i++) {
164 if (!addToolbar(settings["toolbar" + i])) {
169 // Generate toolbar or default toolbar unless it's disabled
170 if (!toolbars.length && settings.toolbar !== false) {
171 addToolbar(settings.toolbar || defaultToolbar);
174 if (toolbars.length) {
178 classes: "toolbar-grp",
187 * Creates the menu buttons based on config.
189 * @return {Array} Menu buttons array.
191 function createMenuButtons() {
192 var name, menuButtons = [];
194 function createMenuItem(name) {
201 menuItem = editor.menuItems[name];
206 function createMenu(context) {
207 var menuButton, menu, menuItems, isUserDefined, removedMenuItems;
209 removedMenuItems = tinymce.makeMap((settings.removed_menuitems || '').split(/[ ,]/));
213 menu = settings.menu[context];
214 isUserDefined = true;
216 menu = defaultMenus[context];
220 menuButton = {text: menu.title};
223 // Default/user defined items
224 each((menu.items || '').split(/[ ,]/), function(item) {
225 var menuItem = createMenuItem(item);
227 if (menuItem && !removedMenuItems[item]) {
228 menuItems.push(createMenuItem(item));
232 // Added though context
233 if (!isUserDefined) {
234 each(editor.menuItems, function(menuItem) {
235 if (menuItem.context == context) {
236 if (menuItem.separator == 'before') {
237 menuItems.push({text: '|'});
240 if (menuItem.prependToContext) {
241 menuItems.unshift(menuItem);
243 menuItems.push(menuItem);
246 if (menuItem.separator == 'after') {
247 menuItems.push({text: '|'});
253 for (var i = 0; i < menuItems.length; i++) {
254 if (menuItems[i].text == '|') {
255 if (i === 0 || i == menuItems.length - 1) {
256 menuItems.splice(i, 1);
261 menuButton.menu = menuItems;
263 if (!menuButton.menu.length) {
271 var defaultMenuBar = [];
273 for (name in settings.menu) {
274 defaultMenuBar.push(name);
277 for (name in defaultMenus) {
278 defaultMenuBar.push(name);
282 var enabledMenuNames = typeof settings.menubar == "string" ? settings.menubar.split(/[ ,]/) : defaultMenuBar;
283 for (var i = 0; i < enabledMenuNames.length; i++) {
284 var menu = enabledMenuNames[i];
285 menu = createMenu(menu);
288 menuButtons.push(menu);
296 * Adds accessibility shortcut keys to panel.
298 * @param {tinymce.ui.Panel} panel Panel to add focus to.
300 function addAccessibilityKeys(panel) {
301 function focus(type) {
302 var item = panel.find(type)[0];
309 editor.shortcuts.add('Alt+F9', '', function() {
313 editor.shortcuts.add('Alt+F10', '', function() {
317 editor.shortcuts.add('Alt+F11', '', function() {
318 focus('elementpath');
321 panel.on('cancel', function() {
327 * Resizes the editor to the specified width, height.
329 function resizeTo(width, height) {
330 var containerElm, iframeElm, containerSize, iframeSize;
332 function getSize(elm) {
334 width: elm.clientWidth,
335 height: elm.clientHeight
339 containerElm = editor.getContainer();
340 iframeElm = editor.getContentAreaContainer().firstChild;
341 containerSize = getSize(containerElm);
342 iframeSize = getSize(iframeElm);
344 if (width !== null) {
345 width = Math.max(settings.min_width || 100, width);
346 width = Math.min(settings.max_width || 0xFFFF, width);
348 DOM.setStyle(containerElm, 'width', width + (containerSize.width - iframeSize.width));
349 DOM.setStyle(iframeElm, 'width', width);
352 height = Math.max(settings.min_height || 100, height);
353 height = Math.min(settings.max_height || 0xFFFF, height);
354 DOM.setStyle(iframeElm, 'height', height);
356 editor.fire('ResizeEditor');
359 function resizeBy(dw, dh) {
360 var elm = editor.getContentAreaContainer();
361 self.resizeTo(elm.clientWidth + dw, elm.clientHeight + dh);
365 * Handles contextual toolbars.
367 function addContextualToolbars() {
370 function getContextToolbars() {
371 return editor.contextToolbars || [];
374 function getElementRect(elm) {
375 var pos, targetRect, root;
377 pos = tinymce.DOM.getPos(editor.getContentAreaContainer());
378 targetRect = editor.dom.getRect(elm);
379 root = editor.dom.getRoot();
381 // Adjust targetPos for scrolling in the editor
382 if (root.nodeName == 'BODY') {
383 targetRect.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
384 targetRect.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
387 targetRect.x += pos.x;
388 targetRect.y += pos.y;
393 function hideAllFloatingPanels() {
394 each(editor.contextToolbars, function(toolbar) {
396 toolbar.panel.hide();
401 function reposition(match) {
402 var relPos, panelRect, elementRect, contentAreaRect, panel, relRect, testPositions;
404 if (editor.removed) {
408 if (!match || !match.toolbar.panel) {
409 hideAllFloatingPanels();
419 panel = match.toolbar.panel;
422 elementRect = getElementRect(match.element);
423 panelRect = tinymce.DOM.getRect(panel.getEl());
424 contentAreaRect = tinymce.DOM.getRect(editor.getContentAreaContainer() || editor.getBody());
426 if (!editor.inline) {
427 contentAreaRect.w = editor.getDoc().documentElement.offsetWidth;
430 // Inflate the elementRect so it doesn't get placed above resize handles
431 if (editor.selection.controlSelection.isResizable(match.element)) {
432 elementRect = Rect.inflate(elementRect, 0, 7);
435 relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, testPositions);
438 each(testPositions.concat('inside'), function(pos) {
439 panel.classes.toggle('tinymce-inline-' + pos, pos == relPos);
442 relRect = Rect.relativePosition(panelRect, elementRect, relPos);
443 panel.moveTo(relRect.x, relRect.y);
445 each(testPositions, function(pos) {
446 panel.classes.toggle('tinymce-inline-' + pos, false);
449 panel.classes.toggle('tinymce-inline-inside', true);
451 elementRect = Rect.intersect(contentAreaRect, elementRect);
454 relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, [
455 'tc-tc', 'tl-tl', 'tr-tr'
459 relRect = Rect.relativePosition(panelRect, elementRect, relPos);
460 panel.moveTo(relRect.x, relRect.y);
462 panel.moveTo(elementRect.x, elementRect.y);
469 //drawRect(contentAreaRect, 'blue');
470 //drawRect(elementRect, 'red');
471 //drawRect(panelRect, 'green');
474 function repositionHandler() {
476 if (editor.selection) {
477 reposition(findFrontMostMatch(editor.selection.getNode()));
481 if (window.requestAnimationFrame) {
482 window.requestAnimationFrame(execute);
488 function bindScrollEvent() {
489 if (!scrollContainer) {
490 scrollContainer = editor.selection.getScrollContainer() || editor.getWin();
491 tinymce.$(scrollContainer).on('scroll', repositionHandler);
493 editor.on('remove', function() {
494 tinymce.$(scrollContainer).off('scroll');
499 function showContextToolbar(match) {
502 if (match.toolbar.panel) {
503 match.toolbar.panel.show();
510 panel = Factory.create({
513 classes: 'tinymce tinymce-inline',
521 items: createToolbar(match.toolbar.items)
524 match.toolbar.panel = panel;
525 panel.renderTo(document.body).reflow();
529 function hideAllContextToolbars() {
530 tinymce.each(getContextToolbars(), function(toolbar) {
532 toolbar.panel.hide();
537 function findFrontMostMatch(targetElm) {
538 var i, y, parentsAndSelf, toolbars = getContextToolbars();
540 parentsAndSelf = editor.$(targetElm).parents().add(targetElm);
541 for (i = parentsAndSelf.length - 1; i >= 0; i--) {
542 for (y = toolbars.length - 1; y >= 0; y--) {
543 if (toolbars[y].predicate(parentsAndSelf[i])) {
545 toolbar: toolbars[y],
546 element: parentsAndSelf[i]
555 editor.on('click keyup blur', function() {
556 // Needs to be delayed to avoid Chrome img focus out bug
557 window.setTimeout(function() {
560 if (editor.removed) {
564 match = findFrontMostMatch(editor.selection.getNode());
566 showContextToolbar(match);
568 hideAllContextToolbars();
573 editor.on('ObjectResizeStart', function() {
574 var match = findFrontMostMatch(editor.selection.getNode());
576 if (match && match.toolbar.panel) {
577 match.toolbar.panel.hide();
581 editor.on('nodeChange ResizeEditor ResizeWindow', repositionHandler);
583 editor.on('remove', function() {
584 tinymce.each(getContextToolbars(), function(toolbar) {
586 toolbar.panel.remove();
590 editor.contextToolbars = {};
595 * Renders the inline editor UI.
597 * @return {Object} Name/value object with theme data.
599 function renderInlineUI(args) {
600 var panel, inlineToolbarContainer;
602 if (settings.fixed_toolbar_container) {
603 inlineToolbarContainer = DOM.select(settings.fixed_toolbar_container)[0];
606 function reposition() {
607 if (panel && panel.moveRel && panel.visible() && !panel._fixed) {
608 // TODO: This is kind of ugly and doesn't handle multiple scrollable elements
609 var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody();
610 var deltaX = 0, deltaY = 0;
612 if (scrollContainer) {
613 var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer);
615 deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x);
616 deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y);
619 panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl', 'tr-br']).moveBy(deltaX, deltaY);
627 DOM.addClass(editor.getBody(), 'mce-edit-focus');
633 // We require two events as the inline float panel based toolbar does not have autohide=true
636 // All other autohidden float panels will be closed below.
637 FloatPanel.hideAll();
639 DOM.removeClass(editor.getBody(), 'mce-edit-focus');
645 if (!panel.visible()) {
652 // Render a plain panel inside the inlineToolbarContainer if it's defined
653 panel = self.panel = Factory.create({
654 type: inlineToolbarContainer ? 'panel' : 'floatpanel',
656 classes: 'tinymce tinymce-inline',
662 fixed: !!inlineToolbarContainer,
665 settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
666 createToolbars(settings.toolbar_items_size)
671 /*if (settings.statusbar !== false) {
672 panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [
673 {type: 'elementpath'}
677 editor.fire('BeforeRenderUI');
678 panel.renderTo(inlineToolbarContainer || document.body).reflow();
680 addAccessibilityKeys(panel);
682 addContextualToolbars();
684 editor.on('nodeChange', reposition);
685 editor.on('activate', show);
686 editor.on('deactivate', hide);
688 editor.nodeChanged();
691 settings.content_editable = true;
693 editor.on('focus', function() {
694 // Render only when the CSS file has been loaded
695 if (args.skinUiCss) {
696 tinymce.DOM.styleSheetLoader.load(args.skinUiCss, render, render);
702 editor.on('blur hide', hide);
704 // Remove the panel when the editor is removed
705 editor.on('remove', function() {
713 if (args.skinUiCss) {
714 tinymce.DOM.styleSheetLoader.load(args.skinUiCss);
721 * Renders the iframe editor UI.
723 * @param {Object} args Details about target element etc.
724 * @return {Object} Name/value object with theme data.
726 function renderIframeUI(args) {
727 var panel, resizeHandleCtrl, startSize;
729 if (args.skinUiCss) {
730 tinymce.DOM.loadCSS(args.skinUiCss);
734 panel = self.panel = Factory.create({
738 style: 'visibility: hidden',
742 settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
743 createToolbars(settings.toolbar_items_size),
744 {type: 'panel', name: 'iframe', layout: 'stack', classes: 'edit-area', html: '', border: '1 0 0 0'}
748 if (settings.resize !== false) {
750 type: 'resizehandle',
751 direction: settings.resize,
753 onResizeStart: function() {
754 var elm = editor.getContentAreaContainer().firstChild;
757 width: elm.clientWidth,
758 height: elm.clientHeight
762 onResize: function(e) {
763 if (settings.resize == 'both') {
764 resizeTo(startSize.width + e.deltaX, startSize.height + e.deltaY);
766 resizeTo(null, startSize.height + e.deltaY);
772 // Add statusbar if needed
773 if (settings.statusbar !== false) {
774 panel.add({type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [
775 {type: 'elementpath'},
780 if (settings.readonly) {
781 panel.find('*').disabled(true);
784 editor.fire('BeforeRenderUI');
785 panel.renderBefore(args.targetNode).reflow();
787 if (settings.width) {
788 tinymce.DOM.setStyle(panel.getEl(), 'width', settings.width);
791 // Remove the panel when the editor is removed
792 editor.on('remove', function() {
797 // Add accesibility shortcuts
798 addAccessibilityKeys(panel);
799 addContextualToolbars();
802 iframeContainer: panel.find('#iframe')[0].getEl(),
803 editorContainer: panel.getEl()
808 * Renders the UI for the theme. This gets called by the editor.
810 * @param {Object} args Details about target element etc.
811 * @return {Object} Theme UI data items.
813 self.renderUI = function(args) {
814 var skin = settings.skin !== false ? settings.skin || 'lightgray' : false;
817 var skinUrl = settings.skin_url;
820 skinUrl = editor.documentBaseURI.toAbsolute(skinUrl);
822 skinUrl = tinymce.baseURL + '/skins/' + skin;
825 // Load special skin for IE7
826 // TODO: Remove this when we drop IE7 support
827 if (tinymce.Env.documentMode <= 7) {
828 args.skinUiCss = skinUrl + '/skin.ie7.min.css';
830 args.skinUiCss = skinUrl + '/skin.min.css';
833 // Load content.min.css or content.inline.min.css
834 editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css');
837 // Handle editor setProgressState change
838 editor.on('ProgressState', function(e) {
839 self.throbber = self.throbber || new tinymce.ui.Throbber(self.panel.getEl('body'));
842 self.throbber.show(e.time);
844 self.throbber.hide();
848 if (settings.inline) {
849 return renderInlineUI(args);
852 return renderIframeUI(args);
855 self.resizeTo = resizeTo;
856 self.resizeBy = resizeBy;