diff --git a/djedi/__init__.py b/djedi/__init__.py index 1baba01f..80dab235 100644 --- a/djedi/__init__.py +++ b/djedi/__init__.py @@ -49,6 +49,7 @@ def configure(): "cio.plugins.txt.TextPlugin", "cio.plugins.md.MarkdownPlugin", "djedi.plugins.img.ImagePlugin", + "djedi.plugins.list.ListPlugin", ], "THEME": "darth", } diff --git a/djedi/admin/api.py b/djedi/admin/api.py index e15f4735..45be721b 100644 --- a/djedi/admin/api.py +++ b/djedi/admin/api.py @@ -4,12 +4,14 @@ from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.template.response import TemplateResponse from django.utils.http import urlunquote +from django.utils.safestring import mark_safe from django.views.decorators.cache import never_cache from django.views.decorators.clickjacking import xframe_options_exempt from django.views.decorators.csrf import csrf_exempt from django.views.generic import View import cio +from cio.node import Node from cio.plugins import plugins from cio.plugins.exceptions import UnknownPlugin from cio.utils.uri import URI @@ -173,11 +175,14 @@ def post(self, request, ext): try: plugin = plugins.get(ext) data, meta = self.get_post_data(request) - data = plugin.load(data) + uri = URI(ext=ext) + node = Node(uri=uri, content=data) + data = plugin.load_node(node) + node.content = data except UnknownPlugin: raise Http404 else: - content = plugin.render(data) + content = plugin.render_node(node, data) return self.render_to_response(content) @@ -187,12 +192,7 @@ class NodeEditor(JSONResponseMixin, DjediContextMixin, APIView): def get(self, request, uri): try: uri = self.decode_uri(uri) - uri = URI(uri) - plugin = plugins.resolve(uri) - plugin_context = self.get_context_data(uri=uri) - - if isinstance(plugin, DjediPlugin): - plugin_context = plugin.get_editor_context(**plugin_context) + plugin_context = self.get_plugin_context(request, uri) except UnknownPlugin: raise Http404 @@ -208,17 +208,29 @@ def post(self, request, uri): context = cio.load(node.uri) context["content"] = node.content + context.update(self.get_plugin_context(request, context["uri"])) if request.is_ajax(): return self.render_to_json(context) else: return self.render_plugin(request, context) + def get_plugin_context(self, request, uri): + uri = URI(uri) + plugin = plugins.resolve(uri) + + context = {"uri": uri, "plugin": uri.ext} + if isinstance(plugin, DjediPlugin): + context = plugin.get_editor_context(request, **context) + + context["uri"] = mark_safe(context["uri"]) + return context + def render_plugin(self, request, context): return TemplateResponse( request, [ - "djedi/plugins/%s/editor.html" % context["uri"].ext, + "djedi/plugins/%s/editor.html" % context["plugin"], "djedi/plugins/base/editor.html", ], self.get_context_data(**context), diff --git a/djedi/admin/mixins.py b/djedi/admin/mixins.py index 71c0af05..49523da5 100644 --- a/djedi/admin/mixins.py +++ b/djedi/admin/mixins.py @@ -1,9 +1,11 @@ import simplejson as json from django.conf import settings as django_settings from django.http import HttpResponse +from django.utils.safestring import mark_safe import djedi from cio.conf import settings +from cio.plugins import plugins # TODO: Switch simplejson to ujson or other? @@ -38,5 +40,6 @@ def get_context_data(self, **context): context["THEME"] = theme context["VERSION"] = djedi.__version__ + context["PLUGINS"] = mark_safe(json.dumps(list(plugins.plugins.keys()))) return context diff --git a/djedi/plugins/base.py b/djedi/plugins/base.py index d6796236..8657f234 100644 --- a/djedi/plugins/base.py +++ b/djedi/plugins/base.py @@ -2,8 +2,8 @@ class DjediPlugin(BasePlugin): - def get_editor_context(self, **kwargs): + def get_editor_context(self, request, **context): """ Returns custom context """ - return kwargs + return context diff --git a/djedi/plugins/form.py b/djedi/plugins/form.py index 4d5e2a84..ad605559 100644 --- a/djedi/plugins/form.py +++ b/djedi/plugins/form.py @@ -43,8 +43,9 @@ class FormsBasePlugin(DjediPlugin): def forms(self): return {} - def get_editor_context(self, **context): - context.update({"forms": {tab: form() for tab, form in self.forms.items()}}) + def get_editor_context(self, request, **context): + if not request.is_ajax(): + context.update({"forms": {tab: form() for tab, form in self.forms.items()}}) return context diff --git a/djedi/plugins/list.py b/djedi/plugins/list.py new file mode 100644 index 00000000..cbe3eb14 --- /dev/null +++ b/djedi/plugins/list.py @@ -0,0 +1,172 @@ +import json + +import cio +from cio.node import Node +from cio.plugins import plugins + +from .base import DjediPlugin + + +class ListPlugin(DjediPlugin): + ext = "list" + + def get_editor_context(self, request, **context): + uri = context["uri"] + plugin_ext = self.get_query_param(uri, "plugin") + if plugin_ext: + context["plugin"] = plugin_ext + plugin = plugins.get(plugin_ext) + if isinstance(plugin, DjediPlugin): + context.update(plugin.get_editor_context(request, **context)) + + return context + + def load(self, content): + if content: + try: + return json.loads(content) + except ValueError: + pass + return {"direction": "col", "children": []} + + def load_node(self, node): + list_data = self.load(node.content) + + # Root data + if self.is_leaf_list_node(node.uri): + return list_data + + # Child data + child_node, key = self.get_child_node(node.uri, list_data) + plugin = self.resolve_child_plugin(child_node.uri) + + return plugin.load_node(child_node) + + def render_node(self, node, data): + if not self.is_leaf_list_node(node.uri): + child_plugin = self.resolve_child_plugin(node.uri) + if child_plugin: + child_uri, _ = self.get_child_uri(node.uri) + return child_plugin.render_node(Node(uri=child_uri), data) + + return "".join(self.stream_node(node, data)) + + def stream_node(self, node, data): + yield '" + + def resolve_child_plugin(self, uri): + if self.is_nested(uri): + ext = self.ext + else: + ext = self.get_query_param(uri, "plugin") + + return plugins.get(ext or self.ext) + + def find_child(self, data, key): + if not key: + return None + + for child in data["children"]: + if child["key"] == key: + return child + + return None + + def get_child_node(self, uri, parent_data, default=None): + # TODO: modify uri or content instead of new Nodes? + child_uri, key = self.get_child_uri(uri) + child = self.find_child(parent_data, key) + content = child["data"] if child else default + return Node(uri=child_uri, content=content), key + + def get_query_param(self, uri, param): + value = (uri.query or {}).get(param) + return value[0] if value else "" + + def is_nested(self, uri): + return bool(self.get_query_param(uri, "key")) + + def get_child_key(self, uri): + key = self.get_query_param(uri, "key") + if not key: + return None, None + + key, _, rest = key.partition("_") + return key, rest + + def get_child_uri(self, uri): + key, rest = self.get_child_key(uri) + + if uri.query: + query = dict(uri.query) + if not rest: + query.pop("key", None) + else: + query["key"] = [rest] + uri = uri.clone(query=query) + + return uri, key + + def is_leaf_list_node(self, uri): + plugin = self.get_query_param(uri, "plugin") + return not plugin or plugin == self.ext and not self.is_nested(uri) + + def save_node(self, node): + if not self.get_query_param(node.uri, "plugin"): + return node + + root_node = cio.load(node.uri.clone(query=None)) + root_data = root_node["data"] or self.load(None) + + node.content = self.save_child( + node.content, parent_node=node, parent_data=root_data + ) # TODO: deep clone data? + + return node + + def save_child(self, leaf_data, parent_node, parent_data): + child_node, key = self.get_child_node( + parent_node.uri, parent_data, default=parent_node.content + ) + + plugin = self.resolve_child_plugin(child_node.uri) + if plugin.ext == self.ext: + if not self.is_nested(child_node.uri): + child_content = leaf_data + else: + child_data = self.load(child_node.content) + child_content = self.save_child( + leaf_data, parent_node=child_node, parent_data=child_data + ) + else: + child_node.content = leaf_data + child_node = plugin.save_node(child_node) + child_content = child_node.content + + child = self.find_child(parent_data, key) + + if child: + child["data"] = child_content + else: + parent_data["children"].append( + { + "key": key, + "plugin": plugin.ext, + "data": child_content, + } + ) + + return self.save(parent_data) + + def save(self, content): + return json.dumps(content) diff --git a/djedi/static/djedi/cms/js/cms.coffee b/djedi/static/djedi/cms/js/cms.coffee index a99e56c0..847d8c91 100644 --- a/djedi/static/djedi/cms/js/cms.coffee +++ b/djedi/static/djedi/cms/js/cms.coffee @@ -35,7 +35,7 @@ class Settings ################################################[ NODE ]############################################################## -class Node +class window.Node selected: no @@ -253,7 +253,7 @@ class Page ################################################[ PLUGIN ]############################################################ -class Plugin +window.Plugin = class Plugin constructor: (@node) -> @uri = @node.uri.valueOf() @@ -388,5 +388,5 @@ class CMS else @page.$cms.css style - -new CMS +window.makeCms = -> + new CMS diff --git a/djedi/static/djedi/cms/js/cms.js b/djedi/static/djedi/cms/js/cms.js index febb61a1..12fe501d 100644 --- a/djedi/static/djedi/cms/js/cms.js +++ b/djedi/static/djedi/cms/js/cms.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.8.0 (function() { - var CMS, Events, Node, Page, Plugin, Search, Settings, + var CMS, Events, Page, Plugin, Search, Settings, __slice = [].slice, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; @@ -55,7 +55,7 @@ })(); - Node = (function() { + window.Node = (function() { Node.prototype.selected = false; function Node(uri, data, container) { @@ -340,7 +340,7 @@ })(); - Plugin = (function() { + window.Plugin = Plugin = (function() { function Plugin(node) { this.node = node; this.connect = __bind(this.connect, this); @@ -514,6 +514,8 @@ })(); - new CMS; + window.makeCms = function() { + return new CMS; + }; }).call(this); diff --git a/djedi/static/djedi/cms/js/uri.coffee b/djedi/static/djedi/cms/js/uri.coffee index c6020907..a20ecf9f 100644 --- a/djedi/static/djedi/cms/js/uri.coffee +++ b/djedi/static/djedi/cms/js/uri.coffee @@ -15,6 +15,7 @@ String::to_uri = -> s += @namespace + '@' if @namespace s += @path s += '.' + @ext if @ext + s += '?' + @stringify_query(@query) if @query s += '#' + @version if @version s #@scheme + '://' + @namespace + '@' + @path + '.' + @ext + '#' + @version @@ -27,6 +28,7 @@ String::to_uri = -> @path = obj.path @ext = obj.ext @version = obj.version + @query = obj.query @parts = scheme: @scheme @@ -34,9 +36,18 @@ String::to_uri = -> path: @path ext: @ext version: @version + query: @query @parse = (uri_str) -> [base, _, version] = partition(uri_str, '#') + if base.indexOf('?') != -1 + [base, _, querystring] = rpartition(base,'?') + param_pairs = querystring.split('&') + query = {} + for pair in param_pairs + [key, _, val] = partition(pair, '=') + if !query[key] or query[key].length == 0 + query[key] = [decodeURIComponent(val)] [scheme, _, path] = rpartition(base, '://') [namespace, _, path] = rpartition(path, '@') [path, _, ext] = partition(path, '.') @@ -46,12 +57,31 @@ String::to_uri = -> path: path ext: ext or null version: version or null + query: query or null + @from_str = (uri_str) -> @from_parts @parse uri_str @clone = (obj) -> - @from_parts _.extend(obj, @parts) + parts = Object.assign({}, @parts) + _uri = ((' ' + @valueOf()).slice(1)).to_uri() + for key, val of obj + parts[key] = val + _uri.from_parts(parts) + return _uri + + @stringify_query = (query) -> + simplified_query = {} + for key, arr of @query + simplified_query[key] = arr[0] + return $.param(simplified_query) + + @get_query_param = (key) -> + if @query and @query[key] and @query[key].length > 0 + return @query[key][0] + else + return undefined @from_parts @parse @ @ diff --git a/djedi/static/djedi/cms/js/uri.js b/djedi/static/djedi/cms/js/uri.js index 755cd517..879c8afc 100644 --- a/djedi/static/djedi/cms/js/uri.js +++ b/djedi/static/djedi/cms/js/uri.js @@ -28,6 +28,9 @@ if (this.ext) { s += '.' + this.ext; } + if (this.query) { + s += '?' + this.stringify_query(this.query); + } if (this.version) { s += '#' + this.version; } @@ -42,33 +45,73 @@ this.path = obj.path; this.ext = obj.ext; this.version = obj.version; + this.query = obj.query; return this.parts = { scheme: this.scheme, namespace: this.namespace, path: this.path, ext: this.ext, - version: this.version + version: this.version, + query: this.query }; }; this.parse = function(uri_str) { - var base, ext, namespace, path, scheme, version, _, _ref, _ref1, _ref2, _ref3; + var base, ext, key, namespace, pair, param_pairs, path, query, querystring, scheme, val, version, _, _i, _len, _ref, _ref1, _ref2, _ref3, _ref4, _ref5; _ref = partition(uri_str, '#'), base = _ref[0], _ = _ref[1], version = _ref[2]; - _ref1 = rpartition(base, '://'), scheme = _ref1[0], _ = _ref1[1], path = _ref1[2]; - _ref2 = rpartition(path, '@'), namespace = _ref2[0], _ = _ref2[1], path = _ref2[2]; - _ref3 = partition(path, '.'), path = _ref3[0], _ = _ref3[1], ext = _ref3[2]; + if (base.indexOf('?') !== -1) { + _ref1 = rpartition(base, '?'), base = _ref1[0], _ = _ref1[1], querystring = _ref1[2]; + param_pairs = querystring.split('&'); + query = {}; + for (_i = 0, _len = param_pairs.length; _i < _len; _i++) { + pair = param_pairs[_i]; + _ref2 = partition(pair, '='), key = _ref2[0], _ = _ref2[1], val = _ref2[2]; + if (!query[key] || query[key].length === 0) { + query[key] = [decodeURIComponent(val)]; + } + } + } + _ref3 = rpartition(base, '://'), scheme = _ref3[0], _ = _ref3[1], path = _ref3[2]; + _ref4 = rpartition(path, '@'), namespace = _ref4[0], _ = _ref4[1], path = _ref4[2]; + _ref5 = partition(path, '.'), path = _ref5[0], _ = _ref5[1], ext = _ref5[2]; return { scheme: scheme || null, namespace: namespace || null, path: path, ext: ext || null, - version: version || null + version: version || null, + query: query || null }; }; this.from_str = function(uri_str) { return this.from_parts(this.parse(uri_str)); }; this.clone = function(obj) { - return this.from_parts(_.extend(obj, this.parts)); + var key, parts, val, _uri; + parts = Object.assign({}, this.parts); + _uri = ((' ' + this.valueOf()).slice(1)).to_uri(); + for (key in obj) { + val = obj[key]; + parts[key] = val; + } + _uri.from_parts(parts); + return _uri; + }; + this.stringify_query = function(query) { + var arr, key, simplified_query, _ref; + simplified_query = {}; + _ref = this.query; + for (key in _ref) { + arr = _ref[key]; + simplified_query[key] = arr[0]; + } + return $.param(simplified_query); + }; + this.get_query_param = function(key) { + if (this.query && this.query[key] && this.query[key].length > 0) { + return this.query[key][0]; + } else { + return void 0; + } }; this.from_parts(this.parse(this)); return this; diff --git a/djedi/static/djedi/plugins/base/js/editor.coffee b/djedi/static/djedi/plugins/base/js/editor.coffee index 7131e4bb..6dd8cbd9 100644 --- a/djedi/static/djedi/plugins/base/js/editor.coffee +++ b/djedi/static/djedi/plugins/base/js/editor.coffee @@ -134,8 +134,12 @@ class window.Editor @$version = $ 'header .version' @$flag = $ 'header .flag' - $('#button-publish').on 'click', @publish - $('#button-discard').on 'click', @discard + @$doc.on 'editor:save', () => @$form.submit() + @$doc.on 'editor:publish', () => @onPublish() + + @actions.publish.on 'click', @publish + @actions.discard.on 'click', @discard + @actions.save.on 'click', @save # Use ajaxForm from downloads @$form.ajaxForm @@ -153,6 +157,8 @@ class window.Editor @api.load config.uri, @onLoad @callback 'initialize', config @initialized = yes + window.editor = @ + @trigger 'editor:initialized', @, config callback: (name, args...) -> callback = @config[name] @@ -171,7 +177,7 @@ class window.Editor prepareForm: -> onLoad: (node) => - console.log 'Editor.onLoad()', node.uri + console.log 'Editor.onLoad()' initial = @node == undefined # Fetch default node data from embedder @@ -196,6 +202,7 @@ class window.Editor onFormChange: (event) => console.log 'Editor.onFormChange()' + @trigger 'editor:dirty' @setState 'dirty' @callback 'onFormChange', event @@ -203,8 +210,14 @@ class window.Editor console.log 'Editor.onSave()' node = @setNode node @render node + @trigger 'node:update', node.uri.valueOf(), node @trigger 'node:render', node.uri.valueOf(), node.content + onPublish: () => + node = @api.publish @node.uri.valueOf() + @setNode node + @setState 'published' + setNode: (node) -> console.log 'Editor.setNode()' @node = node @@ -232,6 +245,7 @@ class window.Editor setState: (state) -> console.log 'Editor.setState()', state if state != @state + oldState = @state @state = state @$version.removeClass 'label-default label-warning label-danger label-info label-success' switch state @@ -260,20 +274,22 @@ class window.Editor @actions.discard.disable() @actions.save.disable() @actions.publish.enable() + @trigger 'editor:state-changed', oldState, state, @node + renderHeader: (node) -> uri = node.uri - color = (uri.ext[0].toUpperCase().charCodeAt() - 65) % 5 + 1 + color = @getPluginColor(uri.ext) parts = ( for part in uri.path.split '/' when part != '' (part[..0].toUpperCase() + part[1..-1]).replace /[_-]/g, ' ' ) - path = parts.join " / " + path = parts.join " / " lang = uri.namespace.split('-')[0] if uri.scheme == 'i18n' - @$plugin.html(uri.ext).addClass "plugin-fg-#{color}" + @$plugin.html(uri.ext).addClass color @$path.html path @$flag.addClass "flag-#{lang}" @@ -329,13 +345,18 @@ class window.Editor render: (node) -> console.log 'Editor.render()' + @trigger 'editor:render', node @callback 'render', node loadRevision: (event) => console.log 'Editor.loadRevision()' event.preventDefault() - $revision = $ event.target + if $(event.target).is('i') + $revision = $(event.target).parent() + else + $revision = $ event.target + uri = $revision.data('uri') published = $revision.data 'published' @@ -354,7 +375,12 @@ class window.Editor renderContent: (data, doTrigger, callback) -> console.log 'Editor.renderContent()' - plugin = @node.uri.ext + + if @node.uri.query and @node.uri.query['plugin'] + plugin = @node.uri.query['plugin'] + else + plugin = @node.uri.ext + data = {data: data} if typeof(data) == 'string' content = '' @@ -372,9 +398,8 @@ class window.Editor content publish: => - node = @api.publish @node.uri.valueOf() - @setNode node - @setState 'published' + if @state == "draft" || @state == "revert" + @trigger "editor:publish", @node.uri discard: => if @node.uri.version == 'draft' @@ -385,3 +410,12 @@ class window.Editor @node = null @api.load uri.valueOf(), @onLoad + @trigger "editor:discard", uri + + save: => + if @state == "dirty" + @trigger 'editor:save', @node.uri + + getPluginColor: (ext) => + color = (ext[0].toUpperCase().charCodeAt() - 65) % 5 + 1 + return "plugin-fg-#{color}" diff --git a/djedi/static/djedi/plugins/base/js/editor.js b/djedi/static/djedi/plugins/base/js/editor.js index cd897506..6643229a 100644 --- a/djedi/static/djedi/plugins/base/js/editor.js +++ b/djedi/static/djedi/plugins/base/js/editor.js @@ -156,9 +156,12 @@ window.Editor = (function() { function Editor(config) { this.config = config; + this.getPluginColor = __bind(this.getPluginColor, this); + this.save = __bind(this.save, this); this.discard = __bind(this.discard, this); this.publish = __bind(this.publish, this); this.loadRevision = __bind(this.loadRevision, this); + this.onPublish = __bind(this.onPublish, this); this.onSave = __bind(this.onSave, this); this.onFormChange = __bind(this.onFormChange, this); this.onLoad = __bind(this.onLoad, this); @@ -191,8 +194,19 @@ this.$path = $('header .uri'); this.$version = $('header .version'); this.$flag = $('header .flag'); - $('#button-publish').on('click', this.publish); - $('#button-discard').on('click', this.discard); + this.$doc.on('editor:save', (function(_this) { + return function() { + return _this.$form.submit(); + }; + })(this)); + this.$doc.on('editor:publish', (function(_this) { + return function() { + return _this.onPublish(); + }; + })(this)); + this.actions.publish.on('click', this.publish); + this.actions.discard.on('click', this.discard); + this.actions.save.on('click', this.save); this.$form.ajaxForm({ beforeSubmit: this.prepareForm, success: this.onSave @@ -209,7 +223,9 @@ }); this.api.load(config.uri, this.onLoad); this.callback('initialize', config); - return this.initialized = true; + this.initialized = true; + window.editor = this; + return this.trigger('editor:initialized', this, config); }; Editor.prototype.callback = function() { @@ -240,7 +256,7 @@ Editor.prototype.onLoad = function(node) { var initial; - console.log('Editor.onLoad()', node.uri); + console.log('Editor.onLoad()'); initial = this.node === void 0; if (initial) { this.trigger('page:node:fetch', node.uri.valueOf(), (function(_this) { @@ -267,6 +283,7 @@ Editor.prototype.onFormChange = function(event) { console.log('Editor.onFormChange()'); + this.trigger('editor:dirty'); this.setState('dirty'); return this.callback('onFormChange', event); }; @@ -275,9 +292,17 @@ console.log('Editor.onSave()'); node = this.setNode(node); this.render(node); + this.trigger('node:update', node.uri.valueOf(), node); return this.trigger('node:render', node.uri.valueOf(), node.content); }; + Editor.prototype.onPublish = function() { + var node; + node = this.api.publish(this.node.uri.valueOf()); + this.setNode(node); + return this.setState('published'); + }; + Editor.prototype.setNode = function(node) { console.log('Editor.setNode()'); this.node = node; @@ -305,8 +330,10 @@ }; Editor.prototype.setState = function(state) { + var oldState; console.log('Editor.setState()', state); if (state !== this.state) { + oldState = this.state; this.state = state; this.$version.removeClass('label-default label-warning label-danger label-info label-success'); switch (state) { @@ -314,35 +341,40 @@ this.$version.addClass('label-default'); this.actions.discard.disable(); this.actions.save.enable(); - return this.actions.publish.disable(); + this.actions.publish.disable(); + break; case 'dirty': this.$version.addClass('label-danger'); this.actions.discard.enable(); this.actions.save.enable(); - return this.actions.publish.disable(); + this.actions.publish.disable(); + break; case 'draft': this.$version.addClass('label-primary'); this.actions.discard.enable(); this.actions.save.disable(); - return this.actions.publish.enable(); + this.actions.publish.enable(); + break; case 'published': this.$version.addClass('label-success'); this.actions.discard.disable(); this.actions.save.disable(); - return this.actions.publish.disable(); + this.actions.publish.disable(); + break; case 'revert': this.$version.addClass('label-warning'); this.actions.discard.disable(); this.actions.save.disable(); - return this.actions.publish.enable(); + this.actions.publish.enable(); } + return this.trigger('editor:state-changed', oldState, state, this.node); } }; Editor.prototype.renderHeader = function(node) { var color, lang, part, parts, path, uri, v; uri = node.uri; - color = (uri.ext[0].toUpperCase().charCodeAt() - 65) % 5 + 1; + color = this.getPluginColor(uri.ext); parts = (function() { var _i, _len, _ref, _results; _ref = uri.path.split('/'); @@ -355,11 +387,11 @@ } return _results; })(); - path = parts.join(" / "); + path = parts.join(" / "); if (uri.scheme === 'i18n') { lang = uri.namespace.split('-')[0]; } - this.$plugin.html(uri.ext).addClass("plugin-fg-" + color); + this.$plugin.html(uri.ext).addClass(color); this.$path.html(path); this.$flag.addClass("flag-" + lang); v = this.$version.find('var'); @@ -413,6 +445,7 @@ Editor.prototype.render = function(node) { console.log('Editor.render()'); + this.trigger('editor:render', node); return this.callback('render', node); }; @@ -420,7 +453,11 @@ var $revision, data, published, uri; console.log('Editor.loadRevision()'); event.preventDefault(); - $revision = $(event.target); + if ($(event.target).is('i')) { + $revision = $(event.target).parent(); + } else { + $revision = $(event.target); + } uri = $revision.data('uri'); published = $revision.data('published'); if (uri.version) { @@ -448,7 +485,11 @@ Editor.prototype.renderContent = function(data, doTrigger, callback) { var content, plugin; console.log('Editor.renderContent()'); - plugin = this.node.uri.ext; + if (this.node.uri.query && this.node.uri.query['plugin']) { + plugin = this.node.uri.query['plugin']; + } else { + plugin = this.node.uri.ext; + } if (typeof data === 'string') { data = { data: data @@ -476,10 +517,9 @@ }; Editor.prototype.publish = function() { - var node; - node = this.api.publish(this.node.uri.valueOf()); - this.setNode(node); - return this.setState('published'); + if (this.state === "draft" || this.state === "revert") { + return this.trigger("editor:publish", this.node.uri); + } }; Editor.prototype.discard = function() { @@ -490,7 +530,20 @@ uri = this.node.uri; uri.version = null; this.node = null; - return this.api.load(uri.valueOf(), this.onLoad); + this.api.load(uri.valueOf(), this.onLoad); + return this.trigger("editor:discard", uri); + }; + + Editor.prototype.save = function() { + if (this.state === "dirty") { + return this.trigger('editor:save', this.node.uri); + } + }; + + Editor.prototype.getPluginColor = function(ext) { + var color; + color = (ext[0].toUpperCase().charCodeAt() - 65) % 5 + 1; + return "plugin-fg-" + color; }; return Editor; diff --git a/djedi/static/djedi/plugins/img/js/img.coffee b/djedi/static/djedi/plugins/img/js/img.coffee index 29b407e1..4142286c 100644 --- a/djedi/static/djedi/plugins/img/js/img.coffee +++ b/djedi/static/djedi/plugins/img/js/img.coffee @@ -319,6 +319,8 @@ class window.ImageEditor extends window.Editor @field = $ config.field @preview = $ config.preview + @enable_crop_preview = config.enable_crop_preview + @dropzone = new Dropzone field: @field el: config.dropzone @@ -413,7 +415,9 @@ class window.ImageEditor extends window.Editor $image.on 'crop:preview', (event, html) => @crop?.setPreviewAttributes @getHtmlFields() # This replaces the image on the page with the preview image. - @triggerRender html + + if @enable_crop_preview + @triggerRender html $image.on 'crop:attributes', => @updateImageAttributes() if @crop diff --git a/djedi/static/djedi/plugins/img/js/img.js b/djedi/static/djedi/plugins/img/js/img.js index 419e8b4e..b9650400 100644 --- a/djedi/static/djedi/plugins/img/js/img.js +++ b/djedi/static/djedi/plugins/img/js/img.js @@ -331,6 +331,7 @@ this.firstRender = true; this.field = $(config.field); this.preview = $(config.preview); + this.enable_crop_preview = config.enable_crop_preview; this.dropzone = new Dropzone({ field: this.field, el: config.dropzone @@ -461,7 +462,9 @@ if ((_ref = _this.crop) != null) { _ref.setPreviewAttributes(_this.getHtmlFields()); } - return _this.triggerRender(html); + if (_this.enable_crop_preview) { + return _this.triggerRender(html); + } }); $image.on('crop:attributes', function() { _this.updateImageAttributes(); diff --git a/djedi/static/djedi/plugins/list/css/list.css b/djedi/static/djedi/plugins/list/css/list.css new file mode 100644 index 00000000..4a82e29e --- /dev/null +++ b/djedi/static/djedi/plugins/list/css/list.css @@ -0,0 +1,190 @@ +#editor { + flex: 1; + max-height: none; + height: auto; + overflow: auto; + padding: 0; +} + +#editor .helpers { + padding: 15px; + margin-bottom: 15px; + border-bottom: 1px dashed #ccc; + display: flex; + align-items: flex-start; + justify-content: space-between; +} + +#editor .helpers #direction-options { + flex: 1; +} + +#editor .helpers .direction-title { + margin-bottom: 0; + margin-right: 5px; + vertical-align: middle; +} + +#editor .helpers #direction-options .radio { + display: inline-block; + margin: 0; + margin-right: 10px; + +} + +.subnodes { + display: flex; + flex-direction: column; + margin-left: 5px; +} + +.subnodes #editor-iframe { + height: 355px; +} + +.subnodes__item { + display: block; + margin-bottom: 15px; +} + +.subnodes__item-remove { + display: inline-block; + width: 25px; + cursor: pointer; + order: 2; + text-align: center; + height: 25px; + line-height: 24px; + border-radius: 20px; + margin-right: 1px; +} + +.subnodes__item-remove:hover { + background-color: #6a6a6a; +} + +.subnodes__item-drag { + display: block; + background-color: #3a3a3a; + text-align: center; + padding: 1px 0px; + font-size: 16px; + cursor: move; +} + +.subnodes__item-shift { + display:flex; + flex-direction: column; + width: 30px; + order: 3; +} + +.subnodes__item-shift > a { + flex: 1; + text-align: center; + color: white; + cursor: pointer; + } + +.subnodes__item-shift > a:hover { + color:black; + text-decoration: none; + background-color: #6a6a6a; +} + +.subnodes__item-shift--disabled > a { + cursor: not-allowed; +} + +.subnodes__item-shift--disabled > a:hover { + color:#999; +} + +.subnodes__item-title { + display: flex; + flex-direction: row; + align-items: center; + + background: rgba(0, 0, 0, 0.25); + border-bottom: 1px solid #666; + text-transform: uppercase; + -webkit-box-shadow: none; + box-shadow: none; + text-shadow: 0px 1px 2px #111; + padding: 0; + padding-left: 45px; + font-weight: bold; + position: relative; +} + +.subnodes__item-title__text { + font-size: 9px; + flex: 1; +} + +.subnodes__item-title:after { + position: absolute; + top: 50%; + left: 15px; + transform: translateY(-50%); + + display: block; + content: ' '; + cursor: pointer; + + + border: inset; + border-bottom: 6px solid white; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 0px; +} + +.subnodes__item--closed .subnodes__item-title:after { + border-top: 6px solid white; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 0px; +} + +.subnodes__item-content { + +} + +.subnodes__item--closed .subnodes__item-content { + display: none; +} + +#subnode-data { + display: none; +} + +.djedi-list { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; +} + +.djedi-list li { +} + +.djedi-list--col { + flex-direction: column; +} + +.djedi-list--ucol { + flex-direction: column-reverse; +} + +.djedi-list--row { + flex-direction: row; +} + +.djedi-list--urow { + flex-direction: row-reverse; +} + +.node-add { + text-transform: uppercase; +} diff --git a/djedi/static/djedi/plugins/list/js/list.coffee b/djedi/static/djedi/plugins/list/js/list.coffee new file mode 100644 index 00000000..fbb5b347 --- /dev/null +++ b/djedi/static/djedi/plugins/list/js/list.coffee @@ -0,0 +1,347 @@ + + +################################################[ EDITOR ]############################################################ +class window.ListEditor extends window.Editor + + initDataStructure: () -> + return { + direction: 'col', + children: [] + } + + + initialize: (config) -> + console.log 'ListEditor.initialize', @ + + super config + @subnodeCss = ' + + '; + + @editor = this; + @subnodeIframes = [] + @data = @initDataStructure() + @saveQueue = [] + @loading = false + @preventParentReload = false + @subnodeDirty = false + @doShallowSave = false + + @container = $('#node-list') + @dataHolder = $('#subnode-data') + @directions = $('#direction-options') + @editor.$add_list = $('#plugin-list') + + $('#form input').unbind() + $('#form textarea').unbind() + $('#form select').unbind() + + @directions.find('input').on 'change', (e) => + @setDirection e.target.value + + for plg in config.plugins + if plg != 'list' + $("
  • #{plg}
  • ").appendTo @editor.$add_list + + @editor.$add = $('.node-add') + @editor.$add.on 'click', (evt) => + if (!@subnodeDirty) + @addSubnode(@getSubnodeUriKey(), $(evt.target).text(), true) + + setDirection: (dir, refreshData = true) => + @directions.find('[name="direction"]').prop('checked', false); + target = @directions.find("[value='#{dir}']"); + if target.length == 1 + target.prop('checked', true); + @data.direction = dir + @updateData(refreshData) + @setDirty() + else + @setDirection "col", refreshData; + + addSubnode: (key, plugin, markDirty, defaultData = "") => + @spawnSubnode @node.uri.clone({ + query: { + key: [key], + plugin: [plugin] + }, + version: "", + }).valueOf(), markDirty, defaultData + + onLoad: (node) => + @loading = true + @clearList() + super node + @frameBias = "node/" + encodeURIComponent((encodeURIComponent(node.uri.valueOf().replace('#'+node.uri.version, '')))) + "/editor" + try + codedData = node.data + for entry in codedData.children + @addSubnode(@getSubnodeUriKey(entry.key), entry.plugin, false, entry.data) + @setDirection codedData.direction, false + catch exception + @clearList() + @updateData(true) + console.log "ListEditor.onLoad(), error when loading. Data invalid: ", exception + @loading = false + + render: (node) => + console.log('ListEditor.render()', node.content, @) + @dataHolder.val(JSON.stringify(@data)) + super node + + setState: (state) => + if state == 'draft' && @preventParentReload || state == 'dirty' && @loading + return + if state == "dirty" && @subnodeDirty + @toggleListActions() + super state + + spawnSubnode: (uri, refreshValue = true, data = "") => + console.log("ListEditor.spawnSubNode()") + classes = 'subnodes__item' + + node_container = $("
    ").appendTo @container + title = $("
    ").appendTo node_container + holder = $("
    ").appendTo node_container + + title.on 'click', (e) => + $(e.target).parent().toggleClass 'subnodes__item--closed' + + $("
    ").appendTo(title).on 'click', (e) => + @popSubnode($(e.target).parents('.subnodes__item').attr "uri-ref") + + handle = $("
    + + +
    ").prependTo title + handle.find('a').on 'click', (event) => + if @subnodeDirty + return false + newOrder = false + if ($(event.target).hasClass('subnodes__item-shift--up') || $(event.target).hasClass('icon-chevron-up')) + newOrder = @moveChild(uri, -1) + else + newOrder = @moveChild(uri, 1) + if newOrder != false + @resortNodes() + @updateData true + @setDirty() + @shallowSave() + + node = new window.Node uri, data, holder + title.append (""+(node.uri.get_query_param('plugin') or 'unknown')+"") + title.find('.subnodes__item-title__text').addClass(@getPluginColor(node.uri.get_query_param('plugin') or 'plugin-fg-unknown')) + + node_container.attr 'uri-ref', node.uri.valueOf() + node_container.attr 'data-key', node.uri.get_query_param('key') + + node_iframe = new window.Plugin node + + ref_uri = @node.uri.clone({ + version: "" + }).valueOf() + + path = document.location.pathname.replace("node/#{encodeURIComponent(encodeURIComponent ref_uri)}/editor", "") + path = path.replace("node/#{encodeURIComponent(encodeURIComponent @node.uri)}/editor", "") + node_iframe.$el.attr 'src', path + "node/#{encodeURIComponent(encodeURIComponent uri)}/editor" + + node_container.css('order', @data.children.length); + @subnodeIframes.push node_iframe + @data.children.push { + key: @getSubnodeKey(node.uri.get_query_param('key')), + plugin: node.uri.get_query_param('plugin'), + data: data, + } + holder.append node_iframe.$el + + windowRef = node_iframe.$el[0].contentWindow + + $(node_iframe.$el).on 'load', () => + head = windowRef.$(node_iframe.$el[0]).contents().find("head"); + windowRef.$(head).append(@subnodeCss) + windowRef.$(windowRef.document).on 'editor:state-changed', (event, oldState, newState, node) => + if oldState == 'dirty' && newState == 'draft' + @workSaveQueue() + @updateSubnode(node.uri.to_uri().get_query_param('key'), node) + + windowRef.$(windowRef.document).on 'editor:dirty', () => + @subnodeDirty = true + @setDirty() + + windowRef.$(windowRef.document).on 'node:update', (event, uri, node) => + @updateSubnode(node.uri.to_uri().get_query_param('key'), node) + + windowRef.$(windowRef.document).on 'node:render', (event, uri, content) => + @renderSubnode(uri, content) + + @updateData(refreshValue) + if refreshValue + @setDirty() + + save: () -> + @preventParentReload = true + for subnode_iframe in @subnodeIframes + @saveQueue.push(subnode_iframe) + super + + shallowSave: () -> + @doShallowSave = true + if @state == "dirty" + @trigger 'editor:save', @node.uri + + onSave: (node) -> + super node + if !@doShallowSave + @workSaveQueue() + else + @doShallowSave = false; + + onPublish: () => + super + @loadRevisionByClass('.published') + @setState 'published' + + workSaveQueue: () => + console.log "ListEditor.workSaveQueue()", @saveQueue.length + if @saveQueue.length > 0 + @saveSubnode(@saveQueue.pop()) + else + @preventParentReload = false + @loadRevisionByClass('.draft') + @setState('draft') + @subnodeDirty = false + @toggleListActions(true) + + saveSubnode: (plugin) => + windowRef = plugin.$el[0].contentWindow + if windowRef and windowRef.editor and windowRef.editor.state != 'dirty' + @workSaveQueue() + else if windowRef and windowRef.editor + windowRef.editor.save() + + popSubnode: (uri) => + console.log("ListEditor.popSubnode()") + targetUri = uri + targetKey = @getSubnodeKey(targetUri.to_uri().get_query_param('key')) + @subnodeIframes = @subnodeIframes.filter (value) => + if value.uri.valueOf() != targetUri + return true + + value.close() + @container.find('[uri-ref="'+targetUri+'"]').remove() + return false + + @data.children = @data.children.filter (value) -> + if value.key != targetKey + return true + return false + @setDirty() + @updateData(true) + + clearList: () => + @container.empty() + @subnodeIframes = [] + @data = @initDataStructure() + + updateData: (reRender = false) => + collection = JSON.stringify @data + + @dataHolder.val collection + @dataHolder.change() + + @node.data = collection + + if (reRender) + @api.render "list", { + data: collection + }, (response) => + contentSet = $(response)[0] + @node.content = contentSet + @editor.triggerRender (@node.content) + + renderSubnode: (uri, content) => + console.log("ListEditor.renderSubnode()") + key = @getSubnodeKey(decodeURIComponent(uri.to_uri().get_query_param('key'))) + newContent = $(@node.content).find('#'+key).html(content).end()[0]; + @updateData(false) + @node.content = newContent + @editor.triggerRender newContent + + updateSubnode: (uuid, node, norender = false) => + console.log("ListEditor.updateSubnode()", uuid) + index = 0; + if node['data'] + for child in @data.children + if child.key == uuid + @data.children[index].data = node['data'] + index++ + @renderSubnode(node['uri'], node['content']) + + toggleListActions: (enable = false) => + @container.find('.subnodes__item-shift').toggleClass('subnodes__item-shift--disabled', !enable) + @editor.$add.toggleClass('disabled', !enable) + @directions.find('input').prop('disabled', !enable) + + setDirty: () => + @setState 'dirty' + @trigger 'editor:dirty' + + array_move: (arr, old_index, new_index) -> + if new_index >= arr.length + k = new_index - arr.length + 1 + while k-- + arr.push(undefined) + arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + + moveChild: (uri, steps) => + _uri = uri.to_uri(); + step = 0; + for child in @data.children + if (child.key == _uri.get_query_param('key')) + if (step+steps >= 0 && step+steps < @data.children.length) + @array_move(@data.children, step, step+steps) + return step+steps; + else + step++; + return false; + + resortNodes: () => + step = 0; + for child in @data.children + $("[data-key="+child.key+"]").css('order', step) + step++; + + getSubnodeUriKey: (key = undefined) => + keys = "" + uri = @node.uri.to_uri() + if uri.get_query_param('key') + keys += @node.uri.to_uri().get_query_param('key') + "_" + return keys + (key or @generateGuid()) + + getSubnodeKey: (composite_key) => + keys = composite_key.split('_') + return keys[keys.length - 1] + + generateGuid: () -> + result = '' + for j in [0...32] + if j == 8 || j == 12 || j == 16 || j == 20 + result = result + '-' + i = Math.floor(Math.random()*16).toString(16).toUpperCase() + result = result + i + return result + + loadRevisionByClass: (targetVersionClass) => + @loadRevision({ + type:'click', + target: $('#revisions').find(targetVersionClass).find('a').get()[0], + preventDefault: () -> {}, + }) diff --git a/djedi/static/djedi/plugins/list/js/list.js b/djedi/static/djedi/plugins/list/js/list.js new file mode 100644 index 00000000..8456d3f9 --- /dev/null +++ b/djedi/static/djedi/plugins/list/js/list.js @@ -0,0 +1,490 @@ +// Generated by CoffeeScript 1.8.0 +(function() { + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + __hasProp = {}.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; + + window.ListEditor = (function(_super) { + __extends(ListEditor, _super); + + function ListEditor() { + this.loadRevisionByClass = __bind(this.loadRevisionByClass, this); + this.getSubnodeKey = __bind(this.getSubnodeKey, this); + this.getSubnodeUriKey = __bind(this.getSubnodeUriKey, this); + this.resortNodes = __bind(this.resortNodes, this); + this.moveChild = __bind(this.moveChild, this); + this.setDirty = __bind(this.setDirty, this); + this.toggleListActions = __bind(this.toggleListActions, this); + this.updateSubnode = __bind(this.updateSubnode, this); + this.renderSubnode = __bind(this.renderSubnode, this); + this.updateData = __bind(this.updateData, this); + this.clearList = __bind(this.clearList, this); + this.popSubnode = __bind(this.popSubnode, this); + this.saveSubnode = __bind(this.saveSubnode, this); + this.workSaveQueue = __bind(this.workSaveQueue, this); + this.onPublish = __bind(this.onPublish, this); + this.spawnSubnode = __bind(this.spawnSubnode, this); + this.setState = __bind(this.setState, this); + this.render = __bind(this.render, this); + this.onLoad = __bind(this.onLoad, this); + this.addSubnode = __bind(this.addSubnode, this); + this.setDirection = __bind(this.setDirection, this); + return ListEditor.__super__.constructor.apply(this, arguments); + } + + ListEditor.prototype.initDataStructure = function() { + return { + direction: 'col', + children: [] + }; + }; + + ListEditor.prototype.initialize = function(config) { + var plg, _i, _len, _ref; + console.log('ListEditor.initialize', this); + ListEditor.__super__.initialize.call(this, config); + this.subnodeCss = ''; + this.editor = this; + this.subnodeIframes = []; + this.data = this.initDataStructure(); + this.saveQueue = []; + this.loading = false; + this.preventParentReload = false; + this.subnodeDirty = false; + this.doShallowSave = false; + this.container = $('#node-list'); + this.dataHolder = $('#subnode-data'); + this.directions = $('#direction-options'); + this.editor.$add_list = $('#plugin-list'); + $('#form input').unbind(); + $('#form textarea').unbind(); + $('#form select').unbind(); + this.directions.find('input').on('change', (function(_this) { + return function(e) { + return _this.setDirection(e.target.value); + }; + })(this)); + _ref = config.plugins; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + plg = _ref[_i]; + if (plg !== 'list') { + $("
  • " + plg + "
  • ").appendTo(this.editor.$add_list); + } + } + this.editor.$add = $('.node-add'); + return this.editor.$add.on('click', (function(_this) { + return function(evt) { + if (!_this.subnodeDirty) { + return _this.addSubnode(_this.getSubnodeUriKey(), $(evt.target).text(), true); + } + }; + })(this)); + }; + + ListEditor.prototype.setDirection = function(dir, refreshData) { + var target; + if (refreshData == null) { + refreshData = true; + } + this.directions.find('[name="direction"]').prop('checked', false); + target = this.directions.find("[value='" + dir + "']"); + if (target.length === 1) { + target.prop('checked', true); + this.data.direction = dir; + this.updateData(refreshData); + return this.setDirty(); + } else { + return this.setDirection("col", refreshData); + } + }; + + ListEditor.prototype.addSubnode = function(key, plugin, markDirty, defaultData) { + if (defaultData == null) { + defaultData = ""; + } + return this.spawnSubnode(this.node.uri.clone({ + query: { + key: [key], + plugin: [plugin] + }, + version: "" + }).valueOf(), markDirty, defaultData); + }; + + ListEditor.prototype.onLoad = function(node) { + var codedData, entry, exception, _i, _len, _ref; + this.loading = true; + this.clearList(); + ListEditor.__super__.onLoad.call(this, node); + this.frameBias = "node/" + encodeURIComponent(encodeURIComponent(node.uri.valueOf().replace('#' + node.uri.version, ''))) + "/editor"; + try { + codedData = node.data; + _ref = codedData.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + entry = _ref[_i]; + this.addSubnode(this.getSubnodeUriKey(entry.key), entry.plugin, false, entry.data); + } + this.setDirection(codedData.direction, false); + } catch (_error) { + exception = _error; + this.clearList(); + this.updateData(true); + console.log("ListEditor.onLoad(), error when loading. Data invalid: ", exception); + } + return this.loading = false; + }; + + ListEditor.prototype.render = function(node) { + console.log('ListEditor.render()', node.content, this); + this.dataHolder.val(JSON.stringify(this.data)); + return ListEditor.__super__.render.call(this, node); + }; + + ListEditor.prototype.setState = function(state) { + if (state === 'draft' && this.preventParentReload || state === 'dirty' && this.loading) { + return; + } + if (state === "dirty" && this.subnodeDirty) { + this.toggleListActions(); + } + return ListEditor.__super__.setState.call(this, state); + }; + + ListEditor.prototype.spawnSubnode = function(uri, refreshValue, data) { + var classes, handle, holder, node, node_container, node_iframe, path, ref_uri, title, windowRef; + if (refreshValue == null) { + refreshValue = true; + } + if (data == null) { + data = ""; + } + console.log("ListEditor.spawnSubNode()"); + classes = 'subnodes__item'; + node_container = $("
    ").appendTo(this.container); + title = $("
    ").appendTo(node_container); + holder = $("
    ").appendTo(node_container); + title.on('click', (function(_this) { + return function(e) { + return $(e.target).parent().toggleClass('subnodes__item--closed'); + }; + })(this)); + $("
    ").appendTo(title).on('click', (function(_this) { + return function(e) { + return _this.popSubnode($(e.target).parents('.subnodes__item').attr("uri-ref")); + }; + })(this)); + handle = $("
    ").prependTo(title); + handle.find('a').on('click', (function(_this) { + return function(event) { + var newOrder; + if (_this.subnodeDirty) { + return false; + } + newOrder = false; + if ($(event.target).hasClass('subnodes__item-shift--up') || $(event.target).hasClass('icon-chevron-up')) { + newOrder = _this.moveChild(uri, -1); + } else { + newOrder = _this.moveChild(uri, 1); + } + if (newOrder !== false) { + _this.resortNodes(); + _this.updateData(true); + _this.setDirty(); + return _this.shallowSave(); + } + }; + })(this)); + node = new window.Node(uri, data, holder); + title.append("" + (node.uri.get_query_param('plugin') || 'unknown') + ""); + title.find('.subnodes__item-title__text').addClass(this.getPluginColor(node.uri.get_query_param('plugin') || 'plugin-fg-unknown')); + node_container.attr('uri-ref', node.uri.valueOf()); + node_container.attr('data-key', node.uri.get_query_param('key')); + node_iframe = new window.Plugin(node); + ref_uri = this.node.uri.clone({ + version: "" + }).valueOf(); + path = document.location.pathname.replace("node/" + (encodeURIComponent(encodeURIComponent(ref_uri))) + "/editor", ""); + path = path.replace("node/" + (encodeURIComponent(encodeURIComponent(this.node.uri))) + "/editor", ""); + node_iframe.$el.attr('src', path + ("node/" + (encodeURIComponent(encodeURIComponent(uri))) + "/editor")); + node_container.css('order', this.data.children.length); + this.subnodeIframes.push(node_iframe); + this.data.children.push({ + key: this.getSubnodeKey(node.uri.get_query_param('key')), + plugin: node.uri.get_query_param('plugin'), + data: data + }); + holder.append(node_iframe.$el); + windowRef = node_iframe.$el[0].contentWindow; + $(node_iframe.$el).on('load', (function(_this) { + return function() { + var head; + head = windowRef.$(node_iframe.$el[0]).contents().find("head"); + windowRef.$(head).append(_this.subnodeCss); + windowRef.$(windowRef.document).on('editor:state-changed', function(event, oldState, newState, node) { + if (oldState === 'dirty' && newState === 'draft') { + _this.workSaveQueue(); + return _this.updateSubnode(node.uri.to_uri().get_query_param('key'), node); + } + }); + windowRef.$(windowRef.document).on('editor:dirty', function() { + _this.subnodeDirty = true; + return _this.setDirty(); + }); + windowRef.$(windowRef.document).on('node:update', function(event, uri, node) { + return _this.updateSubnode(node.uri.to_uri().get_query_param('key'), node); + }); + return windowRef.$(windowRef.document).on('node:render', function(event, uri, content) { + return _this.renderSubnode(uri, content); + }); + }; + })(this)); + this.updateData(refreshValue); + if (refreshValue) { + return this.setDirty(); + } + }; + + ListEditor.prototype.save = function() { + var subnode_iframe, _i, _len, _ref; + this.preventParentReload = true; + _ref = this.subnodeIframes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + subnode_iframe = _ref[_i]; + this.saveQueue.push(subnode_iframe); + } + return ListEditor.__super__.save.apply(this, arguments); + }; + + ListEditor.prototype.shallowSave = function() { + this.doShallowSave = true; + if (this.state === "dirty") { + return this.trigger('editor:save', this.node.uri); + } + }; + + ListEditor.prototype.onSave = function(node) { + ListEditor.__super__.onSave.call(this, node); + if (!this.doShallowSave) { + return this.workSaveQueue(); + } else { + return this.doShallowSave = false; + } + }; + + ListEditor.prototype.onPublish = function() { + ListEditor.__super__.onPublish.apply(this, arguments); + this.loadRevisionByClass('.published'); + return this.setState('published'); + }; + + ListEditor.prototype.workSaveQueue = function() { + console.log("ListEditor.workSaveQueue()", this.saveQueue.length); + if (this.saveQueue.length > 0) { + return this.saveSubnode(this.saveQueue.pop()); + } else { + this.preventParentReload = false; + this.loadRevisionByClass('.draft'); + this.setState('draft'); + this.subnodeDirty = false; + return this.toggleListActions(true); + } + }; + + ListEditor.prototype.saveSubnode = function(plugin) { + var windowRef; + windowRef = plugin.$el[0].contentWindow; + if (windowRef && windowRef.editor && windowRef.editor.state !== 'dirty') { + return this.workSaveQueue(); + } else if (windowRef && windowRef.editor) { + return windowRef.editor.save(); + } + }; + + ListEditor.prototype.popSubnode = function(uri) { + var targetKey, targetUri; + console.log("ListEditor.popSubnode()"); + targetUri = uri; + targetKey = this.getSubnodeKey(targetUri.to_uri().get_query_param('key')); + this.subnodeIframes = this.subnodeIframes.filter((function(_this) { + return function(value) { + if (value.uri.valueOf() !== targetUri) { + return true; + } + value.close(); + _this.container.find('[uri-ref="' + targetUri + '"]').remove(); + return false; + }; + })(this)); + this.data.children = this.data.children.filter(function(value) { + if (value.key !== targetKey) { + return true; + } + return false; + }); + this.setDirty(); + return this.updateData(true); + }; + + ListEditor.prototype.clearList = function() { + this.container.empty(); + this.subnodeIframes = []; + return this.data = this.initDataStructure(); + }; + + ListEditor.prototype.updateData = function(reRender) { + var collection; + if (reRender == null) { + reRender = false; + } + collection = JSON.stringify(this.data); + this.dataHolder.val(collection); + this.dataHolder.change(); + this.node.data = collection; + if (reRender) { + return this.api.render("list", { + data: collection + }, (function(_this) { + return function(response) { + var contentSet; + contentSet = $(response)[0]; + _this.node.content = contentSet; + return _this.editor.triggerRender(_this.node.content); + }; + })(this)); + } + }; + + ListEditor.prototype.renderSubnode = function(uri, content) { + var key, newContent; + console.log("ListEditor.renderSubnode()"); + key = this.getSubnodeKey(decodeURIComponent(uri.to_uri().get_query_param('key'))); + newContent = $(this.node.content).find('#' + key).html(content).end()[0]; + this.updateData(false); + this.node.content = newContent; + return this.editor.triggerRender(newContent); + }; + + ListEditor.prototype.updateSubnode = function(uuid, node, norender) { + var child, index, _i, _len, _ref; + if (norender == null) { + norender = false; + } + console.log("ListEditor.updateSubnode()", uuid); + index = 0; + if (node['data']) { + _ref = this.data.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (child.key === uuid) { + this.data.children[index].data = node['data']; + } + index++; + } + } + return this.renderSubnode(node['uri'], node['content']); + }; + + ListEditor.prototype.toggleListActions = function(enable) { + if (enable == null) { + enable = false; + } + this.container.find('.subnodes__item-shift').toggleClass('subnodes__item-shift--disabled', !enable); + this.editor.$add.toggleClass('disabled', !enable); + return this.directions.find('input').prop('disabled', !enable); + }; + + ListEditor.prototype.setDirty = function() { + this.setState('dirty'); + return this.trigger('editor:dirty'); + }; + + ListEditor.prototype.array_move = function(arr, old_index, new_index) { + var k; + if (new_index >= arr.length) { + k = new_index - arr.length + 1; + while (k--) { + arr.push(void 0); + } + } + return arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); + }; + + ListEditor.prototype.moveChild = function(uri, steps) { + var child, step, _i, _len, _ref, _uri; + _uri = uri.to_uri(); + step = 0; + _ref = this.data.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (child.key === _uri.get_query_param('key')) { + if (step + steps >= 0 && step + steps < this.data.children.length) { + this.array_move(this.data.children, step, step + steps); + return step + steps; + } + } else { + step++; + } + } + return false; + }; + + ListEditor.prototype.resortNodes = function() { + var child, step, _i, _len, _ref, _results; + step = 0; + _ref = this.data.children; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + $("[data-key=" + child.key + "]").css('order', step); + _results.push(step++); + } + return _results; + }; + + ListEditor.prototype.getSubnodeUriKey = function(key) { + var keys, uri; + if (key == null) { + key = void 0; + } + keys = ""; + uri = this.node.uri.to_uri(); + if (uri.get_query_param('key')) { + keys += this.node.uri.to_uri().get_query_param('key') + "_"; + } + return keys + (key || this.generateGuid()); + }; + + ListEditor.prototype.getSubnodeKey = function(composite_key) { + var keys; + keys = composite_key.split('_'); + return keys[keys.length - 1]; + }; + + ListEditor.prototype.generateGuid = function() { + var i, j, result, _i; + result = ''; + for (j = _i = 0; _i < 32; j = ++_i) { + if (j === 8 || j === 12 || j === 16 || j === 20) { + result = result + '-'; + } + i = Math.floor(Math.random() * 16).toString(16).toUpperCase(); + result = result + i; + } + return result; + }; + + ListEditor.prototype.loadRevisionByClass = function(targetVersionClass) { + return this.loadRevision({ + type: 'click', + target: $('#revisions').find(targetVersionClass).find('a').get()[0], + preventDefault: function() { + return {}; + } + }); + }; + + return ListEditor; + + })(window.Editor); + +}).call(this); diff --git a/djedi/static/djedi/themes/base/theme.less b/djedi/static/djedi/themes/base/theme.less index 3379ba04..58de4ecd 100644 --- a/djedi/static/djedi/themes/base/theme.less +++ b/djedi/static/djedi/themes/base/theme.less @@ -66,8 +66,27 @@ body { background-color: @body-bg; color: @text-color; .antialiased(); + display: block; //fallback + display: flex; + flex-direction: column; + & .tab-content { + flex: 1; + display: flex; + flex-direction: column; + + .tab-pane { + flex: 1; + height: auto; + max-height: none; + + #editor-iframe { + height: 100%; + } + } + } &.embedded { + overflow: hidden; &.closed { @@ -408,7 +427,27 @@ body { } body.editor { - + .dropdown-menu { + background-color: rgba(0, 0, 0, 0.75); + border-color: #666; + text-shadow: none; + font-size: 9px; + min-width: 80px; + overflow: auto; + max-height: 300px; + margin-right: 45px; + + > li { + > a { + font-weight: bold; + color: #ffad3c; + &:hover { + color: #fff; + background-color: #222; + } + } + } + } header { .navbar { background: @editor-title-bg; diff --git a/djedi/static/djedi/themes/darth/theme.css b/djedi/static/djedi/themes/darth/theme.css index a4d20a20..2dea21c3 100644 --- a/djedi/static/djedi/themes/darth/theme.css +++ b/djedi/static/djedi/themes/darth/theme.css @@ -5343,6 +5343,22 @@ body { -ms-font-smoothing: antialiased; -o-font-smoothing: antialiased; font-smoothing: antialiased; + display: block; + display: flex; + flex-direction: column; +} +body .tab-content { + flex: 1; + display: flex; + flex-direction: column; +} +body .tab-content .tab-pane { + flex: 1; + height: auto; + max-height: none; +} +body .tab-content .tab-pane #editor-iframe { + height: 100%; } body.embedded { overflow: hidden; @@ -5687,6 +5703,24 @@ body.open .navbar-brand > a > i.dc-djedi-icon { font-style: normal; cursor: pointer; } +body.editor .dropdown-menu { + background-color: rgba(0, 0, 0, 0.75); + border-color: #666; + text-shadow: none; + font-size: 9px; + min-width: 80px; + overflow: auto; + max-height: 300px; + margin-right: 45px; +} +body.editor .dropdown-menu > li > a { + font-weight: bold; + color: #ffad3c; +} +body.editor .dropdown-menu > li > a:hover { + color: #fff; + background-color: #222; +} body.editor header .navbar { background: rgba(0, 0, 0, 0.25); border-bottom: 1px solid #1b1b1b; diff --git a/djedi/static/djedi/themes/luke/theme.css b/djedi/static/djedi/themes/luke/theme.css index 4ccae96c..2dd3617d 100644 --- a/djedi/static/djedi/themes/luke/theme.css +++ b/djedi/static/djedi/themes/luke/theme.css @@ -5343,6 +5343,22 @@ body { -ms-font-smoothing: antialiased; -o-font-smoothing: antialiased; font-smoothing: antialiased; + display: block; + display: flex; + flex-direction: column; +} +body .tab-content { + flex: 1; + display: flex; + flex-direction: column; +} +body .tab-content .tab-pane { + flex: 1; + height: auto; + max-height: none; +} +body .tab-content .tab-pane #editor-iframe { + height: 100%; } body.embedded { overflow: hidden; @@ -5687,6 +5703,24 @@ body.open .navbar-brand > a > i.dc-djedi-icon { font-style: normal; cursor: pointer; } +body.editor .dropdown-menu { + background-color: rgba(0, 0, 0, 0.75); + border-color: #666; + text-shadow: none; + font-size: 9px; + min-width: 80px; + overflow: auto; + max-height: 300px; + margin-right: 45px; +} +body.editor .dropdown-menu > li > a { + font-weight: bold; + color: #ffad3c; +} +body.editor .dropdown-menu > li > a:hover { + color: #fff; + background-color: #222; +} body.editor header .navbar { background: #fff; border-bottom: 1px solid #e1e1e1; diff --git a/djedi/templates/djedi/cms/cms.html b/djedi/templates/djedi/cms/cms.html index 02973457..309dc73c 100644 --- a/djedi/templates/djedi/cms/cms.html +++ b/djedi/templates/djedi/cms/cms.html @@ -44,5 +44,6 @@ + diff --git a/djedi/templates/djedi/plugins/base/editor.html b/djedi/templates/djedi/plugins/base/editor.html index ebcceadf..bcccbf03 100644 --- a/djedi/templates/djedi/plugins/base/editor.html +++ b/djedi/templates/djedi/plugins/base/editor.html @@ -42,7 +42,7 @@ diff --git a/djedi/templates/djedi/plugins/img/editor.html b/djedi/templates/djedi/plugins/img/editor.html index b9956884..c243d960 100644 --- a/djedi/templates/djedi/plugins/img/editor.html +++ b/djedi/templates/djedi/plugins/img/editor.html @@ -59,6 +59,7 @@ uri: '{{ uri }}', field: '#image', preview: '#preview', + enable_crop_preview: {% if disable_crop_preview %}false{% else %}true{% endif %}, dropzone: '#dropzone' }); diff --git a/djedi/templates/djedi/plugins/list/editor.html b/djedi/templates/djedi/plugins/list/editor.html new file mode 100644 index 00000000..18e3da2a --- /dev/null +++ b/djedi/templates/djedi/plugins/list/editor.html @@ -0,0 +1,39 @@ +{% extends 'djedi/plugins/base/editor.html' %} +{% load static %} +{% block editor %} +
    +
    + +
    +
    +
    + + +
    +
    +
    +{% endblock editor %} + +{% block plugin_style %} + +{% endblock %} + +{% block plugin_script %} + + + +{% endblock plugin_script %} diff --git a/djedi/tests/test_rest.py b/djedi/tests/test_rest.py index 358120ac..eefe79ae 100644 --- a/djedi/tests/test_rest.py +++ b/djedi/tests/test_rest.py @@ -252,7 +252,9 @@ def test_editor(self): assert set(response.context_data.keys()) == { "THEME", "VERSION", + "PLUGINS", "uri", + "plugin", "forms", } assert "HTML" in response.context_data["forms"] @@ -266,7 +268,13 @@ def test_editor(self): ) else: - assert set(response.context_data.keys()) == {"THEME", "VERSION", "uri"} + assert set(response.context_data.keys()) == { + "THEME", + "VERSION", + "PLUGINS", + "uri", + "plugin", + } self.assertNotIn(b"document.domain", response.content) @@ -335,6 +343,289 @@ def test_upload(self): response = self.post("api", "i18n://sv-se@header/logo.img", form) self.assertEqual(response.status_code, 200) + def test_save_nested_content(self): + data = { + "direction": "col", + "children": [ + { + "key": "abc123", + "plugin": "md", + "data": "# One banana", + }, + { + "key": "321cba", + "plugin": "txt", + "data": "Bananas", + }, + ], + } + listnode = cio.set("sv-se@page/apa.list", json.dumps(data)) + text_node_uri = listnode.uri.clone(query={"key": ["321cba"], "plugin": ["txt"]}) + self.assertEqual( + listnode.content, + ( + '" + ), + ) + text_node = cio.load(text_node_uri) + self.assertEqual(text_node["data"], "Bananas") + self.assertEqual(text_node["content"], "Bananas") + + md_node = cio.load( + listnode.uri.clone(query={"key": ["abc123"], "plugin": ["md"]}) + ) + self.assertEqual(md_node["content"], "

    One banana

    ") + self.assertEqual(md_node["data"], "# One banana") + + form = { + "data[width]": "64", + "data[height]": "64", + "data[crop]": "64,64,128,128", + "data[id]": "vw", + "data[class]": "year-53", + "data[alt]": "Zwitter", + "meta[comment]": "VW", + } + response = self.post( + "api", "i18n://sv-se@page/apa.list?key=imagekey&plugin=img", form + ) + self.assertEqual(response.status_code, 200) + + # TODO: Test getting default data + # img_node_in_list = cio.load('sv-se@page/apa.list?key=idontexist&plugin=img') + # img_node = cio.load('sv-se@page/monkeydo.img') + # self.assertEqual(img_node_in_list['data'], img_node['data']) + # self.assertEqual(img_node_in_list['content'], img_node['content']) + + # Test setting new subnode data + cio.set("sv-se@page/apa.list?key=newkey&plugin=md", "# Banan") + node_data = cio.load("sv-se@page/apa.list?key=newkey&plugin=md") + self.assertEqual(node_data["content"], "

    Banan

    ") + self.assertEqual(node_data["data"], "# Banan") + + # Test setting existing subnode data + cio.set("sv-se@page/apa.list?key=abc123&plugin=md", "# Two Bananas") + node_data = cio.load("sv-se@page/apa.list?key=abc123&plugin=md") + + self.assertEqual(node_data["content"], "

    Two Bananas

    ") + self.assertEqual(node_data["data"], "# Two Bananas") + + # Test setting it multiple times + cio.set( + "sv-se@page/apa.list#draft", + json.dumps( + { + "direction": "col", + "children": [ + { + "key": "abc123", + "plugin": "md", + "data": "# One banana", + } + ], + } + ), + ) + cio.set("sv-se@page/apa.list?key=abc123&plugin=md", "# No bananas") + cio.set("sv-se@page/apa.list?key=abc123&plugin=md", "# Many bananas") + cio.set("sv-se@page/apa.list?key=abc123&plugin=md", "# Many bananas") + node_data = cio.load("sv-se@page/apa.list?key=abc123&plugin=md") + parent_node = cio.load("sv-se@page/apa.list") + self.assertEqual(node_data["content"], "

    Many bananas

    ") + self.assertEqual(node_data["data"], "# Many bananas") + self.assertDictEqual( + parent_node["data"], + { + "direction": "col", + "children": [ + { + "key": "abc123", + "plugin": "md", + "data": "# Many bananas", + } + ], + }, + ) + + # Test nested list + cio.set( + "sv-se@page/apa.list", + json.dumps( + { + "direction": "col", + "children": [ + { + "key": "abc123", + "plugin": "md", + "data": "# One banana", + } + ], + } + ), + ) + cio.set( + "sv-se@page/apa.list?key=321cba&plugin=list", + json.dumps( + { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "# My banana", + } + ], + } + ), + ) + node_data = cio.load("sv-se@page/apa.list?key=abc123&plugin=md") + list_node = cio.load("sv-se@page/apa.list?key=321cba&plugin=list") + parent_node = cio.load("sv-se@page/apa.list") + child_node = cio.load("sv-se@page/apa.list?key=321cba_betterkey&plugin=md") + self.assertEqual(node_data["content"], "

    One banana

    ") + self.assertEqual(node_data["data"], "# One banana") + self.assertDictEqual( + parent_node["data"], + { + "direction": "col", + "children": [ + { + "key": "abc123", + "plugin": "md", + "data": "# One banana", + }, + { + "key": "321cba", + "plugin": "list", + "data": json.dumps( + { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "# My banana", + } + ], + } + ), + }, + ], + }, + ) + self.assertDictEqual( + list_node["data"], + { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "# My banana", + } + ], + }, + ) + self.assertEqual(child_node["data"], "# My banana") + self.assertEqual(child_node["content"], "

    My banana

    ") + + cio.set("sv-se@page/apa.list?key=321cba_betterkey&plugin=md", "# Not yours") + deep_node = cio.load("sv-se@page/apa.list?key=321cba_betterkey&plugin=md") + list_node = cio.load("sv-se@page/apa.list?key=321cba&plugin=list") + self.assertEqual(deep_node["data"], "# Not yours") + self.assertEqual(deep_node["content"], "

    Not yours

    ") + self.assertEqual( + list_node["data"], + { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "# Not yours", + } + ], + }, + ) + + empty_subnode = cio.set( + "sv-se@page/apa.list?key=321cba_betterkey&plugin=md", "" + ) + list_node = cio.load("sv-se@page/apa.list?key=321cba&plugin=list") + self.assertIsNone(empty_subnode.content) + self.assertEqual( + list_node["data"], + { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "", + } + ], + }, + ) + + cio.set( + "sv-se@page/render.list", json.dumps({"direction": "col", "children": []}) + ) + response = self.post("api.render", "md", {"data": "# Djedi"}) + assert response.status_code == 200 + self.assertRenderedMarkdown(smart_text(response.content), "# Djedi") + + data = { + "direction": "col", + "children": [ + { + "key": "betterkey", + "plugin": "md", + "data": "# Not yours", + } + ], + } + + response = self.post("api.render", "list", {"data": json.dumps(data)}) + assert response.status_code == 200 + self.assertEqual( + response.content, + b'", + ) + + empty_subnode = cio.set( + "sv-se@page/listthatdoesntexist.list?key=321cba&plugin=md", "# Hej" + ) + self.assertEqual(empty_subnode.content, "

    Hej

    ") + + response = self.get("cms.editor", "sv-se@page/context-test.list?plugin=img") + self.assertEqual(response.status_code, 200) + assert set(response.context_data.keys()) == { + "THEME", + "VERSION", + "PLUGINS", + "uri", + "plugin", + "forms", + } + assert "HTML" in response.context_data["forms"] + assert isinstance(response.context_data["forms"]["HTML"], BaseEditorForm) + + self.assertListEqual( + ["data__id", "data__alt", "data__class"], + list(response.context_data["forms"]["HTML"].fields.keys()), + ) + class PublicRestTest(ClientTest): def test_api_root_not_found(self): diff --git a/docs/plugins.rst b/docs/plugins.rst index 5b5113ed..62702259 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1,7 +1,7 @@ .. _plugins: Plugins -======= +======== Djedi CMS plugins handles content serialization and rendering. Each plugin has its own extension which is referred to in the request URI. diff --git a/example/example/templates/index.html b/example/example/templates/index.html index b59f9093..a19cbd7f 100644 --- a/example/example/templates/index.html +++ b/example/example/templates/index.html @@ -14,6 +14,26 @@ max-width: 100%; height: auto; } + + .djedi-list { + list-style-type: none; + margin:0; + padding: 0; + display: flex; + } + + .djedi-list--col { + flex-direction: column; + } + .djedi-list--ucol { + flex-direction: column-reverse; + } + .djedi-list--row { + flex-direction: row; + } + .djedi-list--urow { + flex-direction: row-reverse; + } @@ -27,6 +47,7 @@

    {% node 'page/title.txt' default='Djedi' %}

    {% node 'page/image.img' %} {% node 'page/image2.img' %} +{% node 'page/listnode.list' %}
    {% if not request.user.is_authenticated %}