Issue #98, #4 Implemented countable.js

This commit is contained in:
Gabor Javorszky 2013-06-01 10:34:31 +01:00 committed by ErisDS
parent cf2d777ca4
commit f783adbe7f
4 changed files with 421 additions and 32 deletions

View File

@ -1,9 +1,13 @@
// # Article Editor
/*global window, document, history, jQuery, Showdown, CodeMirror, shortcut */
(function ($, ShowDown, CodeMirror, shortcut) {
/*global window, document, history, jQuery, Showdown, CodeMirror, shortcut, Countable */
(function ($, ShowDown, CodeMirror, shortcut, Countable) {
"use strict";
var wordCount = $('.entry-word-count'),
charCount = $('.entry-character-count'),
paragraphCount = $('.entry-paragraph-count'),
// ## Converter Initialisation
/**
* @property converter
@ -11,13 +15,14 @@
*/
// Initialise the Showdown converter for Markdown.
// var delay;
var converter = new ShowDown.converter({extensions: ['ghostdown']}),
converter = new ShowDown.converter({extensions: ['ghostdown']}),
editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
tabMode: 'indent',
lineWrapping: true
});
// ## Functions
/**
* @method Update word count
@ -25,14 +30,14 @@
* @constructor
*/
// This updates the word count on the editor preview panel.
function updateWordCount() {
var wordCount = document.getElementsByClassName('entry-word-count')[0],
editorValue = editor.getValue();
// function updateWordCount() {
// var wordCount = document.getElementsByClassName('entry-word-count')[0],
// editorValue = editor.getValue();
if (editorValue.length) {
wordCount.innerHTML = editorValue.match(/\S+/g).length + ' words';
}
}
// if (editorValue.length) {
// wordCount.innerHTML = editorValue.match(/\S+/g).length + ' words';
// }
// }
/**
* @method updatePreview
@ -44,8 +49,14 @@
function updatePreview() {
var preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = converter.makeHtml(editor.getValue());
console.log(preview);
Countable.once(preview, function (counter) {
// updateWordCount(counter);
wordCount.text(counter.words + ' words');
charCount.text(counter.characters + ' characters');
paragraphCount.text(counter.paragraphs + ' paragraphs');
updateWordCount();
});
}
/**
@ -228,4 +239,4 @@
});
});
});
}(jQuery, Showdown, CodeMirror, shortcut));
}(jQuery, Showdown, CodeMirror, shortcut, Countable));

View File

@ -0,0 +1,377 @@
/**
* Countable is a script to allow for live paragraph-, word- and character-
* counting on an HTML element.
*
* @author Sacha Schmid (<https://github.com/RadLikeWhoa>)
* @version 2.0.0
* @license MIT
* @see <http://radlikewhoa.github.io/Countable/>
*/
/**
* Note: For the purpose of this internal documentation, arguments of the type
* {Nodes} are to be interpreted as either {NodeList} or {Element}.
*/
;(function (global) {
'use strict'
/**
* @private
*
* `_liveElements` holds all elements that have the live-counting
* functionality bound to them.
*
* `_event` holds the event to handle the live counting, based on the
* browser's capabilities.
*/
var _liveElements = [],
_event = 'oninput' in document ? 'input' : 'keyup'
/**
* `String.trim()` polyfill for non-supporting browsers. This is the
* recommended polyfill on MDN.
*
* @see <http://goo.gl/uYveB>
* @see <http://goo.gl/xjIxJ>
*
* @return {String} The original string with leading and trailing whitespace
* removed.
*/
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '')
}
}
/**
* `ucs2decode` function from the punycode.js library.
*
* Creates an array containing the decimal code points of each Unicode
* character in the string. While JavaScript uses UCS-2 internally, this
* function will convert a pair of surrogate halves (each of which UCS-2
* exposes as separate characters) into a single code point, matching
* UTF-16.
*
* @see <http://goo.gl/8M09r>
* @see <http://goo.gl/u4UUC>
*
* @param {String} string The Unicode input string (UCS-2).
*
* @return {Array} The new array of code points.
*/
function _decode (string) {
var output = [],
counter = 0,
length = string.length,
value, extra
while (counter < length) {
value = string.charCodeAt(counter++)
if ((value & 0xF800) == 0xD800 && counter < length) {
// High surrogate, and there is a next character.
extra = string.charCodeAt(counter++)
if ((extra & 0xFC00) == 0xDC00) {
// Low surrogate.
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000)
} else {
output.push(value, extra)
}
} else {
output.push(value)
}
}
return output
}
/**
* `_validateArguments` validates the arguments given to each function call.
* Errors are logged to the console as warnings, but Countable fails silently.
*
* @private
*
* @param {Nodes} elements The (collection of) element(s) to
* validate.
*
* @param {Function} callback The callback function to validate.
*
* @return {Boolean} Returns whether all arguments are vaild.
*/
function _validateArguments (elements, callback) {
var elementsValid = elements && ((Object.prototype.toString.call(elements) === '[object NodeList]' && elements.length) || (elements.nodeType === 1)),
callbackValid = callback && typeof callback === 'function'
if ('console' in window && 'warn' in console) {
if (!elementsValid) console.warn('Countable: No valid elements were found')
if (!callbackValid) console.warn('Countable: "' + callback + '" is not a valid callback function')
}
return elementsValid && callbackValid
}
/**
* `_extendDefaults` is a function to extend a set of default options with the
* ones given in the function call. Available options are described below.
*
* {Boolean} hardReturns Use two returns to seperate a paragraph instead of
* one.
* {Boolean} stripTags Strip HTML tags before counting the values.
*
* @private
*
* @param {Object} options Countable allows the options described above.
* They can be used in a function call to override
* the default behaviour.
*
* @return {Object} The new options object.
*/
function _extendDefaults (options) {
var defaults = { hardReturns: false, stripTags: false }
for (var prop in options) {
if (defaults.hasOwnProperty(prop)) defaults[prop] = options[prop]
}
return defaults
}
/**
* `_count` trims an element's value, optionally strips HTML tags and counts
* paragraphs, words, characters and characters plus spaces.
*
* @private
*
* @param {Element} element The element whose value is to be counted.
*
* @param {Object} options The options to use for the counting.
*
* @return {Object} The object containing the number of paragraphs,
* words, characters and characters plus spaces.
*/
function _count (element, options) {
var original = 'value' in element ? element.value : element.innerText || element.textContent,
temp, trimmed
/**
* The initial implementation to allow for HTML tags stripping was created
* @craniumslows while the current one was created by @Rob--W.
*
* @see <http://goo.gl/Exmlr>
* @see <http://goo.gl/gFQQh>
*/
if (options.stripTags) original = original.replace(/<\/?[a-z][^>]*>/gi, '')
trimmed = original.trim()
/**
* Most of the performance improvements are based on the works of @epmatsw.
*
* @see <http://goo.gl/SWOLB>
*/
return {
paragraphs: trimmed ? (trimmed.match(options.hardReturns ? /\n{2,}/g : /\n+/g) || []).length + 1 : 0,
words: trimmed ? (trimmed.replace(/['";:,.?¿\-!¡]+/g, '').match(/\S+/g) || []).length : 0,
characters: trimmed ? _decode(trimmed.replace(/\s/g, '')).length : 0,
all: _decode(original.replace(/[\n\r]/g, '')).length
}
}
/**
* `_loop` is a helper function to iterate over a collection, e.g. a NodeList
* or an Array. The callback receives the current element as the single
* parameter.
*
* @private
*
* @param {Array} which The collection to iterate over.
*
* @param {Function} callback The callback function to call on each
* iteration.
*/
function _loop (which, callback) {
var len = which.length
if (typeof len !== 'undefined') {
while (len--) {
callback(which[len])
}
} else {
callback(which)
}
}
/**
* This is the main object that will later be exposed to other scripts. It
* holds all the public methods that can be used to enable the Countable
* functionality.
*/
var Countable = {
/**
* The `live` method binds the counting handler to all given elements. The
* event is either `oninput` or `onkeydown`, based on the capabilities of
* the browser.
*
* @param {Nodes} elements All elements that should receive the
* Countable functionality.
*
* @param {Function} callback The callback to fire whenever the
* element's value changes. The callback is
* called with the relevant element bound to
* `this` and the counted values as the
* single parameter.
*
* @param {Object} [options] An object to modify Countable's
* behaviour. Refer to `_extendDefaults` for
* a list of available options.
*
* @return {Object} Returns the Countable object to allow for chaining.
*/
live: function (elements, callback, options) {
var ops = _extendDefaults(options),
bind = function (element) {
var handler = function () {
callback.call(element, _count(element, ops))
}
_liveElements.push({ element: element, handler: handler })
handler()
if (element.addEventListener) {
element.addEventListener(_event, handler, false)
} else if (element.attachEvent) {
element.attachEvent('on' + _event, handler)
}
}
if (!_validateArguments(elements, callback)) return
if (elements.length) {
_loop(elements, bind)
} else {
bind(elements)
}
return this
},
/**
* The `die` method removes the Countable functionality from all given
* elements.
*
* @param {Nodes} elements All elements whose Countable functionality
* should be unbound.
*
* @return {Object} Returns the Countable object to allow for chaining.
*/
die: function (elements) {
if (!_validateArguments(elements, function () {})) return
_loop(elements, function (element) {
var liveElement
_loop(_liveElements, function (live) {
if (live.element === element) liveElement = live
})
if (!liveElement) return
if (element.removeEventListener) {
element.removeEventListener(_event, liveElement.handler, false)
} else if (element.detachEvent) {
element.detachEvent('on' + _event, liveElement.handler)
}
_liveElements.splice(_liveElements.indexOf(liveElement), 1)
})
return this
},
/**
* The `once` method works mostly like the `live` method, but no events are
* bound, the functionality is only executed once.
*
* @param {Nodes} elements All elements that should receive the
* Countable functionality.
*
* @param {Function} callback The callback to fire whenever the
* element's value changes. The callback is
* called with the relevant element bound to
* `this` and the counted values as the
* single parameter.
*
* @param {Object} [options] An object to modify Countable's
* behaviour. Refer to `_extendDefaults`
* for a list of available options.
*
* @return {Object} Returns the Countable object to allow for chaining.
*/
once: function (elements, callback, options) {
if (!_validateArguments(elements, callback)) return
_loop(elements, function (element) {
callback.call(element, _count(element, _extendDefaults(options)))
})
return this
},
/**
* The `enabled` method checks if the live-counting functionality is bound
* to an element.
*
* @param {Element} element A single Element.
*
* @return {Boolean} A boolean value representing whether Countable
* functionality is bound to the given element.
*/
enabled: function (element) {
var isEnabled = false
if (element && element.nodeType === 1) {
_loop(_liveElements, function (live) {
if (live.element === element) isEnabled = true
})
}
return isEnabled
}
}
/**
* Expose Countable depending on the module system used across the
* application. (Node / CommonJS, AMD, global)
*/
if (typeof exports === 'object') {
module.exports = Countable
} else if (typeof define === 'function' && define.amd) {
define(function () { return Countable })
} else {
global.Countable = Countable
}
}(this))

View File

@ -3,7 +3,7 @@
* used for publishing content with Ghost.
*
* Table of Contents:
*
*
* Editor / Preview
* Post Preview Content
* Full Screen Mode
@ -16,7 +16,7 @@
============================================================================= */
.editor {
// The main post title
.entry-title {
@extend %box;
@ -42,11 +42,11 @@
}
}
// The two content panel wrappers, positioned left/right
.entry-markdown { left:0; border-right:$lightbrown 2px solid; }
.entry-preview { right:0; border-left:$lightbrown 2px solid; }
// The visual styles for both panels
.entry-markdown, .entry-preview {
@include box-sizing(border-box);
@ -61,7 +61,7 @@
@include breakpoint($mobile) {
@include box-shadow(none);
}
// Convert all content areas to small boxes
@include breakpoint($netbook) {
top:109px;
@ -76,7 +76,7 @@
overflow: hidden;
}
}
.floatingheader {
// Turn headers into tabs which act as links
@ -116,19 +116,19 @@
}
}
// Give the tab with the .active class the highest z-index
&.active {
z-index: 200;
}
// Restore the normal height of the .active tab (inactive tab stays small, hidden behind)
&.active .markdown,
&.active .entry-preview-content {
height:auto;
overflow: auto;
}
// Restore the white bg of the currently .active tab, remove hand cursor from currently active tab
&.active header {
@include breakpoint($netbook) {
@ -141,7 +141,7 @@
}
}
}
// Hide markdown icon + wordcount when we hit mobile
@include breakpoint($mobile) {
.markdown-help,
@ -166,7 +166,7 @@
right:0;
bottom:0;
left:0;
&:focus {
outline: 0;
}
@ -183,7 +183,7 @@
font-size:1.1em;
line-height:1.2em;
color: lighten($darkgrey, 30%);
.CodeMirror-focused,
.CodeMirror-selected {
color: $darkgrey;
@ -220,7 +220,7 @@
.cm-quote {color:#000;}
}
.entry-preview {
// Align the tab of entry-preview on the right
.floatingheader {
@ -240,7 +240,7 @@
left:0;
padding: 60px 40px 40px 40px;
overflow: auto;
// Tweak padding for smaller screens
@include breakpoint($netbook) {padding-top: 20px;}
@include breakpoint($mobile) {padding: 15px;}
@ -268,7 +268,7 @@
}
}
}
}//.editor
@ -323,7 +323,7 @@
}
img {
width: 100%;
width: 100%;
height: auto;
}
}
@ -342,7 +342,7 @@ body.zen {
overflow: hidden;
@include transition(all 0.5s ease-out);
}
main {top: 15px;@include transition(all 0.5s ease-out);}
.entry-markdown, .entry-preview {bottom:0;@include transition(all 0.5s ease-out);}
@ -394,7 +394,7 @@ body.zen {
};
}
}
.splitbutton-save{
.button-save{
@include transition(width 0.25s ease);
@ -457,7 +457,7 @@ body.zen {
float: left;
margin-right: 5px;
padding: 0 5px;
color: $lightgrey;
color: $lightgrey;
background: lighten($grey, 15%);
border-radius: $rounded;
@include box-shadow(
@ -495,7 +495,7 @@ body.zen {
@include icon($i-settings, 1.1em){vertical-align:0;};
@include box-sizing(border-box);
display:inline-block;
padding: 0 10px;
padding: 0 10px;
line-height: 1.8em;
color: $midgrey;
@include transition;

View File

@ -29,6 +29,7 @@
<script type="text/javascript" src="/core/admin/assets/lib/underscore.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/backbone/backbone.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/backbone/backbone-layout.js"></script>
<script type="text/javascript" src="/core/admin/assets/lib/countable.js"></script>
<script type="text/javascript" src="/core/admin/assets/js/toggle.js"></script>
<script type="text/javascript" src="/core/admin/assets/js/admin-ui-temp.js"></script>
{{{block "headScripts"}}}