]> BookStack Code Mirror - bookstack/commitdiff
Finished moving tag-manager from a vue to a component
authorDan Brown <redacted>
Mon, 29 Jun 2020 21:11:03 +0000 (22:11 +0100)
committerDan Brown <redacted>
Mon, 29 Jun 2020 21:11:03 +0000 (22:11 +0100)
Now tags load with the page, not via AJAX.

21 files changed:
app/Actions/TagRepo.php
app/Http/Controllers/TagController.php
resources/js/components/add-remove-rows.js
resources/js/components/auto-suggest.js
resources/js/components/collapsible.js
resources/js/components/index.js
resources/js/components/sortable-list.js [new file with mode: 0644]
resources/js/components/tag-manager.js [new file with mode: 0644]
resources/js/services/util.js
resources/js/vues/components/autosuggest.js [deleted file]
resources/js/vues/tag-manager.js [deleted file]
resources/js/vues/vues.js
resources/views/books/form.blade.php
resources/views/chapters/form.blade.php
resources/views/common/home.blade.php
resources/views/components/tag-manager-list.blade.php
resources/views/components/tag-manager.blade.php
resources/views/pages/editor-toolbox.blade.php
resources/views/search/form/term-list.blade.php
resources/views/shelves/form.blade.php
routes/web.php

index b8b1eb464f72957b7e211cda8555b1a9d7bdc274..0297d8bc6997b790085a485b1761085cc946ce59 100644 (file)
@@ -2,71 +2,31 @@
 
 use BookStack\Auth\Permissions\PermissionService;
 use BookStack\Entities\Entity;
+use DB;
+use Illuminate\Support\Collection;
 
-/**
- * Class TagRepo
- * @package BookStack\Repos
- */
 class TagRepo
 {
 
     protected $tag;
-    protected $entity;
     protected $permissionService;
 
     /**
      * TagRepo constructor.
-     * @param \BookStack\Actions\Tag $attr
-     * @param \BookStack\Entities\Entity $ent
-     * @param \BookStack\Auth\Permissions\PermissionService $ps
      */
-    public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
+    public function __construct(Tag $tag, PermissionService $ps)
     {
-        $this->tag = $attr;
-        $this->entity = $ent;
+        $this->tag = $tag;
         $this->permissionService = $ps;
     }
 
-    /**
-     * Get an entity instance of its particular type.
-     * @param $entityType
-     * @param $entityId
-     * @param string $action
-     * @return \Illuminate\Database\Eloquent\Model|null|static
-     */
-    public function getEntity($entityType, $entityId, $action = 'view')
-    {
-        $entityInstance = $this->entity->getEntityInstance($entityType);
-        $searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
-        $searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
-        return $searchQuery->first();
-    }
-
-    /**
-     * Get all tags for a particular entity.
-     * @param string $entityType
-     * @param int $entityId
-     * @return mixed
-     */
-    public function getForEntity($entityType, $entityId)
-    {
-        $entity = $this->getEntity($entityType, $entityId);
-        if ($entity === null) {
-            return collect();
-        }
-
-        return $entity->tags;
-    }
-
     /**
      * Get tag name suggestions from scanning existing tag names.
      * If no search term is given the 50 most popular tag names are provided.
-     * @param $searchTerm
-     * @return array
      */
-    public function getNameSuggestions($searchTerm = false)
+    public function getNameSuggestions(?string $searchTerm): Collection
     {
-        $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
+        $query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
 
         if ($searchTerm) {
             $query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@@ -82,13 +42,10 @@ class TagRepo
      * Get tag value suggestions from scanning existing tag values.
      * If no search is given the 50 most popular values are provided.
      * Passing a tagName will only find values for a tags with a particular name.
-     * @param $searchTerm
-     * @param $tagName
-     * @return array
      */
-    public function getValueSuggestions($searchTerm = false, $tagName = false)
+    public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
     {
-        $query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
+        $query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
 
         if ($searchTerm) {
             $query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
@@ -96,7 +53,7 @@ class TagRepo
             $query = $query->orderBy('count', 'desc')->take(50);
         }
 
-        if ($tagName !== false) {
+        if ($tagName) {
             $query = $query->where('name', '=', $tagName);
         }
 
@@ -106,34 +63,28 @@ class TagRepo
 
     /**
      * Save an array of tags to an entity
-     * @return array|\Illuminate\Database\Eloquent\Collection
      */
-    public function saveTagsToEntity(Entity $entity, array $tags = [])
+    public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
     {
         $entity->tags()->delete();
-        $newTags = [];
 
-        foreach ($tags as $tag) {
-            if (trim($tag['name']) === '') {
-                continue;
-            }
-            $newTags[] = $this->newInstanceFromInput($tag);
-        }
+        $newTags = collect($tags)->filter(function ($tag) {
+            return boolval(trim($tag['name']));
+        })->map(function ($tag) {
+            return $this->newInstanceFromInput($tag);
+        })->all();
 
         return $entity->tags()->saveMany($newTags);
     }
 
     /**
      * Create a new Tag instance from user input.
-     * @param $input
-     * @return \BookStack\Actions\Tag
+     * Input must be an array with a 'name' and an optional 'value' key.
      */
-    protected function newInstanceFromInput($input)
+    protected function newInstanceFromInput(array $input): Tag
     {
         $name = trim($input['name']);
         $value = isset($input['value']) ? trim($input['value']) : '';
-        // Any other modification or cleanup required can go here
-        $values = ['name' => $name, 'value' => $value];
-        return $this->tag->newInstance($values);
+        return $this->tag->newInstance(['name' => $name, 'value' => $value]);
     }
 }
index 6abbeeebaa451e437e8b150a2ae496f15fe37827..8c6d6748fa5b79d41090e56f8fd1b1c73dab57c4 100644 (file)
@@ -10,7 +10,6 @@ class TagController extends Controller
 
     /**
      * TagController constructor.
-     * @param $tagRepo
      */
     public function __construct(TagRepo $tagRepo)
     {
@@ -18,39 +17,23 @@ class TagController extends Controller
         parent::__construct();
     }
 
-    /**
-     * Get all the Tags for a particular entity
-     * @param $entityType
-     * @param $entityId
-     * @return \Illuminate\Http\JsonResponse
-     */
-    public function getForEntity($entityType, $entityId)
-    {
-        $tags = $this->tagRepo->getForEntity($entityType, $entityId);
-        return response()->json($tags);
-    }
-
     /**
      * Get tag name suggestions from a given search term.
-     * @param Request $request
-     * @return \Illuminate\Http\JsonResponse
      */
     public function getNameSuggestions(Request $request)
     {
-        $searchTerm = $request->get('search', false);
+        $searchTerm = $request->get('search', null);
         $suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
         return response()->json($suggestions);
     }
 
     /**
      * Get tag value suggestions from a given search term.
-     * @param Request $request
-     * @return \Illuminate\Http\JsonResponse
      */
     public function getValueSuggestions(Request $request)
     {
-        $searchTerm = $request->get('search', false);
-        $tagName = $request->get('name', false);
+        $searchTerm = $request->get('search', null);
+        $tagName = $request->get('name', null);
         $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
         return response()->json($suggestions);
     }
index 81eeb43c42fc68b61f00e03c1398f84be1fc36c8..9a5f019c501e66c79e8855601eefd4e087e2d1bd 100644 (file)
@@ -1,4 +1,5 @@
 import {onChildEvent} from "../services/dom";
+import {uniqueId} from "../services/util";
 
 /**
  * AddRemoveRows
@@ -11,21 +12,43 @@ class AddRemoveRows {
         this.modelRow = this.$refs.model;
         this.addButton = this.$refs.add;
         this.removeSelector = this.$opts.removeSelector;
+        this.rowSelector = this.$opts.rowSelector;
         this.setupListeners();
     }
 
     setupListeners() {
-        this.addButton.addEventListener('click', e => {
-            const clone = this.modelRow.cloneNode(true);
-            clone.classList.remove('hidden');
-            this.modelRow.parentNode.insertBefore(clone, this.modelRow);
-        });
+        this.addButton.addEventListener('click', this.add.bind(this));
 
         onChildEvent(this.$el, this.removeSelector, 'click', (e) => {
-            const row = e.target.closest('tr');
+            const row = e.target.closest(this.rowSelector);
             row.remove();
         });
     }
+
+    // For external use
+    add() {
+        const clone = this.modelRow.cloneNode(true);
+        clone.classList.remove('hidden');
+        this.setClonedInputNames(clone);
+        this.modelRow.parentNode.insertBefore(clone, this.modelRow);
+        window.components.init(clone);
+    }
+
+    /**
+     * Update the HTML names of a clone to be unique if required.
+     * Names can use placeholder values. For exmaple, a model row
+     * may have name="tags[randrowid][name]".
+     * These are the available placeholder values:
+     * - randrowid - An random string ID, applied the same across the row.
+     * @param {HTMLElement} clone
+     */
+    setClonedInputNames(clone) {
+        const rowId = uniqueId();
+        const randRowIdElems = clone.querySelectorAll(`[name*="randrowid"]`);
+        for (const elem of randRowIdElems) {
+            elem.name = elem.name.split('randrowid').join(rowId);
+        }
+    }
 }
 
 export default AddRemoveRows;
\ No newline at end of file
index 7fce09890e735021996977ae6c9ae6bc899193b1..68de49b4a32740143e7fc6fe24e442c0e502af5e 100644 (file)
@@ -16,6 +16,7 @@ class AutoSuggest {
         this.input = this.$refs.input;
         this.list = this.$refs.list;
 
+        this.lastPopulated = 0;
         this.setupListeners();
     }
 
@@ -44,7 +45,10 @@ class AutoSuggest {
 
     selectSuggestion(value) {
         this.input.value = value;
+        this.lastPopulated = Date.now();
         this.input.focus();
+        this.input.dispatchEvent(new Event('input', {bubbles: true}));
+        this.input.dispatchEvent(new Event('change', {bubbles: true}));
         this.hideSuggestions();
     }
 
@@ -79,8 +83,12 @@ class AutoSuggest {
     }
 
     async requestSuggestions() {
+        if (Date.now() - this.lastPopulated < 50) {
+            return;
+        }
+
         const nameFilter = this.getNameFilterIfNeeded();
-        const search = this.input.value.slice(0, 3);
+        const search = this.input.value.slice(0, 3).toLowerCase();
         const suggestions = await this.loadSuggestions(search, nameFilter);
         let toShow = suggestions.slice(0, 6);
         if (search.length > 0) {
index a630f38f2a577591916f2e86796d8b796f4634a1..544f91008c7d13eaeec88a9f119ebfb47e839a85 100644 (file)
@@ -37,7 +37,7 @@ class Collapsible {
     }
 
     openIfContainsError() {
-        const error = this.content.querySelector('.text-neg');
+        const error = this.content.querySelector('.text-neg.text-small');
         if (error) {
             this.open();
         }
index 1cea8949e0b2e7463a7a0adbecba12dd76668e6a..68f97b2800d7b11fba5910b227d82ecc34774a89 100644 (file)
@@ -70,13 +70,20 @@ function initComponent(name, element) {
 function parseRefs(name, element) {
     const refs = {};
     const manyRefs = {};
+
     const prefix = `${name}@`
-    const refElems = element.querySelectorAll(`[refs*="${prefix}"]`);
+    const selector = `[refs*="${prefix}"]`;
+    const refElems = [...element.querySelectorAll(selector)];
+    if (element.matches(selector)) {
+        refElems.push(element);
+    }
+
     for (const el of refElems) {
         const refNames = el.getAttribute('refs')
             .split(' ')
             .filter(str => str.startsWith(prefix))
-            .map(str => str.replace(prefix, ''));
+            .map(str => str.replace(prefix, ''))
+            .map(kebabToCamel);
         for (const ref of refNames) {
             refs[ref] = el;
             if (typeof manyRefs[ref] === 'undefined') {
diff --git a/resources/js/components/sortable-list.js b/resources/js/components/sortable-list.js
new file mode 100644 (file)
index 0000000..6efcb4e
--- /dev/null
@@ -0,0 +1,19 @@
+import Sortable from "sortablejs";
+
+/**
+ * SortableList
+ * @extends {Component}
+ */
+class SortableList {
+    setup() {
+        this.container = this.$el;
+        this.handleSelector = this.$opts.handleSelector;
+
+        new Sortable(this.container, {
+            handle: this.handleSelector,
+            animation: 150,
+        });
+    }
+}
+
+export default SortableList;
\ No newline at end of file
diff --git a/resources/js/components/tag-manager.js b/resources/js/components/tag-manager.js
new file mode 100644 (file)
index 0000000..99302b6
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * TagManager
+ * @extends {Component}
+ */
+class TagManager {
+    setup() {
+        this.addRemoveComponentEl = this.$refs.addRemove;
+        this.container = this.$el;
+        this.rowSelector = this.$opts.rowSelector;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        this.container.addEventListener('change', event => {
+            const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows'];
+            if (!this.hasEmptyRows()) {
+                addRemoveComponent.add();
+            }
+        });
+    }
+
+    hasEmptyRows() {
+        const rows = this.container.querySelectorAll(this.rowSelector);
+        const firstEmpty = [...rows].find(row => {
+            return [...row.querySelectorAll('input')].filter(input => input.value).length === 0;
+        });
+        return firstEmpty !== undefined;
+    }
+}
+
+export default TagManager;
\ No newline at end of file
index b44b7de6c2f62365e20fbddba8fda6b51115c0b8..de2ca20c13eb934b00a475b817489e9fa29d9670 100644 (file)
@@ -60,4 +60,14 @@ export function escapeHtml(unsafe) {
         .replace(/>/g, "&gt;")
         .replace(/"/g, "&quot;")
         .replace(/'/g, "&#039;");
+}
+
+/**
+ * Generate a random unique ID.
+ *
+ * @returns {string}
+ */
+export function uniqueId() {
+    const S4 = () => (((1+Math.random())*0x10000)|0).toString(16).substring(1);
+    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
 }
\ No newline at end of file
diff --git a/resources/js/vues/components/autosuggest.js b/resources/js/vues/components/autosuggest.js
deleted file mode 100644 (file)
index f4bb3d8..0000000
+++ /dev/null
@@ -1,134 +0,0 @@
-
-const template = `
-    <div>
-        <input :value="value" :autosuggest-type="type" ref="input"
-            :placeholder="placeholder"
-            :name="name"
-            type="text"
-            @input="inputUpdate($event.target.value)"
-            @focus="inputUpdate($event.target.value)"
-            @blur="inputBlur"
-            @keydown="inputKeydown"
-            :aria-label="placeholder"
-            autocomplete="off"
-        />
-        <ul class="suggestion-box" v-if="showSuggestions">
-            <li v-for="(suggestion, i) in suggestions"
-                @click="selectSuggestion(suggestion)"
-                :class="{active: (i === active)}">{{suggestion}}</li>
-        </ul>
-    </div>
-`;
-
-function data() {
-    return {
-        suggestions: [],
-        showSuggestions: false,
-        active: 0,
-    };
-}
-
-const ajaxCache = {};
-
-const props = ['url', 'type', 'value', 'placeholder', 'name'];
-
-function getNameInputVal(valInput) {
-    let parentRow = valInput.parentNode.parentNode;
-    let nameInput = parentRow.querySelector('[autosuggest-type="name"]');
-    return (nameInput === null) ? '' : nameInput.value;
-}
-
-const methods = {
-
-    inputUpdate(inputValue) {
-        this.$emit('input', inputValue);
-        let params = {};
-
-        if (this.type === 'value') {
-            let nameVal = getNameInputVal(this.$el);
-            if (nameVal !== "") params.name = nameVal;
-        }
-
-        this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => {
-            if (inputValue.length === 0) {
-                this.displaySuggestions(suggestions.slice(0, 6));
-                return;
-            }
-            // Filter to suggestions containing searched term
-            suggestions = suggestions.filter(item => {
-                return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
-            }).slice(0, 4);
-            this.displaySuggestions(suggestions);
-        });
-    },
-
-    inputBlur() {
-        setTimeout(() => {
-            this.$emit('blur');
-            this.showSuggestions = false;
-        }, 100);
-    },
-
-    inputKeydown(event) {
-        if (event.key === 'Enter') event.preventDefault();
-        if (!this.showSuggestions) return;
-
-        // Down arrow
-        if (event.key === 'ArrowDown') {
-            this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
-        }
-        // Up Arrow
-        else if (event.key === 'ArrowUp') {
-            this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
-        }
-        // Enter key
-        else if ((event.key === 'Enter') && !event.shiftKey) {
-            this.selectSuggestion(this.suggestions[this.active]);
-        }
-        // Escape key
-        else if (event.key === 'Escape') {
-            this.showSuggestions = false;
-        }
-    },
-
-    displaySuggestions(suggestions) {
-        if (suggestions.length === 0) {
-            this.suggestions = [];
-            this.showSuggestions = false;
-            return;
-        }
-
-        this.suggestions = suggestions;
-        this.showSuggestions = true;
-        this.active = 0;
-    },
-
-    selectSuggestion(suggestion) {
-        this.$refs.input.value = suggestion;
-        this.$refs.input.focus();
-        this.$emit('input', suggestion);
-        this.showSuggestions = false;
-    },
-
-    /**
-     * Get suggestions from BookStack. Store and use local cache if already searched.
-     * @param {String} input
-     * @param {Object} params
-     */
-    getSuggestions(input, params) {
-        params.search = input;
-        const cacheKey = `${this.url}:${JSON.stringify(params)}`;
-
-        if (typeof ajaxCache[cacheKey] !== "undefined") {
-            return Promise.resolve(ajaxCache[cacheKey]);
-        }
-
-        return this.$http.get(this.url, params).then(resp => {
-            ajaxCache[cacheKey] = resp.data;
-            return resp.data;
-        });
-    }
-
-};
-
-export default {template, data, props, methods};
\ No newline at end of file
diff --git a/resources/js/vues/tag-manager.js b/resources/js/vues/tag-manager.js
deleted file mode 100644 (file)
index 65233cb..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-import draggable from 'vuedraggable';
-import autosuggest from './components/autosuggest';
-
-const data = {
-    entityId: false,
-    entityType: null,
-    tags: [],
-};
-
-const components = {draggable, autosuggest};
-const directives = {};
-
-const methods = {
-
-    addEmptyTag() {
-        this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
-    },
-
-    /**
-     * When an tag changes check if another empty editable field needs to be added onto the end.
-     * @param tag
-     */
-    tagChange(tag) {
-        let tagPos = this.tags.indexOf(tag);
-        if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag();
-    },
-
-    /**
-     * When an tag field loses focus check the tag to see if its
-     * empty and therefore could be removed from the list.
-     * @param tag
-     */
-    tagBlur(tag) {
-        let isLast = (this.tags.indexOf(tag) === this.tags.length-1);
-        if (tag.name !== '' || tag.value !== '' || isLast) return;
-        let cPos = this.tags.indexOf(tag);
-        this.tags.splice(cPos, 1);
-    },
-
-    removeTag(tag) {
-        let tagPos = this.tags.indexOf(tag);
-        if (tagPos === -1) return;
-        this.tags.splice(tagPos, 1);
-    },
-
-    getTagFieldName(index, key) {
-        return `tags[${index}][${key}]`;
-    },
-};
-
-function mounted() {
-    this.entityId = Number(this.$el.getAttribute('entity-id'));
-    this.entityType = this.$el.getAttribute('entity-type');
-
-    let url = window.baseUrl(`/ajax/tags/get/${this.entityType}/${this.entityId}`);
-    this.$http.get(url).then(response => {
-        let tags = response.data;
-        for (let i = 0, len = tags.length; i < len; i++) {
-            tags[i].key = Math.random().toString(36).substring(7);
-        }
-        this.tags = tags;
-        this.addEmptyTag();
-    });
-}
-
-export default {
-    data, methods, mounted, components, directives
-};
\ No newline at end of file
index f1872492903f28d0bdfd4681e73c3fcb7f73bab1..0d3817f0ed34ad0b4ed9f29ddf0d555ffbc2ae6e 100644 (file)
@@ -5,13 +5,11 @@ function exists(id) {
 }
 
 import imageManager from "./image-manager";
-import tagManager from "./tag-manager";
 import attachmentManager from "./attachment-manager";
 import pageEditor from "./page-editor";
 
 let vueMapping = {
     'image-manager': imageManager,
-    'tag-manager': tagManager,
     'attachment-manager': attachmentManager,
     'page-editor': pageEditor,
 };
index a3235036e02c81c76bfefa799787d3cfd27bf7e3..840d0604c8e0c22185d6e33ea43eac02e880ff88 100644 (file)
@@ -31,7 +31,7 @@
         <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => $book ?? null, 'entityType' => 'chapter'])
+        @include('components.tag-manager', ['entity' => $book ?? null])
     </div>
 </div>
 
index cd240e685dd651f5501a4659e74d8e145c0c1760..60cfe6674f1b7eeaf7382575a24e970264698593 100644 (file)
@@ -16,7 +16,7 @@
         <label for="tags">{{ trans('entities.chapter_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => isset($chapter)?$chapter:null, 'entityType' => 'chapter'])
+        @include('components.tag-manager', ['entity' => $chapter ?? null])
     </div>
 </div>
 
index 7df6d4ce6f0aad67dafca4e92d00922c852cfc1e..2631f1a57ed878b01ad5099f7a539e07169eba58 100644 (file)
@@ -66,7 +66,4 @@
         </div>
     </div>
 
-    @include('components.tag-manager', ['entity' => \BookStack\Entities\Book::find(1), 'entityType' => 'book'])
-
-
 @stop
index 99ee8778294bf1844a58140ee58844679f98d087..6fbce2f882eb01f23173ccf663d621731efc9882 100644 (file)
@@ -1,5 +1,5 @@
-@foreach(array_merge($tags, [new \BookStack\Actions\Tag]) as $index => $tag)
-    <div class="card drag-card">
+@foreach(array_merge($tags, [null, null]) as $index => $tag)
+    <div class="card drag-card {{ $loop->last ? 'hidden' : '' }}" @if($loop->last) refs="add-remove-rows@model" @endif>
         <div class="handle">@icon('grip')</div>
         @foreach(['name', 'value'] as $type)
             <div component="auto-suggest"
@@ -9,16 +9,16 @@
                 <input value="{{ $tag->$type ?? '' }}"
                        placeholder="{{ trans('entities.tag_' . $type) }}"
                        aria-label="{{ trans('entities.tag_' . $type) }}"
-                       name="tags[{{ $index }}][{{ $type }}]"
+                       name="tags[{{ $loop->parent->last ? 'randrowid' : $index }}][{{ $type }}]"
                        type="text"
                        refs="auto-suggest@input"
                        autocomplete="off"/>
                 <ul refs="auto-suggest@list" class="suggestion-box dropdown-menu"></ul>
             </div>
         @endforeach
-        <button refs="tag-manager@remove" type="button"
+        <button type="button"
                 aria-label="{{ trans('entities.tags_remove') }}"
-                class="text-center drag-card-action text-neg {{ count($tags) > 0 ? '' : 'hidden' }}">
+                class="text-center drag-card-action text-neg">
             @icon('close')
         </button>
     </div>
index 0fab30d63f05cc87959e95cb28f151a10f6170a9..aad5fb9d696fe5cf9c7bebcc19ffd035acff6789 100644 (file)
@@ -1,24 +1,16 @@
-<div id="tag-manager" entity-id="{{ $entity->id ?? 0 }}" entity-type="{{ $entity ? $entity->getType() : $entityType }}">
-    <div class="tags">
-        <p class="text-muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
+<div components="tag-manager add-remove-rows"
+     option:add-remove-rows:row-selector=".card"
+     option:add-remove-rows:remove-selector="button.text-neg"
+     option:tag-manager:row-selector=".card:not(.hidden)"
+     refs="tag-manager@add-remove"
+     class="tags">
 
-        @include('components.tag-manager-list', ['tags' => $entity->tags->all() ?? []])
+        <p class="text-muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
 
-        <draggable :options="{handle: '.handle'}" :list="tags" element="div">
-            <div v-for="(tag, i) in tags" :key="tag.key" class="card drag-card">
-                <div class="handle" >@icon('grip')</div>
-                <div>
-                    <autosuggest url="{{ url('/ajax/tags/suggest/names') }}" type="name" class="outline" :name="getTagFieldName(i, 'name')"
-                                 v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_name') }}"/>
-                </div>
-                <div>
-                    <autosuggest url="{{ url('/ajax/tags/suggest/values') }}" type="value" class="outline" :name="getTagFieldName(i, 'value')"
-                                 v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"/>
-                </div>
-                <button type="button" aria-label="{{ trans('entities.tags_remove') }}" v-show="tags.length !== 1" class="text-center drag-card-action text-neg" @click="removeTag(tag)">@icon('close')</button>
-            </div>
-        </draggable>
+        <div component="sortable-list"
+             option:sortable-list:handle-selector=".handle">
+            @include('components.tag-manager-list', ['tags' => $entity->tags->all() ?? []])
+        </div>
 
-        <button @click="addEmptyTag" type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
-    </div>
+        <button refs="add-remove-rows@add" type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
 </div>
\ No newline at end of file
index 6ea651820ef8f46f437f35a635b20712606847ce..3741c9246ef982ffa75973196043e5f394fd57e3 100644 (file)
@@ -12,7 +12,7 @@
     <div toolbox-tab-content="tags">
         <h4>{{ trans('entities.page_tags') }}</h4>
         <div class="px-l">
-            @include('components.tag-manager', ['entity' => $page, 'entityType' => 'page'])
+            @include('components.tag-manager', ['entity' => $page])
         </div>
     </div>
 
index 435de73f1ea1dcedd5d472fa80c3534f578e6510..3fbfa18fef25fadf32adb306ceb6401bcc964df9 100644 (file)
@@ -4,6 +4,7 @@
 --}}
 <table component="add-remove-rows"
        option:add-remove-rows:remove-selector="button.text-neg"
+       option:add-remove-rows:row-selector="tr"
        class="no-style">
     @foreach(array_merge($currentList, ['']) as $term)
         <tr @if(empty($term)) class="hidden" refs="add-remove-rows@model" @endif>
index 19c5bbecd69ae95c70bd782703cfe377ddd32b30..e635455bfd93a359ae2d3352b716fd9dc399085d 100644 (file)
@@ -60,7 +60,7 @@
         <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
     </button>
     <div class="collapse-content" collapsible-content>
-        @include('components.tag-manager', ['entity' => $shelf ?? null, 'entityType' => 'bookshelf'])
+        @include('components.tag-manager', ['entity' => $shelf ?? null])
     </div>
 </div>
 
index 3e05e394d29698498e19f2ec739e17dd0d1a4a1c..6b7911825c57d5cce5e79dfc52d53e7ccd958665 100644 (file)
@@ -134,8 +134,7 @@ Route::group(['middleware' => 'auth'], function () {
     Route::delete('/ajax/page/{id}', 'PageController@ajaxDestroy');
 
     // Tag routes (AJAX)
-    Route::group(['prefix' => 'ajax/tags'], function() {
-        Route::get('/get/{entityType}/{entityId}', 'TagController@getForEntity');
+    Route::group(['prefix' => 'ajax/tags'], function () {
         Route::get('/suggest/names', 'TagController@getNameSuggestions');
         Route::get('/suggest/values', 'TagController@getValueSuggestions');
     });