From 3e9a9f5ca974d06a624d2570e600e0720ce5c7ed Mon Sep 17 00:00:00 2001 From: Lucas Madureira Date: Thu, 22 Apr 2021 12:28:23 -0300 Subject: [PATCH 01/17] Removing sortablelists plugin from code and including it on requirements list. Necessary adjustments are: * Tweaks on generated DOM now accomplished by a mutation observer * Sortablelists export methods are now exposed by this plugin (requires data to have an id property) --- README.md | 8 +- jquery-menu-editor.js | 1023 +++---------------------------------- jquery-menu-editor.min.js | 12 +- 3 files changed, 71 insertions(+), 972 deletions(-) diff --git a/README.md b/README.md index ffc748b..bc029a8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This project is based on jQuery-Sortable-lists (v1.4.0) http://camohub.github.io * jQuery >= 3.x * Fontawesome 5.3.1 * Bootstrap Iconpicker 1.10.0 +* jQuery-Sortable-lists 1.4.0 (with mobile support) ## How to use ### Include the Css and scripts @@ -32,6 +33,7 @@ This project is based on jQuery-Sortable-lists (v1.4.0) http://camohub.github.io + ``` @@ -113,12 +115,12 @@ $('#btnAdd').click(function(){ ### Load data from a Json We have the method setData: ```javascript -var arrayjson = [{"href":"http://home.com","icon":"fas fa-home","text":"Home", "target": "_top", "title": "My Home"},{"icon":"fas fa-chart-bar","text":"Opcion2"},{"icon":"fas fa-bell","text":"Opcion3"},{"icon":"fas fa-crop","text":"Opcion4"},{"icon":"fas fa-flask","text":"Opcion5"},{"icon":"fas fa-map-marker","text":"Opcion6"},{"icon":"fas fa-search","text":"Opcion7","children":[{"icon":"fas fa-plug","text":"Opcion7-1","children":[{"icon":"fas fa-filter","text":"Opcion7-1-1"}]}]}]; +var arrayJson = [{"href":"http://home.com","icon":"fas fa-home","text":"Home", "target": "_top", "title": "My Home"},{"icon":"fas fa-chart-bar","text":"Opcion2"},{"icon":"fas fa-bell","text":"Opcion3"},{"icon":"fas fa-crop","text":"Opcion4"},{"icon":"fas fa-flask","text":"Opcion5"},{"icon":"fas fa-map-marker","text":"Opcion6"},{"icon":"fas fa-search","text":"Opcion7","children":[{"icon":"fas fa-plug","text":"Opcion7-1","children":[{"icon":"fas fa-filter","text":"Opcion7-1-1"}]}]}]; editor.setData(arrayJson); ``` ### Output -We have the function getString +We have the function getJsonString ```javascript -var str = editor.getString(); +var str = editor.getJsonString(); $("#myTextarea").text(str); ``` diff --git a/jquery-menu-editor.js b/jquery-menu-editor.js index 27a8108..f871bef 100644 --- a/jquery-menu-editor.js +++ b/jquery-menu-editor.js @@ -5,963 +5,6 @@ * */ ( function( $ ) { - /** - * @desc jQuery plugin to sort html list also the tree structures - * @version 1.4.0 - * @author Vladimír Čamaj - * @license MIT - * @desc jQuery plugin - * @param options - * @returns this to unsure chaining - */ - $.fn.sortableLists = function( options ) - { - // Local variables. This scope is available for all the functions in this closure. - var jQBody = $( 'body' ).css( 'position', 'relative' ), - - defaults = { - currElClass: '', - placeholderClass: '', - placeholderCss: { - 'position': 'relative', - 'padding': 0 - }, - hintClass: '', - hintCss: { - 'display': 'none', - 'position': 'relative', - 'padding': 0 - }, - hintWrapperClass: '', - hintWrapperCss: { /* Description is below the defaults in this var section */ }, - baseClass: '', - baseCss: { - 'position': 'absolute', - 'top': 0 - parseInt( jQBody.css( 'margin-top' ) ), - 'left': 0 - parseInt( jQBody.css( 'margin-left' ) ), - 'margin': 0, - 'padding': 0, - 'z-index': 2500 - }, - opener: { - active: false, - open: '', - close: '', - openerCss: { - 'float': 'left', - 'display': 'inline-block', - 'background-position': 'center center', - 'background-repeat': 'no-repeat' - }, - openerClass: '' - }, - listSelector: 'ul', - listsClass: '', // Used for hintWrapper and baseElement - listsCss: {}, - insertZone: 50, - insertZonePlus: false, - scroll: 20, - ignoreClass: '', - isAllowed: function( cEl, hint, target ) { return true; }, // Params: current el., hint el. - onDragStart: function( e, cEl ) { return true; }, // Params: e jQ. event obj., current el. - onChange: function( cEl ) { return true; }, // Params: current el. - complete: function( cEl ) { return true; } // Params: current el. - }, - - setting = $.extend( true, {}, defaults, options ), - - // base element from which is counted position of draged element - base = $( '<' + setting.listSelector + ' />' ) - .prependTo( jQBody ) - .attr( 'id', 'sortableListsBase' ) - .css( setting.baseCss ) - .addClass( setting.listsClass + ' ' + setting.baseClass ), - - // placeholder != state.placeholderNode - // placeholder is document fragment and state.placeholderNode is document node - placeholder = $( '
  • ' ) - .attr( 'id', 'sortableListsPlaceholder' ) - .css( setting.placeholderCss ) - .addClass( setting.placeholderClass ), - - // hint is document fragment - hint = $( '
  • ' ) - .attr( 'id', 'sortableListsHint' ) - .css( setting.hintCss ) - .addClass( setting.hintClass ), - - // Is document fragment used as wrapper if hint is inserted to the empty li - hintWrapper = $( '<' + setting.listSelector + ' />' ) - .attr( 'id', 'sortableListsHintWrapper' ) - .addClass( setting.listsClass + ' ' + setting.hintWrapperClass ) - .css( setting.listsCss ) - .css( setting.hintWrapperCss ), - - // Is +/- ikon to open/close nested lists - opener = $( '' ) - .addClass( 'sortableListsOpener ' + setting.opener.openerClass ) - .css( setting.opener.openerCss ) - .on( 'mousedown touchstart', function( e ) - { - var li = $( this ).closest( 'li' ); - - if ( li.hasClass( 'sortableListsClosed' ) ) - { - open( li ); - } - else - { - close( li ); - } - - return false; // Prevent default - } ); - - if ( setting.opener.as == 'class' ) - { - opener.addClass( setting.opener.close ); - } - else if ( setting.opener.as == 'html' ) - { - opener.html( setting.opener.close ); - } - else - { - console.error('Invalid setting for opener.as'); - } - - // Container with all actual elements and parameters - var state = { - isDragged: false, - isRelEFP: null, // How browser counts elementFromPoint() position (relative to window/document) - oEl: null, // overElement is element which returns elementFromPoint() method - rootEl: null, - cEl: null, // currentElement is currently dragged element - upScroll: false, - downScroll: false, - pX: 0, - pY: 0, - cX: 0, - cY: 0, - isAllowed: true, // The function is defined in setting - e: { pageX: 0, pageY: 0, clientX: 0, clientY: 0 }, // TODO: unused?? - doc: $( document ), - win: $( window ) - }; - - if ( setting.opener.active ) - { - if ( ! setting.opener.open ) throw 'Opener.open value is not defined. It should be valid url, html or css class.'; - if ( ! setting.opener.close ) throw 'Opener.close value is not defined. It should be valid url, html or css class.'; - - $( this ).find( 'li' ).each( function() - { - var li = $( this ); - - if ( li.children( setting.listSelector ).length ) - { - opener.clone( true ).prependTo( li.children( 'div' ).first() ); - - if ( ! li.hasClass( 'sortableListsOpen' ) ) - { - close( li ); - } - else - { - open( li ); - } - } - } ); - } - - // Return this ensures chaining - return this.on( 'mousedown touchstart', function( e ) - { - var target = $( e.target ); - - if ( state.isDragged !== false || ( setting.ignoreClass && target.hasClass( setting.ignoreClass ) ) ) return; // setting.ignoreClass is checked cause hasClass('') returns true - - // Solves selection/range highlighting - e.preventDefault(); - - if ( e.type === 'touchstart' ) - { - setTouchEvent( e ); - } - - // El must be li in jQuery object - var el = target.closest( 'li' ), - rEl = $( this ); - - // Check if el is not empty - if ( el[ 0 ] ) - { - setting.onDragStart( e, el ); - startDrag( e, el, rEl ); - } - } - ); - - /** - * @desc Binds events dragging and endDrag, sets some init. values - * @param e event obj. - * @param el curr. dragged element - * @param rEl root element - */ - function startDrag( e, el, rEl ) - { - state.isDragged = true; - - var elMT = parseInt( el.css( 'margin-top' ) ), // parseInt is necesary cause value has px at the end - elMB = parseInt( el.css( 'margin-bottom' ) ), - elML = parseInt( el.css( 'margin-left' ) ), - elMR = parseInt( el.css( 'margin-right' ) ), - elXY = el.offset(), - elIH = el.innerHeight(); - - state.rootEl = { - el: rEl, - offset: rEl.offset(), - rootElClass: rEl.attr( 'class' ) - }; - - state.cEl = { - el: el, - mT: elMT, mL: elML, mB: elMB, mR: elMR, - offset: elXY - }; - - state.cEl.xyOffsetDiff = { X: e.pageX - state.cEl.offset.left, Y: e.pageY - state.cEl.offset.top }; - state.cEl.el.addClass( 'sortableListsCurrent' + ' ' + setting.currElClass ); - - el.before( placeholder ); // Now document has node placeholder - - var placeholderNode = state.placeholderNode = $( '#sortableListsPlaceholder' ); // jQuery object && document node - - el.css( { - 'width': el.width(), - 'position': 'absolute', - 'top': elXY.top - elMT, - 'left': elXY.left - elML - } ).prependTo( base ); - - placeholderNode.css( { - 'display': 'block', - 'height': elIH - } ); - - hint.css( 'height', elIH ); - - state.doc - .on( 'mousemove touchmove', dragging ) - .on( 'mouseup touchend touchcancel', endDrag ); - - } - - /** - * @desc Start dragging - * @param e event obj. - */ - function dragging( e ) - { - if ( state.isDragged ) - { - var cEl = state.cEl, - doc = state.doc, - win = state.win; - - if ( e.type === 'touchmove' ) - { - setTouchEvent( e ); - } - - // event triggered by trigger() from setInterval does not have XY properties - if ( ! e.pageX ) - { - setEventPos( e ); - } - - // Scrolling up - if ( doc.scrollTop() > state.rootEl.offset.top - 10 && e.clientY < 50 ) - { - if ( ! state.upScroll ) // Has to be here after cond. e.clientY < 50 cause else unsets the interval - { - setScrollUp( e ); - } - else - { - e.pageY = e.pageY - setting.scroll; - $( 'html, body' ).each( function( i ) - { - $( this ).scrollTop( $( this ).scrollTop() - setting.scroll ); - } ); - setCursorPos( e ); - } - } - // Scrolling down - else if ( doc.scrollTop() + win.height() < state.rootEl.offset.top + state.rootEl.el.outerHeight( false ) + 10 && win.height() - e.clientY < 50 ) - { - if ( ! state.downScroll ) - { - setScrollDown( e ); - } - else - { - e.pageY = e.pageY + setting.scroll; - $( 'html, body' ).each( function( i ) - { - $( this ).scrollTop( $( this ).scrollTop() + setting.scroll ); - } ); - setCursorPos( e ); - } - } - else - { - scrollStop( state ); - } - - // Script needs to know old oEl - state.oElOld = state.oEl; - - cEl.el[ 0 ].style.visibility = 'hidden'; // This is important for the next row - state.oEl = oEl = elFromPoint( e.pageX, e.pageY ); - cEl.el[ 0 ].style.visibility = 'visible'; - - showHint( e, state ); - - setCElPos( e, state ); - - } - } - - /** - * @desc endDrag unbinds events mousemove/mouseup and removes redundant elements - * @param e - */ - function endDrag( e ) - { - var cEl = state.cEl, - hintNode = $( '#sortableListsHint', state.rootEl.el ), - hintStyle = hint[ 0 ].style, - targetEl = null, // hintNode/placeholderNode - isHintTarget = false, // if cEl will be placed to the hintNode - hintWrapperNode = $( '#sortableListsHintWrapper' ); - - if ( e.type === 'touchend' || e.type === 'touchcancel' ) - { - setTouchEvent( e ); - } - - if ( hintStyle.display == 'block' && hintNode.length && state.isAllowed ) - { - targetEl = hintNode; - isHintTarget = true; - } - else - { - targetEl = state.placeholderNode; - isHintTarget = false; - } - - offset = targetEl.offset(); - - cEl.el.animate( { left: offset.left - state.cEl.mL, top: offset.top - state.cEl.mT }, 250, - function() // complete callback - { - tidyCurrEl( cEl ); - - targetEl.after( cEl.el[ 0 ] ); - targetEl[ 0 ].style.display = 'none'; - hintStyle.display = 'none'; - // This have to be document node, not hint as a part of documentFragment. - hintNode.remove(); - - hintWrapperNode - .removeAttr( 'id' ) - .removeClass( setting.hintWrapperClass ); - - if ( hintWrapperNode.length ) - { - //hintWrapperNode.prev( 'div' ).append( opener.clone( true ) ); // original - hintWrapperNode.prev( 'div' ).prepend( opener.clone( true ) ); //david - } - - // Directly removed placeholder looks bad. It jumps up if the hint is below. - if ( isHintTarget ) - { - state.placeholderNode.slideUp( 150, function() - { - state.placeholderNode.remove(); - tidyEmptyLists(); - setting.onChange( cEl.el ); - setting.complete( cEl.el ); // Have to be here cause is necessary to remove placeholder before complete call. - state.isDragged = false; - } ); - } - else - { - state.placeholderNode.remove(); - tidyEmptyLists(); - setting.complete( cEl.el ); - state.isDragged = false; - } - - } ); - - scrollStop( state ); - - state.doc - .unbind( "mousemove touchmove", dragging ) - .unbind( "mouseup touchend touchcancel", endDrag ); - - - } - - //////// Helpers ///////////////////////////////////////////////////////////////////////////////////// - - //////// Scroll handlers ///////////////////////////////////////////////////////////////////////////// - - /** - * @desc Ensures autoscroll up. - * @param e - * @return No value - */ - function setScrollUp( e ) - { - if ( state.upScroll ) return; - - state.upScroll = setInterval( function() - { - state.doc.trigger( 'mousemove' ); - }, 50 ); - - } - - /** - * @desc Ensures autoscroll down. - * @param e - * @return No value - */ - function setScrollDown( e ) - { - if ( state.downScroll ) return; - - state.downScroll = setInterval( function() - { - state.doc.trigger( 'mousemove' ); - }, 50 ); - - } - - /** - * @desc This properties are used when setScrollUp()/Down() calls trigger('mousemove'), cause trigger() produce event object without pageY/Y and clientX/Y. - * @param e - * @return No value - */ - function setCursorPos( e ) - { - state.pY = e.pageY; - state.pX = e.pageX; - state.cY = e.clientY; - state.cX = e.clientX; - } - - /** - * @desc Necessary while scrolling, cause trigger('mousemove') does not set cursor XY values in event object - * @param e - * @return No value - */ - function setEventPos( e ) - { - e.pageY = state.pY; - e.pageX = state.pX; - e.clientY = state.cY; - e.clientX = state.cX; - } - - /** - * @desc Stops scrolling and sets variables - * @param state - * @return No value - */ - function scrollStop( state ) - { - clearInterval( state.upScroll ); - clearInterval( state.downScroll ); - // clearInterval have to be before upScroll/downScroll is set to false - state.upScroll = state.downScroll = false; - } - - /////// End of Scroll handlers ////////////////////////////////////////////////////////////// - /////// Current element handlers ////////////////////////////////////////////////////////////// - - /** - * Sets the e.page/e.screen properties - * @param e - */ - function setTouchEvent( e ) - { - e.pageX = e.originalEvent.changedTouches[ 0 ].pageX; - e.pageY = e.originalEvent.changedTouches[ 0 ].pageY; - e.screenX = e.originalEvent.changedTouches[ 0 ].screenX; - e.screenY = e.originalEvent.changedTouches[ 0 ].screenY; - } - - /** - * @desc Sets the position of dragged element - * @param e event object - * @param state state object - * @return No value - */ - function setCElPos( e, state ) - { - var cEl = state.cEl; - - cEl.el.css( { - 'top': e.pageY - cEl.xyOffsetDiff.Y - cEl.mT, - 'left': e.pageX - cEl.xyOffsetDiff.X - cEl.mL - } ) - - } - - /** - * @desc Return elementFromPoint() result as jQuery object - * @param x e.pageX - * @param y e.pageY - * @return null|jQuery object - */ - function elFromPoint( x, y ) - { - if ( ! document.elementFromPoint ) return null; - - // FF/IE/CH needs coordinates relative to the window, unlike - // Opera/Safari which needs absolute coordinates of document in elementFromPoint() - var isRelEFP = state.isRelEFP; - - // isRelative === null means it is not checked yet - if ( isRelEFP === null ) - { - var s, res; - if ( (s = state.doc.scrollTop()) > 0 ) - { - isRelEFP = ( (res = document.elementFromPoint( 0, s + $( window ).height() - 1 ) ) == null - || res.tagName.toUpperCase() == 'HTML'); // IE8 returns html - } - if ( (s = state.doc.scrollLeft()) > 0 ) - { - isRelEFP = ( (res = document.elementFromPoint( s + $( window ).width() - 1, 0 ) ) == null - || res.tagName.toUpperCase() == 'HTML'); // IE8 returns html - } - } - - if ( isRelEFP ) - { - x -= state.doc.scrollLeft(); - y -= state.doc.scrollTop(); - } - - // Returns jQuery object - var el = $( document.elementFromPoint( x, y ) ); - - if ( ! state.rootEl.el.find( el ).length ) // el is outside the rootEl - { - return null; - } - else if ( el.is( '#sortableListsPlaceholder' ) || el.is( '#sortableListsHint' ) ) // el is #placeholder/#hint - { - return null; - } - else if ( ! el.is( 'li' ) ) // el is ul or div or something else in li elem. - { - el = el.closest( 'li' ); - return el[ 0 ] ? el : null; - } - else if ( el.is( 'li' ) ) // el is most wanted li - { - return el; - } - - } - //////// End of current element handlers ////////////////////////////////////////////////////// - //////// Show hint handlers ////////////////////////////////////////////////////// - - /** - * @desc Shows or hides or does not show hint element - * @param e event - * @param state - * @return No value - */ - function showHint( e, state ) - { - var oEl = state.oEl; - - // If oEl is null or if this is the first call in dragging - if ( ! oEl || ! state.oElOld ) return; - - var oElH = oEl.outerHeight( false ), - relY = e.pageY - oEl.offset().top; - - if ( setting.insertZonePlus ) - { - if ( 14 > relY ) // Inserting on top - { - showOnTopPlus( e, oEl, 7 > relY ); // Last bool param express if hint insert outside/inside - } - else if ( oElH - 14 < relY ) // Inserting on bottom - { - showOnBottomPlus( e, oEl, oElH - 7 < relY ); - } - } - else - { - if ( 5 > relY ) // Inserting on top - { - showOnTop( e, oEl ); - } - else if ( oElH - 5 < relY ) // Inserting on bottom - { - showOnBottom( e, oEl ); - } - } - } - - /** - * @desc Called from showHint method. Displays or hides hint element - * @param e event - * @param oEl oElement - * @return No value - */ - function showOnTop( e, oEl ) - { - if ( $( '#sortableListsHintWrapper', state.rootEl.el ).length ) - { - hint.unwrap(); // If hint is wrapped by ul/ol #sortableListsHintWrapper - } - - // Hint outside the oEl - if ( e.pageX - oEl.offset().left < setting.insertZone ) - { - // Ensure display:none if hint will be next to the placeholder - if ( oEl.prev( '#sortableListsPlaceholder' ).length ) - { - hint.css( 'display', 'none' ); - return; - } - oEl.before( hint ); - } - // Hint inside the oEl - else - { - var children = oEl.children(), - list = oEl.children( setting.listSelector ).first(); - - if ( list.children().first().is( '#sortableListsPlaceholder' ) ) - { - hint.css( 'display', 'none' ); - return; - } - - // Find out if is necessary to wrap hint by hintWrapper - if ( ! list.length ) - { - children.first().after( hint ); - hint.wrap( hintWrapper ); - } - else - { - list.prepend( hint ); - } - - if ( state.oEl ) - { - open( oEl ); // TODO:animation??? .children('ul,ol').css('display', 'block'); - } - - } - - hint.css( 'display', 'block' ); - // Ensures posible formating of elements. Second call is in the endDrag method. - state.isAllowed = setting.isAllowed( state.cEl.el, hint, hint.parents( 'li' ).first() ); - - } - - /** - * @desc Called from showHint method. Displays or hides hint element - * @param e event - * @param oEl oElement - * @param outside bool - * @return No value - */ - function showOnTopPlus( e, oEl, outside ) - { - if ( $( '#sortableListsHintWrapper', state.rootEl.el ).length ) - { - hint.unwrap(); // If hint is wrapped by ul/ol #sortableListsHintWrapper - } - - // Hint inside the oEl - if ( ! outside && e.pageX - oEl.offset().left > setting.insertZone ) - { - var children = oEl.children(), - list = oEl.children( setting.listSelector ).first(); - - if ( list.children().first().is( '#sortableListsPlaceholder' ) ) - { - hint.css( 'display', 'none' ); - return; - } - - // Find out if is necessary to wrap hint by hintWrapper - if ( ! list.length ) - { - children.first().after( hint ); - hint.wrap( hintWrapper ); - } - else - { - list.prepend( hint ); - } - - if ( state.oEl ) - { - open( oEl ); // TODO:animation??? .children('ul,ol').css('display', 'block'); - } - } - // Hint outside the oEl - else - { - // Ensure display:none if hint will be next to the placeholder - if ( oEl.prev( '#sortableListsPlaceholder' ).length ) - { - hint.css( 'display', 'none' ); - return; - } - oEl.before( hint ); - - } - - hint.css( 'display', 'block' ); - // Ensures posible formating of elements. Second call is in the endDrag method. - state.isAllowed = setting.isAllowed( state.cEl.el, hint, hint.parents( 'li' ).first() ); - - } - - /** - * @desc Called from showHint function. Displays or hides hint element. - * @param e event - * @param oEl oElement - * @return No value - */ - function showOnBottom( e, oEl ) - { - if ( $( '#sortableListsHintWrapper', state.rootEl.el ).length ) - { - hint.unwrap(); // If hint is wrapped by ul/ol sortableListsHintWrapper - } - - // Hint outside the oEl - if ( e.pageX - oEl.offset().left < setting.insertZone ) - { - // Ensure display:none if hint will be next to the placeholder - if ( oEl.next( '#sortableListsPlaceholder' ).length ) - { - hint.css( 'display', 'none' ); - return; - } - oEl.after( hint ); - } - // Hint inside the oEl - else - { - var children = oEl.children(), - list = oEl.children( setting.listSelector ).last(); // ul/ol || empty jQuery obj - - if ( list.children().last().is( '#sortableListsPlaceholder' ) ) - { - hint.css( 'display', 'none' ); - return; - } - - // Find out if is necessary to wrap hint by hintWrapper - if ( list.length ) - { - children.last().append( hint ); - } - else - { - oEl.append( hint ); - hint.wrap( hintWrapper ); - } - - if ( state.oEl ) - { - open( oEl ); // TODO: animation??? - } - - } - - hint.css( 'display', 'block' ); - // Ensures posible formating of elements. Second call is in the endDrag method. - state.isAllowed = setting.isAllowed( state.cEl.el, hint, hint.parents( 'li' ).first() ); - - } - - /** - * @desc Called from showHint function. Displays or hides hint element. - * @param e event - * @param oEl oElement - * @param outside bool - * @return No value - */ - function showOnBottomPlus( e, oEl, outside ) - { - if ( $( '#sortableListsHintWrapper', state.rootEl.el ).length ) - { - hint.unwrap(); // If hint is wrapped by ul/ol sortableListsHintWrapper - } - - // Hint inside the oEl - if ( ! outside && e.pageX - oEl.offset().left > setting.insertZone ) - { - var children = oEl.children(), - list = oEl.children( setting.listSelector ).last(); // ul/ol || empty jQuery obj - - if ( list.children().last().is( '#sortableListsPlaceholder' ) ) - { - hint.css( 'display', 'none' ); - return; - } - - // Find out if is necessary to wrap hint by hintWrapper - if ( list.length ) - { - children.last().append( hint ); - } - else - { - oEl.append( hint ); - hint.wrap( hintWrapper ); - } - - if ( state.oEl ) - { - open( oEl ); // TODO: animation??? - } - - } - // Hint outside the oEl - else - { - // Ensure display:none if hint will be next to the placeholder - if ( oEl.next( '#sortableListsPlaceholder' ).length ) - { - hint.css( 'display', 'none' ); - return; - } - oEl.after( hint ); - - } - - hint.css( 'display', 'block' ); - // Ensures posible formating of elements. Second call is in the endDrag method. - state.isAllowed = setting.isAllowed( state.cEl.el, hint, hint.parents( 'li' ).first() ); - - } - - //////// End of show hint handlers //////////////////////////////////////////////////// - //////// Open/close handlers ////////////////////////////////////////////////////////// - - /** - * @desc Handles opening nested lists - * @param li - */ - function open( li ) - { - li.removeClass( 'sortableListsClosed' ).addClass( 'sortableListsOpen' ); - li.children( setting.listSelector ).css( 'display', 'block' ); - - var opener = li.children( 'div' ).children( '.sortableListsOpener' ).first(); - - if ( setting.opener.as == 'html' ) - { - opener.html( setting.opener.close ); - } - else if ( setting.opener.as == 'class' ) - { - opener.addClass( setting.opener.close ).removeClass( setting.opener.open ); - } - else - { - opener.css( 'background-image', 'url(' + setting.opener.close + ')' ); - } - } - - /** - * @desc Handles opening nested lists - * @param li - */ - function close( li ) - { - li.removeClass( 'sortableListsOpen' ).addClass( 'sortableListsClosed' ); - li.children( setting.listSelector ).css( 'display', 'none' ); - - var opener = li.children( 'div' ).children( '.sortableListsOpener' ).first(); - - if ( setting.opener.as == 'html' ) - { - opener.html( setting.opener.open ); - } - else if ( setting.opener.as == 'class' ) - { - opener.addClass( setting.opener.open ).removeClass( setting.opener.close ); - } - else - { - opener.css( 'background-image', 'url(' + setting.opener.open + ')' ); - } - - } - - /////// Enf of open/close handlers ////////////////////////////////////////////// - - /** - * @desc Places the currEl to the target place - * @param cEl - */ - function tidyCurrEl( cEl ) - { - var cElStyle = cEl.el[ 0 ].style; - - cEl.el.removeClass( setting.currElClass + ' ' + 'sortableListsCurrent' ); - cElStyle.top = '0'; - cElStyle.left = '0'; - cElStyle.position = 'relative'; - cElStyle.width = 'auto'; - - } - - /** - * @desc Removes empty lists and redundant openers - */ - function tidyEmptyLists() - { - // Remove every empty ul/ol from root and also with .sortableListsOpener - // hintWrapper can not be removed before the hint - $( setting.listSelector, state.rootEl.el ).each( function( i ) - { - if ( ! $( this ).children().length ) - { - $( this ).prev( 'div' ).children( '.sortableListsOpener' ).first().remove(); - $( this ).remove(); - } - } - ); - - } - - }; - - /** END PLUGIN sortableLists */ /** * @desc Handles opening nested lists @@ -1100,6 +143,7 @@ function MenuEditor(idSelector, options) { } }; $.extend(true, settings, options); + var baseIdCounters = {}; var itemEditing = null; var sortableReady = true; var $form = null; @@ -1109,6 +153,23 @@ function MenuEditor(idSelector, options) { var iconPicker = $('#'+idSelector+'_icon').iconpicker(iconPickerOpt); $main.sortableLists(settings.listOptions); + // A mutation observer, used to make a tweak in the position of the nested + // list opener button, which is generated and appended by the sortablelists + // plugin. Should it ever change this DOM structure, this code will also + // have to be adjusted. + MutationObserver = window.MutationObserver || window.WebKitMutationObserver; + var openerWrapperObserver = new MutationObserver(function(mutations) { + $.each(mutations, function(i, mutation) { + if (mutation.addedNodes.length == 1) { + var $target = $(mutation.target); + var $el = $(mutation.addedNodes); + if ($el.hasClass('sortableListsOpener') && !$el.is(':first-child')) { + $target.prepend($el); + } + } + }); + }); + /* EVENTS */ iconPicker.on('change', function (e) { $form.find("[name=icon]").val(e.icon); @@ -1262,6 +323,43 @@ function MenuEditor(idSelector, options) { $li.append(createMenu(v.children, level + 1)); } $elem.append($li); + + $div.each(function () { + openerWrapperObserver.observe(this, { + subtree: false, + attributes: false, + childList: true + }); + }); + + // Setting element value propety, which is retrieved by + // sortableListsToHierarchy and sortableListsToArray + if (v.value !== undefined) { + $li.data('value', v.value); + } + + // Setting element id. This has to be done after appending + // it (and its descendants) in order to check whether its + // potencial id is already taken. + if (v.id) { + var id; + var baseId = $main.attr('id') + '_li_' + v.id; + // $elem might not yet be appended to DOM + var isUnique = !$('#'+baseId).length && !$elem.find('#'+baseId).length; + if (isUnique) { + id = baseId; + } else { + if (baseIdCounters[baseId] === undefined) { + baseIdCounters[baseId] = 0; + } + do { + id = baseId + '_' + ++baseIdCounters[baseId]; + isUnique = !$('#'+id).length && !$elem.find('#'+id).length; + } while (!isUnique); + } + $li.attr('id', id); + } + }); return $elem; } @@ -1365,10 +463,19 @@ function MenuEditor(idSelector, options) { * Data Output * @return String JSON menu scheme */ - this.getString = function () { + this.getJsonString = function () { var obj = $main.sortableListsToJson(); return JSON.stringify(obj); }; + this.getString = function () { + return $main.sortableListsToString(); + }; + this.getArray = function () { + return $main.sortableListsToArray(); + }; + this.getHierarchy = function () { + return $main.sortableListsToHierarchy(); + }; /** * Data Input * @param {Array} Object array. The nested menu scheme diff --git a/jquery-menu-editor.min.js b/jquery-menu-editor.min.js index 49bc77f..10bd69d 100644 --- a/jquery-menu-editor.min.js +++ b/jquery-menu-editor.min.js @@ -1,11 +1 @@ -function MenuEditor(e,t){var s=$("#"+e).data("level","0"),l={labelEdit:'',labelRemove:'',textConfirmDelete:"This item will be deleted. Are you sure?",iconPicker:{cols:4,rows:4,footer:!1,iconset:"fontawesome5"},maxLevel:-1,listOptions:{hintCss:{border:"1px dashed #13981D"},opener:{as:"html",close:'',open:'',openerCss:{"margin-right":"10px",float:"none"},openerClass:"btn btn-success btn-sm"},placeholderCss:{"background-color":"gray"},ignoreClass:"clickable",listsClass:"pl-0",listsCss:{"padding-top":"10px"},complete:function(e){return MenuEditor.updateButtons(s),s.updateLevels(0),!0},isAllowed:function(e,t,s){return h(e,s)}}};$.extend(!0,l,t);var n=null,o=!0,i=null,r=null,a=l.iconPicker,c=(t=l.listOptions,$("#"+e+"_icon").iconpicker(a));function d(){i[0].reset(),(c=c.iconpicker(a)).iconpicker("setIcon","empty"),r.attr("disabled",!0),n=null}function p(e){return $("").addClass(e.classCss).addClass("clickable").attr("href","#").html(e.text)}function u(){var e=$("
    ").addClass("btn-group float-right"),t=p({classCss:"btn btn-primary btn-sm btnEdit",text:l.labelEdit}),s=p({classCss:"btn btn-danger btn-sm btnRemove",text:l.labelRemove}),n=p({classCss:"btn btn-secondary btn-sm btnUp btnMove",text:''}),o=p({classCss:"btn btn-secondary btn-sm btnDown btnMove",text:''}),i=p({classCss:"btn btn-secondary btn-sm btnOut btnMove",text:''}),r=p({classCss:"btn btn-secondary btn-sm btnIn btnMove",text:''});return e.append(n).append(o).append(r).append(i).append(t).append(s),e}function f(e){$("").addClass("sortableListsOpener "+t.opener.openerClass).css(t.opener.openerCss).on("mousedown touchstart",function(e){var s=$(this).closest("li");return s.hasClass("sortableListsClosed")?s.iconOpen(t):s.iconClose(t),!1}).prependTo(e.children("div").first()),e.hasClass("sortableListsOpen")?e.iconOpen(t):e.iconClose(t)}function h(e,t){if(l.maxLevel<0)return!0;var s=0,n=e.find("ul").length;return s=0==t.length?0:parseInt(t.parent().data("level"))+1,console.log(s+n),s+n<=l.maxLevel}s.sortableLists(l.listOptions),c.on("change",function(e){i.find("[name=icon]").val(e.icon)}),$(document).on("click",".btnRemove",function(t){if(t.preventDefault(),confirm(l.textConfirmDelete)){var n=$(this).closest("ul");$(this).closest("li").remove();var o=!1;void 0!==n.attr("id")&&(o=n.attr("id").toString()===e),n.children().length||o||(n.prev("div").children(".sortableListsOpener").first().remove(),n.remove()),MenuEditor.updateButtons(s)}}),$(document).on("click",".btnEdit",function(e){e.preventDefault(),function(e){var t=e.data();$.each(t,function(e,t){i.find("[name="+e+"]").val(t)}),i.find(".item-menu").first().focus(),t.hasOwnProperty("icon")?c.iconpicker("setIcon",t.icon):c.iconpicker("setIcon","empty");r.removeAttr("disabled")}(n=$(this).closest("li"))}),s.on("click",".btnUp",function(e){e.preventDefault();var t=$(this).closest("li");t.prev("li").before(t),MenuEditor.updateButtons(s)}),s.on("click",".btnDown",function(e){e.preventDefault();var t=$(this).closest("li");t.next("li").after(t),MenuEditor.updateButtons(s)}),s.on("click",".btnOut",function(e){e.preventDefault();var t=$(this).closest("ul"),l=$(this).closest("li");l.closest("ul").closest("li").after(l),t.children().length<=0&&(t.prev("div").children(".sortableListsOpener").first().remove(),t.remove()),MenuEditor.updateButtons(s),s.updateLevels()}),s.on("click",".btnIn",function(e){e.preventDefault();var t=$(this).closest("li"),l=t.prev("li");if(!h(t,l))return!1;if(l.length>0)if((n=l.children("ul")).length>0)n.append(t);else{var n=$("