Kaynağa Gözat

Pro text shortcuts feature

Vijayakrishnan Krishnan 4 yıl önce
ebeveyn
işleme
db99f4f477

+ 4 - 0
app/Models/Pro.php

@@ -69,4 +69,8 @@ class Pro extends Model
             ->where('code', 'NOT LIKE', 'RM%')
             ->get();
     }
+
+    public function shortcuts() {
+        return $this->hasMany(ProTextShortcut::class, 'pro_id');
+    }
 }

+ 12 - 0
app/Models/ProTextShortcut.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Models;
+
+# use Illuminate\Database\Eloquent\Model;
+
+class ProTextShortcut extends Model
+{
+
+    protected $table = 'pro_text_shortcut';
+
+}

+ 36 - 0
public/css/shortcut.css

@@ -0,0 +1,36 @@
+.stag-shortcuts {
+    position: absolute;
+    border: 1px solid #aaa;
+    background: #fff;
+    margin-top: 1.3rem;
+    display: none;
+    min-width: 160px;
+    max-width: 280px;
+}
+.stag-shortcuts>.sc {
+    font-size: 13px;
+    padding: 2px 6px;
+    white-space: nowrap;
+    width: 100%;
+    cursor: pointer;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.stag-shortcuts>.sc:hover {
+     background-color: aliceblue;
+}
+.stag-shortcuts>.sc:not(:last-child) {
+    border-bottom: 1px solid #ccc;
+}
+.stag-shortcuts>.sc.active {
+    background-color: #cfe1f1;
+}
+form#create-shortcut-form {
+    position: absolute;
+    background: #fff;
+    padding: 10px;
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    display: none;
+    z-index: 101;
+}

+ 65 - 0
public/js/find-event-handlers.js

@@ -0,0 +1,65 @@
+window.findEventHandlers = function (eventType, jqSelector) {
+    var results = [];
+    var $ = jQuery; // to avoid conflict between others frameworks like Mootools
+
+    var arrayIntersection = function (array1, array2) {
+        return $(array1).filter(function (index, element) {
+            return $.inArray(element, $(array2)) !== -1;
+        });
+    };
+
+    var haveCommonElements = function (array1, array2) {
+        return arrayIntersection(array1, array2).length !== 0;
+    };
+
+    var addEventHandlerInfo = function (element, event, $elementsCovered) {
+        var extendedEvent = event;
+        if ($elementsCovered !== void 0 && $elementsCovered !== null) {
+            $.extend(extendedEvent, {
+                targets: $elementsCovered.toArray()
+            });
+        }
+        var eventInfo;
+        var eventsInfo = $.grep(results, function (evInfo, index) {
+            return element === evInfo.element;
+        });
+
+        if (eventsInfo.length === 0) {
+            eventInfo = {
+                element: element,
+                events: [extendedEvent]
+            };
+            results.push(eventInfo);
+        } else {
+            eventInfo = eventsInfo[0];
+            eventInfo.events.push(extendedEvent);
+        }
+    };
+
+
+    var $elementsToWatch = $(jqSelector);
+    if (jqSelector === "*") //* does not include document and we might be interested in handlers registered there
+        $elementsToWatch = $elementsToWatch.add(document);
+    var $allElements = $("*").add(document);
+
+    $.each($allElements, function (elementIndex, element) {
+        var allElementEvents = $._data(element, "events");
+        if (allElementEvents !== void 0 && allElementEvents[eventType] !== void 0) {
+            var eventContainer = allElementEvents[eventType];
+            $.each(eventContainer, function (eventIndex, event) {
+                var isDelegateEvent = event.selector !== void 0 && event.selector !== null;
+                var $elementsCovered;
+                if (isDelegateEvent) {
+                    $elementsCovered = $(event.selector, element); //only look at children of the element, since those are the only ones the handler covers
+                } else {
+                    $elementsCovered = $(element); //just itself
+                }
+                if (haveCommonElements($elementsCovered, $elementsToWatch)) {
+                    addEventHandlerInfo(element, event, $elementsCovered);
+                }
+            });
+        }
+    });
+
+    return results;
+};

+ 22 - 68
public/js/mc.js

@@ -1,70 +1,3 @@
-var findEventHandlers = function (eventType, jqSelector) {
-    var results = [];
-    var $ = jQuery; // to avoid conflict between others frameworks like Mootools
-
-    var arrayIntersection = function (array1, array2) {
-        return $(array1).filter(function (index, element) {
-            return $.inArray(element, $(array2)) !== -1;
-        });
-    };
-
-    var haveCommonElements = function (array1, array2) {
-        return arrayIntersection(array1, array2).length !== 0;
-    };
-
-
-    var addEventHandlerInfo = function (element, event, $elementsCovered) {
-        var extendedEvent = event;
-        if ($elementsCovered !== void 0 && $elementsCovered !== null) {
-            $.extend(extendedEvent, {
-                targets: $elementsCovered.toArray()
-            });
-        }
-        var eventInfo;
-        var eventsInfo = $.grep(results, function (evInfo, index) {
-            return element === evInfo.element;
-        });
-
-        if (eventsInfo.length === 0) {
-            eventInfo = {
-                element: element,
-                events: [extendedEvent]
-            };
-            results.push(eventInfo);
-        } else {
-            eventInfo = eventsInfo[0];
-            eventInfo.events.push(extendedEvent);
-        }
-    };
-
-
-    var $elementsToWatch = $(jqSelector);
-    if (jqSelector === "*") //* does not include document and we might be interested in handlers registered there
-        $elementsToWatch = $elementsToWatch.add(document);
-    var $allElements = $("*").add(document);
-
-    $.each($allElements, function (elementIndex, element) {
-        var allElementEvents = $._data(element, "events");
-        if (allElementEvents !== void 0 && allElementEvents[eventType] !== void 0) {
-            var eventContainer = allElementEvents[eventType];
-            $.each(eventContainer, function (eventIndex, event) {
-                var isDelegateEvent = event.selector !== void 0 && event.selector !== null;
-                var $elementsCovered;
-                if (isDelegateEvent) {
-                    $elementsCovered = $(event.selector, element); //only look at children of the element, since those are the only ones the handler covers
-                } else {
-                    $elementsCovered = $(element); //just itself
-                }
-                if (haveCommonElements($elementsCovered, $elementsToWatch)) {
-                    addEventHandlerInfo(element, event, $elementsCovered);
-                }
-            });
-        }
-    });
-
-    return results;
-};
-
 window.top.addEventListener('popstate', function (event) {
     window.setTimeout(function () {
         hideMask();
@@ -105,6 +38,8 @@ $(document).ready(function () {
     initFastLoad();
     initPrimaryForm();
     initPatientPresenceIndicator();
+    runMCInitializers();
+
     if(typeof initializeCalendar !== 'undefined') {
         initializeCalendar();
     }
@@ -217,6 +152,7 @@ function onFastLoaded(_data, _href, _history) {
             initFastLoad(targetParent);
             initPrimaryForm();
             initPatientPresenceIndicator();
+            runMCInitializers();
             $(window).scrollTop(0);
         }, 50);
         if(typeof initializeCalendar !== 'undefined') {
@@ -339,7 +275,19 @@ function initQuillEdit(_selector = '.note-content[auto-edit]') {
     if(!$(_selector).length) return;
     var noteUid = $(_selector).attr('data-note-uid');
     var qe = new Quill(_selector, {
-        theme: 'snow'
+        theme: 'snow',
+        modules: {
+            keyboard: {
+                bindings: {
+                    handleEnter: {
+                        key: 13,
+                        handler: function() {
+                            if(!$('.stag-shortcuts:visible').length) return true;
+                        }
+                    }
+                }
+            }
+        }
     });
     var toolbar = $(qe.container).prev('.ql-toolbar');
     var saveButton = $('<button class="btn btn-sm btn-primary w-auto px-3 py-0 text-sm text-white save-note-content">Save</button>');
@@ -359,9 +307,15 @@ function initQuillEdit(_selector = '.note-content[auto-edit]') {
             }
         }, 'json');
     });
+
+    // add button for new shortcut
+    var newSCButton = $('<button class="btn btn-sm btn-default w-auto px-2 ml-2 border py-0 text-sm add-shortcut">+ Shortcut</button>');
+    toolbar.append(newSCButton);
+
     // qe.on('text-change', function() {
     //     saveButton.prop('disabled', false);
     // });
+    $('.ql-editor[contenteditable]').attr('with-shortcuts', 1);
 }
 
 var patientPresenceTimer = false;

+ 238 - 0
public/js/shortcut.js

@@ -0,0 +1,238 @@
+// shortcut suggest functionality
+// auto attaches to all [with-shortcuts] elements
+(function() {
+    let input = null, menu = null, options = [], index = -1;
+    let backtrackAfterApplying = false;
+    function getCaretPos(win) {
+        win = win || window;
+        let doc = win.document;
+        let sel, range, rects, rect;
+        let x = 0, y = 0;
+        if (win.getSelection) { // won't work if getSelection() isn't available!
+            sel = win.getSelection();
+            if(sel.toString() !== '') return null; // won't work if there isa
+            if (sel.rangeCount) {
+                range = sel.getRangeAt(0).cloneRange();
+                if (range.getClientRects) {
+                    range.collapse(true);
+                    let span = doc.createElement("span");
+                    if (span.getClientRects) {
+                        // Ensure span has dimensions and position by
+                        // adding a zero-width space character
+                        span.appendChild( doc.createTextNode("\u200bXX") );
+                        range.insertNode(span);
+                        rect = span.getClientRects()[0];
+                        x = rect.left;
+                        y = rect.top;
+                        let spanParent = span.parentNode;
+                        spanParent.removeChild(span);
+
+                        // Glue any broken text nodes back together
+                        spanParent.normalize();
+
+                        return [x, y];
+                    }
+                    return null
+                }
+            }
+        }
+        return { x: x, y: y };
+    }
+    function show(_input) {
+        let pos = getCaretPos();
+        if(pos) {
+            index = -1;
+            let rawOptions = $(_input).closest('[data-shortcuts]').attr('data-shortcuts');
+            options = [];
+            rawOptions = rawOptions.split('^^^');
+            for (let i = 0; i < rawOptions.length; i++) {
+                let parts = rawOptions[i].split('|||');
+                options.push({
+                    name: parts[0],
+                    value: parts[1]
+                });
+            }
+            menu.empty();
+            for(let i = 0; i < options.length; i++) {
+                menu.append(
+                    $('<div/>')
+                        .addClass('sc')
+                        .text(options[i].name)
+                        .attr('title', options[i].value)
+                );
+            }
+            menu
+                .css({
+                    left: pos[0] + 'px',
+                    top: pos[1] + 'px',
+                })
+                .show();
+
+        }
+    }
+    function discard() {
+        if($('.stag-shortcuts:visible').length) {
+            $('.stag-shortcuts').hide();
+            return false;
+        }
+    }
+    function highlightOption() {
+        menu.find('.sc').removeClass('active');
+        if(options && options.length && index >= 0 && index < options.length) {
+            menu.find('.sc:eq(' + index + ')').addClass('active');
+        }
+    }
+    function apply() {
+        if(input && options && options.length && index >= 0 && index < options.length) {
+            $(input).focus();
+            if(backtrackAfterApplying) {
+                document.execCommand("delete", true, null);
+            }
+            document.execCommand("insertText", true, options[index].value);
+            discard();
+        }
+    }
+    function isVisible() {
+        return !!$('.stag-shortcuts:visible').length;
+    }
+    function init() {
+
+        var selectedText = '';
+
+        $('.stag-shortcuts').remove();
+        options = [];
+        menu = $('<div/>')
+            .addClass('stag-shortcuts')
+            .appendTo('body');
+
+        $(document)
+            .off('mousedown.outside-shortcuts')
+            .on('mousedown.outside-shortcuts', function(_e) {
+                return discard();
+            });
+        $(document)
+            .off('keypress.shortcuts', '[with-shortcuts]')
+            .on('keypress.shortcuts', '[with-shortcuts]', function(_e) {
+                // console.log('KP: ', _e.which);
+                input = this;
+                switch(_e.which) {
+                    case 64:
+                        backtrackAfterApplying = true;
+                        show(this);
+                        break;
+                    default:
+                        if(isVisible()) return false;
+                        break;
+                }
+            })
+            .off('keydown.shortcuts', '[with-shortcuts]')
+            .on('keydown.shortcuts', '[with-shortcuts]', function(_e) {
+                // console.log('KD: ', _e.which);
+                input = this;
+                let consumed = false;
+                switch(_e.which) {
+                    case 32:
+                        if(_e.ctrlKey) {
+                            backtrackAfterApplying = false;
+                            show(this);
+                            return false;
+                        }
+                        else {
+                            if(!isVisible()) return;
+                            _e.preventDefault();
+                            apply();
+                            consumed = true;
+                        }
+                        break;
+                    case 27:
+                        if(!isVisible()) return;
+                        consumed = !discard();
+                        break;
+                    case 38:
+                        if(!isVisible()) return;
+                        if(index > 0) index--;
+                        highlightOption();
+                        consumed = true;
+                        break;
+                    case 40:
+                        if(!isVisible()) return;
+                        if(index < options.length - 1) index++;
+                        highlightOption();
+                        consumed = true;
+                        break;
+                    case 13:
+                        if(!isVisible()) return;
+                        apply();
+                        consumed = true;
+                        break;
+                    default:
+                        consumed = isVisible();
+                        break;
+                }
+                if(consumed) return false;
+            })
+            .off('selectionchange.shortcuts')
+            .on('selectionchange.shortcuts', function(_e) {
+                console.log(_e);
+            });
+        $(document)
+            .off('click.apply-shortcuts', '.stag-shortcuts>.sc')
+            .on('click.apply-shortcuts', '.stag-shortcuts>.sc', function(_e) {
+                apply();
+                return false;
+            });
+        menu.off('mousedown.inside-shortcuts')
+            .on('mousedown.inside-shortcuts', function(_e) {
+                return false;
+            });
+        $(document)
+            .off('mousedown.add-shortcuts', '.add-shortcut')
+            .on('mousedown.add-shortcuts', '.add-shortcut', function(_e) {
+                let hasFocus = $(document.activeElement).closest('.note-content').length;
+                if(hasFocus) {
+                    selectedText = window.getSelection().toString();
+                    if(selectedText !== '') return;
+                }
+                return false;
+            })
+            .off('click.add-shortcuts', '.add-shortcut')
+            .on('click.add-shortcuts', '.add-shortcut', function(_e) {
+                if(selectedText === '') return;
+                $('#selected-sc-text').val(selectedText);
+                $('#create-shortcut-form')
+                    .css({
+                        left: $(this).offset().left + 'px',
+                        top: ($(this).offset().top + $(this).outerHeight()) + 'px'
+                    })
+                    .show();
+                showMoeFormMask();
+                return false;
+            })
+            .off('mousedown.inside-form', '#create-shortcut-form *')
+            .on('mousedown.inside-form', '#create-shortcut-form *', function(_e) {
+                // return false;
+            })
+            .off('submit.add-shortcut', '#create-shortcut-form')
+            .on('submit.add-shortcut', '#create-shortcut-form', function(_e) {
+                var label = $(this).find('[name="shortcut"]').val(),
+                    content = $(this).find('[name="text"]').val();
+                $.post('/api/proTextShortcut/create', $(this).serialize(), function(_data) {
+                    if(_data && _data.success && input) {
+                        var options = [$(input).closest('[data-shortcuts]').attr('data-shortcuts')]
+                        options.push(label + '|||' + content);
+                        options = options.join('^^^');
+                        $(input).closest('[data-shortcuts]').attr('data-shortcuts', options);
+                        toastr.success('Shortcut saved');
+                        hideMoeFormMask();
+                        $('#create-shortcut-form').hide();
+                        $(input).focus();
+                    }
+                }, 'json');
+                return false;
+            });
+
+    }
+    addMCInitializer('shortcut-suggest', init);
+})();
+
+

+ 2 - 1
public/js/yemi.js

@@ -20,6 +20,7 @@ var showMoeFormMask = function () {
 
 var hideMoeFormMask = function () {
     $('#moe-form-mask').hide();
+    $('#create-shortcut-form').hide();
 }
 
 $(document).ready(function () {
@@ -247,7 +248,7 @@ $(function () {
 jQuery(document).ready(function () {
     var $ = jQuery;
     $('body').mousedown(function(e){
-        if($(e.target).closest('[moe]').length){
+        if($(e.target).closest('[moe]').length || $(e.target).closest('#create-shortcut-form').length || $(e.target).is('#create-shortcut-form')){
             return;
         }
         $('[moe] form:not([show])').hide();

+ 11 - 1
resources/views/app/patient/note/dashboard.blade.php

@@ -270,10 +270,20 @@
 
                 <div class="mb-3">
                     <div>
+                        <?php
+                        $shortcuts = $pro->shortcuts;
+                        $packed = [];
+                        foreach ($shortcuts as $shortcut) {
+                            $packed[] = $shortcut->shortcut . '|||' . $shortcut->text;
+                        }
+                        $packed = implode("^^^", $packed);
+                        ?>
                         <div class="primary-form">
                             <div class="note-content {{ $note->is_cancelled ? 'cancelled' : '' }} {{ $note->is_signed_by_hcp ? 'readonly' : '' }}"
                                  data-note-uid="{{ $note->uid  }}"
-                                 {{ !$note->is_cancelled && empty($note->free_text_html) ? 'auto-edit' : '' }}>{!! $note->free_text_html !!}</div>
+                                 {{ !$note->is_cancelled && empty($note->free_text_html) ? 'auto-edit' : '' }}
+                                 data-shortcuts="{{ $packed }}"
+                            >{!! $note->free_text_html !!}</div>
                         </div>
                         {{--<div moe class="d-inline">
                             <a show start><i class="fa fa-edit"></i></a>

+ 38 - 1
resources/views/layouts/template.blade.php

@@ -10,6 +10,22 @@
     <!-- Fonts -->
     <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
 
+    {{-- mc initializers --}}
+    <script>
+        window.mcInitializers = {};
+        window.addMCInitializer = function(_name, _func) {
+            window.mcInitializers[_name] = _func;
+        };
+        window.runMCInitializers = function() {
+            // run all mcInitializers
+            if(!!mcInitializers) {
+                for(var func in mcInitializers) {
+                    if(mcInitializers.hasOwnProperty(func)) mcInitializers[func]();
+                }
+            }
+        };
+    </script>
+
     {{-- vue --}}
     <script src="https://cdn.jsdelivr.net/npm/vue"></script>
 
@@ -89,9 +105,30 @@
 
     </main><!-- /.container -->
 
-    <!-- script to handle history & back/forward for mc/xxx pages -->
+
+    <!-- shortcut/suggest component -->
+    <link href="/css/shortcut.css" rel=stylesheet>
+    <script src="/js/shortcut.js" type="application/javascript"></script>
+
+    <!-- script to handle history/back/forward for mc/xxx pages
+    + all other JS initialization needed in fastLoaded pages  -->
+    <script src="/js/find-event-handlers.js" type="application/javascript"></script>
     <script src="/js/mc.js?_=4" type="application/javascript"></script>
 
+    <form url="/api/proTextShortcut/create" id="create-shortcut-form" class="mcp-theme-1">
+        <input type="hidden" name="proUid" value="{{ $pro->uid  }}">
+        <div class="mb-2">
+            <input type="text" class="form-control form-control-sm" name="shortcut" value="" placeholder="Shortcut Name">
+        </div>
+        <div class="mb-2">
+            <textarea name="text" class="form-control form-control-sm" rows="3" id="selected-sc-text" placeholder="Content"></textarea>
+        </div>
+        <div class="mb-0">
+            <button class="btn btn-success btn-sm" type="submit">Sign</button>
+            <button class="btn btn-default border btn-sm ml-1" type="reset">Cancel</button>
+        </div>
+    </form>
+
 </body>
 
 </html>