From 1d07e208b0897af64f71755c974762bc7cd19ca0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=C5=81ukasz=20Rekucki?= Date: Thu, 8 Oct 2009 12:29:40 +0200 Subject: [PATCH] HTML View load&save --- apps/api/handlers/library_handlers.py | 99 +++++-- apps/api/urls.py | 7 +- apps/api/views.py | 40 +++ apps/explorer/models.py | 4 +- apps/wysiwyg/tests.py | 23 -- lib/wlrepo/__init__.py | 2 +- lib/wlrepo/mercurial_backend/document.py | 2 +- project/static/css/html.css | 52 ++-- project/static/css/master.css | 45 ++- project/static/js/editor.js | 2 +- project/static/js/editor.ui.js | 359 ----------------------- project/static/js/models.js | 123 +++++++- project/static/js/views/html.js | 161 +++++++--- project/static/js/views/xml.js | 5 +- project/templates/explorer/editor.html | 3 +- project/templates/wysiwyg.html | 7 +- 16 files changed, 435 insertions(+), 499 deletions(-) delete mode 100644 apps/wysiwyg/tests.py delete mode 100755 project/static/js/editor.ui.js diff --git a/apps/api/handlers/library_handlers.py b/apps/api/handlers/library_handlers.py index b7260674..4435fe96 100644 --- a/apps/api/handlers/library_handlers.py +++ b/apps/api/handlers/library_handlers.py @@ -13,10 +13,11 @@ from datetime import date from django.core.urlresolvers import reverse from django.utils import simplejson as json +from django.db import IntegrityError import librarian import librarian.html -from librarian import dcparser +from librarian import dcparser, parser from wlrepo import * from explorer.models import PullRequest, GalleryForDocument @@ -229,7 +230,12 @@ class DocumentHTMLHandler(BaseHandler): return response.BadRequest().django_response({'reason': 'name-mismatch', 'message': 'Provided revision refers, to document "%s", but provided "%s"' % (document.id, docid) }) - return librarian.html.transform(document.data('xml'), is_file=False, parse_dublincore=False) + return librarian.html.transform(document.data('xml'), is_file=False, \ + parse_dublincore=False, stylesheet="partial",\ + options={ + "with-paths": 'boolean(1)', + }) + except (EntryNotFound, RevisionNotFound), e: return response.EntityNotFound().django_response({ 'reason': 'not-found', 'message': e.message}) @@ -287,6 +293,7 @@ XINCLUDE_REGEXP = r"""<(?:\w+:)?include\s+[^>]*?href=("|')wlrepo://(?P[^\1 # # # + class DocumentTextHandler(BaseHandler): allowed_methods = ('GET', 'POST') @@ -294,6 +301,8 @@ class DocumentTextHandler(BaseHandler): def read(self, request, docid, lib): """Read document as raw text""" revision = request.GET.get('revision', 'latest') + part = request.GET.get('part', False) + try: if revision == 'latest': document = lib.document(docid) @@ -305,22 +314,35 @@ class DocumentTextHandler(BaseHandler): 'message': 'Provided revision is not valid for this document'}) # TODO: some finer-grained access control - return document.data('xml') + if part is False: + # we're done :) + return document.data('xml') + else: + xdoc = parser.WLDocument.from_string(document.data('xml')) + ptext = xdoc.part_as_text(part) + + if ptext is None: + return response.EntityNotFound().django_response({ + 'reason': 'no-part-in-document' + }) + + return ptext + except librarian.ParseError: + return response.EntityNotFound().django_response({ + 'reason': 'invalid-document-state', + 'exception': type(e), 'message': e.message + }) except (EntryNotFound, RevisionNotFound), e: return response.EntityNotFound().django_response({ - 'exception': type(e), 'message': e.message}) + 'reason': 'not-found', + 'exception': type(e), 'message': e.message + }) @hglibrary def create(self, request, docid, lib): try: - data = request.POST['contents'] revision = request.POST['revision'] - if request.POST.has_key('message'): - msg = u"$USER$ " + request.POST['message'] - else: - msg = u"$AUTO$ XML content update." - current = lib.document(docid, request.user.username) orig = lib.document_for_rev(revision) @@ -330,6 +352,33 @@ class DocumentTextHandler(BaseHandler): "provided_revision": orig.revision, "latest_revision": current.revision }) + if request.POST.has_key('message'): + msg = u"$USER$ " + request.POST['message'] + else: + msg = u"$AUTO$ XML content update." + + if request.POST.has_key('contents'): + data = request.POST['contents'] + else: + if not request.POST.has_key('chunks'): + # bad request + return response.BadRequest().django_response({'reason': 'invalid-arguments', + 'message': 'No contents nor chunks specified.'}) + + # TODO: validate + parts = json.loads(request.POST['chunks']) + xdoc = parser.WLDocument.from_string(current.data('xml')) + + errors = xdoc.merge_chunks(parts) + + if len(errors): + return response.EntityConflict().django_response({ + "reason": "invalid-chunks", + "message": "Unable to merge following parts into the document: %s " % ",".join(errors) + }) + + data = xdoc.serialize() + # try to find any Xinclude tags includes = [m.groupdict()['link'] for m in (re.finditer(\ XINCLUDE_REGEXP, data, flags=re.UNICODE) or []) ] @@ -498,21 +547,25 @@ class MergeHandler(BaseHandler): "provided": target_rev, "latest": udoc.revision }) - if not request.user.has_perm('explorer.book.can_share'): + if not request.user.has_perm('explorer.document.can_share'): # User is not permitted to make a merge, right away # So we instead create a pull request in the database - prq = PullRequest( - comitter=request.user, - document=docid, - source_revision = str(udoc.revision), - status="N", - comment = form.cleaned_data['message'] or '$AUTO$ Document shared.' - ) - - prq.save() - return response.RequestAccepted().django_response(\ - ticket_status=prq.status, \ - ticket_uri=reverse("pullrequest_view", args=[prq.id]) ) + try: + prq, created = PullRequest.get_or_create( + source_revision = str(udoc.revision), + defaults = { + 'comitter': request.user, + 'document': docid, + 'status': "N", + 'comment': form.cleaned_data['message'] or '$AUTO$ Document shared.', + } + ) + + return response.RequestAccepted().django_response(\ + ticket_status=prq.status, \ + ticket_uri=reverse("pullrequest_view", args=[prq.id]) ) + except IntegrityError, e: + return response.InternalError().django_response() if form.cleaned_data['type'] == 'update': # update is always performed from the file branch diff --git a/apps/api/urls.py b/apps/api/urls.py index 0ae14203..deac319a 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -16,6 +16,9 @@ urlpatterns = patterns('', # url(r'^hello$', hello_resource, {'emitter_format': 'json'}), # url(r'^hello\.(?P.+)$', hello_resource), + # HTML Renderer service + url(r'^render$', 'api.views.render'), + # Toolbar url(r'^toolbar/buttons$', toolbar_buttons, {'emitter_format': 'json'}, name="toolbar_buttons" @@ -52,12 +55,12 @@ urlpatterns = patterns('', # XML url(urlpath(r'documents', DOC, 'text', format=False), - document_text_resource, {'emitter_format': 'rawxml'}, + document_text_resource, {'emitter_format': 'raw'}, name="doctext_view"), # HTML url(urlpath(r'documents', DOC, 'html', format=False), - document_html_resource, {'emitter_format': 'rawhtml'}, + document_html_resource, {'emitter_format': 'raw'}, name="dochtml_view"), # DC diff --git a/apps/api/views.py b/apps/api/views.py index 60f00ef0..b96fc470 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -1 +1,41 @@ # Create your views here. + +from django.http import HttpResponse +from librarian import html +from lxml import etree +from StringIO import StringIO +import re + +LINE_SWAP_EXPR = re.compile(r'/\s', re.MULTILINE | re.UNICODE); + +def render(request): + style_filename = html.get_stylesheet('partial') + + data = request.POST['fragment'] + path = request.POST['part'] + + base, me = path.rsplit('/', 1) + match = re.match(r'([^\[]+)\[(\d+)\]', me) + tag, pos = match.groups() + + print "Redner:", path, base, tag, pos + + style = etree.parse(style_filename) + + data = LINE_SWAP_EXPR.sub(u'
\n', data) + doc = etree.parse( StringIO(data) ) + + opts = { + 'with-paths': 'boolean(1)', + 'base-path': "'%s'" % base, + 'base-offset': pos, + } + + print opts + + result = doc.xslt(style, **opts) + + print result + + return HttpResponse( + etree.tostring(result, encoding=unicode, pretty_print=True) ) \ No newline at end of file diff --git a/apps/explorer/models.py b/apps/explorer/models.py index 7ab6b095..23acd7f1 100644 --- a/apps/explorer/models.py +++ b/apps/explorer/models.py @@ -55,12 +55,12 @@ class EditorPanel(models.Model): def __unicode__(self): return self.display_name -class Book(models.Model): +class Document(models.Model): class Meta: permissions = ( ("can_share", "Can share documents without pull requests."), ) - abstract=True + pass class PullRequest(models.Model): diff --git a/apps/wysiwyg/tests.py b/apps/wysiwyg/tests.py deleted file mode 100644 index 2247054b..00000000 --- a/apps/wysiwyg/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} - diff --git a/lib/wlrepo/__init__.py b/lib/wlrepo/__init__.py index ab6f319f..430e59f9 100644 --- a/lib/wlrepo/__init__.py +++ b/lib/wlrepo/__init__.py @@ -51,7 +51,7 @@ class Document(object): Should be called on the user version of document. If not, it doesn nothing.""" def data(self, entry): - """Returns the specified entry as a file-like object.""" + """Returns the specified entry as a unicode data.""" pass @property diff --git a/lib/wlrepo/mercurial_backend/document.py b/lib/wlrepo/mercurial_backend/document.py index a579fb75..e7145616 100644 --- a/lib/wlrepo/mercurial_backend/document.py +++ b/lib/wlrepo/mercurial_backend/document.py @@ -189,7 +189,7 @@ class MercurialDocument(wlrepo.Document): return False if changed: - local = local.latest() + local = self.latest()._revision success, changed = main.merge_with(local, user=user,\ message=message) diff --git a/project/static/css/html.css b/project/static/css/html.css index 4904a0d6..336a365e 100644 --- a/project/static/css/html.css +++ b/project/static/css/html.css @@ -3,7 +3,7 @@ font-size: 16px; font: Georgia, "Times New Roman", serif; line-height: 1.5em; - padding: 3em; + padding: 3em; } .htmlview div { @@ -100,14 +100,14 @@ /* = Numbering = */ /* ============= */ .htmlview .anchor { - position: absolute; - margin: -0.25em -0.5em; - left: 1em; + position: relative; + margin: 0em; + left: -2.2em; color: #777; font-size: 12px; width: 2em; text-align: center; - padding: 0.25em 0.5em; + padding: 0.25em 0.7em; line-height: 1.5em; } @@ -119,45 +119,45 @@ /* =================== */ /* = Custom elements = */ /* =================== */ -.htmlview span.author { +.htmlview .autor_utwor { font-size: 0.5em; display: block; line-height: 1.5em; margin-bottom: 0.25em; } -.htmlview span.collection { +.htmlview .dzielo_nadrzedne { font-size: 0.375em; display: block; line-height: 1.5em; margin-bottom: -0.25em; } -.htmlview span.subtitle { +.htmlview .podtytul { font-size: 0.5em; display: block; line-height: 1.5em; margin-top: -0.25em; } -.htmlview div.didaskalia { +.htmlview .didaskalia { font-style: italic; margin: 0.5em 0 0 1.5em; } -.htmlview div.kwestia { +.htmlview .kwestia { margin: 0.5em 0 0; } -.htmlview div.stanza { +.htmlview .strofa { margin: 1.5em 0 0; } -.htmlview div.kwestia div.stanza { +.htmlview .kwestia .strofa { margin: 0; } -.htmlview p.paragraph { +.htmlview .akap, .htmlview .akap_cd, .htmlview .akap_dialog { text-align: justify; margin: 1.5em 0 0; } @@ -178,48 +178,50 @@ padding-bottom: 1.5em; } -.htmlview div.note p, .htmlview div.dedication p, -.htmlview div.note p.paragraph, .htmlview div.dedication p.paragraph { +.htmlview div.nota p, +.htmlview div.dedykacja p { text-align: right; font-style: italic; } -.htmlview hr.spacer { +.htmlview br.sekcja_swiatlo { height: 3em; - visibility: hidden; + /* visibility: hidden; */ } -.htmlview hr.spacer-line { +.htmlview hr.separator_linia { margin: 1.5em 0; border: none; border-bottom: 0.1em solid #000; } -.htmlview p.spacer-asterisk { +.htmlview p.sekcja_asterysk { padding: 0; margin: 1.5em 0; text-align: center; } -.htmlview div.person-list ol { +.htmlview div.lista_osob ol { list-style: none; padding: 0 0 0 1.5em; } -.htmlview p.place-and-time { +.htmlview p.miejsce_czas { font-style: italic; } -.htmlview em.math, .htmlview em.foreign-word, -.htmlview em.book-title, .htmlview em.didaskalia { +.htmlview .mat, +.htmlview .slowo_obce, +.htmlview .tytul_dziela, +.htmlview .didaskalia { font-style: italic; } -.htmlview em.author-emphasis { +.htmlview .wyroznienie { letter-spacing: 0.1em; } -.htmlview em.person { +.htmlview .osoba { font-style: normal; font-variant: small-caps; } diff --git a/project/static/css/master.css b/project/static/css/master.css index 653b8640..d9309094 100644 --- a/project/static/css/master.css +++ b/project/static/css/master.css @@ -343,10 +343,53 @@ body#base button { border-width: 1px; padding: 0px 0.5em; font-family: Sans-Serif; - color: #000; + /* color: #000; */ margin: 2px 4px; } body#base button:hover { background-color: #EEE; +} + +/* HTML editor interactive elements */ + +.html-editarea { + border: 2px solid black; + background-color: gray; + padding: 1px; + + z-index: 2000; +} + +.html-editarea textarea +{ + + border: 0px; + margin: 0px; + padding: 0px; + + width: 100%; + height: 100%; + + z-index: 0; + font-size: 10pt; + background-color: ivory; +} + +.html-editarea p.html-editarea-toolbar { + position: absolute; + background: gray; + + bottom: -26px; + height: 24px; + + left: 0px; + right: 0px; + + border: 2px solid black; + + margin: 0px; + padding: 0px; + + z-index: 100; } \ No newline at end of file diff --git a/project/static/js/editor.js b/project/static/js/editor.js index 6918f9e5..f52950ff 100644 --- a/project/static/js/editor.js +++ b/project/static/js/editor.js @@ -574,5 +574,5 @@ $(function() { // do the layout editor.loadConfig(); - editor.setupUI(); + editor.setupUI(); }); diff --git a/project/static/js/editor.ui.js b/project/static/js/editor.ui.js deleted file mode 100755 index c8d47f66..00000000 --- a/project/static/js/editor.ui.js +++ /dev/null @@ -1,359 +0,0 @@ -/* - * UI related Editor methods - */ -Editor.prototype.setupUI = function() { -// // set up the UI visually and attach callbacks - var self = this; -// -// var resize_start = function(event, mydata) { -// $(document).bind('mousemove', mydata, resize_changed). -// bind('mouseup', mydata, resize_stop); -// -// $('.panel-overlay', mydata.root).css('display', 'block'); -// return false; -// } -// var resize_changed = function(event) { -// var old_width = parseInt(event.data.overlay.css('width')); -// var delta = event.pageX + event.data.hotspot_x - old_width; -// -// if(old_width + delta < 12) delta = 12 - old_width; -// if(old_width + delta > $(window).width()) -// delta = $(window).width() - old_width; -// -// event.data.overlay.css({ -// 'width': old_width + delta -// }); -// -// if(event.data.overlay.next) { -// var left = parseInt(event.data.overlay.next.css('left')); -// event.data.overlay.next.css('left', left+delta); -// } -// -// return false; -// }; -// -// var resize_stop = function(event) { -// $(document).unbind('mousemove', resize_changed).unbind('mouseup', resize_stop); -// // $('.panel-content', event.data.root).css('display', 'block'); -// var overlays = $('.panel-content-overlay', event.data.root); -// $('.panel-content-overlay', event.data.root).each(function(i) { -// if( $(this).data('panel').hasClass('last-panel') ) -// $(this).data('panel').css({ -// 'left': $(this).css('left'), -// 'right': $(this).css('right') -// }); -// else -// $(this).data('panel').css({ -// 'left': $(this).css('left'), -// 'width': $(this).css('width') -// }); -// }); -// $('.panel-overlay', event.data.root).css('display', 'none'); -// $(event.data.root).trigger('stopResize'); -// }; -// -// /* -// * Prepare panels (overlays & stuff) -// */ -// /* create an overlay */ -// var panel_root = self.rootDiv; -// var overlay_root = $("
"); -// panel_root.append(overlay_root); -// -// var prev = null; -// -// $('*.panel-wrap', panel_root).each( function() -// { -// var panel = $(this); -// var handle = $('.panel-slider', panel); -// var overlay = $("
 
"); -// overlay_root.append(overlay); -// overlay.data('panel', panel); -// overlay.data('next', null); -// -// if (prev) prev.next = overlay; -// -// if( panel.hasClass('last-panel') ) -// { -// overlay.css({ -// 'left': panel.css('left'), -// 'right': panel.css('right') -// }); -// } -// else { -// overlay.css({ -// 'left': panel.css('left'), -// 'width': panel.css('width') -// }); -// // $.log('Has handle: ' + panel.attr('id')); -// overlay.append(handle.clone()); -// /* attach the trigger */ -// handle.mousedown(function(event) { -// var touch_data = { -// root: panel_root, -// overlay: overlay, -// hotspot_x: event.pageX - handle.position().left -// }; -// -// $(this).trigger('hpanel:panel-resize-start', touch_data); -// return false; -// }); -// $('.panel-content', panel).css('right', -// (handle.outerWidth() || 10) + 'px'); -// $('.panel-content-overlay', panel).css('right', -// (handle.outerWidth() || 10) + 'px'); -// }; -// -// prev = overlay; -// }); -// -// panel_root.bind('hpanel:panel-resize-start', resize_start); -// self.rootDiv.bind('stopResize', function() { -// self.savePanelOptions(); -// }); -// - /* - * Connect panel actions - */ - $('#panels > *.panel-wrap').each(function() { - var panelWrap = $(this); - // $.log('wrap: ', panelWrap); - var panel = new Panel(panelWrap); - panelWrap.data('ctrl', panel); // attach controllers to wraps - panel.load($('.panel-toolbar select', panelWrap).val()); - - $('.panel-toolbar select', panelWrap).change(function() { - var url = $(this).val(); - panelWrap.data('ctrl').load(url); - self.savePanelOptions(); - }); - - $('.panel-toolbar button.refresh-button', panelWrap).click( - function() { - panel.refresh(); - } ); - - self.rootDiv.bind('stopResize', function() { - panel.callHook('toolbarResized'); - }); - }); - - $(document).bind('panel:contentChanged', function() { - self.onContentChanged.apply(self, arguments) - }); - - /* - * Connect various buttons - */ - - $('#toolbar-button-quick-save').click( function (event, data) { - self.saveToBranch(); - } ); - - $('#toolbar-button-save').click( function (event, data) { - $('#commit-dialog').jqmShow( {callback: $.fbind(self, self.saveToBranch)} ); - } ); - - $('#toolbar-button-update').click( function (event, data) { - if (self.updateUserBranch()) { - // commit/update can be called only after proper, save - // this means all panels are clean, and will get refreshed - // do this only, when there are any changes to local branch - self.refreshPanels(); - } - } ); - - /* COMMIT DIALOG */ - $('#commit-dialog'). - jqm({ - modal: true, - onShow: $.fbind(self, self.loadRelatedIssues) - }); - - $('#toolbar-button-commit').click( function (event, data) { - $('#commit-dialog').jqmShow( {callback: $.fbind(self, self.sendMergeRequest)} ); - } ); - - /* STATIC BINDS */ - $('#commit-dialog-cancel-button').click(function() { - $('#commit-dialog-error-empty-message').hide(); - $('#commit-dialog').jqmHide(); - }); - - - /* SPLIT DIALOG */ - $('#split-dialog').jqm({ - modal: true, - onShow: $.fbind(self, self.loadSplitDialog) - }). - jqmAddClose('button.dialog-close-button'); -} - -Editor.prototype.loadRelatedIssues = function(hash) -{ - var self = this; - var c = $('#commit-dialog-related-issues'); - - $('#commit-dialog-save-button').click( function (event, data) - { - if( $('#commit-dialog-message').val().match(/^\s*$/)) { - $('#commit-dialog-error-empty-message').fadeIn(); - } - else { - $('#commit-dialog-error-empty-message').hide(); - $('#commit-dialog').jqmHide(); - - var message = $('#commit-dialog-message').val(); - $('#commit-dialog-related-issues input:checked'). - each(function() { message += ' refs #' + $(this).val(); }); - $.log("COMMIT APROVED", hash.t); - hash.t.callback(message); - } - - return false; - }); - - $("div.loading-box", c).show(); - $("div.fatal-error-box", c).hide(); - $("div.container-box", c).hide(); - - $.getJSON( c.attr('ui:ajax-src') + '?callback=?', - function(data, status) - { - var fmt = ''; - $(data).each( function() { - fmt += '\n' - }); - $("div.container-box", c).html(fmt); - $("div.loading-box", c).hide(); - $("div.container-box", c).show(); - }); - - hash.w.show(); -} - -Editor.prototype.loadSplitDialog = function(hash) -{ - var self = this; - - $("div.loading-box", hash.w).show(); - $("div.fatal-error-box", hash.w).hide(); - $('div.container-box', hash.w).hide(); - hash.w.show(); - - function onFailure(rq, tstat, err) { - $('div.container-box', hash.w).html(''); - $("div.loading-box", hash.w).hide(); - $("div.fatal-error-box", hash.w).show(); - hash.t.failure(); - }; - - function onSuccess(data, status) { - // put the form into the window - $('div.container-box', hash.w).html(data); - $("div.loading-box", hash.w).hide(); - $('form input[name=splitform-splittext]', hash.w).val(hash.t.selection); - $('form input[name=splitform-fulltext]', hash.w).val(hash.t.fulltext); - $('div.container-box', hash.w).show(); - - // connect buttons - $('#split-dialog-button-accept').click(function() { - self.postSplitRequest(onSuccess, onFailure); - return false; - }); - - $('#split-dialog-button-close').click(function() { - hash.w.jqmHide(); - $('div.container-box', hash.w).html(''); - hash.t.failure(); - }); - - $('#split-dialog-button-dismiss').click(function() { - hash.w.jqmHide(); - $('div.container-box', hash.w).html(''); - hash.t.success(); - }); - - /* if($('#id_splitform-autoxml').is(':checked')) - $('#split-form-dc-subform').show(); - else - $('#split-form-dc-subform').hide(); - - $('#id_splitform-autoxml').change(function() { - if( $(this).is(':checked') ) - $('#split-form-dc-subform').show(); - else - $('#split-form-dc-subform').hide(); - }); */ - }; - - $.ajax({ - url: 'split', - dataType: 'html', - success: onSuccess, - error: onFailure, - type: 'GET', - data: {} - }); -} - -/* Refreshing routine */ -Editor.prototype.refreshPanels = function() { - var self = this; - - self.allPanels().each(function() { - var panel = $(this).data('ctrl'); - $.log('Refreshing: ', this, panel); - if ( panel.changed() ) - panel.unmarkChanged(); - else - panel.refresh(); - }); - - $('button.provides-save').attr('disabled', 'disabled'); - $('button.requires-save').removeAttr('disabled'); -}; - -/* - * Pop-up messages - */ -Editor.prototype.showPopup = function(name, text, timeout) -{ - timeout = timeout || 4000; - var self = this; - self.popupQueue.push( [name, text, timeout] ) - - if( self.popupQueue.length > 1) - return; - - var box = $('#message-box > #' + name); - $('*.data', box).html(text || ''); - box.fadeIn(100); - - if(timeout > 0) - setTimeout( $.fbind(self, self.advancePopupQueue), timeout); -}; - -Editor.prototype.advancePopupQueue = function() { - var self = this; - var elem = this.popupQueue.shift(); - if(elem) { - var box = $('#message-box > #' + elem[0]); - - box.fadeOut(100, function() - { - $('*.data', box).html(''); - - if( self.popupQueue.length > 0) { - var ibox = $('#message-box > #' + self.popupQueue[0][0]); - $('*.data', ibox).html(self.popupQueue[0][1] || ''); - ibox.fadeIn(100); - if(self.popupQueue[0][2] > 0) - setTimeout( $.fbind(self, self.advancePopupQueue), self.popupQueue[0][2]); - } - }); - } -}; - - diff --git a/project/static/js/models.js b/project/static/js/models.js index 0b00c7ce..d183f992 100644 --- a/project/static/js/models.js +++ b/project/static/js/models.js @@ -147,22 +147,30 @@ Editor.XMLModel = Editor.Model.extend({ Editor.HTMLModel = Editor.Model.extend({ _className: 'Editor.HTMLModel', - serverURL: null, - data: '', + dataURL: null, + htmlURL: null, + renderURL: null, + displaData: '', + xmlParts: {}, state: 'empty', - init: function(serverURL, revision) { + init: function(htmlURL, revision, dataURL) { this._super(); this.set('state', 'empty'); this.set('revision', revision); - this.serverURL = serverURL; + this.htmlURL = htmlURL; + this.dataURL = dataURL; + this.renderURL = "http://localhost:8000/api/render"; + this.xmlParts = {}; }, load: function(force) { if (force || this.get('state') == 'empty') { this.set('state', 'loading'); + + // load the transformed data $.ajax({ - url: this.serverURL, + url: this.htmlURL, dataType: 'text', data: {revision: this.get('revision')}, success: this.loadingSucceeded.bind(this), @@ -187,6 +195,109 @@ Editor.HTMLModel = Editor.Model.extend({ this.set('state', 'error'); }, + getXMLPart: function(elem, callback) + { + var path = elem.attr('wl2o:path'); + if(!this.xmlParts[path]) + this.loadXMLPart(elem, callback); + else + callback(path, this.xmlParts[path]); + }, + + loadXMLPart: function(elem, callback) + { + var path = elem.attr('wl2o:path'); + var self = this; + + $.ajax({ + url: this.dataURL, + dataType: 'text; charset=utf-8', + data: { + revision: this.get('revision'), + part: path + }, + success: function(data) { + self.xmlParts[path] = data; + callback(path, data); + }, + // TODO: error handling + error: function(data) { + console.log('Failed to load fragment'); + callback(undefined, undefined); + } + }); + }, + + putXMLPart: function(elem, data) { + var self = this; + + var path = elem.attr('wl2o:path'); + this.xmlParts[path] = data; + + this.set('state', 'unsynced'); + + /* re-render the changed fragment */ + $.ajax({ + url: this.renderURL, + type: "POST", + dataType: 'text; charset=utf-8', + data: {fragment: data, part: path}, + success: function(htmldata) { + elem.replaceWith(htmldata); + self.set('state', 'dirty'); + } + }); + }, + + update: function(message) { + if (this.get('state') == 'dirty') { + this.set('state', 'updating'); + + var payload = { + chunks: $.toJSON(this.xmlParts), + revision: this.get('revision') + }; + + if (message) { + payload.message = message; + } + + console.log(payload) + + $.ajax({ + url: this.dataURL, + type: 'post', + dataType: 'json', + data: payload, + success: this.updatingSucceeded.bind(this), + error: this.updatingFailed.bind(this) + }); + return true; + } + return false; + + }, + + updatingSucceeded: function(data) { + if (this.get('state') != 'updating') { + alert('erroneous state:', this.get('state')); + } + + // flush the cache + this.xmlParts = {}; + + this.set('revision', data.revision); + this.set('state', 'updated'); + }, + + updatingFailed: function() { + if (this.get('state') != 'updating') { + alert('erroneous state:', this.get('state')); + } + messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się'); + this.set('state', 'dirty'); + }, + // For debbuging set: function(property, value) { if (property == 'state') { @@ -277,7 +388,7 @@ Editor.DocumentModel = Editor.Model.extend({ this.set('state', 'synced'); this.contentModels = { 'xml': new Editor.XMLModel(data.text_url, data.user_revision), - 'html': new Editor.HTMLModel(data.html_url, data.user_revision), + 'html': new Editor.HTMLModel(data.html_url, data.user_revision, data.text_url), 'gallery': new Editor.ImageGalleryModel(data.gallery_url) }; for (var key in this.contentModels) { diff --git a/project/static/js/views/html.js b/project/static/js/views/html.js index a8eb4d01..ebdf90af 100644 --- a/project/static/js/views/html.js +++ b/project/static/js/views/html.js @@ -1,59 +1,130 @@ /*global View render_template panels */ var HTMLView = View.extend({ - _className: 'HTMLView', - element: null, - model: null, - template: 'html-view-template', + _className: 'HTMLView', + element: null, + model: null, + template: 'html-view-template', - init: function(element, model, parent, template) { - this._super(element, model, template); - this.parent = parent; + init: function(element, model, parent, template) { + this._super(element, model, template); + this.parent = parent; - this.model - .addObserver(this, 'data', this.modelDataChanged.bind(this)) - .addObserver(this, 'state', this.modelStateChanged.bind(this)); + this.model + .addObserver(this, 'data', this.modelDataChanged.bind(this)) + .addObserver(this, 'state', this.modelStateChanged.bind(this)); - $('.htmlview', this.element).html(this.model.get('data')); - this.modelStateChanged('state', this.model.get('state')); - this.model.load(); - }, - - modelDataChanged: function(property, value) { - $('.htmlview', this.element).html(value); + $('.htmlview', this.element).html(this.model.get('data')); + this.modelStateChanged('state', this.model.get('state')); + this.model.load(); + }, + + modelDataChanged: function(property, value) { + $('.htmlview', this.element).html(value); - var base = this.$printLink.attr('ui:baseref'); - this.$printLink.attr('href', base + "?revision=" + this.model.get('revision')); - }, + var base = this.$printLink.attr('ui:baseref'); + this.$printLink.attr('href', base + "?revision=" + this.model.get('revision')); + }, - modelStateChanged: function(property, value) { - if (value == 'synced' || value == 'dirty') { - this.unfreeze(); - } else if (value == 'unsynced') { - this.freeze('Niezsynchronizowany...'); - } else if (value == 'loading') { - this.freeze('Ładowanie...'); - } else if (value == 'saving') { - this.freeze('Zapisywanie...'); - } else if (value == 'error') { - this.freeze(this.model.get('error')); - } - }, + modelStateChanged: function(property, value) { + if (value == 'synced' || value == 'dirty') { + this.unfreeze(); + } else if (value == 'unsynced') { + this.freeze('Niezsynchronizowany...'); + } else if (value == 'loading') { + this.freeze('Ładowanie...'); + } else if (value == 'saving') { + this.freeze('Zapisywanie...'); + } else if (value == 'error') { + this.freeze(this.model.get('error')); + } + }, + render: function() { + this.element.unbind('click'); - render: function() { - if(this.$printLink) this.$printLink.unbind(); - this._super(); - this.$printLink = $('.html-print-link', this.element); - }, + if(this.$printLink) this.$printLink.unbind(); + this._super(); + this.$printLink = $('.html-print-link', this.element); + + this.element.bind('click', this.itemClicked.bind(this)); + }, + + reload: function() { + this.model.load(true); + }, - reload: function() { - this.model.load(true); - }, + dispose: function() { + this.model.removeObserver(this); + this._super(); + }, + + itemClicked: function(event) + { + var self = this; + + console.log('click:', event, event.ctrlKey, event.target); + var editableContent = null; + var $e = $(event.target); + + var n = 0; + + while( ($e[0] != this.element[0]) && !($e.attr('wl2o:editable')) + && n < 50) + { + // console.log($e, $e.parent(), this.element); + $e = $e.parent(); + n += 1; + } + + if(!$e.attr('wl2o:editable')) + return true; + + // start edition on this node + + + var $overlay = $( + '
\n\ +

\n\ + \n\ + \n\ +

\n\ + \n\ +
'); + + var x = $e[0].offsetLeft; + var y = $e[0].offsetTop; + var w = $e.outerWidth(); + var h = $e.innerHeight(); + $overlay.css({position: 'absolute', height: h, left: "5%", top: y, width: "90%"}); + $e.offsetParent().append($overlay); + + // load the original XML content + console.log($e, $e.offsetParent(), $overlay); + + $('.html-editarea-cancel-button', $overlay).click(function() { + $overlay.remove(); + }); + + $('.html-editarea-save-button', $overlay).click(function() { + $overlay.remove(); + + // put the part back to the model + self.model.putXMLPart($e, $('textarea', $overlay).val()); + }); + + $('textarea', $overlay).focus(function() { + $overlay.css('z-index', 3000); + }).blur(function() { + $overlay.css('z-index', 2000); + }); + + this.model.getXMLPart($e, function(path, data) { + $('textarea', $overlay).val(data); + }); + + return false; + } - dispose: function() { - this.model.removeObserver(this); - this._super(); - } }); // Register view diff --git a/project/static/js/views/xml.js b/project/static/js/views/xml.js index 02154723..00547d19 100644 --- a/project/static/js/views/xml.js +++ b/project/static/js/views/xml.js @@ -102,7 +102,7 @@ var XMLView = View.extend({ || (code >= 65 && code <= 90)) ) return null; var ch = String.fromCharCode(code & 0xff).toLowerCase(); - console.log(ch.charCodeAt(0), '#', buttons); + /* # console.log(ch.charCodeAt(0), '#', buttons); */ var buttons = $('.buttontoolbarview-button[title='+ch+']', this.element); var mod = 0; @@ -129,8 +129,7 @@ var XMLView = View.extend({ }, isHotkey: function() { - console.log(arguments); - + /* console.log(arguments); */ if(this.getHotkey.apply(this, arguments)) return true; else diff --git a/project/templates/explorer/editor.html b/project/templates/explorer/editor.html index edb3bfcd..fb7b2520 100644 --- a/project/templates/explorer/editor.html +++ b/project/templates/explorer/editor.html @@ -13,6 +13,7 @@ {# Libraries #} + {# Scriptlets #} @@ -58,7 +59,7 @@
- +