diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f7281a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{js,json,yml,scss,stylelintrc}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e4af26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.idea +/vendor +/node_modules +package-lock.json +composer.phar +composer.lock +phpunit.xml +.phpunit.result.cache +.DS_Store +Thumbs.db +.php_cs.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..e9505aa --- /dev/null +++ b/.php_cs @@ -0,0 +1,87 @@ + true, + 'blank_line_after_opening_tag' => true, + 'concat_space' => ['spacing' => 'none'], + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_empty_statement' => true, + 'simplified_null_return' => false, + 'encoding' => true, + 'single_blank_line_at_eof' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_spaces_after_function_name' => true, + 'function_declaration' => true, + 'include' => true, + 'indentation_type' => true, + 'no_alias_functions' => true, + 'blank_line_after_namespace' => true, + 'line_ending' => true, + 'no_trailing_comma_in_list_call' => true, + 'not_operator_with_successor_space' => false, + 'lowercase_constants' => true, + 'lowercase_keywords' => true, + 'method_argument_space' => true, + 'trailing_comma_in_multiline_array' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'single_import_per_statement' => true, + 'no_leading_namespace_whitespace' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'object_operator_without_whitespace' => true, + 'no_spaces_inside_parenthesis' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag' => true, + 'phpdoc_no_access' => true, + 'phpdoc_scalar' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_var_without_name' => true, + 'no_leading_import_slash' => true, + 'braces' => false, + 'blank_line_before_return' => true, + 'self_accessor' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_short_echo_tag' => true, + 'full_opening_tag' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'single_blank_line_before_namespace' => true, + 'single_line_after_imports' => true, + 'single_quote' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'cast_spaces' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'no_trailing_whitespace' => true, + 'trim_array_spaces' => true, + 'binary_operator_spaces' => ['align_equals' => false], + 'unary_operator_spaces' => true, + 'no_unused_imports' => true, + 'visibility_required' => true, + 'no_whitespace_in_blank_line' => true, +]; + +$finder = PhpCsFixer\Finder::create() + ->name('*.php') + ->exclude($excludes) + ->ignoreDotFiles(true) + ->in(__DIR__); + +return PhpCsFixer\Config::create() + ->setRules($rules) + ->setFinder($finder) + ->setUsingCache(true); diff --git a/README.md b/README.md new file mode 100644 index 0000000..54b360f --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Nova Order Field nestedset + +A field that make your resources orderable using [the laravel nestedset package](https://github.com/lazychaser/laravel-nestedset). + +## Requirements + +* PHP >= 7.1 +* Laravel Nova >= 2.0 +* Laravel Framework 5.8+ + +## Installation + +```sh +composer require novius/laravel-nova-order-nestedset-field +``` + +## Usage + +**Step 1** + +Use Kalnoy\Nestedset `NodeTrait` and Novius\LaravelNovaOrderNestedsetField `Orderable` trait on your model. + +Example : + +```php +use Kalnoy\Nestedset\NodeTrait; +use Novius\LaravelNovaOrderNestedsetField\Traits\Orderable; + +class Foo extends Model { + use NodeTrait; + use Orderable; + + public function getLftName() + { + return 'left'; + } + + public function getRgtName() + { + return 'right'; + } + + public function getParentIdName() + { + return 'parent'; + } +} + +``` + +**Step 2** + +Add the field to your resource and specify order for your resources. + + +```php +use Novius\LaravelNovaOrderNestedsetField\OrderNestedsetField; + +class FooResource extends Resource +{ + public function fields(Request $request) + { + return [ + OrderNestedsetField::make('Order'), + ]; + } + + /** + * @param \Illuminate\Database\Eloquent\Builder $query + * @param array $orderings + * @return \Illuminate\Database\Eloquent\Builder + */ + protected static function applyOrderings($query, array $orderings) + { + return $query->orderBy('left', 'asc'); + } +} + +``` + +**Scoping** + +Imagine you have `Menu` model and `MenuItems`. There is a one-to-many relationship +set up between these models. `MenuItem` has `menu_id` attribute for joining models +together. `MenuItem` incorporates nested sets. It is obvious that you would want to +process each tree separately based on `menu_id` attribute. In order to do so, you +need to specify this attribute as scope attribute: + +```php + protected function getScopeAttributes() + { + return ['menu_id']; + } +``` + +[Retrieve more information about usage on official doc](https://github.com/lazychaser/laravel-nestedset#scoping). + + +## Override default languages files + +Run: + +```sh +php artisan vendor:publish --provider="Novius\LaravelNovaOrderNestedsetField\OrderNestedsetFieldServiceProvider" --tag="lang" +``` + +## Lint + +Run php-cs with: + +```sh +composer run-script lint +``` + +## Contributing + +Contributions are welcome! +Leave an issue on Github, or create a Pull Request. + + +## Licence + +This package is under [GNU Affero General Public License v3](http://www.gnu.org/licenses/agpl-3.0.html) or (at your option) any later version. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6109ea5 --- /dev/null +++ b/composer.json @@ -0,0 +1,46 @@ +{ + "name": "novius/laravel-nova-order-nestedset-field", + "description": "A Laravel Nova field that make your resources orderable", + "keywords": [ + "laravel", + "nova", + "nestedset", + "trees", + "hierarchies" + ], + "license": "AGPL-3.0-or-later", + "authors": [ + { + "name": "Novius Agency", + "email": "team-developpeurs@novius.com", + "homepage": "https://www.novius.com" + } + ], + "require": { + "php": ">=7.1.0", + "kalnoy/nestedset": "^4.3.5" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.15.0" + }, + "autoload": { + "psr-4": { + "Novius\\LaravelNovaOrderNestedsetField\\": "src/" + } + }, + "scripts": { + "lint": [ + "php-cs-fixer fix --dry-run --config .php_cs -vv --diff --allow-risky=yes" + ] + }, + "extra": { + "laravel": { + "providers": [ + "Novius\\LaravelNovaOrderNestedsetField\\OrderNestedsetFieldServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + } +} diff --git a/dist/css/field.css b/dist/css/field.css new file mode 100644 index 0000000..e69de29 diff --git a/dist/js/field.js b/dist/js/field.js new file mode 100644 index 0000000..f8f9ac8 --- /dev/null +++ b/dist/js/field.js @@ -0,0 +1,401 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +__webpack_require__(1); +module.exports = __webpack_require__(6); + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +Nova.booting(function (Vue, router, store) { + Vue.component('index-order-nestedset-field', __webpack_require__(2)); +}); + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +var disposed = false +var normalizeComponent = __webpack_require__(3) +/* script */ +var __vue_script__ = __webpack_require__(4) +/* template */ +var __vue_template__ = __webpack_require__(5) +/* template functional */ +var __vue_template_functional__ = false +/* styles */ +var __vue_styles__ = null +/* scopeId */ +var __vue_scopeId__ = null +/* moduleIdentifier (server only) */ +var __vue_module_identifier__ = null +var Component = normalizeComponent( + __vue_script__, + __vue_template__, + __vue_template_functional__, + __vue_styles__, + __vue_scopeId__, + __vue_module_identifier__ +) +Component.options.__file = "resources/js/components/IndexField.vue" + +/* hot reload */ +if (false) {(function () { + var hotAPI = require("vue-hot-reload-api") + hotAPI.install(require("vue"), false) + if (!hotAPI.compatible) return + module.hot.accept() + if (!module.hot.data) { + hotAPI.createRecord("data-v-9e63f81a", Component.options) + } else { + hotAPI.reload("data-v-9e63f81a", Component.options) + } + module.hot.dispose(function (data) { + disposed = true + }) +})()} + +module.exports = Component.exports + + +/***/ }), +/* 3 */ +/***/ (function(module, exports) { + +/* globals __VUE_SSR_CONTEXT__ */ + +// IMPORTANT: Do NOT use ES2015 features in this file. +// This module is a runtime utility for cleaner component module output and will +// be included in the final webpack user bundle. + +module.exports = function normalizeComponent ( + rawScriptExports, + compiledTemplate, + functionalTemplate, + injectStyles, + scopeId, + moduleIdentifier /* server only */ +) { + var esModule + var scriptExports = rawScriptExports = rawScriptExports || {} + + // ES6 modules interop + var type = typeof rawScriptExports.default + if (type === 'object' || type === 'function') { + esModule = rawScriptExports + scriptExports = rawScriptExports.default + } + + // Vue.extend constructor export interop + var options = typeof scriptExports === 'function' + ? scriptExports.options + : scriptExports + + // render functions + if (compiledTemplate) { + options.render = compiledTemplate.render + options.staticRenderFns = compiledTemplate.staticRenderFns + options._compiled = true + } + + // functional template + if (functionalTemplate) { + options.functional = true + } + + // scopedId + if (scopeId) { + options._scopeId = scopeId + } + + var hook + if (moduleIdentifier) { // server build + hook = function (context) { + // 2.3 injection + context = + context || // cached call + (this.$vnode && this.$vnode.ssrContext) || // stateful + (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional + // 2.2 with runInNewContext: true + if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { + context = __VUE_SSR_CONTEXT__ + } + // inject component styles + if (injectStyles) { + injectStyles.call(this, context) + } + // register component module identifier for async chunk inferrence + if (context && context._registeredComponents) { + context._registeredComponents.add(moduleIdentifier) + } + } + // used by ssr in case component is cached and beforeCreate + // never gets called + options._ssrRegister = hook + } else if (injectStyles) { + hook = injectStyles + } + + if (hook) { + var functional = options.functional + var existing = functional + ? options.render + : options.beforeCreate + + if (!functional) { + // inject component registration as beforeCreate hook + options.beforeCreate = existing + ? [].concat(existing, hook) + : [hook] + } else { + // for template-only hot-reload because in that case the render fn doesn't + // go through the normalizer + options._injectStyles = hook + // register for functioal component in vue file + options.render = function renderWithStyleInjection (h, context) { + hook.call(context) + return existing(h, context) + } + } + } + + return { + esModule: esModule, + exports: scriptExports, + options: options + } +} + + +/***/ }), +/* 4 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + +/* harmony default export */ __webpack_exports__["default"] = ({ + props: ['resourceName', 'field'], + computed: { + resourceId: function resourceId() { + return this.$parent.resource.id.value; + } + }, + methods: { + reorderResource: function reorderResource(direction) { + var _this = this; + + Nova.request().post('/nova-vendor/nova-order-nestedset-field/' + this.resourceName, { + direction: direction, + resource: this.resourceName, + resourceId: this.resourceId + }).then(function () { + _this.$toasted.show(_this.__('nova-order-nestedset-field::messages.order_updated'), { type: 'success' }); + + _this.$router.go(_this.$router.currentRoute); + }); + } + } +}); + +/***/ }), +/* 5 */ +/***/ (function(module, exports, __webpack_require__) { + +var render = function() { + var _vm = this + var _h = _vm.$createElement + var _c = _vm._self._c || _h + return _c("div", { staticClass: "flex items-center" }, [ + _vm.field.last != _vm.resourceId + ? _c( + "button", + { + staticClass: "cursor-pointer text-70 hover:text-primary mr-3", + on: { + click: function($event) { + return _vm.reorderResource("down") + } + } + }, + [ + _c( + "svg", + { + staticClass: "fill-white", + attrs: { + xmlns: "http://www.w3.org/2000/svg", + height: "22", + width: "22", + stroke: "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "2", + viewBox: "0 0 24 24" + } + }, + [ + _c("circle", { attrs: { cx: "12", cy: "12", r: "10" } }), + _vm._v(" "), + _c("polyline", { attrs: { points: "8 12 12 16 16 12" } }), + _vm._v(" "), + _c("line", { attrs: { x1: "12", x2: "12", y1: "8", y2: "16" } }) + ] + ) + ] + ) + : _vm._e(), + _vm._v(" "), + _vm.field.first != _vm.resourceId + ? _c( + "button", + { + staticClass: "cursor-pointer text-70 hover:text-primary", + on: { + click: function($event) { + return _vm.reorderResource("up") + } + } + }, + [ + _c( + "svg", + { + staticClass: "fill-white", + attrs: { + xmlns: "http://www.w3.org/2000/svg", + height: "22", + width: "22", + stroke: "currentColor", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "2", + viewBox: "0 0 24 24" + } + }, + [ + _c("circle", { attrs: { cx: "12", cy: "12", r: "10" } }), + _vm._v(" "), + _c("polyline", { attrs: { points: "16 12 12 8 8 12" } }), + _vm._v(" "), + _c("line", { attrs: { x1: "12", x2: "12", y1: "16", y2: "8" } }) + ] + ) + ] + ) + : _vm._e() + ]) +} +var staticRenderFns = [] +render._withStripped = true +module.exports = { render: render, staticRenderFns: staticRenderFns } +if (false) { + module.hot.accept() + if (module.hot.data) { + require("vue-hot-reload-api") .rerender("data-v-9e63f81a", module.exports) + } +} + +/***/ }), +/* 6 */ +/***/ (function(module, exports) { + +// removed by extract-text-webpack-plugin + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/dist/mix-manifest.json b/dist/mix-manifest.json new file mode 100644 index 0000000..5cd31d2 --- /dev/null +++ b/dist/mix-manifest.json @@ -0,0 +1,4 @@ +{ + "/js/field.js": "/js/field.js", + "/css/field.css": "/css/field.css" +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f91618 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "private": true, + "scripts": { + "dev": "npm run development", + "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", + "watch-poll": "npm run watch -- --watch-poll", + "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", + "prod": "npm run production", + "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" + }, + "devDependencies": { + "cross-env": "^5.0.0", + "laravel-mix": "^1.0", + "laravel-nova": "^1.0" + }, + "dependencies": { + "vue": "^2.5.0" + } +} diff --git a/resources/js/components/IndexField.vue b/resources/js/components/IndexField.vue new file mode 100644 index 0000000..fcbb743 --- /dev/null +++ b/resources/js/components/IndexField.vue @@ -0,0 +1,60 @@ + + + diff --git a/resources/js/field.js b/resources/js/field.js new file mode 100644 index 0000000..f2721b8 --- /dev/null +++ b/resources/js/field.js @@ -0,0 +1,3 @@ +Nova.booting((Vue, router, store) => { + Vue.component('index-order-nestedset-field', require('./components/IndexField')) +}); diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php new file mode 100644 index 0000000..b196ff7 --- /dev/null +++ b/resources/lang/en/errors.php @@ -0,0 +1,6 @@ + 'Error : model :model should implements following trait :class', + 'bad_direction' => 'Error : bad direction', +]; diff --git a/resources/lang/en/messages.php b/resources/lang/en/messages.php new file mode 100644 index 0000000..290cb9d --- /dev/null +++ b/resources/lang/en/messages.php @@ -0,0 +1,5 @@ + 'Position successfully updated.', +]; diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php new file mode 100644 index 0000000..a2786f1 --- /dev/null +++ b/resources/lang/fr/errors.php @@ -0,0 +1,6 @@ + 'Erreur : le modèle :model doit implémenter le trait :class', + 'bad_direction' => 'Erreur : mauvaise direction spécifiée', +]; diff --git a/resources/lang/fr/messages.php b/resources/lang/fr/messages.php new file mode 100644 index 0000000..eafb9b4 --- /dev/null +++ b/resources/lang/fr/messages.php @@ -0,0 +1,5 @@ + 'L\'ordre a bien été modifié.', +]; diff --git a/resources/sass/field.scss b/resources/sass/field.scss new file mode 100644 index 0000000..f85ad40 --- /dev/null +++ b/resources/sass/field.scss @@ -0,0 +1 @@ +// Nova Tool CSS diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..b182e23 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,5 @@ +get('resourceId'); + $model = $request->findModelOrFail($resourceId); + + if (!in_array(Orderable::class, class_uses($model))) { + abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [ + 'class' => Orderable::class, + 'model' => get_class($model), + ])); + } + + if (!in_array(NodeTrait::class, class_uses($model))) { + abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [ + 'class' => NodeTrait::class, + 'model' => get_class($model), + ])); + } + + $direction = (string) $request->get('direction', ''); + if (!in_array($direction, ['up', 'down'])) { + abort(500, trans('nova-order-nestedset-field::errors.bad_direction')); + } + + if ($direction === 'up') { + $model->moveOrderUp(); + } else { + $model->moveOrderDown(); + } + } +} diff --git a/src/OrderNestedsetField.php b/src/OrderNestedsetField.php new file mode 100644 index 0000000..cab13a6 --- /dev/null +++ b/src/OrderNestedsetField.php @@ -0,0 +1,59 @@ + Orderable::class, + 'model' => get_class($resource), + ])); + } + + if (!in_array(NodeTrait::class, class_uses($resource))) { + abort(500, trans('nova-order-nestedset-field::errors.model_should_use_trait', [ + 'class' => NodeTrait::class, + 'model' => get_class($resource), + ])); + } + + $first = $resource->buildSortQuery()->ordered()->first(); + $last = $resource->buildSortQuery()->ordered('desc')->first(); + + $this->withMeta([ + 'first' => is_null($first) ? null : $first->id, + 'last' => is_null($last) ? null : $last->id, + ]); + + return data_get($resource, $attribute); + } +} diff --git a/src/OrderNestedsetFieldServiceProvider.php b/src/OrderNestedsetFieldServiceProvider.php new file mode 100644 index 0000000..f5daf34 --- /dev/null +++ b/src/OrderNestedsetFieldServiceProvider.php @@ -0,0 +1,57 @@ +app->booted(function () { + $this->routes(); + }); + + Nova::serving(function (ServingNova $event) { + Nova::script('laravel-nova-order-nestedset-field', __DIR__.'/../dist/js/field.js'); + Nova::style('laravel-nova-order-nestedset-field', __DIR__.'/../dist/css/field.css'); + }); + + $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'nova-order-nestedset-field'); + $this->publishes([__DIR__.'/../resources/lang' => resource_path('lang/vendor/nova-order-nestedset-field')], 'lang'); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + // + } + + /** + * Register the tool's routes. + * + * @return void + */ + protected function routes() + { + if ($this->app->routesAreCached()) { + return; + } + + Route::middleware(['nova']) + ->prefix('nova-vendor/nova-order-nestedset-field') + ->group(__DIR__.'/../routes/api.php'); + } +} diff --git a/src/Traits/Orderable.php b/src/Traits/Orderable.php new file mode 100644 index 0000000..9b61028 --- /dev/null +++ b/src/Traits/Orderable.php @@ -0,0 +1,43 @@ +getPrevSibling(); + if (!empty($prevItem)) { + $this->insertBeforeNode($prevItem); + } + } + + public function moveOrderDown() + { + $nextItem = $this->getNextSibling(); + if (!empty($nextItem)) { + $this->insertAfterNode($nextItem); + } + } + + public function buildSortQuery() + { + $query = static::query()->where($this->getParentIdName(), $this->getParentId()); + if (!empty($this->getScopeAttributes())) { + foreach ($this->getScopeAttributes() as $attributeName) { + if (!empty($this->{$attributeName})) { + $query->where($attributeName, $this->{$attributeName}); + } + } + } + + return $query; + } + + public function scopeOrdered(Builder $query, string $direction = 'asc') + { + return $query->orderBy($this->getLftName(), $direction); + } +} diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 0000000..d9623de --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,5 @@ +let mix = require('laravel-mix') + +mix.setPublicPath('dist') + .js('resources/js/field.js', 'js') + .sass('resources/sass/field.scss', 'css')