Skip to content

Commit

Permalink
Truncate long bodies and quotes
Browse files Browse the repository at this point in the history
Introduces auto-truncation of long bodies and quotes which expand when
you click on a "More" link.

Feature flagged as 'truncate_annotations'.
  • Loading branch information
JakeHartnell authored and nickstenning committed Oct 9, 2015
1 parent 81ca666 commit 4c8f9bf
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 18 deletions.
1 change: 1 addition & 0 deletions h/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'streamer': "Enable 'live streaming' for annotations via the websocket?",
'search_normalized': "Assume all data has normalized URI fields?",
'show_unanchored_annotations': "Show annotations that fail to anchor?",
'truncate_annotations': "Truncate long quotes and bodies in annotations?",
}


Expand Down
1 change: 1 addition & 0 deletions h/static/scripts/app.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module.exports = angular.module('h', [

.directive('annotation', require('./directive/annotation'))
.directive('deepCount', require('./directive/deep-count'))
.directive('excerpt', require('./directive/excerpt').directive)
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list').directive)
Expand Down
7 changes: 5 additions & 2 deletions h/static/scripts/directive/annotation.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,8 @@ AnnotationController = [
#
###
module.exports = [
'$document',
($document) ->
'$document', 'features'
($document, features) ->
linkFn = (scope, elem, attrs, [ctrl, thread, threadFilter, counter]) ->
# Observe the isSidebar attribute
attrs.$observe 'isSidebar', (value) ->
Expand All @@ -419,6 +419,9 @@ module.exports = [
scope.$evalAsync ->
ctrl.save()

# Give template access to feature flags
scope.feature = features.flagEnabled

scope.share = (event) ->
$container = angular.element(event.currentTarget).parent()
$container.addClass('open').find('input').focus().select()
Expand Down
95 changes: 95 additions & 0 deletions h/static/scripts/directive/excerpt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';

function ExcerptController() {
var collapsed = true;

// Enabled is a test seam: overwritten in link function.
this.enabled = function () { return true; };

// Overflowing is a test seam: overwritten in link function.
this.overflowing = function () { return false; };

// Is the excerpt collapsed? True if no-one has toggled the excerpt open
// and the element is overflowing.
this.collapsed = function () {
if (!collapsed) {
return false;
}
return this.overflowing();
};

this.uncollapsed = function () {
return !collapsed;
};

this.toggle = function () {
collapsed = !collapsed;
};

return this;
}

/**
* @ngdoc directive
* @name excerpt
* @restrict E
* @description This directive truncates its contents to a height specified in
* CSS, and provides controls for expanding and collapsing the
* resulting truncated element. For example, with the following
* template HTML:
*
* <article class="post">
* <excerpt>
* <div class="body" ng-model="post.body"></div>
* </excerpt>
* </article>
*
* You would need to define the allowable height of the excerpt in
* CSS:
*
* article.post .excerpt {
* max-height: 10em;
* }
*
* And the excerpt directive will take care of the rest.
*
* You can selectively disable truncation by providing a boolean
* expression to the `enabled` parameter, e.g.:
*
* <excerpt enabled="!post.inFull">...</excerpt>
*/
function excerpt() {
return {
controller: ExcerptController,
controllerAs: 'vm',
link: function (scope, elem, attrs, ctrl) {
// Test if the transcluded element is overflowing its container. We use
// clientHeight rather than offsetHeight because we assume you'll be using
// this with "overflow: hidden;" (i.e. no scrollbars) and it's usually
// much faster to calculate than offsetHeight (which includes scrollbars).
ctrl.overflowing = function overflowing() {
var excerpt = elem[0].querySelector('.excerpt');
if (!excerpt) {
return false;
}
return (excerpt.scrollHeight > excerpt.clientHeight);
};

// If the `enabled` attr was provided, we override the enabled function.
if (attrs.enabled) {
ctrl.enabled = scope.enabled;
}
},
scope: {
enabled: '&?',
},
restrict: 'E',
transclude: true,
templateUrl: 'excerpt.html',
};
}

module.exports = {
directive: excerpt,
Controller: ExcerptController,
};
11 changes: 10 additions & 1 deletion h/static/scripts/directive/test/annotation-test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe 'annotation', ->
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeDrafts = null
fakeFeatures = null
fakeFlash = null
fakeGroups = null
fakeMomentFilter = null
Expand Down Expand Up @@ -56,6 +57,9 @@ describe 'annotation', ->
add: sandbox.stub()
remove: sandbox.stub()
}
fakeFeatures = {
flagEnabled: sandbox.stub().returns(true)
}
fakeFlash = sandbox.stub()

fakeMomentFilter = sandbox.stub().returns('ages ago')
Expand Down Expand Up @@ -92,6 +96,7 @@ describe 'annotation', ->
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'drafts', fakeDrafts
$provide.value 'features', fakeFeatures
$provide.value 'flash', fakeFlash
$provide.value 'momentFilter', fakeMomentFilter
$provide.value 'permissions', fakePermissions
Expand Down Expand Up @@ -610,7 +615,7 @@ describe("AnnotationController", ->
Return an annotation directive instance and stub services etc.
###
createAnnotationDirective = ({annotation, personaFilter, momentFilter,
urlencodeFilter, drafts, flash,
urlencodeFilter, drafts, features, flash,
permissions, session, tags, time, annotationUI,
annotationMapper, groups,
documentTitleFilter, documentDomainFilter}) ->
Expand All @@ -622,6 +627,9 @@ describe("AnnotationController", ->
add: ->
remove: ->
}
features: features or {
flagEnabled: -> true
}
flash: flash or {
info: ->
error: ->
Expand Down Expand Up @@ -651,6 +659,7 @@ describe("AnnotationController", ->
$provide.value("momentFilter", locals.momentFilter)
$provide.value("urlencodeFilter", locals.urlencodeFilter)
$provide.value("drafts", locals.drafts)
$provide.value("features", locals.features)
$provide.value("flash", locals.flash)
$provide.value("permissions", locals.permissions)
$provide.value("session", locals.session)
Expand Down
86 changes: 86 additions & 0 deletions h/static/scripts/directive/test/excerpt-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

var util = require('./util');
var excerpt = require('../excerpt');


describe('excerpt.Controller', function () {
var ctrl;

beforeEach(function() {
ctrl = new excerpt.Controller();
ctrl.overflowing = function () { return false; };
});

it('starts collapsed if the element is overflowing', function () {
ctrl.overflowing = function () { return true; };

assert.isTrue(ctrl.collapsed());
});

it('does not start collapsed if the element is not overflowing', function () {
assert.isFalse(ctrl.collapsed());
});

it('is not initially uncollapsed if the element is overflowing', function () {
assert.isFalse(ctrl.uncollapsed());
});

it('is not initially uncollapsed if the element is not overflowing', function () {
assert.isFalse(ctrl.uncollapsed());
});

describe('.toggle()', function () {
beforeEach(function () {
ctrl.overflowing = function () { return true; };
});

it('toggles the collapsed state', function () {
var a = ctrl.collapsed();
ctrl.toggle();
var b = ctrl.collapsed();
ctrl.toggle();
var c = ctrl.collapsed();

assert.notEqual(a, b);
assert.notEqual(b, c);
assert.equal(a, c);
});
});
});


describe('excerpt.excerpt', function () {
function excerptDirective(attrs, content) {
return util.createDirective(document, 'excerpt', attrs, {}, content);
}

before(function () {
angular.module('app', [])
.directive('excerpt', excerpt.directive);
});

beforeEach(function () {
angular.mock.module('app');
angular.mock.module('h.templates');
});

it('renders its contents in a .excerpt element by default', function () {
var element = excerptDirective({}, '<span id="foo"></span>');

assert.equal(element.find('.excerpt #foo').length, 1);
});

it('when enabled, renders its contents in a .excerpt element', function () {
var element = excerptDirective({enabled: true}, '<span id="foo"></span>');

assert.equal(element.find('.excerpt #foo').length, 1);
});

it('when disabled, renders its contents but not in a .excerpt element', function () {
var element = excerptDirective({enabled: false}, '<span id="foo"></span>');

assert.equal(element.find('.excerpt #foo').length, 0);
assert.equal(element.find('#foo').length, 1);
});
});
18 changes: 12 additions & 6 deletions h/static/scripts/directive/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,31 @@ function hyphenate(name) {
* attrA: 'initial-value'
* }, {
* scopePropery: scopeValue
* });
* },
* 'Hello, world!');
*
* Will generate '<my-component attr-a="attrA"></my-component>' and
* Will generate '<my-component attr-a="attrA">Hello, world!</my-component>' and
* compile and link it with the scope:
*
* { attrA: 'initial-value', scopeProperty: scopeValue }
*
* @param {Document} document - The DOM Document to create the element in
* @param {string} name - The name of the directive to instantiate
* @param {Object} attrs - A map of attribute names (in camelCase) to initial values.
* @param {Object} initialScope - A dictionary of properties to set on the
* scope when the element is linked
* @param {Object} [attrs] - A map of attribute names (in camelCase) to initial
* values.
* @param {Object} [initialScope] - A dictionary of properties to set on the
* scope when the element is linked
* @param {string} [initialHtml] - Initial inner HTML content for the directive
* element.
*
* @return {DOMElement} The Angular jqLite-wrapped DOM element for the component.
* The returned object has a link(scope) method which will
* re-link the component with new properties.
*/
function createDirective(document, name, attrs, initialScope) {
function createDirective(document, name, attrs, initialScope, initialHtml) {
attrs = attrs || {};
initialScope = initialScope || {};
initialHtml = initialHtml || '';

// create a template consisting of a single element, the directive
// we want to create and compile it
Expand All @@ -53,6 +58,7 @@ function createDirective(document, name, attrs, initialScope) {
}
templateElement.setAttribute(attrName, attrKey);
});
templateElement.innerHTML = initialHtml;

// setup initial scope
Object.keys(attrs).forEach(function (key) {
Expand Down
21 changes: 21 additions & 0 deletions h/static/styles/annotations.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@
.annotation-header { margin-top: 0 }
.annotation-footer { margin-bottom: 0 }

.annotation-section {
.excerpt { max-height: 4.8em; }
.excerpt-control a {
font-style: italic;
font-family: $serif-font-family;
font-weight: normal;
}
.excerpt--collapsed:after {
height: $base-line-height;
@include background(linear-gradient(
to right,
$mask-start-color,
$mask-end-color
));
}
}

.annotation-body {
.excerpt { max-height: 16.2em; }
}

.annotation-user {
color: $text-color;
font-weight: bold;
Expand Down
1 change: 1 addition & 0 deletions h/static/styles/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import 'mixins/responsive';
@import 'grid';
@import 'annotations';
@import 'excerpt';
@import 'forms';
@import 'markdown-editor';
@import 'spinner';
Expand Down
31 changes: 31 additions & 0 deletions h/static/styles/excerpt.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@import "compass/css3/images";

.excerpt {
position: relative;
overflow: hidden;
}

.excerpt--collapsed:after {
position: absolute;
bottom: 0;
height: $base-line-height * 2; // This controls the apparent height of the gradient.
width: 100%;
content: "";
pointer-events: none;
@include background(linear-gradient(
to bottom,
$mask-start-color,
$mask-end-color
));
}

.excerpt--uncollapsed {
max-height: 100% !important;
}

.excerpt-control a {
display: block;
text-align: right;
font-weight: bold;
width: 100%;
}
3 changes: 3 additions & 0 deletions h/static/styles/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ $button-background-gradient: top, $button-background-start, $button-background-e
$error-color: #f0480c !default;
$success-color: #1cbd41 !default;

$mask-start-color: rgba($white, 0) !default;
$mask-end-color: $white !default;

@function color-weight($c, $n: 500) {
@if $n == 50 {
@return tint($c, 85%);
Expand Down
Loading

0 comments on commit 4c8f9bf

Please sign in to comment.