From b9166b62220208f49bfd3521e547d60d377ab7c1 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 23 Aug 2021 14:39:57 +0200 Subject: [PATCH] Add language tags definition, caret and bubbles in editor. --- src/redakcja/settings/__init__.py | 2 + src/redakcja/static/css/html.scss | 256 +++------------ src/redakcja/static/css/master.css | 20 +- src/redakcja/static/js/wiki/caret.js | 256 +++++++++++++++ .../static/js/wiki/view_editor_wysiwyg.js | 252 +-------------- .../static/js/wiki/view_properties.js | 51 ++- src/redakcja/static/js/wiki/wikiapi.js | 32 +- src/redakcja/static/js/wiki/xslt.js | 2 +- src/redakcja/urls.py | 1 + .../templates/wiki/document_details_base.html | 3 +- .../templates/wiki/tabs/wysiwyg_editor.html | 1 + src/wlxml/__init__.py | 0 src/wlxml/admin.py | 41 +++ src/wlxml/apps.py | 6 + src/wlxml/migrations/0001_initial.py | 76 +++++ src/wlxml/migrations/0002_initial_data.py | 281 +++++++++++++++++ .../migrations/0003_auto_20210813_2321.py | 23 ++ .../migrations/0004_auto_20210814_1052.py | 23 ++ src/wlxml/migrations/0005_tag_example_txt.py | 18 ++ src/wlxml/migrations/__init__.py | 0 src/wlxml/models.py | 164 ++++++++++ src/wlxml/templates/wlxml/editor.css | 10 + src/wlxml/templates/wlxml/tag_detail.html | 23 ++ src/wlxml/templates/wlxml/tag_list.html | 5 + src/wlxml/templates/wlxml/wl2html.xsl | 291 ++++++++++++++++++ src/wlxml/urls.py | 11 + src/wlxml/views.py | 34 ++ 27 files changed, 1388 insertions(+), 494 deletions(-) create mode 100644 src/redakcja/static/js/wiki/caret.js create mode 100644 src/wlxml/__init__.py create mode 100644 src/wlxml/admin.py create mode 100644 src/wlxml/apps.py create mode 100644 src/wlxml/migrations/0001_initial.py create mode 100644 src/wlxml/migrations/0002_initial_data.py create mode 100644 src/wlxml/migrations/0003_auto_20210813_2321.py create mode 100644 src/wlxml/migrations/0004_auto_20210814_1052.py create mode 100644 src/wlxml/migrations/0005_tag_example_txt.py create mode 100644 src/wlxml/migrations/__init__.py create mode 100644 src/wlxml/models.py create mode 100644 src/wlxml/templates/wlxml/editor.css create mode 100644 src/wlxml/templates/wlxml/tag_detail.html create mode 100644 src/wlxml/templates/wlxml/tag_list.html create mode 100644 src/wlxml/templates/wlxml/wl2html.xsl create mode 100644 src/wlxml/urls.py create mode 100644 src/wlxml/views.py diff --git a/src/redakcja/settings/__init__.py b/src/redakcja/settings/__init__.py index ba7a738a..dca64e5f 100644 --- a/src/redakcja/settings/__init__.py +++ b/src/redakcja/settings/__init__.py @@ -98,6 +98,7 @@ INSTALLED_APPS = ( 'toolbar', 'apiclient', 'email_mangler', + 'wlxml.apps.WlxmlConfig', ) if DEBUG: @@ -175,6 +176,7 @@ PIPELINE = { 'js/slugify.js', # wiki scripts + 'js/wiki/caret.js', 'js/wiki/wikiapi.js', 'wiki/js/themes.js', 'js/wiki/xslt.js', diff --git a/src/redakcja/static/css/html.scss b/src/redakcja/static/css/html.scss index b716bc59..0b9e9433 100644 --- a/src/redakcja/static/css/html.scss +++ b/src/redakcja/static/css/html.scss @@ -44,10 +44,13 @@ max-width: 36em; } + +// unused? .htmlview #toc { display: none; } +// where is it used, if any? .htmlview a { color: blue; text-decoration: none; @@ -57,62 +60,11 @@ text-decoration: none; } -.htmlview .nazwa_utworu { - font-size: 3em; - margin: 1.5em 0; - text-align: center; - line-height: 1.5em; - font-weight: bold; -} - -.htmlview .autor_utworu, -.htmlview .dzielo_nadrzedne, -.htmlview .naglowek_czesc, -.htmlview .srodtytul, -.htmlview .naglowek_akt -{ - font-size: 2em; - margin: 1.5em 0 0; - font-weight: bold; - line-height: 1.5em; -} - -.htmlview .podtytul, -.htmlview .naglowek_scena, -.htmlview .naglowek_rozdzial -{ - font-size: 1.5em; - margin: 1.5em 0 0; - font-weight: normal; - line-height: 1.5em; -} - -.htmlview .naglowek_podrozdzial, -.htmlview .naglowek_osoba -{ - font-size: 1em; - margin: 1.5em 0 0; - line-height: 1.5em; - font-weight: bold; -} - +// unused? .htmlview p { margin: 0; } -.htmlview #footnotes div { - margin: 1.5em 0 0 0; -} - -.htmlview #footnotes p { - margin-left: 2.5em; - font-size: 0.875em; -} - -.htmlview blockquote { - font-size: 0.875em; -} - .htmlview pre { overflow-x: auto; /* Use horizontal scroller if needed; for Firefox 2, not needed in Firefox 3 */ white-space: pre-wrap; /* css-3 */ @@ -146,29 +98,6 @@ /* =================== */ /* = Custom elements = */ /* =================== */ -.htmlview .autor_utwor { - font-size: 0.5em; - display: block; - line-height: 1.5em; - margin-bottom: 0.25em; -} - -.htmlview .dzielo_nadrzedne { - /* */ -} - -.htmlview .podtytul { - /* */ -} - -.htmlview .didaskalia { - font-style: italic; - margin: 0.5em 0 0 1.5em; -} - -.htmlview .kwestia { - margin: 0.5em 0 0; -} .htmlview .strofa { margin: 1.5em 0 0.5em auto; @@ -203,12 +132,8 @@ margin-left: 6em; } -.htmlview .strofa .wers_do_prawej { - text-align: right; -} - /* błędne wersy */ -.htmlview *:not(.strofa) > *[x-verse]::after { +.htmlview *:not([x-node='strofa']) > *[x-verse]::after { content: "Ten wers znajduje się poza strofą."; display: inline; background: red; @@ -225,61 +150,11 @@ margin: 0; } -.htmlview .akap, .htmlview .akap_cd, .htmlview .akap_dialog { - text-align: justify; - margin: 1.5em 0 0; -} - -.htmlview .motto_container { - display: inline-block; - margin: 1.5em 0 0; - clear: right; -} - -.htmlview .motto { - text-align: justify; - font-style: italic; -} - -.htmlview p.motto_podpis { - position: relative; - right: -3em; - text-align: right; -} - -.htmlview div.fragment { - border-bottom: 0.1em solid #999; - padding-bottom: 1.5em; -} - -.htmlview div.nota p, .htmlview div.dedykacja p { - text-align: right; - font-style: italic; -} .htmlview div.ramka { border: 1px darkgray solid; } -.htmlview .nota_red { - background-color: #eee; - border: 1px solid #888; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; - display: block; - padding: 0 1em 1em 1em; -} - -.htmlview hr.sekcja_swiatlo { - margin: 2em 0; - visibility: hidden; -} - -.htmlview hr.separator_linia { - margin: 1.5em 0; - border: none; - border-bottom: 0.1em solid #000; -} .htmlview hr.sekcja_asterysk { border: none; @@ -288,57 +163,11 @@ text-align: center; } -.htmlview hr.sekcja_asterysk:after { - content: "*"; -} - .htmlview div.lista_osob ol { list-style: none; padding: 0 0 0 1.5em; } -.htmlview p.miejsce_czas { - font-style: italic; -} - -.htmlview .mat, .htmlview .slowo_obce, .htmlview .tytul_dziela, .htmlview .didaskalia { - font-style: italic; -} - - -.htmlview .indeks_dolny { - font-style: normal; - vertical-align: sub; - font-size: .9em; -} - -.htmlview { - .wyroznienie { - // letter-spacing: 0.1em; - background: #dfd; - border: 1px solid #0f0; - } - .slowo_obce { - background: #ddf; - border: 1px solid #88f; - } - .tytul_dziela { - background: #fdf; - border: 1px solid #f8f; - } -} - - -.htmlview .osoba { - font-style: normal; - font-variant: small-caps; -} - -.htmlview em.wieksze_odstepy { - font-style: normal; - word-spacing: 1em; -} - .htmlview .ilustr img { max-width: 100%; } @@ -397,7 +226,7 @@ div[x-node] > .uwaga { background: green; } -.htmlview .motyw { +.htmlview [x-node='motyw'] { /* position: absolute; */ float: right; left: auto; @@ -471,51 +300,36 @@ div[x-node] > .uwaga { * Przypisy w tekście */ -.htmlview .annotation-inline-box:hover > span[x-annotation-box]{ - display: block; -} - -.htmlview .annotation-inline-box > span[x-annotation-box] { - display: none; - width: 300px; - font-size: 10pt; - line-height: 12pt; - font-weight: normal; - font-style: normal; - background: #fffe93; - border: 1px solid black; - border-radius: 10px; - -moz-border-radius: 10px; - -webkit-border-radius: 10px; - padding: 3px 5px; - text-decoration: none; - z-index: 600; -} - -/* - * Przypisy na końcu utworu (aktualnie nieuzywane) - */ -.htmlview .annotations-block { - counter-reset: secondary; -} - -.htmlview .annotations-block .annotation-body { - position: relative; - padding-left: 2.5em; - padding-top: 0.2em; -} - -.htmlview .annotations-block .annotation-backref { - position: absolute; - top: 0.4em; - left: -0.4em; - width: 2.5em; - text-align: right; -} +.htmlview .annotation-inline-box { + &:hover > span[x-annotation-box] { + display: block; + } -.htmlview .annotations-block .annotation-backref:before { - content: "[" counter(secondary) "]"; - counter-increment: secondary; + > span[x-annotation-box] { + display: none; + width: 300px; + font-size: 10pt; + line-height: 12pt; + font-weight: normal; + font-style: normal; + background: #fffe93; + border: 1px solid black; + border-radius: 10px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + padding: 3px 5px; + text-decoration: none; + z-index: 600; + + position: absolute; + left: 0; + top: 1em; + + &.editing { + display: block; + } + } + } /* diff --git a/src/redakcja/static/css/master.css b/src/redakcja/static/css/master.css index 566183e9..9d3744ba 100644 --- a/src/redakcja/static/css/master.css +++ b/src/redakcja/static/css/master.css @@ -161,6 +161,17 @@ img.tabclose { #simple-editor { display: flex; flex-direction: column; + position: relative; +} + +#simple-editor #bubbles { + position: absolute; + bottom: 10px; + right: 20px; +} +#simple-editor #bubbles .badge { + display: block; + margin-top: 5px; } .htmlview { @@ -178,14 +189,9 @@ img.tabclose { opacity: 0.4; } -.poezja_cyt { - margin:1.5em 2em 0; - font-size:0.875em -} -.wers_akap { - padding-left: 1em; -} + + .saveNotify { position:absolute; diff --git a/src/redakcja/static/js/wiki/caret.js b/src/redakcja/static/js/wiki/caret.js new file mode 100644 index 00000000..8b3cbd84 --- /dev/null +++ b/src/redakcja/static/js/wiki/caret.js @@ -0,0 +1,256 @@ +class Caret { + constructor(view) { + let self = this; + self.view = view; + self.singleClick = false; + + let caret = this.element = $(''); + + // When user writes into caret, add it to the document. + $('textarea', caret).on('input', function() { + let v = $(this).val(); + $(this).val(''); + self.insertChar(v); + + }); + + // On click on x-node element, set caret position. + self.view.on('click', '*[x-node]', function(e) { + if (e.redakcja_caret_inserted) return; + e.redakcja_caret_inserted = true; + + if (self.singleClick) { + self.singleClick = false; + return; + } + + self.detach(); + + var selection = window.getSelection(); + if (!selection.isCollapsed) return; + + self.singleClick = true; + setTimeout(function() { + if (self.singleClick) { + self.element.insertBefore( + selection.anchorNode.splitText( + selection.anchorOffset + ) + ) + self.focus(); + } + self.singleClick = false; + }, 250); + + }); + + self.element.on('keydown', function(e) { + console.log('KEY'); + + // TODO: + // delete selection? + + // cases: + // we are in (no going up) + // we are in (can go up) + // we are next to (can go inside) + + switch (e.key) { + case "ArrowRight": + if (e.shiftKey) { + self.detach(); + return; + } + + self.moveRight(); + break; + case "ArrowLeft": + if (e.shiftKey) { + self.detach(); + return; + } + + self.moveLeft(); + break; + case "ArrowUp": + if (e.shiftKey) { + self.detach(); + return; + } + break; + case "ArrowDown": + if (e.shiftKey) { + self.detach(); + return; + } + break; + case "Backspace": + self.deleteBefore(); + break; + case "Delete": + self.deleteAfter(); + break; + case "Enter": + self.splitBlock(); + break; + // default: + // console.log('key', e.key, e.code); + } + }) + } + + get attached() { + return this.element.parent().length; + } + + detach() { + console.log(this.view); + + let p; + if (this.attached) { + p = this.element.parent()[0] + this.element.detach(); + p.normalize() + } + } + + focus() { + $("textarea", self.element).focus(); + } + + normalize() { + this.element.parent()[0].normalize(); + } + + insertChar(ch) { + $(document.createTextNode(ch)).insertBefore(this.element); + this.normalize(); + } + + deleteBefore() { + let contents = this.element.parent().contents(); + // Find the text before caret. + let textBefore = contents[contents.index(this.element) - 1]; + + // Should be text, but what if not? + textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1); + this.normalize(); + + } + + deleteAfter() { + let contents = this.element.parent().contents(); + // Find the text after caret. + let textAfter = contents[contents.index(this.element) + 1]; + textAfter.textContent = textAfter.textContent.substr(1); + } + + splitBlock() { + let splitter = this.element; + let parent, newParent, splitIndex, index; + + while (!splitter.is('div[x-node]')) { + parent = splitter.parent(); + splitIndex = parent.contents().index(splitter); + newParent = parent.clone(); + index = parent.contents().length - 1; + while (index >= splitIndex) { + newParent.contents()[index].remove(); + --index; + } + while (index >= 0) { + console.log(newParent, index); + parent.contents()[index].remove(); + -- index; + } + newParent.insertBefore(parent); + + console.log('split', parent); + splitter = parent; + } + } + + moveLeft() { + this.move({ + move: -1, + edge: (i, l) => {return !i;}, + enter: (l) => {return l - 1;}, + splitTarget: (t) => {return t.splitText(t.length - 1);}, + noSplitTarget: (t) => {return t.splitText(t.length);}, + }) + } + + moveRight() { + this.move({ + move: 1, + edge: (i, l) => {return i == l - 1;}, + enter: (l) => {return 0;}, + splitTarget: (t) => {return t.splitText(1);}, + noSplitTarget: (t) => {return t;}, + }) + } + + move(opts) { + if (!this.attached) return; + + this.normalize(); + + let contents = this.element.parent().contents(); + let index = contents.index(this.element); + let target, moved, oldparent; + + let parent = this.element.parent()[0]; + + if (opts.edge(index, contents.length)) { + // We're at the end -- what to do? + // can we go up? + + if (parent.nodeName == 'EM') { + oldparent = parent; + parent = parent.parentNode; + contents = $(parent).contents(); + index = contents.index(oldparent); + } + } + + index += opts.move; + target = contents[index]; + moved = false; + + while (target.nodeType == 1) { + // we've encountered a node. + // can we go inside? + + if (target.nodeName == 'EM') { + // enter + parent = $(target); + contents = parent.contents(); + index = opts.enter(contents.length); + target = contents[index]; + + // what if it has no elements? + } else { + // skip + index += opts.move; // again, what if end? + target = contents[index]; + moved = true; + } + + // if editable? + // what if editable but empty? + + } + + if (target.nodeType == 3) { + if (!moved) { + target = opts.splitTarget(target); + } else { + target = opts.noSplitTarget(target); + } + + this.element.insertBefore(target); + } + this.normalize(); + this.focus(); + } +} diff --git a/src/redakcja/static/js/wiki/view_editor_wysiwyg.js b/src/redakcja/static/js/wiki/view_editor_wysiwyg.js index 82920fde..4554bbe7 100644 --- a/src/redakcja/static/js/wiki/view_editor_wysiwyg.js +++ b/src/redakcja/static/js/wiki/view_editor_wysiwyg.js @@ -1,229 +1,4 @@ (function($) { - class Caret { - constructor(view) { - self = this; - self.view = view; - self.singleClick = false; - - let caret = this.element = $(''); - - // When user writes into caret, add it to the document. - $('textarea', caret).on('input', function() { - let v = $(this).val(); - $(this).val(''); - self.insertChar(v); - - }); - - // On click on x-node element, set caret position. - self.view.on('click', '*[x-node]', function(e) { - if (e.redakcja_caret_inserted) return; - e.redakcja_caret_inserted = true; - - if (self.singleClick) { - self.singleClick = false; - return; - } - - self.detach(); - - var selection = window.getSelection(); - if (!selection.isCollapsed) return; - - self.singleClick = true; - setTimeout(function() { - if (self.singleClick) { - self.element.insertBefore( - selection.anchorNode.splitText( - selection.anchorOffset - ) - ) - self.focus(); - } - self.singleClick = false; - }, 250); - - }); - - self.view.on('keydown', function(e) { - // TODO: - // Enter (split block) - // delete selection? - - // cases: - // we are in (no going up) - // we are in (can go up) - // we are next to (can go inside) - - switch (e.key) { - case "ArrowRight": - if (e.shiftKey) { - self.detach(); - return; - } - - self.moveRight(); - break; - case "ArrowLeft": - if (e.shiftKey) { - self.detach(); - return; - } - - self.moveLeft(); - break; - case "ArrowUp": - if (e.shiftKey) { - self.detach(); - return; - } - break; - case "ArrowDown": - if (e.shiftKey) { - self.detach(); - return; - } - break; - case "Backspace": - self.deleteBefore(); - break; - case "Delete": - self.deleteAfter(); - break; -// default: -// console.log('key', e.key, e.code); - } - }) - } - - get attached() { - return this.element.parent().length; - } - - detach() { - let p; - if (this.attached) { - p = this.element.parent()[0] - this.element.detach(); - p.normalize() - } - } - - focus() { - $("textarea", self.element).focus(); - } - - normalize() { - this.element.parent()[0].normalize(); - } - - insertChar(ch) { - $(document.createTextNode(ch)).insertBefore(this.element); - this.normalize(); - } - - deleteBefore() { - let contents = this.element.parent().contents(); - // Find the text before caret. - let textBefore = contents[contents.index(this.element) - 1]; - - // Should be text, but what if not? - textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1); - this.normalize(); - - } - - deleteAfter() { - let contents = this.element.parent().contents(); - // Find the text after caret. - let textAfter = contents[contents.index(this.element) + 1]; - textAfter.textContent = textAfter.textContent.substr(1); - } - - moveLeft() { - this.move({ - move: -1, - edge: (i, l) => {return !i;}, - enter: (l) => {return l - 1;}, - splitTarget: (t) => {return t.splitText(t.length - 1);}, - noSplitTarget: (t) => {return t.splitText(t.length);}, - }) - } - - moveRight() { - this.move({ - move: 1, - edge: (i, l) => {return i == l - 1;}, - enter: (l) => {return 0;}, - splitTarget: (t) => {return t.splitText(1);}, - noSplitTarget: (t) => {return t;}, - }) - } - - move(opts) { - if (!this.attached) return; - this.normalize(); - - let contents = this.element.parent().contents(); - let index = contents.index(this.element); - let target, moved, oldparent; - - let parent = this.element.parent()[0]; - - if (opts.edge(index, contents.length)) { - // We're at the end -- what to do? - // can we go up? - - if (parent.nodeName == 'EM') { - oldparent = parent; - parent = parent.parentNode; - contents = $(parent).contents(); - index = contents.index(oldparent); - } - } - - index += opts.move; - target = contents[index]; - moved = false; - - while (target.nodeType == 1) { - // we've encountered a node. - // can we go inside? - - if (target.nodeName == 'EM') { - // enter - parent = $(target); - contents = parent.contents(); - index = opts.enter(contents.length); - target = contents[index]; - - // what if it has no elements? - } else { - // skip - index += opts.move; // again, what if end? - target = contents[index]; - moved = true; - } - - // if editable? - // what if editable but empty? - - } - - if (target.nodeType == 3) { - if (!moved) { - target = opts.splitTarget(target); - } else { - target = opts.noSplitTarget(target); - } - - this.element.insertBefore(target); - } - this.normalize(); - this.focus(); - } - } - /* Show theme to the user */ function selectTheme(themeId){ @@ -867,16 +642,17 @@ } if (editable.is('.annotation-inline-box')) { $('*[x-annotation-box]', editable).css({ - position: 'absolute', - left: event.clientX - editable.offset().left + 5, - top: event.clientY - editable.offset().top + 5 +// left: event.clientX - editable.offset().left + 5, +// top: event.clientY - editable.offset().top + 5 }).show(); } else { - $('*[x-annotation-box]').hide(); +// $('*[x-annotation-box]').hide(); } }); + perspective.caret = new Caret(element); + $('#insert-reference-button').click(function(){ addReference(); return false; @@ -915,6 +691,12 @@ selectTheme($(this).attr('theme-class')); }); + element.on('click', '.annotation', function(event) { + event.preventDefault(); + $('[x-annotation-box]', $(this).parent()).toggleClass('editing'); + + }); + old_callback.call(this); }; @@ -923,10 +705,6 @@ VisualPerspective.prototype = new $.wiki.Perspective(); - VisualPerspective.prototype.freezeState = function(){ - - }; - VisualPerspective.prototype.onEnter = function(success, failure){ $.wiki.Perspective.prototype.onEnter.call(this); @@ -949,10 +727,6 @@ var htmlView = $('#html-view'); htmlView.html(element); - perspective.caret = new Caret(htmlView); - - - htmlView.find('*[x-node]').dblclick(function(e) { if($(e.target).is('textarea')) return; @@ -976,6 +750,8 @@ VisualPerspective.prototype.onExit = function(success, failure){ var self = this; + self.caret.detach(); + $.blockUI({ message: 'Zapisywanie widoku...' }); @@ -1068,8 +844,6 @@ }); }; - - $.wiki.VisualPerspective = VisualPerspective; })(jQuery); diff --git a/src/redakcja/static/js/wiki/view_properties.js b/src/redakcja/static/js/wiki/view_properties.js index e6ad1ca2..5af43e67 100644 --- a/src/redakcja/static/js/wiki/view_properties.js +++ b/src/redakcja/static/js/wiki/view_properties.js @@ -57,6 +57,10 @@ self.edit($(this).data('node')); }); + $(document).on('click', '#bubbles .badge', function(e) { + self.edit($(this).data('node')); + }); + self.$pane.on('change', '.form-control', function() { let $input = $(this); @@ -97,6 +101,14 @@ }); self.$edited; }); + + self.$pane.on('click', '.current-convert', function() { + self.convert($(this).attr('data-to')); + }); + self.$pane.on('click', '#current-delete', function() { + self.delete(); + }); + oldCallback.call(this); }; @@ -111,11 +123,22 @@ let $node = $(element); $("#parents", self.$pane).empty(); + $("#bubbles").empty(); + + let b = $("
").text($node.attr('x-node')); + b.data('node', element); + $("#bubbles").append(b); + $node.parents('[x-node]').each(function() { let a = $("