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 '
'.format(**data)
+ for child in data["children"]:
+ ext = child["plugin"]
+ child_uri = node.uri.clone(query={"plugin": [ext], "key": [child["key"]]})
+ child_node = Node(uri=child_uri, content=child["data"])
+ plugin = plugins.get(ext)
+ content = plugin.load_node(child_node)
+ yield '- '.format(**child)
+ yield plugin.render_node(child_node, content) or ""
+ yield "
"
+ 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 @@
+