]> _ Git - fluidbook-toolbox.git/commitdiff
wip #7634 @6:00
authorsoufiane <soufiane@cubedesigners.com>
Wed, 23 Jul 2025 16:24:04 +0000 (18:24 +0200)
committersoufiane <soufiane@cubedesigners.com>
Wed, 23 Jul 2025 16:24:04 +0000 (18:24 +0200)
app/Http/Controllers/Admin/Operations/FluidbookPublication/MarkdownOperation.php
resources/markdowneditor/js/markdowneditor.js
resources/markdowneditor/js/markdowneditor.save.js
resources/markdowneditor/js/markdowneditor.undo.js
resources/markdowneditor/js/markdowneditor.versions.js [new file with mode: 0644]
resources/markdowneditor/style/style.sass
resources/views/fluidbook_publication/markdown_editor.blade.php

index 5eaf70624b4b4feedd57a5363198c69820af175c..0fe0fad42fb44aa548b6a62747ff3664d7b6d0e9 100644 (file)
@@ -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];
+    }
+
 }
index 3100a829e894c3491d073883127bf082e322eefe..0cd065d6701364fb46b7d6b43b05c255159bb8bd 100644 (file)
@@ -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) {
index 610e3bc2adff2d2f9fa040814d181cdc4d343a5c..20c91fa6461e949d17e019b2c5d315273498fa1c 100644 (file)
@@ -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 () {
index 4e0e70f0c91fd8cd2aa6f295d587b8c7d53d42ae..1796d9590a197d8c4e0285a72d26b29985c5b8f7 100644 (file)
@@ -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 (file)
index 0000000..d23ef97
--- /dev/null
@@ -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 = '<div class="row">';
+            item += '<div class="col1">';
+            item += '<div class="date">' + version.date + '</div>';
+            item += '<div class="name">' + version.name + '</div>';
+            item += '<div class="comments">' + version.comments + '</div>';
+            item += '<div class="col2">';
+            item += '<div class="actions"><a nohref data-action="versions.restoreVersion" data-action-args="' + actionArgs + '" data-icon="wayback-machine" data-tooltip="' + TRANSLATIONS.restore_version_tooltip + '" draggable="false"></a></div>';
+            item += '</div>'
+            item += '</div>'
+            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;
index f681ef024d1a699039a766b9b5c64149412e5f58..91d418cb2715cbc4b0bbfaba3a28cc0ecb29dde6 100644 (file)
@@ -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%
index 0b482657ccc41db93eaac4da928e53ee1f223e3a..862447dcb23ad6c3ea25d541b34a761a73b863f5 100644 (file)
@@ -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"></a>
             </div>
             <div class="markdown-revision-panel">
+                <div id="markdown-panel-versions">
+                    <div id="markdown-panel-versions-list"></div>
+                </div>
             </div>
             <div class="handle"></div>
         </div>
@@ -29,7 +92,7 @@
                        data-tooltip="{{__('Annuler la dernière modification')}} (Ctrl+Z)"
                        data-key="ctrl+z" data-key-skipintextfields></a>
                     <a href="#" data-icon="redo" data-action="undo.redo"
-                       data-tooltip="{{__('Rétablir la derière modification')}} (Ctrl+Maj+Z)"
+                       data-tooltip="{{__('Rétablir la dernière modification')}} (Ctrl+Maj+Z)"
                        data-key="ctrl+shift+z" data-key-skipintextfields></a>
                     <div class="separator"></div>
                 </nav>
 
 @push('markdown_scripts')
     <script>
+        var TRANSLATIONS = @json($translations);
         var FLUIDBOOK_DATA = @json($fbdata);
         var MARKDOWN_DATA = @json($contents['pages']);
+        console.log(MARKDOWN_DATA)
     </script>
     <script
         src="/packages/markdowneditor/js/markdowneditor.js?v={{filemtime(public_path('packages/markdowneditor/js/markdowneditor.js'))}}"></script>