ANTH 101 – A Deeper Dive

Michael Wesch - Pop!Tech 2009 - Camden, ME

It’s been a good while since I had the pleasure of working with Mike Wesch and Ryan Klataske on ANTH 101. I revisited the course recently to write a letter of support for an award submission for online courses. I am posting an extended version of that letter below because I think it paints a path with online courses that is rarely followed but is, nonetheless, replicable and worth considering.

Bigger Picture?

I see ANTH101 as a path forward that makes me hopeful in an online space that seems increasingly depressing.1 You have two races currently in online learning. There is a race to be the cheapest and easiest place to enroll.2 This article on Liberty “University” paints that picture pretty well. This world will be like the fast food industry in many ways. How uniform can we make things? How automated? What’s the least we can pay the fewest humans? The only path to profit will be through ridiculous scale. There will be very little difference between these providers. They will use LMS products that are very similar while following very similar online course rubrics and probably (poorly) paying many of the same adjunct/itinerant online course faculty. Additional sadness will occur when the same OPM is creating content, marketing etc. for multiple universities for the same courses and programs.3 There will be increasing overlap as course, LMS, and textbook become one thing you buy (as a student or an institution). The differences will grow ever smaller. Scale will need to increase ever more.4

Then there is the prestige battle. That’s obviously trickier as restricting access to the Harvards of the world is what makes them prestigious.5 The media here will likely be high dollar. They’ll do TED-Talkish-Dineyfied-Edutainment™. Read what colleges are paying to develop these courses. It is insane. They’ll likely try to sell off badges and certificates to try to get at the scale they need to make it profitable without becoming less exclusive. You see that happening with Coursera and Ed X MOOCs. Exclusivity for the masses! Do not worry. The brand will be protected. Order will be preserved. We’ll resell these courses to other Universities.6 US News & World Reports will still tell us what the best education is.7

With all that at least partially vented, let’s look at what ANTH 101 does differently.

ANTH 101

The core message of ANTH 101 is that you have to “live your way to a new way of thinking.” In life, and especially in online courses, it’s easy to do what has been done before. It’s easy to take the safe road. Textbook publishers and LMS vendors have built the content and tools that expeditiously serve the established patterns. Those goals have been pretty low–counting discussion board responses and automatically grading quizzes as students plod along well-trod paths. Standardized tools and standardized content result in standardized experiences. ANTH 101 breaks those expectations and sets a new path for possibilities with patterns and tools that are broadly applicable, replicable, and focused on engaging students.

ANTH 101 doesn’t just take on the most difficult aspects of online learning; it actually turns these difficulties into features that improve the entire experience. It is the ability to take seemingly opposing forces and unite them in a way that is more powerful than the two parts that makes ANTH 101 a course worthy of admiration and emulation.

The idea that an online course needs to live in a single online space is a popular myth. ANTH 101 leverages a variety of tools and platforms to make a cohesive experience that is powerful and compelling because these different tools do different things well. The efficacy with which these things are blended together with a large and diverse student body (not to mention an external faculty) is proof that this can be done and it can be done well.

The main site (anth101.com) acts as the central hub for a number of tools and platforms. This site is built in WordPress which enables Professor Wesch and others to create, edit, and publish multimedia content without a high degree of technical overhead while still maintaining the ability to create a website with impressive aesthetic polish. The course doesn’t feel like a mundane LMS course–it feels and behaves like the kind of website students would visit without the coercion of grades.

A large portion of student content creation is done in Instagram. Using this tool helps deal with a number of significant problems. In a class with large numbers of authors creating multimedia content based on due dates you get large but short pulses of resource demands. Those peaks in demand are followed by longer valleys where you don’t need nearly that much server power. It becomes more difficult to predict the level of resources you’ll need when you open the course to the public. This drives up expenses and increases technical overhead. By passing the media creation and storage to Instagram, ANTH 101 can be hosted on a very low-cost server. Additionally, Instagram is a tool and community that most of the students are familiar with already. This lowers support needs with the additional advantage of being able to use Instagram’s support team when technical issues do arise. Using a familiar tool that is associated with a student’s life outside education also helps reinforce the theme that this experience is about making changes in how you live and perceive things outside of a single course. ANTH 101 is about the real world and it uses the tools you use in that world.

One of the more important features of the ANTH 101 is the attention to detail. One place that’s especially obvious is in the course is how the work has been framed. They are not assignments but challenges. Shifting the language from the outset helps change how students perceive the work they’re doing for the course. Assignments are things forced on you by outside actors. Challenges are heroic things you take up of your own volition. Each challenge is designed to cover particular learning objectives as one would expect, but the challenge is also meant to create media that other students would be interested in experiencing. These challenges are the kind of content that invite curiosity and investigation. As such, student work becomes an integral part of the course.  Previous exemplary student responses to these challenges are highlighted to help set high expectations. Students actively read and comment on each other’s content increasing their own exposure to a variety of anthropological concepts while also binding them together more deeply as a community. The fact that ANTH 101 students continue to participate and communicate long after the course is over is one of the strongest possible indicators of the success of this course.

ANTH 101 is a large-enrollment class with hundreds of students taking the course at Kansas State each semester both online and in person. Additionally, the course is open to faculty and students at other universities.  Large numbers of students from multiple universities taking a general education class results in extraordinary diversity. Students come to this class with a variety of life experiences, widely varying educational backgrounds, and vastly different reasons for taking the course. That diversity is incorporated into the course as an affordance rather than an obstacle. Student perspectives change because they are exposed to and interact with so many differing viewpoints.

While ANTH 101 is a large community it uses hashtags in Instagram to create communities at various scales so that all participants can find connections and inspiration. There is the main course tag #anth101 which lets students and faculty see any challenge from across the entire community. This scale may not be optimal for community building but the sheer volume of content helps creates energy and enthusiasm. Students see and participate in a community beyond their individual course and see perspectives that are different than their own. There are also challenge-specific hashtags like #anth101challenge3 which focus the content to that one challenge. Seeing students from all over the world doing the same challenge changes how students perceive their own work and helps reinforce thematic elements of the course. Smaller communities led by teaching assistants or outside faculty have their own hashtags like #anth204hedges. This allows for the creation of smaller more tightly knit communities. On a pragmatic level, the combination of challenge hashtags and community hashtags makes it easy for teaching assistants to stay on top of student participation.

The role of content in this course is powerful and innovative in a number of ways. The videos that Professor Wesch creates establish his social presence while also teaching the content. There is a continuous juxtaposition of his life with the larger frame of anthropology. The tandem progression of getting to know your professor and getting to know anthropology is a powerful narrative.  The student work follows this pattern. Students get to know themselves and their environment while gaining larger anthropological understanding. Students also get to see their professor as a person who is not only actively participating in the challenges he asks of them but who is also engaging in the same risks and adventures. Group cohesion is continually reinforced when students see and comment on one another’s work throughout the class.

This content also embodies the principles of the class as Wesch captures his own mistakes, documents his fears, and “lives his way into a different way of thinking.” These adventures are often very physical–like when he learns to do a handstand or to play an instrument–but are captured and reflected on digitally. Once again, we have two seeming opposites–the digital and the physical–being used to create something greater than the two parts.

ANTH 101 provides media in a myriad of formats. The course content exists online in a mixture of text, audio, and video. It’s also accompanied by a digital textbook that can be accessed for free or printed out for a small fee. An interactive digital textbook is available through Top Hat which includes self-grading quizzes and interactive elements.

There is no question that Professor Wesch is a talented creator of all kinds of multimedia. That requires a great deal of skill and commitment. But Professor Wesch also does a remarkable job of using publicly available media to reinforce conceptual messages and support students technically. For instance, in Challenge One – Talking to Strangers, media from popular culture is leveraged in a number of important ways. First, the challenge itself has been a popular challenge on social media. There are strong examples out there and it’s likely a number of students have seen them. That helps set a higher bar for what the students are going to produce while making the concept more approachable because of its familiarity. Because the concept is popular, Wesch can then choose from a variety of sophisticated media that has already been created to provide conceptual and technical support for students. You can see that clearly in the conceptual framing given to this challenge in the Humans of New York video and in the Big Talk video. Five Tips for Better Street Photography hits the other end of the spectrum by providing technical advice for this type of photography. Students get the advantage of powerful and engaging media featuring perspectives from outside academia while Wesch is able to incorporate sophisticated multimedia from subject matter experts without incurring additional cost. This blending of course content and “real world” content is a vital aspect of the course.

What ANTH 101 does is create an experience that leverages the digital to do more in the physical. It embodies the values of the discipline of anthropology in the challenges and the tools that comprise the course. The faculty of ANTH 101 live the experience with their students. Most impressive of all, the course continues to evolve. It would have been easy to stop when the course was successful. That didn’t happen. The course itself follows the model and continues to “live itself” into something better, something larger, and more powerful with each semester.

The Biggest Picture?

Given this was a letter of support, I didn’t get into concerns with Instagram8 and data privacy. That is something I worry about but one I worry about with Blackboard and Google and everything else we use with students.9 I don’t know how we deal with scale or create certain levels of technological sophistication without investing time/people/cash in a way that doesn’t seem to be happening these days. We seem to have let this lie for a long time and we are now so reliant on “free” services I don’t know what the path back looks like.

I also wonder if Mike could create something like this without such a blend of skills and interests. I don’t think you can hand off the creation/consideration of media and tool(s) to someone else and create a course like this. That’s just not how it works. If I’m right that would mean we need faculty who are more involved with the media and tools of online in addition to their discipline. They don’t have to be programmers but we can’t opt them out of the process the way many people are encouraging for online courses. The venn diagram of knowledge that is teaching, tools, and discipline should overlap significantly for all the people involved. That’s not an easy or popular argument. Those people aren’t cheap or easy to replace.

Long term, I also wonder about the University itself. If we’re teaching online courses and the students are not interacting in substantive and intentional ways with other students in other courses (within and outside their particular program) then what purpose is the larger University construct serving? Why wouldn’t you cut out the University as a middleman that adds little value while increasing costs? It would seem a short step for OPMs to begin getting accredited. From there the idea of gathering super star faculty that they could pay like tutors in South Korea seems equally plausible.

The window for higher ed to do some good things is closing. The path to a future does not lie in creating more of what already exists. The path to a successful restaurant does not lie in reselling pre-packaged food and hoping branding wins the day. We have lots of talented people but if we’re not aspiring to things worth their effort why would they stick around? If online higher education can’t make being part of a large community an exciting and beautiful thing, why bother with the overhead?


1 I will be your faithful Mumen Rider on this journey but only if you’re watching the original series with subtitles.

2 Now with 24hr a day rolling admission and a free phone shaped like a graduation hat! Operators, naturally, are standing by.

3 Online professional masters degrees are so hot right now.

via GIPHY

4 Do our current visa issues with China impact online education? New markets must be found! Can we educate pets online?

5 9 million people applied . . . we didn’t let anyone in. How exclusive is that?

6Many of these investors, of course, hope to make money on MOOCs, most likely not by selling the courses directly to students, but by renting them to small schools that then pair the outsourced lectures with proctored tests and, in some cases, group discussions and extra assignments.

7 Which is worse though, the rater or the desperate ratees?

8 I’m also frustrated with their API changes which broke a really nice aspect of the course. We have no recourse there.

9 What information does the school’s internet provider have?

Digital Histology & the Long Haul

Digital Histology homescreen screenshot.

Normally we finish our projects in anywhere from a few hours to a few weeks. Digital Histology has been the exception to that rule. I can see a reference to the site going back to Nov. of 2016! That doesn’t mean we’ve worked on this site continuously for years. The gaps have been frequent and long. OER grants have been written and won. Presentations have been made. Work has ebbed and flowed as the massive amount of content has been entered. There are more than 1500 pages1 and over 5GBs of images. It’s a large site. A ton of work has gone into its construction, new goals have developed, and just about all of it is a little strange. 2 I figured I’d better document some of this before I forgot all of it.

Made with Macromedia logo.

The History

I don’t recall all the details but essentially long ago in a Macromedia Authorware galaxy far, far away a digital histology program was constructed. Time passed. Acorns grew into trees. WINE was now required to launch the digital histology program. The screen was a tiny 752×613. It only ran on desktops. Updating it was nearly impossible. Things were not good. After much wandering we found one another and endeavored to put this work online for the betterment of humankind.

Having a previous project did some good things for us — the content was mostly created and there was experience working with digital projects. The previous construction patterns in Macromedia were very different from the way building on the web works today. We did quite a bit of work to parallel the previous interactions. I don’t know if that’s how I would have done it had we started from scratch. This was also the first time I’d built anything substantial with ACF.

The stacked tiering of the histology menu.

The Menu/Main Page

The menu has gone through a few iterations as we came up with different ways to deal with just how many pages were involved and how to deal with a really odd linking pattern. I’ll try to draw the menu pattern below. We had to figure out which pages had no grandchildren and for each one of those we would keep the title but link directly to the first child. Pages with no children would not be shown at all. Not super weird I guess but not normal.

Histology menu pattern - parent to child to terminal child

To deal with the scale, Jeff wrote a slick little plugin to dump the pages data into JSON. That saves us a lot of time especially given the way that WordPress recursively builds menus. Jeff also had the layout generated in Vue.

/* Plugin Name: Menu Cache Plugin
 * Version: 1.0.2
 * Author: Jeff Everhart
 * Author URI: http://altlab.vcu.edu/team-members/jeff-everhart/
 * License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * Description: This is a helper plugin for developing complex menus. On post save, we cache all of the data for pages in a JSON file to be used on the front end.
 *
*/

function create_menu($post_id) {
    global $wpdb;
    $results = $wpdb->get_results( "SELECT `ID`, `post_title`, `post_parent`, `post_name`, `guid` FROM {$wpdb->prefix}posts WHERE post_type='page' and post_status = 'publish' ", ARRAY_A );
    file_put_contents(plugin_dir_path(__FILE__) . 'results.json', json_encode($results));

}
add_action('save_post', 'create_menu');

Then something happened3 that required me to deal with a chunk of issues. I failed to do it in Vue enough times that I got mad and built it again in jQuery.4 You can see Jeff’s pretty code with consts and lets below.

function hasAnyGrandchildren (tree){
    let newTree = []
            let length = tree.length

            for (let i = 0; i < length; i++) {
                const node = tree[i]
                let hasGrandchildren = false
                if (node.children){
                  let children = this.hasAnyGrandchildren(node.children)
                  children.forEach(child => {
                        if (child.children && child.children.length > 0) {
                            hasGrandchildren = true
                        }
                    })
                }
                node.hasGrandchildren = hasGrandchildren
                newTree.push(node)

            }
            return newTree
}

function createTree () {
    fetch( histology_directory.data_directory+'/results.json' ) //histology_directory.data_directory+'/results.json'
            .then(result => {
                result.json().then(json => {

                    function parseTree(nodes, parentID){
                        let tree = []
                        let length = nodes.length
                        for (let i = 0; i < length; i++){
                            let node = nodes[i]
                            if(node.post_parent == parentID){
                                let children = parseTree(nodes, node.ID)

                                if (children.length > 0) {
                                    node.children = children
                                }
                                tree.push(node)
                            }
                        }

                        return tree
                    }

                    const completeTree = parseTree(json, "0")
                    const annotatedTree = this.hasAnyGrandchildren(completeTree)
                    this.tree = annotatedTree
                    //console.log(annotatedTree)
                    publishTree(annotatedTree)
                    return annotatedTree
                })
            })
        }    

And then I come in with this mess of stuff. You can see most of it explained in the comments. It added things like arrows for pages that would expand in the menu (rather than taking you to pages), it set the URL so you could link to expanded menu items via a URL, it removed the additional descriptions from overview pages, etc.


//DOING MOST OF THE CONSTRUCTION WORK via concat bc I am lazy
function publishTree(tree){
    var menu = ''
    tree.forEach(function(item){
    //console.log(item)   
      if ( item.hasGrandchildren === true) {
            menu = menu.concat('<li><h2>' + item.post_title) + '</h2>'
            menu = menu.concat('<div class="cell-main-index">')
              menu = menu.concat(makeLimb(item.children, 'childbearing top'))
        menu.concat('</li>')  
        menu = menu.concat('</div>')
        limbMenu = ''
        }
        
    })
     jQuery(menu).appendTo( "#app ul" );
     stunLinks()
     checkUrl()
     specialAddition()
}

var limbMenu = ''

//OOOOOH RECURSION for limb construction 
function makeLimb(data, type){
    limbMenu = limbMenu.concat('<ul>')
    data.forEach(function(item){
            if (item.hasGrandchildren === true){
                limbMenu = limbMenu.concat('<li><a id="menu_' + item.ID + '" class="' + type +'" href="' + item.guid + '">' + overviewClean(item.post_title) + ifParent(item.hasGrandchildren) + '</a>')
                makeLimb(item.children, "childbearing")
                limbMenu = limbMenu.concat('</li>')
            } if (item.children && !item.hasGrandchildren) {
                limbMenu = limbMenu.concat('<li><a class="live" href="' + item.children[0].guid + '">' + overviewClean(item.post_title) + '</a>')
                makeLimb(item.children, "live")
            } //this is super ugly but this appears to be the only item that violates the pattern
            if (item.post_title == "Overview of connective tissues"){
              //console.log(item.post_title + ' foo')
              limbMenu = limbMenu.concat('<li><a class="live" href="' + item.guid + '">' + overviewClean(item.post_title) + '</a>')             
            }
    })
    limbMenu = limbMenu.concat('</ul>') 
    return limbMenu
}


//add arrow to indicate menu item has children to display vs taking you to the page URL 
function ifParent(kids){
    if (kids === true){
        return '<i class="fa fa-arrow-right"></i>'
    } else {
        return ""
    }
}


createTree();


//THIS CAME UP BC PAGES WERE CALLED OVERVIEW OF BLAH BLAH BLAH and they wanted to remove the blah blah blah part
function overviewClean(title){
  var regex = /overview/i;
  var found = title.match(regex)
  if (found === null){
    return title
  } else {
    return title.substring(0, 8)
  }
}


//MAKE LINKS NOT BEHAVE LIKE LINKS instead add/remove classes
function stunLinks(){
    jQuery(".childbearing").click(function (e) {
      e.preventDefault(); 
      jQuery('.active').removeClass('active');
      jQuery(this).parent().children('ul').toggleClass('active');
      jQuery(this).parentsUntil('.cell-main-index').addClass('active');
      updateURL(jQuery(this)["0"].id)
    });
}


//GET THE URL PATTERN TO EXPOSE MENU LEVELS via parameters
function checkUrl(){
  var id = getQueryVariable("menu");
  if (id){
     jQuery('#'+id).parent().children('ul').addClass('active');
     jQuery('#'+id).parents().addClass('active');
  }
}


//from https://css-tricks.com/snippets/javascript/get-url-variables/
function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

//THIS WAS DONE BC *ONE* PAGE DIDN'T FIT THE PATTERN 
function specialAddition(){
  if (document.getElementById('menu_325')){
    var exocrine = document.getElementById('menu_325')
    var parent = exocrine.parentElement.parentElement

    var node = document.createElement('li');                 // Create a <li> node
    var a = document.createElement('a'); // Create a text node
    a.setAttribute('href', 'https://rampages.us/histology/?menu=menu_212');
    a.textContent = 'Endocrine ';
    node.appendChild(a);                              // Append the text to <li>
    parent.appendChild(node); 
    a.innerHTML = a.innerHTML + '<i class="fa fa-arrow-right"></i>'
  }

}

//make url change per menu change so it's easier to share links etc.
//from https://eureka.ykyuen.info/2015/04/08/javascript-add-query-parameter-to-current-url-without-reload/
function updateURL(id) {
      if (history.pushState) {
          var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?menu='+id;
          window.history.pushState({path:newurl},'',newurl);
      }
    }

Expanded menu for the histology page.
We now have something that’s pretty decent on desktops but I really need to rethink it fundamentally for mobile. One the histology faculty side there is a dislike that bottom tiering menu items like Ear->Inner Ear break the “frame” as they expand downward. In the previous application, I think they just hand assigned how things would layout. That’s relatively easy when you only have one window size and don’t allow people to alter things in a fluid manner. This kind of thing is something that can be dealt with but on my end I have to weigh the effort to do it across a variety of screen sizes vs the impact it’s likely to have on the average user on the site. Right now I can’t justify putting in the extra time.

Recently there was the desire to add multiple background images for the home page. I added an ACF repeater field for images and used a PHP function to randomize between the elements added there.

function randomHomeBackground(){
    $rows = get_field('background' ); // get all the rows
    $rand_row = $rows[ array_rand( $rows ) ]; // get a random row
    $rand_row_image = $rand_row['background_image' ]; // get the sub field value 

    return $rand_row_image;
}

That gets used in the template like so.

<div id="content" class="clearfix row" style="background-image: url(<?php echo randomHomeBackground() ;?>)">

The Cell Pages

The old application cell layout.
You can see the previous layout above. We have an annotation layer on the right which adds overlays to the existing image and changes the text displayed under the cell. We also have the ability navigate through additional cell images which change the annotation layers but still relate to the main topic.

So each page that is associated with content has a template that’s tied into ACF. It has a repeater field that lets the author associate as many title, description, and image pairings as they’d like and uses them to build the navigation on the right side.
Histology cell authoring page on the editor side.
The navigation on the bottom is built by querying other pages with the same parents. You can see a slider element in the old version and there’s been discussion about including a similar slider. I don’t believe it would work well in this scenario for a variety of reasons so I’ve been resistant. In this case we are loading a new page so scrolling would likely be slow and given the wide variation between the number of pages in these structures the layout would be awkward or intensive to develop. I also don’t see scrolling as a common way for navigating this type of web element. It’s not an interaction pattern I see elsewhere and the names don’t give you enough information for informed scrolling. I did tie the arrows to keyboard navigation via some javascript.


//KEY BINDING for nav
function leftArrowPressed() {
   var url = document.getElementById('nav-arrow-left').parentElement.href;
   window.location.href = url;
}

function rightArrowPressed() {
   var url = document.getElementById('nav-arrow-right').parentElement.href;
   window.location.href = url;

}

document.onkeydown = function(evt) {
    evt = evt || window.event;
    switch (evt.keyCode) {
        case 37:
            leftArrowPressed();
            break;
        case 39:
            rightArrowPressed();
            break;
    }
};

There’s a bunch of PHP and javascript going on to make all this happen but I wrote most of it around 2 years ago and I don’t want to inflict it on anyone. The nice thing is I’ve learned a lot in two years. The bad thing is considering rewriting the whole thing.5

You might also notice a button labeled ‘hide’ in towards the upper right, it replaces the right hand navigation names with ‘* * *’ and blanks out the text so students can quiz themselves. There’s some other possibilities there that might get more complex but that exists after the latest round of conversations.

//HIDE AND SEEK FOR QUIZ YOURSELF STUFF
function hideSlideTitles(){
    var mainSlide = document.getElementById('slide-button-0'); 
    if (mainSlide){
      var buttons = document.getElementsByClassName('button');
      var subslides = document.getElementsByClassName('sub-deep');
      for (var i = 0; i < buttons.length; i++){
        var original = buttons[i].innerHTML;
        buttons[i].innerHTML = '<span class="hidden">' + original + '</span>* * *';        
        }
      for (var i = 0; i < subslides.length; i++){
            subslides[i].classList.add('nope')
        }
        document.getElementById('the_slide_title').classList.add('nope')
        document.getElementById('the_slide_content').classList.add('nope')
        document.getElementById('quizzer').dataset.quizstate = 'hidden'
        document.getElementById('quizzer').innerHTML = 'Show'
    }
}


function showSlideTitles(){
  var mainSlide = document.getElementById('slide-button-0'); 
    if (mainSlide){
      var buttons = document.getElementsByClassName('button');

      for (var i =0; i < buttons.length; i++){
        var hidden = buttons[i].firstChild.innerHTML;
          buttons[i].innerHTML = hidden;       
        }
        document.getElementById('the_slide_title').classList.remove('nope')
        document.getElementById('the_slide_content').classList.remove('nope')
        document.getElementById('quizzer').dataset.quizstate = 'visible'
        document.getElementById('quizzer').innerHTML = 'Hide'
        var subslides = document.getElementsByClassName('sub-deep');
        for (var i = 0; i < subslides.length; i++){
            subslides[i].classList.remove('nope')
        }
    }
}


function setQuizState(){
  var state = document.getElementById('quizzer').dataset.quizstate
  if (state === 'hidden'){
    showSlideTitles()
  } else {
    hideSlideTitles()
  }
}

function retainQuizState(){
  var state = document.getElementById('quizzer').dataset.quizstate
  if (state === 'hidden'){
    hideSlideTitles()
  } else if (state === 'visible'){
    showSlideTitles()
  }
}


jQuery( document ).ready(function() {
  document.getElementById('quizzer').addEventListener("click", setQuizState);
});

Quizzes

The site also has a set of quizzes built in H5P. They’re on this page based on having the common page parent Quiz. We had to set up some custom CSS to make the images go to full size by default and then add it via some PHP so it’d work the way that we desired.

.h5p-column-content.h5p-image > img, .h5p-question-image-scalable  {
  width: 100% !important;
  height: auto !important;
  max-width: 100%  !important;
}

.h5p-question-scorebar-container {
	display: none !important;
}
function h5p_full_img_alter_styles(&$styles, $libraries, $embed_type) {
  $styles[] = (object) array(
    // Path must be relative to wp-content/uploads/h5p or absolute.
    'path' => get_stylesheet_directory_uri() . '/custom-h5p.css',
    'version' => '?ver=0.1' // Cache buster
  );
}
add_action('h5p_alter_library_styles', 'h5p_full_img_alter_styles', 10, 3);

1 Watch the pages scroll by . . .

2 I’m not sure if that’s because of the way the project got started or a result of choices I made.

3 I can’t recall what. Thus the need to write these blog posts more often.

4 I am not the cutting edge. I am not the edge. I am not the cut. I am bailing wire, duct tape, and stubbornness.

5 Refactoring if you’re nasty.

Digital Histology & the Long Haul

Digital Histology homescreen screenshot.

Normally we finish our projects in anywhere from a few hours to a few weeks. Digital Histology has been the exception to that rule. I can see a reference to the site going back to Nov. of 2016! That doesn’t mean we’ve worked on this site continuously for years. The gaps have been frequent and long. OER grants have been written and won. Presentations have been made. Work has ebbed and flowed as the massive amount of content has been entered. There are more than 1500 pages1 and over 5GBs of images. It’s a large site. A ton of work has gone into its construction, new goals have developed, and just about all of it is a little strange. 2 I figured I’d better document some of this before I forgot all of it.

Made with Macromedia logo.

The History

I don’t recall all the details but essentially long ago in a Macromedia Authorware galaxy far, far away a digital histology program was constructed. Time passed. Acorns grew into trees. WINE was now required to launch the digital histology program. The screen was a tiny 752×613. It only ran on desktops. Updating it was nearly impossible. Things were not good. After much wandering we found one another and endeavored to put this work online for the betterment of humankind.

Having a previous project did some good things for us — the content was mostly created and there was experience working with digital projects. The previous construction patterns in Macromedia were very different from the way building on the web works today. We did quite a bit of work to parallel the previous interactions. I don’t know if that’s how I would have done it had we started from scratch. This was also the first time I’d built anything substantial with ACF.

The stacked tiering of the histology menu.

The Menu/Main Page

The menu has gone through a few iterations as we came up with different ways to deal with just how many pages were involved and how to deal with a really odd linking pattern. I’ll try to draw the menu pattern below. We had to figure out which pages had no grandchildren and for each one of those we would keep the title but link directly to the first child. Pages with no children would not be shown at all. Not super weird I guess but not normal.

Histology menu pattern - parent to child to terminal child

To deal with the scale, Jeff wrote a slick little plugin to dump the pages data into JSON. That saves us a lot of time especially given the way that WordPress recursively builds menus. Jeff also had the layout generated in Vue.

/* Plugin Name: Menu Cache Plugin
 * Version: 1.0.2
 * Author: Jeff Everhart
 * Author URI: http://altlab.vcu.edu/team-members/jeff-everhart/
 * License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * Description: This is a helper plugin for developing complex menus. On post save, we cache all of the data for pages in a JSON file to be used on the front end.
 *
*/

function create_menu($post_id) {
    global $wpdb;
    $results = $wpdb->get_results( "SELECT `ID`, `post_title`, `post_parent`, `post_name`, `guid` FROM {$wpdb->prefix}posts WHERE post_type='page' and post_status = 'publish' ", ARRAY_A );
    file_put_contents(plugin_dir_path(__FILE__) . 'results.json', json_encode($results));

}
add_action('save_post', 'create_menu');

Then something happened3 that required me to deal with a chunk of issues. I failed to do it in Vue enough times that I got mad and built it again in jQuery.4 You can see Jeff’s pretty code with consts and lets below.

function hasAnyGrandchildren (tree){
    let newTree = []
            let length = tree.length

            for (let i = 0; i < length; i++) {
                const node = tree[i]
                let hasGrandchildren = false
                if (node.children){
                  let children = this.hasAnyGrandchildren(node.children)
                  children.forEach(child => {
                        if (child.children && child.children.length > 0) {
                            hasGrandchildren = true
                        }
                    })
                }
                node.hasGrandchildren = hasGrandchildren
                newTree.push(node)

            }
            return newTree
}

function createTree () {
    fetch( histology_directory.data_directory+'/results.json' ) //histology_directory.data_directory+'/results.json'
            .then(result => {
                result.json().then(json => {

                    function parseTree(nodes, parentID){
                        let tree = []
                        let length = nodes.length
                        for (let i = 0; i < length; i++){
                            let node = nodes[i]
                            if(node.post_parent == parentID){
                                let children = parseTree(nodes, node.ID)

                                if (children.length > 0) {
                                    node.children = children
                                }
                                tree.push(node)
                            }
                        }

                        return tree
                    }

                    const completeTree = parseTree(json, "0")
                    const annotatedTree = this.hasAnyGrandchildren(completeTree)
                    this.tree = annotatedTree
                    //console.log(annotatedTree)
                    publishTree(annotatedTree)
                    return annotatedTree
                })
            })
        }    

And then I come in with this mess of stuff. You can see most of it explained in the comments. It added things like arrows for pages that would expand in the menu (rather than taking you to pages), it set the URL so you could link to expanded menu items via a URL, it removed the additional descriptions from overview pages, etc.


//DOING MOST OF THE CONSTRUCTION WORK via concat bc I am lazy
function publishTree(tree){
    var menu = ''
    tree.forEach(function(item){
    //console.log(item)   
      if ( item.hasGrandchildren === true) {
            menu = menu.concat('<li><h2>' + item.post_title) + '</h2>'
            menu = menu.concat('<div class="cell-main-index">')
              menu = menu.concat(makeLimb(item.children, 'childbearing top'))
        menu.concat('</li>')  
        menu = menu.concat('</div>')
        limbMenu = ''
        }
        
    })
     jQuery(menu).appendTo( "#app ul" );
     stunLinks()
     checkUrl()
     specialAddition()
}

var limbMenu = ''

//OOOOOH RECURSION for limb construction 
function makeLimb(data, type){
    limbMenu = limbMenu.concat('<ul>')
    data.forEach(function(item){
            if (item.hasGrandchildren === true){
                limbMenu = limbMenu.concat('<li><a id="menu_' + item.ID + '" class="' + type +'" href="' + item.guid + '">' + overviewClean(item.post_title) + ifParent(item.hasGrandchildren) + '</a>')
                makeLimb(item.children, "childbearing")
                limbMenu = limbMenu.concat('</li>')
            } if (item.children && !item.hasGrandchildren) {
                limbMenu = limbMenu.concat('<li><a class="live" href="' + item.children[0].guid + '">' + overviewClean(item.post_title) + '</a>')
                makeLimb(item.children, "live")
            } //this is super ugly but this appears to be the only item that violates the pattern
            if (item.post_title == "Overview of connective tissues"){
              //console.log(item.post_title + ' foo')
              limbMenu = limbMenu.concat('<li><a class="live" href="' + item.guid + '">' + overviewClean(item.post_title) + '</a>')             
            }
    })
    limbMenu = limbMenu.concat('</ul>') 
    return limbMenu
}


//add arrow to indicate menu item has children to display vs taking you to the page URL 
function ifParent(kids){
    if (kids === true){
        return '<i class="fa fa-arrow-right"></i>'
    } else {
        return ""
    }
}


createTree();


//THIS CAME UP BC PAGES WERE CALLED OVERVIEW OF BLAH BLAH BLAH and they wanted to remove the blah blah blah part
function overviewClean(title){
  var regex = /overview/i;
  var found = title.match(regex)
  if (found === null){
    return title
  } else {
    return title.substring(0, 8)
  }
}


//MAKE LINKS NOT BEHAVE LIKE LINKS instead add/remove classes
function stunLinks(){
    jQuery(".childbearing").click(function (e) {
      e.preventDefault(); 
      jQuery('.active').removeClass('active');
      jQuery(this).parent().children('ul').toggleClass('active');
      jQuery(this).parentsUntil('.cell-main-index').addClass('active');
      updateURL(jQuery(this)["0"].id)
    });
}


//GET THE URL PATTERN TO EXPOSE MENU LEVELS via parameters
function checkUrl(){
  var id = getQueryVariable("menu");
  if (id){
     jQuery('#'+id).parent().children('ul').addClass('active');
     jQuery('#'+id).parents().addClass('active');
  }
}


//from https://css-tricks.com/snippets/javascript/get-url-variables/
function getQueryVariable(variable)
{
       var query = window.location.search.substring(1);
       var vars = query.split("&");
       for (var i=0;i<vars.length;i++) {
               var pair = vars[i].split("=");
               if(pair[0] == variable){return pair[1];}
       }
       return(false);
}

//THIS WAS DONE BC *ONE* PAGE DIDN'T FIT THE PATTERN 
function specialAddition(){
  if (document.getElementById('menu_325')){
    var exocrine = document.getElementById('menu_325')
    var parent = exocrine.parentElement.parentElement

    var node = document.createElement('li');                 // Create a <li> node
    var a = document.createElement('a'); // Create a text node
    a.setAttribute('href', 'https://rampages.us/histology/?menu=menu_212');
    a.textContent = 'Endocrine ';
    node.appendChild(a);                              // Append the text to <li>
    parent.appendChild(node); 
    a.innerHTML = a.innerHTML + '<i class="fa fa-arrow-right"></i>'
  }

}

//make url change per menu change so it's easier to share links etc.
//from https://eureka.ykyuen.info/2015/04/08/javascript-add-query-parameter-to-current-url-without-reload/
function updateURL(id) {
      if (history.pushState) {
          var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?menu='+id;
          window.history.pushState({path:newurl},'',newurl);
      }
    }

Expanded menu for the histology page.
We now have something that’s pretty decent on desktops but I really need to rethink it fundamentally for mobile. One the histology faculty side there is a dislike that bottom tiering menu items like Ear->Inner Ear break the “frame” as they expand downward. In the previous application, I think they just hand assigned how things would layout. That’s relatively easy when you only have one window size and don’t allow people to alter things in a fluid manner. This kind of thing is something that can be dealt with but on my end I have to weigh the effort to do it across a variety of screen sizes vs the impact it’s likely to have on the average user on the site. Right now I can’t justify putting in the extra time.

Recently there was the desire to add multiple background images for the home page. I added an ACF repeater field for images and used a PHP function to randomize between the elements added there.

function randomHomeBackground(){
    $rows = get_field('background' ); // get all the rows
    $rand_row = $rows[ array_rand( $rows ) ]; // get a random row
    $rand_row_image = $rand_row['background_image' ]; // get the sub field value 

    return $rand_row_image;
}

That gets used in the template like so.

<div id="content" class="clearfix row" style="background-image: url(<?php echo randomHomeBackground() ;?>)">

The Cell Pages

The old application cell layout.
You can see the previous layout above. We have an annotation layer on the right which adds overlays to the existing image and changes the text displayed under the cell. We also have the ability navigate through additional cell images which change the annotation layers but still relate to the main topic.

So each page that is associated with content has a template that’s tied into ACF. It has a repeater field that lets the author associate as many title, description, and image pairings as they’d like and uses them to build the navigation on the right side.
Histology cell authoring page on the editor side.
The navigation on the bottom is built by querying other pages with the same parents. You can see a slider element in the old version and there’s been discussion about including a similar slider. I don’t believe it would work well in this scenario for a variety of reasons so I’ve been resistant. In this case we are loading a new page so scrolling would likely be slow and given the wide variation between the number of pages in these structures the layout would be awkward or intensive to develop. I also don’t see scrolling as a common way for navigating this type of web element. It’s not an interaction pattern I see elsewhere and the names don’t give you enough information for informed scrolling. I did tie the arrows to keyboard navigation via some javascript.


//KEY BINDING for nav
function leftArrowPressed() {
   var url = document.getElementById('nav-arrow-left').parentElement.href;
   window.location.href = url;
}

function rightArrowPressed() {
   var url = document.getElementById('nav-arrow-right').parentElement.href;
   window.location.href = url;

}

document.onkeydown = function(evt) {
    evt = evt || window.event;
    switch (evt.keyCode) {
        case 37:
            leftArrowPressed();
            break;
        case 39:
            rightArrowPressed();
            break;
    }
};

There’s a bunch of PHP and javascript going on to make all this happen but I wrote most of it around 2 years ago and I don’t want to inflict it on anyone. The nice thing is I’ve learned a lot in two years. The bad thing is considering rewriting the whole thing.5

You might also notice a button labeled ‘hide’ in towards the upper right, it replaces the right hand navigation names with ‘* * *’ and blanks out the text so students can quiz themselves. There’s some other possibilities there that might get more complex but that exists after the latest round of conversations.

//HIDE AND SEEK FOR QUIZ YOURSELF STUFF
function hideSlideTitles(){
    var mainSlide = document.getElementById('slide-button-0'); 
    if (mainSlide){
      var buttons = document.getElementsByClassName('button');
      var subslides = document.getElementsByClassName('sub-deep');
      for (var i = 0; i < buttons.length; i++){
        var original = buttons[i].innerHTML;
        buttons[i].innerHTML = '<span class="hidden">' + original + '</span>* * *';        
        }
      for (var i = 0; i < subslides.length; i++){
            subslides[i].classList.add('nope')
        }
        document.getElementById('the_slide_title').classList.add('nope')
        document.getElementById('the_slide_content').classList.add('nope')
        document.getElementById('quizzer').dataset.quizstate = 'hidden'
        document.getElementById('quizzer').innerHTML = 'Show'
    }
}


function showSlideTitles(){
  var mainSlide = document.getElementById('slide-button-0'); 
    if (mainSlide){
      var buttons = document.getElementsByClassName('button');

      for (var i =0; i < buttons.length; i++){
        var hidden = buttons[i].firstChild.innerHTML;
          buttons[i].innerHTML = hidden;       
        }
        document.getElementById('the_slide_title').classList.remove('nope')
        document.getElementById('the_slide_content').classList.remove('nope')
        document.getElementById('quizzer').dataset.quizstate = 'visible'
        document.getElementById('quizzer').innerHTML = 'Hide'
        var subslides = document.getElementsByClassName('sub-deep');
        for (var i = 0; i < subslides.length; i++){
            subslides[i].classList.remove('nope')
        }
    }
}


function setQuizState(){
  var state = document.getElementById('quizzer').dataset.quizstate
  if (state === 'hidden'){
    showSlideTitles()
  } else {
    hideSlideTitles()
  }
}

function retainQuizState(){
  var state = document.getElementById('quizzer').dataset.quizstate
  if (state === 'hidden'){
    hideSlideTitles()
  } else if (state === 'visible'){
    showSlideTitles()
  }
}


jQuery( document ).ready(function() {
  document.getElementById('quizzer').addEventListener("click", setQuizState);
});

Quizzes

The site also has a set of quizzes built in H5P. They’re on this page based on having the common page parent Quiz. We had to set up some custom CSS to make the images go to full size by default and then add it via some PHP so it’d work the way that we desired.

.h5p-column-content.h5p-image > img, .h5p-question-image-scalable  {
  width: 100% !important;
  height: auto !important;
  max-width: 100%  !important;
}

.h5p-question-scorebar-container {
	display: none !important;
}
function h5p_full_img_alter_styles(&$styles, $libraries, $embed_type) {
  $styles[] = (object) array(
    // Path must be relative to wp-content/uploads/h5p or absolute.
    'path' => get_stylesheet_directory_uri() . '/custom-h5p.css',
    'version' => '?ver=0.1' // Cache buster
  );
}
add_action('h5p_alter_library_styles', 'h5p_full_img_alter_styles', 10, 3);

1 Watch the pages scroll by . . .

2 I’m not sure if that’s because of the way the project got started or a result of choices I made.

3 I can’t recall what. Thus the need to write these blog posts more often.

4 I am not the cutting edge. I am not the edge. I am not the cut. I am bailing wire, duct tape, and stubbornness.

5 Refactoring if you’re nasty.