login

Table links published on Feb 9, tagged with html, javascript, website

Often you might want to present a table of items, each of which links to its own page. Typically you might add an additional cell with a link to go to the item-specific page.

Wouldn’t it be better if the entire row was itself clickable? Well, I did the googling, and here’s one easy way I’ve found to accomplish that.

You’ll need a little jQuery:

$(function() {
  $('tbody.link tr').click(function() {
    window.location = $(this).find('a').attr('href');
  }).hover(function() {
    $(this).toggleClass('pointer');
  });
});

Of course, you can choose to put this only on pages that need it, but it’s not very heavy and if it’s on your site-wide template, you can quickly apply this method to any table you want by just adding a class to the tbody tag (all of your tables do have thead and tbody tags, right?)

For that hover callback to have the desired effect and let your users know they should click on the row, you’ll need a little bit of css as well:

.pointer { cursor: pointer; }

You could put this css change right in the javascript, but I find this pointer class comes in handy throughout my site anyway.

Finally, for any tables which you want to behave this way, just use markup like the following:

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>

  <tbody class="link"><!-- 1. add the class -->
    <tr>
      <td>
        <a href="/items/1"></a><!-- 2. add the link(s) -->
        Item_1
      </td>
      <td>
        The first item
      </td>
    </tr>
    <tr>
      <td>
        <a href="/items/2"></a>
        Item_2
      </td>
      <td>
        The second item
      </td>
    </tr>

    <!-- ... -->

  </tbody>
</table>

Notice the content of the “Name” field is outside of the link tag and the link itself has no content. This ensures no actual link will be visible to confuse users, all they have to do is click anywhere on the row.

For a real example, checkout the archives page.

Live Search (part 2) published on Jan 30, tagged with javascript, website

In my last post I went over setting up sphinx full-text search using an xml data source from a yesod application as well as hooking into sphinx to return search results for a given query as a JSON data feed.

In this (shorter) post, I’ll go over the front-end javascript that I used to implement a fairly simple search-as-you-type interface.

Object oriented

Now, I could have easily defined some simple functions in the global namespace to execute the search, display the results, then attach an event handler to the changes to the input box, but I’d rather not.

Javascript can be used fairly effectively in an object oriented way. No, I’m not doing any inheritance or method overloads, but I do want to try and group all my logic in an instance of some object. This will let me store some values in instance variables (properties) for use between methods as well as give me a namespace for all my stuff.

Here’s the structure:

var Search = {
    execute: function(qstring) {
        // actually execute the search and call display as the success 
        // callback
    },

    display: function(results) {
        // update the page with the contents of the search results.
    },

    attach: function() {
        // attach a listener for changes to the input element and fire 
        // off the search when appropriate.
    }
};

Our feed is accessed at /search/j/query-string and returns something like this:

[
  {
    "slug":    "some_post",
    "title":   "Some post",
    "excerpt": "... some excerpt with matches in it ..."
  },
  {
    "slug":   "other_post",
    "title":  "Other title",
    "excerpt":"... other excerpt with matches ..."
  },
  ...
]

Given that, our execute and display functions should look like this:

    execute: function(qstring) {
        var search = this;
        var url    = "/search/j/" + encodeURIComponent(qstring);

        $.getJSON(url, function(data) {
            search.display(data);
        });
    },

    display: function(results) {
        var html = "";

        $.each(results, function(id, result) {
            html += '<div class="result">'
                  + '<h3><a href="/posts/' + result['slug'] + '/">' + result['title'] + "</a></h3>"
                  + '<div class="result-excerpt">' + result['excerpt'] + '</div>'
                  + '</div>';
        });

        // assume this property exists for now
        this.results.html(html);
    },

Our attach method will handle a few things:

  1. Store selectors for the input element and the results container as properties on our object.

  2. Attach a listener to the input element that fires every time a character is entered.

  3. Check that the entered search term is non-empty, big enough, and has actually changed – to prevent a “needless” search.

    attach: function() {
        this.search  = $('#search');
        this.results = $('#results');

        var search = this;

        this.search.keyup(function() {
            var $this = $(this);

            var newVal = $this.val();
            var oldVal = $this.data('old-value');

            if (newVal.length >= 3 && newVal != oldVal) {
                search.execute(newVal);
            }

            $this.data('old-value', newVal);
        });
    }

I use jQuery’s data function to store the input’s current value between each event to see if it’s changed since last time.

Note that we also have to store a reference to this outside of the keyup callback, since calling this inside that closure means something else (the element itself).

With all that in place, a search page that uses this object would look something like this:

<input id="search">

<div id="results"></div>

<script>
    $(function() {
        Search.attach();
    });
</script>

That’s it, simple and effective. Go ahead, try it out.

Live Search published on Jan 29, tagged with haskell, website, yesod

I’ve had some fun recently, adding full-text search support to the posts on the site to try and make a simple-but-still-useful archive.

I’d like to post a bit about the feature and how it works. It’s got a few moving parts so I’m going to break it up a bit.

This post will focus on the backend, setting up sphinx, providing content to it from a yesod application, and executing a search from within a handler. The second post will go into the front-end javascript that I implemented for a pretty simple but effective search-as-you-type interface.

For the full context, including required imports and supporting packages, please see this feature in the wild.

Sphinx

Sphinx is a full-text search tool. This assumes you’ve got some concept of “documents” hanging around with lots of content you want to search through by key word.

What sphinx does is let you define a source – a way to get at all of the content you have in a digestible format. It will then consume all that content and build an index which you can search very efficiently returning a list of Ids. You can then use those Ids to display the results to your users.

There are other aspects re: weighting and attributes, but I’m not going to go into that here.

The first thing you need to do (after installing sphinx) is to get your content into a sphinx-index.

If you’ve got the complete text you’ll be searching actually in your database, sphinx can natively pull from mysql or postgresql. In my case, the content is stored on disk in markdown files. For such a scenario, sphinx allows an “xmlpipe” source.

What this means is that you provide sphinx with a command to fetch an xml document containing the content it should index.

Now, if you’ve got a large amount of content, you’re going to want to use clever conduit/enumerator tricks to stream the xml to the indexer in constant memory. That’s what’s being done in this example. I’m doing something a little bit more naive – for two reasons:

  1. I need to break out into IO to get the content. This is difficult from within a lifted conduit Monad, etc.
  2. I don’t have that much shit – the thing indexes in almost no time and using almost no memory even with this naive approach.

So, here’s the simple way:

getSearchXmlR :: Handler RepXml
getSearchXmlR = do
    -- select all posts
    posts <- runDB $ selectList [] []

    -- convert each post into an xml block
    blocks <- liftIO $ forM posts $ \post -> do
        docBlock (entityKey post) (entityVal post)

    -- concat those blocks together to one xml document
    fmap RepXml $ htmlToContent $ mconcat blocks

    where
        htmlToContent :: Html -> Handler Content
        htmlToContent = hamletToContent . const

docBlock :: PostId -> Post -> IO Html
docBlock pid post = do
    let file = pandocFile $ postSlug post

    -- content is kept in markdown files on disk, if the file can't be 
    -- found, try to use the in-db description, else just give up.
    exists <- doesFileExist file
    mkd    <- case (exists, postDescr post) of
        (True, _         ) -> markdownFromFile file
        (_   , Just descr) -> return descr
        _                  -> return $ Markdown "nothing?"

    return $
        -- this is the simple document structure expected by sphinx's 
        -- "xmlpipe" source
        [xshamlet|
            <document>
                <id>#{toPathPiece pid}
                <title>#{postTitle post}
                <body>#{markdownToText mkd}
            |]

    where
        markdownToText :: Markdown -> Text
        markdownToText (Markdown s) = T.pack s

With this route in place, a sphinx source can be setup like the following:

source pbrisbin-src
{
	type		= xmlpipe
        xmlpipe_command = curl http://localhost:3001/search/xmlpipe
}

index pbrisbin-idx
{
	source		= pbrisbin-src
	path		= /var/lib/sphinx/data/pbrisbin
	docinfo		= extern
	charset_type	= utf-8
}

Notice how I actually hit localhost? Since pbrisbin.com is reverse proxied via nginx to 3 warp instances running on 3001 through 3003 there’s no need to go out to the internet, dns, and back through nginx – I can just hit the backend directly.

With that setup, we can do a test search to make sure all is well:

$ sphinx-indexer --all # setup the index, ensure no errors
$ sphinx-search mutt
Sphinx 2.1.0-id64-dev (r3051)
Copyright (c) 2001-2011, Andrew Aksyonoff
Copyright (c) 2008-2011, Sphinx Technologies Inc 
(http://sphinxsearch.com)

using config file '/etc/sphinx/sphinx.conf'...
index 'pbrisbin-idx': query 'mutt ': returned 6 matches of 6 total in 
0.000 sec

displaying matches:
1. document=55, weight=2744, gid=1, ts=Wed Dec 31 19:00:01 1969
2. document=62, weight=2728, gid=1, ts=Wed Dec 31 19:00:01 1969
3. document=73, weight=1736, gid=1, ts=Wed Dec 31 19:00:01 1969
4. document=68, weight=1720, gid=1, ts=Wed Dec 31 19:00:01 1969
5. document=56, weight=1691, gid=1, ts=Wed Dec 31 19:00:01 1969
6. document=57, weight=1655, gid=1, ts=Wed Dec 31 19:00:01 1969

words:
1. 'mutt': 6 documents, 103 hits

Sweet.

Haskell

Now we need to be able to execute these searches from haskell. This part is actually going to be split up into two sub-parts: first, the interface to sphinx which returns a list of SearchResults for a given query, and second, the handler to return JSON search results to some abstract client.

I’ve started to get used to the following “design pattern” with my yesod sites:

Keep Handlers as small as possible.

I mean no bigger than this:

getFooR :: Handler RepHtml
getFooR = do
    things      <- getYourThings

    otherThings <- doRouteSpecificStuffTo things

    defaultLayout $ do
        setTitle "..."
        $(widgetFile "...")

And that’s it. Some of my handlers break this rule, but many of them fell into it accidentally. I’ll be going through and trying to enforce it throughout my codebase soon.

For this reason, I’ve come to love per-handler helpers. Tuck all that business logic into a per-handler or per-model (which often means the same thing) helper and export a few smartly named functions to call from within that skinny handler.

Anyway, I digress – Here’s the sphinx interface implemented as Helpers.Search leveraging gweber’s great sphinx package:

The below helper actually violates my second “design pattern”: Keep Helpers generic and could be generalized away from anything app-specific by simply passing a few extra arguments around. You can see a more generic example here.

sport :: Int
sport = 9312

index :: String
index = "pbrisbin-idx"

-- here's what I want returned to my Handler
data SearchResult = SearchResult
    { resultSlug    :: Text
    , resultTitle   :: Text
    , resultExcerpt :: Text
    }

-- and here's how I'll get it:
executeSearch :: Text -> Handler [SearchResult]
executeSearch text = do
    res <- liftIO $ query config index (T.unpack text)

    case res of
        Ok sres -> do
            let pids = map (Key . PersistInt64 . documentId) $ matches sres

            posts <- runDB $ selectList [PostId <-. pids] []

            forM posts $ \(Entity _ post) -> do
                excerpt <- liftIO $ do
                    context <- do
                        let file = pandocFile $ postSlug post

                        exists <- doesFileExist file
                        mkd    <- case (exists, postDescr post) of
                            (True, _         ) -> markdownFromFile file
                            (_   , Just descr) -> return descr
                            _                  -> return $ Markdown "nothing?"

                        return $ markdownToString mkd

                    buildExcerpt context (T.unpack text)

                return $ SearchResult
                            { resultSlug    = postSlug post
                            , resultTitle   = postTitle post
                            , resultExcerpt = excerpt
                            }

        _ -> return []

    where
        markdownToString :: Markdown -> String
        markdownToString (Markdown s) = s

        config :: Configuration
        config = defaultConfig
            { port   = sport
            , mode   = Any
            }

-- sphinx can also build excerpts. it doesn't do this as part of the 
-- search itself but once you have your results and some context, you 
-- can ask sphinx to do it after the fact, as I do above.
buildExcerpt :: String -- ^ context
             -> String -- ^ search string
             -> IO Text
buildExcerpt context qstring = do
    excerpt <- buildExcerpts config [concatMap escapeChar context] index qstring
    return $ case excerpt of
        Ok bss -> T.pack $ C8.unpack $ L.concat bss
        _      -> ""

    where
        config :: E.ExcerptConfiguration
        config = E.altConfig { E.port = sport }

        escapeChar :: Char -> String
        escapeChar '<' = "&lt;"
        escapeChar '>' = "&gt;"
        escapeChar '&' = "&amp;"
        escapeChar c   = [c]

OK, so now that I have a nice clean executeSearch which I don’t have to think about, I can implement a JSON route to actually be used by clients:

getSearchR :: Text -> Handler RepJson
getSearchR qstring = do
    results <- executeSearch qstring

    objects <- forM results $ \result -> do
        return $ object [ ("slug"   , resultSlug    result)
                        , ("title"  , resultTitle   result)
                        , ("excerpt", resultExcerpt result)
                        ]

    jsonToRepJson $ array objects

Gotta love that skinny handler, does its structure look familiar?

You can see the result by visiting search/j/mutt for example.

In the next post, I’ll give you the javascript that consumes this, creating the search-as-you-type interface you see on the Archives page.

UI Refresh published on Jan 27, tagged with website, yesod

Astute readers may have noticed, the site looks a little bit different today. I know, it’s tough to discern, but if you look closely you might see… It’s now dark on light!

This is actually a small, tangential change that I made as part of a sweeping upgrade and cleanup effort. In moving to Yesod 0.10 (the 1.0 release candidate), I decided to take an axe to some of the bloatier areas of the site.

After dropping a few pounds in backend logic, I decided to keep going and attack the css as well – and by attack, I mean drop entirely.

Believe it or not just about all styling on the site is now coming from twitter’s awesome bootstrap framework.

Breadcrumbs, notices, login dropdowns, general forms, and sweet tables all without a line of styling by me.

The change does make the site less-then-great on less-than-wide monitors, but I’m not sure how many people are viewing this on mobile devices, etc. We’ll see if I need to bring back my @media queries.

Bootstrap 2.0 brings a “responsive” grid, so now the site looks pretty good on just about any device.

I should be posting more in the coming weeks on some of the specific changes as well a new search feature I’m hoping to roll out soon, but I figured such a noticeable visual change should have an accompanying post… So, there it was.

Static Refactor published on Nov 22, tagged with website

Just a quick heads-up post about a recent site refactoring.

I decided to switch to nginx from lighttpd, let it do the static file serving, and at the same time drop all the complicated redirects I’d been carrying since going live on yesod. I also cleaned out the /static directory a little bit and streamlined its folder structure.

Below please find info about the deprecated routes that I’ve finally dropped (and some that were dropped a while ago).

Please use pbrisbin.com to view the site.

No longer redirecting /dotfiles and /bin to github

Please see github for all of my configs and other projects.

No longer redirecting *.rss to /feed

Please use pbrisbin.com/feed/ for my rss.

Removed /music

Please email if you really were interested in that stuff.

Rearranged documentation folders

Haskell docs (including xmonad libraries) are in /static/docs/haskell and ruby docs are in /static/docs/ruby.

I think that’s it – let me know if I’ve missed something and I’ll add a note here.