1. #1
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Post HtmlEditor & Toolbar Extended for Extensibility

    HtmlEditor & Toolbar Extended for Extensibility


    I have created new extensions to the Ext.form.HtmlEditor and Ext.Toolbar components that I believe are better suited for handling plugins. As Jack et al have commented that the HtmlEditor was designed to be 'lightweight', it follows that the tool should at least be fit for the easy integration of plugins. I found it wasn't, so I have extended it to cater for them.

    The code attached is only a suggestion of the way the editor should function in future. I'm open to anyone suggesting it should work another way. Use it if you feel it is of value, or not, if you don't.

    I'll start with the toolbar. I called it Ext.ux.HTMLEditorToolbar, as I felt that the regular toolbar worked sufficiently for non-editor needs, so I wrote this one solely with the editor in mind.

    The main problem with the existing toolbar was that the editor added the tools directly into it, both sequentially and at the time of render. I wanted the tools to be added prior to the toolbar being rendered. I also wanted the tools to be inserted, if needed, rather than just added to the end.

    Code:
    // Ext.ux.HTMLEditorToolbar
    // extension of Ext.Toolbar to cater for extensibility
    Ext.ux.HTMLEditorToolbar = Ext.extend(Ext.Toolbar, {
    
      // overrides Ext.Toolbar.initComponent
      // first function to be called upon creation of toolbar
      initComponent: function() {
    
        // call Ext.Toolbar.initComponent
        Ext.ux.HTMLEditorToolbar.superclass.initComponent.call(this);
    
        // unable to use existing items collection for pre-render
        // configuration as it's updated by Ext.Toolbar during render
        this.tools = new Ext.util.MixedCollection(false, function(tool) {
          return tool.itemId || tool.id || Ext.id();
        });
    
      },
    
      // add tools (pre-render)
      addTools: function(tools) {
        tools = (tools instanceof Array) ? tools : [tools];
        for (var i = 0, len = tools.length; i < len; i++) {
          this.tools.add(tools[i]);
        }
      },
    
      // insert tools (pre-render)
      insertTools: function(index, tools) {
        tools = (tools instanceof Array) ? tools : [tools];
        for (var i = 0, len = tools.length; i < len; i++) {
          this.tools.insert(index + i, tools[i]);
        }
      },
      
      // insert tools before another tool (pre-render)
      insertToolsBefore: function(itemId, tools) {
        var index = this.tools.indexOfKey(itemId);
        this.insertTools(index, tools);
      },
      
      // insert tools after another tool (pre-render)
      insertToolsAfter: function(itemId, tools) {
        var index = this.tools.indexOfKey(itemId) + 1;
        this.insertTools(index, tools);
      },
    
      // render tools (performed after tools/plugins have been configured/reordered)
      renderTool: function(tool) {
    
        // cater for new tbcombo component
        // created to split configuration from render
        if (typeof tool == "object" && tool.xtype && tool.xtype == "tbcombo") {
    
          // not catered for in Ext.Toolbar.add function
          // as it defaults to addField instead of addItem
          this.addItem(Ext.ComponentMgr.create(tool));
    
        }
        else {
          
          // else use existing Ext.Toolbar.add function
          // to render tools
          this.add(tool);
    
        }
    
      },
    
      // overrides Ext.Toolbar.onRender
      onRender: function(ct, position) {
        
        // call Ext.Toolbar.onRender
        Ext.ux.HTMLEditorToolbar.superclass.onRender.call(this, ct, position);
    
        // loop through pre-configured/reordered tools and render each accordingly
        this.tools.each(this.renderTool, this);
    
      }
      
    });
    I also added a new component to the toolbar to handle comboboxes. The existing fontnames combobox could only be created at the time of render, so adding a new component meant that I could configure it in memory prior to it being rendered to the toolbar.

    Code:
    // Ext.ux.HTMLEditorToolbar.ComboBox
    // created to handle the pre-configuration of a combobox (pre-render)
    Ext.ux.HTMLEditorToolbar.ComboBox = function(config) {
      
      Ext.apply(this, config);
    
      // create combobox in memory before render
      var selEl = document.createElement("select");
      selEl.className = this.cls;
      for (var i = 0, len = this.opts.length; i < len; i++) {
        var opt = this.opts[i];
        var optEl = document.createElement('option');
        optEl.text = opt.text;
        optEl.value = opt.value;
        if (opt.selected) {
          optEl.selected = true;
          this.defaultValue = opt.value;
        }
        selEl.options.add(optEl);
      }
      if (! this.defaultValue) {
        this.defaultValue = this.opts[0].value;
      }
      
      // call Ext.Toolbar.Item constructor passing combobox
      Ext.ux.HTMLEditorToolbar.ComboBox.superclass.constructor.call(this, selEl);
      
    }
    
    // Ext.ux.HTMLEditorToolbar.ComboBox
    // extension of Ext.Toolbar.Item
    Ext.extend(Ext.ux.HTMLEditorToolbar.ComboBox, Ext.Toolbar.Item, {
    
      // overrides Ext.Toolbar.Item.render
      render: function(td) {
        
        // call Ext.Toolbar.Item.render
        Ext.ux.HTMLEditorToolbar.ComboBox.superclass.render.call(this, td);
    
        // add handler for combobox change event
        Ext.EventManager.on(this.el, 'change', this.handler, this.scope);
        
      }
      
    });
    
    // register Ext.ux.HTMLEditorToolbar.ComboBox as a new component
    Ext.ComponentMgr.registerType('tbcombo', Ext.ux.HTMLEditorToolbar.ComboBox);
    Now, to the HtmlEditor. I have called the extended version Ext.ux.HTMLEditor. It has changed significantly.

    I no longer use the enable... flags to include the tools, but use an array, called toolbarItems, that can be overwritten in the config file (I also included a config array called toolbarItemExcludes, as an alternative, to exclude tools contained in the standard toolbarItems array). Using this array means that you can display the tools in any order.

    Once the editor is initialized, the toolbar will have all the tools configured, meaning that any plugins can then manipulate the tools list by adding or inserting new tools as needed. This can all happen prior to the toolbar being rendered.

    Code:
    // Ext.ux.HTMLEditor
    // extends Ext.form.HtmlEditor to provide extensibility
    Ext.ux.HTMLEditor = Ext.extend(Ext.form.HtmlEditor, {
    
      // using the enable... flags to define content meant that items
      // were always added in the same order.
      // using the toolbarItems list instead allows the user to override
      // the order of items, and even exclude items not wanted.
      // the enable... flags are now no longer used
      toolbarItems: [
        'fonts',
        'allformats',
        'allfontsizes',
        'allcolors',
        'allalignments',
        'alllinks',
        'alllists',
        'sourceedit'
      ],
    
      // as an alternative, the toolbarItemExcludes list can be used to
      // exclude items from the toolbarItem list
      toolbarItemExcludes: [],
    
      // overrides Ext.form.HtmlEditor.initComponent
      // first function to be called upon creation of the editor
      initComponent: function() {
    
        // call Ext.form.HtmlEditor.initComponent
        Ext.ux.HTMLEditor.superclass.initComponent.call(this);
    
        // add important event missing from Ext.form.HtmlEditor
        this.addEvents({
          editorevent: true
        });
    
        // remove any toolbarItemExcludes from the toolbarItems array
        for (var i = 0, iMax = this.toolbarItemExcludes.length; i < iMax; i++) {
          var item = this.toolbarItemExcludes[i].toLowerCase();
          for (var j = 0, jMax = this.toolbarItems.length; j < jMax; j++) {
            if (this.toolbarItems[j] == item) {
              this.toolbarItems.splice(j, 1);
              break;
            }
          }
        }
    
        // create the editor toolbar
        this.tb = new Ext.ux.HTMLEditorToolbar();
        
        // create the toolbar items
        this.createTools(this.toolbarItems);
            
      },
    
      // overrides Ext.form.HtmlEditor.createFontOptions
      createFontOptions: function() {
        var opts = [], ffs = this.fontFamilies, ff;
        for (var i = 0, len = ffs.length; i < len; i++) {
          ff = ffs[i];
          fflc = ff.toLowerCase();
          var opt = {text: ff, value: fflc};
          if (fflc == this.defaultFont) opt.selected = true;
          opts.push(opt);
        }
        return opts;
      },
    
      // create default button config
      btn: function(id, toggle, queryState, handler) {
        return {
          itemId: id,
          cls: 'x-btn-icon x-edit-' + id,
          enableToggle: toggle !== false,
          queryState: queryState !== false,
          handler: handler || this.relayBtnCmd,
          scope: this,
          clickEvent: 'mousedown',
          tooltip: this.buttonTips[id] || undefined,
          tabIndex: -1
        };
      },
    
      // create known tools based on the passed item list (initially
      // from the toolbarItems list) and add it to the tools collection.
      // this function allows random tool allocation as opposed
      // to the old version that added tools sequentially
      createTools: function(toolbarItems) {
    
        // convert single items to a list
        toolbarItems = (toolbarItems instanceof Array) ? toolbarItems : [toolbarItems];
    
        // loop through the item list
        for (var i = 0, len = toolbarItems.length; i < len; i++) {
    
          //add the item to the toolbar
          var item = toolbarItems[i];
          switch (item) {
            
            // add the fonts combobox
            case 'fonts':
              if (! Ext.isSafari) {
                this.tb.addTools({
                  itemId: 'fontname',
                  xtype: 'tbcombo',
                  cls: 'x-font-select',
                  opts: this.createFontOptions(),
                  queryValue: true,
                  handler: function(event, el) {
                    this.relayCmd('fontname', el.value);
                    this.deferFocus();
                  },
                  scope: this
                });
      	      }
              break;
      
            // add the bold button
            case 'bold':
              this.tb.addTools(this.btn('bold'));
              break;
      
            // add the italic button
            case 'italic':
              this.tb.addTools(this.btn('italic'));
              break;
      
            // add the underline button
            case 'underline':
              this.tb.addTools(this.btn('underline'));
              break;
      
            // add all format buttons (with a leading separator)
            case 'allformats':
              this.createTools(['-', 'bold', 'italic', 'underline']);
              break;
      
            // add the increasefontsize button
            case 'increasefontsize':
              this.tb.addTools(this.btn('increasefontsize', false, false, this.adjustFont));
              break;
      
            // add the decreasefontsize button
            case 'decreasefontsize':
              this.tb.addTools(this.btn('decreasefontsize', false, false, this.adjustFont));
              break;
      
            // add both fontsize buttons (with a leading separator)
            case 'allfontsizes':
              this.createTools(['-', 'increasefontsize', 'decreasefontsize']);
              break;
      
            // add the forecolor button and associated menu
            case 'forecolor':
              this.tb.addTools({
                itemId: 'forecolor',
                cls: 'x-btn-icon x-edit-forecolor',
                clickEvent: 'mousedown',
                tooltip: this.buttonTips['forecolor'],
                tabIndex: -1,
                menu: new Ext.menu.ColorMenu({
                  allowReselect: true,
                  focus: Ext.emptyFn,
                  value: '000000',
                  plain: true,
                  selectHandler: function(cp, color) {
                    this.execCmd('forecolor', Ext.isSafari || Ext.isIE ? '#' + color : color);
                    this.deferFocus();
                  },
                  scope: this,
                  clickEvent:'mousedown'
                })
              });
              break;
      
            // add the backcolor button and associated menu
            case 'backcolor':
              this.tb.addTools({
                itemId: 'backcolor',
                cls: 'x-btn-icon x-edit-backcolor',
                clickEvent: 'mousedown',
                tooltip: this.buttonTips['backcolor'],
                tabIndex: -1,
                menu: new Ext.menu.ColorMenu({
                  focus: Ext.emptyFn,
                  value: 'FFFFFF',
                  plain: true,
                  allowReselect: true,
                  selectHandler: function(cp, color) {
                    if (Ext.isGecko) {
                      this.execCmd('useCSS', false);
                      this.execCmd('hilitecolor', color);
                      this.execCmd('useCSS', true);
                      this.deferFocus();
                    }
                    else {
                      this.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor',
                        Ext.isSafari || Ext.isIE ? '#' + color : color);
                      this.deferFocus();
                    }
                  },
                  scope: this,
                  clickEvent: 'mousedown'
                })
              });
              break;
      
            // add both color buttons (with a leading separator)
            case 'allcolors':
              this.createTools(['-', 'forecolor', 'backcolor']);
              break;
      
            // add the justifyleft button
            case 'justifyleft':
              this.tb.addTools(this.btn('justifyleft'));
              break;
      
            // add the justifycenter button
            case 'justifycenter':
              this.tb.addTools(this.btn('justifycenter'));
              break;
      
            // add the justifyright button
            case 'justifyright':
              this.tb.addTools(this.btn('justifyright'));
              break;
      
            // add all alignment buttons (with a leading separator)
            case 'allalignments':
              this.createTools(['-', 'justifyleft', 'justifycenter', 'justifyright']);
              break;
      
            // add the link button
            case 'link':
              if (! Ext.isSafari) {
                this.tb.addTools(this.btn('createlink', false, false, this.createLink));
              }
              break;
      
            // add the link button (with a leading separator)
            case 'alllinks':
              if (! Ext.isSafari) {
                this.createTools(['-', 'link']);
              }
              break;
      
            // add the orderedlist button
            case 'orderedlist':
              if (! Ext.isSafari) {
                this.tb.addTools(this.btn('insertorderedlist'));
              }
              break;
      
            // add the unorderedlist button
            case 'unorderedlist':
              if (! Ext.isSafari) {
                this.tb.addTools(this.btn('insertunorderedlist'));
              }
              break;
      
            // add both list buttons (with a leading separator)
            case 'alllists':
              if (! Ext.isSafari) {
                this.createTools(['-', 'orderedlist', 'unorderedlist']);
              }
              break;
      
            // add the sourceedit button
            case 'sourceedit':
              if (! Ext.isSafari) {
                this.tb.addTools(this.btn('sourceedit', true, false, function(btn) {
                  this.toggleSourceEdit(btn.pressed);
                }));
              }
              break;
      
            // allows for '-', 'separator', ' ', '->', labels, or other item types
            default:
              this.tb.addTools(item);
      
          }
        }
    
      },
    
      // overrides Ext.form.HtmlEditor.createToolbar
      // most functionality has been removed as this is called
      // upon render 
      createToolbar: function() {
    
        // render toolbar
        this.tb.render(this.wrap.dom.firstChild);
    
        // inherited
        this.tb.el.on('click', function(e) {
          e.preventDefault();
        });
    
      },
    
      // overrides Ext.form.HtmlEditor.getDocMarkup
      // provides ability to include stylesheets in the editor document
      // created by bpjohnson (see http://extjs.com/forum/showthread.php?t=9588)
      getDocMarkup: function() {
        var markup = '<html><head><style type="text/css">body{border:0;margin:0;padding:3px;height:98%;cursor:text;}</style>';
        if (this.styles) {
          for (var i = 0; i < this.styles.length; i++) {
            markup = markup + '<link rel="stylesheet" type="text/css" href="' + this.styles[i] + '" />';
          }
        }
        markup = markup + '</head><body></body></html>';
        return markup;
      },
    
      // overrides Ext.form.HtmlEditor.onEditorEvent
      onEditorEvent: function(e) {
        
        // call Ext.form.HtmlEditor.onEditorEvent
        Ext.ux.HTMLEditor.superclass.onEditorEvent.call(this, e);
    
        // fire new editorevent to tell plugins that an event occurred
        // in the editor.
        // this saves plugins from having to monitor multiple events
        // i.e. 'click', 'keyup', etc.
        this.fireEvent('editorevent', this, e);
        
      },
    
      // overrides Ext.form.HtmlEditor.updateToolbar
      // does not call superclass function as much of it was no
      // longer needed, but duplicates some code
      updateToolbar: function() {
    
        // inherited
        if (! this.activated) {
          this.onFirstFocus();
          return;
        }
        
        // loop through toolbar items and update status based on
        // query values return from the browser (if configured)
        this.tb.items.each(function(item) {
          if (item.queryState) {
            item.toggle(this.doc.queryCommandState(item.itemId));
          }
          else if (item.queryEnabled) {
            item.setDisabled(! this.doc.queryCommandEnabled(item.itemId));
          }
          else if (item.xtype = "tbcombo" && item.queryValue) {
            var value = (this.doc.queryCommandValue(item.itemId) || item.defaultValue).toLowerCase();
            if (value != item.el.value) {
              item.el.value = value;
            }
          }
        }, this);
        
        // inherited
        Ext.menu.MenuMgr.hideAll();
    
        // inherited
        this.syncValue();
    
      }
    
    });
    I have attached a demo of the functionality, including an updated example of my HTMLEditorStyles plugin, which now utilises the new functionality.

    For those who requested a live demo, click here.
    Attached Files
    Last edited by dangreenfield; 17 Sep 2008 at 2:19 PM. Reason: Added link to demo

  2. #2
    Sencha User
    Join Date
    May 2007
    Location
    Germany
    Posts
    18
    Vote Rating
    0
    ralf is on a distinguished road

      0  

    Default Use more than one combo and toggle visibility

    Use more than one combo and toggle visibility


    Hi Dan,

    this works like a charm. Thank you for sharing!

    I extended your code a little bit since I needed to insert more than one style combo box and to toggle the visibility at runtime:

    htmleditorstyles.js
    Code:
    // extended contructor to configure id and added parameter styleId
    Ext.ux.HTMLEditorStyles = function(id, styles) {
      this.styleId= id;
    ...
    // use styleId instead of 'style'
          this.editor.tb.insertToolsBefore('fontname', [{
            itemId: this.styleId,
            xtype: 'tbcombo',
    ....
    // use styleId instead of 'style'
      this.onEditorEvent = function() {
        var element = Ext.isIE ? this.editor.doc.selection.createRange().parentElement() :
          this.editor.win.getSelection().anchorNode;
        var parent = getParentStyleElement(element);
        var style = parent ? parent.className : "none";
        if (this.editor.tb.items.map[this.styleId].el.value != style) {
          	this.editor.tb.items.map[this.styleId].el.value = style;
        }
      }
    htmleditor.js
    Code:
      ....
      //hide and show single toolbar items at runtime 
      hideTool: function(id) {
    		this.tb.items.map[id].hide();  	
      }, 
      showTool: function(id) {
      	this.tb.items.map[id].show();
      }

  3. #3
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Exclamation Top Notch

    Top Notch


    Hi Ralf. You made the mod perfectly. I would have done it the same way!

    NOTE: I had never previously discovered a best practice way to coding in javascript, so my code, up until this point, was pretty much just thrown together. Since discovering this great article in the Community Manual on Basic Application Design (see http://extjs.com/learn/Manual:Basic_Application_Design), I've decided to code all my scripts using this format (ie, separating private from public functions). Because of this, the HTMLEditorStyles script, included in the first post above, has changed. If you wish to view the old code to understand Ralf's comments above then please see the old HTMLEditorStyles version below.
    Attached Files
    Last edited by dangreenfield; 2 Dec 2007 at 5:11 PM. Reason: Attached old version of HTMLEditorStyles plugin

  4. #4
    Ext JS Premium Member stever's Avatar
    Join Date
    Mar 2007
    Posts
    1,407
    Vote Rating
    6
    stever will become famous soon enough stever will become famous soon enough

      0  

    Default


    Is this your latest? I'm just now converting my stuff to use this plugin model where appropriate and so would like to start from the latest.

  5. #5
    Sencha User dangreenfield's Avatar
    Join Date
    Mar 2007
    Location
    Hawkes Bay, New Zealand
    Posts
    69
    Vote Rating
    0
    dangreenfield is on a distinguished road

      0  

    Smile It is the latest

    It is the latest


    Quote Originally Posted by stever View Post
    Is this your latest? I'm just now converting my stuff to use this plugin model where appropriate and so would like to start from the latest.
    Yes, the first post contains the latest version. I'm happy with my code as it stands and noone else has suggested any changes. Good luck.

  6. #6
    Ext User
    Join Date
    Oct 2007
    Posts
    42
    Vote Rating
    0
    mykes is on a distinguished road

      0  

    Default


    It's an awesome enhancement, thanks!

    I have a question/suggestion, though.

    When you get enough buttons on the toolbar, you really want to make the toolbar two rows, so you can fit then in a smallish window neatly. Or to organize the buttons by functions.

    For an example, just look at this message board's Reply to Thread editor - it has two rows of buttons - one with font selection/size, etc., on the top row, the bottom row has bold, italics, etc.

    The simple editor is sufficient to do almost all things, since you can directly edit the HTML, but if people want to really turn this into something more advanced for their purpose/applications, they might want to add "create table," "choose style (h1, h2, etc.)," "insert video," "insert smilie," and several other buttons...

    I figure as long as you've extended the HTMLEditor toolbar class, you might want to take my suggestions into consideration.

    Regards

  7. #7
    Ext JS Premium Member stever's Avatar
    Join Date
    Mar 2007
    Posts
    1,407
    Vote Rating
    6
    stever will become famous soon enough stever will become famous soon enough

      0  

    Default


    There is work going on now to make all toolbars into containers. Then you could have a single-line layout, two-line layout, ribbon-layout, etc. I think it would be worth waiting to see if that gets put into the main Ext codebase, since that would make this a lot easier...

  8. #8
    Ext User
    Join Date
    Jul 2007
    Posts
    1
    Vote Rating
    0
    Colter is on a distinguished road

      0  

    Default


    Do you have a screen shot or demo to view? Thanks

  9. #9
    Ext User
    Join Date
    Aug 2007
    Posts
    72
    Vote Rating
    0
    vlados is on a distinguished road

      0  

    Default


    I agree, let's see it

  10. #10
    Ext User
    Join Date
    Oct 2007
    Posts
    42
    Vote Rating
    0
    mykes is on a distinguished road

      0  

    Default


    I've been examining the code closely and the undo/redo plugin references the editor.tb toolbar directly. Are you concerned that turning everything into containers is going to break a lot of code like I just mentioned?