From 1a0f2e771ce590c7fbd320087012869bfa16c33e Mon Sep 17 00:00:00 2001 From: soufiane Date: Wed, 23 Jul 2025 18:24:04 +0200 Subject: [PATCH] wip #7634 @6:00 --- .../MarkdownOperation.php | 92 +++++++++++++++++++ resources/markdowneditor/js/markdowneditor.js | 29 +++++- .../markdowneditor/js/markdowneditor.save.js | 20 ++-- .../markdowneditor/js/markdowneditor.undo.js | 9 +- .../js/markdowneditor.versions.js | 74 +++++++++++++++ resources/markdowneditor/style/style.sass | 37 ++++++++ .../markdown_editor.blade.php | 67 +++++++++++++- 7 files changed, 309 insertions(+), 19 deletions(-) create mode 100644 resources/markdowneditor/js/markdowneditor.versions.js diff --git a/app/Http/Controllers/Admin/Operations/FluidbookPublication/MarkdownOperation.php b/app/Http/Controllers/Admin/Operations/FluidbookPublication/MarkdownOperation.php index 5eaf70624..0fe0fad42 100644 --- a/app/Http/Controllers/Admin/Operations/FluidbookPublication/MarkdownOperation.php +++ b/app/Http/Controllers/Admin/Operations/FluidbookPublication/MarkdownOperation.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers\Admin\Operations\FluidbookPublication; +use App\Fluidbook\Link\LinksData; use App\Models\FluidbookPublication; +use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; @@ -10,10 +12,13 @@ use Cubist\Util\Files\Files; trait MarkdownOperation { + static $_names = []; protected function setupMarkdownRoutes($segment, $routeName, $controller) { Route::match(['get'], $segment . '/{id}/edit/markdown', $controller . '@markdown')->name('fluidbook_markdowneditor'); + Route::match(['get'], $segment . '/{id}/edit/markdown/versions', $controller . '@getMarkdownsVersions'); Route::match(['get'], $segment . '/{id}/markdown', $controller . '@getFilesById'); + Route::match(['put'], $segment . '/{id}/save/markdown', $controller . '@saveMarkdown'); } public function markdown($id) @@ -28,4 +33,91 @@ trait MarkdownOperation return view('fluidbook_publication.markdown_editor', ['contents' => $contents, 'version' => 'stable', 'id' => $id, 'fluidbook' => $fluidbook, 'access' => "", 'token' => $token]); } + + protected function saveMarkdown($fluidbook_id) + { + if (!FluidbookPublication::hasPermission($fluidbook_id)) { + abort(401); + } + + $markdowns = request('markdowns', '[]'); + $comments = request('message'); + $user_id = backpack_user()->id; + + /** @var FluidbookPublication $fluidbook */ + $fluidbook = FluidbookPublication::withoutGlobalScopes()->find($fluidbook_id); + $meta = ['comments' => $comments, 'user' => $user_id]; + + $base = self::getMarkdownsDir($fluidbook_id) . '/' . time(); + $latestMarkdown = self::getMarkdownsDir($fluidbook_id) . '/latest.markdown3.gz'; + $latestMeta = self::getMarkdownsDir($fluidbook_id) . '/latest.meta3.gz'; + file_put_contents($base . '.markdown3.gz', gzencode(json_encode($markdowns))); + file_put_contents($base . '.meta3.gz', gzencode(json_encode($meta))); + copy($base . '.markdown3.gz', $latestMarkdown); + copy($base . '.meta3.gz', $latestMeta); + + $fluidbook->touch(); + + return response()->json(['versions' => self::getMarkdownsVersions($fluidbook_id)]); + } + + public static function getMarkdownsDir($fluidbook_id) + { + return Files::mkdir(protected_path('fluidbookpublication/markdowns/' . $fluidbook_id)); + } + + public static function getMarkdownsVersions($book_id) + { + $dir = self::getMarkdownsDir($book_id); + $dr = opendir($dir); + $updates = []; + while ($f = readdir($dr)) { + if ($f === '.' || $f === '..') { + continue; + } + $e = explode('.', $f, 2); + if (($e[1] !== 'meta.gz' && $e[1] !== 'meta3.gz') || $e[0] === 'latest') { + continue; + } + + $updates[$e[0]] = self::getMeta($book_id, $e[0]); + } + krsort($updates); + + + $res = []; + foreach ($updates as $timestamp => $u) { + $u['name'] = self::getName($u['user']); + $u['date'] = date('Y-m-d H:i:s', $timestamp); + $u['timestamp'] = $timestamp; + $res[] = $u; + } + + return $res; + } + + public static function getMeta($book_id, $update = 'latest') + { + return json_decode(gzdecode(file_get_contents(Files::firstThatExists(self::getMarkdownsDir($book_id) . '/' . $update . '.meta3.gz', self::getMarkdownsDir($book_id) . '/' . $update . '.meta.gz'))), true); + } + + protected static function getName($u) + { + if (is_array($u)) { + if (isset($u['firstname'])) { + return $u['firstname'] . ' ' . $u['lastname']; + } else { + return '-'; + } + } + if (!isset(static::$_names[$u])) { + try { + static::$_names[$u] = User::find($u)->name; + } catch (\Exception $e) { + static::$_names[$u] = '-'; + } + } + return static::$_names[$u]; + } + } diff --git a/resources/markdowneditor/js/markdowneditor.js b/resources/markdowneditor/js/markdowneditor.js index 3100a829e..0cd065d67 100644 --- a/resources/markdowneditor/js/markdowneditor.js +++ b/resources/markdowneditor/js/markdowneditor.js @@ -1,6 +1,10 @@ import Editor from '@toast-ui/editor'; import MarkdowneditorToolbar from "./markdowneditor.toolbar"; import MarkdowneditorUndo from "./markdowneditor.undo"; +import MarkdowneditorSave from "./markdowneditor.save"; +import MarkdowneditorVersions from "./markdowneditor.versions"; +import tippy from "tippy.js"; +import 'tippy.js/dist/tippy.css'; window.$ = window.jQuery = require('jquery'); $.ajaxSetup({ @@ -26,12 +30,15 @@ MarkdownEditor.prototype = { init: function() { new MarkdowneditorToolbar(this); this.undo = new MarkdowneditorUndo(this); + this.save = new MarkdowneditorSave(this); + this.versions = new MarkdowneditorVersions(this); const $this = this this.initIcons(); this.markdown(); this.changePage(); + $(window).on('hashchange', function () { /*if ($this.maskHashEvent) { return; @@ -87,6 +94,14 @@ MarkdownEditor.prototype = { }); }, + initTooltips: function () { + $('[data-tooltip]:not(.init-tooltip)').each(function () { + let i = tippy($(this).get(0), {content: $(this).data('tooltip')}); + $(this).addClass('init-tooltip'); + $(this).data('tippyinstance', i) + }); + }, + loadPage: function() { this.loadPageHtml(this.currentPage); this.setContentMarkdown() @@ -147,7 +162,7 @@ MarkdownEditor.prototype = { this.editor.setMarkdown(state[lastKey]) this.editor.moveCursorToStart(true) }else { - this.contentMarkdown = MARKDOWN_DATA["p"+this.currentPage] + this.contentMarkdown = MARKDOWN_DATA[this.currentPage] this.editor.setMarkdown(this.contentMarkdown) this.editor.moveCursorToStart(true) } @@ -170,9 +185,15 @@ MarkdownEditor.prototype = { this.editor.getMarkdown(); }, - setCurrentState: function (state) { - this.editor.setMarkdown(state) - this.editor.moveCursorToStart(true) + setCurrentState: function (state,scroll) { + MARKDOWN_DATA[this.currentPage] = state + this.editor.reset() + this.editor.insertText(state) + this.editor.blur() + + this.editor.setScrollTop(20) + + //this.editor.moveCursorToStart(true) }, changePage: function(page) { diff --git a/resources/markdowneditor/js/markdowneditor.save.js b/resources/markdowneditor/js/markdowneditor.save.js index 610e3bc2a..20c91fa64 100644 --- a/resources/markdowneditor/js/markdowneditor.save.js +++ b/resources/markdowneditor/js/markdowneditor.save.js @@ -1,5 +1,5 @@ -function MarkdowneditorSave(linkeditor) { - this.linkeditor = linkeditor; +function MarkdowneditorSave(markdowneditor) { + this.markdowneditor = markdowneditor; this.init(); } @@ -57,31 +57,27 @@ MarkdowneditorSave.prototype = { } $.ajax({ - url: '/fluidbook-publication/' + FLUIDBOOK_DATA.id + '/save/links', method: 'post', data: { + url: '/fluidbook-publication/' + FLUIDBOOK_DATA.id + '/save/markdown', method: 'post', data: { _method: 'put', message: message, - rulers: JSON.stringify(window.RULERS), - links: JSON.stringify(window.LINKS), + markdowns: MARKDOWN_DATA, }, success: function (data) { if (notify) { - $this.linkeditor.notification(TRANSLATIONS.success_save); + //$this.linkeditor.notification(TRANSLATIONS.success_save); } clearTimeout($this.automaticSaveTimeout); $this.unsavedChanges = false; $this.runningAutomaticSaveTimeout = false; - window.ASSETS = data.assets; - $this.linkeditor.versions.setVersions(data.versions); + $this.markdowneditor.versions.setVersions(data.versions); callback(); }, error: function (jqXHR, status, error) { - $this.linkeditor.hasChanged(); - $this.linkeditor.notification(TRANSLATIONS.error_save + ' : ' + error, 'error'); + //$this.linkeditor.hasChanged(); + //$this.linkeditor.notification(TRANSLATIONS.error_save + ' : ' + error, 'error'); }, }); - - $this.linkeditor.links.loadFontSize(); }, automaticSave: function () { diff --git a/resources/markdowneditor/js/markdowneditor.undo.js b/resources/markdowneditor/js/markdowneditor.undo.js index 4e0e70f0c..1796d9590 100644 --- a/resources/markdowneditor/js/markdowneditor.undo.js +++ b/resources/markdowneditor/js/markdowneditor.undo.js @@ -3,6 +3,7 @@ function MarkdowneditorUndo(markdowneditor) { this.ignoreStatesChanges = false; this.states = []; this.indexes = []; + this.scrolls = []; this.init(); } @@ -65,6 +66,7 @@ MarkdowneditorUndo.prototype = { let index = this.indexes[this.markdowneditor.getCurrentPage()]; if (index === 0) { this.states[this.markdowneditor.getCurrentPage()] = []; + this.scrolls[this.markdowneditor.getCurrentPage()] = []; } let cs = this.markdowneditor.editor.getMarkdown(); @@ -78,6 +80,7 @@ MarkdowneditorUndo.prototype = { this.states[this.markdowneditor.getCurrentPage()] = this.states[this.markdowneditor.getCurrentPage()].slice(0, index); } this.states[this.markdowneditor.getCurrentPage()].push(cs); + this.scrolls[this.markdowneditor.getCurrentPage()].push($(".toastui-editor-md-preview").scrollTop()); this.indexes[this.markdowneditor.getCurrentPage()]++; //console.log('push current index', index, 'states length', this.states[this.markdowneditor.getCurrentPage()].length); @@ -92,8 +95,9 @@ MarkdowneditorUndo.prototype = { let index = this.indexes[this.markdowneditor.getCurrentPage()]; index--; let state = this.states[this.markdowneditor.getCurrentPage()][index - 1]; + let scroll = this.scrolls[this.markdowneditor.getCurrentPage()][index - 1]; this.ignoreStatesChanges = true; - this.markdowneditor.setCurrentState(state); + this.markdowneditor.setCurrentState(state,scroll); var $this = this; setTimeout(function () { $this.ignoreStatesChanges = false; @@ -111,8 +115,9 @@ MarkdowneditorUndo.prototype = { } let index = this.indexes[this.markdowneditor.getCurrentPage()]; let state = this.states[this.markdowneditor.getCurrentPage()][index]; + let scroll = this.scrolls[this.markdowneditor.getCurrentPage()][index]; this.ignoreStatesChanges = true; - this.markdowneditor.setCurrentState(state); + this.markdowneditor.setCurrentState(state,scroll); var $this = this; setTimeout(function () { $this.ignoreStatesChanges = false; diff --git a/resources/markdowneditor/js/markdowneditor.versions.js b/resources/markdowneditor/js/markdowneditor.versions.js new file mode 100644 index 000000000..d23ef9720 --- /dev/null +++ b/resources/markdowneditor/js/markdowneditor.versions.js @@ -0,0 +1,74 @@ +function MarkdownVersions(markdowneditor) { + this.markdowneditor = markdowneditor; + this.init(); +} + +MarkdownVersions.prototype = { + init: function () { + this.refresh(); + }, + + refresh: function () { + var $this = this; + $.ajax({ + url: '/fluidbook-publication/' + FLUIDBOOK_DATA.id + '/edit/markdown/versions', method: 'get', + success: function (data) { + $this.setVersions(data); + } + }); + }, + + setVersions: function (data) { + var list = $("#markdown-panel-versions-list"); + list.html(''); + $.each(data, function (k, version) { + let actionArgs = JSON.stringify([version.timestamp]); + var item = '
'; + item += '
'; + item += '
' + version.date + '
'; + item += '
' + version.name + '
'; + item += '
' + version.comments + '
'; + item += '
'; + item += '
'; + item += '
' + item += '
' + list.append(item); + }); + this.markdowneditor.initIcons(); + this.markdowneditor.initTooltips(); + }, + + restoreVersion: function (timestamp) { + var $this = this; + var callback = function () { + $this._restoreVersion(timestamp); + } + + //Restore links from 2022-12-07 13:37:15 + this.linkeditor.save.saveIfUnsavedChanges(TRANSLATIONS.before_restore_message,false,callback); + }, + + _restoreVersion(timestamp) { + $.ajax({ + url: '/fluidbook-publication/' + FLUIDBOOK_DATA.id + '/edit/links/versions/restore/' + timestamp + '', + success: function (data) { + window.location.reload(); + }, + }); + }, + + resize: function () { + var w = $("#linkeditor-panel-versions-list").outerWidth(); + if (w <= 0) { + return; + } + if (w < 300) { + $("#linkeditor-panel-versions-list").addClass('small'); + } else { + $("#linkeditor-panel-versions-list").removeClass('small'); + } + }, + + +}; +export default MarkdownVersions; diff --git a/resources/markdowneditor/style/style.sass b/resources/markdowneditor/style/style.sass index f681ef024..91d418cb2 100644 --- a/resources/markdowneditor/style/style.sass +++ b/resources/markdowneditor/style/style.sass @@ -4,6 +4,8 @@ body padding: 0 margin: 0 + font-family: "Montserrat", sans-serif + white-space: nowrap &.user-select-none user-select: none @@ -96,6 +98,40 @@ body &-panel position: relative height: 100% + flex: 1 + overflow: hidden auto + width: 0 + + #markdown-panel-versions + user-select: none + padding: 5px 10px + &-list + font-size: 12px + width: 100% + border-collapse: collapse + color: #5d5d5d + .row + padding: 5px 0 + vertical-align: top + border-bottom: 1px solid currentColor + position: relative + .col1 + width: calc(100% - 25px) + .col2 + position: absolute + top: 10px + right: 10px + width: 18px + padding: 0 + .date + font-weight: bold + .comments + font-style: italic + .actions a + background: transparent + cursor: pointer + &:hover + color: inherit &-toolbar background-color: #dbdddf @@ -160,6 +196,7 @@ body &-wrapper display: flex align-items: center + user-select: none &-page width: 100% diff --git a/resources/views/fluidbook_publication/markdown_editor.blade.php b/resources/views/fluidbook_publication/markdown_editor.blade.php index 0b482657c..862447dcb 100644 --- a/resources/views/fluidbook_publication/markdown_editor.blade.php +++ b/resources/views/fluidbook_publication/markdown_editor.blade.php @@ -3,6 +3,66 @@ $fbdata['settings']['pages']=$fbdata['pages']=$fluidbook->getPagesNumber(); $fbdata['settings']['imageFormat']=$fluidbook->getImageFormat(); $fbdata['page_dimensions']=[]; + $translations=[ + 'success_save'=>__('Liens sauvegardés'), + 'error_save'=>__('Une erreur s\'est produite lors de la sauvegarde des liens'), + 'error'=>__('Une erreur s\'est produite'), + 'manual_save_message'=>__('Sauvegarde manuelle'), + 'automatic_save_message'=>__('Sauvegarde automatique'), + 'warning_unsaved_changes'=>__('Des données n\'ont pas été sauvegardées'), + 'before_export_save_message'=>__("Sauvegarde avant export"), + 'before_restore_message'=>__("Sauvegarde avant la restauration des liens"), + 'upload_save_message'=>__("Après l'upload d'un fichier"), + 'restore_version_tooltip'=>__('Restaurer cette version'), + 'export_version_tooltip'=>__('Exporter les liens au format xlsx'), + 'delete_link'=>__('Supprimer le lien'), + 'edit_image_link'=>__('Editer les liens de l\'image'), + 'delete_selection'=>__('Supprimer la sélection'), + 'edit_link_order'=>__('Modifier l\'ordre'), + 'reorder_selection'=>__('Réordonner la sélection'), + 'reorder_lines'=>__('Par lignes'), + 'reorder_columns'=>__('Par colonnes'), + 'reorder_selection_lines'=>__('Réordonner la sélection par lignes'), + 'reorder_selection_columns'=>__('Réordonner la sélection par colonnes'), + 'move_order_start'=>__('Déplacer la sélection au début'), + 'move_order_up'=>__('Remonter la sélection'), + 'move_order_down'=>__('Descendre la sélection'), + 'move_order_end'=>__('Déplacer la sélection à la fin'), + 'move_up'=>__('Avant'), + 'move_down'=>__('Après'), + 'move_beginning'=>__('Au début'), + 'move_end'=>__('À la fin'), + 'order_all_lines'=>__('Réordonner les liens de toute la publication par lignes'), + 'order_all_columns'=>__('Réordonner les liens de toute la publication par colonnes'), + 'order_page_lines'=>__('Réordonner les liens de la page par lignes'), + 'order_page_columns'=>__('Réordonner les liens de la page par colonnes'), + 'select_all'=>__('Tout sélectionner'), + 'error_open_image_link'=>__('Impossible d\'ajouter des liens au contenu de ce lien'), + 'empty'=>__('Vide'), + 'copy_link_id'=>__('Copier l\'identifiant unique'), + 'level'=>__('Niveau'), + 'before_fix_drifted'=>__('Sauvegarde avant la correction de la dérive des liens'), + 'before_import_links_from_pdf'=>__('Sauvegarde avant de restaurer les liens du PDF'), + 'copy'=>__('Copier'), + 'cut'=>__('Couper'), + 'paste_here'=>__('Coller ici'), + 'paste_in_place'=>__('Coller en place'), + 'paste_on_left'=>__('Coller en décalant vers la gauche'), + 'paste_on_right'=>__('Coller en décalant vers la droite'), + "cover"=>__('Recouvrir').' ...', + 'cover_page_0'=>__('la page sans marge'), + 'cover_doublepage_0'=>__('la double-page sans marge'), + 'cover_page_1'=>__('la page avec une marge de :margin',['margin'=>'1px']), + 'cover_doublepage_1'=>__('la double-page avec une marge de :margin',['margin'=>'1px']), + 'n_links_copied'=>__('%nb% liens copiés'), + 'n_links_cut'=>__('%nb% liens coupés'), + 'click_to_copy_id'=>__('Cliquer pour copier l\'identifiant du lien'), + 'id_copied'=>__('Identifiant copié !'), + 'lock'=>__('Vérouiller'), + 'fix_offset'=>__('Corriger décalage de page'), + 'interactive_links'=>__('Liens interactifs'), + 'noninteractive_links'=>__('Liens non-interactifs'), + ]; @endphp @extends('layouts.markdowneditor') @@ -17,6 +77,9 @@ data-tooltip="{{__('Paramètres du lien')}} (F8)" data-key="f8">
+
+
+
@@ -29,7 +92,7 @@ data-tooltip="{{__('Annuler la dernière modification')}} (Ctrl+Z)" data-key="ctrl+z" data-key-skipintextfields>
@@ -65,8 +128,10 @@ @push('markdown_scripts') -- 2.39.5