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) {
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)) {
93 if (settings.toolbar_items_size) {
94 item.size = settings.toolbar_items_size;
97 toolbarItems.push(item);
101 buttonGroup = {type: 'buttongroup', items: []};
102 toolbarItems.push(buttonGroup);
105 if (editor.buttons[item]) {
106 // TODO: Move control creation to some UI class
108 item = editor.buttons[itemName];
110 if (typeof item == "function") {
114 item.type = item.type || 'button';
116 if (settings.toolbar_items_size) {
117 item.size = settings.toolbar_items_size;
120 item = Factory.create(item);
121 buttonGroup.items.push(item);
123 if (editor.initialized) {
124 bindSelectorChanged();
126 editor.on('init', bindSelectorChanged);
141 * Creates the toolbars from config and returns a toolbar array.
143 * @return {Array} Array with toolbars.
145 function createToolbars() {
148 function addToolbar(items) {
150 toolbars.push(createToolbar(items));
155 // Convert toolbar array to multiple options
156 if (tinymce.isArray(settings.toolbar)) {
157 // Empty toolbar array is the same as a disabled toolbar
158 if (settings.toolbar.length === 0) {
162 tinymce.each(settings.toolbar, function(toolbar, i) {
163 settings["toolbar" + (i + 1)] = toolbar;
166 delete settings.toolbar;
169 // Generate toolbar<n>
170 for (var i = 1; i < 10; i++) {
171 if (!addToolbar(settings["toolbar" + i])) {
176 // Generate toolbar or default toolbar unless it's disabled
177 if (!toolbars.length && settings.toolbar !== false) {
178 addToolbar(settings.toolbar || defaultToolbar);
181 if (toolbars.length) {
185 classes: "toolbar-grp",
194 * Creates the menu buttons based on config.
196 * @return {Array} Menu buttons array.
198 function createMenuButtons() {
199 var name, menuButtons = [];
201 function createMenuItem(name) {
208 menuItem = editor.menuItems[name];
213 function createMenu(context) {
214 var menuButton, menu, menuItems, isUserDefined, removedMenuItems;
216 removedMenuItems = tinymce.makeMap((settings.removed_menuitems || '').split(/[ ,]/));
220 menu = settings.menu[context];
221 isUserDefined = true;
223 menu = defaultMenus[context];
227 menuButton = {text: menu.title};
230 // Default/user defined items
231 each((menu.items || '').split(/[ ,]/), function(item) {
232 var menuItem = createMenuItem(item);
234 if (menuItem && !removedMenuItems[item]) {
235 menuItems.push(createMenuItem(item));
239 // Added though context
240 if (!isUserDefined) {
241 each(editor.menuItems, function(menuItem) {
242 if (menuItem.context == context) {
243 if (menuItem.separator == 'before') {
244 menuItems.push({text: '|'});
247 if (menuItem.prependToContext) {
248 menuItems.unshift(menuItem);
250 menuItems.push(menuItem);
253 if (menuItem.separator == 'after') {
254 menuItems.push({text: '|'});
260 for (var i = 0; i < menuItems.length; i++) {
261 if (menuItems[i].text == '|') {
262 if (i === 0 || i == menuItems.length - 1) {
263 menuItems.splice(i, 1);
268 menuButton.menu = menuItems;
270 if (!menuButton.menu.length) {
278 var defaultMenuBar = [];
280 for (name in settings.menu) {
281 defaultMenuBar.push(name);
284 for (name in defaultMenus) {
285 defaultMenuBar.push(name);
289 var enabledMenuNames = typeof settings.menubar == "string" ? settings.menubar.split(/[ ,]/) : defaultMenuBar;
290 for (var i = 0; i < enabledMenuNames.length; i++) {
291 var menu = enabledMenuNames[i];
292 menu = createMenu(menu);
295 menuButtons.push(menu);
303 * Adds accessibility shortcut keys to panel.
305 * @param {tinymce.ui.Panel} panel Panel to add focus to.
307 function addAccessibilityKeys(panel) {
308 function focus(type) {
309 var item = panel.find(type)[0];
316 editor.shortcuts.add('Alt+F9', '', function() {
320 editor.shortcuts.add('Alt+F10', '', function() {
324 editor.shortcuts.add('Alt+F11', '', function() {
325 focus('elementpath');
328 panel.on('cancel', function() {
334 * Resizes the editor to the specified width, height.
336 function resizeTo(width, height) {
337 var containerElm, iframeElm, containerSize, iframeSize;
339 function getSize(elm) {
341 width: elm.clientWidth,
342 height: elm.clientHeight
346 containerElm = editor.getContainer();
347 iframeElm = editor.getContentAreaContainer().firstChild;
348 containerSize = getSize(containerElm);
349 iframeSize = getSize(iframeElm);
351 if (width !== null) {
352 width = Math.max(settings.min_width || 100, width);
353 width = Math.min(settings.max_width || 0xFFFF, width);
355 DOM.setStyle(containerElm, 'width', width + (containerSize.width - iframeSize.width));
356 DOM.setStyle(iframeElm, 'width', width);
359 height = Math.max(settings.min_height || 100, height);
360 height = Math.min(settings.max_height || 0xFFFF, height);
361 DOM.setStyle(iframeElm, 'height', height);
363 editor.fire('ResizeEditor');
366 function resizeBy(dw, dh) {
367 var elm = editor.getContentAreaContainer();
368 self.resizeTo(elm.clientWidth + dw, elm.clientHeight + dh);
372 * Handles contextual toolbars.
374 function addContextualToolbars() {
377 function getContextToolbars() {
378 return editor.contextToolbars || [];
381 function getElementRect(elm) {
382 var pos, targetRect, root;
384 pos = tinymce.DOM.getPos(editor.getContentAreaContainer());
385 targetRect = editor.dom.getRect(elm);
386 root = editor.dom.getRoot();
388 // Adjust targetPos for scrolling in the editor
389 if (root.nodeName == 'BODY') {
390 targetRect.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
391 targetRect.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
394 targetRect.x += pos.x;
395 targetRect.y += pos.y;
400 function hideAllFloatingPanels() {
401 each(editor.contextToolbars, function(toolbar) {
403 toolbar.panel.hide();
408 function reposition(match) {
409 var relPos, panelRect, elementRect, contentAreaRect, panel, relRect, testPositions;
411 if (editor.removed) {
415 if (!match || !match.toolbar.panel) {
416 hideAllFloatingPanels();
426 panel = match.toolbar.panel;
429 elementRect = getElementRect(match.element);
430 panelRect = tinymce.DOM.getRect(panel.getEl());
431 contentAreaRect = tinymce.DOM.getRect(editor.getContentAreaContainer() || editor.getBody());
433 if (!editor.inline) {
434 contentAreaRect.w = editor.getDoc().documentElement.offsetWidth;
437 // Inflate the elementRect so it doesn't get placed above resize handles
438 if (editor.selection.controlSelection.isResizable(match.element)) {
439 elementRect = Rect.inflate(elementRect, 0, 7);
442 relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, testPositions);
445 each(testPositions.concat('inside'), function(pos) {
446 panel.classes.toggle('tinymce-inline-' + pos, pos == relPos);
449 relRect = Rect.relativePosition(panelRect, elementRect, relPos);
450 panel.moveTo(relRect.x, relRect.y);
452 each(testPositions, function(pos) {
453 panel.classes.toggle('tinymce-inline-' + pos, false);
456 panel.classes.toggle('tinymce-inline-inside', true);
458 elementRect = Rect.intersect(contentAreaRect, elementRect);
461 relPos = Rect.findBestRelativePosition(panelRect, elementRect, contentAreaRect, [
462 'tc-tc', 'tl-tl', 'tr-tr'
466 relRect = Rect.relativePosition(panelRect, elementRect, relPos);
467 panel.moveTo(relRect.x, relRect.y);
469 panel.moveTo(elementRect.x, elementRect.y);
476 //drawRect(contentAreaRect, 'blue');
477 //drawRect(elementRect, 'red');
478 //drawRect(panelRect, 'green');
481 function repositionHandler() {
483 if (editor.selection) {
484 reposition(findFrontMostMatch(editor.selection.getNode()));
488 if (window.requestAnimationFrame) {
489 window.requestAnimationFrame(execute);
495 function bindScrollEvent() {
496 if (!scrollContainer) {
497 scrollContainer = editor.selection.getScrollContainer() || editor.getWin();
498 tinymce.$(scrollContainer).on('scroll', repositionHandler);
500 editor.on('remove', function() {
501 tinymce.$(scrollContainer).off('scroll');
506 function showContextToolbar(match) {
509 if (match.toolbar.panel) {
510 match.toolbar.panel.show();
517 panel = Factory.create({
520 classes: 'tinymce tinymce-inline',
528 items: createToolbar(match.toolbar.items)
531 match.toolbar.panel = panel;
532 panel.renderTo(document.body).reflow();
536 function hideAllContextToolbars() {
537 tinymce.each(getContextToolbars(), function(toolbar) {
539 toolbar.panel.hide();
544 function findFrontMostMatch(targetElm) {
545 var i, y, parentsAndSelf, toolbars = getContextToolbars();
547 parentsAndSelf = editor.$(targetElm).parents().add(targetElm);
548 for (i = parentsAndSelf.length - 1; i >= 0; i--) {
549 for (y = toolbars.length - 1; y >= 0; y--) {
550 if (toolbars[y].predicate(parentsAndSelf[i])) {
552 toolbar: toolbars[y],
553 element: parentsAndSelf[i]
562 editor.on('click keyup blur', function() {
563 // Needs to be delayed to avoid Chrome img focus out bug
564 window.setTimeout(function() {
567 if (editor.removed) {
571 match = findFrontMostMatch(editor.selection.getNode());
573 showContextToolbar(match);
575 hideAllContextToolbars();
580 editor.on('ObjectResizeStart', function() {
581 var match = findFrontMostMatch(editor.selection.getNode());
583 if (match && match.toolbar.panel) {
584 match.toolbar.panel.hide();
588 editor.on('nodeChange ResizeEditor ResizeWindow', repositionHandler);
590 editor.on('remove', function() {
591 tinymce.each(getContextToolbars(), function(toolbar) {
593 toolbar.panel.remove();
597 editor.contextToolbars = {};
602 * Renders the inline editor UI.
604 * @return {Object} Name/value object with theme data.
606 function renderInlineUI(args) {
607 var panel, inlineToolbarContainer;
609 if (settings.fixed_toolbar_container) {
610 inlineToolbarContainer = DOM.select(settings.fixed_toolbar_container)[0];
613 function reposition() {
614 if (panel && panel.moveRel && panel.visible() && !panel._fixed) {
615 // TODO: This is kind of ugly and doesn't handle multiple scrollable elements
616 var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody();
617 var deltaX = 0, deltaY = 0;
619 if (scrollContainer) {
620 var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer);
622 deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x);
623 deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y);
626 panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl', 'tr-br']).moveBy(deltaX, deltaY);
634 DOM.addClass(editor.getBody(), 'mce-edit-focus');
640 // We require two events as the inline float panel based toolbar does not have autohide=true
643 // All other autohidden float panels will be closed below.
644 FloatPanel.hideAll();
646 DOM.removeClass(editor.getBody(), 'mce-edit-focus');
652 if (!panel.visible()) {
659 // Render a plain panel inside the inlineToolbarContainer if it's defined
660 panel = self.panel = Factory.create({
661 type: inlineToolbarContainer ? 'panel' : 'floatpanel',
663 classes: 'tinymce tinymce-inline',
669 fixed: !!inlineToolbarContainer,
672 settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
678 /*if (settings.statusbar !== false) {
679 panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [
680 {type: 'elementpath'}
684 editor.fire('BeforeRenderUI');
685 panel.renderTo(inlineToolbarContainer || document.body).reflow();
687 addAccessibilityKeys(panel);
689 addContextualToolbars();
691 editor.on('nodeChange', reposition);
692 editor.on('activate', show);
693 editor.on('deactivate', hide);
695 editor.nodeChanged();
698 settings.content_editable = true;
700 editor.on('focus', function() {
701 // Render only when the CSS file has been loaded
702 if (args.skinUiCss) {
703 tinymce.DOM.styleSheetLoader.load(args.skinUiCss, render, render);
709 editor.on('blur hide', hide);
711 // Remove the panel when the editor is removed
712 editor.on('remove', function() {
720 if (args.skinUiCss) {
721 tinymce.DOM.styleSheetLoader.load(args.skinUiCss);
728 * Renders the iframe editor UI.
730 * @param {Object} args Details about target element etc.
731 * @return {Object} Name/value object with theme data.
733 function renderIframeUI(args) {
734 var panel, resizeHandleCtrl, startSize;
736 if (args.skinUiCss) {
737 tinymce.DOM.loadCSS(args.skinUiCss);
741 panel = self.panel = Factory.create({
745 style: 'visibility: hidden',
749 settings.menubar === false ? null : {type: 'menubar', border: '0 0 1 0', items: createMenuButtons()},
751 {type: 'panel', name: 'iframe', layout: 'stack', classes: 'edit-area', html: '', border: '1 0 0 0'}
755 if (settings.resize !== false) {
757 type: 'resizehandle',
758 direction: settings.resize,
760 onResizeStart: function() {
761 var elm = editor.getContentAreaContainer().firstChild;
764 width: elm.clientWidth,
765 height: elm.clientHeight
769 onResize: function(e) {
770 if (settings.resize == 'both') {
771 resizeTo(startSize.width + e.deltaX, startSize.height + e.deltaY);
773 resizeTo(null, startSize.height + e.deltaY);
779 // Add statusbar if needed
780 if (settings.statusbar !== false) {
781 panel.add({type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [
782 {type: 'elementpath'},
787 if (settings.readonly) {
788 panel.find('*').disabled(true);
791 editor.fire('BeforeRenderUI');
792 panel.renderBefore(args.targetNode).reflow();
794 if (settings.width) {
795 tinymce.DOM.setStyle(panel.getEl(), 'width', settings.width);
798 // Remove the panel when the editor is removed
799 editor.on('remove', function() {
804 // Add accesibility shortcuts
805 addAccessibilityKeys(panel);
806 addContextualToolbars();
809 iframeContainer: panel.find('#iframe')[0].getEl(),
810 editorContainer: panel.getEl()
815 * Renders the UI for the theme. This gets called by the editor.
817 * @param {Object} args Details about target element etc.
818 * @return {Object} Theme UI data items.
820 self.renderUI = function(args) {
821 var skin = settings.skin !== false ? settings.skin || 'lightgray' : false;
824 var skinUrl = settings.skin_url;
827 skinUrl = editor.documentBaseURI.toAbsolute(skinUrl);
829 skinUrl = tinymce.baseURL + '/skins/' + skin;
832 // Load special skin for IE7
833 // TODO: Remove this when we drop IE7 support
834 if (tinymce.Env.documentMode <= 7) {
835 args.skinUiCss = skinUrl + '/skin.ie7.min.css';
837 args.skinUiCss = skinUrl + '/skin.min.css';
840 // Load content.min.css or content.inline.min.css
841 editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css');
844 // Handle editor setProgressState change
845 editor.on('ProgressState', function(e) {
846 self.throbber = self.throbber || new tinymce.ui.Throbber(self.panel.getEl('body'));
849 self.throbber.show(e.time);
851 self.throbber.hide();
855 if (settings.inline) {
856 return renderInlineUI(args);
859 return renderIframeUI(args);
862 self.resizeTo = resizeTo;
863 self.resizeBy = resizeBy;