Tuesday, June 10, 2014

CPWebView: Displaying Customized HTML

Cappuccino converts the view hierarchy into a nested stack of <DIV> elements, which is both deep and impossible to predict when it comes to adding custom HTML into our page.  For those moments that we need to enhance the capabilities of Cappuccino by using additional features available in other frameworks, we can turn to the CPWebView class to insert these elements into our page.

At the end of the article, I will share a git resource to an amazing example of this kind of modification, which I recommend to all who need a text editor in their application, because Cappuccino has no objects to represent a <TEXTAREA></TEXTAREA> input field.

Create A New Project

Our typical starting place: Hello World!
As we have done so many times before, create a brand new project in a new folder.  I am calling this project CPWebView, so that I can keep all these tutorials separate from one another.  When I load my new project, I see nothing more than the usual "Hello World" screen. 

Our First WebView example

As usual, open AppController.j and edit the applicationDidFinishLaunching method.  Remove all the lines pertaining to the label in the center of the screen, but leave the variable definition of our window and content view elements. You should also leave the orderFront message at the end of the method.

After the variable definition and before the orderFront method call, add the following code:
var margin = 20,
        windowWidth = CGRectGetWidth([contentView frame]),
        contentWidth = windowWidth - 2 * margin, /*left and right margins */
        xPos = 0 + margin, /* 0 is our current starting point, we shift by one margin */
        yPos = 100,
        height = 300,
        webView = [[CPWebView alloc] initWithFrame:CGRectMake(xPos,yPos,contentWidth,height)],
        HTMLString = "<style type=\"text/css\">body{ background-color:lightgreen; }</style><h1>This is Our Custom Header</h1><p id=\"paragraph1\">This paragraph was inserted manually, and is a custom id which can be referenced in javascript or used programmaticaly to make changes.</p>";
[webView loadHTMLString:HTMLString];
[contentView addSubview:webView];
Reloading the page shows a green background which is positioned 20 units from the left and right edges of the page.  On the green background is the title, which we set with the substring "<h1>This is Our Custom Header</h1>", and a paragraph which we set with the substring "<p id=\"paragraph1\">This paragraph was inserted manually, and is a custom id which can be referenced in javascript or used programmaticaly to make changes.</p>".

This web view does not keep the 20 unit margin it is created with when we change the dimensions of the browser window, but we can change that by adding one line of code:

[webView setAutoresizingMask: CPViewWidthSizable ];
After adding this line of code, our web view now changes along with the browser window, after a brief delay.

What is interesting to note about this view, is that the style element, which addresses the <BODY></BODY> element, only has affect on the content view window that we originally defined, which is as wide as the browser window (less the margins) and 300 units tall.  This subview is a web-page unto itself.  It is integrated into our design by an <IFRAME></IFRAME> element, which allows it to operate completely independent of the surrounding elements.

Using a page inspector (I have used Chrome in this example; see image), we see both the layout of this iFrame and also note that our paragraph has an id attribute that is set to what we have defined in our HTML string, which gives us the control we need to implement some advanced functionality outside of Cappuccino's limitations.
Our custom web page elements are within an iframe element and displays the HTML as we defined it.
Defining a webpage within the quotations of a string definition is tiresome and not generally recommended.  Like any good web view should, the Cappuccino CPWebView class allows us to reference a file located at a URL instead of relying on setting string values all the time.  Let's change our code to the following:
    /* COMMENTED OUT ****
    [webView loadHTMLString:HTMLString];
    *************/
    [webView setMainFrameURL:@"http://localhost/CPWebView/examplePage.php"];
Let us also create the file examplePage.php in our project root:
<!DOCTYPE html>
<html>
<head>
        <title>An Example Page</title>
</head>
<body>
<h1>Eureka!</h1>
<p>This document is dual purposed. It shows that any URL can be used, and that t
he url need not point to a static HTML document. Sever-side scripting can be use
d to generate dynamic content for your Web View content.</p>
<p>This document was generated at: <?php $date = new DateTime("now", new DateTimeZone("America/Los_Angeles")); echo $date->format("M d, Y H:i:s"); ?></p>
</body>
</html> 
Before we reload the page, I want to make an important note: the url I have used here is for the system as I have set it up on my machine. If you are reading this tutorial, I hope you have a basic understanding of URL's, and you should be able to write a proper URL to point to the examplePage.php file, wherever it is that you have saved this document.  Please correct the URL to match your setup before continuing.

Also, this particular example uses PHP as a server-side processing.  If you do not have PHP installed on your web server, omit the second paragraph in examplePage.php.  You should be able to still use the PHP file extension, because the web browser does not use the file extension to determine that HTML is being presented, so the content of the file will display as a regular HTML file in this case.

If you have set the URL to your page correctly, when you reload the page you will see the following:
We use our CPWebView to call on a web page stored elsewhere in order to fill content in our page.
For those who have PHP installed on the web server, they will notice that the date and time displays, which shows that we can use the CPWebView to call on any web page that we might display using a regular browser, and this content can be put into the framework of our page as a subView to any view element.

This means that we can create handy HUD editors like the one that we create next using WKTextView, which is a class developed outside of Cappucccino using the Google Closure framework within a CPWebView.

A Practical and Terrific Example of Using CPWebView

Getting WKTextView

Before we continue, I am giving a big kudos Alexander Ljungberg, who developed WKTextView in response to Cappuccino's lacking implementation of a NSTextField that wraps while one is editing text (CPTextField does wrap, but when one edits the text value, the text displays on a single line, making editing the text field very difficult).  Mr. Ljungberg not only handles this issue, but also provides us methods, which allow us to created toolbar functionality that changes font styles, bold settings, and more.

The WKTextView application relies on the Google Closure library.  I had difficulty getting everything to work on my system until I installed the closure library per the instructions at Google.  Please take a moment to follow the installation instructions of Google Closure at this location: Google Closure.

Download or clone a copy of WKTextView from: https://github.com/wireload/WKTextView.git.  In my web-root folder, using the Terminal.app (MacOS X), I have typed the following in order to clone a copy of the WKTextView code:

DevLaptop:Documents jaredclemence$ git clone https://github.com/wireload/WKTextView.git ./WKTextView
This resulted in the following output, confirming the transfer:
Cloning into './WKTextView'...
remote: Reusing existing pack: 526, done.
remote: Total 526 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (526/526), 103.73 KiB | 0 bytes/s, done.
Resolving deltas: 100% (287/287), done.
Checking connectivity... done. 
From within the WKTextView directory, I type the following per the README file in the packet:
DevLaptop:WKTexTView jaredclemence$ git submodule init
Submodule 'auxiliary/google-closure-library' (git://github.com/aljungberg/google-closure-library.git) registered for path 'auxiliary/google-closure-library'

And then:
DevLaptop:WKTexTView jaredclemence$ git submodule update
Cloning into 'auxiliary/google-closure-library'...
remote: Counting objects: 32709, done.
remote: Compressing objects: 100% (5576/5576), done.
remote: Total 32709 (delta 26852), reused 32709 (delta 26852)
Receiving objects: 100% (32709/32709), 15.10 MiB | 3.44 MiB/s, done.
Resolving deltas: 100% (26852/26852), done.
Checking connectivity... done.
Submodule path 'auxiliary/google-closure-library': checked out '926fcba10539dad4c6bbb6aadfa5a39940833053'
After the project was installed, I first ran the file "auxilliary/build.sh" with sudo.  Some changes were needed to the variables within to point to the google closure installer that I had just previously installed.  This file, build.sh creates a javascript file called "closure-editor.js" and puts it in the Resources folder for use by the editor.html file.

From within the WKTextView folder, after file modifications type:
sudo auxilliary/build.sh

Next, I ran "./samplify.sh", also with sudo.

sudo ./samplify.sh
Then, to make the files easier to navigate with Mac Finder, I changed the folder name of the new folder  from "sample.dist" to "sampleFolder."  This folder contains a sample program using the WKTextField.j object.  We are creating our own, so for now, just copy the WKTextView.j file and the resources folder into our project.

Reviewing WKTextView

The first thing that we should notice is that the WKTextView class is derived from _WKWebView, which is derived from CPWebView.  We have all the usual methods available to us for class definition, including initWithFrame, which goes through the process of setting up our default layout within the CGRect we provide.

Also, we notice that the object refers to the Resource folder for an HTML file that contains the primary editor on this line:
[self setMainFrameURL:[[CPBundle bundleForClass:[self class]] pathForResource:"WKTextView/editor.html"]];
The CPBundle class has not yet been reviewed in this tutorial.  In general, we will say that this object helps in resource management.  Ultimately, this line does the same thing as passing a url to the file editor.html, which is currently in the path "Resources/WKTextView/editor.html" with respect to our web root.

The class also has several well commented methods defined which handle various scenarios which affect redisplaying the editor.html page when necessary as well as when to allow and when not to allow editing on the document area.

Next, we observe that a method is defined to return a CPString; this method is named htmlValue, and it likely will return the content of the editor window.  Similarly, there is a setHTMLValue: method which should let us load the editor with previously entered text.

Lastly, we observe that there are several delegate functions defined, which allow us to receive signals about the status of the WKTextView editor.  These delegate functions include:

  • textViewDidLoad:
  • textViewDidChange:
  • textViewDidBeginEditing:
  • textViewDidEndEditing:
  • textViewCursorDidMove:

Using WKTextView

After we have spent some time reviewing the well written code inside WKTextView.j, we can begin adding WKTextView to our current project.  Edit AppController.j to contain the following (warning, this is a bigger file, most of which is copied from the sample application):
/*
 * AppController.j
 * CPWebView
 *
 * Created by You on June 10, 2014.
 * Copyright 2014, Your Company All rights reserved.
 */
@import <Foundation/Foundation.j>
@import <AppKit/AppKit.j>
@import 'WKTextView.j' /* we must import the object class to use it */
/* these constants are defined in the sample.dist of WKTextview */
var NewToolbarItemIdentifier = "NewToolbarItemIdentifier",
    BoldToolbarItemIdentifier = "BoldToolbarItemIdentifier",
    ItalicsToolbarItemIdentifier = "ItalicsToolbarItemIdentifier",
    UnderlineToolbarItemIdentifier = "UnderlineToolbarItemIdentifier",
    StrikethroughToolbarItemIdentifier = "StrikethroughToolbarItemIdentifier",
    AlignLeftToolbarItemIdentifier = "AlignLeftToolbarItemIdentifier",
    AlignRightToolbarItemIdentifier = "AlignRightToolbarItemIdentifier",
    AlignCenterToolbarItemIdentifier = "AlignCenterToolbarItemIdentifier",
    AlignFullToolbarItemIdentifier = "AlignFullToolbarItemIdentifier",
    InsertLinkToolbarItemIdentifier = "InsertLinkToolbarItemIdentifier",
    UnlinkToolbarItemIdentifier = "UnlinkToolbarItemIdentifier",
    InsertImageToolbarItemIdentifier = "InsertImageToolbarItemIdentifier",
    FontToolbarItemIdentifier = "FontToolbarItemIdentifier",
    BulletsToolbarItemIdentifier = "BulletsToolbarItemIdentifier",
    NumbersToolbarItemIdentifier = "NumbersToolbarItemIdentifier",
    RandomTextToolbarItemIdentifier = "RandomTextToolbarItemIdentifier";
@implementation AppController : CPObject
{
    /* we should add a handle to the HUD view that we are going to implement, as well as the primary window */
    CPWindow mainWindow;
    CPPanel hudEditor;
    CPTextField textDisplay;
    WKTextField editorView;
    CPToolBar toolbar;
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
    mainWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask];
    hudEditor = [[CPPanel alloc] initWithContentRect:CGRectMake(40,40,700,300) styleMask:CPHUDBackgroundWindowMask | CPResizableWindowMask | CPClosableWindowMask];
  
    [self fillHud: hudEditor];
    [self layoutMainWindow: mainWindow];
    [mainWindow orderFront:self];
    // Uncomment the following line to turn on the standard menu bar.
    //[CPMenu setMenuBarVisible:YES];
}
-(void)fillHud:(CPPanel)aPanel
{
    var pContentView = [aPanel contentView],
        pFrame = [pContentView frame],
        margin = 20,
        topMargin = 40,
        eWidth = CGRectGetWidth( pFrame ) - 2 * margin,
        eXpos = margin,
        eYpos = topMargin,
        eHeight = CGRectGetHeight(pFrame) - margin - topMargin,
        eFrame = CGRectMake( eXpos, eYpos, eWidth, eHeight ),
        editor = [[WKTextView alloc] initWithFrame:eFrame];
    toolbar = [[CPToolbar alloc] initWithIdentifier:"Styling"];
    [toolbar setDelegate:self];
    [toolbar setVisible:YES];
    [aPanel setToolbar:toolbar];

    [aPanel setTitle:@"Example Text Editor"];
    [editor setAutoresizingMask: CPViewWidthSizable | CPViewHeightSizable | CPViewMinYMargin];
    [editor setDelegate: self];
    [pContentView addSubview:editor];
    editorView = editor;
}
-(void)textViewDidChange:(WKTextView)anEditor
{
    var htmlString = [anEditor htmlValue];
    [textDisplay setStringValue:htmlString];
    [textDisplay sizeToFit];
}
-(void)layoutMainWindow:(CPWindow)aWindow
{
    var button = [CPButton buttonWithTitle:"Show Editor"],
        contentView = [aWindow contentView],
        contentFrame = [contentView frame],
        cWidth = CGRectGetWidth( contentFrame ),
        cHeight = CGRectGetHeight( contentFrame ),
        buttonFrame = [button frame],
        bWidth = CGRectGetWidth( buttonFrame ),
        bHeight = CGRectGetHeight( buttonFrame ),
        margin = 20,
        buttonXpos = cWidth - margin - bWidth,
        buttonYpos = cHeight - margin - bHeight,
        buttonOrigin = CGPointMake( buttonXpos, buttonYpos );
    textDisplay = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
    [textDisplay setEditable:NO];
    [textDisplay setLineBreakMode:CPLineBreakByWordWrapping];
    [textDisplay setFrameSize:CPSizeMake(400,20)];
    [textDisplay setCenter:[contentView center]];
    [button setTarget:self];
    [button setAction:@selector(showEditor:)];
    [button setFrameOrigin:buttonOrigin];
    [contentView addSubview:textDisplay];
    [contentView addSubview:button];
}
-(void)showEditor:(CPButton)aButton
{
    [hudEditor orderFront:self];
}
// Return an array of toolbar item identifier (all the toolbar items that may be present in the toolbar)
- (CPArray)toolbarAllowedItemIdentifiers:(CPToolbar)aToolbar
{
    return [NewToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, BoldToolbarItemIdentifier, ItalicsToolbarItemIdentifier, UnderlineToolbarItemIdentifier, StrikethroughToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, AlignLeftToolbarItemIdentifier, AlignRightToolbarItemIdentifier, AlignCenterToolbarItemIdentifier, AlignFullToolbarItemIdentifier, CPToolbarSpaceItemIdentifier, BulletsToolbarItemIdentifier, NumbersToolbarItemIdentifier, InsertLinkToolbarItemIdentifier, UnlinkToolbarItemIdentifier, InsertImageToolbarItemIdentifier, FontToolbarItemIdentifier, CPToolbarFlexibleSpaceItemIdentifier];
}
// Return an array of toolbar item identifier (the default toolbar items that are present in the toolbar)
- (CPArray)toolbarDefaultItemIdentifiers:(CPToolbar)aToolbar
{
    return [self toolbarAllowedItemIdentifiers:aToolbar];
}
// Create the toolbar item that is requested by the toolbar.
- (CPToolbarItem)toolbar:(CPToolbar)aToolbar itemForItemIdentifier:(CPString)anItemIdentifier willBeInsertedIntoToolbar:(BOOL)aFlag
{
    // Create the toolbar item and associate it with its identifier
    var toolbarItem = [[CPToolbarItem alloc] initWithItemIdentifier:anItemIdentifier];
    var mainBundle = [CPBundle mainBundle];
    var actionMap =
    {
        NewToolbarItemIdentifier:           { 'image': 'page_white.png',        'label': 'New',     'target': editorView,   'action':@selector(clearText:) },
        BoldToolbarItemIdentifier:          { 'image': 'text_bold.png',         'label': 'Bold',    'target': editorView,   'action':@selector(boldSelection:) },
        ItalicsToolbarItemIdentifier:       { 'image': 'text_italic.png',       'label': 'Italics', 'target': editorView,   'action':@selector(italicSelection:) },
        UnderlineToolbarItemIdentifier:     { 'image': 'text_underline.png',    'label': 'Under',   'target': editorView,   'action':@selector(underlineSelection:) },
        RandomTextToolbarItemIdentifier:    { 'image': 'page_white_text.png',   'label': 'Lorem',   'target': self,         'action':@selector(setRandomText:) },
        StrikethroughToolbarItemIdentifier: { 'image': 'text_strikethrough.png','label': 'Strike',  'target': editorView,   'action':@selector(strikethroughSelection:) },
        AlignLeftToolbarItemIdentifier:     { 'image': 'text_align_left.png',   'label': 'Left',    'target': editorView,   'action':@selector(alignSelectionLeft:) },
        AlignRightToolbarItemIdentifier:    { 'image': 'text_align_right.png',  'label': 'Right',   'target': editorView,   'action':@selector(alignSelectionRight:) },
        AlignCenterToolbarItemIdentifier:   { 'image': 'text_align_center.png', 'label': 'Center',  'target': editorView,   'action':@selector(alignSelectionCenter:) },
        AlignFullToolbarItemIdentifier:     { 'image': 'text_align_justify.png','label': 'Justify', 'target': editorView,   'action':@selector(alignSelectionFull:) },
        BulletsToolbarItemIdentifier:       { 'image': 'text_list_bullets.png', 'label': 'Bullets', 'target': editorView,   'action':@selector(insertUnorderedList:) },
        NumbersToolbarItemIdentifier:       { 'image': 'text_list_numbers.png', 'label': 'Numbers', 'target': editorView,   'action':@selector(insertOrderedList:) },
        InsertLinkToolbarItemIdentifier:    { 'image': 'link.png',              'label': 'Link',    'target': self,         'action':@selector(doLink:) },
        UnlinkToolbarItemIdentifier:        { 'image': 'link_break.png',        'label': 'Unlink',  'target': editorView,   'action':@selector(unlinkSelection:) },
        InsertImageToolbarItemIdentifier:   { 'image': 'picture.png',           'label': 'Image',   'target': self,         'action':@selector(doImage:) },
    };
    var action = actionMap[anItemIdentifier];
    if (action)
    {
        var image = [[CPImage alloc] initWithContentsOfFile:[mainBundle pathForResource:@"silk/"+action['image']] size:CPSizeMake(16, 16)];
        [toolbarItem setImage:image];
        [toolbarItem setTarget:action['target']];
        [toolbarItem setAction:action['action']];
        [toolbarItem setLabel:action['label']];
        [toolbarItem setMinSize:CGSizeMake(16, 16)];
        [toolbarItem setMaxSize:CGSizeMake(16, 16)];
    }
    else if (anItemIdentifier == FontToolbarItemIdentifier)
    {
        [toolbarItem setMinSize:CGSizeMake(160, 24)];
        [toolbarItem setMaxSize:CGSizeMake(160, 24)];
        var dropdown = [[CPPopUpButton alloc] initWithFrame:CGRectMake(0, 0, 160, 24) pullsDown:NO];
        [dropdown setTarget:self];
        [dropdown setAction:@selector(doFont:)];
        var fonts = [[CPFontManager sharedFontManager] availableFonts];
        for(var i=0; i<fonts.length; i++)
        {
            var fontName = fonts[i],
                menuItem = [[CPMenuItem alloc] initWithTitle:fontName action:nil keyEquivalent:nil];
            [dropdown addItem:menuItem];
        }
        [dropdown setTitle:@"Select Font..."];
        [toolbarItem setView:dropdown];
        [toolbarItem setLabel:"Font"];
    }
    return toolbarItem;
}
- (void)doFont:(id)button {
    var fontName = [button titleOfSelectedItem];
    [editorView setFontNameForSelection:fontName];
}
- (@action)doLink:(id)sender
{
    var link = prompt("Enter a link: ", "http://www.280north.com");
    if (link)
        [editorView linkSelectionToURL:link];
}
- (@action)doImage:(id)sender
{
    var link = prompt("Enter an image URL: ", "http://objective-j.org/images/cappuccino-icon.png");
    if (link)
        [editorView insertImageWithURL:link];
}
- (@action)setRandomText:(id)sender
{
    [editorView setHtmlValue:"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut est urna, vulputate sed viverra dignissim, consequat vitae eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse ut sapien enim, et pellentesque elit. In commodo facilisis est, et tempus lacus aliquam vitae. Maecenas quam nulla, elementum ut tristique quis, cursus a nisi. Duis mollis risus vel velit molestie convallis nec a purus. Donec neque arcu, suscipit sit amet mattis eu, fringilla ac sapien. Ut lorem nibh, mollis in tincidunt at, volutpat ut turpis. Maecenas nulla est, tincidunt pharetra consectetur vel, laoreet sed nibh. Pellentesque tempor diam vel elit commodo aliquet. Donec congue fringilla eros a tincidunt. Praesent accumsan mi tincidunt arcu ultricies nec pellentesque dolor faucibus. Mauris sed nisl in ligula porta congue et quis turpis. Suspendisse in lorem at felis tempus semper. In porta enim a ipsum aliquet consectetur.</p><p>Mauris ac tellus orci. Aenean egestas porta ornare. Cras nisl lorem, vulputate ac pellentesque eu, aliquet ac leo. Proin eros libero, tincidunt sed sodales eget, elementum non augue. Praesent convallis auctor venenatis. Suspendisse id urna quam. Aliquam sagittis, leo commodo laoreet interdum, arcu felis dictum velit, a sodales justo tortor a erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nibh magna, consequat et congue eu, bibendum id nisi. Cras gravida risus in nulla pharetra sagittis. Cras neque eros, consectetur nec bibendum eget, bibendum dictum libero. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nam rutrum dictum neque vel eleifend. Vivamus tempus, lorem vel ultricies ullamcorper, ante risus imperdiet massa, nec aliquam orci ipsum ut nisl. Aliquam id justo eu lorem dapibus tincidunt. Donec suscipit consequat metus, sed venenatis lorem malesuada sit amet. Ut at risus ut ligula vulputate auctor. Vivamus rutrum elementum porttitor. Fusce quam arcu, tristique eget consectetur eu, iaculis in urna.</p><p>Donec a metus ac elit faucibus sagittis non a ligula. In aliquet, lectus sed pulvinar bibendum, justo ligula faucibus sem, vestibulum eleifend lacus augue a eros. Suspendisse potenti. Phasellus vehicula blandit ultrices. Donec tortor nulla, fermentum nec viverra id, consequat non metus. Fusce nunc urna, aliquet sit amet varius ut, dapibus a sem. Aliquam erat volutpat. Vestibulum at enim et magna lacinia sollicitudin id nec dolor. Sed ultricies urna ut justo blandit tincidunt. Sed sit amet orci et justo pellentesque iaculis accumsan ac quam.</p><p>Nunc tristique felis quis leo blandit eget iaculis lacus hendrerit. Maecenas euismod consequat lacus quis porttitor. Quisque consequat, metus eu interdum vulputate, quam dolor porttitor dui, non faucibus quam nibh nec erat. Integer sit amet gravida quam. Proin nunc eros, tincidunt sit amet accumsan laoreet, dictum vel sapien. Praesent at fringilla orci. Etiam vehicula lacinia nisi, et molestie justo congue molestie. Maecenas tempus, quam nec placerat suscipit, lorem sapien feugiat augue, id pharetra augue enim eu nisl. Morbi ullamcorper lacus ac dolor ultricies vel pellentesque odio consequat. Maecenas a pellentesque nunc. Phasellus a varius massa. Vestibulum eget tortor eget ante iaculis molestie. Pellentesque eu augue metus, ut pellentesque purus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur consequat feugiat tincidunt. Proin tellus tortor, pharetra vel rhoncus ac, varius eget nisi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras et nunc metus. Pellentesque tincidunt iaculis erat id porta. Curabitur eget magna et velit eleifend tempor.</p><p>Vestibulum ultricies leo at augue malesuada congue. Maecenas laoreet metus et nunc consectetur placerat. Nulla facilisi. Duis iaculis tristique feugiat. Ut quis consectetur justo. Praesent condimentum sagittis dui, in lobortis tellus accumsan quis. In aliquam lacus non dolor accumsan rutrum. Etiam sed urna dolor. Donec consectetur lacus eu ante sodales feugiat. Aliquam aliquet nibh vel massa mattis pellentesque. Curabitur et tortor nisl, ut consequat felis. Mauris non orci at tortor ultrices condimentum. Aliquam erat volutpat. Mauris porttitor, diam convallis semper hendrerit, erat mi tempor dolor, id semper augue justo fermentum odio. Sed vitae nulla eu augue fringilla pellentesque vel ac neque. Nullam arcu nibh, auctor ut accumsan ac, ullamcorper eu velit. Integer in ligula nec felis auctor viverra. In commodo malesuada volutpat. Etiam justo elit, tincidunt ac semper sed, eleifend eu odio. Cras ac nulla eget lorem tempor venenatis.</p>"];
}
- (void)textViewDidLoad:(WKTextView)textView
{
    // Update the selected font.
    [self textViewCursorDidMove:textView];
}
- (void)textViewCursorDidMove:(WKTextView)textView
{
    var items = [toolbar visibleItems];
    for (i=0; i<items.length; i++)
    {
        var item = items[i];
        if ([item itemIdentifier] == FontToolbarItemIdentifier)
        {
            var font = [editorView font];
            [[item view] selectItemWithTitle:font];
        }
    }
}

@end 

Most of the code there builds the toolbar and handles requests such as font type changes and more.  The example code uses a CPToolBar to create a list of clickable menu items.  The buttons are pointed to functions and methods made available on the WKTextEditor to enact their predefined functionality, with a few exceptions that require additional processing.  For example, requests for setting a font style, a link, or an image requires more information than a direct function call will provide, so we point these buttons to a handler function in another object so that we can do some pre-processing before invoking the editor method.

I have found that the delegate function textViewDidEndEditing: is extremely difficult to trigger, so we use textViewDidChange: to give us an opportunity to remove the text from the editor and update our textField on the main window.  You will notice as you type that characters, which would be interpreted as HTML, are encoded so as not to disturb the CPWebView's display of the data.  This ensures that a less-than sign is always interpreted as a less than sign and not the beginning of an HTML tag.

However, if you play with bold, italic, or font settings, you will see that HTML is evident in the text, and is used to display the text as it has been formatted.  Ideally, we would save the text in this manner for easy access, and we could then use a CPWebView to display the text any time we needed to display the text in exactly the same manner in which it was originally formatted.

This screen shot shows an example of unformatted text with some encoded characters at the end:

Final Thoughts

I had a particularly difficult time getting the WKTextView example to work, because I did not realize how dependent it is on the Google Closure library. Overall, the results were worth the time and effort struggling with the closure installation and making changes to WKTextView to handle my localized settings.

This editor and the CPWebView can be powerful tools in an application that requires a text editor interface and provides the user ability to change or update text formatting within the document.

It is extremely important to note that the string value returned by the text editor is html encoded text, so it must be specially processed to remove formatting tags before the raw text becomes available.  The WKTextView class does not provide any functions to get the raw string data without the embedded formatting.

No comments:

Post a Comment