diff --git a/.travis.yml b/.travis.yml index e23794d4f..3e0a4bb98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ services: # Package installation install: - python setup.py install - - pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand + - pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand embedly - pip install coveralls # Pre-test configuration before_script: diff --git a/CHANGELOG.txt b/CHANGELOG.txt index b708cfd21..ddd41d76e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,7 +2,16 @@ Changelog ========= 0.4 (xx.xx.20xx) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~ + * Added 'original' as a resizing rule supported by the 'image' tag + * Hallo.js updated to version 1.0.4 + * Snippets are now ordered alphabetically + * Removed the "More" section from the admin menu + * Added pagination to page listings in admin + * Support for setting a subpage_types property on page models, to define which page types are allowed as subpages + * Fix: Animated GIFs are now coalesced before resizing + * Fix: Wand backend clones images before modifying them + * Fix: Admin breadcrumb now positioned correctly on mobile 0.3.1 (03.06.2014) ~~~~~~~~~~~~~~~~~~ diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index d7222307d..a3f9c7221 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -28,6 +28,7 @@ Contributors * Ben Margolis * Tom Talbot * Jeffrey Hearn +* Robert Clark Translators =========== diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index e5f583397..f0054eaab 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -94,7 +94,7 @@ The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, set The syntax for the tag is thus:: - {% image [image] [method]-[dimension(s)] %} + {% image [image] [resize-rule] %} For example: @@ -108,16 +108,20 @@ For example: {% image self.photo fill-80x80 %} -In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[method]`` defines which resizing algorithm to use and ``[dimension(s)]`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm. +In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[resize-rule]`` defines how the image is to be resized when inserted into the page; various resizing methods are supported, to cater for different usage cases (e.g. lead images that span the whole width of the page, or thumbnails to be cropped to a fixed size). -Note that a space separates ``[image]`` and ``[method]``, but not ``[method]`` and ``[dimensions]``: a hyphen between ``[method]`` and ``[dimensions]`` is mandatory. Multiple dimensions must be separated by an ``x``. +Note that a space separates ``[image]`` and ``[resize-rule]``, but the resize rule must not contain spaces. -The available ``method`` s are: +The available resizing methods are: .. glossary:: ``max`` (takes two dimensions) + .. code-block:: django + + {% image self.photo max-1000x500 %} + Fit **within** the given dimensions. The longest edge will be reduced to the equivalent dimension size defined. e.g A portrait image of width 1000, height 2000, treated with the ``max`` dimensions ``1000x500`` (landscape) would result in the image shrunk so the *height* was 500 pixels and the width 250. @@ -125,6 +129,10 @@ The available ``method`` s are: ``min`` (takes two dimensions) + .. code-block:: django + + {% image self.photo min-500x200 %} + **Cover** the given dimensions. This may result in an image slightly **larger** than the dimensions you specify. e.g A square image of width 2000, height 2000, treated with the ``min`` dimensions ``500x200`` (landscape) would have it's height and width changed to 500, i.e matching the width required, but greater than the height. @@ -132,27 +140,45 @@ The available ``method`` s are: ``width`` (takes one dimension) + .. code-block:: django + + {% image self.photo width-640 %} + Reduces the width of the image to the dimension specified. ``height`` (takes one dimension) + .. code-block:: django + + {% image self.photo height-480 %} + Resize the height of the image to the dimension specified.. ``fill`` (takes two dimensions) + .. code-block:: django + + {% image self.photo fill-200x200 %} + Resize and **crop** to fill the **exact** dimensions. This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200. **The crop always aligns on the centre of the image.** -.. Note:: - Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. + ``original`` + (takes no dimensions) + + .. code-block:: django + + {% image self.photo original %} + + Leaves the image at its original size - no resizing is performed. .. Note:: - Wagtail does not make the "original" version of an image explicitly available. To request it, you could rely on the lack of upscaling by requesting an image larger than its maximum dimensions. e.g to insert an image whose dimensions are unknown at its maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. + Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. .. _image_tag_alt: @@ -190,6 +216,32 @@ Only fields using ``RichTextField`` need this applied in the template. .. Note:: Note that the template tag loaded differs from the name of the filter. +Responsive Embeds +----------------- + +Wagtail embeds and images are included at their full width, which may overflow the bounds of the content container you've defined in your templates. To make images and embeds responsive -- meaning they'll resize to fit their container -- include the following CSS. + +.. code-block:: css + + .rich-text img { + max-width: 100%; + height: auto; + } + + .responsive-object { + position: relative; + } + .responsive-object iframe, + .responsive-object object, + .responsive-object embed { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + Internal links (tag) ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/editing_api.rst b/docs/editing_api.rst index c06370026..88fc9458f 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -371,8 +371,8 @@ Edit Handler API ~~~~~~~~~~~~~~~~ -Hooks ------ +Admin Hooks +----------- On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a ``Page`` object is saved or when the main menu is constructed. @@ -547,6 +547,38 @@ Where ``'hook'`` is one of the following hook strings and ``function`` is a func hooks.register('insert_editor_css', editor_css) +Image Formats in the Rich Text Editor +------------------------------------- + +On loading, Wagtail will search for any app with the file ``image_formats.py`` and execute the contents. This provides a way to customize the formatting options shown to the editor when inserting images in the ``RichTextField`` editor. + +As an example, add a "thumbnail" format: + +.. code-block:: python + # image_formats.py + from wagtail.wagtailimages.formats import Format, register_image_format + + register_image_format(Format('thumbnail', 'Thumbnail', 'richtext-image thumbnail', 'max-120x120')) + + +To begin, import the the ``Format`` class, ``register_image_format`` function, and optionally ``unregister_image_format`` function. To register a new ``Format``, call the ``register_image_format`` with the ``Format`` object as the argument. The ``Format`` takes the following init arguments: + +``name`` + The unique key used to identify the format. To unregister this format, call ``unregister_image_format`` with this string as the only argument. + +``label`` + The label used in the chooser form when inserting the image into the ``RichTextField``. + +``classnames`` + The string to assign to the ``class`` attribute of the generated ```` tag. + +``filter_spec`` + The string specification to create the image rendition. For more, see the :ref:`image_tag`. + + +To unregister, call ``unregister_image_format`` with the string of the ``name`` of the ``Format`` as the only argument. + + Content Index Pages (CRUD) -------------------------- diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index 458de387b..de635256c 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -65,7 +65,7 @@ Wagtail instance available as the basis for your new site: - Install `Vagrant `_ 1.1+ - Clone the demonstration site, create the Vagrant box and initialise Wagtail:: - git clone git@github.com:torchbox/wagtaildemo.git + git clone https://github.com/torchbox/wagtaildemo.git cd wagtaildemo vagrant up vagrant ssh diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 642a5e741..3d8ea26b2 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -224,10 +224,13 @@ Prerequisites are the Elasticsearch service itself and, via pip, the `elasticuti .. code-block:: guess - pip install elasticutils pyelasticsearch + pip install elasticutils==0.8.2 pyelasticsearch .. note:: - The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. + ElasticUtils 0.9+ is not supported. + +.. note:: + The dependency on elasticutils and pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. The backend is configured in settings: diff --git a/requirements-dev.txt b/requirements-dev.txt index d35d6b087..4a9ff5c5e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ # For coverage and PEP8 linting coverage==3.7.1 flake8==2.1.0 +mock==1.0.1 diff --git a/runtests.py b/runtests.py index 6fd37ebeb..186052bcd 100755 --- a/runtests.py +++ b/runtests.py @@ -101,7 +101,9 @@ if not settings.configured: ), COMPRESS_ENABLED=False, # disable compression so that we can run tests on the content of the compress tag WAGTAILSEARCH_BACKENDS=WAGTAILSEARCH_BACKENDS, - WAGTAIL_SITE_NAME='Test Site' + WAGTAIL_SITE_NAME='Test Site', + LOGIN_REDIRECT_URL='wagtailadmin_home', + LOGIN_URL='wagtailadmin_login', ) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index e061a4bfe..f798129dc 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -294,3 +294,16 @@ class ZuluSnippet(models.Model): def __unicode__(self): return self.text + + +class StandardIndex(Page): + pass + +class StandardChild(Page): + pass + +class BusinessIndex(Page): + subpage_types = ['tests.BusinessChild'] + +class BusinessChild(Page): + subpage_types = [] diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index 6590e6dcc..0b13e5932 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,7 @@ +from django.test import TestCase from django.contrib.auth.models import User +from django.utils.six.moves.urllib.parse import urlparse, ParseResult +from django.http import QueryDict # We need to make sure that we're using the same unittest library that Django uses internally # Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors @@ -12,11 +15,30 @@ except ImportError: import unittest -def login(client): - # Create a user - user = User.objects.create_superuser(username='test', email='test@email.com', password='password') +class WagtailTestUtils(object): + def login(self): + # Create a user + user = User.objects.create_superuser(username='test', email='test@email.com', password='password') - # Login - client.login(username='test', password='password') + # Login + self.client.login(username='test', password='password') - return user + return user + + # From: https://github.com/django/django/blob/255449c1ee61c14778658caae8c430fa4d76afd6/django/contrib/auth/tests/test_views.py#L70-L85 + def assertURLEqual(self, url, expected, parse_qs=False): + """ + Given two URLs, make sure all their components (the ones given by + urlparse) are equal, only comparing components that are present in both + URLs. + If `parse_qs` is True, then the querystrings are parsed with QueryDict. + This is useful if you don't want the order of parameters to matter. + Otherwise, the query strings are compared as-is. + """ + fields = ParseResult._fields + + for attr, x, y in zip(fields, urlparse(url), urlparse(expected)): + if parse_qs and attr == 'query': + x, y = QueryDict(x), QueryDict(y) + if x and y and x != y: + self.fail("%r != %r (%s doesn't match)" % (url, expected, attr)) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js b/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js index 9aa90f857..bba601468 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/modal-workflow.js @@ -16,9 +16,12 @@ function ModalWorkflow(opts) { /* remove any previous modals before continuing (closing doesn't remove them from the dom) */ $('body > .modal').remove(); + // set default contents of container var container = $(''); + + // add container to body and hide it, so content can be added to it before display $('body').append(container); - container.modal(); + container.modal('hide'); self.body = container.find('.modal-body'); @@ -49,15 +52,19 @@ function ModalWorkflow(opts) { self.loadResponseText = function(responseText) { var response = eval('(' + responseText + ')'); + self.loadBody(response); }; - self.loadBody = function(body) { - if (body.html) { - self.body.html(body.html); + self.loadBody = function(response) { + if (response.html) { + // if the response is html + self.body.html(response.html); + container.modal('show'); } - if (body.onload) { - body.onload(self); + if (response.onload) { + // if the response is a function + response.onload(self); } }; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js new file mode 100644 index 000000000..36141c4fc --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/bootstrap-transition.js @@ -0,0 +1,65 @@ +/* ======================================================================== + * Bootstrap: transition.js v3.1.1 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function () { 'use strict'; + + (function (o_o) { + typeof define == 'function' && define.amd ? define(['jquery'], o_o) : + typeof exports == 'object' ? o_o(require('jquery')) : o_o(jQuery) + })(function ($) { + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + + }) + +}(); \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js index 7ba3ff2c0..af5950fdb 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/vendor/hallo.js @@ -1,671 +1,607 @@ -// Generated by CoffeeScript 1.3.3 -(function() { - - $.widget("ncri.hallohtml", { - options: { - editable: null, +/* Hallo 1.0.4 - rich text editor for jQuery UI +* by Henri Bergius and contributors. Available under the MIT license. +* See http://hallojs.org for more information +*/(function() { + (function(jQuery) { + return jQuery.widget('IKS.hallo', { toolbar: null, - uuid: "", - lang: 'en', - dialogOpts: { - autoOpen: false, - width: 600, - height: 'auto', - modal: false, - resizable: true, - draggable: true, - dialogClass: 'htmledit-dialog' + bound: false, + originalContent: '', + previousContent: '', + uuid: '', + selection: null, + _keepActivated: false, + originalHref: null, + options: { + editable: true, + plugins: {}, + toolbar: 'halloToolbarContextual', + parentElement: 'body', + buttonCssClass: null, + toolbarCssClass: null, + toolbarPositionAbove: false, + toolbarOptions: {}, + placeholder: '', + forceStructured: true, + checkTouch: true, + touchScreen: null }, - dialog: null, - buttonCssClass: null - }, - translations: { - en: { - title: 'Edit HTML', - update: 'Update' + _create: function() { + var options, plugin, _ref, + _this = this; + this.id = this._generateUUID(); + if (this.options.checkTouch && this.options.touchScreen === null) { + this.checkTouch(); + } + _ref = this.options.plugins; + for (plugin in _ref) { + options = _ref[plugin]; + if (!jQuery.isPlainObject(options)) { + options = {}; + } + jQuery.extend(options, { + editable: this, + uuid: this.id, + buttonCssClass: this.options.buttonCssClass + }); + jQuery(this.element)[plugin](options); + } + this.element.one('halloactivated', function() { + return _this._prepareToolbar(); + }); + return this.originalContent = this.getContents(); }, - de: { - title: 'HTML bearbeiten', - update: 'Aktualisieren' - } - }, - texts: null, - populateToolbar: function($toolbar) { - var $buttonHolder, $buttonset, id, widget; - widget = this; - this.texts = this.translations[this.options.lang]; - this.options.toolbar = $toolbar; - this.options.dialog = $("
").attr('id', "" + this.options.uuid + "-htmledit-dialog"); - $buttonset = $("").addClass(widget.widgetName); - id = "" + this.options.uuid + "-htmledit"; - $buttonHolder = $(''); - $buttonHolder.hallobutton({ - label: this.texts.title, - icon: 'icon-list-alt', - editable: this.options.editable, - command: null, - queryState: false, - uuid: this.options.uuid, - cssClass: this.options.buttonCssClass - }); - $buttonset.append($buttonHolder); - this.button = $buttonHolder; - this.button.click(function() { - if (widget.options.dialog.dialog("isOpen")) { - widget._closeDialog(); + _init: function() { + if (this.options.editable) { + return this.enable(); } else { - widget._openDialog(); + return this.disable(); + } + }, + destroy: function() { + var options, plugin, _ref; + this.disable(); + if (this.toolbar) { + this.toolbar.remove(); + this.element[this.options.toolbar]('destroy'); + } + _ref = this.options.plugins; + for (plugin in _ref) { + options = _ref[plugin]; + jQuery(this.element)[plugin]('destroy'); + } + return jQuery.Widget.prototype.destroy.call(this); + }, + disable: function() { + var _this = this; + this.element.attr("contentEditable", false); + this.element.off("focus", this._activated); + this.element.off("blur", this._deactivated); + this.element.off("keyup paste change", this._checkModified); + this.element.off("keyup", this._keys); + this.element.off("keyup mouseup", this._checkSelection); + this.bound = false; + jQuery(this.element).removeClass('isModified'); + jQuery(this.element).removeClass('inEditMode'); + this.element.parents('a').addBack().each(function(idx, elem) { + var element; + element = jQuery(elem); + if (!element.is('a')) { + return; + } + if (!_this.originalHref) { + return; + } + return element.attr('href', _this.originalHref); + }); + return this._trigger("disabled", null); + }, + enable: function() { + var _this = this; + this.element.parents('a[href]').addBack().each(function(idx, elem) { + var element; + element = jQuery(elem); + if (!element.is('a[href]')) { + return; + } + _this.originalHref = element.attr('href'); + return element.removeAttr('href'); + }); + this.element.attr("contentEditable", true); + if (!jQuery.parseHTML(this.element.html())) { + this.element.html(this.options.placeholder); + jQuery(this.element).addClass('inPlaceholderMode'); + this.element.css({ + 'min-width': this.element.innerWidth(), + 'min-height': this.element.innerHeight() + }); + } + if (!this.bound) { + this.element.on("focus", this, this._activated); + this.element.on("blur", this, this._deactivated); + this.element.on("keyup paste change", this, this._checkModified); + this.element.on("keyup", this, this._keys); + this.element.on("keyup mouseup", this, this._checkSelection); + this.bound = true; + } + if (this.options.forceStructured) { + this._forceStructured(); + } + return this._trigger("enabled", null); + }, + activate: function() { + return this.element.focus(); + }, + containsSelection: function() { + var range; + range = this.getSelection(); + return this.element.has(range.startContainer).length > 0; + }, + getSelection: function() { + var range, sel; + sel = rangy.getSelection(); + range = null; + if (sel.rangeCount > 0) { + range = sel.getRangeAt(0); + } else { + range = rangy.createRange(); + } + return range; + }, + restoreSelection: function(range) { + var sel; + sel = rangy.getSelection(); + return sel.setSingleRange(range); + }, + replaceSelection: function(cb) { + var newTextNode, r, range, sel, t; + if (navigator.appName === 'Microsoft Internet Explorer') { + t = document.selection.createRange().text; + r = document.selection.createRange(); + return r.pasteHTML(cb(t)); + } else { + sel = window.getSelection(); + range = sel.getRangeAt(0); + newTextNode = document.createTextNode(cb(range.extractContents())); + range.insertNode(newTextNode); + range.setStartAfter(newTextNode); + sel.removeAllRanges(); + return sel.addRange(range); + } + }, + removeAllSelections: function() { + if (navigator.appName === 'Microsoft Internet Explorer') { + return range.empty(); + } else { + return window.getSelection().removeAllRanges(); + } + }, + getPluginInstance: function(plugin) { + var instance; + instance = jQuery(this.element).data("IKS-" + plugin); + if (instance) { + return instance; + } + instance = jQuery(this.element).data(plugin); + if (instance) { + return instance; + } + throw new Error("Plugin " + plugin + " not found"); + }, + getContents: function() { + var cleanup, instance, plugin; + for (plugin in this.options.plugins) { + instance = this.getPluginInstance(plugin); + if (!instance) { + continue; + } + cleanup = instance.cleanupContentClone; + if (!jQuery.isFunction(cleanup)) { + continue; + } + jQuery(this.element)[plugin]('cleanupContentClone', this.element); + } + return this.element.html(); + }, + setContents: function(contents) { + return this.element.html(contents); + }, + isModified: function() { + if (!this.previousContent) { + this.previousContent = this.originalContent; + } + return this.previousContent !== this.getContents(); + }, + setUnmodified: function() { + jQuery(this.element).removeClass('isModified'); + return this.previousContent = this.getContents(); + }, + setModified: function() { + jQuery(this.element).addClass('isModified'); + return this._trigger('modified', null, { + editable: this, + content: this.getContents() + }); + }, + restoreOriginalContent: function() { + return this.element.html(this.originalContent); + }, + execute: function(command, value) { + if (document.execCommand(command, false, value)) { + return this.element.trigger("change"); + } + }, + protectFocusFrom: function(el) { + var _this = this; + return el.on("mousedown", function(event) { + event.preventDefault(); + _this._protectToolbarFocus = true; + return setTimeout(function() { + return _this._protectToolbarFocus = false; + }, 300); + }); + }, + keepActivated: function(_keepActivated) { + this._keepActivated = _keepActivated; + }, + _generateUUID: function() { + var S4; + S4 = function() { + return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1); + }; + return "" + (S4()) + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + "-" + (S4()) + (S4()) + (S4()); + }, + _prepareToolbar: function() { + var defaults, instance, plugin, populate, toolbarOptions; + this.toolbar = jQuery('
').hide(); + if (this.options.toolbarCssClass) { + this.toolbar.addClass(this.options.toolbarCssClass); + } + defaults = { + editable: this, + parentElement: this.options.parentElement, + toolbar: this.toolbar, + positionAbove: this.options.toolbarPositionAbove + }; + toolbarOptions = jQuery.extend({}, defaults, this.options.toolbarOptions); + this.element[this.options.toolbar](toolbarOptions); + for (plugin in this.options.plugins) { + instance = this.getPluginInstance(plugin); + if (!instance) { + continue; + } + populate = instance.populateToolbar; + if (!jQuery.isFunction(populate)) { + continue; + } + this.element[plugin]('populateToolbar', this.toolbar); + } + this.element[this.options.toolbar]('setPosition'); + return this.protectFocusFrom(this.toolbar); + }, + changeToolbar: function(element, toolbar, hide) { + var originalToolbar; + if (hide == null) { + hide = false; + } + originalToolbar = this.options.toolbar; + this.options.parentElement = element; + if (toolbar) { + this.options.toolbar = toolbar; + } + if (!this.toolbar) { + return; + } + this.element[originalToolbar]('destroy'); + this.toolbar.remove(); + this._prepareToolbar(); + if (hide) { + return this.toolbar.hide(); + } + }, + _checkModified: function(event) { + var widget; + widget = event.data; + if (widget.isModified()) { + return widget.setModified(); + } + }, + _keys: function(event) { + var old, widget; + widget = event.data; + if (event.keyCode === 27) { + old = widget.getContents(); + widget.restoreOriginalContent(event); + widget._trigger("restored", null, { + editable: widget, + content: widget.getContents(), + thrown: old + }); + return widget.turnOff(); + } + }, + _rangesEqual: function(r1, r2) { + if (r1.startContainer !== r2.startContainer) { + return false; + } + if (r1.startOffset !== r2.startOffset) { + return false; + } + if (r1.endContainer !== r2.endContainer) { + return false; + } + if (r1.endOffset !== r2.endOffset) { + return false; + } + return true; + }, + _checkSelection: function(event) { + var widget; + if (event.keyCode === 27) { + return; + } + widget = event.data; + return setTimeout(function() { + var sel; + sel = widget.getSelection(); + if (widget._isEmptySelection(sel) || widget._isEmptyRange(sel)) { + if (widget.selection) { + widget.selection = null; + widget._trigger("unselected", null, { + editable: widget, + originalEvent: event + }); + } + return; + } + if (!widget.selection || !widget._rangesEqual(sel, widget.selection)) { + widget.selection = sel.cloneRange(); + return widget._trigger("selected", null, { + editable: widget, + selection: widget.selection, + ranges: [widget.selection], + originalEvent: event + }); + } + }, 0); + }, + _isEmptySelection: function(selection) { + if (selection.type === "Caret") { + return true; } return false; - }); - this.options.editable.element.on("hallodeactivated", function() { - return widget._closeDialog(); - }); - $toolbar.append($buttonset); - this.options.dialog.dialog(this.options.dialogOpts); - return this.options.dialog.dialog("option", "title", this.texts.title); - }, - _openDialog: function() { - var $editableEl, html, widget, xposition, yposition, - _this = this; - widget = this; - $editableEl = $(this.options.editable.element); - xposition = $editableEl.offset().left + $editableEl.outerWidth() + 10; - yposition = this.options.toolbar.offset().top - $(document).scrollTop(); - this.options.dialog.dialog("option", "position", [xposition, yposition]); - this.options.editable.keepActivated(true); - this.options.dialog.dialog("open"); - this.options.dialog.on('dialogclose', function() { - $('label', _this.button).removeClass('ui-state-active'); - _this.options.editable.element.focus(); - return _this.options.editable.keepActivated(false); - }); - this.options.dialog.html($("