entybion/js/jquery.meltdown.js
2014-05-01 15:09:46 +02:00

963 lines
No EOL
32 KiB
JavaScript

/*global jQuery, addResizeListener*/
/*
* Meltdown (Markup Extra Live Toolbox)
* Version: 0.2 (??-APR-2014)
* Requires: jQuery v1.7.2 or later (1.9.1 recommended)
*/
(function ($, window, document, undefined) {
'use strict';
var ver = '0.2',
plgName = 'meltdown',
dbg = true,
isIE8 = document.all && !document.addEventListener, // From: http://tanalin.com/en/articles/ie-version-js/
isOldjQuery = parseFloat($.fn.jquery) < 1.8,
doc = $(document),
body = $("body");
function debug(msg) {
if (window.console && dbg) {
window.console.log(msg);
}
}
// Used to test the bottom offset of elements:
var bottomPositionTest = $('<div style="bottom: 0;" />');
// Helper for users that want to change the controls (For usage, see: $.meltdown.defaults.controls below)
var controlsGroup = function(name, label, controls) {
controls.name = name;
controls.label = label;
return controls;
};
$.meltdown = {
version: ver,
// Expose publicly:
controlsGroup: controlsGroup,
// Default meltdown options:
defaults: {
// Use $.meltdown.controlsGroup() to make groups and subgroups of controls.
// The available control names come from the keys of $.meltdown.controlDefs (see below)
controls: controlsGroup("", "", [
"preview",
"bold",
"italics",
"ul",
"ol",
"|",
"table",
controlsGroup("h", "Headers", ["h1", "h2", "h3", "h4", "h5", "h6"]),
"|",
controlsGroup("kitchenSink", "Kitchen Sink", [
"link",
"img",
"blockquote",
"codeblock",
"code",
"footnote",
"hr"
]),
"fullscreen",
"sidebyside"
]),
// If true, goes directly in fullscreen mode:
fullscreen: false,
// Should the preview be visible by default ?
openPreview: false,
// A CSS height or "editorHeight" or "auto" (to let the height adjust to the content).
previewHeight: "editorHeight",
// If true, when the preview is toggled it will (un)collapse resulting in the total height of the wrap to change.
// Set this to false if you want the editor to expand/shrinkin the opposite way of the preview.
// Setting this to false can be useful if you want to restrict or lock the total height.
previewCollapses: true,
// If true, editor and preview will be displayed side by side instead of one on the other.
sidebyside: false,
// If true, when the preview is fully scrolled it will stay scrolled while typing.
// Very convenient when typing/adding text at the end of the editor.
autoScrollPreview: true,
// Duration of the preview toggle animation:
previewDuration: 400,
// The parser. The function takes a string and returns an html formatted string.
// Set this to false to use an _identity_ function (for a direct HTML "parser").
parser: window.Markdown
},
// Definitions for the toolbar controls:
controlDefs: {
bold: {
label: "B",
altText: "Bold",
before: "**",
after: "**"
},
italics: {
label: "I",
altText: "Italics",
before: "*",
after: "*"
},
ul: {
label: "UL",
altText: "Unordered List",
preselectLine: true,
before: "* ",
placeholder: "Item\n* Item",
isBlock: true
},
ol: {
label: "OL",
altText: "Ordered List",
preselectLine: true,
before: "1. ",
placeholder: "Item 1\n2. Item 2\n3. Item 3",
isBlock: true
},
table: {
label: "Table",
altText: "Table",
before: "First Header | Second Header\n------------- | -------------\nContent Cell | Content Cell\nContent Cell | Content Cell\n",
isBlock: true
},
link: {
label: "Link",
altText: "Link",
before: "[",
placeholder: "Example link",
after: "](http:// \"Link title\")"
},
img: {
label: "Image",
altText: "Image",
before: "![Alt text](",
placeholder: "http://",
after: ")"
},
blockquote: {
label: "Blockquote",
altText: "Blockquote",
preselectLine: true,
before: "> ",
placeholder: "Quoted text",
isBlock: true
},
codeblock: {
label: "Code Block",
altText: "Code Block",
preselectLine: true,
before: "~~~\n",
placeholder: "Code",
after: "\n~~~",
isBlock: true
},
code: {
label: "Code",
altText: "Inline Code",
before: "`",
placeholder: "code",
after: "`"
},
footnote: {
label: "Footnote",
altText: "Footnote",
before: "[^1]\n\n[^1]:",
placeholder: "Example footnote",
isBlock: true
},
hr: {
label: "HR",
altText: "Horizontal Rule",
before: "----------",
placeholder: "",
isBlock: true
},
fullscreen: {
label: "Fullscreen",
altText: "Toggle fullscreen",
click: function(meltdown /*, def, control, execAction */) {
meltdown.toggleFullscreen();
}
},
sidebyside: {
label: "Sidebyside",
altText: "Toggle sidebyside",
click: function(meltdown /*, def, control, execAction */) {
meltdown.toggleSidebyside();
}
},
preview: {
label: "Preview",
altText: "Toggle preview",
click: function(meltdown /*, def, control, execAction */) {
meltdown.togglePreview();
}
}
}
};
// Add h1...h6 control definitions to $.meltdown.controlDefs:
(function(controlDefs) {
for (var pounds = "", i = 1; i <= 6; i++) {
pounds += "#";
controlDefs['h' + i] = {
label: "H" + i,
altText: "Header " + i,
preselectLine: true,
before: pounds + " "
};
}
})($.meltdown.controlDefs);
function addControlEventHandler(meltdown, def, control) {
var editor = meltdown.editor,
execAction = function () {
var text = editor.val(),
selection = editor.getSelection(),
before = def.before || "",
placeholder = def.placeholder || "",
after = def.after || "";
// Extend selection if needed:
if (def.preselectLine) {
var lineStart = text.lastIndexOf('\n', selection.start) + 1,
lineEnd = text.indexOf('\n', selection.end);
if (lineEnd === -1) {
lineEnd = text.length;
}
editor.setSelection(lineStart, lineEnd);
selection = editor.getSelection();
}
// placeholder is only used if there is no selected text:
if (selection.length > 0) {
placeholder = selection.text;
}
// isBlock means that there should be empty line before and after the selection:
if (def.isBlock) {
for (var i = 0; i < 2; i++) {
var charBefore = text.charAt(selection.start - 1 - i),
charAfter = text.charAt(selection.end + i);
if (charBefore !== "\n" && charBefore !== "") {
before = "\n" + before;
}
if (charAfter !== "\n" && charAfter !== "") {
after = after + "\n";
}
}
}
// Insert placeholder:
if (selection.text !== placeholder) {
editor.replaceSelectedText(placeholder, "select");
}
// Insert before and after selection:
editor.surroundSelectedText(before, after, "select");
};
control.click(function (e) {
if (!control.hasClass('disabled')) {
if (def.click) {
def.click(meltdown, def, control, execAction);
} else {
execAction();
}
meltdown.update();
}
editor.focus();
e.preventDefault();
});
}
function addGroupClickHandler(control) {
control.on('click', function () {
control.siblings('li').removeClass(plgName + '_controlgroup-open').children('ul').hide();
control.toggleClass(plgName + '_controlgroup-open').children('ul').toggle();
});
}
function buildControls(meltdown, controlsGroup, subGroup) {
var controlList = $('<ul />');
if (subGroup) {
controlList.css("display", "none");
controlList.addClass(plgName + "_controlgroup-" + controlsGroup.name + " " + plgName + '_controlgroup-dropdown');
} else {
controlList.addClass("meltdown_controls");
}
for (var i = 0; i < controlsGroup.length; i++) {
var controlName = controlsGroup[i],
control = $('<li />'),
span = $('<span />').appendTo(control);
if ($.type(controlName) === "string") {
if (controlName === "|") { // Separator
controlList.append(control.addClass(plgName + '_controlsep ' + plgName + '_controlbutton'));
continue;
}
var def = $.meltdown.controlDefs[controlName];
if (def === undefined) {
debug("Control not found: " + controlName);
continue;
}
control.addClass(plgName + "_control-" + controlName + " " + plgName + '_control ' + plgName + '_controlbutton ' + (def.styleClass || ""));
span.text(def.label).attr("title", def.altText);
addControlEventHandler(meltdown, def, control);
} else if ($.isArray(controlName)) {
control.addClass(plgName + "_controlgroup-" + controlName.name + " " + plgName + '_controlgroup ' + plgName + '_controlbutton');
span.text(controlName.label).append('<i class="meltdown-icon-caret-down" />');
addGroupClickHandler(control);
control.append(buildControls(meltdown, controlName, true));
}
controlList.append(control);
}
return controlList;
}
function addWarning(meltdown, element) {
element.click(function(e) {
var warning = $('<div class"' + plgName + '_warning"/>').html('<center><b>The preview area is a tech preview feature</b></center><br/>'
+ 'Live previews <b>can</b> cause the browser tab to stop responding.<br/><br/>'
+ 'There is a <a target="_blank" href="https://github.com/iphands/Meltdown/issues/1">known issue</a> with <a target="_blank" href="https://github.com/tanakahisateru/js-markdown-extra#notice">one of the libraries</a> used to generate the live preview.<br/><br/>'
+ 'This warning will be removed when the issue is resolved.<br/><br/>'
+ '<center><i>Click to continue.</i></center>').css({background: "#fdd", cursor: "pointer"});
warning.on("click", function(e) {
if (!$(e.target).is("a, a *")) { // Ignore clicks on links
meltdown.update(true);
}
});
meltdown.preview.empty().append(warning);
e.preventDefault();
});
}
// Setup event handlers for the resize handle:
function setupResizeHandle(resizeHandle, firstElem, lastElem, vertical, meltdown) {
resizeHandle.addClass("meltdown_resizehandle-" + (vertical ? "vert" : "horiz"));
var propName = vertical ? "height" : "width",
pageName = vertical ? "pageY" : "pageX",
lastEditorPercentName = vertical ? "lastEditorPercentHeight" : "lastEditorPercentWidth",
minSize = vertical ? 15 : 60;
var startPos, minPos, maxPos, originalFirstElemSize, originalLastElemSize,
moveEventHandler = function(e) {
var delta = Math.min(Math.max(e[pageName] , minPos), maxPos) - startPos,
firstElemSize = originalFirstElemSize + delta,
lastElemSize = originalLastElemSize - delta;
firstElem[propName](firstElemSize);
lastElem[propName](lastElemSize);
if (!vertical) {
firstElem[0].style.maxWidth = firstElemSize + "px";
lastElem[0].style.maxWidth = lastElemSize + "px";
}
var editorElem = vertical ? meltdown.editor[0] : meltdown.editorWrap[0],
editorSize = firstElem[0] === editorElem ? firstElemSize : lastElemSize;
meltdown[lastEditorPercentName] = editorSize / (firstElemSize + lastElemSize);
};
// Init dragging handlers only on mousedown:
resizeHandle.on("mousedown", function(e) {
if (meltdown.isSidebyside() === vertical) {
return;
}
// Sort elems in document order:
var elems = firstElem.add(lastElem);
// The first elem is assumed to be before resizeHandle, and the last is after:
firstElem = elems.first();
lastElem = elems.last();
// Init dragging properties:
startPos = e[pageName];
originalFirstElemSize = firstElem[propName]();
originalLastElemSize = lastElem[propName]();
minPos = startPos - originalFirstElemSize + minSize;
maxPos = startPos + originalLastElemSize - minSize;
// Setup event handlers:
doc.on("mousemove", moveEventHandler).one("mouseup", function() {
doc.off("mousemove", moveEventHandler);
body.removeClass("unselectable");
meltdown.editor.focus();
});
// Prevent text selection while dragging:
body.addClass("unselectable");
});
}
function debounce(func, wait, returnValue) {
var context, args, timeout,
exec = function() {
func.apply(context, args);
};
return function() {
context = this;
args = arguments;
clearTimeout(timeout);
timeout = setTimeout(exec, wait);
return returnValue;
};
}
// Return true, false or undefined.
// If newState is undefined or not a boolean, return !state (this is the toggle action)
// If newState === state, return newState or if force, return undefined (to tell that no state change is required)
function checkToggleState(newState, state, force) {
if (newState !== true && newState !== false) {
return !state;
}
if (newState === state) {
return force ? newState : undefined;
}
return newState;
}
function splitSize(availableSize, firstPercentSize, minSize) {
var firstSize = Math.round(firstPercentSize * availableSize),
lastSize = availableSize - firstSize;
if (firstSize < minSize) {
lastSize -= minSize - firstSize;
firstSize = minSize;
} else if (lastSize < minSize) {
firstSize -= minSize - lastSize;
lastSize = minSize;
}
return {firstSize: firstSize, lastSize: lastSize};
}
// Meltdown base class:
var Meltdown = $.meltdown.Meltdown = function(elem) {
this.element = $(elem);
};
// The Meltdown methods.
// Methods are publicly available: elem.meltdown("methodName", args...)
$.meltdown.methods = $.extend(Meltdown.prototype, {
_init: function(userOptions) {
var self = this,
_options = this._options = $.extend({}, $.meltdown.defaults, userOptions);
this._lastUpdateText = "";
// If parser is false, use a HTML parser (ie. directly use the text as the HTML source)
this.parser = _options.parser || function(text) {
return text;
};
this.editorPreInitOuterWidth = this.element.outerWidth();
// Setup everything detached from the document:
this.wrap = $('<div class="' + plgName + '_wrap previewopen" />');
this.topmargin = $('<div class="' + plgName + '_topmargin"/>').appendTo(this.wrap);
this.bar = $('<div class="meltdown_bar"></div>').appendTo(this.wrap);
this.editorWrap = $('<div class="' + plgName + '_editor-wrap" />').appendTo(this.wrap);
this.editorDeco = $('<div class="' + plgName + '_editor-deco" />').appendTo(this.editorWrap);
this.editor = this.element.addClass("meltdown_editor");
this.previewWrap = $('<div class="' + plgName + '_preview-wrap" />').appendTo(this.wrap);
this.resizeHandle = $('<div class="' + plgName + '_resizehandle"><span></span></div>').appendTo(this.previewWrap);
this.previewHeader = $('<span class="' + plgName + '_preview-header">Preview Area (<a class="meltdown_techpreview" href="https://github.com/iphands/Meltdown/issues/1">Tech Preview</a>)</span>').appendTo(this.previewWrap);
this.preview = $('<div class="' + plgName + '_preview" />').appendTo(this.previewWrap);
this.bottommargin = $('<div class="' + plgName + '_bottommargin"/>').appendTo(this.wrap);
// Setup meltdown sizes:
this.wrap.outerWidth(this.editorPreInitOuterWidth); // jQuery 1.8+ (undocumented: http://bugs.jquery.com/ticket/10877)
if (isOldjQuery) this.wrap.width(this.editorPreInitOuterWidth); // Good enough.
var previewHeight = _options.previewHeight;
if (previewHeight === "editorHeight") {
previewHeight = this.editor.height();
}
this.preview.height(previewHeight);
// Build toolbar:
this.controls = buildControls(this, _options.controls).appendTo(this.bar);
addWarning(this, this.previewHeader.find(".meltdown_techpreview"));
// editorDeco's CSS need a bit of help:
this.editor.focus(function() {
self.editorDeco.addClass("focus");
}).blur(function() {
self.editorDeco.removeClass("focus");
});
// Need to put a div in the wrap to allow absolute positioning for child elements.
// Bug in FF < 31: https://bugzilla.mozilla.org/show_bug.cgi?id=63895
this.previewWrap2 = $('<div class="' + plgName + '_preview-wrap2"></div>').appendTo(this.previewWrap);
this.previewWrap2.append(this.resizeHandle, this.previewHeader, this.preview);
setupResizeHandle(this.resizeHandle, this.editor, this.preview, true, this);
setupResizeHandle(this.resizeHandle, this.editorWrap, this.previewWrap, false, this);
// Setup update:
this.debouncedUpdate = debounce(this.update, 350, this);
this.editor.on('keyup', $.proxy(this.debouncedUpdate, this));
// Store datas needed by fullscreen mode:
this.fullscreenData = {};
// Insert meltdown in the document:
this.editor.after(this.wrap).appendTo(this.editorDeco);
this._checkToolbarOverflowedControls();
// Setup display state (preview open and _heightsManaged):
this._previewCollapses = _options.previewCollapses;
this.togglePreview(true, 0, true, !_options.openPreview); // Do not update the preview if !_options.openPreview
if (!this.isPreviewCollapses() && _options.previewHeight === "auto") {
this.preview.height("+=0"); // If !_previewCollapses, we cannot have a dynamic height.
}
this._checkHeightsManaged("", undefined, true); // Set CSS height of wrap.
// Define the wrap min height from the editor and the preview min heights:
var wrapHeight = this.wrap.height(),
minWrapHeights = parseFloat(this.editor.css("minHeight")) + parseFloat(this.preview.css("minHeight")),
editorHeight = this.editor.height();
previewHeight = this.preview.height();
this.wrap.css("minHeight", wrapHeight - editorHeight - previewHeight + minWrapHeights);
// Setup editor and preview resizing when wrap is resized:
this.lastWrapWidth = this.wrap.width();
this.lastWrapHeight = wrapHeight;
this.lastEditorPercentWidth = 0.5;
this.lastEditorPercentHeight = editorHeight / (editorHeight + previewHeight);
addResizeListener(this.wrap[0], $.proxy(this._wrapResizeListener, this));
// Now that all measures were made, we can close the preview if needed:
if (!_options.openPreview) {
this.togglePreview(false, 0);
}
// And set the sidebyside and fullscreen modes:
this.toggleSidebyside(_options.sidebyside, true);
if (_options.fullscreen) {
this.toggleFullscreen(_options.fullscreen);
}
return this; // Chaining
},
options: function(name, value) {
if (arguments.length === 1) {
return this._options[name];
} else if (arguments.length > 1) {
this._options[name] = value;
return this; // Chaining
}
},
update: function(force) {
return this.updateWith(this.editor.val(), force);
},
updateWith: function(text, force) {
if (force === true || (this.isPreviewOpen() && text !== this._lastUpdateText)) {
// If the preview is scrolled to the bottom, keept it scrolled after update:
var previewNode = this.preview[0],
scrolledToBottom = previewNode.scrollHeight - previewNode.scrollTop === previewNode.clientHeight;
this.preview.html(this.parser(text));
if (scrolledToBottom) {
previewNode.scrollTop = previewNode.scrollHeight;
}
this._lastUpdateText = text;
}
return this; // Chaining
},
isPreviewOpen: function() {
return this.wrap.hasClass("previewopen");
},
togglePreview: function(open, duration, force, noUpdate) {
open = checkToggleState(open, this.isPreviewOpen(), force);
if (open === undefined) {
return this; // Chaining
}
if (duration === undefined) {
duration = this._options.previewDuration;
}
// Function to resize the editor when the preview is resized:
var self = this,
editorHeight = this.editor.height(),
previewWrapHeightStart = open ? 0 : this.previewWrap.outerHeight(),
availableHeight = editorHeight + previewWrapHeightStart,
progress = this._isHeightsManaged() ? function(/* animation, progress */) {
self.editor.height(availableHeight - self.previewWrap.outerHeight());
} : $.noop,
editorWrapWidth = this.editorWrap.width(),
previewWrapWidth = open ? 0 : this.previewWrap.width(),
sidebysideStep = function (now /*, fx */) {
self.previewWrap[0].style.maxWidth = now + "px";
var newEditorWrapWidth = editorWrapWidth + (previewWrapWidth - now);
self.editorWrap.width(newEditorWrapWidth);
self.editorWrap[0].style.maxWidth = newEditorWrapWidth + "px";
},
unsetPreviewWrapDisplay = function() {
self.previewWrap.css("display", "");
};
if (open) {
this.wrap.addClass("previewopen");
if (!noUpdate) {
this.update();
}
if (this.isSidebyside()) {
this.previewWrap.stop().animate({
width: "show"
}, {
duration: duration,
step: sidebysideStep,
start: function(fx) { // jQuery 1.8+
var sizes = splitSize(self.wrap.width(), self.lastEditorPercentWidth, 60);
fx.tweens[0].end = sizes.lastSize;
unsetPreviewWrapDisplay(); // Why jQuery sets this to "block" ?
},
complete: unsetPreviewWrapDisplay // Why jQuery sets this to "block" ?
});
} else {
var previewWrapHeightUsed = this.previewWrap.outerHeight();
// Check that preview is not too big:
if (this._heightsManaged && previewWrapHeightUsed > editorHeight - 15) {
this.preview.height("-=" + (previewWrapHeightUsed - (editorHeight - 15)));
}
if (!isOldjQuery) {
this.previewWrap.stop().slideDown({
duration: duration,
progress: progress, // jQuery 1.8+
start: unsetPreviewWrapDisplay, // Why jQuery sets this to "block" ? // jQuery 1.8+
complete: unsetPreviewWrapDisplay // Why jQuery sets this to "block" ?
});
} else {
if (this._heightsManaged) {
this.editor.height("-=" + previewWrapHeightUsed);
}
this.previewWrap.stop().show();
unsetPreviewWrapDisplay(); // Why jQuery sets this to "block" ?
}
}
} else {
if (this.isSidebyside()) {
this.previewWrap.stop().animate({
width: "hide"
}, {
duration: duration,
step: sidebysideStep,
complete: function() {
self.previewWrap.css("max-width", "");
}
});
} else {
if (!isOldjQuery && this.previewWrap.is(":visible") && duration > 0) { // slideUp() doesn't work on hidden elements.
this.previewWrap.stop().slideUp({
duration: duration,
progress: progress // jQuery 1.8+
});
} else {
this.previewWrap.stop().hide();
if (this._heightsManaged) {
this.editor.height(availableHeight);
}
}
}
this.wrap.removeClass("previewopen");
}
return this; // Chaining
},
isFullscreen: function() {
return this.wrap.hasClass('fullscreen');
},
toggleFullscreen: function(full) {
full = checkToggleState(full, this.isFullscreen());
if (full === undefined) {
return this; // Chaining
}
var data = this.fullscreenData;
if (full) {
data.originalWrapHeight = this.wrap.height();
data.availableHeight = this.editor.height() + this.preview.height();
// Keep height in case it is "auto" or "" or whatever:
data.originalWrapStyleHeight = this.wrap[0].style.height;
this._checkHeightsManaged("fullscreen", true);
this.wrap.addClass('fullscreen');
var self = this;
doc.on("keypress." + plgName + ".fullscreenEscKey", function(e) {
if (e.keyCode === 27) { // Esc key
self.toggleFullscreen(false);
}
});
} else {
doc.off("keypress." + plgName + ".fullscreenEscKey");
this.wrap.removeClass('fullscreen');
if (this._isHeightsManaged()) {
this._adjustHeights(data.originalWrapHeight);
this.lastWrapHeight = data.originalWrapHeight;
} else {
var sizes = splitSize(data.availableHeight, this.lastEditorPercentHeight, 15);
this.editor.height(sizes.firstSize);
this.preview.height(sizes.lastSize);
}
this._checkHeightsManaged("fullscreen", false);
this.wrap[0].style.height = data.originalWrapStyleHeight;
}
this._wrapResizeListener();
return this; // Chaining
},
isSidebyside: function() {
return this.wrap.hasClass('sidebyside');
},
toggleSidebyside: function(sidebyside, force) {
sidebyside = checkToggleState(sidebyside, this.isSidebyside(), force);
if (sidebyside === undefined) {
return this; // Chaining
}
var isPreviewOpen = this.isPreviewOpen(),
originalBottommarginTop = this.bottommargin.offset().top;
if (sidebyside) {
this.wrap.addClass("sidebyside");
this._adjustWidths(this.wrap.width());
if (!isPreviewOpen) {
this.togglePreview(true, 0, false, true);
}
var editorBottom = bottomPositionTest.appendTo(this.editorWrap).offset().top,
previewBottom = bottomPositionTest.appendTo(this.previewWrap).offset().top;
bottomPositionTest.detach();
if (!isPreviewOpen) {
this.togglePreview(false, 0, false, true);
}
var diffHeights = editorBottom - previewBottom;
this.preview.height("+=" + diffHeights);
var deltaWrapHeight = originalBottommarginTop - this.bottommargin.offset().top;
this.editor.height("+=" + deltaWrapHeight);
this.preview.height("+=" + deltaWrapHeight);
this._checkHeightsManaged("sidebyside", true);
} else {
if (!isPreviewOpen) {
this.togglePreview(true, 0, false, true);
}
var originalWrapHeight = this.wrap.height();
this.editorWrap.css("width", "");
this._checkHeightsManaged("sidebyside", false);
this.editorWrap.css({width: "", maxWidth: ""});
this.previewWrap.css({width: "", maxWidth: ""});
this.wrap.removeClass("sidebyside");
var deltaBottommarginTop = this.bottommargin.offset().top - originalBottommarginTop;
this.lastWrapHeight = originalWrapHeight + deltaBottommarginTop;
this._adjustHeights(originalWrapHeight);
this.lastWrapHeight = originalWrapHeight;
if (!isPreviewOpen) {
this.togglePreview(false, 0, false, true);
}
}
return this; // Chaining
},
isPreviewCollapses: function() {
return this._previewCollapses;
},
togglePreviewCollapses: function(previewCollapses, force) {
previewCollapses = checkToggleState(previewCollapses, this._previewCollapses, force);
if (previewCollapses === undefined) {
return this; // Chaining
}
this._previewCollapses = previewCollapses;
this._checkHeightsManaged();
return this; // Chaining
},
_isHeightsManaged: function() {
return this._heightsManaged;
},
_toggleHeightsManaged: function(manage, force) {
manage = checkToggleState(manage, this._heightsManaged, force);
if (manage === undefined) {
return this; // Chaining
}
if (manage) {
this.wrap.height("+=0").addClass("heightsManaged");
} else {
this.wrap.height("auto").removeClass("heightsManaged");
}
this._heightsManaged = manage;
return this; // Chaining
},
_checkHeightsManaged: function(change, value, force) {
var previewCollapses = change === "previewCollapses" ? value : this._previewCollapses,
fullscreen = change === "fullscreen" ? value : this.isFullscreen(),
sidebyside = change === "sidebyside" ? value : this.isSidebyside(),
manage = !previewCollapses || fullscreen || sidebyside;
if (force || manage !== this._heightsManaged) {
this._toggleHeightsManaged(manage, force);
}
},
_wrapResizeListener: function() {
var newWidth = this.wrap.width(),
newHeight = this.wrap.height();
if (newWidth !== this.lastWrapWidth) {
this._checkToolbarOverflowedControls();
this._adjustWidths(newWidth);
this.lastWrapWidth = newWidth;
}
if (newHeight !== this.lastWrapHeight) {
if (this._heightsManaged) {
this._adjustHeights(newHeight);
} else {
var editorHeight = this.editor.height();
this.lastEditorPercentHeight = editorHeight / (editorHeight + this.preview.height());
}
this.lastWrapHeight = newHeight;
}
},
// When the wrap height changes, this will resize the editor and the preview,
// keeping the height ratio between them.
_adjustHeights: function(wrapHeight) {
// To avoid document reflow, we only set the values at the end.
var sizes;
if (this.isSidebyside()) {
var deltaHeight = wrapHeight - this.lastWrapHeight;
sizes = {
firstSize: this.editor.height() + deltaHeight,
lastSize: this.preview.height() + deltaHeight
};
} else {
var isPreviewOpen = this.isPreviewOpen(),
editorHeight = this.editor.height(),
previewHeight = isPreviewOpen ? this.preview.height() : 0,
availableHeight = editorHeight + previewHeight + (wrapHeight - this.lastWrapHeight);
sizes = splitSize(availableHeight, this.lastEditorPercentHeight, 15);
if (!isPreviewOpen) {
// Keep the previewHeight for when the preview will slide down again.
// But allow editorHeight to take the whole available height:
sizes.firstSize = editorHeight + (wrapHeight - this.lastWrapHeight);
}
}
this.editor.height(sizes.firstSize);
this.preview.height(sizes.lastSize);
return this; // Chaining
},
_adjustWidths: function(wrapWidth) {
if (this.isSidebyside()) {
var sizes = splitSize(wrapWidth, this.lastEditorPercentWidth, 60);
if (!this.isPreviewOpen()) {
sizes.firstSize += sizes.lastSize;
}
this.editorWrap.width(sizes.firstSize);
this.previewWrap.width(sizes.lastSize);
this.editorWrap[0].style.maxWidth = sizes.firstSize + "px";
this.previewWrap[0].style.maxWidth = sizes.lastSize + "px";
}
return this; // Chaining
},
// Call this to manage controls that are overflowing the toolbar
// when its width changes:
_checkToolbarOverflowedControls: function() {
var controls = this.controls.children(),
control = $(controls[0]),
defaultTop = control.position().top,
foundOverflowed = false;
// First we look for overflowed controls:
for (var i = controls.length - 1; i > 1; i--) {
control = $(controls[i]);
if (control.hasClass("overflowedControl")) {
continue;
}
else if (control.position().top <= defaultTop) {
break;
}
control.addClass("overflowedControl");
foundOverflowed = true;
}
// If no new overflowed control was found,
// then look for controls that are no more overflowed:
if (!foundOverflowed) {
for (; i < controls.length; i++) {
control = $(controls[i]);
if (!$(controls[i]).hasClass("overflowedControl")) {
continue;
}
// Test if it would overflow:
control.removeClass("overflowedControl");
if (control.position().top > defaultTop) {
control.addClass("overflowedControl");
break;
}
}
}
return this; // Chaining
}
});
// THE $(...).meltdown() function:
// Inspired by: http://api.jqueryui.com/jQuery.widget/
$.fn.meltdown = function (arg) {
// Get method name and method arguments:
var methodName = $.type(arg) === "string" ? arg : "_init",
args = Array.prototype.slice.call(arguments, methodName === "_init" ? 0 : 1);
// Dispatch method call:
for (var elem, meltdown, returnValue, i = 0; i < this.length; i++) {
elem = this[i];
// Get the Meltdown object or create it:
meltdown = $.data(elem, "Meltdown");
if (methodName === "_init") {
if (meltdown) continue; // Don't re-create it.
meltdown = new Meltdown(elem);
$.data(elem, "Meltdown", meltdown);
}
// Call the method:
returnValue = meltdown[methodName].apply(meltdown, args);
// If the method is a getter, return the value
// (See: http://bililite.com/blog/2009/04/23/improving-jquery-ui-widget-getterssetters/)
if (returnValue !== meltdown) {
return returnValue;
}
}
return this; // Chaining
};
if (isIE8||true) {
// Fixing the textarea deselection on click:
// (http://stackoverflow.com/questions/3558939/javascript-get-selected-text-from-textarea-in-ie8)
var oldBuildControls = buildControls;
buildControls = function() {
var ret = oldBuildControls.apply(this, arguments);
ret.find("span").attr("unselectable", "on");
return ret;
};
}
if (isOldjQuery) {
$.meltdown.controlDefs.sidebyside.styleClass = "disabled";
$.meltdown.controlDefs.sidebyside.altText = "Disabled: requires jQuery 1.8+";
Meltdown.prototype.toggleSidebyside = function() {
debug("Requires jQuery 1.8+");
return this;
};
}
}(jQuery, window, document));