From 3031d582788b31197a3d0e10de5f0fb479f72031 Mon Sep 17 00:00:00 2001 From: Mike Macgirvin Date: Sun, 23 Jul 2023 09:01:55 +1000 Subject: [PATCH] update readmore.js, fix disabling it, and use \Code\Nomad\Profile wherever appropriate --- Code/Lib/Activity.php | 4 +- Code/Lib/Libzot.php | 3 +- Code/Lib/Libzotdir.php | 10 +- Code/Module/Channel.php | 3 - Code/Module/Pubstream.php | 5 +- Code/Module/Stream.php | 3 - include/items.php | 9 +- library/readmorejs/CHANGELOG.md | 36 ++ library/readmorejs/LICENSE | 22 + library/readmorejs/README.md | 189 ++++++++ library/readmorejs/ajax-demo.html | 128 +++++ library/readmorejs/bower.json | 29 ++ library/readmorejs/demo.html | 243 ++++++++++ library/readmorejs/gulpfile.js | 14 + library/readmorejs/jquery.mockjax.js | 692 +++++++++++++++++++++++++++ library/readmorejs/package.json | 35 ++ library/readmorejs/readmore.js | 342 +++++++++++++ library/readmorejs/readmore.min.js | 11 + view/php/theme_init.php | 2 +- 19 files changed, 1760 insertions(+), 20 deletions(-) create mode 100644 library/readmorejs/CHANGELOG.md create mode 100644 library/readmorejs/LICENSE create mode 100644 library/readmorejs/README.md create mode 100644 library/readmorejs/ajax-demo.html create mode 100644 library/readmorejs/bower.json create mode 100644 library/readmorejs/demo.html create mode 100644 library/readmorejs/gulpfile.js create mode 100644 library/readmorejs/jquery.mockjax.js create mode 100644 library/readmorejs/package.json create mode 100644 library/readmorejs/readmore.js create mode 100644 library/readmorejs/readmore.min.js diff --git a/Code/Lib/Activity.php b/Code/Lib/Activity.php index 64fb291c9..4702848b9 100644 --- a/Code/Lib/Activity.php +++ b/Code/Lib/Activity.php @@ -7,6 +7,7 @@ use Code\Access\PermissionLimits; use Code\ActivityStreams\Actor; use Code\ActivityStreams\ASObject; use Code\ActivityStreams\Link; +use Code\Nomad\Profile; use Code\Web\HTTPSig; use Code\Access\Permissions; use Code\Access\PermissionRoles; @@ -2575,7 +2576,8 @@ class Activity } } - Libzotdir::import_directory_profile($url, ['about' => $about, 'keywords' => $keywords, 'dob' => '0000-00-00'], null, 0, true); + $nomadProfile = new Profile(['about' => $about, 'keywords' => $keywords, 'dob' => '0000-00-00']); + Libzotdir::import_directory_profile($url, $nomadProfile, null, 0, true); if ($collections) { set_xconfig($url, 'activitypub', 'collections', $collections); diff --git a/Code/Lib/Libzot.php b/Code/Lib/Libzot.php index 35a972473..72d9629e8 100644 --- a/Code/Lib/Libzot.php +++ b/Code/Lib/Libzot.php @@ -1012,7 +1012,8 @@ class Libzot if (array_key_exists('profile', $arr) && is_array($arr['profile'])) { - $profile_changed = Libzotdir::import_directory_profile($xchan_hash, $arr['profile'], $address, $ud_flags, 1); + $nomadProfile = new Profile($arr['profile']); + $profile_changed = Libzotdir::import_directory_profile($xchan_hash, $nomadProfile, $address, $ud_flags, 1); if ($profile_changed) { $what .= 'profile '; $changed = true; diff --git a/Code/Lib/Libzotdir.php b/Code/Lib/Libzotdir.php index e99c6025f..5ea90132a 100644 --- a/Code/Lib/Libzotdir.php +++ b/Code/Lib/Libzotdir.php @@ -215,11 +215,11 @@ class Libzotdir ); } - $arr = ['channel_id' => $uid, 'hash' => $hash, 'profile' => $profile->toArray()]; + // Caveat: for hook listeners, $profile is an instance of Code\Nomad\Profile + $arr = ['channel_id' => $uid, 'hash' => $hash, 'profile' => $profile]; Hook::call('local_dir_update', $arr); $address = Channel::get_webfinger($value); - if (perm_is_allowed($uid, '', 'view_profile')) { self::import_directory_profile($hash, $arr['profile'], $address, 0); } else { @@ -244,21 +244,21 @@ class Libzotdir * @brief Imports a directory profile. * * @param string $hash - * @param array $profile + * @param Profile $nomadProfile * @param string $addr * @param number $ud_flags (optional) UPDATE_FLAGS_UPDATED * @param number $suppress_update (optional) default 0 * @return bool $updated if something changed */ - public static function import_directory_profile($hash, $profile, $addr, $ud_flags = UPDATE_FLAGS_UPDATED, $suppress_update = 0) + public static function import_directory_profile($hash, $nomadProfile, $addr, $ud_flags = UPDATE_FLAGS_UPDATED, $suppress_update = 0) { logger('import_directory_profile', LOGGER_DEBUG); if (!$hash) { return false; } - + $profile = $nomadProfile->toArray(); $maxlen = get_max_import_size(); diff --git a/Code/Module/Channel.php b/Code/Module/Channel.php index acbb1e46d..9f9183ee6 100644 --- a/Code/Module/Channel.php +++ b/Code/Module/Channel.php @@ -464,9 +464,6 @@ class Channel extends Controller // because browser prefetching might change it on us. We have to deliver it with the page. $maxheight = get_pconfig(App::$profile['profile_uid'], 'system', 'channel_divmore_height'); - if (!$maxheight) { - $maxheight = 400; - } $o .= '
' . "\r\n"; $o .= " +``` + +Or, using Webpack or Browserify: + +```javascript +require('readmore-js'); +``` + + +## Use + +```javascript +$('article').readmore(); +``` + +It's that simple. You can change the speed of the animation, the height of the collapsed block, and the open and close elements. + +```javascript +$('article').readmore({ + speed: 75, + lessLink: 'Read less' +}); +``` + +### The options: + +* `speed: 100` in milliseconds +* `collapsedHeight: 200` in pixels +* `heightMargin: 16` in pixels, avoids collapsing blocks that are only slightly larger than `collapsedHeight` +* `moreLink: 'Read more'` +* `lessLink: 'Close'` +* `embedCSS: true` insert required CSS dynamically, set this to `false` if you include the necessary CSS in a stylesheet +* `blockCSS: 'display: block; width: 100%;'` sets the styling of the blocks, ignored if `embedCSS` is `false` +* `startOpen: false` do not immediately truncate, start in the fully opened position +* `beforeToggle: function() {}` called after a more or less link is clicked, but *before* the block is collapsed or expanded +* `afterToggle: function() {}` called *after* the block is collapsed or expanded +* `blockProcessed: function() {}` called once per block during initilization after Readmore.js has processed the block. + +If the element has a `max-height` CSS property, Readmore.js will use that value rather than the value of the `collapsedHeight` option. + +### The callbacks: + +The `beforeToggle` and `afterToggle` callbacks both receive the same arguments: `trigger`, `element`, and `expanded`. + +* `trigger`: the "Read more" or "Close" element that was clicked +* `element`: the block that is being collapsed or expanded +* `expanded`: Boolean; `true` means the block is expanded + +The `blockProcessed` callback receives `element` and `collapsable`. + +* `element`: the block that has just been processed +* `collapsable`: Boolean; `false` means the block was shorter than the specified minimum `collapsedHeight`--the block will not have a "Read more" link + +#### Callback example: + +Here's an example of how you could use the `afterToggle` callback to scroll back to the top of a block when the "Close" link is clicked. + +```javascript +$('article').readmore({ + afterToggle: function(trigger, element, expanded) { + if(! expanded) { // The "Close" link was clicked + $('html, body').animate( { scrollTop: element.offset().top }, {duration: 100 } ); + } + } +}); +``` + +### Removing Readmore: + +You can remove the Readmore.js functionality like so: + +```javascript +$('article').readmore('destroy'); +``` + +Or, you can be more surgical by specifying a particular element: + +```javascript +$('article:first').readmore('destroy'); +``` + +### Toggling blocks programmatically: + +You can toggle a block from code: + +```javascript +$('article:nth-of-type(3)').readmore('toggle'); +``` + + +## CSS: + +Readmore.js is designed to use CSS for as much functionality as possible: collapsed height can be set in CSS with the `max-height` property; "collapsing" is achieved by setting `overflow: hidden` on the containing block and changing the `height` property; and, finally, the expanding/collapsing animation is done with CSS3 transitions. + +By default, Readmore.js inserts the following CSS, in addition to some transition-related rules: + +```css +selector + [data-readmore-toggle], selector[data-readmore] { + display: block; + width: 100%; +} +``` + +_`selector` would be the element you invoked `readmore()` on, e.g.: `$('selector').readmore()`_ + +You can override the base rules when you set up Readmore.js like so: + +```javascript +$('article').readmore({blockCSS: 'display: inline-block; width: 50%;'}); +``` + +If you want to include the necessary styling in your site's stylesheet, you can disable the dynamic embedding by setting `embedCSS` to `false`: + +```javascript +$('article').readmore({embedCSS: false}); +``` + +### Media queries and other CSS tricks: + +If you wanted to set a `maxHeight` based on lines, you could do so in CSS with something like: + +```css +body { + font: 16px/1.5 sans-serif; +} + +/* Show only 4 lines in smaller screens */ +article { + max-height: 6em; /* (4 * 1.5 = 6) */ +} +``` + +Then, with a media query you could change the number of lines shown, like so: + +```css +/* Show 8 lines on larger screens */ +@media screen and (min-width: 640px) { + article { + max-height: 12em; + } +} +``` + + +## Contributing + +Pull requests are always welcome, but not all suggested features will get merged. Feel free to contact me if you have an idea for a feature. + +Pull requests should include the minified script and this readme and the demo HTML should be updated with descriptions of your new feature. + +You'll need NPM: + +``` +$ npm install +``` + +Which will install the necessary development dependencies. Then, to build the minified script: + +``` +$ npm run build +``` + diff --git a/library/readmorejs/ajax-demo.html b/library/readmorejs/ajax-demo.html new file mode 100644 index 000000000..92ccea30d --- /dev/null +++ b/library/readmorejs/ajax-demo.html @@ -0,0 +1,128 @@ + + + + + + + + Readmore.js: jQuery plugin for long blocks of text + + + + + + + + + + + + +
+
+

Readmore.js

+ +

A smooth, responsive jQuery plugin for collapsing and expanding long blocks of text with “Read more” and “Close” links.

+
+ +

AJAX Demo

+ +
+ +
+

Artisanal Narwahls

+ +

From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there – on a mote of dust suspended in a sunbeam.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+ +

Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+ +
+

Portland Leggings

+ +

Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.

+ +

I am Duncan Macleod, born 400 years ago in the Highlands of Scotland. I am Immortal, and I am not alone. For centuries, we have waited for the time of the Gathering when the stroke of a sword and the fall of a head will release the power of the Quickening. In the end, there can be only one.

+ +

From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there – on a mote of dust suspended in a sunbeam.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+ +
+

This section is shorter than the Readmore minimum

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+
+ +
+
+ + + + + + + + + + diff --git a/library/readmorejs/bower.json b/library/readmorejs/bower.json new file mode 100644 index 000000000..6455a69be --- /dev/null +++ b/library/readmorejs/bower.json @@ -0,0 +1,29 @@ +{ + "name": "readmore-js", + "main": "readmore.js", + "version": "2.1.0", + "homepage": "http://jedfoster.com/Readmore.js/", + "authors": [ + "Jed Foster " + ], + "description": "A lightweight jQuery plugin for collapsing and expanding long blocks of text with \"Read more\" and \"Close\" links.", + "keywords": [ + "css", + "jquery", + "readmore", + "expand", + "collapse" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests", + "gulpfile.js" + ], + "dependencies": { + "jquery": ">=2.1.4" + } +} diff --git a/library/readmorejs/demo.html b/library/readmorejs/demo.html new file mode 100644 index 000000000..fbb9ceecd --- /dev/null +++ b/library/readmorejs/demo.html @@ -0,0 +1,243 @@ + + + + + + + + Readmore.js: jQuery plugin for long blocks of text + + + + + + + + + + + + +
+
+

Readmore.js

+ +

A smooth, responsive jQuery plugin for collapsing and expanding long blocks of text with “Read more” and “Close” links.

+
+ +
+

The markup Readmore.js requires is so simple, you can probably use it with your existing HTML—there’s no need for complicated sets of div’s or hardcoded classes, just call .readmore() on the element containing your block of text and Readmore.js takes care of the rest. Readmore.js plays well in a responsive environment, too.

+ +

Readmore.js is tested with—and supported on—all versions of jQuery greater than 1.9.1. All the “good” browsers are supported, as well as IE10+; IE8 & 9 should work, but are not supported and the experience will not be ideal.

+ +

Install

+ +

Install Readmore.js with npm:

+ +
$ npm install readmore-js
+ +

Then include it in your HTML:

+ +
<script src="/node_modules/readmore-js/readmore.min.js"></script>
+ +

Or, using Webpack or Browserify:

+ +
require('readmore-js');
+ +

Use

+ +
$('article').readmore();
+ +

It’s that simple. You can change the speed of the animation, the height of the collapsed block, and the open and close elements.

+ +
$('article').readmore({
+  speed: 75,
+  lessLink: '<a href="#">Read less</a>'
+});
+ +

The options:

+ +
    +
  • speed: 100 in milliseconds
  • +
  • collapsedHeight: 200 in pixels
  • +
  • heightMargin: 16 in pixels, avoids collapsing blocks that are only slightly larger than collapsedHeight
  • +
  • moreLink: '<a href="#">Read more</a>'
  • +
  • lessLink: '<a href="#">Close</a>'
  • +
  • embedCSS: true insert required CSS dynamically, set this to false if you include the necessary CSS in a stylesheet
  • +
  • blockCSS: 'display: block; width: 100%;' sets the styling of the blocks, ignored if embedCSS is false
  • +
  • startOpen: false do not immediately truncate, start in the fully opened position
  • +
  • beforeToggle: function() {} called after a more or less link is clicked, but before the block is collapsed or expanded
  • +
  • afterToggle: function() {} called after the block is collapsed or expanded
  • +
  • blockProcessed: function() {} called once per block during initilization after Readmore.js has processed the block.
  • +
+ +

If the element has a max-height CSS property, Readmore.js will use that value rather than the value of the collapsedHeight option.

+ +

The callbacks:

+ +

The beforeToggle and afterToggle callbacks both receive the same arguments: trigger, element, and expanded.

+ +
    +
  • trigger: the “Read more” or “Close” element that was clicked
  • +
  • element: the block that is being collapsed or expanded
  • +
  • expanded: Boolean; true means the block is expanded
  • +
+ +

The blockProcessed callback receives element and collapsable.

+ +
    +
  • element: the block that has just been processed
  • +
  • collapsable: Boolean; false means the block was shorter than the specified minimum collapsedHeight—the block will not have a "Read more" link
  • +
+ +

Callback example:

+ +

Here’s an example of how you could use the afterToggle callback to scroll back to the top of a block when the “Close” link is clicked.

+ +
$('article').readmore({
+  afterToggle: function(trigger, element, expanded) {
+    if(! expanded) { // The "Close" link was clicked
+      $('html, body').animate( { scrollTop: element.offset().top }, {duration: 100 } );
+    }
+  }
+});
+ +

Removing Readmore:

+ +

You can remove the Readmore.js functionality like so:

+ +
$('article').readmore('destroy');
+ +

Or, you can be more surgical by specifying a particular element:

+ +
$('article:first').readmore('destroy');
+ +

Toggling blocks programmatically:

+ +

You can toggle a block from code:

+ +
$('article:nth-of-type(3)').readmore('toggle');
+ +

CSS:

+ +

Readmore.js is designed to use CSS for as much functionality as possible: collapsed height can be set in CSS with the max-height property; “collapsing” is achieved by setting overflow: hidden on the containing block and changing the height property; and, finally, the expanding/collapsing animation is done with CSS3 transitions.

+ +

By default, Readmore.js inserts the following CSS, in addition to some transition-related rules:

+ +
selector + [data-readmore-toggle], selector[data-readmore] {
+  display: block;
+  width: 100%;
+}
+ +

selector would be the element you invoked readmore() on, e.g.: $('selector').readmore()

+ +

You can override the base rules when you set up Readmore.js like so:

+ +
$('article').readmore({blockCSS: 'display: inline-block; width: 50%;'});
+ +

If you want to include the necessary styling in your site’s stylesheet, you can disable the dynamic embedding by setting embedCSS to false:

+ +
$('article').readmore({embedCSS: false});
+ +

Media queries and other CSS tricks:

+ +

If you wanted to set a maxHeight based on lines, you could do so in CSS with something like:

+ +
body {
+  font: 16px/1.5 sans-serif;
+}
+
+/* Show only 4 lines in smaller screens */
+article {
+  max-height: 6em; /* (4 * 1.5 = 6) */
+}
+ +

Then, with a media query you could change the number of lines shown, like so:

+ +
/* Show 8 lines on larger screens */
+@media screen and (min-width: 640px) {
+  article {
+    max-height: 12em;
+  }
+}
+ +

Contributing

+ +

Pull requests are always welcome, but not all suggested features will get merged. Feel free to contact me if you have an idea for a feature.

+ +

Pull requests should include the minified script and this readme and the demo HTML should be updated with descriptions of your new feature.

+ +

You’ll need NPM:

+ +
$ npm install
+ +

Which will install the necessary development dependencies. Then, to build the minified script:

+ +
$ gulp compress
+
+ +

Demo

+ +
+ +
+

Artisanal Narwahls

+ +

From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there – on a mote of dust suspended in a sunbeam.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+ +

Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+ +
+

Portland Leggings

+ +

Here's how it is: Earth got used up, so we terraformed a whole new galaxy of Earths, some rich and flush with the new technologies, some not so much. Central Planets, them was formed the Alliance, waged war to bring everyone under their rule; a few idiots tried to fight it, among them myself. I'm Malcolm Reynolds, captain of Serenity. Got a good crew: fighters, pilot, mechanic. We even picked up a preacher, and a bona fide companion. There's a doctor, too, took his genius sister out of some Alliance camp, so they're keeping a low profile. You got a job, we can do it, don't much care what it is.

+ +

I am Duncan Macleod, born 400 years ago in the Highlands of Scotland. I am Immortal, and I am not alone. For centuries, we have waited for the time of the Gathering when the stroke of a sword and the fall of a head will release the power of the Quickening. In the end, there can be only one.

+ +

From this distant vantage point, the Earth might not seem of any particular interest. But for us, it's different. Consider again that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there – on a mote of dust suspended in a sunbeam.

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+ +
+

This section is shorter than the Readmore minimum

+ +

Space, the final frontier. These are the voyages of the starship Enterprise. Its five year mission: to explore strange new worlds, to seek out new life and new civilizations, to boldly go where no man has gone before!

+
+
+
+ + + + + + + + + diff --git a/library/readmorejs/gulpfile.js b/library/readmorejs/gulpfile.js new file mode 100644 index 000000000..32f72680a --- /dev/null +++ b/library/readmorejs/gulpfile.js @@ -0,0 +1,14 @@ +var gulp = require('gulp'), + uglify = require('gulp-uglify'), + rename = require('gulp-rename'); + +gulp.task('compress', function() { + gulp.src('readmore.js') + .pipe(uglify({ + mangle: true, + compress: true, + preserveComments: 'some' + })) + .pipe(rename('readmore.min.js')) + .pipe(gulp.dest('./')); +}); diff --git a/library/readmorejs/jquery.mockjax.js b/library/readmorejs/jquery.mockjax.js new file mode 100644 index 000000000..585783a34 --- /dev/null +++ b/library/readmorejs/jquery.mockjax.js @@ -0,0 +1,692 @@ +/*! + * MockJax - jQuery Plugin to Mock Ajax requests + * + * Version: 1.6.1 + * Released: + * Home: https://github.com/jakerella/jquery-mockjax + * Author: Jonathan Sharp (http://jdsharp.com) + * License: MIT,GPL + * + * Copyright (c) 2014 appendTo, Jordan Kasper + * NOTE: This repository was taken over by Jordan Kasper (@jakerella) October, 2014 + * + * Dual licensed under the MIT or GPL licenses. + * http://opensource.org/licenses/MIT OR http://www.gnu.org/licenses/gpl-2.0.html + */ +(function($) { + var _ajax = $.ajax, + mockHandlers = [], + mockedAjaxCalls = [], + unmockedAjaxCalls = [], + CALLBACK_REGEX = /=\?(&|$)/, + jsc = (new Date()).getTime(); + + + // Parse the given XML string. + function parseXML(xml) { + if ( window.DOMParser == undefined && window.ActiveXObject ) { + DOMParser = function() { }; + DOMParser.prototype.parseFromString = function( xmlString ) { + var doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + doc.loadXML( xmlString ); + return doc; + }; + } + + try { + var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); + if ( $.isXMLDoc( xmlDoc ) ) { + var err = $('parsererror', xmlDoc); + if ( err.length == 1 ) { + throw new Error('Error: ' + $(xmlDoc).text() ); + } + } else { + throw new Error('Unable to parse XML'); + } + return xmlDoc; + } catch( e ) { + var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); + $(document).trigger('xmlParseError', [ msg ]); + return undefined; + } + } + + // Check if the data field on the mock handler and the request match. This + // can be used to restrict a mock handler to being used only when a certain + // set of data is passed to it. + function isMockDataEqual( mock, live ) { + var identical = true; + // Test for situations where the data is a querystring (not an object) + if (typeof live === 'string') { + // Querystring may be a regex + return $.isFunction( mock.test ) ? mock.test(live) : mock == live; + } + $.each(mock, function(k) { + if ( live[k] === undefined ) { + identical = false; + return identical; + } else { + if ( typeof live[k] === 'object' && live[k] !== null ) { + if ( identical && $.isArray( live[k] ) ) { + identical = $.isArray( mock[k] ) && live[k].length === mock[k].length; + } + identical = identical && isMockDataEqual(mock[k], live[k]); + } else { + if ( mock[k] && $.isFunction( mock[k].test ) ) { + identical = identical && mock[k].test(live[k]); + } else { + identical = identical && ( mock[k] == live[k] ); + } + } + } + }); + + return identical; + } + + // See if a mock handler property matches the default settings + function isDefaultSetting(handler, property) { + return handler[property] === $.mockjaxSettings[property]; + } + + // Check the given handler should mock the given request + function getMockForRequest( handler, requestSettings ) { + // If the mock was registered with a function, let the function decide if we + // want to mock this request + if ( $.isFunction(handler) ) { + return handler( requestSettings ); + } + + // Inspect the URL of the request and check if the mock handler's url + // matches the url for this ajax request + if ( $.isFunction(handler.url.test) ) { + // The user provided a regex for the url, test it + if ( !handler.url.test( requestSettings.url ) ) { + return null; + } + } else { + // Look for a simple wildcard '*' or a direct URL match + var star = handler.url.indexOf('*'); + if (handler.url !== requestSettings.url && star === -1 || + !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace(/\*/g, '.+')).test(requestSettings.url)) { + return null; + } + } + + // Inspect the data submitted in the request (either POST body or GET query string) + if ( handler.data ) { + if ( ! requestSettings.data || !isMockDataEqual(handler.data, requestSettings.data) ) { + // They're not identical, do not mock this request + return null; + } + } + // Inspect the request type + if ( handler && handler.type && + handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) { + // The request type doesn't match (GET vs. POST) + return null; + } + + return handler; + } + + function parseResponseTimeOpt(responseTime) { + if ($.isArray(responseTime)) { + var min = responseTime[0]; + var max = responseTime[1]; + return (typeof min === 'number' && typeof max === 'number') ? Math.floor(Math.random() * (max - min)) + min : null; + } else { + return (typeof responseTime === 'number') ? responseTime: null; + } + } + + // Process the xhr objects send operation + function _xhrSend(mockHandler, requestSettings, origSettings) { + + // This is a substitute for < 1.4 which lacks $.proxy + var process = (function(that) { + return function() { + return (function() { + // The request has returned + this.status = mockHandler.status; + this.statusText = mockHandler.statusText; + this.readyState = 1; + + var finishRequest = function () { + this.readyState = 4; + + var onReady; + // Copy over our mock to our xhr object before passing control back to + // jQuery's onreadystatechange callback + if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) { + this.responseText = JSON.stringify(mockHandler.responseText); + } else if ( requestSettings.dataType == 'xml' ) { + if ( typeof mockHandler.responseXML == 'string' ) { + this.responseXML = parseXML(mockHandler.responseXML); + //in jQuery 1.9.1+, responseXML is processed differently and relies on responseText + this.responseText = mockHandler.responseXML; + } else { + this.responseXML = mockHandler.responseXML; + } + } else if (typeof mockHandler.responseText === 'object' && mockHandler.responseText !== null) { + // since jQuery 1.9 responseText type has to match contentType + mockHandler.contentType = 'application/json'; + this.responseText = JSON.stringify(mockHandler.responseText); + } else { + this.responseText = mockHandler.responseText; + } + if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) { + this.status = mockHandler.status; + } + if( typeof mockHandler.statusText === "string") { + this.statusText = mockHandler.statusText; + } + // jQuery 2.0 renamed onreadystatechange to onload + onReady = this.onreadystatechange || this.onload; + + // jQuery < 1.4 doesn't have onreadystate change for xhr + if ( $.isFunction( onReady ) ) { + if( mockHandler.isTimeout) { + this.status = -1; + } + onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined ); + } else if ( mockHandler.isTimeout ) { + // Fix for 1.3.2 timeout to keep success from firing. + this.status = -1; + } + }; + + // We have an executable function, call it to give + // the mock handler a chance to update it's data + if ( $.isFunction(mockHandler.response) ) { + // Wait for it to finish + if ( mockHandler.response.length === 2 ) { + mockHandler.response(origSettings, function () { + finishRequest.call(that); + }); + return; + } else { + mockHandler.response(origSettings); + } + } + + finishRequest.call(that); + }).apply(that); + }; + })(this); + + if ( mockHandler.proxy ) { + // We're proxying this request and loading in an external file instead + _ajax({ + global: false, + url: mockHandler.proxy, + type: mockHandler.proxyType, + data: mockHandler.data, + dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType, + complete: function(xhr) { + mockHandler.responseXML = xhr.responseXML; + mockHandler.responseText = xhr.responseText; + // Don't override the handler status/statusText if it's specified by the config + if (isDefaultSetting(mockHandler, 'status')) { + mockHandler.status = xhr.status; + } + if (isDefaultSetting(mockHandler, 'statusText')) { + mockHandler.statusText = xhr.statusText; + } + this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime) || 0); + } + }); + } else { + // type == 'POST' || 'GET' || 'DELETE' + if ( requestSettings.async === false ) { + // TODO: Blocking delay + process(); + } else { + this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime) || 50); + } + } + } + + // Construct a mocked XHR Object + function xhr(mockHandler, requestSettings, origSettings, origHandler) { + // Extend with our default mockjax settings + mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler); + + if (typeof mockHandler.headers === 'undefined') { + mockHandler.headers = {}; + } + if (typeof requestSettings.headers === 'undefined') { + requestSettings.headers = {}; + } + if ( mockHandler.contentType ) { + mockHandler.headers['content-type'] = mockHandler.contentType; + } + + return { + status: mockHandler.status, + statusText: mockHandler.statusText, + readyState: 1, + open: function() { }, + send: function() { + origHandler.fired = true; + _xhrSend.call(this, mockHandler, requestSettings, origSettings); + }, + abort: function() { + clearTimeout(this.responseTimer); + }, + setRequestHeader: function(header, value) { + requestSettings.headers[header] = value; + }, + getResponseHeader: function(header) { + // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery + if ( mockHandler.headers && mockHandler.headers[header] ) { + // Return arbitrary headers + return mockHandler.headers[header]; + } else if ( header.toLowerCase() == 'last-modified' ) { + return mockHandler.lastModified || (new Date()).toString(); + } else if ( header.toLowerCase() == 'etag' ) { + return mockHandler.etag || ''; + } else if ( header.toLowerCase() == 'content-type' ) { + return mockHandler.contentType || 'text/plain'; + } + }, + getAllResponseHeaders: function() { + var headers = ''; + // since jQuery 1.9 responseText type has to match contentType + if (mockHandler.contentType) { + mockHandler.headers['Content-Type'] = mockHandler.contentType; + } + $.each(mockHandler.headers, function(k, v) { + headers += k + ': ' + v + "\n"; + }); + return headers; + } + }; + } + + // Process a JSONP mock request. + function processJsonpMock( requestSettings, mockHandler, origSettings ) { + // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here + // because there isn't an easy hook for the cross domain script tag of jsonp + + processJsonpUrl( requestSettings ); + + requestSettings.dataType = "json"; + if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) { + createJsonpCallback(requestSettings, mockHandler, origSettings); + + // We need to make sure + // that a JSONP style response is executed properly + + var rurl = /^(\w+:)?\/\/([^\/?#]+)/, + parts = rurl.exec( requestSettings.url ), + remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); + + requestSettings.dataType = "script"; + if(requestSettings.type.toUpperCase() === "GET" && remote ) { + var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings ); + + // Check if we are supposed to return a Deferred back to the mock call, or just + // signal success + if(newMockReturn) { + return newMockReturn; + } else { + return true; + } + } + } + return null; + } + + // Append the required callback parameter to the end of the request URL, for a JSONP request + function processJsonpUrl( requestSettings ) { + if ( requestSettings.type.toUpperCase() === "GET" ) { + if ( !CALLBACK_REGEX.test( requestSettings.url ) ) { + requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") + + (requestSettings.jsonp || "callback") + "=?"; + } + } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) { + requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?"; + } + } + + // Process a JSONP request by evaluating the mocked response text + function processJsonpRequest( requestSettings, mockHandler, origSettings ) { + // Synthesize the mock request for adding a script tag + var callbackContext = origSettings && origSettings.context || requestSettings, + newMock = null; + + + // If the response handler on the moock is a function, call it + if ( mockHandler.response && $.isFunction(mockHandler.response) ) { + mockHandler.response(origSettings); + } else { + + // Evaluate the responseText javascript in a global context + if( typeof mockHandler.responseText === 'object' ) { + $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')'); + } else { + $.globalEval( '(' + mockHandler.responseText + ')'); + } + } + + // Successful response + setTimeout(function() { + jsonpSuccess( requestSettings, callbackContext, mockHandler ); + jsonpComplete( requestSettings, callbackContext, mockHandler ); + }, parseResponseTimeOpt(mockHandler.responseTime) || 0); + + // If we are running under jQuery 1.5+, return a deferred object + if($.Deferred){ + newMock = new $.Deferred(); + if(typeof mockHandler.responseText == "object"){ + newMock.resolveWith( callbackContext, [mockHandler.responseText] ); + } + else{ + newMock.resolveWith( callbackContext, [$.parseJSON( mockHandler.responseText )] ); + } + } + return newMock; + } + + + // Create the required JSONP callback function for the request + function createJsonpCallback( requestSettings, mockHandler, origSettings ) { + var callbackContext = origSettings && origSettings.context || requestSettings; + var jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++); + + // Replace the =? sequence both in the query string and the data + if ( requestSettings.data ) { + requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1"); + } + + requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1"); + + + // Handle JSONP-style loading + window[ jsonp ] = window[ jsonp ] || function( tmp ) { + data = tmp; + jsonpSuccess( requestSettings, callbackContext, mockHandler ); + jsonpComplete( requestSettings, callbackContext, mockHandler ); + // Garbage collect + window[ jsonp ] = undefined; + + try { + delete window[ jsonp ]; + } catch(e) {} + + if ( head ) { + head.removeChild( script ); + } + }; + } + + // The JSONP request was successful + function jsonpSuccess(requestSettings, callbackContext, mockHandler) { + // If a local callback was specified, fire it and pass it the data + if ( requestSettings.success ) { + requestSettings.success.call( callbackContext, mockHandler.responseText || "", status, {} ); + } + + // Fire the global callback + if ( requestSettings.global ) { + (requestSettings.context ? $(requestSettings.context) : $.event).trigger("ajaxSuccess", [{}, requestSettings]); + } + } + + // The JSONP request was completed + function jsonpComplete(requestSettings, callbackContext) { + // Process result + if ( requestSettings.complete ) { + requestSettings.complete.call( callbackContext, {} , status ); + } + + // The request was completed + if ( requestSettings.global ) { + (requestSettings.context ? $(requestSettings.context) : $.event).trigger("ajaxComplete", [{}, requestSettings]); + } + + // Handle the global AJAX counter + if ( requestSettings.global && ! --$.active ) { + $.event.trigger( "ajaxStop" ); + } + } + + + // The core $.ajax replacement. + function handleAjax( url, origSettings ) { + var mockRequest, requestSettings, mockHandler, overrideCallback; + + // If url is an object, simulate pre-1.5 signature + if ( typeof url === "object" ) { + origSettings = url; + url = undefined; + } else { + // work around to support 1.5 signature + origSettings = origSettings || {}; + origSettings.url = url; + } + + // Extend the original settings for the request + requestSettings = $.extend(true, {}, $.ajaxSettings, origSettings); + + // Generic function to override callback methods for use with + // callback options (onAfterSuccess, onAfterError, onAfterComplete) + overrideCallback = function(action, mockHandler) { + var origHandler = origSettings[action.toLowerCase()]; + return function() { + if ( $.isFunction(origHandler) ) { + origHandler.apply(this, [].slice.call(arguments)); + } + mockHandler['onAfter' + action](); + }; + }; + + // Iterate over our mock handlers (in registration order) until we find + // one that is willing to intercept the request + for(var k = 0; k < mockHandlers.length; k++) { + if ( !mockHandlers[k] ) { + continue; + } + + mockHandler = getMockForRequest( mockHandlers[k], requestSettings ); + if(!mockHandler) { + // No valid mock found for this request + continue; + } + + mockedAjaxCalls.push(requestSettings); + + // If logging is enabled, log the mock to the console + $.mockjaxSettings.log( mockHandler, requestSettings ); + + + if ( requestSettings.dataType && requestSettings.dataType.toUpperCase() === 'JSONP' ) { + if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) { + // This mock will handle the JSONP request + return mockRequest; + } + } + + + // Removed to fix #54 - keep the mocking data object intact + //mockHandler.data = requestSettings.data; + + mockHandler.cache = requestSettings.cache; + mockHandler.timeout = requestSettings.timeout; + mockHandler.global = requestSettings.global; + + // In the case of a timeout, we just need to ensure + // an actual jQuery timeout (That is, our reponse won't) + // return faster than the timeout setting. + if ( mockHandler.isTimeout ) { + if ( mockHandler.responseTime > 1 ) { + origSettings.timeout = mockHandler.responseTime - 1; + } else { + mockHandler.responseTime = 2; + origSettings.timeout = 1; + } + mockHandler.isTimeout = false; + } + + // Set up onAfter[X] callback functions + if ( $.isFunction( mockHandler.onAfterSuccess ) ) { + origSettings.success = overrideCallback('Success', mockHandler); + } + if ( $.isFunction( mockHandler.onAfterError ) ) { + origSettings.error = overrideCallback('Error', mockHandler); + } + if ( $.isFunction( mockHandler.onAfterComplete ) ) { + origSettings.complete = overrideCallback('Complete', mockHandler); + } + + copyUrlParameters(mockHandler, origSettings); + + (function(mockHandler, requestSettings, origSettings, origHandler) { + + mockRequest = _ajax.call($, $.extend(true, {}, origSettings, { + // Mock the XHR object + xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); } + })); + })(mockHandler, requestSettings, origSettings, mockHandlers[k]); + + return mockRequest; + } + + // We don't have a mock request + unmockedAjaxCalls.push(origSettings); + if($.mockjaxSettings.throwUnmocked === true) { + throw new Error('AJAX not mocked: ' + origSettings.url); + } + else { // trigger a normal request + return _ajax.apply($, [origSettings]); + } + } + + /** + * Copies URL parameter values if they were captured by a regular expression + * @param {Object} mockHandler + * @param {Object} origSettings + */ + function copyUrlParameters(mockHandler, origSettings) { + //parameters aren't captured if the URL isn't a RegExp + if (!(mockHandler.url instanceof RegExp)) { + return; + } + //if no URL params were defined on the handler, don't attempt a capture + if (!mockHandler.hasOwnProperty('urlParams')) { + return; + } + var captures = mockHandler.url.exec(origSettings.url); + //the whole RegExp match is always the first value in the capture results + if (captures.length === 1) { + return; + } + captures.shift(); + //use handler params as keys and capture resuts as values + var i = 0, + capturesLength = captures.length, + paramsLength = mockHandler.urlParams.length, + //in case the number of params specified is less than actual captures + maxIterations = Math.min(capturesLength, paramsLength), + paramValues = {}; + for (i; i < maxIterations; i++) { + var key = mockHandler.urlParams[i]; + paramValues[key] = captures[i]; + } + origSettings.urlParams = paramValues; + } + + + // Public + + $.extend({ + ajax: handleAjax + }); + + $.mockjaxSettings = { + //url: null, + //type: 'GET', + log: function( mockHandler, requestSettings ) { + if ( mockHandler.logging === false || + ( typeof mockHandler.logging === 'undefined' && $.mockjaxSettings.logging === false ) ) { + return; + } + if ( window.console && console.log ) { + var message = 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url; + var request = $.extend({}, requestSettings); + + if (typeof console.log === 'function') { + console.log(message, request); + } else { + try { + console.log( message + ' ' + JSON.stringify(request) ); + } catch (e) { + console.log(message); + } + } + } + }, + logging: true, + status: 200, + statusText: "OK", + responseTime: 500, + isTimeout: false, + throwUnmocked: false, + contentType: 'text/plain', + response: '', + responseText: '', + responseXML: '', + proxy: '', + proxyType: 'GET', + + lastModified: null, + etag: '', + headers: { + etag: 'IJF@H#@923uf8023hFO@I#H#', + 'content-type' : 'text/plain' + } + }; + + $.mockjax = function(settings) { + var i = mockHandlers.length; + mockHandlers[i] = settings; + return i; + }; + $.mockjax.clear = function(i) { + if ( arguments.length == 1 ) { + mockHandlers[i] = null; + } else { + mockHandlers = []; + } + mockedAjaxCalls = []; + unmockedAjaxCalls = []; + }; + // support older, deprecated version + $.mockjaxClear = function(i) { + window.console && window.console.warn && window.console.warn( 'DEPRECATED: The $.mockjaxClear() method has been deprecated in 1.6.0. Please use $.mockjax.clear() as the older function will be removed soon!' ); + $.mockjax.clear(); + }; + $.mockjax.handler = function(i) { + if ( arguments.length == 1 ) { + return mockHandlers[i]; + } + }; + $.mockjax.mockedAjaxCalls = function() { + return mockedAjaxCalls; + }; + $.mockjax.unfiredHandlers = function() { + var results = []; + for (var i=0, len=mockHandlers.length; i", + "license": "MIT", + "bugs": { + "url": "https://github.com/jedfoster/Readmore.js/issues" + }, + "homepage": "https://github.com/jedfoster/Readmore.js", + "dependencies": { + "jquery": ">2.1.4" + }, + "devDependencies": { + "gulp": "^3.9.0", + "gulp-rename": "^1.2.0", + "gulp-uglify": "^1.0.2" + } +} diff --git a/library/readmorejs/readmore.js b/library/readmorejs/readmore.js new file mode 100644 index 000000000..37596795c --- /dev/null +++ b/library/readmorejs/readmore.js @@ -0,0 +1,342 @@ +/*! + * @preserve + * + * Readmore.js jQuery plugin + * Author: @jed_foster + * Project home: http://jedfoster.github.io/Readmore.js + * Licensed under the MIT license + * + * Debounce function from http://davidwalsh.name/javascript-debounce-function + */ + +/* global jQuery */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function($) { + 'use strict'; + + var readmore = 'readmore', + defaults = { + speed: 100, + collapsedHeight: 200, + heightMargin: 16, + moreLink: 'Read More', + lessLink: 'Close', + embedCSS: true, + blockCSS: 'display: block; width: 100%;', + startOpen: false, + + // callbacks + blockProcessed: function() {}, + beforeToggle: function() {}, + afterToggle: function() {} + }, + cssEmbedded = {}, + uniqueIdCounter = 0; + + function debounce(func, wait, immediate) { + var timeout; + + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (! immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + func.apply(context, args); + } + }; + } + + function uniqueId(prefix) { + var id = ++uniqueIdCounter; + + return String(prefix === null ? 'rmjs-' : prefix) + id; + } + + function setBoxHeights(element) { + var el = element.clone().css({ + height: 'auto', + width: element.width(), + maxHeight: 'none', + overflow: 'hidden' + }).insertAfter(element), + expandedHeight = el.outerHeight(), + cssMaxHeight = parseInt(el.css({maxHeight: ''}).css('max-height').replace(/[^-\d\.]/g, ''), 10), + defaultHeight = element.data('defaultHeight'); + + el.remove(); + + var collapsedHeight = cssMaxHeight || element.data('collapsedHeight') || defaultHeight; + + // Store our measurements. + element.data({ + expandedHeight: expandedHeight, + maxHeight: cssMaxHeight, + collapsedHeight: collapsedHeight + }) + // and disable any `max-height` property set in CSS + .css({ + maxHeight: 'none' + }); + } + + var resizeBoxes = debounce(function() { + $('[data-readmore]').each(function() { + var current = $(this), + isExpanded = (current.attr('aria-expanded') === 'true'); + + setBoxHeights(current); + + current.css({ + height: current.data( (isExpanded ? 'expandedHeight' : 'collapsedHeight') ) + }); + }); + }, 100); + + function embedCSS(options) { + if (! cssEmbedded[options.selector]) { + var styles = ' '; + + if (options.embedCSS && options.blockCSS !== '') { + styles += options.selector + ' + [data-readmore-toggle], ' + + options.selector + '[data-readmore]{' + + options.blockCSS + + '}'; + } + + // Include the transition CSS even if embedCSS is false + styles += options.selector + '[data-readmore]{' + + 'transition: height ' + options.speed + 'ms;' + + 'overflow: hidden;' + + '}'; + + (function(d, u) { + var css = d.createElement('style'); + css.type = 'text/css'; + + if (css.styleSheet) { + css.styleSheet.cssText = u; + } + else { + css.appendChild(d.createTextNode(u)); + } + + d.getElementsByTagName('head')[0].appendChild(css); + }(document, styles)); + + cssEmbedded[options.selector] = true; + } + } + + function Readmore(element, options) { + this.element = element; + + this.options = $.extend({}, defaults, options); + + embedCSS(this.options); + + this._defaults = defaults; + this._name = readmore; + + this.init(); + + // IE8 chokes on `window.addEventListener`, so need to test for support. + if (window.addEventListener) { + // Need to resize boxes when the page has fully loaded. + window.addEventListener('load', resizeBoxes); + window.addEventListener('resize', resizeBoxes); + } + else { + window.attachEvent('load', resizeBoxes); + window.attachEvent('resize', resizeBoxes); + } + } + + + Readmore.prototype = { + init: function() { + var current = $(this.element); + + current.data({ + defaultHeight: this.options.collapsedHeight, + heightMargin: this.options.heightMargin + }); + + setBoxHeights(current); + + var collapsedHeight = current.data('collapsedHeight'), + heightMargin = current.data('heightMargin'); + + if (current.outerHeight(true) <= collapsedHeight + heightMargin) { + // The block is shorter than the limit, so there's no need to truncate it. + if (this.options.blockProcessed && typeof this.options.blockProcessed === 'function') { + this.options.blockProcessed(current, false); + } + return true; + } + else { + var id = current.attr('id') || uniqueId(), + useLink = this.options.startOpen ? this.options.lessLink : this.options.moreLink; + + current.attr({ + 'data-readmore': '', + 'aria-expanded': this.options.startOpen, + 'id': id + }); + + current.after($(useLink) + .on('click', (function(_this) { + return function(event) { + _this.toggle(this, current[0], event); + }; + })(this)) + .attr({ + 'data-readmore-toggle': id, + 'aria-controls': id + })); + + if (! this.options.startOpen) { + current.css({ + height: collapsedHeight + }); + } + + if (this.options.blockProcessed && typeof this.options.blockProcessed === 'function') { + this.options.blockProcessed(current, true); + } + } + }, + + toggle: function(trigger, element, event) { + if (event) { + event.preventDefault(); + } + + if (! trigger) { + trigger = $('[aria-controls="' + this.element.id + '"]')[0]; + } + + if (! element) { + element = this.element; + } + + var $element = $(element), + newHeight = '', + newLink = '', + expanded = false, + collapsedHeight = $element.data('collapsedHeight'); + + if ($element.height() <= collapsedHeight) { + newHeight = $element.data('expandedHeight') + 'px'; + newLink = 'lessLink'; + expanded = true; + } + else { + newHeight = collapsedHeight; + newLink = 'moreLink'; + } + + // Fire beforeToggle callback + // Since we determined the new "expanded" state above we're now out of sync + // with our true current state, so we need to flip the value of `expanded` + if (this.options.beforeToggle && typeof this.options.beforeToggle === 'function') { + this.options.beforeToggle(trigger, $element, ! expanded); + } + + $element.css({'height': newHeight}); + + // Fire afterToggle callback + $element.on('transitionend', (function(_this) { + return function() { + if (_this.options.afterToggle && typeof _this.options.afterToggle === 'function') { + _this.options.afterToggle(trigger, $element, expanded); + } + + $(this).attr({ + 'aria-expanded': expanded + }).off('transitionend'); + }; + })(this)); + + $(trigger).replaceWith($(this.options[newLink]) + .on('click', (function(_this) { + return function(event) { + _this.toggle(this, element, event); + }; + })(this)) + .attr({ + 'data-readmore-toggle': $element.attr('id'), + 'aria-controls': $element.attr('id') + })); + }, + + destroy: function() { + $(this.element).each(function() { + var current = $(this); + + current.attr({ + 'data-readmore': null, + 'aria-expanded': null + }) + .css({ + maxHeight: '', + height: '' + }) + .next('[data-readmore-toggle]') + .remove(); + + current.removeData(); + }); + } + }; + + + $.fn.readmore = function(options) { + var args = arguments, + selector = this.selector; + + options = options || {}; + + if (typeof options === 'object') { + return this.each(function() { + if ($.data(this, 'plugin_' + readmore)) { + var instance = $.data(this, 'plugin_' + readmore); + instance.destroy.apply(instance); + } + + options.selector = selector; + + $.data(this, 'plugin_' + readmore, new Readmore(this, options)); + }); + } + else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { + return this.each(function () { + var instance = $.data(this, 'plugin_' + readmore); + if (instance instanceof Readmore && typeof instance[options] === 'function') { + instance[options].apply(instance, Array.prototype.slice.call(args, 1)); + } + }); + } + }; + +})); + diff --git a/library/readmorejs/readmore.min.js b/library/readmorejs/readmore.min.js new file mode 100644 index 000000000..365d73e0d --- /dev/null +++ b/library/readmorejs/readmore.min.js @@ -0,0 +1,11 @@ +/*! + * @preserve + * + * Readmore.js jQuery plugin + * Author: @jed_foster + * Project home: http://jedfoster.github.io/Readmore.js + * Licensed under the MIT license + * + * Debounce function from http://davidwalsh.name/javascript-debounce-function + */ +!function(t){"function"==typeof define&&define.amd?define(["jquery"],t):"object"==typeof exports?module.exports=t(require("jquery")):t(jQuery)}(function(t){"use strict";function e(t,e,i){var o;return function(){var n=this,a=arguments,s=function(){o=null,i||t.apply(n,a)},r=i&&!o;clearTimeout(o),o=setTimeout(s,e),r&&t.apply(n,a)}}function i(t){var e=++h;return String(null==t?"rmjs-":t)+e}function o(t){var e=t.clone().css({height:"auto",width:t.width(),maxHeight:"none",overflow:"hidden"}).insertAfter(t),i=e.outerHeight(),o=parseInt(e.css({maxHeight:""}).css("max-height").replace(/[^-\d\.]/g,""),10),n=t.data("defaultHeight");e.remove();var a=o||t.data("collapsedHeight")||n;t.data({expandedHeight:i,maxHeight:o,collapsedHeight:a}).css({maxHeight:"none"})}function n(t){if(!d[t.selector]){var e=" ";t.embedCSS&&""!==t.blockCSS&&(e+=t.selector+" + [data-readmore-toggle], "+t.selector+"[data-readmore]{"+t.blockCSS+"}"),e+=t.selector+"[data-readmore]{transition: height "+t.speed+"ms;overflow: hidden;}",function(t,e){var i=t.createElement("style");i.type="text/css",i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),t.getElementsByTagName("head")[0].appendChild(i)}(document,e),d[t.selector]=!0}}function a(e,i){this.element=e,this.options=t.extend({},r,i),n(this.options),this._defaults=r,this._name=s,this.init(),window.addEventListener?(window.addEventListener("load",c),window.addEventListener("resize",c)):(window.attachEvent("load",c),window.attachEvent("resize",c))}var s="readmore",r={speed:100,collapsedHeight:200,heightMargin:16,moreLink:'Read More',lessLink:'Close',embedCSS:!0,blockCSS:"display: block; width: 100%;",startOpen:!1,blockProcessed:function(){},beforeToggle:function(){},afterToggle:function(){}},d={},h=0,c=e(function(){t("[data-readmore]").each(function(){var e=t(this),i="true"===e.attr("aria-expanded");o(e),e.css({height:e.data(i?"expandedHeight":"collapsedHeight")})})},100);a.prototype={init:function(){var e=t(this.element);e.data({defaultHeight:this.options.collapsedHeight,heightMargin:this.options.heightMargin}),o(e);var n=e.data("collapsedHeight"),a=e.data("heightMargin");if(e.outerHeight(!0)<=n+a)return this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!1),!0;var s=e.attr("id")||i(),r=this.options.startOpen?this.options.lessLink:this.options.moreLink;e.attr({"data-readmore":"","aria-expanded":this.options.startOpen,id:s}),e.after(t(r).on("click",function(t){return function(i){t.toggle(this,e[0],i)}}(this)).attr({"data-readmore-toggle":s,"aria-controls":s})),this.options.startOpen||e.css({height:n}),this.options.blockProcessed&&"function"==typeof this.options.blockProcessed&&this.options.blockProcessed(e,!0)},toggle:function(e,i,o){o&&o.preventDefault(),e||(e=t('[aria-controls="'+this.element.id+'"]')[0]),i||(i=this.element);var n=t(i),a="",s="",r=!1,d=n.data("collapsedHeight");n.height()<=d?(a=n.data("expandedHeight")+"px",s="lessLink",r=!0):(a=d,s="moreLink"),this.options.beforeToggle&&"function"==typeof this.options.beforeToggle&&this.options.beforeToggle(e,n,!r),n.css({height:a}),n.on("transitionend",function(i){return function(){i.options.afterToggle&&"function"==typeof i.options.afterToggle&&i.options.afterToggle(e,n,r),t(this).attr({"aria-expanded":r}).off("transitionend")}}(this)),t(e).replaceWith(t(this.options[s]).on("click",function(t){return function(e){t.toggle(this,i,e)}}(this)).attr({"data-readmore-toggle":n.attr("id"),"aria-controls":n.attr("id")}))},destroy:function(){t(this.element).each(function(){var e=t(this);e.attr({"data-readmore":null,"aria-expanded":null}).css({maxHeight:"",height:""}).next("[data-readmore-toggle]").remove(),e.removeData()})}},t.fn.readmore=function(e){var i=arguments,o=this.selector;return e=e||{},"object"==typeof e?this.each(function(){if(t.data(this,"plugin_"+s)){var i=t.data(this,"plugin_"+s);i.destroy.apply(i)}e.selector=o,t.data(this,"plugin_"+s,new a(this,e))}):"string"==typeof e&&"_"!==e[0]&&"init"!==e?this.each(function(){var o=t.data(this,"plugin_"+s);o instanceof a&&"function"==typeof o[e]&&o[e].apply(o,Array.prototype.slice.call(i,1))}):void 0}}); \ No newline at end of file diff --git a/view/php/theme_init.php b/view/php/theme_init.php index 208d74813..b7eef08aa 100644 --- a/view/php/theme_init.php +++ b/view/php/theme_init.php @@ -18,7 +18,7 @@ Head::add_js('/library/textcomplete/textcomplete.min.js'); Head::add_js('autocomplete.js'); Head::add_js('/library/jquery.timeago.js'); -Head::add_js('/library/readmore.js/readmore.js'); +Head::add_js('/library/readmorejs/readmore.js'); Head::add_js('/library/sticky-kit/sticky-kit.min.js'); Head::add_js('/library/jgrowl/jquery.jgrowl.min.js');