]> BookStack Code Mirror - bookstack/commitdiff
Migrated entity selector out of angular
authorDan Brown <redacted>
Sun, 27 Aug 2017 13:31:34 +0000 (14:31 +0100)
committerDan Brown <redacted>
Sun, 27 Aug 2017 13:31:34 +0000 (14:31 +0100)
resources/assets/js/components/entity-selector-popup.js [new file with mode: 0644]
resources/assets/js/components/entity-selector.js [new file with mode: 0644]
resources/assets/js/components/index.js
resources/assets/js/components/notification.js
resources/assets/js/directives.js
resources/assets/js/dom-polyfills.js [new file with mode: 0644]
resources/assets/js/global.js
resources/assets/js/pages/page-form.js
resources/views/components/entity-selector-popup.blade.php
resources/views/components/entity-selector.blade.php

diff --git a/resources/assets/js/components/entity-selector-popup.js b/resources/assets/js/components/entity-selector-popup.js
new file mode 100644 (file)
index 0000000..64c0c62
--- /dev/null
@@ -0,0 +1,47 @@
+
+class EntitySelectorPopup {
+
+    constructor(elem) {
+        this.elem = elem;
+        window.EntitySelectorPopup = this;
+
+        this.callback = null;
+        this.selection = null;
+
+        this.selectButton = elem.querySelector('.entity-link-selector-confirm');
+        this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this));
+
+        window.$events.listen('entity-select-change', this.onSelectionChange.bind(this));
+        window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this));
+    }
+
+    show(callback) {
+        this.callback = callback;
+        this.elem.components.overlay.show();
+    }
+
+    hide() {
+        this.elem.components.overlay.hide();
+    }
+
+    onSelectButtonClick() {
+        this.hide();
+        if (this.selection !== null && this.callback) this.callback(this.selection);
+    }
+
+    onSelectionConfirm(entity) {
+        this.hide();
+        if (this.callback && entity) this.callback(entity);
+    }
+
+    onSelectionChange(entity) {
+        this.selection = entity;
+        if (entity === null) {
+            this.selectButton.setAttribute('disabled', 'true');
+        } else {
+            this.selectButton.removeAttribute('disabled');
+        }
+    }
+}
+
+module.exports = EntitySelectorPopup;
\ No newline at end of file
diff --git a/resources/assets/js/components/entity-selector.js b/resources/assets/js/components/entity-selector.js
new file mode 100644 (file)
index 0000000..57b2499
--- /dev/null
@@ -0,0 +1,113 @@
+
+class EntitySelector {
+
+    constructor(elem) {
+        this.elem = elem;
+        this.search = '';
+        this.lastClick = 0;
+
+        let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
+        this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`);
+
+        this.input = elem.querySelector('[entity-selector-input]');
+        this.searchInput = elem.querySelector('[entity-selector-search]');
+        this.loading = elem.querySelector('[entity-selector-loading]');
+        this.resultsContainer = elem.querySelector('[entity-selector-results]');
+
+        this.elem.addEventListener('click', this.onClick.bind(this));
+
+        let lastSearch = 0;
+        this.searchInput.addEventListener('input', event => {
+            lastSearch = Date.now();
+            this.showLoading();
+            setTimeout(() => {
+                if (Date.now() - lastSearch < 199) return;
+                this.searchEntities(this.searchInput.value);
+            }, 200);
+        });
+        this.searchInput.addEventListener('keydown', event => {
+            if (event.keyCode === 13) event.preventDefault();
+        });
+
+        this.showLoading();
+        this.initialLoad();
+    }
+
+    showLoading() {
+        this.loading.style.display = 'block';
+        this.resultsContainer.style.display = 'none';
+    }
+
+    hideLoading() {
+        this.loading.style.display = 'none';
+        this.resultsContainer.style.display = 'block';
+    }
+
+    initialLoad() {
+        window.$http.get(this.searchUrl).then(resp => {
+            this.resultsContainer.innerHTML = resp.data;
+            this.hideLoading();
+        })
+    }
+
+    searchEntities(searchTerm) {
+        this.input.value = '';
+        let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`;
+        window.$http.get(url).then(resp => {
+            this.resultsContainer.innerHTML = resp.data;
+            this.hideLoading();
+        });
+    }
+
+    isDoubleClick() {
+        let now = Date.now();
+        let answer = now - this.lastClick < 300;
+        this.lastClick = now;
+        return answer;
+    }
+
+    onClick(event) {
+        let t = event.target;
+
+        if (t.matches('.entity-list a')) {
+            event.preventDefault();
+            event.stopPropagation();
+            let item = t.closest('[data-entity-type]');
+            this.selectItem(item);
+        } else if (t.matches('[data-entity-type]')) {
+            this.selectItem(t)
+        }
+
+    }
+
+    selectItem(item) {
+        let isDblClick = this.isDoubleClick();
+        let type = item.getAttribute('data-entity-type');
+        let id = item.getAttribute('data-entity-id');
+        let isSelected = item.classList.contains('selected') || isDblClick;
+
+        this.unselectAll();
+        this.input.value = isSelected ? `${type}:${id}` : '';
+
+        if (!isSelected) window.$events.emit('entity-select-change', null);
+        if (!isDblClick && !isSelected) return;
+
+        let link = item.querySelector('.entity-list-item-link').getAttribute('href');
+        let name = item.querySelector('.entity-list-item-name').textContent;
+        let data = {id: Number(id), name: name, link: link};
+
+        if (isDblClick) window.$events.emit('entity-select-confirm', data);
+        if (isSelected) window.$events.emit('entity-select-change', data);
+    }
+
+    unselectAll() {
+        let selected = this.elem.querySelectorAll('.selected');
+        for (let i = 0, len = selected.length; i < len; i++) {
+            selected[i].classList.remove('selected');
+            selected[i].classList.remove('primary-background');
+        }
+    }
+
+}
+
+module.exports = EntitySelector;
\ No newline at end of file
index 43466a0d9c1ebb804589782a52e5125f76444e0b..a324ab0c907bdd494798d29499bc5d15763f6c5c 100644 (file)
@@ -6,6 +6,8 @@ let componentMapping = {
     'notification': require('./notification'),
     'chapter-toggle': require('./chapter-toggle'),
     'expand-toggle': require('./expand-toggle'),
+    'entity-selector-popup': require('./entity-selector-popup'),
+    'entity-selector': require('./entity-selector'),
 };
 
 window.components = {};
index 1a981970267e3376806f04ffc301ff6756d283a8..daef5bd6f4e178e8b66df8c6eefecdfc859bcf08 100644 (file)
@@ -6,7 +6,7 @@ class Notification {
         this.type = elem.getAttribute('notification');
         this.textElem = elem.querySelector('span');
         this.autohide = this.elem.hasAttribute('data-autohide');
-        window.Events.listen(this.type, text => {
+        window.$events.listen(this.type, text => {
             this.show(text);
         });
         elem.addEventListener('click', this.hide.bind(this));
index fc92121ff7b1cd35b7ff808040d97323c4754fd8..8813eb88183383c57f01923ad6d86d9d30276904 100644 (file)
@@ -252,7 +252,7 @@ module.exports = function (ngApp, events) {
                 // Show the popup link selector and insert a link when finished
                 function showLinkSelector() {
                     let cursorPos = cm.getCursor('from');
-                    window.showEntityLinkSelector(entity => {
+                    window.EntitySelectorPopup.show(entity => {
                         let selectedText = cm.getSelection() || entity.name;
                         let newText = `[${selectedText}](${entity.link})`;
                         cm.focus();
@@ -387,154 +387,6 @@ module.exports = function (ngApp, events) {
         }
     }]);
 
-    ngApp.directive('entityLinkSelector', [function($http) {
-        return {
-            restrict: 'A',
-            link: function(scope, element, attrs) {
-
-                const selectButton = element.find('.entity-link-selector-confirm');
-                let callback = false;
-                let entitySelection = null;
-
-                // Handle entity selection change, Stores the selected entity locally
-                function entitySelectionChange(entity) {
-                    entitySelection = entity;
-                    if (entity === null) {
-                        selectButton.attr('disabled', 'true');
-                    } else {
-                        selectButton.removeAttr('disabled');
-                    }
-                }
-                events.listen('entity-select-change', entitySelectionChange);
-
-                // Handle selection confirm button click
-                selectButton.click(event => {
-                    hide();
-                    if (entitySelection !== null) callback(entitySelection);
-                });
-
-                // Show selector interface
-                function show() {
-                    element.fadeIn(240);
-                }
-
-                // Hide selector interface
-                function hide() {
-                    element.fadeOut(240);
-                }
-                scope.hide = hide;
-
-                // Listen to confirmation of entity selections (doubleclick)
-                events.listen('entity-select-confirm', entity => {
-                    hide();
-                    callback(entity);
-                });
-
-                // Show entity selector, Accessible globally, and store the callback
-                window.showEntityLinkSelector = function(passedCallback) {
-                    show();
-                    callback = passedCallback;
-                };
-
-            }
-        };
-    }]);
-
-
-    ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) {
-        return {
-            restrict: 'A',
-            scope: true,
-            link: function (scope, element, attrs) {
-                scope.loading = true;
-                scope.entityResults = false;
-                scope.search = '';
-
-                // Add input for forms
-                const input = element.find('[entity-selector-input]').first();
-
-                // Detect double click events
-                let lastClick = 0;
-                function isDoubleClick() {
-                    let now = Date.now();
-                    let answer = now - lastClick < 300;
-                    lastClick = now;
-                    return answer;
-                }
-
-                // Listen to entity item clicks
-                element.on('click', '.entity-list a', function(event) {
-                    event.preventDefault();
-                    event.stopPropagation();
-                    let item = $(this).closest('[data-entity-type]');
-                    itemSelect(item, isDoubleClick());
-                });
-                element.on('click', '[data-entity-type]', function(event) {
-                    itemSelect($(this), isDoubleClick());
-                });
-
-                // Select entity action
-                function itemSelect(item, doubleClick) {
-                    let entityType = item.attr('data-entity-type');
-                    let entityId = item.attr('data-entity-id');
-                    let isSelected = !item.hasClass('selected') || doubleClick;
-                    element.find('.selected').removeClass('selected').removeClass('primary-background');
-                    if (isSelected) item.addClass('selected').addClass('primary-background');
-                    let newVal = isSelected ? `${entityType}:${entityId}` : '';
-                    input.val(newVal);
-
-                    if (!isSelected) {
-                        events.emit('entity-select-change', null);
-                    }
-
-                    if (!doubleClick && !isSelected) return;
-
-                    let link = item.find('.entity-list-item-link').attr('href');
-                    let name = item.find('.entity-list-item-name').text();
-
-                    if (doubleClick) {
-                        events.emit('entity-select-confirm', {
-                            id: Number(entityId),
-                            name: name,
-                            link: link
-                        });
-                    }
-
-                    if (isSelected) {
-                        events.emit('entity-select-change', {
-                            id: Number(entityId),
-                            name: name,
-                            link: link
-                        });
-                    }
-                }
-
-                // Get search url with correct types
-                function getSearchUrl() {
-                    let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter');
-                    return window.baseUrl(`/ajax/search/entities?types=${types}`);
-                }
-
-                // Get initial contents
-                $http.get(getSearchUrl()).then(resp => {
-                    scope.entityResults = $sce.trustAsHtml(resp.data);
-                    scope.loading = false;
-                });
-
-                // Search when typing
-                scope.searchEntities = function() {
-                    scope.loading = true;
-                    input.val('');
-                    let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search);
-                    $http.get(url).then(resp => {
-                        scope.entityResults = $sce.trustAsHtml(resp.data);
-                        scope.loading = false;
-                    });
-                };
-            }
-        };
-    }]);
-
     ngApp.directive('commentReply', [function () {
         return {
             restrict: 'E',
diff --git a/resources/assets/js/dom-polyfills.js b/resources/assets/js/dom-polyfills.js
new file mode 100644 (file)
index 0000000..fcd89b7
--- /dev/null
@@ -0,0 +1,20 @@
+/**
+ * Polyfills for DOM API's
+ */
+
+if (!Element.prototype.matches) {
+    Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
+}
+
+if (!Element.prototype.closest) {
+    Element.prototype.closest = function (s) {
+        var el = this;
+        var ancestor = this;
+        if (!document.documentElement.contains(el)) return null;
+        do {
+            if (ancestor.matches(s)) return ancestor;
+            ancestor = ancestor.parentElement;
+        } while (ancestor !== null);
+        return null;
+    };
+}
\ No newline at end of file
index ee7cf3cc12913926a7dee6557ba33d18cf4ff959..85f9f77a6867504572d4ba206f83a255c648ea21 100644 (file)
@@ -1,5 +1,6 @@
 "use strict";
 require("babel-polyfill");
+require('./dom-polyfills');
 
 // Url retrieval function
 window.baseUrl = function(path) {
@@ -13,9 +14,11 @@ window.baseUrl = function(path) {
 class EventManager {
     constructor() {
         this.listeners = {};
+        this.stack = [];
     }
 
     emit(eventName, eventData) {
+        this.stack.push({name: eventName, data: eventData});
         if (typeof this.listeners[eventName] === 'undefined') return this;
         let eventsToStart = this.listeners[eventName];
         for (let i = 0; i < eventsToStart.length; i++) {
@@ -32,7 +35,7 @@ class EventManager {
     }
 }
 
-window.Events = new EventManager();
+window.$events = new EventManager();
 
 const Vue = require("vue");
 const axios = require("axios");
@@ -47,13 +50,13 @@ axiosInstance.interceptors.request.use(resp => {
     return resp;
 }, err => {
     if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err);
-    if (typeof err.response.data.error !== "undefined") window.Events.emit('error', err.response.data.error);
-    if (typeof err.response.data.message !== "undefined") window.Events.emit('error', err.response.data.message);
+    if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error);
+    if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message);
 });
 window.$http = axiosInstance;
 
 Vue.prototype.$http = axiosInstance;
-Vue.prototype.$events = window.Events;
+Vue.prototype.$events = window.$events;
 
 
 // AngularJS - Create application and load components
@@ -78,8 +81,8 @@ require("./components");
 // Load in angular specific items
 const Directives = require('./directives');
 const Controllers = require('./controllers');
-Directives(ngApp, window.Events);
-Controllers(ngApp, window.Events);
+Directives(ngApp, window.$events);
+Controllers(ngApp, window.$events);
 
 //Global jQuery Config & Extensions
 
index 08e4c0c34b61fa38ec05a7fe73da236452337537..497dc0212521797f54eb410d0bc178cfa534b44d 100644 (file)
@@ -274,7 +274,7 @@ module.exports = function() {
         file_browser_callback: function (field_name, url, type, win) {
 
             if (type === 'file') {
-                window.showEntityLinkSelector(function(entity) {
+                window.EntitySelectorPopup.show(function(entity) {
                     let originalField = win.document.getElementById(field_name);
                     originalField.value = entity.link;
                     $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
index 39d25bfa6e6c44c9862ad4b73953b46a53dcff65..ecd03c80f3fd32e66f5c82a33f782e90e1bbbaa0 100644 (file)
@@ -1,5 +1,5 @@
 <div id="entity-selector-wrap">
-    <div overlay entity-link-selector>
+    <div overlay entity-selector-popup>
         <div class="popup-body small flex-child">
             <div class="popup-header primary-background">
                 <div class="popup-title">{{ trans('entities.entity_select') }}</div>
index 8fb2187e6ff7395e8264c657813990fb79472baf..03e2066ed194b9f6ad597a53d497f8b0bb88745e 100644 (file)
@@ -1,8 +1,8 @@
 <div class="form-group">
     <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
         <input type="hidden" entity-selector-input name="{{$name}}" value="">
-        <input type="text" placeholder="{{ trans('common.search') }}" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
-        <div class="text-center loading" ng-show="loading">@include('partials.loading-icon')</div>
-        <div ng-show="!loading" ng-bind-html="entityResults"></div>
+        <input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
+        <div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
+        <div entity-selector-results></div>
     </div>
 </div>
\ No newline at end of file