diff --git a/plugins/ahmadfatoni/apigenerator/Plugin.php b/plugins/ahmadfatoni/apigenerator/Plugin.php new file mode 100644 index 0000000..575a4fd --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/Plugin.php @@ -0,0 +1,18 @@ + October CMS plugin to build RESTful APIs. + +## Features + + - Auto generate routes + - Auto Generate Controller (CRUD) + - Support relationship restful API + +## Install +``` +composer require AhmadFatoni.ApiGenerator +``` + +## Usage + +### Form +- API Name : Name of your API module +- Base Endpoint : Base endpoint of your API, ex : api/v1/modulename +- Short Description : Describe your API +- Model : select model that will be created API +- Custom Condition : Build customer response using JSON modeling + +### Custom Condition Example +``` +{ + 'fillable': 'id,title,content', + 'relation': [{ + 'name': 'user', + 'fillable': 'id,first_name' + }, { + 'name': 'categories', + 'fillable': 'id,name + }] +} +``` +* please replace single quote with quote + +## Contribute + +Pull Requests accepted. + +## Contact + +You can communicate with me using [linkedin](https://www.linkedin.com/in/ahmad-fatoni) + +## License +The OctoberCMS platform is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/plugins/ahmadfatoni/apigenerator/composer.json b/plugins/ahmadfatoni/apigenerator/composer.json new file mode 100644 index 0000000..8892120 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/composer.json @@ -0,0 +1,8 @@ +{ + "name": "ahmadfatoni/apigenerator-plugin", + "type": "october-plugin", + "description": "None", + "require": { + "composer/installers": "~1.0" + } +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/ApiGeneratorController.php b/plugins/ahmadfatoni/apigenerator/controllers/ApiGeneratorController.php new file mode 100644 index 0000000..1eea0a4 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/ApiGeneratorController.php @@ -0,0 +1,336 @@ +files = $files; + } + + /** + * delete selected data (multiple delete) + * @return [type] [description] + */ + public function index_onDelete() + { + if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { + + foreach ($checkedIds as $id) { + if ((!$item = ApiGenerator::find($id))) + continue; + $name = $item->name; + if($item->delete()){ + $this->deleteApi($name); + } + } + + Flash::success('Successfully deleted those data.'); + } + + return $this->listRefresh(); + } + + /** + * generate API + * @param Request $request [description] + * @return [type] [description] + */ + public function generateApi(Request $request){ + + $data['model'] = $request->model; + $modelname = explode("\\", $request->model); + $modelname = $modelname[count($modelname)-1]; + $data['modelname'] = $modelname; + $data['controllername'] = str_replace(" ", "", $request->name); + $data['endpoint'] = $request->endpoint; + $data['custom_format'] = $request->custom_format; + + if( strpos($data['controllername'], ".") OR strpos($data['controllername'], "/") ){ + + Flash::success('Failed to create data, invalid API name.'); + return Redirect::to( Backend::url($this->homePage)); + + } + + if( isset($request->id) ){ + $this->deleteApi($request->oldname, 'false'); + } + + $this->files->put(__DIR__ . $this->path . $data['controllername'].'Controller.php', $this->compile($data)); + + $this->files->put(__DIR__ . '/'.'../routes.php', $this->compileRoute($data)); + + return Redirect::to( Backend::url($this->homePage)); + + } + + /** + * delete available API + * @param [type] $name [description] + * @param [type] $redirect [description] + * @return [type] [description] + */ + public function deleteApi($name, $redirect = null){ + + $fileLocation = __DIR__ . $this->path.$name; + $fileLocation = str_replace(".", "", $fileLocation); + + if( ! file_exists($fileLocation.'Controller.php') ){ + + Flash::success('Failed to delete data, invalid file location.'); + return Redirect::to( Backend::url($this->homePage)); + + } + + if( strpos( strtolower($name), 'apigenerator' ) === false){ + $data = []; + + //generate new route + $this->files->put(__DIR__ . '/'.'../routes.php', $this->compileRoute($data)); + + //remove controller + if (file_exists( __DIR__ . $this->path.$name.'Controller.php' )) { + + unlink(__DIR__ . $this->path.$name.'Controller.php'); + + } + + if( $redirect != null ){ + return 'success without redirect'; + } + } + + return Redirect::to( Backend::url($this->homePage)); + + } + + public function updateApi($name){ + + } + + /** + * compile controller from template + * @param [type] $data [description] + * @return [type] [description] + */ + public function compile($data){ + if( $data['custom_format'] != ''){ + + $template = $this->files->get(__DIR__ .'/../template/customcontroller.dot'); + $template = $this->replaceAttribute($template, $data); + $template = $this->replaceCustomAttribute($template, $data); + }else{ + $template = $this->files->get(__DIR__ .'/../template/controller.dot'); + $template = $this->replaceAttribute($template, $data); + } + return $template; + } + + /** + * replace attribute + * @param [type] $template [description] + * @param [type] $data [description] + * @return [type] [description] + */ + public function replaceAttribute($template, $data){ + if( isset( $data['model'] ) ){ + $template = str_replace('{{model}}', $data['model'], $template); + } + $template = str_replace('{{modelname}}', $data['modelname'], $template); + $template = str_replace('{{controllername}}', $data['controllername'], $template); + return $template; + } + + /** + * replace custom attribute + * @param [type] $template [description] + * @param [type] $data [description] + * @return [type] [description] + */ + public function replaceCustomAttribute($template, $data){ + + $arr = str_replace('\t', '', $data['custom_format']); + $arr = json_decode($arr); + $select = str_replace('
', '', $this->compileOpenIndexFunction($data['modelname'], 'index')); + $show = str_replace('
', '', $this->compileOpenIndexFunction($data['modelname'], 'show')); + $fillableParent = ''; + + if( isset($arr->fillable) AND $arr->fillable != null ) { + $fillableParent = $this->compileFillableParent($arr->fillable); + } + + if( isset($arr->relation) AND $arr->relation != null AND is_array($arr->relation) AND count($arr->relation) > 0) { + $select .= str_replace('
', '', $this->compileFillableChild($arr->relation)); + $show .= str_replace('
', '', $this->compileFillableChild($arr->relation)); + } + + $select .= "->select(".$fillableParent.")"; + $show .= "->select(".$fillableParent.")->where('id', '=', \$id)->first();"; + + ( $fillableParent != '') ? $select .= "->get()->toArray();" : $select .= "->toArray();" ; + + $closeFunction = str_replace('
', '', nl2br( + " + return \$this->helpers->apiArrayResponseBuilder(200, 'success', \$data); + }")); + $select .= $closeFunction; + $show .= $closeFunction; + + $template = str_replace('{{select}}', $select, $template); + $template = str_replace('{{show}}', $show, $template); + + return $template; + } + + public function compileOpenIndexFunction($modelname, $type){ + if( $type == 'index'){ + return nl2br(" + public function index(){ + \$data = \$this->".$modelname); + }else{ + return nl2br(" + public function show(\$id){ + \$data = \$this->".$modelname); + } + + } + + public function compileFillableParent($fillable){ + + $fillableParentArr = explode(",", $fillable); + $fillableParent = ''; + + foreach ($fillableParentArr as $key) { + + $fillableParent .= ",'".$key."'"; + + } + + $fillableParent = substr_replace($fillableParent, '', 0 , 1); + + return $fillableParent; + } + + public function compileFillableChild($fillable){ + + $select = "->with(array("; + + foreach ($fillable as $key) { + + $fillableChild = ""; + + if( isset($key->fillable) AND $key->fillable != null ){ + $fillableChildArr = explode(",", $key->fillable); + + + foreach ($fillableChildArr as $key2) { + + $fillableChild .= ",'".$key2."'"; + + } + + $fillableChild = substr_replace($fillableChild, '', 0 , 1); + } + + $select .= nl2br( + " + '".$key->name."'=>function(\$query){ + \$query->select(".$fillableChild."); + },"); + + } + + $select .= " ))"; + + return $select; + } + + public function compileRoute($data){ + + $oldData = ApiGenerator::all(); + $routeList = ""; + + if( count($oldData) > 0 ){ + + $routeList .= $this->parseRouteOldData($oldData, $data); + + } + + if( count($data) > 0 ){ + $data['modelname'] = $data['endpoint']; + if( $data['modelname'][0] == "/" ){ + $data['modelname'] = substr_replace($data['modelname'], '', 0 , 1); + } + $routeList .= $this->parseRoute($data); + } + + $route = $this->files->get(__DIR__ .'/../template/routes.dot'); + $route = str_replace('{{route}}', $routeList, $route); + + return $route; + + } + + public function parseRouteOldData($oldData, $data = null){ + + $routeList = ""; + + if( count($data) == 0 ) $data['modelname']=''; + + foreach ( $oldData as $key ) { + + $modelname = explode("\\", $key->model); + $modelname = $modelname[count($modelname)-1]; + $old['modelname'] = $key->endpoint; + $old['controllername'] = $key->name; + + if( $data['modelname'] != $modelname ){ + + if( $old['modelname'][0] == "/" ){ + $old['modelname'] = substr_replace($old['modelname'], '', 0 , 1); + } + + $routeList .= $this->parseRoute($old); + } + } + + return $routeList; + + } + + public function parseRoute($data){ + + $template = $this->files->get(__DIR__ .'/../template/route.dot'); + $template = $this->replaceAttribute($template, $data); + return $template; + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } +} diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/cardController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/cardController.php new file mode 100644 index 0000000..23d9a84 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/api/cardController.php @@ -0,0 +1,99 @@ +Card_data = $Card_data; + $this->helpers = $helpers; + } + + public function index(){ + +// $data = $this->Card_data->all()->toArray(); + $data = $this->Card_data->with(['translations:locale,model_id,attribute_data','image'])->get(); + return $this->helpers->apiArrayResponseBuilder(200, 'success', $data); + } + + public function show($id){ + + $data = $this->Card_data::find($id); + + if ($data){ + return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]); + } else { + $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']); + } + + } + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->Card_data->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->Card_data->rules); + + if( $validation->passes() ){ + $this->Card_data->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->Card_data->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->Card_data->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->Card_data->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->Card_data->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/creditController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/creditController.php new file mode 100644 index 0000000..c58a6ac --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/api/creditController.php @@ -0,0 +1,99 @@ +Credit_data = $Credit_data; + $this->helpers = $helpers; + } + + public function index(){ + +// $data = $this->Credit_data->all()->toArray(); + $data = $this->Credit_data->with(['translations:locale,model_id,attribute_data'])->get(); + return $this->helpers->apiArrayResponseBuilder(200, 'success', $data); + } + + public function show($id){ + + $data = $this->Credit_data::find($id); + + if ($data){ + return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]); + } else { + $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']); + } + + } + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->Credit_data->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->Credit_data->rules); + + if( $validation->passes() ){ + $this->Credit_data->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->Credit_data->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->Credit_data->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->Credit_data->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->Credit_data->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/readme.txt b/plugins/ahmadfatoni/apigenerator/controllers/api/readme.txt new file mode 100644 index 0000000..f972e8d --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/api/readme.txt @@ -0,0 +1 @@ +api controller here \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/typeAccountReplenishmentController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/typeAccountReplenishmentController.php new file mode 100644 index 0000000..37b2df0 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/api/typeAccountReplenishmentController.php @@ -0,0 +1,100 @@ +TypeAccountReplenishment = $TypeAccountReplenishment; + $this->helpers = $helpers; + } + + public function index(){ + + // $data = $this->TypeAccountReplenishment->all()->toArray(); + $data = $this->TypeAccountReplenishment->with(['translations:locale,model_id,attribute_data'])->get(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', $data); + } + + public function show($id){ + + $data = $this->TypeAccountReplenishment::find($id); + + if ($data){ + return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]); + } else { + $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']); + } + + } + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->TypeAccountReplenishment->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->TypeAccountReplenishment->rules); + + if( $validation->passes() ){ + $this->TypeAccountReplenishment->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->TypeAccountReplenishment->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->TypeAccountReplenishment->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->TypeAccountReplenishment->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->TypeAccountReplenishment->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/api/usersigninController.php b/plugins/ahmadfatoni/apigenerator/controllers/api/usersigninController.php new file mode 100644 index 0000000..0535ec8 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/api/usersigninController.php @@ -0,0 +1,99 @@ +User = $User; + $this->helpers = $helpers; + } + + public function index(){ + + $data = $this->User->all()->toArray(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', $data); + } + + public function show($id){ + + $data = $this->User::find($id); + + if ($data){ + return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]); + } else { + $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']); + } + + } + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->User->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->User->rules); + + if( $validation->passes() ){ + $this->User->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->User->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->User->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->User->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->User->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_list_toolbar.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_list_toolbar.htm new file mode 100644 index 0000000..82a3622 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_list_toolbar.htm @@ -0,0 +1,18 @@ +
+ + +
diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_reorder_toolbar.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_reorder_toolbar.htm new file mode 100644 index 0000000..59b78d0 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/_reorder_toolbar.htm @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_form.yaml b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_form.yaml new file mode 100644 index 0000000..9317c6b --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_form.yaml @@ -0,0 +1,10 @@ +name: ApiGeneratorController +form: $/ahmadfatoni/apigenerator/models/apigenerator/fields.yaml +modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator +defaultRedirect: ahmadfatoni/apigenerator/apigeneratorcontroller +create: + redirect: 'ahmadfatoni/apigenerator/apigeneratorcontroller/update/:id' + redirectClose: ahmadfatoni/apigenerator/apigeneratorcontroller +update: + redirect: ahmadfatoni/apigenerator/apigeneratorcontroller + redirectClose: ahmadfatoni/apigenerator/apigeneratorcontroller diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_list.yaml b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_list.yaml new file mode 100644 index 0000000..d36a63f --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_list.yaml @@ -0,0 +1,11 @@ +list: $/ahmadfatoni/apigenerator/models/apigenerator/columns.yaml +modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator +title: ApiGeneratorController +noRecordsMessage: 'backend::lang.list.no_records' +showSetup: true +showCheckboxes: true +toolbar: + buttons: list_toolbar + search: + prompt: 'backend::lang.list.search_prompt' +recordUrl: 'ahmadfatoni/apigenerator/apigeneratorcontroller/update/:id' diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_reorder.yaml b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_reorder.yaml new file mode 100644 index 0000000..70fda7b --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/config_reorder.yaml @@ -0,0 +1,4 @@ +title: ApiGeneratorController +modelClass: AhmadFatoni\ApiGenerator\Models\ApiGenerator +toolbar: + buttons: reorder_toolbar diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/create.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/create.htm new file mode 100644 index 0000000..22322c5 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/create.htm @@ -0,0 +1,97 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
+ formRender() ?> +
+ + + +
+
+ + + + + + + Cancel + +
+
+ + + + + + +

fatalError)) ?>

+

+ + + \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/index.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/index.htm new file mode 100644 index 0000000..ea43a36 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/index.htm @@ -0,0 +1 @@ +listRender() ?> diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/preview.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/preview.htm new file mode 100644 index 0000000..f259af3 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/preview.htm @@ -0,0 +1,22 @@ + + + + +fatalError): ?> + +
+ formRenderPreview() ?> +
+ + +

fatalError) ?>

+ + +

+ + + +

\ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/reorder.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/reorder.htm new file mode 100644 index 0000000..9813ab4 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/reorder.htm @@ -0,0 +1,8 @@ + + + + +reorderRender() ?> \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/update.htm b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/update.htm new file mode 100644 index 0000000..dc6594a --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/controllers/apigeneratorcontroller/update.htm @@ -0,0 +1,133 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
+ formRender() ?> +
+ + +
+
+ + +
Cancel
+ + + + + + + +
+
+ + + +

fatalError)) ?>

+

+ + + + + + + + \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/helpers/Helpers.php b/plugins/ahmadfatoni/apigenerator/helpers/Helpers.php new file mode 100644 index 0000000..1145f7f --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/helpers/Helpers.php @@ -0,0 +1,19 @@ + (isset($statusCode)) ? $statusCode : 500, + 'message' => (isset($message)) ? $message : 'error' + ]; + if (count($data) > 0) { + $arr['data'] = $data; + } + + return response()->json($arr, $arr['status_code']); + //return $arr; + + } +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/lang/en/lang.php b/plugins/ahmadfatoni/apigenerator/lang/en/lang.php new file mode 100644 index 0000000..9ade59e --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/lang/en/lang.php @@ -0,0 +1,6 @@ + [ + 'name' => 'API-Generator', + 'description' => 'Generate API base on Builder Plugin' + ] +]; \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/models/ApiGenerator.php b/plugins/ahmadfatoni/apigenerator/models/ApiGenerator.php new file mode 100644 index 0000000..531bfd1 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/models/ApiGenerator.php @@ -0,0 +1,76 @@ + 'required|unique:ahmadfatoni_apigenerator_data,name|regex:/^[\pL\s\-]+$/u', + 'endpoint' => 'required|unique:ahmadfatoni_apigenerator_data,endpoint', + 'custom_format' => 'json' + ]; + + public $customMessages = [ + 'custom_format.json' => 'Invalid Json Format Custom Condition' + ]; + + /* + * Disable timestamps by default. + * Remove this line if timestamps are defined in the database table. + */ + public $timestamps = false; + + /** + * @var string The database table used by the model. + */ + public $table = 'ahmadfatoni_apigenerator_data'; + + /** + * get model List + * @return [type] [description] + */ + public function getModelOptions(){ + + return ComponentHelper::instance()->listGlobalModels(); + } + + /** + * [setCustomFormatAttribute description] + * @param [type] $value [description] + */ + public function setCustomFormatAttribute($value){ + + $json = str_replace('\t', '', $value); + $json = json_decode($json); + + if( $json != null){ + + if( ! isset($json->fillable) AND ! isset($json->relation) ){ + + return $this->attributes['custom_format'] = 'invalid format'; + + } + + if( isset($json->relation) AND $json->relation != null ){ + foreach ($json->relation as $key) { + if( !isset($key->name) OR $key->name == null ){ + return $this->attributes['custom_format'] = 'invalid format'; + } + } + } + } + + return $this->attributes['custom_format'] = $value; + + } + +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/models/apigenerator/columns.yaml b/plugins/ahmadfatoni/apigenerator/models/apigenerator/columns.yaml new file mode 100644 index 0000000..58ce8eb --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/models/apigenerator/columns.yaml @@ -0,0 +1,9 @@ +columns: + name: + label: 'API NAME' + type: text + searchable: true + sortable: true + endpoint: + label: 'BASE ENDPOINT' + type: text diff --git a/plugins/ahmadfatoni/apigenerator/models/apigenerator/fields.yaml b/plugins/ahmadfatoni/apigenerator/models/apigenerator/fields.yaml new file mode 100644 index 0000000..a8e6e0b --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/models/apigenerator/fields.yaml @@ -0,0 +1,33 @@ +fields: + name: + label: 'API Name' + oc.commentPosition: '' + span: auto + placeholder: 'Name of your API' + required: 1 + type: text + endpoint: + label: 'Base Endpoint' + oc.commentPosition: '' + span: auto + placeholder: api/v1/modulename + required: 1 + type: text + description: + label: 'Short Description' + oc.commentPosition: '' + span: auto + placeholder: 'Descript your API' + type: text + model: + label: 'Select Model' + oc.commentPosition: '' + span: auto + required: 1 + type: dropdown + custom_format: + label: 'Custom Condition (Fillable and Relation)' + size: large + oc.commentPosition: '' + span: full + type: textarea diff --git a/plugins/ahmadfatoni/apigenerator/plugin.yaml b/plugins/ahmadfatoni/apigenerator/plugin.yaml new file mode 100644 index 0000000..598021d --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/plugin.yaml @@ -0,0 +1,17 @@ +plugin: + name: 'ahmadfatoni.apigenerator::lang.plugin.name' + description: 'ahmadfatoni.apigenerator::lang.plugin.description' + author: AhmadFatoni + icon: oc-icon-bolt + homepage: '' +navigation: + api-generator: + label: 'API Generator' + url: ahmadfatoni/apigenerator/apigeneratorcontroller + icon: icon-cogs + permissions: + - ahmadfatoni.apigenerator.manage +permissions: + ahmadfatoni.apigenerator.manage: + tab: 'API Generator' + label: 'Manage the API Generator' diff --git a/plugins/ahmadfatoni/apigenerator/routes.php b/plugins/ahmadfatoni/apigenerator/routes.php new file mode 100644 index 0000000..fcb83a5 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/routes.php @@ -0,0 +1,12 @@ + 'fatoni.generate.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@generateApi')); +Route::post('fatoni/update/api/{id}', array('as' => 'fatoni.update.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@updateApi')); +Route::get('fatoni/delete/api/{id}', array('as' => 'fatoni.delete.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@deleteApi')); + +Route::resource('api/v1/credit_data', 'AhmadFatoni\ApiGenerator\Controllers\API\creditController', ['except' => ['destroy', 'create', 'edit']]); +Route::get('api/v1/credit_data/{id}/delete', ['as' => 'api/v1/credit_data.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\creditController@destroy']); +Route::resource('api/v1/card_data', 'AhmadFatoni\ApiGenerator\Controllers\API\cardController', ['except' => ['destroy', 'create', 'edit']]); +Route::get('api/v1/card_data/{id}/delete', ['as' => 'api/v1/card_data.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\cardController@destroy']); +Route::resource('api/v1/type_account_replenishment', 'AhmadFatoni\ApiGenerator\Controllers\API\typeAccountReplenishmentController', ['except' => ['destroy', 'create', 'edit']]); +Route::get('api/v1/type_account_replenishment/{id}/delete', ['as' => 'api/v1/type_account_replenishment.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\typeAccountReplenishmentController@destroy']); \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/template/controller.dot b/plugins/ahmadfatoni/apigenerator/template/controller.dot new file mode 100644 index 0000000..ff20ea3 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/template/controller.dot @@ -0,0 +1,99 @@ +{{modelname}} = ${{modelname}}; + $this->helpers = $helpers; + } + + public function index(){ + + $data = $this->{{modelname}}->all()->toArray(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', $data); + } + + public function show($id){ + + $data = $this->{{modelname}}::find($id); + + if ($data){ + return $this->helpers->apiArrayResponseBuilder(200, 'success', [$data]); + } else { + $this->helpers->apiArrayResponseBuilder(404, 'not found', ['error' => 'Resource id=' . $id . ' could not be found']); + } + + } + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->{{modelname}}->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->{{modelname}}->rules); + + if( $validation->passes() ){ + $this->{{modelname}}->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->{{modelname}}->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->{{modelname}}->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->{{modelname}}->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->{{modelname}}->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/template/controller.php b/plugins/ahmadfatoni/apigenerator/template/controller.php new file mode 100644 index 0000000..f78dde0 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/template/controller.php @@ -0,0 +1,35 @@ +{{modelname}} = ${{modelname}}; + } + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + + // public function create(Request $request){ + + // $arr = $request->all(); + + // while ( $data = current($arr)) { + // $this-> + // } + // return json_encode($this->{{modelname}}->store($request)); + + // } +} diff --git a/plugins/ahmadfatoni/apigenerator/template/customcontroller.dot b/plugins/ahmadfatoni/apigenerator/template/customcontroller.dot new file mode 100644 index 0000000..be5cf6f --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/template/customcontroller.dot @@ -0,0 +1,83 @@ +{{modelname}} = ${{modelname}}; + $this->helpers = $helpers; + } + + {{select}} + + {{show}} + + public function store(Request $request){ + + $arr = $request->all(); + + while ( $data = current($arr)) { + $this->{{modelname}}->{key($arr)} = $data; + next($arr); + } + + $validation = Validator::make($request->all(), $this->{{modelname}}->rules); + + if( $validation->passes() ){ + $this->{{modelname}}->save(); + return $this->helpers->apiArrayResponseBuilder(201, 'created', ['id' => $this->{{modelname}}->id]); + }else{ + return $this->helpers->apiArrayResponseBuilder(400, 'fail', $validation->errors() ); + } + + } + + public function update($id, Request $request){ + + $status = $this->{{modelname}}->where('id',$id)->update($data); + + if( $status ){ + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been updated successfully.'); + + }else{ + + return $this->helpers->apiArrayResponseBuilder(400, 'bad request', 'Error, data failed to update.'); + + } + } + + public function delete($id){ + + $this->{{modelname}}->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + public function destroy($id){ + + $this->{{modelname}}->where('id',$id)->delete(); + + return $this->helpers->apiArrayResponseBuilder(200, 'success', 'Data has been deleted successfully.'); + } + + + public static function getAfterFilters() {return [];} + public static function getBeforeFilters() {return [];} + public static function getMiddleware() {return [];} + public function callAction($method, $parameters=false) { + return call_user_func_array(array($this, $method), $parameters); + } + +} diff --git a/plugins/ahmadfatoni/apigenerator/template/route.dot b/plugins/ahmadfatoni/apigenerator/template/route.dot new file mode 100644 index 0000000..515cb53 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/template/route.dot @@ -0,0 +1,3 @@ + +Route::resource('{{modelname}}', 'AhmadFatoni\ApiGenerator\Controllers\API\{{controllername}}Controller', ['except' => ['destroy', 'create', 'edit']]); +Route::get('{{modelname}}/{id}/delete', ['as' => '{{modelname}}.delete', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\API\{{controllername}}Controller@destroy']); \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/template/routes.dot b/plugins/ahmadfatoni/apigenerator/template/routes.dot new file mode 100644 index 0000000..ddafe59 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/template/routes.dot @@ -0,0 +1,6 @@ + 'fatoni.generate.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@generateApi')); +Route::post('fatoni/update/api/{id}', array('as' => 'fatoni.update.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@updateApi')); +Route::get('fatoni/delete/api/{id}', array('as' => 'fatoni.delete.api', 'uses' => 'AhmadFatoni\ApiGenerator\Controllers\ApiGeneratorController@deleteApi')); +{{route}} \ No newline at end of file diff --git a/plugins/ahmadfatoni/apigenerator/updates/builder_table_create_ahmadfatoni_apigenerator_data.php b/plugins/ahmadfatoni/apigenerator/updates/builder_table_create_ahmadfatoni_apigenerator_data.php new file mode 100644 index 0000000..44cdc81 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/updates/builder_table_create_ahmadfatoni_apigenerator_data.php @@ -0,0 +1,26 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('name'); + $table->string('endpoint'); + $table->string('model'); + $table->string('description')->nullable(); + $table->text('custom_format')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('ahmadfatoni_apigenerator_data'); + } +} diff --git a/plugins/ahmadfatoni/apigenerator/updates/version.yaml b/plugins/ahmadfatoni/apigenerator/updates/version.yaml new file mode 100644 index 0000000..966ff30 --- /dev/null +++ b/plugins/ahmadfatoni/apigenerator/updates/version.yaml @@ -0,0 +1,15 @@ +1.0.1: + - 'Initialize plugin.' +1.0.2: + - 'Database implementation' +1.0.3: + - 'add builder plugin on requirements dependency' + - builder_table_create_ahmadfatoni_apigenerator_data.php +1.0.4: + - 'fixing bug on PHP 7' +1.0.5: + - 'fixing bug on request delete data' +1.0.6: + - 'fixing bug on generate endpoint' +1.0.7: + - 'fixing bug on October CMS v1.0.456' \ No newline at end of file diff --git a/plugins/offline/cors/.gitignore b/plugins/offline/cors/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/plugins/offline/cors/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/plugins/offline/cors/CONTRIBUTING.md b/plugins/offline/cors/CONTRIBUTING.md new file mode 100644 index 0000000..f74dc2f --- /dev/null +++ b/plugins/offline/cors/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# How to contribute + +Contributions to this project are highly welcome. + +1. Submit your pull requests to the `develop` branch +1. Adhere to the [PSR-2 coding](http://www.php-fig.org/psr/psr-2/) standard +1. If you are not sure if your ideas are fit for this project, create an issue and ask \ No newline at end of file diff --git a/plugins/offline/cors/LICENSE b/plugins/offline/cors/LICENSE new file mode 100644 index 0000000..6aece30 --- /dev/null +++ b/plugins/offline/cors/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 OFFLINE GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/offline/cors/Plugin.php b/plugins/offline/cors/Plugin.php new file mode 100644 index 0000000..0dcb0e2 --- /dev/null +++ b/plugins/offline/cors/Plugin.php @@ -0,0 +1,49 @@ +app['Illuminate\Contracts\Http\Kernel'] + ->prependMiddleware(HandleCors::class); + + if (request()->isMethod('OPTIONS')) { + $this->app['Illuminate\Contracts\Http\Kernel'] + ->prependMiddleware(HandlePreflight::class); + } + } + + public function registerPermissions() + { + return [ + 'offline.cors.manage' => [ + 'label' => 'Can manage cors settings', + 'tab' => 'CORS', + ], + ]; + } + + public function registerSettings() + { + return [ + 'cors' => [ + 'label' => 'CORS-Settings', + 'description' => 'Manage CORS headers', + 'category' => 'system::lang.system.categories.cms', + 'icon' => 'icon-code', + 'class' => Settings::class, + 'order' => 500, + 'keywords' => 'cors', + 'permissions' => ['offline.cors.manage'], + ], + ]; + } +} diff --git a/plugins/offline/cors/README.md b/plugins/offline/cors/README.md new file mode 100644 index 0000000..ecbb035 --- /dev/null +++ b/plugins/offline/cors/README.md @@ -0,0 +1,45 @@ +# CORS plugin for October CMS + +This plugin is based on [https://github.com/barryvdh/laravel-cors](https://github.com/barryvdh/laravel-cors/blob/master/config/cors.php). + +All configuration for the plugin can be done via the backend settings. + +The following cors headers are supported: + +* Access-Control-Allow-Origin +* Access-Control-Allow-Headers +* Access-Control-Allow-Methods +* Access-Control-Allow-Credentials +* Access-Control-Expose-Headers +* Access-Control-Max-Age + +Currently these headers are sent for every request. There is no per-route configuration possible at this time. + +## Setup + +After installing the plugin visit the CORS settings page in your October CMS backend settings. + +You can add `*` as an entry to `Allowed origins`, `Allowed headers` and `Allowed methods` to allow any kind of CORS request from everywhere. + +It is advised to be more explicit about these settings. You can add values for each header via the repeater fields. + +> It is important to set these intial settings once for the plugin to work as excpected! + +### Filesystem configuration + +As an alternative to the backend settings you can create a `config/config.php` file in the plugins root directory to configure it. + +The filesystem configuration will overwrite any defined backend setting. + +```php + true, + 'maxAge' => 3600, + 'allowedOrigins' => ['*'], + 'allowedHeaders' => ['*'], + 'allowedMethods' => ['GET', 'POST'], + 'exposedHeaders' => [''], +]; +``` \ No newline at end of file diff --git a/plugins/offline/cors/classes/Cors.php b/plugins/offline/cors/classes/Cors.php new file mode 100644 index 0000000..2ceeade --- /dev/null +++ b/plugins/offline/cors/classes/Cors.php @@ -0,0 +1,71 @@ + [], + 'allowedMethods' => [], + 'allowedOrigins' => [], + 'exposedHeaders' => false, + 'maxAge' => false, + 'supportsCredentials' => false, + ]; + + /** + * Cors constructor. + * + * @param HttpKernelInterface $app + * @param array $options + */ + public function __construct(HttpKernelInterface $app, array $options = []) + { + $this->app = $app; + $this->cors = new CorsService(array_merge($this->defaultOptions, $options)); + + } + + /** + * @param Request $request + * @param int $type + * @param bool $catch + * + * @return bool|Response + */ + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + if ( ! $this->cors->isCorsRequest($request)) { + return $this->app->handle($request, $type, $catch); + } + + if ($this->cors->isPreflightRequest($request)) { + return $this->cors->handlePreflightRequest($request); + } + + if ( ! $this->cors->isActualRequestAllowed($request)) { + return new Response('Not allowed.', 403); + } + + $response = $this->app->handle($request, $type, $catch); + + return $this->cors->addActualRequestHeaders($response, $request); + } +} \ No newline at end of file diff --git a/plugins/offline/cors/classes/CorsService.php b/plugins/offline/cors/classes/CorsService.php new file mode 100644 index 0000000..41758e7 --- /dev/null +++ b/plugins/offline/cors/classes/CorsService.php @@ -0,0 +1,199 @@ +options = $this->normalizeOptions($options); + } + + private function normalizeOptions(array $options = []) + { + $options += [ + 'supportsCredentials' => false, + 'maxAge' => 0, + ]; + + // Make sure these values are arrays, if not specified in the backend settings. + $arrayKeys = [ + 'allowedOrigins', + 'allowedHeaders', + 'exposedHeaders', + 'allowedMethods', + ]; + + foreach ($arrayKeys as $key) { + if (!$options[$key]) { + $options[$key] = []; + } + } + + // normalize array('*') to true + if (in_array('*', $options['allowedOrigins'])) { + $options['allowedOrigins'] = true; + } + if (in_array('*', $options['allowedHeaders'])) { + $options['allowedHeaders'] = true; + } else { + $options['allowedHeaders'] = array_map('strtolower', $options['allowedHeaders']); + } + + if (in_array('*', $options['allowedMethods'])) { + $options['allowedMethods'] = true; + } else { + $options['allowedMethods'] = array_map('strtoupper', $options['allowedMethods']); + } + + return $options; + } + + public function isActualRequestAllowed(Request $request) + { + return $this->checkOrigin($request); + } + + public function isCorsRequest(Request $request) + { + return $request->headers->has('Origin') && $request->headers->get('Origin') !== $request->getSchemeAndHttpHost(); + } + + public function isPreflightRequest(Request $request) + { + return $this->isCorsRequest($request) + && $request->getMethod() === 'OPTIONS' + && $request->headers->has('Access-Control-Request-Method'); + } + + public function addActualRequestHeaders(Response $response, Request $request) + { + if ( ! $this->checkOrigin($request)) { + return $response; + } + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + if ( ! $response->headers->has('Vary')) { + $response->headers->set('Vary', 'Origin'); + } else { + $response->headers->set('Vary', $response->headers->get('Vary') . ', Origin'); + } + + if ($this->options['supportsCredentials']) { + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + } + + if ($this->options['exposedHeaders']) { + $response->headers->set('Access-Control-Expose-Headers', implode(', ', $this->options['exposedHeaders'])); + } + + return $response; + } + + public function handlePreflightRequest(Request $request) + { + if (true !== $check = $this->checkPreflightRequestConditions($request)) { + return $check; + } + + return $this->buildPreflightCheckResponse($request); + } + + private function buildPreflightCheckResponse(Request $request) + { + $response = new Response(); + + if ($this->options['supportsCredentials']) { + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + } + + $response->headers->set('Access-Control-Allow-Origin', $request->headers->get('Origin')); + + if ($this->options['maxAge']) { + $response->headers->set('Access-Control-Max-Age', $this->options['maxAge']); + } + + $allowMethods = $this->options['allowedMethods'] === true + ? strtoupper($request->headers->get('Access-Control-Request-Method')) + : implode(', ', $this->options['allowedMethods']); + $response->headers->set('Access-Control-Allow-Methods', $allowMethods); + + $allowHeaders = $this->options['allowedHeaders'] === true + ? strtoupper($request->headers->get('Access-Control-Request-Headers')) + : implode(', ', $this->options['allowedHeaders']); + $response->headers->set('Access-Control-Allow-Headers', $allowHeaders); + + return $response; + } + + private function checkPreflightRequestConditions(Request $request) + { + if ( ! $this->checkOrigin($request)) { + return $this->createBadRequestResponse(403, 'Origin not allowed'); + } + + if ( ! $this->checkMethod($request)) { + return $this->createBadRequestResponse(405, 'Method not allowed'); + } + + $requestHeaders = []; + // if allowedHeaders has been set to true ('*' allow all flag) just skip this check + if ($this->options['allowedHeaders'] !== true && $request->headers->has('Access-Control-Request-Headers')) { + $headers = strtolower($request->headers->get('Access-Control-Request-Headers')); + $requestHeaders = explode(',', $headers); + + foreach ($requestHeaders as $header) { + if ( ! in_array(trim($header), $this->options['allowedHeaders'])) { + return $this->createBadRequestResponse(403, 'Header not allowed'); + } + } + } + + return true; + } + + private function createBadRequestResponse($code, $reason = '') + { + return new Response($reason, $code); + } + + private function checkOrigin(Request $request) + { + if ($this->options['allowedOrigins'] === true) { + // allow all '*' flag + return true; + } + $origin = $request->headers->get('Origin'); + + foreach ($this->options['allowedOrigins'] as $allowedOrigin) { + if (OriginMatcher::matches($allowedOrigin, $origin)) { + return true; + } + } + + return false; + } + + private function checkMethod(Request $request) + { + if ($this->options['allowedMethods'] === true) { + // allow all '*' flag + return true; + } + + $requestMethod = strtoupper($request->headers->get('Access-Control-Request-Method')); + + return in_array($requestMethod, $this->options['allowedMethods']); + } + +} \ No newline at end of file diff --git a/plugins/offline/cors/classes/HandleCors.php b/plugins/offline/cors/classes/HandleCors.php new file mode 100644 index 0000000..9f2a88d --- /dev/null +++ b/plugins/offline/cors/classes/HandleCors.php @@ -0,0 +1,49 @@ +cors = $cors; + } + + /** + * Handle an incoming request. Based on Asm89\Stack\Cors by asm89 + * @see https://github.com/asm89/stack-cors/blob/master/src/Asm89/Stack/Cors.php + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + if ( ! $this->cors->isCorsRequest($request)) { + return $next($request); + } + + if ( ! $this->cors->isActualRequestAllowed($request)) { + abort(403); + } + + /** @var \Illuminate\Http\Response $response */ + $response = $next($request); + + return $this->cors->addActualRequestHeaders($response, $request); + } + +} \ No newline at end of file diff --git a/plugins/offline/cors/classes/HandlePreflight.php b/plugins/offline/cors/classes/HandlePreflight.php new file mode 100644 index 0000000..392b5a3 --- /dev/null +++ b/plugins/offline/cors/classes/HandlePreflight.php @@ -0,0 +1,74 @@ +cors = $cors; + $this->router = $router; + $this->kernel = $kernel; + } + + /** + * Handle an incoming OPTIONS request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + $response = $next($request); + if ($this->cors->isPreflightRequest($request) && $this->hasMatchingCorsRoute($request)) { + $preflight = $this->cors->handlePreflightRequest($request); + $response->headers->add($preflight->headers->all()); + } + $response->setStatusCode(204); + return $response; + } + + /** + * Verify the current OPTIONS request matches a CORS-enabled route + * + * @param \Illuminate\Http\Request $request + * + * @return boolean + */ + private function hasMatchingCorsRoute($request) + { + // Check if CORS is added in a global middleware + if ($this->kernel->hasMiddleware(HandleCors::class)) { + return true; + } + + // Check if CORS is added as a route middleware + $request = clone $request; + $request->setMethod($request->header('Access-Control-Request-Method')); + + try { + $route = $this->router->getRoutes()->match($request); + // change of method name in laravel 5.3 + if (method_exists($this->router, 'gatherRouteMiddleware')) { + $middleware = $this->router->gatherRouteMiddleware($route); + } else { + $middleware = $this->router->gatherRouteMiddlewares($route); + } + + return in_array(HandleCors::class, $middleware); + } catch (\Exception $e) { + app('log')->error($e); + + return false; + } + } +} diff --git a/plugins/offline/cors/classes/OriginMatcher.php b/plugins/offline/cors/classes/OriginMatcher.php new file mode 100644 index 0000000..bf529d2 --- /dev/null +++ b/plugins/offline/cors/classes/OriginMatcher.php @@ -0,0 +1,100 @@ + $patternComponent) { + if ($patternComponent === '*') { + return true; + } + if ( ! isset($hostComponents[$index])) { + return false; + } + if ($hostComponents[$index] !== $patternComponent) { + return false; + } + } + + return count($patternComponents) === count($hostComponents); + } + + public static function portMatches($pattern, $port) + { + if ($pattern === "*") { + return true; + } + if ((string)$pattern === "") { + return (string)$port === ""; + } + if (preg_match('/\A\d+\z/', $pattern)) { + return (string)$pattern === (string)$port; + } + if (preg_match('/\A(?P\d+)-(?P\d+)\z/', $pattern, $captured)) { + return $captured['from'] <= $port && $port <= $captured['to']; + } + throw new \InvalidArgumentException("Invalid port pattern: ${pattern}"); + } + + public static function parseOriginPattern($originPattern, $component = -1) + { + $matched = preg_match( + '!\A + (?: (?P ([a-z][a-z0-9+\-.]*) ):// )? + (?P (?:\*|[\w-]+)(?:\.[\w-]+)* ) + (?: :(?P (?: \*|\d+(?:-\d+)? ) ) )? + \z!x', + $originPattern, + $captured + ); + if ( ! $matched) { + throw new \InvalidArgumentException("Invalid origin pattern ${originPattern}"); + } + $components = [ + 'scheme' => $captured['scheme'] ?: null, + 'host' => $captured['host'], + 'port' => array_key_exists('port', $captured) ? $captured['port'] : null, + ]; + switch ($component) { + case -1: + return $components; + case PHP_URL_SCHEME: + return $components['scheme']; + case PHP_URL_HOST: + return $components['host']; + case PHP_URL_PORT: + return $components['port']; + } + throw new \InvalidArgumentException("Invalid component: ${component}"); + } +} \ No newline at end of file diff --git a/plugins/offline/cors/classes/ServiceProvider.php b/plugins/offline/cors/classes/ServiceProvider.php new file mode 100644 index 0000000..7138f9c --- /dev/null +++ b/plugins/offline/cors/classes/ServiceProvider.php @@ -0,0 +1,90 @@ +app->singleton(CorsService::class, function ($app) { + return new CorsService($this->getSettings()); + }); + } + + /** + * Return default Settings + */ + protected function getSettings() + { + $supportsCredentials = (bool)$this->getConfigValue('supportsCredentials', false); + $maxAge = (int)$this->getConfigValue('maxAge', 0); + $allowedOrigins = $this->getConfigValue('allowedOrigins', []); + $allowedHeaders = $this->getConfigValue('allowedHeaders', []); + $allowedMethods = $this->getConfigValue('allowedMethods', []); + $exposedHeaders = $this->getConfigValue('exposedHeaders', []); + + return compact( + 'supportsCredentials', + 'allowedOrigins', + 'allowedHeaders', + 'allowedMethods', + 'exposedHeaders', + 'maxAge' + ); + } + + /** + * Returns an effective config value. + * + * If a filesystem config is available it takes precedence + * over the backend settings values. + * + * @param $key + * @param null $default + * + * @return mixed + */ + public function getConfigValue($key, $default = null) + { + return $this->filesystemConfig($key) ?: $this->getValues(Settings::get($key, $default)); + } + + /** + * Return the filesystem config value if available. + * + * @param string $key + * + * @return mixed + */ + public function filesystemConfig($key) + { + return config('offline.cors::' . $key); + } + + /** + * Extract the repeater field values. + * + * @param mixed $values + * + * @return array + */ + protected function getValues($values) + { + return \is_array($values) ? collect($values)->pluck('value')->toArray() : $values; + } +} \ No newline at end of file diff --git a/plugins/offline/cors/composer.json b/plugins/offline/cors/composer.json new file mode 100644 index 0000000..4ce370d --- /dev/null +++ b/plugins/offline/cors/composer.json @@ -0,0 +1,13 @@ +{ + "name": "offline/oc-cors-plugin", + "description": "Setup and manage Cross-Origin Resource Sharing headers in October CMS", + "type": "october-plugin", + "license": "MIT", + "authors": [ + { + "name": "Tobias Kündig", + "email": "tobias@offline.swiss" + } + ], + "require": {} +} diff --git a/plugins/offline/cors/lang/de/lang.php b/plugins/offline/cors/lang/de/lang.php new file mode 100644 index 0000000..0df3bb1 --- /dev/null +++ b/plugins/offline/cors/lang/de/lang.php @@ -0,0 +1,6 @@ + [ + 'name' => 'CORS', + 'description' => 'Verwalte Cross-Origin Resource Sharing Header in October CMS', + ], +]; \ No newline at end of file diff --git a/plugins/offline/cors/lang/en/lang.php b/plugins/offline/cors/lang/en/lang.php new file mode 100644 index 0000000..beaa2e5 --- /dev/null +++ b/plugins/offline/cors/lang/en/lang.php @@ -0,0 +1,6 @@ + [ + 'name' => 'CORS', + 'description' => 'Setup and manage Cross-Origin Resource Sharing headers', + ], +]; \ No newline at end of file diff --git a/plugins/offline/cors/models/Settings.php b/plugins/offline/cors/models/Settings.php new file mode 100644 index 0000000..381d13f --- /dev/null +++ b/plugins/offline/cors/models/Settings.php @@ -0,0 +1,12 @@ + 'rainlab.blog::lang.plugin.name', + 'description' => 'rainlab.blog::lang.plugin.description', + 'author' => 'Alexey Bobkov, Samuel Georges', + 'icon' => 'icon-pencil', + 'homepage' => 'https://github.com/rainlab/blog-plugin' + ]; + } + + public function registerComponents() + { + return [ + 'RainLab\Blog\Components\Post' => 'blogPost', + 'RainLab\Blog\Components\Posts' => 'blogPosts', + 'RainLab\Blog\Components\Categories' => 'blogCategories', + 'RainLab\Blog\Components\RssFeed' => 'blogRssFeed' + ]; + } + + public function registerPermissions() + { + return [ + 'rainlab.blog.manage_settings' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.manage_settings' + ], + 'rainlab.blog.access_posts' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_posts' + ], + 'rainlab.blog.access_categories' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_categories' + ], + 'rainlab.blog.access_other_posts' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_other_posts' + ], + 'rainlab.blog.access_import_export' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_import_export' + ], + 'rainlab.blog.access_publish' => [ + 'tab' => 'rainlab.blog::lang.blog.tab', + 'label' => 'rainlab.blog::lang.blog.access_publish' + ] + ]; + } + + public function registerNavigation() + { + return [ + 'blog' => [ + 'label' => 'rainlab.blog::lang.blog.menu_label', + 'url' => Backend::url('rainlab/blog/posts'), + 'icon' => 'icon-pencil', + 'iconSvg' => 'plugins/rainlab/blog/assets/images/blog-icon.svg', + 'permissions' => ['rainlab.blog.*'], + 'order' => 300, + + 'sideMenu' => [ + 'new_post' => [ + 'label' => 'rainlab.blog::lang.posts.new_post', + 'icon' => 'icon-plus', + 'url' => Backend::url('rainlab/blog/posts/create'), + 'permissions' => ['rainlab.blog.access_posts'] + ], + 'posts' => [ + 'label' => 'rainlab.blog::lang.blog.posts', + 'icon' => 'icon-copy', + 'url' => Backend::url('rainlab/blog/posts'), + 'permissions' => ['rainlab.blog.access_posts'] + ], + 'categories' => [ + 'label' => 'rainlab.blog::lang.blog.categories', + 'icon' => 'icon-list-ul', + 'url' => Backend::url('rainlab/blog/categories'), + 'permissions' => ['rainlab.blog.access_categories'] + ] + ] + ] + ]; + } + + public function registerSettings() + { + return [ + 'blog' => [ + 'label' => 'rainlab.blog::lang.blog.menu_label', + 'description' => 'rainlab.blog::lang.blog.settings_description', + 'category' => 'rainlab.blog::lang.blog.menu_label', + 'icon' => 'icon-pencil', + 'class' => 'RainLab\Blog\Models\Settings', + 'order' => 500, + 'keywords' => 'blog post category', + 'permissions' => ['rainlab.blog.manage_settings'] + ] + ]; + } + + /** + * Register method, called when the plugin is first registered. + */ + public function register() + { + /* + * Register the image tag processing callback + */ + TagProcessor::instance()->registerCallback(function($input, $preview) { + if (!$preview) { + return $input; + } + + return preg_replace('|\([0-9]+)]*)\/>|m', + ' + + Click or drop an image... + + + ', + $input); + }); + } + + public function boot() + { + /* + * Register menu items for the RainLab.Pages plugin + */ + Event::listen('pages.menuitem.listTypes', function() { + return [ + 'blog-category' => 'rainlab.blog::lang.menuitem.blog_category', + 'all-blog-categories' => 'rainlab.blog::lang.menuitem.all_blog_categories', + 'blog-post' => 'rainlab.blog::lang.menuitem.blog_post', + 'all-blog-posts' => 'rainlab.blog::lang.menuitem.all_blog_posts', + 'category-blog-posts' => 'rainlab.blog::lang.menuitem.category_blog_posts', + ]; + }); + + Event::listen('pages.menuitem.getTypeInfo', function($type) { + if ($type == 'blog-category' || $type == 'all-blog-categories') { + return Category::getMenuTypeInfo($type); + } + elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') { + return Post::getMenuTypeInfo($type); + } + }); + + Event::listen('pages.menuitem.resolveItem', function($type, $item, $url, $theme) { + if ($type == 'blog-category' || $type == 'all-blog-categories') { + return Category::resolveMenuItem($item, $url, $theme); + } + elseif ($type == 'blog-post' || $type == 'all-blog-posts' || $type == 'category-blog-posts') { + return Post::resolveMenuItem($item, $url, $theme); + } + }); + } +} diff --git a/plugins/rainlab/blog/README.md b/plugins/rainlab/blog/README.md new file mode 100644 index 0000000..f96addd --- /dev/null +++ b/plugins/rainlab/blog/README.md @@ -0,0 +1,318 @@ +# Blog Plugin + +A simple, extensible blogging platform for October CMS. + +[Blog & Forum Building Tutorial Video](https://player.vimeo.com/video/97088926) + +## Editing posts + +The plugin uses the markdown markup for the posts. You can use any Markdown syntax and some special tags for embedding images and videos (requires RainLab Blog Video plugin). To embed an image use the image placeholder: + + ![1](image) + +The number in the first part is the placeholder index. If you use multiple images in a post you should use an unique index for each image: + + ![1](image) + + ![2](image) + +You can also add classes or ids to images by using the [markdown extra](http://michelf.ca/projects/php-markdown/extra/) syntax: + + ![1](image){#id .class} + +## Excerpt Vs. Read more + +Posts are managed by selecting *Blog > Posts* from the menu. Each post can contain an excerpt by entering some text in this field on the *Manage* tab. This content is displayed on the page using the `summary` attribute of the blog post. + + {{ post.summary|raw }} + +Alternatively this field can be left blank and the excerpt can be captured from the main content (*Edit* tab). Use the special tag `` to specify a summary from the main content, all content above this tag will be treated as the summary. For example: + + This is a great introduction to a great blog post. This text is included as part of the excerpt / summary. + + + + Let's dive in to more detail about why this post is so great. This text will not be included in the summary. + +Finally, if no excerpt is specified and the "more" tag is not used, the blog post will capture the first 600 characters of the content and use this for the summary. + +## Implementing front-end pages + +The plugin provides several components for building the post list page (archive), category page, post details page and category list for the sidebar. + +### Post list page + +Use the `blogPosts` component to display a list of latest blog posts on a page. The component has the following properties: + +* **pageNumber** - this value is used to determine what page the user is on, it should be a routing parameter for the default markup. The default value is **{{ :page }}** to obtain the value from the route parameter `:page`. +* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed. +* **postsPerPage** - how many posts to display on a single page (the pagination is supported automatically). The default value is 10. +* **noPostsMessage** - message to display in the empty post list. +* **sortOrder** - the column name and direction used for the sort order of the posts. The default value is **published_at desc**. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. +* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the default component partial for creating links to the blog posts. +* **exceptPost** - ignore a single post by its slug or unique ID. The ignored post will not be included in the list, useful for showing other/related posts. +* **exceptCategories** - ignore posts from a comma-separated list of categories, given by their unique slug. The ignored posts will not be included in the list. + +The blogPosts component injects the following variables to the page where it's used: + +* **posts** - a list of blog posts loaded from the database. +* **postPage** - contains the value of the `postPage` component's property. +* **category** - the blog category object loaded from the database. If the category is not found, the variable value is **null**. +* **categoryPage** - contains the value of the `categoryPage` component's property. +* **noPostsMessage** - contains the value of the `noPostsMessage` component's property. + +The component supports pagination and reads the current page index from the `:page` URL parameter. The next example shows the basic component usage on the blog home page: + + title = "Blog" + url = "/blog/:page?" + + [blogPosts] + postsPerPage = "5" + == + {% component 'blogPosts' %} + +The next example shows the basic component usage with the category filter: + + title = "Blog Category" + url = "/blog/category/:slug/:page?" + + [blogPosts] + categoryFilter = "{{ :slug }}" + == + function onEnd() + { + // Optional - set the page title to the category name + if ($this->category) + $this->page->title = $this->category->name; + } + == + {% if not category %} +

Category not found

+ {% else %} +

{{ category.name }}

+ + {% component 'blogPosts' %} + {% endif %} + +The post list and the pagination are coded in the default component partial `plugins/rainlab/blog/components/posts/default.htm`. If the default markup is not suitable for your website, feel free to copy it from the default partial and replace the `{% component %}` call in the example above with the partial contents. + +### Post page + +Use the `blogPost` component to display a blog post on a page. The component has the following properties: + +* **slug** - the value used for looking up the post by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. + +The component injects the following variables to the page where it's used: + +* **post** - the blog post object loaded from the database. If the post is not found, the variable value is **null**. + +The next example shows the basic component usage on the blog page: + + title = "Blog Post" + url = "/blog/post/:slug" + + [blogPost] + == + post) + $this->page->title = $this->post->title; + } + ?> + == + {% if post %} +

{{ post.title }}

+ + {% component 'blogPost' %} + {% else %} +

Post not found

+ {% endif %} + +The post details is coded in the default component partial `plugins/rainlab/blog/components/post/default.htm`. + +### Category list + +Use the `blogCategories` component to display a list of blog post categories with links. The component has the following properties: + +* **slug** - the value used for looking up the current category by its slug. The default value is **{{ :slug }}** to obtain the value from the route parameter `:slug`. +* **displayEmpty** - determines if empty categories should be displayed. The default value is false. +* **categoryPage** - path to the category page. The default value is **blog/category** - it matches the pages/blog/category.htm file in the theme directory. This property is used in the default component partial for creating links to the blog categories. + +The component injects the following variables to the page where it's used: + +* **categoryPage** - contains the value of the `categoryPage` component's property. +* **categories** - a list of blog categories loaded from the database. +* **currentCategorySlug** - slug of the current category. This property is used for marking the current category in the category list. + +The component can be used on any page. The next example shows the basic component usage on the blog home page: + + title = "Blog" + url = "/blog/:page?" + + [blogCategories] + == + ... + + ... + +The category list is coded in the default component partial `plugins/rainlab/blog/components/categories/default.htm`. + +### RSS feed + +Use the `blogRssFeed` component to display an RSS feed containing the latest blog posts. The following properties are supported: + +* **categoryFilter** - a category slug to filter the posts by. If left blank, all posts are displayed. +* **postsPerPage** - how many posts to display on the feed. The default value is 10. +* **blogPage** - path to the main blog page. The default value is **blog** - it matches the pages/blog.htm file in the theme directory. This property is used in the RSS feed for creating links to the main blog page. +* **postPage** - path to the post details page. The default value is **blog/post** - it matches the pages/blog/post.htm file in the theme directory. This property is used in the RSS feed for creating links to the blog posts. + +The component can be used on any page, it will hijack the entire page cycle to display the feed in RSS format. The next example shows how to use it: + + title = "RSS Feed" + url = "/blog/rss.xml" + + [blogRssFeed] + blogPage = "blog" + postPage = "blog/post" + == + + +## Configuration + +To overwrite the default configuration create a `config/rainlab/blog/config.php`. You can return only values you want to override. + +### Summary + +A summary attribute is generated for each post. + +If you enter an excerpt manually, it gets used as summary. Alternatively, you can use the `summary_separator` (default is ``) to mark the end of the summary. If a post contains no separator, the text gets truncated after the number of characters specified in `summary_default_length` (default is 600 characters). + +## Markdown guide + +October supports [standard markdown syntax](http://daringfireball.net/projects/markdown/) as well as [extended markdown syntax](http://michelf.ca/projects/php-markdown/extra/) + +### Classes and IDs + +Classes and IDs can be added to images and other elements as shown below: + +``` +[link](url){#id .class} +![1](image){#id .class} +# October {#id .class} +``` + +### Fenced code blogs + +Markdown extra makes it possible to use fenced code blocks. With fenced code blocks you do not need indentation on the areas you want to mark as code: + + + ``` + Code goes here + ``` + +You can also use the `~` symbol: + + ~~~ + Code goes here + ~~~ + +### Tables + +A *simple* table can be defined as follows: + +``` +First Header | Second Header +------------- | ------------- +Content Cell | Content Cell +Content Cell | Content Cell +``` + +If you want to you can also add a leading and tailing pipe: + +``` +| First Header | Second Header | +| ------------- | ------------- | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +To add alignment to the cells you simply need to add a `:` either at the start or end of a separator: + +``` +| First Header | Second Header | +| :------------ | ------------: | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +To center align cell just add `:` on both sides: + +``` +| First Header | Second Header | +| ------------- | :-----------: | +| Content Cell | Content Cell | +| Content Cell | Content Cell | +``` + +### Definition lists + +Below is an example of a simple definition list: + +``` +Laravel +: A popular PHP framework + +October +: Awesome CMS built on Laravel +``` + +A term can also have multiple definitions: + +``` +Laravel +: A popular PHP framework + +October +: Awesome CMS built on Laravel +: Supports markdown extra +``` + +You can also associate more than 1 term to a definition: + +``` +Laravel +October +: Built using PHP +``` + +### Footnotes + +With markdown extra it is possible to create reference style footnotes: + +``` +This is some text with a footnote.[^1] + +[^1]: And this is the footnote. +``` + +### Abbreviations + +With markdown extra you can add abbreviations to your markup. The use this functionality first create a definition list: + +``` +*[HTML]: Hyper Text Markup Language +*[PHP]: Hypertext Preprocessor +``` + +Now markdown extra will convert all occurrences of `HTML` and `PHP` as follows: + +``` +HTML +PHP +``` diff --git a/plugins/rainlab/blog/assets/css/rainlab.blog-export.css b/plugins/rainlab/blog/assets/css/rainlab.blog-export.css new file mode 100644 index 0000000..c0a42a7 --- /dev/null +++ b/plugins/rainlab/blog/assets/css/rainlab.blog-export.css @@ -0,0 +1,3 @@ +.export-behavior .export-columns { + max-height: 450px !important; +} diff --git a/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css b/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css new file mode 100644 index 0000000..eb21587 --- /dev/null +++ b/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css @@ -0,0 +1,85 @@ +.blog-post-preview .editor-preview .preview-content { + padding: 20px; +} +.blog-post-preview .editor-preview span.image-placeholder { + display: block; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone { + background: #ecf0f1; + display: block; + border: 1px solid #e5e9ec; + padding: 25px; + min-height: 123px; + position: relative; + text-align: center; + cursor: pointer; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone span.label { + color: #b1b9be; + font-size: 16px; + display: inline-block; + margin-top: 25px; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:before { + display: inline-block; + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + text-decoration: inherit; + -webkit-font-smoothing: antialiased; + *margin-right: .3em; + content: "\f03e"; + position: absolute; + left: 25px; + top: 25px; + line-height: 100%; + font-size: 73px; + color: #d1d3d4; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover { + background: #2f99da; +} +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover:before, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover:before, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone.hover span.label, +.blog-post-preview .editor-preview span.image-placeholder .upload-dropzone:hover span.label { + color: white; +} +.blog-post-preview .editor-preview span.image-placeholder input[type=file] { + position: absolute; + left: -10000em; +} +.blog-post-preview-container .loading-indicator { + position: absolute; + display: none; + width: 20px; + height: 20px; + padding: 0!important; + background: transparent; + right: 10px; + left: auto; + top: 10px; +} +.blog-post-preview-container.loading-indicator-visible .loading-indicator { + display: block; +} +html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone:before { + display: none; +} +html.cssanimations .blog-post-preview span.image-placeholder.loading .upload-dropzone .indicator { + display: block; + width: 50px; + height: 50px; + position: absolute; + left: 35px; + top: 35px; + background-image: url('../../../../../modules/system/assets/ui/images/loader-transparent.svg'); + background-size: 50px 50px; + background-position: 50% 50%; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; +} diff --git a/plugins/rainlab/blog/assets/images/blog-icon.svg b/plugins/rainlab/blog/assets/images/blog-icon.svg new file mode 100644 index 0000000..ae6a619 --- /dev/null +++ b/plugins/rainlab/blog/assets/images/blog-icon.svg @@ -0,0 +1,30 @@ + + + + blog-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/blog/assets/js/post-form.js b/plugins/rainlab/blog/assets/js/post-form.js new file mode 100644 index 0000000..cc5b866 --- /dev/null +++ b/plugins/rainlab/blog/assets/js/post-form.js @@ -0,0 +1,185 @@ ++function ($) { "use strict"; + var PostForm = function () { + this.$form = $('#post-form') + this.$markdownEditor = $('[data-field-name=content] [data-control=markdowneditor]:first', this.$form) + this.$preview = $('.editor-preview', this.$markdownEditor) + + this.formAction = this.$form.attr('action') + this.sessionKey = $('input[name=_session_key]', this.$form).val() + + if (this.$markdownEditor.length > 0) { + this.codeEditor = this.$markdownEditor.markdownEditor('getEditorObject') + + this.$markdownEditor.on('initPreview.oc.markdowneditor', $.proxy(this.initPreview, this)) + + this.initDropzones() + this.initFormEvents() + this.addToolbarButton() + } + + this.initLayout() + } + + PostForm.prototype.addToolbarButton = function() { + this.buttonClickCount = 1 + + var self = this, + $button = this.$markdownEditor.markdownEditor('findToolbarButton', 'image') + + if (!$button.length) return + + $button.data('button-action', 'insertLine') + $button.data('button-template', '\n\n![1](image)\n') + + $button.on('click', function() { + $button.data('button-template', '\n\n!['+self.buttonClickCount+'](image)\n') + self.buttonClickCount++ + }) + } + + PostForm.prototype.initPreview = function() { + this.initImageUploaders() + } + + PostForm.prototype.updateScroll = function() { + // Reserved in case MarkdownEditor uses scrollbar plugin + // this.$preview.data('oc.scrollbar').update() + } + + PostForm.prototype.initImageUploaders = function() { + var self = this + $('span.image-placeholder .upload-dropzone', this.$preview).each(function(){ + var + $placeholder = $(this).parent(), + $link = $('span.label', $placeholder), + placeholderIndex = $placeholder.data('index') + + var uploaderOptions = { + url: self.formAction, + clickable: [$(this).get(0), $link.get(0)], + previewsContainer: $('
').get(0), + paramName: 'file', + headers: {} + } + + /* + * Add CSRF token to headers + */ + var token = $('meta[name="csrf-token"]').attr('content') + if (token) { + uploaderOptions.headers['X-CSRF-TOKEN'] = token + } + + var dropzone = new Dropzone($(this).get(0), uploaderOptions) + + dropzone.on('error', function(file, error) { + alert('Error uploading file: ' + error) + }) + dropzone.on('success', function(file, data){ + if (data.error) + alert(data.error) + else { + self.pauseUpdates() + var $img = $('') + $img.load(function(){ + self.updateScroll() + }) + + $placeholder.replaceWith($img) + + self.codeEditor.replace('!['+data.file+']('+data.path+')', { + needle: '!['+placeholderIndex+'](image)' + }) + self.resumeUpdates() + } + }) + dropzone.on('complete', function(){ + $placeholder.removeClass('loading') + }) + dropzone.on('sending', function(file, xhr, formData) { + formData.append('X_BLOG_IMAGE_UPLOAD', 1) + formData.append('_session_key', self.sessionKey) + $placeholder.addClass('loading') + }) + }) + } + + PostForm.prototype.pauseUpdates = function() { + this.$markdownEditor.markdownEditor('pauseUpdates') + } + + PostForm.prototype.resumeUpdates = function() { + this.$markdownEditor.markdownEditor('resumeUpdates') + } + + PostForm.prototype.initDropzones = function() { + $(document).bind('dragover', function (e) { + var dropZone = $('span.image-placeholder .upload-dropzone'), + foundDropzone, + timeout = window.dropZoneTimeout + + if (!timeout) + dropZone.addClass('in'); + else + clearTimeout(timeout); + + var found = false, + node = e.target + + do { + if ($(node).hasClass('dropzone')) { + found = true + foundDropzone = $(node) + break + } + + node = node.parentNode; + + } while (node != null); + + dropZone.removeClass('in hover') + + if (found) + foundDropzone.addClass('hover') + + window.dropZoneTimeout = setTimeout(function () { + window.dropZoneTimeout = null + dropZone.removeClass('in hover') + }, 100) + }) + } + + PostForm.prototype.initFormEvents = function() { + $(document).on('ajaxSuccess', '#post-form', function(event, context, data){ + if (context.handler == 'onSave' && !data.X_OCTOBER_ERROR_FIELDS) { + $(this).trigger('unchange.oc.changeMonitor') + } + }) + } + + PostForm.prototype.initLayout = function() { + $('#Form-secondaryTabs .tab-pane.layout-cell:not(:first-child)').addClass('padded-pane') + $('#Form-secondaryTabs .nav-tabs > li:not(:first-child)').addClass('tab-content-bg') + } + + PostForm.prototype.replacePlaceholder = function(placeholder, placeholderHtmlReplacement, mdCodePlaceholder, mdCodeReplacement) { + this.pauseUpdates() + placeholder.replaceWith(placeholderHtmlReplacement) + + this.codeEditor.replace(mdCodeReplacement, { + needle: mdCodePlaceholder + }) + this.updateScroll() + this.resumeUpdates() + } + + $(document).ready(function(){ + var form = new PostForm() + + if ($.oc === undefined) + $.oc = {} + + $.oc.blogPostForm = form + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less b/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less new file mode 100644 index 0000000..95334a0 --- /dev/null +++ b/plugins/rainlab/blog/assets/less/rainlab.blog-preview.less @@ -0,0 +1,99 @@ +@import "../../../../../modules/backend/assets/less/core/boot.less"; + +.blog-post-preview .editor-preview { + .preview-content { + padding: 20px; + } + + span.image-placeholder { + display: block; + + .upload-dropzone { + background: #ecf0f1; + display: block; + border: 1px solid #e5e9ec; + padding: 25px; + min-height: 123px; + position: relative; + text-align: center; + cursor: pointer; + .box-sizing(border-box); + + span.label { + color: #b1b9be; + font-size: 16px; + display: inline-block; + margin-top: 25px; + } + + &:before { + display: inline-block; + .icon(@picture-o); + position: absolute; + left: 25px; + top: 25px; + line-height: 100%; + font-size: 73px; + color: #d1d3d4; + } + + &.hover, &:hover { + background: #2f99da; + + &:before, span.label { + color: white; + } + } + } + + input[type=file] { + position: absolute; + left: -10000em; + } + } +} + +.blog-post-preview-container { + .loading-indicator { + position: absolute; + display: none; + width: 20px; + height: 20px; + padding: 0!important; + background: transparent; + right: 10px; + left: auto; + top: 10px; + } + + &.loading-indicator-visible { + .loading-indicator { + display: block; + } + } +} + +html.cssanimations { + .blog-post-preview { + span.image-placeholder.loading { + .upload-dropzone { + &:before { + display: none; + } + + .indicator { + display: block; + width: 50px; + height: 50px; + position: absolute; + left: 35px; + top: 35px; + background-image:url('../../../../../modules/system/assets/ui/images/loader-transparent.svg'); + background-size: 50px 50px; + background-position: 50% 50%; + .animation(spin 1s linear infinite); + } + } + } + } +} \ No newline at end of file diff --git a/plugins/rainlab/blog/classes/TagProcessor.php b/plugins/rainlab/blog/classes/TagProcessor.php new file mode 100644 index 0000000..d24f481 --- /dev/null +++ b/plugins/rainlab/blog/classes/TagProcessor.php @@ -0,0 +1,39 @@ +callbacks[] = $callback; + } + + public function processTags($markup, $preview) + { + foreach ($this->callbacks as $callback) { + $markup = $callback($markup, $preview); + } + + return $markup; + } +} diff --git a/plugins/rainlab/blog/components/Categories.php b/plugins/rainlab/blog/components/Categories.php new file mode 100644 index 0000000..8caed92 --- /dev/null +++ b/plugins/rainlab/blog/components/Categories.php @@ -0,0 +1,113 @@ + 'rainlab.blog::lang.settings.category_title', + 'description' => 'rainlab.blog::lang.settings.category_description' + ]; + } + + public function defineProperties() + { + return [ + 'slug' => [ + 'title' => 'rainlab.blog::lang.settings.category_slug', + 'description' => 'rainlab.blog::lang.settings.category_slug_description', + 'default' => '{{ :slug }}', + 'type' => 'string', + ], + 'displayEmpty' => [ + 'title' => 'rainlab.blog::lang.settings.category_display_empty', + 'description' => 'rainlab.blog::lang.settings.category_display_empty_description', + 'type' => 'checkbox', + 'default' => 0, + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.category_page', + 'description' => 'rainlab.blog::lang.settings.category_page_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function onRun() + { + $this->currentCategorySlug = $this->page['currentCategorySlug'] = $this->property('slug'); + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + $this->categories = $this->page['categories'] = $this->loadCategories(); + } + + /** + * Load all categories or, depending on the option, only those that have blog posts + * @return mixed + */ + protected function loadCategories() + { + $categories = BlogCategory::with('posts_count')->getNested(); + if (!$this->property('displayEmpty')) { + $iterator = function ($categories) use (&$iterator) { + return $categories->reject(function ($category) use (&$iterator) { + if ($category->getNestedPostCount() == 0) { + return true; + } + if ($category->children) { + $category->children = $iterator($category->children); + } + return false; + }); + }; + $categories = $iterator($categories); + } + + /* + * Add a "url" helper attribute for linking to each category + */ + return $this->linkCategories($categories); + } + + /** + * Sets the URL on each category according to the defined category page + * @return void + */ + protected function linkCategories($categories) + { + return $categories->each(function ($category) { + $category->setUrl($this->categoryPage, $this->controller); + + if ($category->children) { + $this->linkCategories($category->children); + } + }); + } +} diff --git a/plugins/rainlab/blog/components/Post.php b/plugins/rainlab/blog/components/Post.php new file mode 100644 index 0000000..8faa8ff --- /dev/null +++ b/plugins/rainlab/blog/components/Post.php @@ -0,0 +1,156 @@ + 'rainlab.blog::lang.settings.post_title', + 'description' => 'rainlab.blog::lang.settings.post_description' + ]; + } + + public function defineProperties() + { + return [ + 'slug' => [ + 'title' => 'rainlab.blog::lang.settings.post_slug', + 'description' => 'rainlab.blog::lang.settings.post_slug_description', + 'default' => '{{ :slug }}', + 'type' => 'string', + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.post_category', + 'description' => 'rainlab.blog::lang.settings.post_category_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function init() + { + Event::listen('translate.localePicker.translateParams', function ($page, $params, $oldLocale, $newLocale) { + $newParams = $params; + + if (isset($params['slug'])) { + $records = BlogPost::transWhere('slug', $params['slug'], $oldLocale)->first(); + if ($records) { + $records->translateContext($newLocale); + $newParams['slug'] = $records['slug']; + } + } + + return $newParams; + }); + } + + public function onRun() + { + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + $this->post = $this->page['post'] = $this->loadPost(); + if (!$this->post) { + $this->setStatusCode(404); + return $this->controller->run('404'); + } + } + + public function onRender() + { + if (empty($this->post)) { + $this->post = $this->page['post'] = $this->loadPost(); + } + } + + protected function loadPost() + { + $slug = $this->property('slug'); + + $post = new BlogPost; + $query = $post->query(); + + if ($post->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) { + $query->transWhere('slug', $slug); + } else { + $query->where('slug', $slug); + } + + if (!$this->checkEditor()) { + $query->isPublished(); + } + + $post = $query->first(); + + /* + * Add a "url" helper attribute for linking to each category + */ + if ($post && $post->exists && $post->categories->count()) { + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + } + + return $post; + } + + public function previousPost() + { + return $this->getPostSibling(-1); + } + + public function nextPost() + { + return $this->getPostSibling(1); + } + + protected function getPostSibling($direction = 1) + { + if (!$this->post) { + return; + } + + $method = $direction === -1 ? 'previousPost' : 'nextPost'; + + if (!$post = $this->post->$method()) { + return; + } + + $postPage = $this->getPage()->getBaseFileName(); + + $post->setUrl($postPage, $this->controller); + + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + + return $post; + } + + protected function checkEditor() + { + $backendUser = BackendAuth::getUser(); + + return $backendUser && $backendUser->hasAccess('rainlab.blog.access_posts'); + } +} diff --git a/plugins/rainlab/blog/components/Posts.php b/plugins/rainlab/blog/components/Posts.php new file mode 100644 index 0000000..3ac3c4a --- /dev/null +++ b/plugins/rainlab/blog/components/Posts.php @@ -0,0 +1,259 @@ + 'rainlab.blog::lang.settings.posts_title', + 'description' => 'rainlab.blog::lang.settings.posts_description' + ]; + } + + public function defineProperties() + { + return [ + 'pageNumber' => [ + 'title' => 'rainlab.blog::lang.settings.posts_pagination', + 'description' => 'rainlab.blog::lang.settings.posts_pagination_description', + 'type' => 'string', + 'default' => '{{ :page }}', + ], + 'categoryFilter' => [ + 'title' => 'rainlab.blog::lang.settings.posts_filter', + 'description' => 'rainlab.blog::lang.settings.posts_filter_description', + 'type' => 'string', + 'default' => '', + ], + 'postsPerPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_per_page', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation', + 'default' => '10', + ], + 'noPostsMessage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_no_posts', + 'description' => 'rainlab.blog::lang.settings.posts_no_posts_description', + 'type' => 'string', + 'default' => Lang::get('rainlab.blog::lang.settings.posts_no_posts_default'), + 'showExternalParam' => false, + ], + 'sortOrder' => [ + 'title' => 'rainlab.blog::lang.settings.posts_order', + 'description' => 'rainlab.blog::lang.settings.posts_order_description', + 'type' => 'dropdown', + 'default' => 'published_at desc', + ], + 'categoryPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_category', + 'description' => 'rainlab.blog::lang.settings.posts_category_description', + 'type' => 'dropdown', + 'default' => 'blog/category', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'postPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_post', + 'description' => 'rainlab.blog::lang.settings.posts_post_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'exceptPost' => [ + 'title' => 'rainlab.blog::lang.settings.posts_except_post', + 'description' => 'rainlab.blog::lang.settings.posts_except_post_description', + 'type' => 'string', + 'validationPattern' => '^[a-z0-9\-_,\s]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_except_post_validation', + 'default' => '', + 'group' => 'rainlab.blog::lang.settings.group_exceptions', + ], + 'exceptCategories' => [ + 'title' => 'rainlab.blog::lang.settings.posts_except_categories', + 'description' => 'rainlab.blog::lang.settings.posts_except_categories_description', + 'type' => 'string', + 'validationPattern' => '^[a-z0-9\-_,\s]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_except_categories_validation', + 'default' => '', + 'group' => 'rainlab.blog::lang.settings.group_exceptions', + ], + ]; + } + + public function getCategoryPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getPostPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getSortOrderOptions() + { + $options = BlogPost::$allowedSortingOptions; + + foreach ($options as $key => $value) { + $options[$key] = Lang::get($value); + } + + return $options; + } + + public function onRun() + { + $this->prepareVars(); + + $this->category = $this->page['category'] = $this->loadCategory(); + $this->posts = $this->page['posts'] = $this->listPosts(); + + /* + * If the page number is not valid, redirect + */ + if ($pageNumberParam = $this->paramName('pageNumber')) { + $currentPage = $this->property('pageNumber'); + + if ($currentPage > ($lastPage = $this->posts->lastPage()) && $currentPage > 1) { + return Redirect::to($this->currentPageUrl([$pageNumberParam => $lastPage])); + } + } + } + + protected function prepareVars() + { + $this->pageParam = $this->page['pageParam'] = $this->paramName('pageNumber'); + $this->noPostsMessage = $this->page['noPostsMessage'] = $this->property('noPostsMessage'); + + /* + * Page links + */ + $this->postPage = $this->page['postPage'] = $this->property('postPage'); + $this->categoryPage = $this->page['categoryPage'] = $this->property('categoryPage'); + } + + protected function listPosts() + { + $category = $this->category ? $this->category->id : null; + $categorySlug = $this->category ? $this->category->slug : null; + + /* + * List all the posts, eager load their categories + */ + $isPublished = !$this->checkEditor(); + + $posts = BlogPost::with(['categories', 'featured_images'])->listFrontEnd([ + 'page' => $this->property('pageNumber'), + 'sort' => $this->property('sortOrder'), + 'perPage' => $this->property('postsPerPage'), + 'search' => trim(input('search')), + 'category' => $category, + 'published' => $isPublished, + 'exceptPost' => is_array($this->property('exceptPost')) + ? $this->property('exceptPost') + : preg_split('/,\s*/', $this->property('exceptPost'), -1, PREG_SPLIT_NO_EMPTY), + 'exceptCategories' => is_array($this->property('exceptCategories')) + ? $this->property('exceptCategories') + : preg_split('/,\s*/', $this->property('exceptCategories'), -1, PREG_SPLIT_NO_EMPTY), + ]); + + /* + * Add a "url" helper attribute for linking to each post and category + */ + $posts->each(function($post) use ($categorySlug) { + $post->setUrl($this->postPage, $this->controller, ['category' => $categorySlug]); + + $post->categories->each(function($category) { + $category->setUrl($this->categoryPage, $this->controller); + }); + }); + + return $posts; + } + + protected function loadCategory() + { + if (!$slug = $this->property('categoryFilter')) { + return null; + } + + $category = new BlogCategory; + + $category = $category->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel') + ? $category->transWhere('slug', $slug) + : $category->where('slug', $slug); + + $category = $category->first(); + + return $category ?: null; + } + + protected function checkEditor() + { + $backendUser = BackendAuth::getUser(); + + return $backendUser && + $backendUser->hasAccess('rainlab.blog.access_posts') && + BlogSettings::get('show_all_posts', true); + } +} diff --git a/plugins/rainlab/blog/components/RssFeed.php b/plugins/rainlab/blog/components/RssFeed.php new file mode 100644 index 0000000..295433b --- /dev/null +++ b/plugins/rainlab/blog/components/RssFeed.php @@ -0,0 +1,159 @@ + 'rainlab.blog::lang.settings.rssfeed_title', + 'description' => 'rainlab.blog::lang.settings.rssfeed_description' + ]; + } + + public function defineProperties() + { + return [ + 'categoryFilter' => [ + 'title' => 'rainlab.blog::lang.settings.posts_filter', + 'description' => 'rainlab.blog::lang.settings.posts_filter_description', + 'type' => 'string', + 'default' => '', + ], + 'sortOrder' => [ + 'title' => 'rainlab.blog::lang.settings.posts_order', + 'description' => 'rainlab.blog::lang.settings.posts_order_description', + 'type' => 'dropdown', + 'default' => 'created_at desc', + ], + 'postsPerPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_per_page', + 'type' => 'string', + 'validationPattern' => '^[0-9]+$', + 'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation', + 'default' => '10', + ], + 'blogPage' => [ + 'title' => 'rainlab.blog::lang.settings.rssfeed_blog', + 'description' => 'rainlab.blog::lang.settings.rssfeed_blog_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + 'postPage' => [ + 'title' => 'rainlab.blog::lang.settings.posts_post', + 'description' => 'rainlab.blog::lang.settings.posts_post_description', + 'type' => 'dropdown', + 'default' => 'blog/post', + 'group' => 'rainlab.blog::lang.settings.group_links', + ], + ]; + } + + public function getBlogPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getPostPageOptions() + { + return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + } + + public function getSortOrderOptions() + { + $options = BlogPost::$allowedSortingOptions; + + foreach ($options as $key => $value) { + $options[$key] = Lang::get($value); + } + + return $options; + } + + public function onRun() + { + $this->prepareVars(); + + $xmlFeed = $this->renderPartial('@default'); + + return Response::make($xmlFeed, '200')->header('Content-Type', 'text/xml'); + } + + protected function prepareVars() + { + $this->blogPage = $this->page['blogPage'] = $this->property('blogPage'); + $this->postPage = $this->page['postPage'] = $this->property('postPage'); + $this->category = $this->page['category'] = $this->loadCategory(); + $this->posts = $this->page['posts'] = $this->listPosts(); + + $this->page['link'] = $this->pageUrl($this->blogPage); + $this->page['rssLink'] = $this->currentPageUrl(); + } + + protected function listPosts() + { + $category = $this->category ? $this->category->id : null; + + /* + * List all the posts, eager load their categories + */ + $posts = BlogPost::with('categories')->listFrontEnd([ + 'sort' => $this->property('sortOrder'), + 'perPage' => $this->property('postsPerPage'), + 'category' => $category + ]); + + /* + * Add a "url" helper attribute for linking to each post and category + */ + $posts->each(function($post) { + $post->setUrl($this->postPage, $this->controller); + }); + + return $posts; + } + + protected function loadCategory() + { + if (!$categoryId = $this->property('categoryFilter')) { + return null; + } + + if (!$category = BlogCategory::whereSlug($categoryId)->first()) { + return null; + } + + return $category; + } +} diff --git a/plugins/rainlab/blog/components/categories/default.htm b/plugins/rainlab/blog/components/categories/default.htm new file mode 100644 index 0000000..e9b0280 --- /dev/null +++ b/plugins/rainlab/blog/components/categories/default.htm @@ -0,0 +1,10 @@ +{% if __SELF__.categories|length > 0 %} +
    + {% partial __SELF__ ~ "::items" + categories = __SELF__.categories + currentCategorySlug = __SELF__.currentCategorySlug + %} +
+{% else %} +

No categories were found.

+{% endif %} diff --git a/plugins/rainlab/blog/components/categories/items.htm b/plugins/rainlab/blog/components/categories/items.htm new file mode 100644 index 0000000..cb1b8e5 --- /dev/null +++ b/plugins/rainlab/blog/components/categories/items.htm @@ -0,0 +1,18 @@ +{% for category in categories %} + {% set postCount = category.post_count %} +
  • + {{ category.name }} + {% if postCount %} + {{ postCount }} + {% endif %} + + {% if category.children|length > 0 %} +
      + {% partial __SELF__ ~ "::items" + categories=category.children + currentCategorySlug=currentCategorySlug + %} +
    + {% endif %} +
  • +{% endfor %} diff --git a/plugins/rainlab/blog/components/post/default.htm b/plugins/rainlab/blog/components/post/default.htm new file mode 100644 index 0000000..291d4f3 --- /dev/null +++ b/plugins/rainlab/blog/components/post/default.htm @@ -0,0 +1,27 @@ +{% set post = __SELF__.post %} + +
    {{ post.content_html|raw }}
    + +{% if post.featured_images|length %} + +{% endif %} + +

    + Posted + {% if post.categories|length %} in + {% for category in post.categories %} + {{ category.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% endif %} + on {{ post.published_at|date('M d, Y') }} +

    diff --git a/plugins/rainlab/blog/components/posts/default.htm b/plugins/rainlab/blog/components/posts/default.htm new file mode 100644 index 0000000..0c0e2b2 --- /dev/null +++ b/plugins/rainlab/blog/components/posts/default.htm @@ -0,0 +1,40 @@ +{% set posts = __SELF__.posts %} + +
      + {% for post in posts %} +
    • +

      {{ post.title }}

      + +

      + Posted + {% if post.categories|length %} in {% endif %} + {% for category in post.categories %} + {{ category.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + on {{ post.published_at|date('M d, Y') }} +

      + +

      {{ post.summary|raw }}

      +
    • + {% else %} +
    • {{ __SELF__.noPostsMessage }}
    • + {% endfor %} +
    + +{% if posts.lastPage > 1 %} +
      + {% if posts.currentPage > 1 %} +
    • ← Prev
    • + {% endif %} + + {% for page in 1..posts.lastPage %} +
    • + {{ page }} +
    • + {% endfor %} + + {% if posts.lastPage > posts.currentPage %} +
    • Next →
    • + {% endif %} +
    +{% endif %} diff --git a/plugins/rainlab/blog/components/rssfeed/default.htm b/plugins/rainlab/blog/components/rssfeed/default.htm new file mode 100644 index 0000000..b4527ec --- /dev/null +++ b/plugins/rainlab/blog/components/rssfeed/default.htm @@ -0,0 +1,18 @@ + + + + {{ this.page.meta_title ?: this.page.title }} + {{ link }} + {{ this.page.meta_description ?: this.page.description }} + + {% for post in posts %} + + {{ post.title }} + {{ post.url }} + {{ post.url }} + {{ post.published_at.toRfc2822String }} + {{ post.summary }} + + {% endfor %} + + diff --git a/plugins/rainlab/blog/composer.json b/plugins/rainlab/blog/composer.json new file mode 100644 index 0000000..7f49a06 --- /dev/null +++ b/plugins/rainlab/blog/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rainlab/blog-plugin", + "type": "october-plugin", + "description": "Blog plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-blog", + "keywords": ["october", "octobercms", "blog"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": ">=7.0", + "composer/installers": "~1.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/blog/config/config.php b/plugins/rainlab/blog/config/config.php new file mode 100644 index 0000000..bdf5743 --- /dev/null +++ b/plugins/rainlab/blog/config/config.php @@ -0,0 +1,18 @@ + '', + + 'summary_default_length' => 600 + +]; diff --git a/plugins/rainlab/blog/controllers/Categories.php b/plugins/rainlab/blog/controllers/Categories.php new file mode 100644 index 0000000..583cdbf --- /dev/null +++ b/plugins/rainlab/blog/controllers/Categories.php @@ -0,0 +1,47 @@ +delete(); + } + + Flash::success(Lang::get('rainlab.blog::lang.category.delete_success')); + } + + return $this->listRefresh(); + } +} diff --git a/plugins/rainlab/blog/controllers/Posts.php b/plugins/rainlab/blog/controllers/Posts.php new file mode 100644 index 0000000..311087f --- /dev/null +++ b/plugins/rainlab/blog/controllers/Posts.php @@ -0,0 +1,153 @@ +vars['postsTotal'] = Post::count(); + $this->vars['postsPublished'] = Post::isPublished()->count(); + $this->vars['postsDrafts'] = $this->vars['postsTotal'] - $this->vars['postsPublished']; + + $this->asExtension('ListController')->index(); + } + + public function create() + { + BackendMenu::setContextSideMenu('new_post'); + + $this->bodyClass = 'compact-container'; + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css'); + $this->addJs('/plugins/rainlab/blog/assets/js/post-form.js'); + + return $this->asExtension('FormController')->create(); + } + + public function update($recordId = null) + { + $this->bodyClass = 'compact-container'; + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-preview.css'); + $this->addJs('/plugins/rainlab/blog/assets/js/post-form.js'); + + return $this->asExtension('FormController')->update($recordId); + } + + public function export() + { + $this->addCss('/plugins/rainlab/blog/assets/css/rainlab.blog-export.css'); + + return $this->asExtension('ImportExportController')->export(); + } + + public function listExtendQuery($query) + { + if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) { + $query->where('user_id', $this->user->id); + } + } + + public function formExtendQuery($query) + { + if (!$this->user->hasAnyAccess(['rainlab.blog.access_other_posts'])) { + $query->where('user_id', $this->user->id); + } + } + + public function formExtendFieldsBefore($widget) + { + if (!$model = $widget->model) { + return; + } + + if ($model instanceof Post && $model->isClassExtendedWith('RainLab.Translate.Behaviors.TranslatableModel')) { + $widget->secondaryTabs['fields']['content']['type'] = 'RainLab\Blog\FormWidgets\MLBlogMarkdown'; + } + + if (BlogSettings::get('use_legacy_editor', false)) { + $widget->secondaryTabs['fields']['content']['legacyMode'] = true; + } + } + + public function index_onDelete() + { + if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { + + foreach ($checkedIds as $postId) { + if ((!$post = Post::find($postId)) || !$post->canEdit($this->user)) { + continue; + } + + $post->delete(); + } + + Flash::success(Lang::get('rainlab.blog::lang.post.delete_success')); + } + + return $this->listRefresh(); + } + + /** + * {@inheritDoc} + */ + public function listInjectRowClass($record, $definition = null) + { + if (!$record->published) { + return 'safe disabled'; + } + } + + public function formBeforeCreate($model) + { + $model->user_id = $this->user->id; + } + + public function onRefreshPreview() + { + $data = post('Post'); + + $previewHtml = Post::formatHtml($data['content'], true); + + return [ + 'preview' => $previewHtml + ]; + } +} diff --git a/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm b/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm new file mode 100644 index 0000000..fe43811 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/_list_toolbar.htm @@ -0,0 +1,25 @@ +
    + + + + + + + + + +
    diff --git a/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm b/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm new file mode 100644 index 0000000..de33eb7 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/_reorder_toolbar.htm @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/config_form.yaml b/plugins/rainlab/blog/controllers/categories/config_form.yaml new file mode 100644 index 0000000..250c41a --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_form.yaml @@ -0,0 +1,16 @@ +# =================================== +# Form Behavior Config +# =================================== + +name: rainlab.blog::lang.blog.create_category +form: $/rainlab/blog/models/category/fields.yaml +modelClass: RainLab\Blog\Models\Category +defaultRedirect: rainlab/blog/categories + +create: + redirect: rainlab/blog/categories/update/:id + redirectClose: rainlab/blog/categories + +update: + redirect: rainlab/blog/categories + redirectClose: rainlab/blog/categories diff --git a/plugins/rainlab/blog/controllers/categories/config_list.yaml b/plugins/rainlab/blog/controllers/categories/config_list.yaml new file mode 100644 index 0000000..c2b6b20 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_list.yaml @@ -0,0 +1,43 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: $/rainlab/blog/models/category/columns.yaml + +# Model Class name +modelClass: RainLab\Blog\Models\Category + +# List Title +title: rainlab.blog::lang.categories.list_title + +# Link URL for each record +recordUrl: rainlab/blog/categories/update/:id + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 5 + +# Display checkboxes next to each record +showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt + +# Legacy (v1) +showTree: true + +# Reordering +structure: + showTree: true + showReorder: true + treeExpanded: true + maxDepth: 0 diff --git a/plugins/rainlab/blog/controllers/categories/config_reorder.yaml b/plugins/rainlab/blog/controllers/categories/config_reorder.yaml new file mode 100644 index 0000000..392b0c5 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/config_reorder.yaml @@ -0,0 +1,17 @@ +# =================================== +# Reorder Behavior Config +# =================================== + +# Reorder Title +title: rainlab.blog::lang.category.reorder + +# Attribute name +nameFrom: name + +# Model Class name +modelClass: RainLab\Blog\Models\Category + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: reorder_toolbar \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/create.htm b/plugins/rainlab/blog/controllers/categories/create.htm new file mode 100644 index 0000000..ea47c33 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/create.htm @@ -0,0 +1,46 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + +
    +
    + + + + +

    fatalError)) ?>

    +

    + diff --git a/plugins/rainlab/blog/controllers/categories/index.htm b/plugins/rainlab/blog/controllers/categories/index.htm new file mode 100644 index 0000000..766877d --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/index.htm @@ -0,0 +1,2 @@ + +listRender() ?> diff --git a/plugins/rainlab/blog/controllers/categories/reorder.htm b/plugins/rainlab/blog/controllers/categories/reorder.htm new file mode 100644 index 0000000..407face --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/reorder.htm @@ -0,0 +1 @@ +reorderRender() ?> \ No newline at end of file diff --git a/plugins/rainlab/blog/controllers/categories/update.htm b/plugins/rainlab/blog/controllers/categories/update.htm new file mode 100644 index 0000000..7949c32 --- /dev/null +++ b/plugins/rainlab/blog/controllers/categories/update.htm @@ -0,0 +1,54 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + + + +
    +
    + + + +

    fatalError)) ?>

    +

    + diff --git a/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm b/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm new file mode 100644 index 0000000..d0f93f6 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/_list_toolbar.htm @@ -0,0 +1,37 @@ +
    + + + + + + user->hasAnyAccess(['rainlab.blog.access_import_export'])): ?> + + +
    diff --git a/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm b/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm new file mode 100644 index 0000000..96951e4 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/_post_toolbar.htm @@ -0,0 +1,56 @@ +formGetContext() == 'create'; + $pageUrl = isset($pageUrl) ? $pageUrl : null; +?> + diff --git a/plugins/rainlab/blog/controllers/posts/config_form.yaml b/plugins/rainlab/blog/controllers/posts/config_form.yaml new file mode 100644 index 0000000..10c090f --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_form.yaml @@ -0,0 +1,16 @@ +# =================================== +# Form Behavior Config +# =================================== + +name: rainlab.blog::lang.blog.create_post +form: $/rainlab/blog/models/post/fields.yaml +modelClass: RainLab\Blog\Models\Post +defaultRedirect: rainlab/blog/posts + +create: + redirect: rainlab/blog/posts/update/:id + redirectClose: rainlab/blog/posts + +update: + redirect: rainlab/blog/posts + redirectClose: rainlab/blog/posts diff --git a/plugins/rainlab/blog/controllers/posts/config_import_export.yaml b/plugins/rainlab/blog/controllers/posts/config_import_export.yaml new file mode 100644 index 0000000..e0cf8c5 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_import_export.yaml @@ -0,0 +1,41 @@ +# =================================== +# Import/Export Behavior Config +# =================================== + +import: + # Page title + title: rainlab.blog::lang.posts.import_post + + # Import List Column configuration + list: $/rainlab/blog/models/postimport/columns.yaml + + # Import Form Field configuration + form: $/rainlab/blog/models/postimport/fields.yaml + + # Import Model class + modelClass: RainLab\Blog\Models\PostImport + + # Redirect when finished + redirect: rainlab/blog/posts + + # Required permissions + permissions: rainlab.blog.access_import_export + +export: + # Page title + title: rainlab.blog::lang.posts.export_post + + # Output file name + fileName: posts.csv + + # Export List Column configuration + list: $/rainlab/blog/models/postexport/columns.yaml + + # Export Model class + modelClass: RainLab\Blog\Models\PostExport + + # Redirect when finished + redirect: rainlab/blog/posts + + # Required permissions + permissions: rainlab.blog.access_import_export diff --git a/plugins/rainlab/blog/controllers/posts/config_list.yaml b/plugins/rainlab/blog/controllers/posts/config_list.yaml new file mode 100644 index 0000000..62c7d23 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/config_list.yaml @@ -0,0 +1,47 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: $/rainlab/blog/models/post/columns.yaml + +# Filter widget configuration +filter: $/rainlab/blog/models/post/scopes.yaml + +# Model Class name +modelClass: RainLab\Blog\Models\Post + +# List Title +title: rainlab.blog::lang.posts.list_title + +# Link URL for each record +recordUrl: rainlab/blog/posts/update/:id + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 25 + +# Displays the list column set up button +showSetup: true + +# Displays the sorting link on each column +showSorting: true + +# Default sorting column +defaultSort: + column: published_at + direction: desc + +# Display checkboxes next to each record +showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt diff --git a/plugins/rainlab/blog/controllers/posts/create.htm b/plugins/rainlab/blog/controllers/posts/create.htm new file mode 100644 index 0000000..e6cd6a5 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/create.htm @@ -0,0 +1,23 @@ +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')), + 'id' => 'post-form' + ]) ?> + formRender() ?> + + +
    + + +
    + +
    +
    +

    fatalError)) ?>

    +

    +
    + diff --git a/plugins/rainlab/blog/controllers/posts/export.htm b/plugins/rainlab/blog/controllers/posts/export.htm new file mode 100644 index 0000000..1023caf --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/export.htm @@ -0,0 +1,27 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + exportRender() ?> +
    + +
    +
    + +
    +
    + + diff --git a/plugins/rainlab/blog/controllers/posts/import.htm b/plugins/rainlab/blog/controllers/posts/import.htm new file mode 100644 index 0000000..e33d6da --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/import.htm @@ -0,0 +1,25 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + importRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/blog/controllers/posts/index.htm b/plugins/rainlab/blog/controllers/posts/index.htm new file mode 100644 index 0000000..ea43a36 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/index.htm @@ -0,0 +1 @@ +listRender() ?> diff --git a/plugins/rainlab/blog/controllers/posts/update.htm b/plugins/rainlab/blog/controllers/posts/update.htm new file mode 100644 index 0000000..3f82c19 --- /dev/null +++ b/plugins/rainlab/blog/controllers/posts/update.htm @@ -0,0 +1,25 @@ +fatalError): ?> + +
    + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('rainlab.blog::lang.post.close_confirm')), + 'id' => 'post-form' + ]) ?> + formRender() ?> + + +
    + + + +
    + +
    +
    +

    fatalError)) ?>

    +

    +
    + + diff --git a/plugins/rainlab/blog/formwidgets/BlogMarkdown.php b/plugins/rainlab/blog/formwidgets/BlogMarkdown.php new file mode 100644 index 0000000..caff718 --- /dev/null +++ b/plugins/rainlab/blog/formwidgets/BlogMarkdown.php @@ -0,0 +1,133 @@ +viewPath = base_path().'/modules/backend/formwidgets/markdowneditor/partials'; + + $this->checkUploadPostback(); + + parent::init(); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets'; + parent::loadAssets(); + } + + /** + * Disable HTML cleaning on the widget level since the PostModel will handle it + * + * @return boolean + */ + protected function shouldCleanHtml() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function onRefresh() + { + $content = post($this->formField->getName()); + + $previewHtml = PostModel::formatHtml($content, true); + + return [ + 'preview' => $previewHtml + ]; + } + + /** + * Handle images being uploaded to the blog post + * + * @return void + */ + protected function checkUploadPostback() + { + if (!post('X_BLOG_IMAGE_UPLOAD')) { + return; + } + + $uploadedFileName = null; + + try { + $uploadedFile = Input::file('file'); + + if ($uploadedFile) + $uploadedFileName = $uploadedFile->getClientOriginalName(); + + $validationRules = ['max:'.File::getMaxFilesize()]; + $validationRules[] = 'mimes:jpg,jpeg,bmp,png,gif'; + + $validation = Validator::make( + ['file_data' => $uploadedFile], + ['file_data' => $validationRules] + ); + + if ($validation->fails()) { + throw new ValidationException($validation); + } + + if (!$uploadedFile->isValid()) { + throw new SystemException(Lang::get('cms::lang.asset.file_not_valid')); + } + + $fileRelation = $this->model->content_images(); + + $file = new File(); + $file->data = $uploadedFile; + $file->is_public = true; + $file->save(); + + $fileRelation->add($file, $this->sessionKey); + $result = [ + 'file' => $uploadedFileName, + 'path' => $file->getPath() + ]; + + $response = Response::make()->setContent($result); + $this->controller->setResponse($response); + + } catch (Exception $ex) { + $message = $uploadedFileName + ? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $uploadedFileName, 'error' => $ex->getMessage()]) + : $ex->getMessage(); + + $result = [ + 'error' => $message, + 'file' => $uploadedFileName + ]; + + $response = Response::make()->setContent($result); + $this->controller->setResponse($response); + } + } +} diff --git a/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php b/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php new file mode 100644 index 0000000..5da6fa9 --- /dev/null +++ b/plugins/rainlab/blog/formwidgets/MLBlogMarkdown.php @@ -0,0 +1,137 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['markdowneditor'] = $parentContent; + + $this->actAsControl(true); + + return $this->makePartial('mlmarkdowneditor'); + } + + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * Returns an array of translated values for this field + * @param $value + * @return array + */ + public function getSaveValue($value) + { + $localeData = $this->getLocaleSaveData(); + + /* + * Set the translated values to the model + */ + if ($this->model->methodExists('setAttributeTranslated')) { + foreach ($localeData as $locale => $value) { + $this->model->setAttributeTranslated('content', $value, $locale); + + $this->model->setAttributeTranslated( + 'content_html', + Post::formatHtml($value), + $locale + ); + } + } + + return array_get($localeData, $this->defaultLocale->code, $value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + + $this->actAsControl(true); + $this->addJs('js/mlmarkdowneditor.js'); + $this->actAsControl(false); + } + } + + protected function actAsParent($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = '/modules/backend/formwidgets/markdowneditor/assets'; + $this->viewPath = base_path('/modules/backend/formwidgets/markdowneditor/partials'); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } + + protected function actAsControl($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = '/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets'; + $this->viewPath = base_path('/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials'); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } +} diff --git a/plugins/rainlab/blog/lang/bg/lang.php b/plugins/rainlab/blog/lang/bg/lang.php new file mode 100644 index 0000000..6e5161d --- /dev/null +++ b/plugins/rainlab/blog/lang/bg/lang.php @@ -0,0 +1,100 @@ + [ + 'name' => 'Блог', + 'description' => 'Стабилната блог платформа.' + ], + 'blog' => [ + 'menu_label' => 'Блог', + 'menu_description' => 'управление на публикациите', + 'posts' => 'публикации', + 'create_post' => 'създай публикация', + 'categories' => 'категории', + 'create_category' => 'създай категория', + 'tab' => 'Блог', + 'access_posts' => 'управление на публикациите', + 'access_categories' => 'управление на категории', + 'access_other_posts' => 'управление на други потребители публикации в блога', + 'delete_confirm' => 'Сигурни ли сте?', + 'chart_published' => 'Публикувано', + 'chart_drafts' => 'Чернови', + 'chart_total' => 'Общо' + ], + 'posts' => [ + 'list_title' => 'Управление публикациите в блога', + 'filter_category' => 'Категория', + 'filter_published' => 'Скрий публикуваните', + 'new_post' => 'Нова публикация' + ], + 'post' => [ + 'title' => 'Заглавие', + 'title_placeholder' => 'Ново заглавие на публикацията', + 'slug' => 'Slug', + 'slug_placeholder' => 'нов slug на публикацията', + 'categories' => 'Категории', + 'created' => 'Създаден', + 'updated' => 'Обновен', + 'published' => 'Публикуван', + 'published_validation' => 'Моля, посочете дата на публикуване', + 'tab_edit' => 'Промяна', + 'tab_categories' => 'Категории', + 'categories_comment' => 'Изберете категории към който пренадлежи публикацията ', + 'categories_placeholder' => 'Няма категирии, Създайте първата?!', + 'tab_manage' => 'Управление', + 'published_on' => 'публикувано в', + 'excerpt' => 'Откъс', + 'featured_images' => 'Избрани снимки', + 'delete_confirm' => 'Наистина ли искате да изтриете тази публикация?', + 'close_confirm' => 'Публикацията не е запазена.', + 'return_to_posts' => 'Върни ме към всички публикации' + ], + 'categories' => [ + 'list_title' => 'Управление категориите в блога', + 'new_category' => 'Нова категория', + 'uncategorized' => 'Без категория' + ], + 'category' => [ + 'name' => 'Име', + 'name_placeholder' => 'Ново име на категорията', + 'slug' => 'Slug', + 'slug_placeholder' => 'нов slug на категотията', + 'posts' => 'публикации', + 'delete_confirm' => 'Наистина ли искате да изтриете тази категория?', + 'return_to_categories' => 'Върни ме към всички категории' + ], + 'settings' => [ + 'category_title' => 'Списък с категории', + 'category_description' => 'Показва списък с категориите на блога.', + 'category_slug' => 'категория slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Показване на празни категории', + 'category_display_empty_description' => 'Показване на категории, които нямат никакви публикации.', + 'category_page' => 'Страница на категория', + 'category_page_description' => 'Име на страницата за категирия. Това се използва подразбиране от компонента.', + 'post_title' => 'Публикация', + 'post_description' => 'Показване на Публикациите в блога на страницата.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Търсене на публикации по зададен slug.", + 'post_category' => 'Страница за Категория', + 'post_category_description' => 'Име на страница за категория за генериране на линк.Това се използва подразбиране от компонента.', + 'posts_title' => 'Лист с Публикации', + 'posts_description' => 'Показване на лист с публикации на страницата.', + 'posts_pagination' => 'Номер на страницата', + 'posts_pagination_description' => 'Тази стойност се използва за определяне на коя страница е потребителя.', + 'posts_filter' => 'Филтер Категория', + 'posts_filter_description' => 'Въведи slug на категория или URL адрес за филтриране по. Оставете празно за да се покажат всички публикации.', + 'posts_per_page' => 'Публикации на страница', + 'posts_per_page_validation' => 'Невалиден формат за публикации на страница', + 'posts_no_posts' => 'Няма публикации', + 'posts_no_posts_description' => 'Съобщение което да се покаже, в случай ,че няма публикации за показване.Това се използва подразбиране от компонента.', + 'posts_order' => 'подреждане на публикации', + 'posts_order_description' => 'Атрибут по който да бъдат подредени публикациите', + 'posts_category' => 'страница на категориите', + 'posts_category_description' => 'Име на страницата за категории , за "публикувано в". Това се използва подразбиране от компонента.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Име на страницата за публикации "Прочетете повече". Това се използва подразбиране от компонента.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/cs/lang.php b/plugins/rainlab/blog/lang/cs/lang.php new file mode 100644 index 0000000..553d125 --- /dev/null +++ b/plugins/rainlab/blog/lang/cs/lang.php @@ -0,0 +1,154 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustní blogová platforma.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Správa blogových příspěvků', + 'posts' => 'Příspěvky', + 'create_post' => 'Příspěvek', + 'categories' => 'Kategorie', + 'create_category' => 'Kategorie příspěvků', + 'tab' => 'Blog', + 'access_posts' => 'Správa blogových příspěvků', + 'access_categories' => 'Správa blogových kategorií', + 'access_other_posts' => 'Správa příspěvků ostatních uživatelů', + 'access_import_export' => 'Možnost importu a exportu příspěvků', + 'access_publish' => 'Možnost publikovat příspěvky', + 'delete_confirm' => 'Jste si jistí?', + 'chart_published' => 'Publikované', + 'chart_drafts' => 'Návrhy', + 'chart_total' => 'Celkem', + ], + 'posts' => [ + 'list_title' => 'Správa blogových příspěvků', + 'filter_category' => 'Kategorie', + 'filter_published' => 'Schovat publikované', + 'filter_date' => 'Datum', + 'new_post' => 'Nový příspěvek', + 'export_post' => 'Export příspěvků', + 'import_post' => 'Import příspěvků', + ], + 'post' => [ + 'title' => 'Název', + 'title_placeholder' => 'Zadejte název', + 'content' => 'Obsah', + 'content_html' => 'HTML obsah', + 'slug' => 'URL příspěvku', + 'slug_placeholder' => 'zadejte-url-prispevku', + 'categories' => 'Kategorie', + 'author_email' => 'E-mail autora', + 'created' => 'Vytvořeno', + 'created_date' => 'Vytvořeno dne', + 'updated' => 'Upraveno', + 'updated_date' => 'Upraveno dne', + 'published' => 'Publikováno', + 'published_date' => 'Publikováno dne', + 'published_validation' => 'Zadejte prosím datum publikace příspěvku', + 'tab_edit' => 'Upravit', + 'tab_categories' => 'Kategorie', + 'categories_comment' => 'Vyberte kategorie do kterých příspěvek patří', + 'categories_placeholder' => 'Nejsou zde žádné kategorie, nejdříve musíte nějaké vytvořit!', + 'tab_manage' => 'Nastavení', + 'published_on' => 'Publikováno dne', + 'excerpt' => 'Perex příspěvku', + 'summary' => 'Shrnutí', + 'featured_images' => 'Obrázky', + 'delete_confirm' => 'Opravdu chcete smazat tento příspěvek?', + 'delete_success' => 'Vybrané příspěvky úspěšně odstraněny.', + 'close_confirm' => 'Příspěvek není uložený.', + 'return_to_posts' => 'Zpět na seznam příspěvků', + ], + 'categories' => [ + 'list_title' => 'Správa blogových kategorií', + 'new_category' => 'Nová kategorie', + 'uncategorized' => 'Nezařazeno', + ], + 'category' => [ + 'name' => 'Název', + 'name_placeholder' => 'Název nové kategorie', + 'description' => 'Popis', + 'slug' => 'URL kategorie', + 'slug_placeholder' => 'zadejte-url-kategorie', + 'posts' => 'Počet příspěvků', + 'delete_confirm' => 'Opravdu chcete smazat tuto kategorii?', + 'delete_success' => 'Vybrané kategorie úspěšně odstraněny.', + 'return_to_categories' => 'Zpět na seznam blogových kategorií', + 'reorder' => 'Změnit pořadí', + ], + 'menuitem' => [ + 'blog_category' => 'Blogová kategorie', + 'all_blog_categories' => 'Všechny blogové kategorie', + 'blog_post' => 'Blogový příspěvek', + 'all_blog_posts' => 'Všechny blogové příspěvky', + 'category_blog_posts' => 'Blog category posts' + ], + 'settings' => [ + 'category_title' => 'Seznam kategorií', + 'category_description' => 'Zobrazí na stránce seznam blogových kategorií.', + 'category_slug' => 'URL kategorie', + 'category_slug_description' => "Najde blogovou kategorii s tímto URL. Používá se pro zobrazení aktivní kategorie.", + 'category_display_empty' => 'Zobrazit prázdné kategorie', + 'category_display_empty_description' => 'Zobrazit kategorie bez blogových příspěvků.', + 'category_page' => 'Stránka kategorií', + 'category_page_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'post_title' => 'Příspěvek', + 'post_description' => 'Zobrazí blogový příspěvek na stránce.', + 'post_slug' => 'URL příspěvku', + 'post_slug_description' => "Najde příspěvek dle zadané URL.", + 'post_category' => 'Stránka kategorie', + 'post_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'posts_title' => 'Seznam příspěvků', + 'posts_description' => 'Zobrazí na stránce seznam posledních příspěvků na stránkách.', + 'posts_pagination' => 'Číslo stránky', + 'posts_pagination_description' => 'Číslo stránky určující na které stránce se uživatel nachází. Použito pro stránkování.', + 'posts_filter' => 'Filtr kategorií', + 'posts_filter_description' => 'Zadejte URL kategorie, nebo URL parametr pro filtrování příspěvků. Nechte prázdné pro zobrazení všech příspěvků.', + 'posts_per_page' => 'Příspěvků na stránku', + 'posts_per_page_validation' => 'Špatný formát počtu příspěvků na stránku, musí být zadáno jako číslo', + 'posts_no_posts' => 'Hláška prázdné stránky', + 'posts_no_posts_description' => 'Zpráva se zobrazí pokud se nepovede najít žádné články.', + 'posts_no_posts_default' => 'Nenalezeny žádné příspěvky', + 'posts_order' => 'Řazení článků', + 'posts_order_decription' => 'Nastaví řazení článků ve výpisu', + 'posts_category' => 'Stránka kategorií', + 'posts_category_description' => 'Vyberte stránku která slouží k zobrazení všech kategorií (nebo detailu kategorie).', + 'posts_post' => 'Stránka příspěvků', + 'posts_post_description' => 'Vyberte stránku která slouží k zobrazení článků (nebo detailu článku).', + 'posts_except_post' => 'Vyloučit příspěvěk', + 'posts_except_post_description' => 'Zadejte ID nebo URL příspěvku který chcete vyloučit', + 'posts_except_categories' => 'Vyloučené kategorie', + 'posts_except_categories_description' => 'Pro vyloučení kategorií zadejte čárkou oddělené URL příspěvků nebo proměnnou, která tento seznam obsahuje.', + 'rssfeed_blog' => 'Blogová stránka', + 'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.', + 'rssfeed_title' => 'RSS Kanál', + 'rssfeed_description' => 'Vygeneruje RSS kanál který obsahuje blogové příspěvky.', + 'group_links' => 'Odkazy', + 'group_exceptions' => 'Výjimky' + ], + 'sorting' => [ + 'title_asc' => 'Název (sestupně)', + 'title_desc' => 'Název (vzestupně)', + 'created_asc' => 'Vytvořeno (sestupně)', + 'created_desc' => 'Vytvořeno (vzestupně)', + 'updated_asc' => 'Upraveno (sestupně)', + 'updated_desc' => 'Upraveno (vzestupně)', + 'published_asc' => 'Publikováno (sestupně)', + 'published_desc' => 'Publikováno (vzestupně)', + 'random' => 'Náhodně' + ], + 'import' => [ + 'update_existing_label' => 'Uprav existující příspěvky', + 'update_existing_comment' => 'Zvolte pokud chcete upravit příspěvky se stejným ID, názvem nebo URL.', + 'auto_create_categories_label' => 'VYtvořit kategorie ze souboru', + 'auto_create_categories_comment' => 'Chcete-li tuto funkci použít, měli byste se shodovat se sloupcem Kategorie, jinak vyberte výchozí kategorie, které chcete použít z níže uvedených položek.', + 'categories_label' => 'Kategorie', + 'categories_comment' => 'Vyberte kategorie ke kterým budou příspěvky přiřazeny (volitelné).', + 'default_author_label' => 'Výchozí autor příspěvků (volitelné)', + 'default_author_comment' => 'Import se pokusí použít existujícího autora, pokud odpovídá sloupci email, jinak se použije výše uvedený autor.', + 'default_author_placeholder' => '-- vyberte autora --' + ] +]; diff --git a/plugins/rainlab/blog/lang/de/lang.php b/plugins/rainlab/blog/lang/de/lang.php new file mode 100644 index 0000000..7e78057 --- /dev/null +++ b/plugins/rainlab/blog/lang/de/lang.php @@ -0,0 +1,132 @@ + [ + 'name' => 'Blog', + 'description' => 'Eine robuste Blog Plattform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog Artikel bearbeiten', + 'posts' => 'Artikel', + 'create_post' => 'Blog Artikel', + 'categories' => 'Kategorien', + 'create_category' => 'Blog Kategorie', + 'tab' => 'Blog', + 'access_posts' => 'Blog Artikel verwalten', + 'access_categories' => 'Blog Kategorien verwalten', + 'access_other_posts' => 'Blog Artikel anderer Benutzer verwalten', + 'access_import_export' => 'Blog Artikel importieren oder exportieren', + 'access_publish' => 'Kann Artikel veröffentlichen', + 'delete_confirm' => 'Bist du sicher?', + 'chart_published' => 'Veröffentlicht', + 'chart_drafts' => 'Entwurf', + 'chart_total' => 'Gesamt' + ], + 'posts' => [ + 'list_title' => 'Blog Artikel verwalten', + 'filter_category' => 'Kategorie', + 'filter_published' => 'Veröffentlichte ausblenden', + 'filter_date' => 'Date', + 'new_post' => 'Neuer Artikel', + 'export_post' => 'Exportiere Artikel', + 'import_post' => 'Importiere Artikel' + ], + 'post' => [ + 'title' => 'Titel', + 'title_placeholder' => 'Neuer Titel', + 'content' => 'Inhalt', + 'content_html' => 'HTML-Inhalt', + 'slug' => 'Slug', + 'slug_placeholder' => 'neuer-artikel-slug', + 'categories' => 'Kategorien', + 'author_email' => 'Autor E-Mail', + 'created' => 'Erstellt', + 'created_date' => 'Erstellzeitpunkt', + 'updated' => 'Aktualisiert', + 'updated_date' => 'Aktualisierungszeitpunk', + 'published' => 'Veröffentlicht', + 'published_date' => 'Veröffentlichungszeitpunkt', + 'published_validation' => 'Bitte gebe das Datum der Veröffentlichung an', + 'tab_edit' => 'Bearbeiten', + 'tab_categories' => 'Kategorien', + 'categories_comment' => 'Wähle die zugehörigen Kategorien', + 'categories_placeholder' => 'Es existieren keine Kategorien. Bitte lege zuerst Kategorien an!', + 'tab_manage' => 'Verwalten', + 'published_on' => 'Veröffentlicht am', + 'excerpt' => 'Textauszug', + 'summary' => 'Zusammenfassung', + 'featured_images' => 'Zugehörige Bilder', + 'delete_confirm' => 'Möchtest du diesen Artikel wirklich löschen?', + 'close_confirm' => 'Der Artikel ist noch nicht gespeichert.', + 'return_to_posts' => 'Zurück zur Artikel-Übersicht', + 'posted_byline' => 'Veröffentlicht in :categories am :date', + 'posted_byline_no_categories' => 'Veröffentlicht am :date', + 'date_format' => 'd. F Y', + ], + 'categories' => [ + 'list_title' => 'Blog Kategorien verwalten', + 'new_category' => 'Neue Kategorie', + 'uncategorized' => 'Allgemein' + ], + 'category' => [ + 'name' => 'Name', + 'name_placeholder' => 'Neuer Kategorie Name', + 'description' => 'Beschreibung', + 'slug' => 'Slug', + 'slug_placeholder' => 'neuer-kategorie-slug', + 'posts' => 'Artikel', + 'delete_confirm' => 'Möchtest du die Kategorie wirklich löschen?', + 'return_to_categories' => 'Zurück zur Kategorie-Übersicht.', + 'reorder' => 'Kategorien sortieren' + ], + 'menuitem' => [ + 'blog_category' => 'Blog Kategorie', + 'all_blog_categories' => 'Alle Blog Kategorien', + 'blog_post' => 'Blog Artikel', + 'all_blog_posts' => 'Alle Blog Artikel', + 'category_blog_posts' => 'Blog Kategorie Artikel' + ], + 'settings' => [ + 'category_title' => 'Blog Kategorie-Übersicht', + 'category_description' => 'Zeigt eine Blog Kategorien-Übersicht.', + 'category_slug' => 'Slug Parametername', + 'category_slug_description' => 'Der URL-Routen-Parameter welcher verwendet wird um die aktuelle Kategorie zu bestimmen. Wird von der Standard-Komponente benötigt um die aktive Kategorie zu markieren.', + 'category_display_empty' => 'Leere Kategorien anzeigen', + 'category_display_empty_description' => 'Kategorien zeigen welche keine Artikel besitzen.', + 'category_page' => 'Kategorien Seite', + 'category_page_description' => 'Name der Kategorien-Seiten-Datei für die Kategorien Links. Wird von der Standard-Komponente benötigt.', + 'post_title' => 'Blog Artikel', + 'post_description' => 'Zeigt einen Blog Artikel auf der Seite.', + 'post_slug' => 'Slug Parametername', + 'post_slug_description' => 'Der URL-Routen-Parameter um den Post mittels "Slug" zu bestimmen.', + 'post_category' => 'Kategorien-Seite', + 'post_category_description' => 'Name der Kategorien-Seiten-Datei für Kategorie-Links.', + 'posts_title' => 'Blog Artikel-Übersicht', + 'posts_description' => 'Stellt eine Liste der neuesten Artikel auf der Seite dar.', + 'posts_pagination' => 'Blättern Parametername', + 'posts_pagination_description' => 'Der erwartete Parametername welcher für Seiten verwendet wird.', + 'posts_filter' => 'Kategorien-Filter', + 'posts_filter_description' => 'Bitte gebe ein Kategorien-Slug oder URL-Parameter an, mittels den die Artikel gefiltert werden. Wenn der Wert leer ist, werden alle Artikel angezeigt.', + 'posts_per_page' => 'Artikel pro Seite', + 'posts_per_page_validation' => 'Ungültiger "Artikel pro Seiten" Wert', + 'posts_no_posts' => 'Keine Artikel Nachricht', + 'posts_no_posts_description' => 'Nachricht welche dargestellt wird wenn keine Artikel vorhanden sind. Dieser Wert wird von der Standard-Komponente verwendet.', + 'posts_order' => 'Artikel Sortierung', + 'posts_order_description' => 'Attribute nach welchem Artikel sortiert werden.', + 'posts_category' => 'Kategorien-Seite', + 'posts_category_description' => 'Name der Kategorien-Seiten-Datei für "Veröffentlicht in" Kategorien-Links. Dieser Wert von der Standard-Komponente verwendet.', + 'posts_post' => 'Artikel Seite', + 'posts_post_description' => 'Name der Artikel-Seiten-Datei für die "Erfahre mehr" Links. Dieser Wert für von der Standard-Komponente verwendet.', + 'posts_except_post' => 'Artikel ausschließen', + 'posts_except_post_description' => 'Gebe direkt die ID/URL oder eine Variable mit der Artikel-ID/URL an um diesen Artikel auszuschließen. Dieser Wert für von der Standard-Komponente verwendet.', + 'posts_except_categories' => 'Kategorien ausschließen', + 'posts_except_categories_description' => 'Gebe eine kommagetrennte Liste von Kategorie-Slugs oder eine Variable mit einer solchen Liste an um deren Artikel auszuschließen. Die Dieser Wert für von der Standard-Komponente verwendet.', + 'rssfeed_blog' => 'Blog Seite', + 'rssfeed_blog_description' => 'Name der Artikel-Seiten-Datei für die Links. Dieser Wert für von der Standard-Komponente verwendet.', + 'rssfeed_title' => 'RSS-Feed', + 'rssfeed_description' => 'Erstellt einen RSS-Feed mit Artikeln aus dem Blog.', + 'group_links' => 'Links', + 'group_exceptions' => 'Ausnahmen' + ] +]; diff --git a/plugins/rainlab/blog/lang/en/lang.php b/plugins/rainlab/blog/lang/en/lang.php new file mode 100644 index 0000000..06e7e47 --- /dev/null +++ b/plugins/rainlab/blog/lang/en/lang.php @@ -0,0 +1,169 @@ + [ + 'name' => 'Blog', + 'description' => 'A robust blogging platform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Manage Blog Posts', + 'posts' => 'Posts', + 'create_post' => 'Blog post', + 'categories' => 'Categories', + 'create_category' => 'Blog category', + 'tab' => 'Blog', + 'access_posts' => 'Manage the blog posts', + 'access_categories' => 'Manage the blog categories', + 'access_other_posts' => 'Manage other users blog posts', + 'access_import_export' => 'Allowed to import and export posts', + 'access_publish' => 'Allowed to publish posts', + 'manage_settings' => 'Manage blog settings', + 'delete_confirm' => 'Are you sure?', + 'chart_published' => 'Published', + 'chart_drafts' => 'Drafts', + 'chart_total' => 'Total', + 'settings_description' => 'Manage blog settings', + 'show_all_posts_label' => 'Show All Posts to Backend Users', + 'show_all_posts_comment' => 'Display both published and unpublished posts on the frontend to backend users', + 'use_legacy_editor_label' => 'Use the Legacy Markdown Editor', + 'use_legacy_editor_comment' => 'Enable the older version of the markdown editor when using October CMS v2 and above', + 'tab_general' => 'General', + 'preview' => 'Preview' + ], + 'posts' => [ + 'list_title' => 'Manage the blog posts', + 'filter_category' => 'Category', + 'filter_published' => 'Published', + 'filter_date' => 'Date', + 'new_post' => 'New Post', + 'export_post' => 'Export Posts', + 'import_post' => 'Import Posts' + ], + 'post' => [ + 'title' => 'Title', + 'title_placeholder' => 'New post title', + 'content' => 'Content', + 'content_html' => 'HTML Content', + 'slug' => 'Slug', + 'slug_placeholder' => 'new-post-slug', + 'categories' => 'Categories', + 'author_email' => 'Author Email', + 'created' => 'Created', + 'created_date' => 'Created date', + 'updated' => 'Updated', + 'updated_date' => 'Updated date', + 'published' => 'Published', + 'published_by' => 'Published by', + 'current_user' => 'Current user', + 'published_date' => 'Published date', + 'published_validation' => 'Please specify the published date', + 'tab_edit' => 'Edit', + 'tab_categories' => 'Categories', + 'categories_comment' => 'Select categories the blog post belongs to', + 'categories_placeholder' => 'There are no categories, you should create one first!', + 'tab_manage' => 'Manage', + 'published_on' => 'Published on', + 'excerpt' => 'Excerpt', + 'summary' => 'Summary', + 'featured_images' => 'Featured Images', + 'delete_confirm' => 'Delete this post?', + 'delete_success' => 'Successfully deleted those posts.', + 'close_confirm' => 'The post is not saved.', + 'return_to_posts' => 'Return to posts list', + 'posted_byline' => 'Posted in :categories on :date.', + 'posted_byline_no_categories' => 'Posted on :date.', + 'date_format' => 'M d, Y', + ], + 'categories' => [ + 'list_title' => 'Manage the blog categories', + 'new_category' => 'New Category', + 'uncategorized' => 'Uncategorized' + ], + 'category' => [ + 'name' => 'Name', + 'name_placeholder' => 'New category name', + 'description' => 'Description', + 'slug' => 'Slug', + 'slug_placeholder' => 'new-category-slug', + 'posts' => 'Posts', + 'delete_confirm' => 'Delete this category?', + 'delete_success' => 'Successfully deleted those categories.', + 'return_to_categories' => 'Return to the blog category list', + 'reorder' => 'Reorder Categories' + ], + 'menuitem' => [ + 'blog_category' => 'Blog category', + 'all_blog_categories' => 'All blog categories', + 'blog_post' => 'Blog post', + 'all_blog_posts' => 'All blog posts', + 'category_blog_posts' => 'Blog category posts' + ], + 'settings' => [ + 'category_title' => 'Category List', + 'category_description' => 'Displays a list of blog categories on the page.', + 'category_slug' => 'Category slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Display empty categories', + 'category_display_empty_description' => 'Show categories that do not have any posts.', + 'category_page' => 'Category page', + 'category_page_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'post_title' => 'Post', + 'post_description' => 'Displays a blog post on the page.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Look up the blog post using the supplied slug value.", + 'post_category' => 'Category page', + 'post_category_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'posts_title' => 'Post List', + 'posts_description' => 'Displays a list of latest blog posts on the page.', + 'posts_pagination' => 'Page number', + 'posts_pagination_description' => 'This value is used to determine what page the user is on.', + 'posts_filter' => 'Category filter', + 'posts_filter_description' => 'Enter a category slug or URL parameter to filter the posts by. Leave empty to show all posts.', + 'posts_per_page' => 'Posts per page', + 'posts_per_page_validation' => 'Invalid format of the posts per page value', + 'posts_no_posts' => 'No posts message', + 'posts_no_posts_description' => 'Message to display in the blog post list in case if there are no posts. This property is used by the default component partial.', + 'posts_no_posts_default' => 'No posts found', + 'posts_order' => 'Post order', + 'posts_order_description' => 'Attribute on which the posts should be ordered', + 'posts_category' => 'Category page', + 'posts_category_description' => 'Name of the category page file for the "Posted into" category links. This property is used by the default component partial.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Name of the blog post page file for the "Learn more" links. This property is used by the default component partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to exclude. You may use a comma-separated list to specify multiple posts.', + 'posts_except_post_validation' => 'Post exceptions must be a single slug or ID, or a comma-separated list of slugs and IDs', + 'posts_except_categories' => 'Except categories', + 'posts_except_categories_description' => 'Enter a comma-separated list of category slugs or variable with such a list of categories you want to exclude', + 'posts_except_categories_validation' => 'Category exceptions must be a single category slug, or a comma-separated list of slugs', + 'rssfeed_blog' => 'Blog page', + 'rssfeed_blog_description' => 'Name of the main blog page file for generating links. This property is used by the default component partial.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Generates an RSS feed containing posts from the blog.', + 'group_links' => 'Links', + 'group_exceptions' => 'Exceptions' + ], + 'sorting' => [ + 'title_asc' => 'Title (ascending)', + 'title_desc' => 'Title (descending)', + 'created_asc' => 'Created (ascending)', + 'created_desc' => 'Created (descending)', + 'updated_asc' => 'Updated (ascending)', + 'updated_desc' => 'Updated (descending)', + 'published_asc' => 'Published (ascending)', + 'published_desc' => 'Published (descending)', + 'random' => 'Random' + ], + 'import' => [ + 'update_existing_label' => 'Update existing posts', + 'update_existing_comment' => 'Check this box to update posts that have exactly the same ID, title or slug.', + 'auto_create_categories_label' => 'Create categories specified in the import file', + 'auto_create_categories_comment' => 'You should match the Categories column to use this feature, otherwise select the default categories to use from the items below.', + 'categories_label' => 'Categories', + 'categories_comment' => 'Select the categories that imported posts will belong to (optional).', + 'default_author_label' => 'Default post author (optional)', + 'default_author_comment' => 'The import will try to use an existing author if you match the Author Email column, otherwise the author specified above is used.', + 'default_author_placeholder' => '-- select author --' + ] +]; diff --git a/plugins/rainlab/blog/lang/es/lang.php b/plugins/rainlab/blog/lang/es/lang.php new file mode 100644 index 0000000..d34e8e4 --- /dev/null +++ b/plugins/rainlab/blog/lang/es/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Una plataforma robusta de blogging.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Administrar Publicaciones', + 'posts' => 'Publicaciones', + 'create_post' => 'Crear publicación', + 'categories' => 'Categorías', + 'create_category' => 'Categoría', + 'tab' => 'Blog', + 'access_posts' => 'Administrar las publicaciones', + 'access_categories' => 'Administrar las categorías', + 'access_other_posts' => 'Administrar publicaciones de otros usuarios', + 'access_import_export' => 'Autorizado para importar y exportar publicaciones', + 'access_publish' => 'Autorizado para publicar publicaciones', + 'manage_settings' => 'Administrar configuración del blog', + 'delete_confirm' => '¿Está seguro?', + 'chart_published' => 'Publicado', + 'chart_drafts' => 'Borradores', + 'chart_total' => 'Total', + 'settings_description' => 'Administrar configuración del blog', + 'show_all_posts_label' => 'Mostrar todas las publicaciones a los usuarios de backend', + 'show_all_posts_comment' => 'Mostrar las publicaciones publicados y los borradores a los usuarios de backend', + 'tab_general' => 'General' + ], + 'posts' => [ + 'list_title' => 'Administrar publicaciones', + 'filter_category' => 'Categoría', + 'filter_published' => 'Publicado', + 'filter_date' => 'Fecha', + 'new_post' => 'Nueva publicación', + 'export_post' => 'Exportar publicaciones', + 'import_post' => 'Importar publicaciones' + ], + 'post' => [ + 'title' => 'Título', + 'title_placeholder' => 'Título de la publicación', + 'content' => 'Contenido', + 'content_html' => 'Contenido HTML', + 'slug' => 'Identificador', + 'slug_placeholder' => 'nueva-publicacion', + 'categories' => 'Categorías', + 'author_email' => 'Email del Autor', + 'created' => 'Creado', + 'created_date' => 'Fecha de Creación', + 'updated' => 'Actualizado', + 'updated_date' => 'Fecha de Actualización', + 'published' => 'Publicado', + 'published_by' => 'Publicado por', + 'current_user' => 'Usuario actual', + 'published_date' => 'Fecha de publicación', + 'published_validation' => 'Por favor, especifique la fecha de publicación', + 'tab_edit' => 'Editar', + 'tab_categories' => 'Categorías', + 'categories_comment' => 'Seleccione las categorías para la publicación', + 'categories_placeholder' => 'No hay categorías, ¡crea una primero!', + 'tab_manage' => 'Administrar', + 'published_on' => 'Publicado el', + 'excerpt' => 'Resumen', + 'summary' => 'Resumen', + 'featured_images' => 'Imágenes Destacadas', + 'delete_confirm' => '¿Borrar la publicación?', + 'delete_success' => 'Publicación borrada correctamente', + 'close_confirm' => 'La publicación no está guardada.', + 'return_to_posts' => 'Volver a la lista de publicaciones', + 'posted_byline' => 'Publicado en :categories el :date.', + 'posted_byline_no_categories' => 'Publicado el :date.', + 'date_format' => 'd de M de Y', + ], + 'categories' => [ + 'list_title' => 'Administrar las categorías', + 'new_category' => 'Nueva categoría', + 'uncategorized' => 'Sin Categoría' + ], + 'category' => [ + 'name' => 'Nombre', + 'name_placeholder' => 'Nombre de la categoría', + 'description' => 'Descripción', + 'slug' => 'Identificador', + 'slug_placeholder' => 'nueva-categoría', + 'posts' => 'Publicaciones', + 'delete_confirm' => '¿Borrar esta categoría?', + 'delete_success' => 'Categorías borradas correctamente.', + 'return_to_categories' => 'Volver a la lista de categorías', + 'reorder' => 'Re-ordenar Categorías' + ], + 'menuitem' => [ + 'blog_category' => 'Categoría del blog', + 'all_blog_categories' => 'Todas las categorías del blog', + 'blog_post' => 'Publicación del blog', + 'all_blog_posts' => 'Todas las publicaciones del blog', + 'category_blog_posts' => 'Publicaciones del blog por categorías' + ], + 'settings' => [ + 'category_title' => 'Lista de Categorías', + 'category_description' => 'Muestra en la página una lista de las categorías.', + 'category_slug' => 'Identificador de la categoría', + 'category_slug_description' => "Localiza una categoría utilizando el identificador proporcionado. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente para marcar la categoría activa.", + 'category_display_empty' => 'Mostrar categorías vacías', + 'category_display_empty_description' => 'Mostrar categorías que no tienen ninguna publicación.', + 'category_page' => 'Página de categorías', + 'category_page_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'post_title' => 'Publicación', + 'post_description' => 'Muestra una publicación en la página.', + 'post_slug' => 'Identificador de la publicación', + 'post_slug_description' => "Se buscará la publicación utilizando el valor del identificador proporcionado.", + 'post_category' => 'Página de categoría', + 'post_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categorías. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_title' => 'Lista de publicaciones', + 'posts_description' => 'Muestra una lista de las últimas publicaciones en la página.', + 'posts_pagination' => 'Número de página', + 'posts_pagination_description' => 'Este valor se utiliza para determinar en que página se encuentra el usuario.', + 'posts_filter' => 'Filtro de categoría', + 'posts_filter_description' => 'Ingrese un identificador de categoría o parámetro URL. Se utilizará para filtrar las publicaciones. Deje el campo vacío para mostrar todas las publicaciones.', + 'posts_per_page' => 'Publicaciones por página', + 'posts_per_page_validation' => 'Formato inválido para el valor de publicaciones por página', + 'posts_no_posts' => 'Mensaje cuando no hay publicaciones', + 'posts_no_posts_description' => 'Mensaje que se mostrará en la lista de publicaciones del blog cuando no haya ningúno. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_no_posts_default' => 'No se encontraron publicaciones.', + 'posts_order' => 'Ordenar publicaciones por', + 'posts_order_description' => 'Atributo mediante el cual se deberán ordenar las publicaciones', + 'posts_category' => 'Página de Categoría', + 'posts_category_description' => 'Nombre del archivo de página utilizado para los enlaces de categoría "Publicado en". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_post' => 'Página de las publicaciones', + 'posts_post_description' => 'Nombre del archivo de página utilizado para los enlaces "Saber más". Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'posts_except_post' => 'Exceptuar publicación', + 'posts_except_post_description' => 'Ingrese una ID/URL o variable que contenga una ID/URL de la publicación que se quiera excluir', + 'posts_except_post_validation' => 'La publicación a excluir debe ser una ID/URL, o una lista separada por comas de IDs/URLs', + 'posts_except_categories' => 'Excluir categorías', + 'posts_except_categories_description' => 'Introduce una lista separada por comas de IDs/URLs de categorías con las categorías a excluir.', + 'posts_except_categories_validation' => 'Las categorías excluidas deben ser una URL de categoría o una lista separada por comas', + 'rssfeed_blog' => 'Página del blog', + 'rssfeed_blog_description' => 'Nombre del archivo de página principal para generación de enlaces. Esta propiedad es utilizada dentro del parcial que viene por defecto en el componente.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Genera un feed de RSS con las publicaciones del blog.', + 'group_links' => 'Enlaces', + 'group_exceptions' => 'Excepciones' + ], + 'sorting' => [ + 'title_asc' => 'Título (ascendiente)', + 'title_desc' => 'Título (descendiente)', + 'created_asc' => 'Creado (ascendiente)', + 'created_desc' => 'Creado (descendiente)', + 'updated_asc' => 'Editado (ascendiente)', + 'updated_desc' => 'Editado (descendiente)', + 'published_asc' => 'Publicado (ascendiente)', + 'published_desc' => 'Publicado (descendiente)', + 'random' => 'Aleatorio' + ], + 'import' => [ + 'update_existing_label' => 'Editar publicaciones existentes', + 'update_existing_comment' => 'Selecciona este check para actualizar las publicaciones con exactamente la misma ID, título o URL.', + 'auto_create_categories_label' => 'Crear categorías especificadas en el archivo a importar', + 'auto_create_categories_comment' => 'Debes hacer coincidir la columna Categoría para usar esta funcionalidad, sino selecciona la categoría por defecto para para usar para los elementos de abajo.', + 'categories_label' => 'Categorías', + 'categories_comment' => 'Selecciona las categorías a las que pertenecerán las publicaciones importadas (opcional).', + 'default_author_label' => 'Autor de publicación por defecto (opcional)', + 'default_author_comment' => 'La importación intentará usar un autor existente si coicide con la columna "Author Email", sino se usará el autor especificado arriba.', + 'default_author_placeholder' => '-- Selecciona Autor/a --' + ] +]; diff --git a/plugins/rainlab/blog/lang/fa/lang.php b/plugins/rainlab/blog/lang/fa/lang.php new file mode 100644 index 0000000..0413451 --- /dev/null +++ b/plugins/rainlab/blog/lang/fa/lang.php @@ -0,0 +1,109 @@ + [ + 'name' => 'وبلاگ', + 'description' => 'پلتفرم قوی برای وبلاگ نویسی' + ], + 'blog' => [ + 'menu_label' => 'وبلاگ', + 'menu_description' => 'مدیریت پست های ارسالی', + 'posts' => 'پست ها', + 'create_post' => 'ایجاد پست جدید', + 'categories' => 'دسته بندی ها', + 'create_category' => 'ایجاد دسته بندی جدید', + 'tab' => 'وبلاگ', + 'access_posts' => 'مدیریت پست های ارسالی', + 'access_categories' => 'مدیریت دسته بندی های وبلاگ', + 'access_other_posts' => 'مدیریت پست های ارسالی سایر کاربران', + 'access_import_export' => 'توانایی واردکردن و خارج کردن پستها', + 'delete_confirm' => 'آیا اطمینان دارید؟', + 'chart_published' => 'منتشر شده', + 'chart_drafts' => 'پیش نویس', + 'chart_total' => 'مجموع' + ], + 'posts' => [ + 'list_title' => 'مدیریت پست های ارسالی', + 'filter_category' => 'دسته بندی', + 'filter_published' => 'مخفی کردن منتشر شده ها', + 'new_post' => 'پست جدید' + ], + 'post' => [ + 'title' => 'عنوان', + 'title_placeholder' => 'عنوان پست جدید', + 'content' => 'محتوی', + 'content_html' => 'محتوی HTML', + 'slug' => 'آدرس', + 'slug_placeholder' => 'آدرس-پست-جدید', + 'categories' => 'دسته بندی ها', + 'author_email' => 'پست الکترونیکی نویسنده', + 'created' => 'ایجاد شده در', + 'created_date' => 'تاریخ ایجاد', + 'updated' => 'به روزرسانی شده در', + 'updated_date' => 'تاریخ به روزرسانی', + 'published' => 'منتشر شده', + 'published_date' => 'تاریخ انتشار', + 'published_validation' => 'لطفا تاریخ انتشار را وارد نمایید', + 'tab_edit' => 'ویرایش', + 'tab_categories' => 'دسته بندی ها', + 'categories_comment' => 'دسته بندی هایی را که پست به آنها تعلق دارد را انتخاب نمایید', + 'categories_placeholder' => 'دسته بندی ای وجود ندارد. ابتدا یک دسته بندی ایجاد نمایید!', + 'tab_manage' => 'مدیریت', + 'published_on' => 'منتشر شده در', + 'excerpt' => 'خلاصه', + 'summary' => 'چکیده', + 'featured_images' => 'تصاویر شاخص', + 'delete_confirm' => 'آیا از حذف این پست اطمینان دارید؟', + 'close_confirm' => 'پست ذخیره نشده است', + 'return_to_posts' => 'بازگشت به لیست پست ها' + ], + 'categories' => [ + 'list_title' => 'مدیریت دسته بندی های وبلاگ', + 'new_category' => 'دسته بندی جدید', + 'uncategorized' => 'بدون دسته بندی' + ], + 'category' => [ + 'name' => 'نام', + 'name_placeholder' => 'نام دسته بندی جدید', + 'slug' => 'آدرس', + 'slug_placeholder' => 'آدرس-جدید-دسته-بندی', + 'posts' => 'پست ها', + 'delete_confirm' => 'آیا از حذف این دسته بندی اطمینان دارید؟', + 'return_to_categories' => 'بازگشت به لیست دسته بندی های وبلاگ', + 'reorder' => 'مرتب سازی دسته بندی ها' + ], + 'settings' => [ + 'category_title' => 'لیست دسته بندی', + 'category_description' => 'نمایش لیست دسته بندی های وبلاگ در صفحه', + 'category_slug' => 'آدرس دسته بندی', + 'category_slug_description' => "دسته بندی وبلاگ توسط آدرس وارد شده جستجو می شود. این عمل توسط ابزار دسته بندی برای برجسته ساختن دسته بندی در حال نمایش استفاده می شود.", + 'category_display_empty' => 'نمایش دسته بندی های خالی', + 'category_display_empty_description' => 'نمایش دسته بندی هایی که هیچ ارسالی در آنها وجود ندارد.', + 'category_page' => 'صفحه دسته بندی', + 'category_page_description' => 'نام صفحه ای که لیست دسته بندی ها در آن نمایش داده می شوند. این گزینه به طور پیشفرض توسط ابزار مورد استفاده قرار میگیرد.', + 'post_title' => 'پست', + 'post_description' => 'نمایش پست در صفحه', + 'post_slug' => 'آدرس پست', + 'post_slug_description' => "پست توسط آدرس وارد شده جستجو میشود.", + 'post_category' => 'صفحه دسته بندی', + 'post_category_description' => 'نام صفحه ای که لیست دسته بندی ها در آن نمایش داده می شوند. این گزینه به طور پیشفرض توسط ابزار مورد استفاده قرار میگیرد.', + 'posts_title' => 'لیست پست ها', + 'posts_description' => 'نمایش لیستی از پستهایی که اخیرا ارسال شده اند در صفحه.', + 'posts_pagination' => 'شماره صفحه', + 'posts_pagination_description' => 'این مقدار جهت تشخیص صفحه ای که کاربر در آن قرار دارد مورد استفاده قرار میگیرد.', + 'posts_filter' => 'فیلتر دسته بندی', + 'posts_filter_description' => 'آدرس دسته بندی ای را که میخواهید پست های آن نمایش داده شوند را وارد نمایید. اگر میخواهید همه پست ها نمایش داده شوند این مقدار را خالی رها کنید.', + 'posts_per_page' => 'تعداد پست ها در هر صفحه', + 'posts_per_page_validation' => 'مقدار ورودی تعداد پست ها در هر صفحه نامعتبر است.', + 'posts_no_posts' => 'پیغام پستی وجود ندارد', + 'posts_no_posts_description' => 'این پیغام در صورتی که پستی جهت نمایش وجود نداشته باشد، نمایش داده می شود.', + 'posts_order' => 'ترتیب پست ها', + 'posts_order_decription' => 'مشخصه ترتیب نمایش پست ها در صفحه', + 'posts_category' => 'صفحه دسته بندی', + 'posts_category_description' => 'نام صفحه دسته بندی برای نمایش پستهای مربوط به آن.', + 'posts_post' => 'صفحه پست', + 'posts_post_description' => 'نام صفحه مربوط به نمایش کامل پست ها جهت لینک ادامه مطلب', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/fi/lang.php b/plugins/rainlab/blog/lang/fi/lang.php new file mode 100644 index 0000000..284c921 --- /dev/null +++ b/plugins/rainlab/blog/lang/fi/lang.php @@ -0,0 +1,163 @@ + [ + 'name' => 'Blogi', + 'description' => 'Vankka bloggausalusta.' + ], + 'blog' => [ + 'menu_label' => 'Blogi', + 'menu_description' => 'Hallitse blogipostauksia', + 'posts' => 'Postaukset', + 'create_post' => 'Blogipostaus', + 'categories' => 'Categories', + 'create_category' => 'Blogikategoria', + 'tab' => 'Blogi', + 'access_posts' => 'Hallitse postauksia', + 'access_categories' => 'Hallitse kategorioita', + 'access_other_posts' => 'Hallitse muiden käyttäjien postauksia', + 'access_import_export' => 'Saa tuoda ja viedä postauksia', + 'access_publish' => 'Saa julkaista postauksia', + 'manage_settings' => 'Manage blog settings', + 'delete_confirm' => 'Olteko varma?', + 'chart_published' => 'Julkaistu', + 'chart_drafts' => 'Luonnokset', + 'chart_total' => 'Yhteensä', + 'settings_description' => 'Hallinnoi blogin asetuksia', + 'show_all_posts_label' => 'Näytä kaikki postaukset ylläpitäjille', + 'show_all_posts_comment' => 'Näytä molemmat sekä julkaistut että julkaisemattomat postaukset ylläpitäjille', + 'tab_general' => 'Yleiset' + ], + 'posts' => [ + 'list_title' => 'Hallitse blogipostauksia', + 'filter_category' => 'Kategoria', + 'filter_published' => 'Julkaistu', + 'filter_date' => 'Päivämäärä', + 'new_post' => 'Uusi postaus', + 'export_post' => 'Vie postaukset', + 'import_post' => 'Tuo postauksia' + ], + 'post' => [ + 'title' => 'Otsikko', + 'title_placeholder' => 'Uuden postauksen otsikko', + 'content' => 'Sisältö', + 'content_html' => 'HTML Sisältö', + 'slug' => 'Slugi', + 'slug_placeholder' => 'uuden-postaukse-slugi', + 'categories' => 'Kategoriat', + 'author_email' => 'Tekijän sähköposti', + 'created' => 'Luotu', + 'created_date' => 'Luomispäivämäärä', + 'updated' => 'Muokattu', + 'updated_date' => 'Muokkauspäivämäärä', + 'published' => 'Julkaistu', + 'published_by' => 'Published by', + 'current_user' => 'Current user', + 'published_date' => 'Julkaisupäivämäärä', + 'published_validation' => 'Määrittele julkaisupäivämäärä', + 'tab_edit' => 'Muokkaa', + 'tab_categories' => 'Kategoriat', + 'categories_comment' => 'Valitse kategoriat joihin postaus kuuluu', + 'categories_placeholder' => 'Kategorioita ei ole, sinun pitäisi luoda ensimmäinen ensin!', + 'tab_manage' => 'Hallitse', + 'published_on' => 'Julkaistu', + 'excerpt' => 'Poiminto', + 'summary' => 'Yhteenveto', + 'featured_images' => 'Esittelykuvat', + 'delete_confirm' => 'Poista tämä postaus?', + 'delete_success' => 'Postaukset poistettu onnistuneesti.', + 'close_confirm' => 'Tämä postaus ei ole tallennettu.', + 'return_to_posts' => 'Palaa postauslistaan' + ], + 'categories' => [ + 'list_title' => 'Hallitse blogikategorioita', + 'new_category' => 'Uusi kategoria', + 'uncategorized' => 'Luokittelematon' + ], + 'category' => [ + 'name' => 'Nimi', + 'name_placeholder' => 'Uuden kategorian nimi', + 'description' => 'Kuvaus', + 'slug' => 'Slugi', + 'slug_placeholder' => 'uuden-kategorian-slugi', + 'posts' => 'Julkaisuja', + 'delete_confirm' => 'Poista tämä kategoria?', + 'delete_success' => 'Kategoriat poistettu onnistuneesti.', + 'return_to_categories' => 'Palaa blogikategorialistaan', + 'reorder' => 'Järjestä kategoriat uudelleen' + ], + 'menuitem' => [ + 'blog_category' => 'Blogikategoria', + 'all_blog_categories' => 'Kaikki blogikategoriat', + 'blog_post' => 'Blogipostaukset', + 'all_blog_posts' => 'Kaikki blogipostaukset', + 'category_blog_posts' => 'Blogin kategorian postaukset' + ], + 'settings' => [ + 'category_title' => 'Kategorialista', + 'category_description' => 'Näyttää listan blogikategorioista sivulla.', + 'category_slug' => 'Kategorian slugi', + 'category_slug_description' => 'Etsii blogikategorian käyttämällä annettua slugi-arvoa. Komponentti käyttää tätä merkitsemään aktiivisen kategorian.', + 'category_display_empty' => 'Näytä tyhjät kategoriat', + 'category_display_empty_description' => 'Näytä kategoriat joilla ei ole yhtään postauksia.', + 'category_page' => 'Kategoriasivu', + 'category_page_description' => 'Kategorialistaussivun tiedostonimi. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'post_title' => 'Postaus', + 'post_description' => 'Näyttää blogipostauksen sivulla.', + 'post_slug' => 'Postauksen slugi', + 'post_slug_description' => 'Etsii blogipostauksen käyttämällä annettua slugi-arvoa.', + 'post_category' => 'Kategoriasivu', + 'post_category_description' => 'Kategorialistaussivun tiedostonimi. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_title' => 'Lista postauksista', + 'posts_description' => 'Näyttää listan uusimmista blogipostauksista sivulla.', + 'posts_pagination' => 'Sivunumero', + 'posts_pagination_description' => 'Tätä arvoa käytetään määrittämään millä sivulla käyttäjä on.', + 'posts_filter' => 'Kategoriasuodatin', + 'posts_filter_description' => 'Lisää kategorian slugi tai URL parametri, jolla suodattaa postauksia. Jätä tyhjäksi näyttääksesi kaikki postaukset.', + 'posts_per_page' => 'Postauksia per sivu', + 'posts_per_page_validation' => 'Postauksia per sivu -kohta sisältää kelvottoman arvon', + 'posts_no_posts' => 'Ei julkaisuja -viesti', + 'posts_no_posts_description' => 'Viesti, joka näytetään silloin kun postauksia ei ole. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_no_posts_default' => 'Ei postauksia', + 'posts_order' => 'Postauksien järjestys', + 'posts_order_description' => 'Attribuutti, jonka mukaan postaukset tulisi järjestää', + 'posts_category' => 'Kategoriasivu', + 'posts_category_description' => 'Kategoriasivun tiedosto "Julkaistu kohteeseen" kategorialinkkejä varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_post' => 'Postaussivu', + 'posts_post_description' => 'Blogisivun tiedostonimi "Lue lisää" linkkejä varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'posts_except_post' => 'Poissulje postauksia', + 'posts_except_post_description' => 'Lisää postauksen ID/URL tai muuttuja, jonka haluat poissulkea', + 'posts_except_post_validation' => 'Postaukset poikkeukset täytyy olla yksittäinen slugi tai ID, pilkulla erotettu slugi-lista ja ID:t', + 'posts_except_categories' => 'Poikkeavat kategoriat', + 'posts_except_categories_description' => 'Lisää pilkulla erotettu listaus kategoria slugeista tai listaus kategorioista jotka haluat jättää ulkopuolelle', + 'posts_except_categories_validation' => 'Poikkeavat kategoriat ovat oltava yksittäinen kategoria slugi tai pilkulla erotettu listaus slugeista', + 'rssfeed_blog' => 'Blogisivu', + 'rssfeed_blog_description' => 'Blogisivun tiedostonimi linkkien generointia varten. Oletuskomponenttiosa käyttää tätä ominaisuutta.', + 'rssfeed_title' => 'RSS syöte', + 'rssfeed_description' => 'Generoi RSS syötteen sisältäen postaukset blogista.', + 'group_links' => 'Linkit', + 'group_exceptions' => 'Poikkeukset' + ], + 'sorting' => [ + 'title_asc' => 'Otsikko (ascending)', + 'title_desc' => 'Otsikko (descending)', + 'created_asc' => 'Luotu (ascending)', + 'created_desc' => 'Luotu (descending)', + 'updated_asc' => 'Päivitetty (ascending)', + 'updated_desc' => 'Päivitetty (descending)', + 'published_asc' => 'Julkaistu (ascending)', + 'published_desc' => 'Julkaistu (descending)', + 'random' => 'Satunnainen' + ], + 'import' => [ + 'update_existing_label' => 'Päivitä olemassa olevat postaukset', + 'update_existing_comment' => 'Valitse tämä laatikko päivittääksesi postaukset, joissa on täsmälleen sama ID, otsikko tai slugi.', + 'auto_create_categories_label' => 'Luo tuotavassa tiedostossa määritellyt kategoriat.', + 'auto_create_categories_comment' => 'Sinun tulisi yhdistää Kategoriat-sarake käyttääksesi tätä toiminnallisuutta. Muussa tapauksessa valitse oletuskategoria alapuolelta.', + 'categories_label' => 'Kategoriat', + 'categories_comment' => 'Valitse kategoriat, joihin tuotavat postaukset liitetään (option).', + 'default_author_label' => 'Oletuskirjoittaja (optio)', + 'default_author_comment' => 'Tuonti yrittää käyttää Kirjoittaja tiedon sähköpostia yhdistäessään kirjoittajaa. Muussa tapauksessa käytetään ylempänä määriteltyä.', + 'default_author_placeholder' => '-- valitse kirjoittaja --' + ] +]; diff --git a/plugins/rainlab/blog/lang/fr/lang.php b/plugins/rainlab/blog/lang/fr/lang.php new file mode 100644 index 0000000..c2b8600 --- /dev/null +++ b/plugins/rainlab/blog/lang/fr/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Une plateforme de blog robuste.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gestion d’articles de blog', + 'posts' => 'Articles', + 'create_post' => 'article de blog', + 'categories' => 'Catégories', + 'create_category' => 'catégorie d’articles', + 'tab' => 'Blog', + 'access_posts' => 'Gérer les articles', + 'access_categories' => 'Gérer les catégories', + 'access_other_posts' => 'Gérer les articles d’autres utilisateurs', + 'access_import_export' => 'Autorisé à importer et exporter des articles', + 'access_publish' => 'Autorisé à publier des articles', + 'manage_settings' => 'Gérer les paramètres du blog', + 'delete_confirm' => 'Confirmez-vous la suppression des articles sélectionnés ?', + 'chart_published' => 'Publié', + 'chart_drafts' => 'Brouillons', + 'chart_total' => 'Total', + 'settings_description' => 'Gérer les paramètres du blog', + 'show_all_posts_label' => "Afficher tous les messages aux utilisateurs du panneaux d'administration", + 'show_all_posts_comment' => 'Afficher autant les publications publiées et non publiées sur le site web pour les utilisateurs principaux', + 'tab_general' => 'Général' + ], + 'posts' => [ + 'list_title' => 'Gérer les articles du blog', + 'filter_category' => 'Catégorie', + 'filter_published' => 'Masquer la publication', + 'filter_date' => 'Date', + 'new_post' => 'Nouvel article', + 'export_post' => 'Exporter les articles', + 'import_post' => 'Importer des articles' + ], + 'post' => [ + 'title' => 'Titre', + 'title_placeholder' => 'Titre du nouvel article', + 'content' => 'Contenu', + 'content_html' => 'Contenu HTML', + 'slug' => 'Adresse', + 'slug_placeholder' => 'adresse-du-nouvel-article', + 'categories' => 'Catégories', + 'author_email' => 'Email de l’auteur', + 'created' => 'Créé', + 'created_date' => 'Date de création', + 'updated' => 'Mis a jour', + 'updated_date' => 'Date de mise à jour', + 'published' => 'Publié', + 'published_by' => 'Publié par', + 'current_user' => 'Utilisateur actuel', + 'published_date' => 'Date de publication', + 'published_validation' => 'Veuillez préciser la date de publication', + 'tab_edit' => 'Rédaction', + 'tab_categories' => 'Catégories', + 'categories_comment' => 'Sélectionnez les catégories auxquelles l’article est lié', + 'categories_placeholder' => 'Il n’existe pas encore de catégorie, mais vous pouvez en créer une !', + 'tab_manage' => 'Configuration', + 'published_on' => 'Publié le', + 'excerpt' => 'Extrait', + 'summary' => 'Résumé', + 'featured_images' => 'Image de promotion', + 'delete_confirm' => 'Confirmez-vous la suppression de cet article ?', + 'delete_success' => 'Ces articles ont été supprimés avec succès.', + 'close_confirm' => 'L’article n’est pas enregistré.', + 'return_to_posts' => 'Retour à la liste des articles', + 'posted_byline' => 'Posté dans :categories le :date.', + 'posted_byline_no_categories' => 'Posté le :date.', + 'date_format' => 'd M Y' + ], + 'categories' => [ + 'list_title' => 'Gérer les catégories', + 'new_category' => 'Nouvelle catégorie', + 'uncategorized' => 'Non catégorisé' + ], + 'category' => [ + 'name' => 'Nom', + 'name_placeholder' => 'Nom de la nouvelle catégorie', + 'description' => 'Description', + 'slug' => 'Adresse URL', + 'slug_placeholder' => 'adresse-de-la-nouvelle-catégorie', + 'posts' => 'Articles', + 'delete_confirm' => 'Confirmez-vous la suppression de cette catégorie ?', + 'delete_success' => 'Ces catégories ont été supprimés avec succès.', + 'return_to_categories' => 'Retour à la liste des catégories', + 'reorder' => 'Réorganiser les catégories' + ], + 'menuitem' => [ + 'blog_category' => 'Catégories du blog', + 'all_blog_categories' => 'Toutes les catégories du blog', + 'blog_post' => 'Articles du blog', + 'all_blog_posts' => 'Tous les articles du blog', + 'category_blog_posts' => "Articles d'une catégorie du blog" + ], + 'settings' => [ + 'category_title' => 'Liste des catégories', + 'category_description' => 'Afficher une liste des catégories sur la page.', + 'category_slug' => 'Adresse URL de la catégorie', + 'category_slug_description' => 'Adresse URL d’accès à la catégorie. Cette propriété est utilisée par le partial par défaut du composant pour marquer la catégorie courante comme active.', + 'category_display_empty' => 'Afficher les catégories vides.', + 'category_display_empty_description' => 'Afficher les catégories qui ne sont liés à aucun article.', + 'category_page' => 'Page des catégories', + 'category_page_description' => 'Nom de la page des catégories pour les liens de catégories. Cette propriété est utilisée par le partial par défaut du composant.', + 'post_title' => 'Article', + 'post_description' => 'Affiche un article de blog sur la page.', + 'post_slug' => 'Adresse URL de l’article', + 'post_slug_description' => 'Adresse URL d’accès à l’article.', + 'post_category' => 'Page des catégories', + 'post_category_description' => 'Nom de la page des catégories pour les liens de catégories. Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_title' => 'Liste d’articles', + 'posts_description' => 'Affiche une liste des derniers articles de blog sur la page.', + 'posts_pagination' => 'Numéro de page', + 'posts_pagination_description' => 'Cette valeur est utilisée pour déterminer à quelle page l’utilisateur se trouve.', + 'posts_filter' => 'Filtre des catégories', + 'posts_filter_description' => 'Entrez une adresse de catégorie ou un paramètre d’URL pour filter les articles. Laissez vide pour afficher tous les articles.', + 'posts_per_page' => 'Articles par page', + 'posts_per_page_validation' => 'Format du nombre d’articles par page incorrect', + 'posts_no_posts' => 'Message en l’absence d’articles', + 'posts_no_posts_description' => 'Message à afficher dans la liste d’articles lorsqu’il n’y a aucun article. Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_no_posts_default' => 'Aucun article trouvé', + 'posts_order' => 'Ordre des articles', + 'posts_order_description' => 'Attribut selon lequel les articles seront ordonnés', + 'posts_category' => 'Page de catégorie', + 'posts_category_description' => 'Nom du fichier de la page de catégorie pour les liens de catégorie "Publié dans". Cette propriété est utilisée par le composant par défaut du modèle partiel.', + 'posts_post' => "Page de l'article", + 'posts_post_description' => 'Nom du fichier de la page de l\'article du blog pour les liens "En savoir plus". Cette propriété est utilisée par le composant par défaut du modèle partiel.', + 'posts_except_post' => 'Article exempté', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + 'posts_category' => 'Page des catégories', + 'posts_category_description' => 'Nom de la page des catégories pour les liens de catégories "Publié dans". Cette propriété est utilisée par le partial par défaut du composant.', + 'posts_post' => 'Page d’article', + 'posts_post_description' => 'Nom de la page d’articles pour les liens "En savoir plus". Cette propriété est utilisée par le partial par défaut du composant.', + 'rssfeed_blog' => 'Page du blog', + 'rssfeed_blog_description' => 'Nom de la page principale du blog pour générer les liens. Cette propriété est utilisé par le composant dans le partial.', + 'rssfeed_title' => 'Flux RSS', + 'rssfeed_description' => 'Génère un Flux RSS contenant les articles du blog.', + 'group_links' => 'Liens', + 'group_exceptions' => 'Exceptions' + ], + 'sorting' => [ + 'title_asc' => 'Titre (ascendant)', + 'title_desc' => 'Titre (descendant)', + 'created_asc' => 'Création le (ascendant)', + 'created_desc' => 'Création (descendant)', + 'updated_asc' => 'Mise à jour (ascendant)', + 'updated_desc' => 'Mise à jour (descendant)', + 'published_asc' => 'Publication (ascendant)', + 'published_desc' => 'Publication (descendant)', + 'random' => 'Aléatoire' + ], + 'import' => [ + 'update_existing_label' => 'Mettre à jour les articles existants', + 'update_existing_comment' => 'Cochez cette case pour mettre à jour les articles qui ont exactement le même identifiant, titre ou slug.', + 'auto_create_categories_label' => "Créer les catégories spécifiées dans le fichier d'importation", + 'auto_create_categories_comment' => 'Vous devez faire correspondre la colonne Catégories pour utiliser cette fonctionnalité. Sinon, sélectionnez les catégories par défaut à utiliser parmi les éléments ci-dessous.', + 'categories_label' => 'Catégories', + 'categories_comment' => 'Sélectionnez les catégories auxquelles appartiendront les articles importées (facultatif).', + 'default_author_label' => 'Auteur par défaut (facultatif)', + 'default_author_comment' => "L'importation tentera d'utiliser un auteur existant si vous correspondez la colonne Email à l'auteur, sinon l'auteur spécifié ci-dessus sera utilisé.", + 'default_author_placeholder' => "-- sélectionnez l'auteur --" + ] +]; diff --git a/plugins/rainlab/blog/lang/hu/lang.php b/plugins/rainlab/blog/lang/hu/lang.php new file mode 100644 index 0000000..e996f60 --- /dev/null +++ b/plugins/rainlab/blog/lang/hu/lang.php @@ -0,0 +1,166 @@ + [ + 'name' => 'Blog', + 'description' => 'Teljeskörű blog alkalmazás.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog bejegyzések kezelése', + 'posts' => 'Bejegyzések', + 'create_post' => 'blog bejegyzés', + 'categories' => 'Kategóriák', + 'create_category' => 'blog kategória', + 'tab' => 'Blog', + 'access_posts' => 'Blog bejegyzések kezelése', + 'access_categories' => 'Blog kategóriák kezelése', + 'access_other_posts' => 'Más felhasználók bejegyzéseinek kezelése', + 'access_import_export' => 'Bejegyzések importálása és exportálása', + 'access_publish' => 'Blog bejegyzések közzététele', + 'manage_settings' => 'Blog beállítások kezelése', + 'delete_confirm' => 'Törölni akarja a kijelölt bejegyzéseket?', + 'chart_published' => 'Közzétéve', + 'chart_drafts' => 'Piszkozatok', + 'chart_total' => 'Összesen', + 'settings_description' => 'Beállítási lehetőségek.', + 'show_all_posts_label' => 'Az összes bejegyzés mutatása az adminisztrátorok számára', + 'show_all_posts_comment' => 'A közzétett és a még nem publikált bejegyzések is egyaránt meg fognak jelenni az oldal szerkesztőinek.', + 'tab_general' => 'Általános' + ], + 'posts' => [ + 'list_title' => 'Blog bejegyzések', + 'filter_category' => 'Kategória', + 'filter_published' => 'Közzétéve', + 'filter_date' => 'Létrehozva', + 'new_post' => 'Új bejegyzés', + 'export_post' => 'Exportálás', + 'import_post' => 'Importálás' + ], + 'post' => [ + 'title' => 'Cím', + 'title_placeholder' => 'Új bejegyzés címe', + 'content' => 'Szöveges tartalom', + 'content_html' => 'HTML tartalom', + 'slug' => 'Keresőbarát cím', + 'slug_placeholder' => 'uj-bejegyzes-cime', + 'categories' => 'Kategóriák', + 'author_email' => 'Szerző e-mail címe', + 'created' => 'Létrehozva', + 'created_date' => 'Létrehozás dátuma', + 'updated' => 'Módosítva', + 'updated_date' => 'Módosítás dátuma', + 'published' => 'Közzétéve', + 'published_by' => 'Szerző:', + 'current_user' => 'Felhasználó', + 'published_date' => 'Közzététel dátuma', + 'published_validation' => 'Adja meg a közzététel dátumát', + 'tab_edit' => 'Szerkesztés', + 'tab_categories' => 'Kategóriák', + 'categories_comment' => 'Jelölje be azokat a kategóriákat, melyekbe be akarja sorolni a bejegyzést', + 'categories_placeholder' => 'Nincsenek kategóriák, előbb létre kell hoznia egyet!', + 'tab_manage' => 'Kezelés', + 'published_on' => 'Közzététel dátuma', + 'excerpt' => 'Kivonat', + 'summary' => 'Összegzés', + 'featured_images' => 'Kiemelt képek', + 'delete_confirm' => 'Valóban törölni akarja ezt a bejegyzést?', + 'delete_success' => 'Sikeresen törölve lettek a bejegyzések.', + 'close_confirm' => 'A bejegyzés nem került mentésre.', + 'return_to_posts' => 'Vissza a bejegyzésekhez', + 'posted_byline' => 'Publikálva: :date, itt: :categories', + 'posted_byline_no_categories' => 'Publikálva: :date.', + 'date_format' => 'Y.M.d.', + ], + 'categories' => [ + 'list_title' => 'Blog kategóriák', + 'new_category' => 'Új kategória', + 'uncategorized' => 'Nincs kategorizálva' + ], + 'category' => [ + 'name' => 'Név', + 'name_placeholder' => 'Új kategória neve', + 'description' => 'Leírás', + 'slug' => 'Keresőbarát cím', + 'slug_placeholder' => 'uj-kategoria-neve', + 'posts' => 'Bejegyzések', + 'delete_confirm' => 'Valóban törölni akarja ezt a kategóriát?', + 'delete_success' => 'Sikeresen törölve lettek a kategóriák.', + 'return_to_categories' => 'Vissza a kategóriákhoz', + 'reorder' => 'Kategóriák sorrendje' + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategória', + 'all_blog_categories' => 'Összes blog kategória', + 'blog_post' => 'Blog bejegyzés', + 'all_blog_posts' => 'Összes blog bejegyzés', + 'category_blog_posts' => 'Blog kategória bejegyzések' + ], + 'settings' => [ + 'category_title' => 'Blog kategória lista', + 'category_description' => 'A blog kategóriákat listázza ki a lapon.', + 'category_slug' => 'Cím paraméter neve', + 'category_slug_description' => 'A webcím útvonal paramétere a jelenlegi kategória keresőbarát címe alapján való kereséséhez. Az alapértelmezett komponensrész ezt a tulajdonságot használja a jelenleg aktív kategória megjelöléséhez.', + 'category_display_empty' => 'Üres kategóriák kijelzése', + 'category_display_empty_description' => 'Azon kategóriák megjelenítése, melyekben nincs egy bejegyzés sem.', + 'category_page' => 'Kategória lap', + 'category_page_description' => 'A kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'post_title' => 'Blog bejegyzés', + 'post_description' => 'Egy blog bejegyzést jelez ki a lapon.', + 'post_slug' => 'Cím paraméter neve', + 'post_slug_description' => 'A webcím útvonal paramétere a bejegyzés keresőbarát címe alapján való kereséséhez.', + 'post_category' => 'Kategória lap', + 'post_category_description' => 'A kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_title' => 'Blog bejegyzések', + 'posts_description' => 'A közzétett blog bejegyzések listázása a honlapon.', + 'posts_pagination' => 'Lapozósáv paraméter neve', + 'posts_pagination_description' => 'A lapozósáv lapjai által használt, várt paraméter neve.', + 'posts_filter' => 'Kategória szűrő', + 'posts_filter_description' => 'Adja meg egy kategória keresőbarát címét vagy webcím paraméterét a bejegyzések szűréséhez. Hagyja üresen az összes bejegyzés megjelenítéséhez.', + 'posts_per_page' => 'Bejegyzések laponként', + 'posts_per_page_validation' => 'A laponkénti bejegyzések értéke érvénytelen formátumú', + 'posts_no_posts' => 'Üzenet ha nincs bejegyzés', + 'posts_no_posts_description' => 'A blog bejegyzés listában kijelezendő üzenet abban az esetben, ha nincsenek bejegyzések. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_no_posts_default' => 'Nem található bejegyzés', + 'posts_order' => 'Bejegyzések sorrendje', + 'posts_order_description' => 'Jellemző, ami alapján rendezni kell a bejegyzéseket', + 'posts_category' => 'Kategória lap', + 'posts_category_description' => 'A "Kategória" kategória hivatkozások kategória lap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_post' => 'Bejegyzéslap', + 'posts_post_description' => 'A "Tovább olvasom" hivatkozások blog bejegyzéslap fájljának neve. Az alapértelmezett komponensrész használja ezt a tulajdonságot.', + 'posts_except_post' => 'Bejegyzés kizárása', + 'posts_except_post_description' => 'Adja meg annak a bejegyzésnek az azonosítóját vagy webcímét, amit nem akar megjeleníteni a listázáskor.', + 'posts_except_post_validation' => 'A kivételnek webcímnek, illetve azonosítónak, vagy pedig ezeknek a vesszővel elválasztott felsorolásának kell lennie.', + 'posts_except_categories' => 'Kategória kizárása', + 'posts_except_categories_description' => 'Adja meg azoknak a kategóriáknak a webcímét vesszővel elválasztva, amiket nem akar megjeleníteni a listázáskor.', + 'posts_except_categories_validation' => 'A kivételnek webcímnek, vagy pedig ezeknek a vesszővel elválasztott felsorolásának kell lennie.', + 'rssfeed_blog' => 'Blog oldal', + 'rssfeed_blog_description' => 'Annak a lapnak a neve, ahol listázódnak a blog bejegyzések. Ezt a beállítást használja alapértelmezetten a blog komponens is.', + 'rssfeed_title' => 'RSS hírfolyam', + 'rssfeed_description' => 'A bloghoz tartozó RSS hírfolyam generálása.', + 'group_links' => 'Hivatkozások', + 'group_exceptions' => 'Kivételek' + ], + 'sorting' => [ + 'title_asc' => 'Név (növekvő)', + 'title_desc' => 'Név (csökkenő)', + 'created_asc' => 'Létrehozva (növekvő)', + 'created_desc' => 'Létrehozva (csökkenő)', + 'updated_asc' => 'Frissítve (növekvő)', + 'updated_desc' => 'Frissítve (csökkenő)', + 'published_asc' => 'Publikálva (növekvő)', + 'published_desc' => 'Publikálva (csökkenő)', + 'random' => 'Véletlenszerű' + ], + 'import' => [ + 'update_existing_label' => 'Meglévő bejegyzések frissítése', + 'update_existing_comment' => 'Két bejegyzés akkor számít ugyanannak, ha megegyezik az azonosító számuk, a címük vagy a webcímük.', + 'auto_create_categories_label' => 'Az import fájlban megadott kategóriák létrehozása', + 'auto_create_categories_comment' => 'A funkció használatához meg kell felelnie a Kategóriák oszlopnak, különben az alábbi elemekből válassza ki az alapértelmezett kategóriákat.', + 'categories_label' => 'Kategóriák', + 'categories_comment' => 'Válassza ki azokat a kategóriákat, amelyekhez az importált bejegyzések tartoznak (nem kötelező).', + 'default_author_label' => 'Alapértelmezett szerző (nem kötelező)', + 'default_author_comment' => 'A rendszer megpróbál egy meglévő felhasználót társítani a bejegyzéshez az Email oszlop alapján. Amennyiben ez nem sikerül, az itt megadott szerzőt fogja alapul venni.', + 'default_author_placeholder' => '-- válasszon felhasználót --' + ] +]; diff --git a/plugins/rainlab/blog/lang/it/lang.php b/plugins/rainlab/blog/lang/it/lang.php new file mode 100644 index 0000000..0eade81 --- /dev/null +++ b/plugins/rainlab/blog/lang/it/lang.php @@ -0,0 +1,107 @@ + [ + 'name' => 'Blog', + 'description' => 'Una solida piattaforma di blogging.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gestisci i post', + 'posts' => 'Post', + 'create_post' => 'post del blog', + 'categories' => 'Categorie', + 'create_category' => 'categorie del blog', + 'tab' => 'Blog', + 'access_posts' => 'Gestisci i post', + 'access_categories' => 'Gestisci le categorie', + 'access_other_posts' => 'Gestisci i post di altri utenti', + 'access_import_export' => 'Permesso ad importare ed esportare i post', + 'delete_confirm' => 'Sei sicuro?', + 'chart_published' => 'Pubblicato', + 'chart_drafts' => 'Bozze', + 'chart_total' => 'Totale' + ], + 'posts' => [ + 'list_title' => 'Gestisci i post', + 'category' => 'Categoria', + 'hide_published' => 'Nascondi pubblicati', + 'new_post' => 'Nuovo post' + ], + 'post' => [ + 'title' => 'Titolo', + 'title_placeholder' => 'Titolo del nuovo post', + 'content' => 'Contenuto', + 'content_html' => 'Contenuto HTML', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-del-nuovo-post', + 'categories' => 'Categorie', + 'author_email' => 'Email dell\'autore', + 'created' => 'Creato', + 'created_date' => 'Data di creazione', + 'updated' => 'Aggiornato', + 'updated_date' => 'Data di aggiornamento', + 'published' => 'Pubblicato', + 'published_date' => 'Data di pubblicazione', + 'published_validation' => 'Per favore fornisci la data di pubblicazione', + 'tab_edit' => 'Modifica', + 'tab_categories' => 'Categorie', + 'categories_comment' => 'Seleziona le categorie a cui appartiene il post', + 'categories_placeholder' => 'Non ci sono categorie, per iniziare dovresti crearne una!', + 'tab_manage' => 'Gestisci', + 'published_on' => 'Pubblicato il', + 'excerpt' => 'Estratto', + 'summary' => 'Riassunto', + 'featured_images' => 'Immagini in evidenza', + 'delete_confirm' => 'Vuoi veramente cancellare questo post?', + 'close_confirm' => 'Questo post non è salvato.', + 'return_to_posts' => 'Ritorna all\'elenco dei post' + ], + 'categories' => [ + 'list_title' => 'Gestisci le categorie del blog', + 'new_category' => 'Nuova categoria', + 'uncategorized' => 'Non categorizzato' + ], + 'category' => [ + 'name' => 'Nome', + 'name_placeholder' => 'Nome della nuova categoria', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-nuova-categoria', + 'posts' => 'Post', + 'delete_confirm' => 'Vuoi veramente cancellare questa categoria?', + 'return_to_categories' => 'Ritorna all\'elenco delle categorie del blog', + 'reorder' => 'Riordino Categorie' + ], + 'settings' => [ + 'category_title' => 'Elenco Categorie', + 'category_description' => 'Mostra un\'elenco delle categorie del blog sulla pagina.', + 'category_slug' => 'Slug categoria', + 'category_slug_description' => "Cerca la categoria del blog usando lo slug fornito. Questa proprietà è usata dal componente parziale di default per segnare la categoria attualmente usata.", + 'category_display_empty' => 'Mostra categorie vuote', + 'category_display_empty_description' => 'Mostra categorie che non hanno alcun post.', + 'category_page' => 'Pagina delle categorie', + 'category_page_description' => 'Nome del file della pagina delle categorie contenente i link delle categorie. Questa proprietà è usata dal componente parziale di default.', + 'post_title' => 'Post', + 'post_description' => 'Mostra un post sulla pagina.', + 'post_slug' => 'Slug del post', + 'post_slug_description' => "Cerca il post con lo slug fornito.", + 'post_category' => 'Pagina delle categorie', + 'post_category_description' => 'Nome del file della pagina delle categorie contenente i link delle categorie. Questa proprietà è usata dal componente parziale di default.', + 'posts_title' => 'Elenco dei post', + 'posts_description' => 'Mostra un\'elenco degli ultimi post sulla pagina.', + 'posts_pagination' => 'Numero di pagina', + 'posts_pagination_description' => 'Questo valore è usato per determinare su quale pagina è l\'utente.', + 'posts_filter' => 'Filtro delle categorie', + 'posts_filter_description' => 'Inserisci lo slug di una categoria o un parametro dell\'URL con il quale filtrare i post. Lascia vuoto per mostrare tutti i post.', + 'posts_per_page' => 'Post per pagina', + 'posts_per_page_validation' => 'Il valore di post per pagina ha un formato non valido ', + 'posts_no_posts' => 'Messaggio per l\'assenza di post', + 'posts_no_posts_description' => 'Messaggio da mostrare nell\'elenco dei post in caso non ce ne siano. Questa proprietà è usata dal componente parziale di default.', + 'posts_order' => 'Ordine dei post', + 'posts_order_description' => 'Attributo sul quale i post dovrebbero esser ordinati', + 'posts_category' => 'Pagina delle categorie', + 'posts_category_description' => 'Nome del file per la pagina delle categorie per i link "Postato in" alle categorie. Questa proprietà è usata dal componente parziale di default.', + 'posts_post' => 'Pagina del post', + 'posts_post_description' => 'Nome del file per la pagina del post per i link "Scopri di più". Questa proprietà è usata dal componente parziale di default.' + ] +]; diff --git a/plugins/rainlab/blog/lang/ja/lang.php b/plugins/rainlab/blog/lang/ja/lang.php new file mode 100644 index 0000000..be6d0b4 --- /dev/null +++ b/plugins/rainlab/blog/lang/ja/lang.php @@ -0,0 +1,100 @@ + [ + 'name' => 'ブログ', + 'description' => 'ロバストなブログプラットフォームです。' + ], + 'blog' => [ + 'menu_label' => 'ブログ', + 'menu_description' => 'ブログの投稿管理', + 'posts' => '投稿', + 'create_post' => '投稿の追加', + 'categories' => 'カテゴリ', + 'create_category' => 'カテゴリの追加', + 'tab' => 'ブログ', + 'access_posts' => '投稿の管理', + 'access_categories' => 'カテゴリの管理', + 'access_other_posts' => '他ユーザーの投稿の管理', + 'delete_confirm' => '削除していいですか?', + 'chart_published' => '公開済み', + 'chart_drafts' => '下書き', + 'chart_total' => '合計' + ], + 'posts' => [ + 'list_title' => '投稿の管理', + 'filter_category' => 'カテゴリ', + 'filter_published' => '下書きのみ', + 'new_post' => '投稿を追加' + ], + 'post' => [ + 'title' => 'タイトル', + 'title_placeholder' => 'タイトルを入力してください', + 'slug' => 'スラッグ', + 'slug_placeholder' => 'new-post-slug−123', + 'categories' => 'カテゴリ', + 'created' => '作成日', + 'updated' => '更新日', + 'published' => '公開する', + 'published_validation' => '投稿の公開日を指定してください。', + 'tab_edit' => '編集', + 'tab_categories' => 'カテゴリ', + 'categories_comment' => '投稿を関連付けるカテゴリを選択してください。(複数選択可)', + 'categories_placeholder' => 'まだカテゴリがありません。先に作成してください。', + 'tab_manage' => '管理', + 'published_on' => '公開日', + 'excerpt' => '投稿の抜粋', + 'featured_images' => 'アイキャッチ画像', + 'delete_confirm' => '削除していいですか?', + 'close_confirm' => '投稿は保存されていません。', + 'return_to_posts' => '投稿一覧に戻る' + ], + 'categories' => [ + 'list_title' => 'カテゴリ管理', + 'new_category' => 'カテゴリの追加', + 'uncategorized' => '未分類' + ], + 'category' => [ + 'name' => '名前', + 'name_placeholder' => 'カテゴリ名をつけてください', + 'slug' => 'スラッグ', + 'slug_placeholder' => 'new-category-slug-123', + 'posts' => '投稿数', + 'delete_confirm' => '削除していいですか?', + 'return_to_categories' => 'カテゴリ一覧に戻る' + ], + 'settings' => [ + 'category_title' => 'カテゴリリスト', + 'category_description' => 'ページ内にカテゴリリストを表示します。', + 'category_slug' => 'カテゴリスラッグ', + 'category_slug_description' => "表示するカテゴリのスラッグを指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。", + 'category_display_empty' => '空のカテゴリの表示', + 'category_display_empty_description' => 'この項目がチェックされている場合、投稿が0件のカテゴリもリストに表示します。', + 'category_page' => 'カテゴリページ', + 'category_page_description' => 'カテゴリページへのリンクを生成するために、カテゴリページのファイル名を指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'post_title' => '投稿', + 'post_description' => 'ページ内に投稿を表示します。', + 'post_slug' => '投稿スラッグ', + 'post_slug_description' => "表示する投稿のスラッグを指定します。特定の投稿のスラッグか、URLパラメータ(:slug)を指定できます。", + 'post_category' => 'カテゴリページ', + 'post_category_description' => 'カテゴリリンクを生成するために、カテゴリページのファイル名を指定します。拡張子(.htm)は省いてください。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_title' => '投稿リスト', + 'posts_description' => 'ページ内に新しい投稿のリストを表示します。', + 'posts_pagination' => 'ページ番号', + 'posts_pagination_description' => 'ページ番号を指定します。URLパラメータ(:page)を指定できます。', + 'posts_filter' => 'カテゴリフィルタ', + 'posts_filter_description' => '投稿リストのフィルタを指定します。カテゴリのスラッグかURLパラメータ(:slug)を指定できます。空の場合、すべての投稿が表示されます。', + 'posts_per_page' => '1ページに表示する投稿数を指定します。', + 'posts_per_page_validation' => '1ページに表示する投稿数の形式が正しくありません。', + 'posts_no_posts' => '0件時メッセージ', + 'posts_no_posts_description' => 'この投稿リストが0件の場合に表示するメッセージを指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_order' => '並び順', + 'posts_order_description' => '投稿リスト内の並び順を指定します。', + 'posts_category' => 'カテゴリページ', + 'posts_category_description' => 'カテゴリリンクを生成するために、カテゴリページのファイル名を指定します。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_post' => '投稿ページ', + 'posts_post_description' => '"Learn more"リンクを生成するため、投稿ページのファイル名を指定します。拡張子(.htm)は省いてください。この項目はコンポーネントのデフォルトパーシャルで使用されます。', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/nb-no/lang.php b/plugins/rainlab/blog/lang/nb-no/lang.php new file mode 100644 index 0000000..d033632 --- /dev/null +++ b/plugins/rainlab/blog/lang/nb-no/lang.php @@ -0,0 +1,99 @@ + [ + 'name' => 'Blogg', + 'description' => 'En robust bloggeplattform.', + ], + 'blog' => [ + 'menu_label' => 'Blogg', + 'menu_description' => 'Administrer blogginnlegg', + 'posts' => 'Innlegg', + 'create_post' => 'innlegg', + 'categories' => 'Kategorier', + 'create_category' => 'kategori', + 'access_posts' => 'Administrer blogginnleggene', + 'access_categories' => 'Administrer bloggkategorier', + 'access_other_posts' => 'Administrere andre brukere sine blogginnlegg', + 'delete_confirm' => 'Er du sikker?', + 'chart_published' => 'Publisert', + 'chart_drafts' => 'Utkast', + 'chart_total' => 'Totalt', + ], + 'posts' => [ + 'list_title' => 'Administrer blogginnlegg', + 'filter_category' => 'Kategori', + 'filter_published' => 'Skjul publiserte', + 'new_post' => 'Nytt innlegg', + ], + 'post' => [ + 'title' => 'Tittel', + 'title_placeholder' => 'Innleggets tittel', + 'slug' => 'Slug', + 'slug_placeholder' => 'innleggets-tittel', + 'categories' => 'Kategorier', + 'created' => 'Opprettet', + 'updated' => 'Oppdatert', + 'published' => 'Publisert', + 'published_validation' => 'Velg en dato når innlegget skal publiseres', + 'tab_edit' => 'Endre', + 'tab_categories' => 'Kategorier', + 'categories_comment' => 'Velg hvilke kategorier innlegget tilhører', + 'categories_placeholder' => 'Det finnes ingen kategorier! Vennligst opprett en først.', + 'tab_manage' => 'Egenskaper', + 'published_on' => 'Publiseringsdato', + 'excerpt' => 'Utdrag', + 'featured_images' => 'Utvalgte bilder', + 'delete_confirm' => 'Vil du virkelig slette dette innlegget?', + 'close_confirm' => 'Innlegget er ikke lagret.', + 'return_to_posts' => 'Tilbake til innleggsliste', + ], + 'categories' => [ + 'list_title' => 'Administrer bloggkategorier', + 'new_category' => 'Ny kategori', + 'uncategorized' => 'Uten kategori', + ], + 'category' => [ + 'name' => 'Navn', + 'name_placeholder' => 'Kategoriens navn', + 'slug' => 'Slug', + 'slug_placeholder' => 'kategoriens-navn', + 'posts' => 'Innlegg', + 'delete_confirm' => 'Vil du virkelig slette denne kategorien?', + 'return_to_categories' => 'Tilbake til kategorilisten', + ], + 'settings' => [ + 'category_title' => 'Category List', + 'category_description' => 'Displays a list of blog categories on the page.', + 'category_slug' => 'Category slug', + 'category_slug_description' => "Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.", + 'category_display_empty' => 'Display empty categories', + 'category_display_empty_description' => 'Show categories that do not have any posts.', + 'category_page' => 'Category page', + 'category_page_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'post_title' => 'Post', + 'post_description' => 'Displays a blog post on the page.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Look up the blog post using the supplied slug value.", + 'post_category' => 'Category page', + 'post_category_description' => 'Name of the category page file for the category links. This property is used by the default component partial.', + 'posts_title' => 'Post List', + 'posts_description' => 'Displays a list of latest blog posts on the page.', + 'posts_pagination' => 'Page number', + 'posts_pagination_description' => 'This value is used to determine what page the user is on.', + 'posts_filter' => 'Category filter', + 'posts_filter_description' => 'Enter a category slug or URL parameter to filter the posts by. Leave empty to show all posts.', + 'posts_per_page' => 'Posts per page', + 'posts_per_page_validation' => 'Invalid format of the posts per page value', + 'posts_no_posts' => 'No posts message', + 'posts_no_posts_description' => 'Message to display in the blog post list in case if there are no posts. This property is used by the default component partial.', + 'posts_order' => 'Post order', + 'posts_order_description' => 'Attribute on which the posts should be ordered', + 'posts_category' => 'Category page', + 'posts_category_description' => 'Name of the category page file for the "Posted into" category links. This property is used by the default component partial.', + 'posts_post' => 'Post page', + 'posts_post_description' => 'Name of the blog post page file for the "Learn more" links. This property is used by the default component partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ], +]; diff --git a/plugins/rainlab/blog/lang/nl/lang.php b/plugins/rainlab/blog/lang/nl/lang.php new file mode 100644 index 0000000..1d5b782 --- /dev/null +++ b/plugins/rainlab/blog/lang/nl/lang.php @@ -0,0 +1,113 @@ + [ + 'name' => 'Blog', + 'description' => 'A robust blogging platform.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Beheer blog artikelen', + 'posts' => 'Artikelen', + 'create_post' => 'Artikel', + 'categories' => 'Categorieën', + 'create_category' => 'blog categorie', + 'tab' => 'Blog', + 'access_posts' => 'Blog artikelen beheren', + 'access_categories' => 'Blog categorieën beheren', + 'access_other_posts' => 'Beheren van blog artikelen van gebruikers', + 'access_import_export' => 'Toegang tot importeren en exporteren van artikelen', + 'delete_confirm' => 'Weet je het zeker?', + 'chart_published' => 'Gepubliceerd', + 'chart_drafts' => 'Concepten', + 'chart_total' => 'Totaal' + ], + 'posts' => [ + 'list_title' => 'Beheren van blog artikelen', + 'filter_category' => 'Categorie', + 'filter_published' => 'Verberg gepubliceerd', + 'new_post' => 'Nieuw artikel' + ], + 'post' => [ + 'title' => 'Titel', + 'title_placeholder' => 'Titel van artikel', + 'content' => 'Inhoud', + 'content_html' => 'HTML Inhoud', + 'slug' => 'Slug', + 'slug_placeholder' => 'nieuw-artikel-slug', + 'categories' => 'Categorieën', + 'author_email' => 'E-mail auteur', + 'created' => 'Aangemaakt', + 'created_date' => 'Aangemaakt op', + 'updated' => 'Bijgewerkt', + 'updated_date' => 'Bijgewerkt op', + 'published' => 'Gepubliceerd', + 'published_by' => 'Gepubliceerd door', + 'current_user' => 'Huidige gebruiker', + 'published_date' => 'Gepubliceerd op', + 'published_validation' => 'Graag een publicatie datum opgeven', + 'tab_edit' => 'Bewerken', + 'tab_categories' => 'Categorieën', + 'categories_comment' => 'Selecteer een categorie waarbij het artikel hoort', + 'categories_placeholder' => 'Er zijn geen categorieën, maak eerst een categorie aan!', + 'tab_manage' => 'Beheer', + 'published_on' => 'Gepubliceerd op', + 'excerpt' => 'Samenvatting', + 'summary' => 'Samenvatting', + 'featured_images' => 'Uitgelichte afbeelding', + 'delete_confirm' => 'Weet je zeker dat je dit artikel wilt verwijderen?', + 'close_confirm' => 'Artikel is nog niet opgeslagen.', + 'return_to_posts' => 'Terug naar artikel overzicht', + 'posted_byline' => 'Gepubliceerd in :categories op :date.', + 'posted_byline_no_categories' => 'Gepubliceerd op :date.', + 'date_format' => 'd, M, Y', + ], + 'categories' => [ + 'list_title' => 'Beheer blog categorieën', + 'new_category' => 'Nieuwe categorie', + 'uncategorized' => 'Ongecategoriseerd' + ], + 'category' => [ + 'name' => 'Naam', + 'name_placeholder' => 'Naam categorie', + 'slug' => 'Slug', + 'slug_placeholder' => 'nieuw-categorie-slug', + 'posts' => 'Artikelen', + 'delete_confirm' => 'Weet je zeker dat je deze categorie wilt verwijderen?', + 'return_to_categories' => 'Terug naar categorie overzicht' + ], + 'settings' => [ + 'category_title' => 'Categorie overzicht', + 'category_description' => 'Geeft een lijst weer van alle categorieën op de pagina.', + 'category_slug' => 'Categorie slug', + 'category_slug_description' => 'Haal blog categorie op a.h.v. de opgegeven slug. Deze waarde wordt standaard gebruikt voor de partial om de actieve categorie te markeren.', + 'category_display_empty' => 'Geef lege categorieën weer', + 'category_display_empty_description' => 'Geef categorieën weer die geen artikelen hebben.', + 'category_page' => 'Categorie pagina', + 'category_page_description' => 'Naam van categorie pagina bestand voor de categorie links. Deze waarde wordt standaard gebruikt door de partial.', + 'post_title' => 'Artikel', + 'post_description' => 'Geef een artikel weer op de pagina.', + 'post_slug' => 'Artikel slug', + 'post_slug_description' => 'Haal een artikel op a.h.v. de opgegeven slug.', + 'post_category' => 'Categorie pagina', + 'post_category_description' => 'Naam van categorie pagina bestand voor de categorie links. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_title' => 'Artikel overzicht', + 'posts_description' => 'Geeft een lijst van de nieuwste artikelen weer op de pagina.', + 'posts_pagination' => 'Pagina nummer', + 'posts_pagination_description' => 'Deze waarde wordt gebruikt om te kijken op welke pagina de gebruiker is.', + 'posts_filter' => 'Categorie filter', + 'posts_filter_description' => 'Geef een categorie slug of URL param om de artikelen hier op te kunnen filteren. Leeg laten als alle artikelen getoond moeten worden.', + 'posts_per_page' => 'Artikelen per pagina', + 'posts_per_page_validation' => 'Ongeldig formaat voor het aantal artikelen per pagina', + 'posts_no_posts' => 'Geen artikelen melding', + 'posts_no_posts_description' => 'Deze tekst wordt getoond als er geen artikelen zijn. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_order' => 'Volgorde artikelen', + 'posts_order_description' => 'Kolom waar de artikelen op gesorteerd moeten worden', + 'posts_category' => 'Categorie pagina', + 'posts_category_description' => 'Naam van categorie pagina bestand voor gekoppeld artikel overzichts pagina. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_post' => 'Artikel pagina', + 'posts_post_description' => 'Naam van blog pagina bestand voor de "Lees meer" links. Deze waarde wordt standaard gebruikt door de partial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + ] +]; diff --git a/plugins/rainlab/blog/lang/pl/lang.php b/plugins/rainlab/blog/lang/pl/lang.php new file mode 100644 index 0000000..6954898 --- /dev/null +++ b/plugins/rainlab/blog/lang/pl/lang.php @@ -0,0 +1,159 @@ + [ + 'name' => 'Blog', + 'description' => 'Solidna platforma blogera', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Zarządzaj postami na blogu', + 'posts' => 'Posty', + 'create_post' => 'Utwórz post', + 'categories' => 'Kategorie', + 'create_category' => 'Utwórz kategorię', + 'tab' => 'Blog', + 'access_posts' => 'Zarządzaj postami', + 'access_categories' => 'Zarządzaj kategoriami na blogu', + 'access_other_posts' => 'Zarządzaj postami innych użytkowników', + 'access_import_export' => 'Zarządzaj importowaniem i eksportowaniem postów', + 'access_publish' => 'Publikuj posty', + 'manage_settings' => 'Zarządzaj ustawieniami bloga', + 'delete_confirm' => 'Czy jesteś pewien?', + 'chart_published' => 'Opublikowane', + 'chart_drafts' => 'Szkice', + 'chart_total' => 'Łącznie', + 'settings_description' => 'Zarządzaj ustawieniami bloga', + 'show_all_posts_label' => 'Pokaż wszystkie posty użytkownikom backendu', + 'show_all_posts_comment' => 'Wyświetl opublikowane i nieopublikowany posty na stronie dla użytkowników backendu', + 'tab_general' => 'Ogólne', + ], + 'posts' => [ + 'list_title' => 'Zarządzaj postami', + 'filter_category' => 'Kategoria', + 'filter_published' => 'Ukryj opublikowane', + 'filter_date' => 'Date', + 'new_post' => 'Nowy post', + 'export_post' => 'Eksportuj posty', + 'import_post' => 'Importuj posty', + ], + 'post' => [ + 'title' => 'Tytuł', + 'title_placeholder' => 'Tytuł nowego posta', + 'content' => 'Zawartość', + 'content_html' => 'Zawartość HTML', + 'slug' => 'Alias', + 'slug_placeholder' => 'alias-nowego-postu', + 'categories' => 'Kategorie', + 'author_email' => 'Email autora', + 'created' => 'Utworzony', + 'created_date' => 'Data utworzenia', + 'updated' => 'Zaktualizowany', + 'updated_date' => 'Data aktualizacji', + 'published' => 'Opublikowany', + 'published_date' => 'Data publikacji', + 'published_validation' => 'Proszę określić datę publikacji', + 'tab_edit' => 'Edytuj', + 'tab_categories' => 'Kategorie', + 'categories_comment' => 'Wybierz kategorie do których post należy', + 'categories_placeholder' => 'Nie ma żadnej kategorii, powinieneś utworzyć przynajmniej jedną.', + 'tab_manage' => 'Zarządzaj', + 'published_on' => 'Opublikowane', + 'excerpt' => 'Zalążek', + 'summary' => 'Summary', + 'featured_images' => 'Załączone grafiki', + 'delete_confirm' => 'Czy naprawdę chcesz usunąć ten post?', + 'delete_success' => 'Posty zostały pomyślnie usunięte.', + 'close_confirm' => 'Ten post nie jest zapisany.', + 'return_to_posts' => 'Wróć do listy postów', + ], + 'categories' => [ + 'list_title' => 'Zarządzaj kategoriami postów', + 'new_category' => 'Nowa kategoria', + 'uncategorized' => 'Bez kategorii', + ], + 'category' => [ + 'name' => 'Nazwa', + 'name_placeholder' => 'Nazwa nowej kategorii', + 'description' => 'Opis', + 'slug' => 'Alias', + 'slug_placeholder' => 'alias-nowej-kategorii', + 'posts' => 'Posty', + 'delete_confirm' => 'Czy naprawdę chcesz usunąć tę kategorię?', + 'delete_success' => 'Kategorie zostały pomyślnie usunięte.', + 'return_to_categories' => 'Wróć do listy kategorii', + 'reorder' => 'Zmień kolejnośc kategorii', + ], + 'menuitem' => [ + 'blog_category' => 'Kategorie', + 'all_blog_categories' => 'Wszystkie kategorie', + 'blog_post' => 'Post na bloga', + 'all_blog_posts' => 'Wszystkie posty', + 'category_blog_posts' => 'Posty w kategorii', + ], + 'settings' => [ + 'category_title' => 'Lista kategorii', + 'category_description' => 'Wyświetla listę blogowych kategorii na stronie.', + 'category_slug' => 'Alias kategorii', + 'category_slug_description' => 'Look up the blog category using the supplied slug value. This property is used by the default component partial for marking the currently active category.', + 'category_display_empty' => 'Pokaż puste kategorie', + 'category_display_empty_description' => 'Pokazuje kategorie, które nie posiadają postów', + 'category_page' => 'Strona kategorii', + 'category_page_description' => 'Nazwa strony kategorii gdzie są pokazywane linki. Ten parametr jest domyślnie używany przez komponent.', + 'post_title' => 'Post', + 'post_description' => 'Wyświetla pojedynczy post na stronie.', + 'post_slug' => 'Alias postu', + 'post_slug_description' => 'Szuka post po nazwie aliasu.', + 'post_category' => 'Strona kategorii', + 'post_category_description' => 'Nazwa strony kategorii gdzie są pokazywane linki. Ten parametr jest domyślnie używany przez komponent.', + 'posts_title' => 'Lista postów', + 'posts_description' => 'Wyświetla kilka ostatnich postów.', + 'posts_pagination' => 'Numer strony', + 'posts_pagination_description' => 'Ta wartość odpowiada za odczytanie numeru strony.', + 'posts_filter' => 'Filtr kategorii', + 'posts_filter_description' => 'Wprowadź alias kategorii lub adres URL aby filtrować posty. Pozostaw puste aby pokazać wszystkie.', + 'posts_per_page' => 'Ilość postów na strone', + 'posts_per_page_validation' => 'Nieprawidłowa wartość ilości postów na strone', + 'posts_no_posts' => 'Komunikat o braku postów', + 'posts_no_posts_description' => 'Wiadomość, która ukaże się kiedy komponent nie odnajdzie postów. Ten parametr jest domyślnie używany przez komponent.', + 'posts_no_posts_default' => 'Nie znaleziono postów', + 'posts_order' => 'Kolejność postów', + 'posts_order_description' => 'Parametr przez który mają być sortowane posty', + 'posts_category' => 'Strona kategorii', + 'posts_category_description' => 'Nazwa strony kategorii w wyświetlaniu linków "Posted into" [Opublikowano w]. Ten parametr jest domyślnie używany przez komponent.', + 'posts_post' => 'Strona postu', + 'posts_post_description' => 'Nazwa strony postu dla linków "Learn more" [Czytaj więcej]. Ten parametr jest domyślnie używany przez komponent.', + 'posts_except_post' => 'Wyklucz posty', + 'posts_except_post_description' => 'Wprowadź ID/URL lub zmienną z ID/URL postu, który chcesz wykluczyć', + 'posts_except_post_validation' => 'Wartość pola wykluczenia postów musi być pojedynczym ID/aliasem lub listą ID/aliasów rozdzieloną przecinkami', + 'posts_except_categories' => 'Wyklucz kategorie', + 'posts_except_categories_description' => 'Wprowadź listę aliasów kategorii rozdzieloną przecinkami lub zmienną zawierającą taką listę', + 'posts_except_categories_validation' => 'Wartośc pola wykluczenia kategorii musi być pojedynczym aliasem lub listą aliasów rozdzielonych przecinkami', + 'rssfeed_blog' => 'Strona bloga', + 'rssfeed_blog_description' => 'Nazwa strony głównej bloga do generowania linków. Używane przez domyślny fragment komponentu.', + 'rssfeed_title' => 'Kanał RSS', + 'rssfeed_description' => 'Generuje kanał RSS zawierający posty z bloga.', + 'group_links' => 'Linki', + 'group_exceptions' => 'Wyjątki', + ], + 'sorting' => [ + 'title_asc' => 'Tytuł (rosnąco)', + 'title_desc' => 'Tytuł (malejąco)', + 'created_asc' => 'Data utworzenia (rosnąco)', + 'created_desc' => 'Data utworzenia (malejąco)', + 'updated_asc' => 'Data aktualizacji (rosnąco)', + 'updated_desc' => 'Data aktualizacji (malejąco)', + 'published_asc' => 'Data publikacji (rosnąco)', + 'published_desc' => 'Data publikacji (malejąco)', + 'random' => 'Losowo', + ], + 'import' => [ + 'update_existing_label' => 'Aktualizuj istniejące wpisy', + 'update_existing_comment' => 'Zaznacz to pole, aby zaktualizować posty, które mają taki sam identyfikator (ID), tytuł lub alias.', + 'auto_create_categories_label' => 'Utwórz kategorie podane w pliku', + 'auto_create_categories_comment' => 'Aby skorzystać z tej funkcji powinieneś dopasować kolumnę Kategorii. W przeciwnym wypadku wybierz domyślną kategorię do użycia poniżej.', + 'categories_label' => 'Kategorie', + 'categories_comment' => 'Wybierz kategorię, do której będą należeć zaimportowane posty (opcjonalne).', + 'default_author_label' => 'Domyślny autor postów (opcjonalne)', + 'default_author_comment' => 'Import spróbuje dopasować istniejącego autora na podstawie kolumny email. W przypadku niepowodzenia zostanie użyty autor wybrany powyżej.', + 'default_author_placeholder' => '-- wybierz autora --', + ], +]; diff --git a/plugins/rainlab/blog/lang/pt-br/lang.php b/plugins/rainlab/blog/lang/pt-br/lang.php new file mode 100644 index 0000000..98037ce --- /dev/null +++ b/plugins/rainlab/blog/lang/pt-br/lang.php @@ -0,0 +1,124 @@ + [ + 'name' => 'Blog', + 'description' => 'A plataforma de blogs robusta.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Gerencie os posts do blog', + 'posts' => 'Posts', + 'create_post' => 'Blog post', + 'categories' => 'Categorias', + 'create_category' => 'Blog categoria', + 'tab' => 'Blog', + 'access_posts' => 'Gerencie os posts do blog', + 'access_categories' => 'Gerenciar as categorias de blog', + 'access_other_posts' => 'Gerencie outros posts de usuários do blog', + 'access_import_export' => 'Permissão para importação e exportação de mensagens', + 'access_publish' => 'Permitido publicar posts', + 'delete_confirm' => 'Você tem certeza?', + 'chart_published' => 'Publicados', + 'chart_drafts' => 'Rascunhos', + 'chart_total' => 'Total' + ], + 'posts' => [ + 'list_title' => 'Gerencie os posts do blog', + 'filter_category' => 'Categoria', + 'filter_published' => 'Esconder publicados', + 'filter_date' => 'Data', + 'new_post' => 'Novo post', + 'export_post' => 'Exportar posts', + 'import_post' => 'Importar posts' + ], + 'post' => [ + 'title' => 'Título', + 'title_placeholder' => 'Novo título do post', + 'content' => 'Conteúdo', + 'content_html' => 'HTML Conteúdo', + 'slug' => 'Slug', + 'slug_placeholder' => 'slug-do-post', + 'categories' => 'Categorias', + 'author_email' => 'Autor Email', + 'created' => 'Criado', + 'created_date' => 'Data de criação', + 'updated' => 'Atualizado', + 'updated_date' => 'Data de atualização', + 'published' => 'Publicado', + 'published_date' => 'Data de publicação', + 'published_validation' => 'Por favor, especifique a data de publicação', + 'tab_edit' => 'Editar', + 'tab_categories' => 'Categorias', + 'categories_comment' => 'Selecione as categorias do blog que o post pertence.', + 'categories_placeholder' => 'Não há categorias, você deve criar um primeiro!', + 'tab_manage' => 'Gerenciar', + 'published_on' => 'Publicado em', + 'excerpt' => 'Resumo', + 'summary' => 'Resumo', + 'featured_images' => 'Imagens destacadas', + 'delete_confirm' => 'Você realmente deseja excluir este post?', + 'close_confirm' => 'O post não foi salvo.', + 'return_to_posts' => 'Voltar à lista de posts' + ], + 'categories' => [ + 'list_title' => 'Gerenciar as categorias do blog', + 'new_category' => 'Nova categoria', + 'uncategorized' => 'Sem categoria' + ], + 'category' => [ + 'name' => 'Nome', + 'name_placeholder' => 'Novo nome para a categoria', + 'description' => 'Descrição', + 'slug' => 'Slug', + 'slug_placeholder' => 'novo-slug-da-categoria', + 'posts' => 'Posts', + 'delete_confirm' => 'Você realmente quer apagar esta categoria?', + 'return_to_categories' => 'Voltar para a lista de categorias do blog', + 'reorder' => 'Reordenar Categorias' + ], + 'menuitem' => [ + 'blog_category' => 'Blog categoria', + 'all_blog_categories' => 'Todas as categorias de blog', + 'blog_post' => 'Blog post', + 'all_blog_posts' => 'Todas as postagens do blog' + ], + 'settings' => [ + 'category_title' => 'Lista de categoria', + 'category_description' => 'Exibe uma lista de categorias de blog na página.', + 'category_slug' => 'Slug da categoria', + 'category_slug_description' => "Olhe para cima, a categoria do blog já está usando o valor fornecido! Esta propriedade é usada pelo componente default parcial para a marcação da categoria atualmente ativa.", + 'category_display_empty' => 'xibir categorias vazias', + 'category_display_empty_description' => 'Mostrar categorias que não tem nenhum post.', + 'category_page' => 'Página da categoria', + 'category_page_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'post_title' => 'Post', + 'post_description' => 'Exibe um post na página.', + 'post_slug' => 'Post slug', + 'post_slug_description' => "Procure o post do blog usando o valor do slug fornecido.", + 'post_category' => 'Página da categoria', + 'post_category_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'posts_title' => 'Lista de posts', + 'posts_description' => 'Exibe uma lista de últimas postagens na página.', + 'posts_pagination' => 'Número da pagina', + 'posts_pagination_description' => 'Esse valor é usado para determinar qual página o usuário está.', + 'posts_filter' => 'Filtro de categoria', + 'posts_filter_description' => 'Digite um slug de categoria ou parâmetro de URL para filtrar as mensagens. Deixe em branco para mostrar todas as mensagens.', + 'posts_per_page' => 'Posts por página', + 'posts_per_page_validation' => 'Formato inválido das mensagens por valor de página', + 'posts_no_posts' => 'Nenhuma mensagem de posts', + 'posts_no_posts_description' => 'Mensagem para exibir na lista post no caso, se não há mensagens. Esta propriedade é usada pelo componente default parcial.', + 'posts_order' => 'Orde posts', + 'posts_order_decription' => 'Atributo em que as mensagens devem ser ordenados', + 'posts_category' => 'Página de Categoria', + 'posts_category_description' => 'Nome do arquivo de página da categoria para os links de categoria. Esta propriedade é usada pelo componente default parcial.', + 'posts_post' => 'Página de posts', + 'posts_post_description' => 'Nome do arquivo post página para os "Saiba mais" links. Esta propriedade é usada pelo componente default parcial.', + 'posts_except_post' => 'Except post', + 'posts_except_post_description' => 'Enter ID/URL or variable with post ID/URL you want to except', + 'rssfeed_blog' => 'Página do Blog', + 'rssfeed_blog_description' => 'Nome do arquivo principal da página do blog para geração de links. Essa propriedade é usada pelo componente padrão parcial.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Gera um feed RSS que contém posts do blog.' + ] +]; diff --git a/plugins/rainlab/blog/lang/ru/lang.php b/plugins/rainlab/blog/lang/ru/lang.php new file mode 100644 index 0000000..4a8be1b --- /dev/null +++ b/plugins/rainlab/blog/lang/ru/lang.php @@ -0,0 +1,159 @@ + [ + 'name' => 'Блог', + 'description' => 'Надежная блоговая-платформа.' + ], + 'blog' => [ + 'menu_label' => 'Блог', + 'menu_description' => 'Управление Блогом', + 'posts' => 'Записи', + 'create_post' => 'записи', + 'categories' => 'Категории', + 'create_category' => 'категории', + 'tab' => 'Блог', + 'access_posts' => 'Управление записями блога', + 'access_categories' => 'Управление категориями блога', + 'access_other_posts' => 'Управление записями других пользователей', + 'access_import_export' => 'Разрешено импортировать и экспортировать записи', + 'access_publish' => 'Разрешено публиковать записи', + 'manage_settings' => 'Управление настройками блога', + 'delete_confirm' => 'Вы уверены, что хотите сделать это?', + 'chart_published' => 'Опубликовано', + 'chart_drafts' => 'Черновики', + 'chart_total' => 'Всего', + 'settings_description' => 'Управление настройками блога', + 'show_all_posts_label' => 'Показывать все записи для внутренних (бэкенд) пользователей', + 'show_all_posts_comment' => 'Показывать опубликованные и неопубликованные записи на фронтенде для внутренних (бэкенд) пользователей', + 'tab_general' => 'Основные' + ], + 'posts' => [ + 'list_title' => 'Управление записями блога', + 'filter_category' => 'Категория', + 'filter_published' => 'Скрыть опубликованные', + 'filter_date' => 'Дата', + 'new_post' => 'Новая запись', + 'export_post' => 'Экспорт записей', + 'import_post' => 'Импорт записей' + ], + 'post' => [ + 'title' => 'Заголовок', + 'title_placeholder' => 'Новый заголовок записи', + 'content' => 'Контент', + 'content_html' => 'HTML Контент', + 'slug' => 'URL записи', + 'slug_placeholder' => 'new-post-slug', + 'categories' => 'Категории', + 'author_email' => 'Email автора', + 'created' => 'Создано', + 'created_date' => 'Дата создания', + 'updated' => 'Обновлено', + 'updated_date' => 'Дата обновления', + 'published' => 'Опубликовано', + 'published_date' => 'Дата публикации', + 'published_validation' => 'Пожалуйста, укажите дату публикации.', + 'tab_edit' => 'Редактор', + 'tab_categories' => 'Категории', + 'categories_comment' => 'Выберите категории, к которым относится эта запись', + 'categories_placeholder' => 'Не найдено ни одной категории, создайте хотя бы одну!', + 'tab_manage' => 'Управление', + 'published_on' => 'Опубликовано', + 'excerpt' => 'Отрывок', + 'summary' => 'Резюме', + 'featured_images' => 'Тематические изображения', + 'delete_confirm' => 'Вы действительно хотите удалить эту запись?', + 'delete_success' => 'Эти записи успешно удалены.', + 'close_confirm' => 'Запись не была сохранена.', + 'return_to_posts' => 'Вернуться к списку записей' + ], + 'categories' => [ + 'list_title' => 'Управление категориями блога', + 'new_category' => 'Новая категория', + 'uncategorized' => 'Без категории' + ], + 'category' => [ + 'name' => 'Название', + 'name_placeholder' => 'Новое имя категории', + 'description' => 'Описание', + 'slug' => 'URL адрес', + 'slug_placeholder' => 'new-category-slug', + 'posts' => 'Записи', + 'delete_confirm' => 'Вы действительно хотите удалить эту категорию?', + 'delete_success' => 'Эти категории успешно удалены.', + 'return_to_categories' => 'Вернуться к списку категорий', + 'reorder' => 'Порядок категорий' + ], + 'menuitem' => [ + 'blog_category' => 'Категория блога', + 'all_blog_categories' => 'Все категории блога', + 'blog_post' => 'Запись блога', + 'all_blog_posts' => 'Все записи блога', + 'category_blog_posts' => 'Записи категории блога' + ], + 'settings' => [ + 'category_title' => 'Список категорий блога', + 'category_description' => 'Отображает список категорий на странице.', + 'category_slug' => 'Параметр URL', + 'category_slug_description' => 'Параметр маршрута, используемый для поиска в текущей категории по URL. Это свойство используется по умолчанию компонентом Фрагменты для маркировки активной категории.', + 'category_display_empty' => 'Пустые категории', + 'category_display_empty_description' => 'Отображать категории, которые не имеют записей.', + 'category_page' => 'Страница категорий', + 'category_page_description' => 'Название страницы категорий. Это свойство используется по умолчанию компонентом Фрагменты.', + 'post_title' => 'Запись блога', + 'post_description' => 'Отображение записи блога', + 'post_slug' => 'Параметр URL', + 'post_slug_description' => 'Параметр маршрута, необходимый для выбора конкретной записи.', + 'post_category' => 'Страница категорий', + 'post_category_description' => 'Название страницы категорий. Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_title' => 'Список записей блога', + 'posts_description' => 'Отображает список последних записей блога на странице.', + 'posts_pagination' => 'Параметр постраничной навигации', + 'posts_pagination_description' => 'Параметр, необходимый для постраничной навигации.', + 'posts_filter' => 'Фильтр категорий', + 'posts_filter_description' => 'Введите URL категории или параметр URL-адреса для фильтрации записей. Оставьте пустым, чтобы посмотреть все записи.', + 'posts_per_page' => 'Записей на странице', + 'posts_per_page_validation' => 'Недопустимый Формат. Ожидаемый тип данных - действительное число.', + 'posts_no_posts' => 'Отсутствие записей', + 'posts_no_posts_description' => 'Сообщение, отображаемое в блоге, если отсутствуют записи. Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_no_posts_default' => 'Записей не найдено', + 'posts_order' => 'Сортировка', + 'posts_order_description' => 'Атрибут, по которому будут сортироваться записи.', + 'posts_category' => 'Страница категорий', + 'posts_category_description' => 'Название категории на странице записи "размещена в категории". Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_post' => 'Страница записи', + 'posts_post_description' => 'Название страницы для ссылки "подробнее". Это свойство используется по умолчанию компонентом Фрагменты.', + 'posts_except_post' => 'Кроме записи', + 'posts_except_post_description' => 'Введите ID/URL или переменную с ID/URL записи, которую вы хотите исключить', + 'posts_except_categories' => 'Кроме категорий', + 'posts_except_categories_description' => 'Введите разделенный запятыми список URL категорий или переменную со списком категорий, которые вы хотите исключить', + 'rssfeed_blog' => 'Страница блога', + 'rssfeed_blog_description' => 'Имя основного файла страницы блога для генерации ссылок. Это свойство используется по умолчанию компонентом Фрагменты.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => 'Создает RSS-канал, содержащий записи из блога.', + 'group_links' => 'Ссылки', + 'group_exceptions' => 'Исключения' + ], + 'sorting' => [ + 'title_asc' => 'Заголовок (по возрастанию)', + 'title_desc' => 'Заголовок (по убыванию)', + 'created_asc' => 'Создано (по возрастанию)', + 'created_desc' => 'Создано (по убыванию)', + 'updated_asc' => 'Обновлено (по возрастанию)', + 'updated_desc' => 'Обновлено (по убыванию)', + 'published_asc' => 'Опубликовано (по возрастанию)', + 'published_desc' => 'Опубликовано (по убыванию)', + 'random' => 'Случайно' + ], + 'import' => [ + 'update_existing_label' => 'Обновить существующие записи', + 'update_existing_comment' => 'Установите этот флажок, чтобы обновлять записи имеющие одинаковый ID, title или URL.', + 'auto_create_categories_label' => 'Создать категории указанные в импортируемом файле', + 'auto_create_categories_comment' => 'Вы должны сопоставить столбец Категории, чтобы использовать эту функцию. В противном случае выберите для назначения категорию по умолчанию из пунктов ниже.', + 'categories_label' => 'Категории', + 'categories_comment' => 'Выберите категории, к которым будут принадлежать импортированные записи (необязательно).', + 'default_author_label' => 'Автор записи по умолчанию (необязательно)', + 'default_author_comment' => 'Импорт попытается использовать существующего автора, если он соответствуете столбцу Email автора, в противном случае используется указанный выше автор.', + 'default_author_placeholder' => '-- выберите автора --' + ] +]; diff --git a/plugins/rainlab/blog/lang/sk/lang.php b/plugins/rainlab/blog/lang/sk/lang.php new file mode 100644 index 0000000..5756778 --- /dev/null +++ b/plugins/rainlab/blog/lang/sk/lang.php @@ -0,0 +1,154 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustná blogová platforma.' + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Správa blogových príspevkov', + 'posts' => 'Príspevky', + 'create_post' => 'Príspevok', + 'categories' => 'Kategórie', + 'create_category' => 'Kategórie príspevkov', + 'tab' => 'Blog', + 'access_posts' => 'Správa blogových príspevkov', + 'access_categories' => 'Správa blogových kategórií', + 'access_other_posts' => 'Správa blogových príspevkov ostatných užívateľov', + 'access_import_export' => 'Možnosť importu a exportu príspevkov', + 'access_publish' => 'Možnosť publikovať príspevky', + 'delete_confirm' => 'Ste si istý?', + 'chart_published' => 'PublikovanéPublished', + 'chart_drafts' => 'Koncepty', + 'chart_total' => 'Celkom' + ], + 'posts' => [ + 'list_title' => 'Správa blogových príspevkov', + 'filter_category' => 'Kategória', + 'filter_published' => 'Publikované', + 'filter_date' => 'Dátum', + 'new_post' => 'Nový príspevok', + 'export_post' => 'Exportovať príspevky', + 'import_post' => 'Importovať príspevky' + ], + 'post' => [ + 'title' => 'Názov', + 'title_placeholder' => 'Názov nového príspevku', + 'content' => 'Obsah', + 'content_html' => 'HTML Obsah', + 'slug' => 'URL príspevku', + 'slug_placeholder' => 'url-nového-príspevku', + 'categories' => 'Kategórie', + 'author_email' => 'Email autora', + 'created' => 'Vytvorené', + 'created_date' => 'Dátum vytvorenia', + 'updated' => 'Upravené', + 'updated_date' => 'Dátum upravenia', + 'published' => 'Publikované', + 'published_date' => 'Dátum publikovania', + 'published_validation' => 'Prosím zvoľte dátum publikovania príspevku', + 'tab_edit' => 'Upraviť', + 'tab_categories' => 'Kategórie', + 'categories_comment' => 'Vyberte kategórie do ktorých tento príspevok patrí', + 'categories_placeholder' => 'Neexistujú žiadne kategórie, najprv nejakú vytvorte!', + 'tab_manage' => 'Nastavenia', + 'published_on' => 'Dátum publikovania', + 'excerpt' => 'Výňatok príspevku', + 'summary' => 'Zhrnutie', + 'featured_images' => 'Obrázky', + 'delete_confirm' => 'Zmazať tento príspevok?', + 'delete_success' => 'Vybrané príspevky boli úspešne odstránené.', + 'close_confirm' => 'Príspevok nie je uložený.', + 'return_to_posts' => 'Späť na zoznam príspevkov' + ], + 'categories' => [ + 'list_title' => 'Správa blogových kategórií', + 'new_category' => 'Nová kategória', + 'uncategorized' => 'Nezaradené' + ], + 'category' => [ + 'name' => 'Názov', + 'name_placeholder' => 'Názov novej kategórie', + 'description' => 'Popis', + 'slug' => 'URL kategórie', + 'slug_placeholder' => 'url-novej-kategórie', + 'posts' => 'Počet príspevkov', + 'delete_confirm' => 'Zmazať túto kategóriu?', + 'delete_success' => 'Vybrané kategórie boli úspešne odstránené.', + 'return_to_categories' => 'Späť na zoznam kategórií', + 'reorder' => 'Zmeniť poradie kategórií' + ], + 'menuitem' => [ + 'blog_category' => 'Blogová kategória', + 'all_blog_categories' => 'Všetky blogové kategórie', + 'blog_post' => 'Blogové príspevky', + 'all_blog_posts' => 'Všetky blogové príspevky', + 'category_blog_posts' => 'Blogové príspevky v kategórií' + ], + 'settings' => [ + 'category_title' => 'Zoznam kategórií', + 'category_description' => 'Zobrazí zoznam blogových kategórií na stránke.', + 'category_slug' => 'URL kategórie', + 'category_slug_description' => "Nájde blogovú kategóriu s týmto URL. Používa sa pre zobrazenie aktívnej kategórie.", + 'category_display_empty' => 'Zobraziť prázdne kategórie', + 'category_display_empty_description' => 'Zobrazí kategórie, ktoré nemajú žiadne príspevky.', + 'category_page' => 'Stránka kategórie', + 'category_page_description' => 'Názov stránky kategórie kam budú smerovať odkazy na kategóriu. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'post_title' => 'Príspevok', + 'post_description' => 'Zobrazí blogový príspevok na stránke.', + 'post_slug' => 'URL príspevku', + 'post_slug_description' => "Nájde blogový príspevok s týmto URL.", + 'post_category' => 'Stránka kategórie', + 'post_category_description' => 'Názov stránky kategórie kam budú smerovať odkazy na kategóriu. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_title' => 'Zoznam príspevkov', + 'posts_description' => 'Zobrazí zoznam blogových príspevkov na stránke.', + 'posts_pagination' => 'číslo stránky', + 'posts_pagination_description' => 'Táto hodnota je použitá na určenie na akej stránke sa užívateľ nachádza.', + 'posts_filter' => 'Filter kategórií', + 'posts_filter_description' => 'Zadajte URL kategórie alebo URL parameter na filtrovanie príspevkov. Nechajte prázdne pre zobrazenie všetkých príspevkov.', + 'posts_per_page' => 'Príspevkov na stránku', + 'posts_per_page_validation' => 'Neplatný formát hodnoty počtu príspevkov na stránku', + 'posts_no_posts' => 'Správa prázdnej stránky', + 'posts_no_posts_description' => 'Správa, ktorá bude zobrazená v zozname príspevkov v prípade, že nie sú žiadne na zobrazenie. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_no_posts_default' => 'Nenašli sa žiadne príspevky', + 'posts_order' => 'Zoradenie príspevkov', + 'posts_order_description' => 'Atribút podľa ktorého budú príspevky zoradené', + 'posts_category' => 'Stránka kategórie', + 'posts_category_description' => 'Názov stránky kategórie kam budú smerovať odkazy "Vložené do". Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_post' => 'Stránka príspevku', + 'posts_post_description' => 'Názov stránky príspevku kam budú smerovať odkazy "Zistiť viac". Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'posts_except_post' => 'Okrem príspevku', + 'posts_except_post_description' => 'Zadajte ID/URL alebo premennú s ID/URL príspevku, ktorý chcete vylúčiť', + 'posts_except_categories' => 'Okrem kategórií', + 'posts_except_categories_description' => 'Zadajte zoznam kategórií oddelený čiarkami alebo premennú s týmto zoznamom, ktoré chcete vylúčiť', + 'rssfeed_blog' => 'Stránka blogu', + 'rssfeed_blog_description' => 'Názov hlavnej stránky blogu na generovanie odkazov. Táto hodnota je použitá v predvolenej čiastočnej stránke komponentu.', + 'rssfeed_title' => 'RSS Kanál', + 'rssfeed_description' => 'Vygeneruje RSS kanál, ktorý obsahuje blogové príspevky.', + 'group_links' => 'Odkazy', + 'group_exceptions' => 'Výnimky' + ], + 'sorting' => [ + 'title_asc' => 'Názov (vzostupne)', + 'title_desc' => 'Názov (zostupne)', + 'created_asc' => 'Vytvorené (vzostupne)', + 'created_desc' => 'Vytvorené (zostupne)', + 'updated_asc' => 'Upravené (vzostupne)', + 'updated_desc' => 'Upravené (zostupne)', + 'published_asc' => 'Publikované (vzostupne)', + 'published_desc' => 'Publikované (zostupne)', + 'random' => 'Náhodne' + ], + 'import' => [ + 'update_existing_label' => 'Aktualizovať existujúce príspevky', + 'update_existing_comment' => 'Začiarknutím tohto políčka aktualizujte príspevky, ktoré majú presne to isté ID, titul alebo URL príspevku.', + 'auto_create_categories_label' => 'Vytvoriť kategórie zadané v importovanom súbore', + 'auto_create_categories_comment' => 'Ak chcete túto funkciu použiť, mali by sa zhodovať so stĺpcom Kategórie, inak vyberte predvolené kategórie, ktoré chcete použiť z nižšie uvedených položiek.', + 'categories_label' => 'Kategórie', + 'categories_comment' => 'Vyberte kategórie, do ktorých budú patriť importované príspevky (voliteľné).', + 'default_author_label' => 'Predvolený autor príspevku (voliteľné)', + 'default_author_comment' => 'Import sa pokúsi použiť existujúceho autora, ak sa zhoduje so stĺpcom e-mail, inak sa použije vyššie uvedený autor.', + 'default_author_placeholder' => '-- vyberte autora --' + ] +]; diff --git a/plugins/rainlab/blog/lang/sl/lang.php b/plugins/rainlab/blog/lang/sl/lang.php new file mode 100644 index 0000000..d0c3c12 --- /dev/null +++ b/plugins/rainlab/blog/lang/sl/lang.php @@ -0,0 +1,163 @@ + [ + 'name' => 'Blog', + 'description' => 'Robustna platforma za bloganje.', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Upravljanje bloga', + 'posts' => 'Objave', + 'create_post' => 'Blog objava', + 'categories' => 'Kategorije', + 'create_category' => 'Blog kategorija', + 'tab' => 'Blog', + 'access_posts' => 'Upravljanje blog objav', + 'access_categories' => 'Upravljanje blog kategorij', + 'access_other_posts' => 'Upravljanje objav drugih uporabnikov', + 'access_import_export' => 'Dovoljenje za uvoz in izvoz objav', + 'access_publish' => 'Dovoljenje za objavljanje objav', + 'manage_settings' => 'Urejanje nastavitev bloga', + 'delete_confirm' => 'Ali ste prepričani?', + 'chart_published' => 'Objavljeno', + 'chart_drafts' => 'Osnutki', + 'chart_total' => 'Skupaj', + 'settings_description' => 'Urejanje nastavitev bloga', + 'show_all_posts_label' => 'Administratorjem prikaži vse objave', + 'show_all_posts_comment' => 'Na spletni strani prikaži administratorjem vse objavljene in neobjavljene objave.', + 'tab_general' => 'Splošno', + ], + 'posts' => [ + 'list_title' => 'Urejanje blog objav', + 'filter_category' => 'Kategorija', + 'filter_published' => 'Objavljeno', + 'filter_date' => 'Datum', + 'new_post' => 'Nova objava', + 'export_post' => 'Izvoz objav', + 'import_post' => 'Uvoz objav', + ], + 'post' => [ + 'title' => 'Naslov', + 'title_placeholder' => 'Naslov nove objave', + 'content' => 'Vsebina', + 'content_html' => 'HTML vsebina', + 'slug' => 'Povezava', + 'slug_placeholder' => 'povezava-nove-objave', + 'categories' => 'Kategorije', + 'author_email' => 'E-pošta avtorja', + 'created' => 'Ustvarjeno', + 'created_date' => 'Ustvarjeno dne', + 'updated' => 'Posodobljeno', + 'updated_date' => 'Posodobljeno dne', + 'published' => 'Objavljeno', + 'published_by' => 'Objavil', + 'current_user' => 'Trenutni uporabnik', + 'published_date' => 'Datum objave', + 'published_validation' => 'Prosimo, podajte datum objave', + 'tab_edit' => 'Upravljanje', + 'tab_categories' => 'Kategorije', + 'categories_comment' => 'Izberite kategorije, v katere spada objava', + 'categories_placeholder' => 'Ni najdenih kategorij, ustvarite vsaj eno kategorijo!', + 'tab_manage' => 'Urejanje', + 'published_on' => 'Datum objave', + 'excerpt' => 'Izvleček', + 'summary' => 'Povzetek', + 'featured_images' => 'Uporabljene slike', + 'delete_confirm' => 'Želite izbrisati to objavo?', + 'delete_success' => 'Te objave so bile uspešno izbrisane.', + 'close_confirm' => 'Ta objava ni shranjena.', + 'return_to_posts' => 'Vrnite se na seznam objav', + ], + 'categories' => [ + 'list_title' => 'Upravljanje blog kategorij', + 'new_category' => 'Nova kategorija', + 'uncategorized' => 'Nekategorizirano', + ], + 'category' => [ + 'name' => 'Naslov', + 'name_placeholder' => 'Naslov nove kategorije', + 'description' => 'Opis', + 'slug' => 'Povezava', + 'slug_placeholder' => 'povezava-nove-kategorije', + 'posts' => 'Objave', + 'delete_confirm' => 'Želite izbrisati to kategorijo?', + 'delete_success' => 'Te kategorije so bile uspešno izbrisane.', + 'return_to_categories' => 'Vrnite se na seznam blog kategorij', + 'reorder' => 'Spremenite vrstni red kategorij', + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategorija', + 'all_blog_categories' => 'Vse blog kategorije', + 'blog_post' => 'Blog objava', + 'all_blog_posts' => 'Vse blog objave', + 'category_blog_posts' => 'Objave v kategoriji', + ], + 'settings' => [ + 'category_title' => 'Seznam kategorij', + 'category_description' => 'Prikaži seznam blog kategorij na strani.', + 'category_slug' => 'Povezava kategorije', + 'category_slug_description' => 'Omogoča pregled blog kategorije na podani povezavi. Ta lastnost je uporabljena v privzeti predlogi komponente za označevanje trenutno aktivne kategorije.', + 'category_display_empty' => 'Prikaži prazne kategorije', + 'category_display_empty_description' => 'Prikaže kategorije brez objav.', + 'category_page' => 'Stran s kategorijo', + 'category_page_description' => 'Naslov strani blog kategorij za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'post_title' => 'Objava', + 'post_description' => 'Prikaži blog objavo na strani.', + 'post_slug' => 'Povezava objave', + 'post_slug_description' => "Omogoča pregled blog objave na podani povezavi.", + 'post_category' => 'Stran s kategorijo', + 'post_category_description' => 'Naslov strani blog kategorije za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_title' => 'Seznam objav', + 'posts_description' => 'Prikaži seznam najnovejših blog objav na strani.', + 'posts_pagination' => 'Številka strani', + 'posts_pagination_description' => 'Ta vrednost se uporablja za določitev, na kateri strani se nahaja uporabnik.', + 'posts_filter' => 'Filter kategorij', + 'posts_filter_description' => 'Vnesite povezavo kategorije ali URL parameter za filtriranje objav. Pustite prazno za prikaz vseh objav.', + 'posts_per_page' => 'Število objav na strani', + 'posts_per_page_validation' => 'Neveljaven format števila objav na strani', + 'posts_no_posts' => 'Sporočilo brez objav', + 'posts_no_posts_description' => 'Sporočilo, ki se prikaže na seznamu blog objav, če ni nobenih objav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_no_posts_default' => 'Ni najdenih objav', + 'posts_order' => 'Vrstni red objav', + 'posts_order_description' => 'Lastnost, glede na katero naj bodo razvrščene objave', + 'posts_category' => 'Stran s kategorijo', + 'posts_category_description' => 'Naslov strani blog kategorije za povezave "Objavljeno v". Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_post' => 'Stran z objavo', + 'posts_post_description' => 'Naslov strani blog objave za povezave "Preberi več". Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'posts_except_post' => 'Izvzete objave', + 'posts_except_post_description' => 'Vnesite ID/URL objave ali spremenljivko z ID-jem/URL-jem objave, ki jo želite izvzeti. Za določitev večjega števila objav lahko uporabite seznam, ločen z vejicami.', + 'posts_except_post_validation' => 'Izvzete objave morajo biti posamezna povezava ali ID objave ali pa seznam povezav oz. ID-jev objav, ločen z vejicami.', + 'posts_except_categories' => 'Izvzete kategorije', + 'posts_except_categories_description' => 'Vnesite seznam povezav kategorij, ločen z vejicami ali pa spremenljivko, ki vključuje takšen seznam kategorij, ki jih želite izvzeti.', + 'posts_except_categories_validation' => 'Izvzete kategorije morajo biti posamezna povezava kategorije ali pa seznam povezav kategorij, ločen z vejicami.', + 'rssfeed_blog' => 'Stran z blogom', + 'rssfeed_blog_description' => 'Naslov glavne strani bloga za ustvarjanje povezav. Ta lastnost je uporabljena v privzeti predlogi komponente.', + 'rssfeed_title' => 'RSS vir', + 'rssfeed_description' => 'Ustvari vir RSS, ki vsebuje objave iz bloga.', + 'group_links' => 'Povezave', + 'group_exceptions' => 'Izjeme', + ], + 'sorting' => [ + 'title_asc' => 'Naslov (naraščajoče)', + 'title_desc' => 'Naslov (padajoče)', + 'created_asc' => 'Ustvarjeno (naraščajoče)', + 'created_desc' => 'Ustvarjeno (padajoče)', + 'updated_asc' => 'Posodobljeno (naraščajoče)', + 'updated_desc' => 'Posodobljeno (padajoče)', + 'published_asc' => 'Objavljeno (naraščajoče)', + 'published_desc' => 'Objavljeno (padajoče)', + 'random' => 'Naključno', + ], + 'import' => [ + 'update_existing_label' => 'Posodobi obstoječe objave', + 'update_existing_comment' => 'Označite kvadratek, če želite posodobiti objave, ki imajo popolnoma enak ID, naslov ali povezavo.', + 'auto_create_categories_label' => 'Ustvari kategorije, določene v uvozni datoteki', + 'auto_create_categories_comment' => "Za uporabo te možnosti morate ali povezati stolpec 'Kategorije' ali pa označiti privzete kategorije za uporabo iz spodnjega seznama.", + 'categories_label' => 'Kategorije', + 'categories_comment' => 'Izberite kategorije, na katere bodo povezavne uvožene objave (neobvezno).', + 'default_author_label' => 'Privzeti avtor objave (neobvezno)', + 'default_author_comment' => "Uvoz bo poskusil uporabiti obstoječega avtorja, če povežete stolpec 'E-pošta avtorja', sicer se bo uporabil zgoraj navedeni avtor.", + 'default_author_placeholder' => '-- izberite avtorja --', + ], +]; diff --git a/plugins/rainlab/blog/lang/tr/lang.php b/plugins/rainlab/blog/lang/tr/lang.php new file mode 100644 index 0000000..25f23f9 --- /dev/null +++ b/plugins/rainlab/blog/lang/tr/lang.php @@ -0,0 +1,161 @@ + [ + 'name' => 'Blog', + 'description' => 'Sağlam blog platformu.', + ], + 'blog' => [ + 'menu_label' => 'Blog', + 'menu_description' => 'Blog Gönderilerini Yönet', + 'posts' => 'Gönderiler', + 'create_post' => 'Blog gönderisi', + 'categories' => 'Kategoriler', + 'create_category' => 'Blog kategorisi', + 'tab' => 'Blog', + 'access_posts' => 'Gönderileri yönetebilsin', + 'access_categories' => 'Blog kategorilerini yönetebilsin', + 'access_other_posts' => 'Diğer kullanıcıların gönderilerini yönetebilsin', + 'access_import_export' => 'Gönderileri içeri/dışarı aktarabilsin', + 'access_publish' => 'Gönderi yayınlayabilsin', + 'manage_settings' => 'Blog ayarlarını yönet', + 'delete_confirm' => 'Emin misiniz?', + 'chart_published' => 'Yayınlandı', + 'chart_drafts' => 'Taslaklar', + 'chart_total' => 'Toplam', + 'settings_description' => 'Blog ayarlarını yönet', + 'show_all_posts_label' => 'Tüm gönderileri yönetim paneli kullanıcılarına göster', + 'show_all_posts_comment' => 'Hem yayınlanmış hem de yayınlanmamış gönderileri önyüzde yönetim paneli kullanıcılarına göster.', + 'tab_general' => 'Genel', + ], + 'posts' => [ + 'list_title' => 'Blog gönderilerini yönet', + 'filter_category' => 'Kategori', + 'filter_published' => 'Yayınlanan', + 'filter_date' => 'Tarih', + 'new_post' => 'Yeni gönderi', + 'export_post' => 'Gönderileri dışarı aktar', + 'import_post' => 'Gönderileri içeri aktar', + ], + 'post' => [ + 'title' => 'Başlık', + 'title_placeholder' => 'Yeni gönderi başlığı', + 'content' => 'İçerik', + 'content_html' => 'HTML İçeriği', + 'slug' => 'Kısa URL', + 'slug_placeholder' => 'yeni-gonderi-basligi', + 'categories' => 'Kategoriler', + 'author_email' => 'Yazar E-mail', + 'created' => 'Oluşturuldu', + 'created_date' => 'Oluşturulma tarihi', + 'updated' => 'Güncellendi', + 'updated_date' => 'Güncellenme tarihi', + 'published' => 'Yayınlandı', + 'published_date' => 'Yayınlanma tarihi', + 'published_validation' => 'Lütfen yayınlama tarihini belirtiniz', + 'tab_edit' => 'Düzenle', + 'tab_categories' => 'Kategoriler', + 'categories_comment' => 'Gönderinin ait olduğu kategorileri seçiniz', + 'categories_placeholder' => 'Kategori yok, öncelikle bir kategori oluşturmalısınız!', + 'tab_manage' => 'Yönet', + 'published_on' => 'Yayınlandı', + 'excerpt' => 'Alıntı', + 'summary' => 'Özet', + 'featured_images' => 'Öne Çıkan Görseller', + 'delete_confirm' => 'Bu yazıyı silmek istiyor musunuz?', + 'delete_success' => 'Gönderi(ler) silindi.', + 'close_confirm' => 'Gönderi kaydedilmedi.', + 'return_to_posts' => 'Gönderi listesine dön', + ], + 'categories' => [ + 'list_title' => 'Blog kategorilerini yönet', + 'new_category' => 'Yeni kategori', + 'uncategorized' => 'Kategorisiz', + ], + 'category' => [ + 'name' => 'İsim', + 'name_placeholder' => 'Yeni kategori adı', + 'description' => 'Açıklama', + 'slug' => 'Kısa URL', + 'slug_placeholder' => 'yeni-kategori-basligi', + 'posts' => 'Gönderiler', + 'delete_confirm' => 'Bu kategoriyi silmek istiyor musunuz?', + 'delete_success' => 'Kategori(ler) silindi.', + 'return_to_categories' => 'Kategori listesine dön', + 'reorder' => 'Kategorileri yeniden sırala', + ], + 'menuitem' => [ + 'blog_category' => 'Blog kategorisi', + 'all_blog_categories' => 'Tüm blog kategorileri', + 'blog_post' => 'Blog gönderisi', + 'all_blog_posts' => 'Tüm blog gönderileri', + 'category_blog_posts' => 'Blog kategori gönderileri', + ], + 'settings' => [ + 'category_title' => 'Kategori Listesi', + 'category_description' => 'Kategorilerin listesini sayfada göster.', + 'category_slug' => 'Kategori Kısa URL', + 'category_slug_description' => 'Verilen kısa URLi kullanarak blog kategorisini görüntüle. Bu özellik şu anki aktif kategoriyi işaretlemek için varsayılan kısmi bileşeni tarafından kullanılır', + 'category_display_empty' => 'Boş kategorileri göster', + 'category_display_empty_description' => 'Herhangi bir gönderi olmayan kategorileri göster.', + 'category_page' => 'Kategori sayfası', + 'category_page_description' => 'Kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'post_title' => 'Gönderi', + 'post_description' => 'Sayfada bir blog gönderisi gösterir.', + 'post_slug' => 'Gönderi Kısa URL', + 'post_slug_description' => 'Verilen kısa URL ile blog gönderisine bakın.', + 'post_category' => 'Kategori sayfası', + 'post_category_description' => 'Kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_title' => 'Gönderi listesi', + 'posts_description' => 'Sayfada son blog gönderilerinin listesini gösterir.', + 'posts_pagination' => 'Sayfa numarası', + 'posts_pagination_description' => 'Bu değer kullanıcının hangi sayfada olduğunu belirlemek için kullanılır.', + 'posts_filter' => 'Kategori filtresi', + 'posts_filter_description' => 'Gönderileri filtrelemek için kategori kısa URLsi ya da URL parametresi girin. Tüm gönderiler için boş bırakın.', + 'posts_per_page' => 'Sayfa başına gönderi', + 'posts_per_page_validation' => 'Sayfa başına gönderi için geçersiz format', + 'posts_no_posts' => 'Gönderi mesajı yok', + 'posts_no_posts_description' => 'Eğer bir gönderi yoksa gönderi listesinde görüntülenecek mesaj. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_no_posts_default' => 'Gönderi yok', + 'posts_order' => 'Gönderi Sırası', + 'posts_order_description' => 'Gönderilerin sıralama türü', + 'posts_category' => 'Kategori sayfası', + 'posts_category_description' => '"Yayınlanan" kategori bağlantıları için kategori sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_post' => 'Gönderi sayfası', + 'posts_post_description' => '"Daha fazla bilgi edinin" bağlantıları için gönderi sayfası dosyasının adı. Bu özellik varsayılan kısmi bileşeni tarafından kullanılır.', + 'posts_except_post' => 'Hariç tutulacak gönderi', + 'posts_except_post_description' => 'Hariç tutmak istediğiniz gönderinin ID/URL ini veya ID/URL içeren bir değişken girin. Birden çok gönderi belirtmek için virgülle ayrılmış liste kullanabilirsiniz.', + 'posts_except_post_validation' => 'Hariç tutulacak gönderi değeri tek bir kısa URL veya ID, veya virgülle ayrılmış kısa URL veya ID listesi olmalıdır.', + 'posts_except_categories' => 'Hariç tutulacak kategoriler', + 'posts_except_categories_description' => 'Hariç tutmak istediğiniz kategori listesini içeren virgülle ayrılmış bir kategori listesi veya listeyi içeren bir değişken girin.', + 'posts_except_categories_validation' => 'Hariç tutulacak kategoriler değeri tek bir kısa URL, veya virgülle ayrılmış kısa URL listesi olmalıdır.', + 'rssfeed_blog' => 'Blog sayfası', + 'rssfeed_blog_description' => 'Linkleri üretmek için ana blog sayfasının adı. Bu özellik, varsayılan kısmi bileşeni tarafından kullanılır.', + 'rssfeed_title' => 'RSS Beslemesi', + 'rssfeed_description' => 'Blog içerisindeki gönderileri veren RSS beslemesi oluşturur.', + 'group_links' => 'Linkler', + 'group_exceptions' => 'Hariç olanlar', + ], + 'sorting' => [ + 'title_asc' => 'Başlık (a-z)', + 'title_desc' => 'Başlık (z-a)', + 'created_asc' => 'Oluşturulma (yeniden eskiye)', + 'created_desc' => 'Oluşturulma (eskiden yeniye)', + 'updated_asc' => 'Güncellenme (yeniden eskiye)', + 'updated_desc' => 'Güncellenme (eskiden yeniye)', + 'published_asc' => 'Yayınlanma (yeniden eskiye)', + 'published_desc' => 'Yayınlanma (eskiden yeniye)', + 'random' => 'Rastgele', + ], + 'import' => [ + 'update_existing_label' => 'Mevcut gönderileri güncelle', + 'update_existing_comment' => 'Tam olarak aynı ID, başlık veya kısa URL içeren gönderileri güncellemek için bu kutuyu işaretleyin.', + 'auto_create_categories_label' => 'İçe aktarma dosyasında bulunan kategorileri oluştur', + 'auto_create_categories_comment' => 'Bu özelliği kullanmak için Kategoriler sütununu eşleştirmelisiniz, aksi takdirde aşağıdaki öğelerden kullanılacak varsayılan kategorileri seçmelisiniz.', + 'categories_label' => 'Kategoriler', + 'categories_comment' => 'İçe aktarılan gönderilerin ait olacağı kategorileri seçin (isteğe bağlı).', + 'default_author_label' => 'Varsayılan gönderi yazarı (isteğe bağlı)', + 'default_author_comment' => 'İçe aktarma, Yazar E-postası sütunuyla eşleşirse mevcut bir yazarı gönderi için kullanmaya çalışır, aksi takdirde yukarıda belirtilen yazar seçilir.', + 'default_author_placeholder' => '-- yazar seçin --', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/blog/lang/zh-cn/lang.php b/plugins/rainlab/blog/lang/zh-cn/lang.php new file mode 100644 index 0000000..f1e2e3d --- /dev/null +++ b/plugins/rainlab/blog/lang/zh-cn/lang.php @@ -0,0 +1,124 @@ + [ + 'name' => '博客', + 'description' => '一个强大的博客平台.' + ], + 'blog' => [ + 'menu_label' => '博客', + 'menu_description' => '管理博客帖子', + 'posts' => '帖子', + 'create_post' => '博客帖子', + 'categories' => '分类', + 'create_category' => '博客分类', + 'tab' => '博客', + 'access_posts' => '管理博客帖子', + 'access_categories' => '管理博客分类', + 'access_other_posts' => '管理其他用户帖子', + 'access_import_export' => '允许导入和导出', + 'access_publish' => '允许发布帖子', + 'delete_confirm' => '你确定?', + 'chart_published' => '已发布', + 'chart_drafts' => '草稿', + 'chart_total' => '总数' + ], + 'posts' => [ + 'list_title' => '管理博客帖子', + 'filter_category' => '分类', + 'filter_published' => '发布', + 'filter_date' => '日期', + 'new_post' => '创建帖子', + 'export_post' => '导出帖子', + 'import_post' => '导入帖子' + ], + 'post' => [ + 'title' => '标题', + 'title_placeholder' => '新帖子标题', + 'content' => '内容', + 'content_html' => 'HTML 内容', + 'slug' => '别名', + 'slug_placeholder' => 'new-post-slug', + 'categories' => '分类', + 'author_email' => '作者邮箱', + 'created' => '创建时间', + 'created_date' => '创建日期', + 'updated' => '更新时间', + 'updated_date' => '更新日期', + 'published' => '发布时间', + 'published_date' => '发布日期', + 'published_validation' => '请指定发布日期', + 'tab_edit' => '编辑', + 'tab_categories' => '分类', + 'categories_comment' => '选择帖子属于那个分类', + 'categories_placeholder' => '没有分类,你应该先创建一个分类!', + 'tab_manage' => '管理', + 'published_on' => '发布于', + 'excerpt' => '摘录', + 'summary' => '总结', + 'featured_images' => '特色图片', + 'delete_confirm' => '确定删除该帖子?', + 'close_confirm' => '该帖子未保存.', + 'return_to_posts' => '返回帖子列表' + ], + 'categories' => [ + 'list_title' => '管理博客分类', + 'new_category' => '新分类', + 'uncategorized' => '未分类' + ], + 'category' => [ + 'name' => '名称', + 'name_placeholder' => '新分类名称', + 'description' => '描述', + 'slug' => '别名', + 'slug_placeholder' => 'new-category-slug', + 'posts' => '帖子', + 'delete_confirm' => '确定删除分类?', + 'return_to_categories' => '返回博客分类列表', + 'reorder' => '重新排序分类' + ], + 'menuitem' => [ + 'blog_category' => '博客分类', + 'all_blog_categories' => '所有博客分类', + 'blog_post' => '博客帖子', + 'all_blog_posts' => '所有博客帖子' + ], + 'settings' => [ + 'category_title' => '分类列表', + 'category_description' => '在页面上显示帖子分类列表.', + 'category_slug' => '分类别名', + 'category_slug_description' => "用分类别名查找博客分类. 该值被默认组件的partial用来激活当前分类.", + 'category_display_empty' => '显示空的分类', + 'category_display_empty_description' => '显示没有帖子的分类.', + 'category_page' => '分类页', + 'category_page_description' => '用来生成分类链接的分类页面文件名称. 该属性被默认组件partial所使用.', + 'post_title' => '帖子', + 'post_description' => '在页面上显示博客帖子.', + 'post_slug' => '帖子别名', + 'post_slug_description' => "用帖子别名查找博客帖子.", + 'post_category' => '分类页', + 'post_category_description' => '用来生成分类链接的分类页面文件名称. 该属性被默认组件partial所使用.', + 'posts_title' => '帖子列表', + 'posts_description' => '在页面上显示最近发布的博客帖子列表.', + 'posts_pagination' => '页数', + 'posts_pagination_description' => '该值用来判定用户所在页面.', + 'posts_filter' => '过滤分类', + 'posts_filter_description' => '输入分类的别名(slug)或者URL参数来过滤帖子. 留空显示所有帖子.', + 'posts_per_page' => '每页帖子数', + 'posts_per_page_validation' => '每一页帖子数量的值的格式错误', + 'posts_no_posts' => '没有帖子的消息', + 'posts_no_posts_description' => '如果博客帖子列表中一个帖子都没有要显示的提示消息. 该属性被默认组件partial所使用.', + 'posts_order' => '帖子排序', + 'posts_order_description' => '帖子排序的属性', + 'posts_category' => '分类页', + 'posts_category_description' => '用来生成"发布到"分类链接的分类页文件名称. 该属性被默认组件partial所使用.', + 'posts_post' => '帖子页', + 'posts_post_description' => ' 查看帖子的"详情"的页面文件. 该属性被默认组件partial所使用.', + 'posts_except_post' => '排除的帖子', + 'posts_except_post_description' => '输入帖子的ID/URL或者变量来排除你不想看见的帖子', + 'rssfeed_blog' => '博客页面', + 'rssfeed_blog_description' => '生成博客帖子首页面文件名称. 该属性被默认组件partial所使用.', + 'rssfeed_title' => 'RSS Feed', + 'rssfeed_description' => '从博客生成一个包含帖子的RSS Feed.' + ] +]; diff --git a/plugins/rainlab/blog/models/Category.php b/plugins/rainlab/blog/models/Category.php new file mode 100644 index 0000000..7f83355 --- /dev/null +++ b/plugins/rainlab/blog/models/Category.php @@ -0,0 +1,315 @@ + 'required', + 'slug' => 'required|between:3,64|unique:rainlab_blog_categories', + 'code' => 'nullable|unique:rainlab_blog_categories', + ]; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = [ + 'name', + 'description', + ['slug', 'index' => true] + ]; + + protected $guarded = []; + + public $belongsToMany = [ + 'posts' => ['RainLab\Blog\Models\Post', + 'table' => 'rainlab_blog_posts_categories', + 'order' => 'published_at desc', + 'scope' => 'isPublished' + ], + 'posts_count' => ['RainLab\Blog\Models\Post', + 'table' => 'rainlab_blog_posts_categories', + 'scope' => 'isPublished', + 'count' => true + ] + ]; + + public function beforeValidate() + { + // Generate a URL slug for this model + if (!$this->exists && !$this->slug) { + $this->slug = Str::slug($this->name); + } + } + + public function afterDelete() + { + $this->posts()->detach(); + } + + public function getPostCountAttribute() + { + return optional($this->posts_count->first())->count ?? 0; + } + + /** + * Count posts in this and nested categories + * @return int + */ + public function getNestedPostCount() + { + return $this->post_count + $this->children->sum(function ($category) { + return $category->getNestedPostCount(); + }); + } + + /** + * Sets the "url" attribute with a URL to this object + * + * @param string $pageName + * @param Cms\Classes\Controller $controller + * + * @return string + */ + public function setUrl($pageName, $controller) + { + $params = [ + 'id' => $this->id, + 'slug' => $this->slug + ]; + + return $this->url = $controller->pageUrl($pageName, $params, false); + } + + /** + * Handler for the pages.menuitem.getTypeInfo event. + * Returns a menu item type information. The type information is returned as array + * with the following elements: + * - references - a list of the item type reference options. The options are returned in the + * ["key"] => "title" format for options that don't have sub-options, and in the format + * ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional, + * required only if the menu item type requires references. + * - nesting - Boolean value indicating whether the item type supports nested items. Optional, + * false if omitted. + * - dynamicItems - Boolean value indicating whether the item type could generate new menu items. + * Optional, false if omitted. + * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to + * resolve the item URL. + * @param string $type Specifies the menu item type + * @return array Returns an array + */ + public static function getMenuTypeInfo($type) + { + $result = []; + + if ($type == 'blog-category') { + $result = [ + 'references' => self::listSubCategoryOptions(), + 'nesting' => true, + 'dynamicItems' => true + ]; + } + + if ($type == 'all-blog-categories') { + $result = [ + 'dynamicItems' => true + ]; + } + + if ($result) { + $theme = Theme::getActiveTheme(); + + $pages = CmsPage::listInTheme($theme, true); + $cmsPages = []; + foreach ($pages as $page) { + if (!$page->hasComponent('blogPosts')) { + continue; + } + + /* + * Component must use a category filter with a routing parameter + * eg: categoryFilter = "{{ :somevalue }}" + */ + $properties = $page->getComponentProperties('blogPosts'); + if (!isset($properties['categoryFilter']) || !preg_match('/{{\s*:/', $properties['categoryFilter'])) { + continue; + } + + $cmsPages[] = $page; + } + + $result['cmsPages'] = $cmsPages; + } + + return $result; + } + + protected static function listSubCategoryOptions() + { + $category = self::getNested(); + + $iterator = function($categories) use (&$iterator) { + $result = []; + + foreach ($categories as $category) { + if (!$category->children) { + $result[$category->id] = $category->name; + } + else { + $result[$category->id] = [ + 'title' => $category->name, + 'items' => $iterator($category->children) + ]; + } + } + + return $result; + }; + + return $iterator($category); + } + + /** + * Handler for the pages.menuitem.resolveItem event. + * Returns information about a menu item. The result is an array + * with the following keys: + * - url - the menu item URL. Not required for menu item types that return all available records. + * The URL should be returned relative to the website root and include the subdirectory, if any. + * Use the Url::to() helper to generate the URLs. + * - isActive - determines whether the menu item is active. Not required for menu item types that + * return all available records. + * - items - an array of arrays with the same keys (url, isActive, items) + the title key. + * The items array should be added only if the $item's $nesting property value is TRUE. + * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item. + * @param \Cms\Classes\Theme $theme Specifies the current theme. + * @param string $url Specifies the current page URL, normalized, in lower case + * The URL is specified relative to the website root, it includes the subdirectory name, if any. + * @return mixed Returns an array. Returns null if the item cannot be resolved. + */ + public static function resolveMenuItem($item, $url, $theme) + { + $result = null; + + if ($item->type == 'blog-category') { + if (!$item->reference || !$item->cmsPage) { + return; + } + + $category = self::find($item->reference); + if (!$category) { + return; + } + + $pageUrl = self::getCategoryPageUrl($item->cmsPage, $category, $theme); + if (!$pageUrl) { + return; + } + + $pageUrl = Url::to($pageUrl); + + $result = []; + $result['url'] = $pageUrl; + $result['isActive'] = $pageUrl == $url; + $result['mtime'] = $category->updated_at; + + if ($item->nesting) { + $iterator = function($categories) use (&$iterator, &$item, &$theme, $url) { + $branch = []; + + foreach ($categories as $category) { + + $branchItem = []; + $branchItem['url'] = self::getCategoryPageUrl($item->cmsPage, $category, $theme); + $branchItem['isActive'] = $branchItem['url'] == $url; + $branchItem['title'] = $category->name; + $branchItem['mtime'] = $category->updated_at; + + if ($category->children) { + $branchItem['items'] = $iterator($category->children); + } + + $branch[] = $branchItem; + } + + return $branch; + }; + + $result['items'] = $iterator($category->children); + } + } + elseif ($item->type == 'all-blog-categories') { + $result = [ + 'items' => [] + ]; + + $categories = self::with('posts_count')->orderBy('name')->get(); + foreach ($categories as $category) { + try { + $postCount = $category->posts_count->first()->count ?? null; + if ($postCount === 0) { + continue; + } + } + catch (\Exception $ex) {} + + $categoryItem = [ + 'title' => $category->name, + 'url' => self::getCategoryPageUrl($item->cmsPage, $category, $theme), + 'mtime' => $category->updated_at + ]; + + $categoryItem['isActive'] = $categoryItem['url'] == $url; + + $result['items'][] = $categoryItem; + } + } + + return $result; + } + + /** + * Returns URL of a category page. + * + * @param $pageCode + * @param $category + * @param $theme + */ + protected static function getCategoryPageUrl($pageCode, $category, $theme) + { + $page = CmsPage::loadCached($theme, $pageCode); + if (!$page) { + return; + } + + $properties = $page->getComponentProperties('blogPosts'); + if (!isset($properties['categoryFilter'])) { + return; + } + + /* + * Extract the routing parameter name from the category filter + * eg: {{ :someRouteParam }} + */ + if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['categoryFilter'], $matches)) { + return; + } + + $paramName = substr(trim($matches[1]), 1); + $url = CmsPage::url($page->getBaseFileName(), [$paramName => $category->slug]); + + return $url; + } +} diff --git a/plugins/rainlab/blog/models/Post.php b/plugins/rainlab/blog/models/Post.php new file mode 100644 index 0000000..d41b41d --- /dev/null +++ b/plugins/rainlab/blog/models/Post.php @@ -0,0 +1,726 @@ + 'required', + 'slug' => ['required', 'regex:/^[a-z0-9\/\:_\-\*\[\]\+\?\|]*$/i', 'unique:rainlab_blog_posts'], + 'content' => 'required', + 'excerpt' => '' + ]; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = [ + 'title', + 'content', + 'content_html', + 'excerpt', + 'metadata', + ['slug', 'index' => true] + ]; + + /** + * @var array Attributes to be stored as JSON + */ + protected $jsonable = ['metadata']; + + /** + * The attributes that should be mutated to dates. + * @var array + */ + protected $dates = ['published_at']; + + /** + * The attributes on which the post list can be ordered. + * @var array + */ + public static $allowedSortingOptions = [ + 'title asc' => 'rainlab.blog::lang.sorting.title_asc', + 'title desc' => 'rainlab.blog::lang.sorting.title_desc', + 'created_at asc' => 'rainlab.blog::lang.sorting.created_asc', + 'created_at desc' => 'rainlab.blog::lang.sorting.created_desc', + 'updated_at asc' => 'rainlab.blog::lang.sorting.updated_asc', + 'updated_at desc' => 'rainlab.blog::lang.sorting.updated_desc', + 'published_at asc' => 'rainlab.blog::lang.sorting.published_asc', + 'published_at desc' => 'rainlab.blog::lang.sorting.published_desc', + 'random' => 'rainlab.blog::lang.sorting.random' + ]; + + /* + * Relations + */ + public $belongsTo = [ + 'user' => BackendUser::class + ]; + + public $belongsToMany = [ + 'categories' => [ + Category::class, + 'table' => 'rainlab_blog_posts_categories', + 'order' => 'name' + ] + ]; + + public $attachMany = [ + 'featured_images' => [\System\Models\File::class, 'order' => 'sort_order'], + 'content_images' => \System\Models\File::class + ]; + + /** + * @var array The accessors to append to the model's array form. + */ + protected $appends = ['summary', 'has_summary']; + + public $preview = null; + + /** + * Limit visibility of the published-button + * + * @param $fields + * @param null $context + * @return void + */ + public function filterFields($fields, $context = null) + { + if (!isset($fields->published, $fields->published_at)) { + return; + } + + $user = BackendAuth::getUser(); + + if (!$user->hasAnyAccess(['rainlab.blog.access_publish'])) { + $fields->published->hidden = true; + $fields->published_at->hidden = true; + } + else { + $fields->published->hidden = false; + $fields->published_at->hidden = false; + } + } + + public function afterValidate() + { + if ($this->published && !$this->published_at) { + throw new ValidationException([ + 'published_at' => Lang::get('rainlab.blog::lang.post.published_validation') + ]); + } + } + + public function getUserOptions() + { + $options = []; + + foreach (BackendUser::all() as $user) { + $options[$user->id] = $user->fullname . ' ('.$user->login.')'; + } + + return $options; + } + + public function beforeSave() + { + if (empty($this->user)) { + $user = BackendAuth::getUser(); + if (!is_null($user)) { + $this->user = $user->id; + } + } + $this->content_html = self::formatHtml($this->content); + } + + /** + * Sets the "url" attribute with a URL to this object. + * @param string $pageName + * @param Controller $controller + * @param array $params Override request URL parameters + * + * @return string + */ + public function setUrl($pageName, $controller, $params = []) + { + $params = array_merge([ + 'id' => $this->id, + 'slug' => $this->slug, + ], $params); + + if (empty($params['category'])) { + $params['category'] = $this->categories->count() ? $this->categories->first()->slug : null; + } + + // Expose published year, month and day as URL parameters. + if ($this->published) { + $params['year'] = $this->published_at->format('Y'); + $params['month'] = $this->published_at->format('m'); + $params['day'] = $this->published_at->format('d'); + } + + return $this->url = $controller->pageUrl($pageName, $params); + } + + /** + * Used to test if a certain user has permission to edit post, + * returns TRUE if the user is the owner or has other posts access. + * @param BackendUser $user + * @return bool + */ + public function canEdit(BackendUser $user) + { + return ($this->user_id == $user->id) || $user->hasAnyAccess(['rainlab.blog.access_other_posts']); + } + + public static function formatHtml($input, $preview = false) + { + $result = Markdown::parse(trim($input)); + + // Check to see if the HTML should be cleaned from potential XSS + $user = BackendAuth::getUser(); + if (!$user || !$user->hasAccess('backend.allow_unsafe_markdown')) { + $result = Html::clean($result); + } + + if ($preview) { + $result = str_replace('
    ', '
    ', $result);
    +        }
    +
    +        $result = TagProcessor::instance()->processTags($result, $preview);
    +
    +        return $result;
    +    }
    +
    +    //
    +    // Scopes
    +    //
    +
    +    public function scopeIsPublished($query)
    +    {
    +        return $query
    +            ->whereNotNull('published')
    +            ->where('published', true)
    +            ->whereNotNull('published_at')
    +            ->where('published_at', '<', Carbon::now())
    +        ;
    +    }
    +
    +    /**
    +     * Lists posts for the frontend
    +     *
    +     * @param        $query
    +     * @param  array $options Display options
    +     * @return Post
    +     */
    +    public function scopeListFrontEnd($query, $options)
    +    {
    +        /*
    +         * Default options
    +         */
    +        extract(array_merge([
    +            'page'             => 1,
    +            'perPage'          => 30,
    +            'sort'             => 'created_at',
    +            'categories'       => null,
    +            'exceptCategories' => null,
    +            'category'         => null,
    +            'search'           => '',
    +            'published'        => true,
    +            'exceptPost'       => null
    +        ], $options));
    +
    +        $searchableFields = ['title', 'slug', 'excerpt', 'content'];
    +
    +        if ($published) {
    +            $query->isPublished();
    +        }
    +
    +        /*
    +         * Except post(s)
    +         */
    +        if ($exceptPost) {
    +            $exceptPosts = (is_array($exceptPost)) ? $exceptPost : [$exceptPost];
    +            $exceptPostIds = [];
    +            $exceptPostSlugs = [];
    +
    +            foreach ($exceptPosts as $exceptPost) {
    +                $exceptPost = trim($exceptPost);
    +
    +                if (is_numeric($exceptPost)) {
    +                    $exceptPostIds[] = $exceptPost;
    +                } else {
    +                    $exceptPostSlugs[] = $exceptPost;
    +                }
    +            }
    +
    +            if (count($exceptPostIds)) {
    +                $query->whereNotIn('id', $exceptPostIds);
    +            }
    +            if (count($exceptPostSlugs)) {
    +                $query->whereNotIn('slug', $exceptPostSlugs);
    +            }
    +        }
    +
    +        /*
    +         * Sorting
    +         */
    +        if (in_array($sort, array_keys(static::$allowedSortingOptions))) {
    +            if ($sort == 'random') {
    +                $query->inRandomOrder();
    +            } else {
    +                @list($sortField, $sortDirection) = explode(' ', $sort);
    +
    +                if (is_null($sortDirection)) {
    +                    $sortDirection = "desc";
    +                }
    +
    +                $query->orderBy($sortField, $sortDirection);
    +            }
    +        }
    +
    +        /*
    +         * Search
    +         */
    +        $search = trim($search);
    +        if (strlen($search)) {
    +            $query->searchWhere($search, $searchableFields);
    +        }
    +
    +        /*
    +         * Categories
    +         */
    +        if ($categories !== null) {
    +            $categories = is_array($categories) ? $categories : [$categories];
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +        }
    +
    +        /*
    +         * Except Categories
    +         */
    +        if (!empty($exceptCategories)) {
    +            $exceptCategories = is_array($exceptCategories) ? $exceptCategories : [$exceptCategories];
    +            array_walk($exceptCategories, 'trim');
    +
    +            $query->whereDoesntHave('categories', function ($q) use ($exceptCategories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('slug', $exceptCategories);
    +            });
    +        }
    +
    +        /*
    +         * Category, including children
    +         */
    +        if ($category !== null) {
    +            $category = Category::find($category);
    +
    +            $categories = $category->getAllChildrenAndSelf()->lists('id');
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +        }
    +
    +        return $query->paginate($perPage, $page);
    +    }
    +
    +    /**
    +     * Allows filtering for specifc categories.
    +     * @param  Illuminate\Query\Builder  $query      QueryBuilder
    +     * @param  array                     $categories List of category ids
    +     * @return Illuminate\Query\Builder              QueryBuilder
    +     */
    +    public function scopeFilterCategories($query, $categories)
    +    {
    +        return $query->whereHas('categories', function($q) use ($categories) {
    +            $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +        });
    +    }
    +
    +    public function scopeSearched($query, $locale, $queryString)
    +    {
    +        if ($locale == 'ru') {
    +            $query = $query->where('title', 'like', "%$queryString%")
    +                ->orWhere('content', 'like', "%$queryString%");
    +        } else {
    +            $query = $query->whereHas('translations', function ($innerQuery) use ($locale, $queryString) {
    +                $innerQuery->where('locale', $locale)
    +                    ->where(function ($searchQuery) use ($queryString) {
    +                        $searchQuery->where('attribute_data->title', 'like', "%$queryString%")
    +                            ->orWhere('attribute_data->content', 'like', "%$queryString%");
    +                    });
    +            });
    +        }
    +
    +        return $query;
    +    }
    +
    +    //
    +    // Summary / Excerpt
    +    //
    +
    +    /**
    +     * Used by "has_summary", returns true if this post uses a summary (more tag).
    +     * @return boolean
    +     */
    +    public function getHasSummaryAttribute()
    +    {
    +        $more = Config::get('rainlab.blog::summary_separator', '');
    +        $length = Config::get('rainlab.blog::summary_default_length', 600);
    +
    +        return (
    +            !!strlen(trim($this->excerpt)) ||
    +            strpos($this->content_html, $more) !== false ||
    +            strlen(Html::strip($this->content_html)) > $length
    +        );
    +    }
    +
    +    /**
    +     * Used by "summary", if no excerpt is provided, generate one from the content.
    +     * Returns the HTML content before the  tag or a limited 600
    +     * character version.
    +     *
    +     * @return string
    +     */
    +    public function getSummaryAttribute()
    +    {
    +        $excerpt = $this->excerpt;
    +        if (strlen(trim($excerpt))) {
    +            return $excerpt;
    +        }
    +
    +        $more = Config::get('rainlab.blog::summary_separator', '');
    +
    +        if (strpos($this->content_html, $more) !== false) {
    +            $parts = explode($more, $this->content_html);
    +
    +            return array_get($parts, 0);
    +        }
    +
    +        $length = Config::get('rainlab.blog::summary_default_length', 600);
    +
    +        return Html::limit($this->content_html, $length);
    +    }
    +
    +    //
    +    // Next / Previous
    +    //
    +
    +    /**
    +     * Apply a constraint to the query to find the nearest sibling
    +     *
    +     *     // Get the next post
    +     *     Post::applySibling()->first();
    +     *
    +     *     // Get the previous post
    +     *     Post::applySibling(-1)->first();
    +     *
    +     *     // Get the previous post, ordered by the ID attribute instead
    +     *     Post::applySibling(['direction' => -1, 'attribute' => 'id'])->first();
    +     *
    +     * @param       $query
    +     * @param array $options
    +     * @return
    +     */
    +    public function scopeApplySibling($query, $options = [])
    +    {
    +        if (!is_array($options)) {
    +            $options = ['direction' => $options];
    +        }
    +
    +        extract(array_merge([
    +            'direction' => 'next',
    +            'attribute' => 'published_at'
    +        ], $options));
    +
    +        $isPrevious = in_array($direction, ['previous', -1]);
    +        $directionOrder = $isPrevious ? 'asc' : 'desc';
    +        $directionOperator = $isPrevious ? '>' : '<';
    +
    +        $query->where('id', '<>', $this->id);
    +
    +        if (!is_null($this->$attribute)) {
    +            $query->where($attribute, $directionOperator, $this->$attribute);
    +        }
    +
    +        return $query->orderBy($attribute, $directionOrder);
    +    }
    +
    +    /**
    +     * Returns the next post, if available.
    +     * @return self
    +     */
    +    public function nextPost()
    +    {
    +        return self::isPublished()->applySibling()->first();
    +    }
    +
    +    /**
    +     * Returns the previous post, if available.
    +     * @return self
    +     */
    +    public function previousPost()
    +    {
    +        return self::isPublished()->applySibling(-1)->first();
    +    }
    +
    +    //
    +    // Menu helpers
    +    //
    +
    +    /**
    +     * Handler for the pages.menuitem.getTypeInfo event.
    +     * Returns a menu item type information. The type information is returned as array
    +     * with the following elements:
    +     * - references - a list of the item type reference options. The options are returned in the
    +     *   ["key"] => "title" format for options that don't have sub-options, and in the format
    +     *   ["key"] => ["title"=>"Option title", "items"=>[...]] for options that have sub-options. Optional,
    +     *   required only if the menu item type requires references.
    +     * - nesting - Boolean value indicating whether the item type supports nested items. Optional,
    +     *   false if omitted.
    +     * - dynamicItems - Boolean value indicating whether the item type could generate new menu items.
    +     *   Optional, false if omitted.
    +     * - cmsPages - a list of CMS pages (objects of the Cms\Classes\Page class), if the item type requires a CMS page reference to
    +     *   resolve the item URL.
    +     *
    +     * @param string $type Specifies the menu item type
    +     * @return array Returns an array
    +     */
    +    public static function getMenuTypeInfo($type)
    +    {
    +        $result = [];
    +
    +        if ($type == 'blog-post') {
    +            $references = [];
    +
    +            $posts = self::orderBy('title')->get();
    +            foreach ($posts as $post) {
    +                $references[$post->id] = $post->title;
    +            }
    +
    +            $result = [
    +                'references'   => $references,
    +                'nesting'      => false,
    +                'dynamicItems' => false
    +            ];
    +        }
    +
    +        if ($type == 'all-blog-posts') {
    +            $result = [
    +                'dynamicItems' => true
    +            ];
    +        }
    +
    +        if ($type == 'category-blog-posts') {
    +            $references = [];
    +
    +            $categories = Category::orderBy('name')->get();
    +            foreach ($categories as $category) {
    +                $references[$category->id] = $category->name;
    +            }
    +
    +            $result = [
    +                'references'   => $references,
    +                'dynamicItems' => true
    +            ];
    +        }
    +
    +        if ($result) {
    +            $theme = Theme::getActiveTheme();
    +
    +            $pages = CmsPage::listInTheme($theme, true);
    +            $cmsPages = [];
    +
    +            foreach ($pages as $page) {
    +                if (!$page->hasComponent('blogPost')) {
    +                    continue;
    +                }
    +
    +                /*
    +                 * Component must use a categoryPage filter with a routing parameter and post slug
    +                 * eg: categoryPage = "{{ :somevalue }}", slug = "{{ :somevalue }}"
    +                 */
    +                $properties = $page->getComponentProperties('blogPost');
    +                if (!isset($properties['categoryPage']) || !preg_match('/{{\s*:/', $properties['slug'])) {
    +                    continue;
    +                }
    +
    +                $cmsPages[] = $page;
    +            }
    +
    +            $result['cmsPages'] = $cmsPages;
    +        }
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * Handler for the pages.menuitem.resolveItem event.
    +     * Returns information about a menu item. The result is an array
    +     * with the following keys:
    +     * - url - the menu item URL. Not required for menu item types that return all available records.
    +     *   The URL should be returned relative to the website root and include the subdirectory, if any.
    +     *   Use the Url::to() helper to generate the URLs.
    +     * - isActive - determines whether the menu item is active. Not required for menu item types that
    +     *   return all available records.
    +     * - items - an array of arrays with the same keys (url, isActive, items) + the title key.
    +     *   The items array should be added only if the $item's $nesting property value is TRUE.
    +     *
    +     * @param \RainLab\Pages\Classes\MenuItem $item Specifies the menu item.
    +     * @param \Cms\Classes\Theme $theme Specifies the current theme.
    +     * @param string $url Specifies the current page URL, normalized, in lower case
    +     * The URL is specified relative to the website root, it includes the subdirectory name, if any.
    +     * @return mixed Returns an array. Returns null if the item cannot be resolved.
    +     */
    +    public static function resolveMenuItem($item, $url, $theme)
    +    {
    +        $result = null;
    +
    +        if ($item->type == 'blog-post') {
    +            if (!$item->reference || !$item->cmsPage) {
    +                return;
    +            }
    +
    +            $category = self::find($item->reference);
    +            if (!$category) {
    +                return;
    +            }
    +
    +            $pageUrl = self::getPostPageUrl($item->cmsPage, $category, $theme);
    +            if (!$pageUrl) {
    +                return;
    +            }
    +
    +            $pageUrl = Url::to($pageUrl);
    +
    +            $result = [];
    +            $result['url'] = $pageUrl;
    +            $result['isActive'] = $pageUrl == $url;
    +            $result['mtime'] = $category->updated_at;
    +        }
    +        elseif ($item->type == 'all-blog-posts') {
    +            $result = [
    +                'items' => []
    +            ];
    +
    +            $posts = self::isPublished()
    +                ->orderBy('title')
    +                ->get()
    +            ;
    +
    +            foreach ($posts as $post) {
    +                $postItem = [
    +                    'title' => $post->title,
    +                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
    +                    'mtime' => $post->updated_at
    +                ];
    +
    +                $postItem['isActive'] = $postItem['url'] == $url;
    +
    +                $result['items'][] = $postItem;
    +            }
    +        }
    +        elseif ($item->type == 'category-blog-posts') {
    +            if (!$item->reference || !$item->cmsPage) {
    +                return;
    +            }
    +
    +            $category = Category::find($item->reference);
    +            if (!$category) {
    +                return;
    +            }
    +
    +            $result = [
    +                'items' => []
    +            ];
    +
    +            $query = self::isPublished()
    +            ->orderBy('title');
    +
    +            $categories = $category->getAllChildrenAndSelf()->lists('id');
    +            $query->whereHas('categories', function($q) use ($categories) {
    +                $q->withoutGlobalScope(NestedTreeScope::class)->whereIn('id', $categories);
    +            });
    +
    +            $posts = $query->get();
    +
    +            foreach ($posts as $post) {
    +                $postItem = [
    +                    'title' => $post->title,
    +                    'url'   => self::getPostPageUrl($item->cmsPage, $post, $theme),
    +                    'mtime' => $post->updated_at
    +                ];
    +
    +                $postItem['isActive'] = $postItem['url'] == $url;
    +
    +                $result['items'][] = $postItem;
    +            }
    +        }
    +
    +        return $result;
    +    }
    +
    +    /**
    +     * Returns URL of a post page.
    +     *
    +     * @param $pageCode
    +     * @param $category
    +     * @param $theme
    +     */
    +    protected static function getPostPageUrl($pageCode, $category, $theme)
    +    {
    +        $page = CmsPage::loadCached($theme, $pageCode);
    +        if (!$page) {
    +            return;
    +        }
    +
    +        $properties = $page->getComponentProperties('blogPost');
    +        if (!isset($properties['slug'])) {
    +            return;
    +        }
    +
    +        /*
    +         * Extract the routing parameter name from the category filter
    +         * eg: {{ :someRouteParam }}
    +         */
    +        if (!preg_match('/^\{\{([^\}]+)\}\}$/', $properties['slug'], $matches)) {
    +            return;
    +        }
    +
    +        $paramName = substr(trim($matches[1]), 1);
    +        $params = [
    +            $paramName => $category->slug,
    +            'year'  => $category->published_at->format('Y'),
    +            'month' => $category->published_at->format('m'),
    +            'day'   => $category->published_at->format('d')
    +        ];
    +        $url = CmsPage::url($page->getBaseFileName(), $params);
    +
    +        return $url;
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/PostExport.php b/plugins/rainlab/blog/models/PostExport.php
    new file mode 100644
    index 0000000..024d94a
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/PostExport.php
    @@ -0,0 +1,94 @@
    + [
    +            'Backend\Models\User',
    +            'key' => 'user_id'
    +        ]
    +    ];
    +
    +    public $belongsToMany = [
    +        'post_categories' => [
    +            'RainLab\Blog\Models\Category',
    +            'table'    => 'rainlab_blog_posts_categories',
    +            'key'      => 'post_id',
    +            'otherKey' => 'category_id'
    +        ]
    +    ];
    +
    +    public $hasMany = [
    +        'featured_images' => [
    +            'System\Models\File',
    +            'order' => 'sort_order',
    +            'key' => 'attachment_id',
    +            'conditions' => "field = 'featured_images' AND attachment_type = 'RainLab\\\\Blog\\\\Models\\\\Post'"
    +        ]
    +    ];
    +
    +    /**
    +     * The accessors to append to the model's array form.
    +     * @var array
    +     */
    +    protected $appends = [
    +        'author_email',
    +        'categories',
    +        'featured_image_urls'
    +    ];
    +
    +    public function exportData($columns, $sessionKey = null)
    +    {
    +        $result = self::make()
    +            ->with([
    +                'post_user',
    +                'post_categories',
    +                'featured_images'
    +            ])
    +            ->get()
    +            ->toArray()
    +        ;
    +
    +        return $result;
    +    }
    +
    +    public function getAuthorEmailAttribute()
    +    {
    +        if (!$this->post_user) {
    +            return '';
    +        }
    +
    +        return $this->post_user->email;
    +    }
    +
    +    public function getCategoriesAttribute()
    +    {
    +        if (!$this->post_categories) {
    +            return '';
    +        }
    +
    +        return $this->encodeArrayValue($this->post_categories->lists('name'));
    +    }
    +
    +    public function getFeaturedImageUrlsAttribute()
    +    {
    +        if (!$this->featured_images) {
    +            return '';
    +        }
    +
    +        return $this->encodeArrayValue($this->featured_images->map(function ($image) {
    +            return $image->getPath();
    +        }));
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/PostImport.php b/plugins/rainlab/blog/models/PostImport.php
    new file mode 100644
    index 0000000..858d9f0
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/PostImport.php
    @@ -0,0 +1,155 @@
    + 'required',
    +        'content' => 'required'
    +    ];
    +
    +    protected $authorEmailCache = [];
    +
    +    protected $categoryNameCache = [];
    +
    +    public function getDefaultAuthorOptions()
    +    {
    +        return AuthorModel::all()->lists('full_name', 'email');
    +    }
    +
    +    public function getCategoriesOptions()
    +    {
    +        return Category::lists('name', 'id');
    +    }
    +
    +    public function importData($results, $sessionKey = null)
    +    {
    +        $firstRow = reset($results);
    +
    +        //  Validation
    +        if ($this->auto_create_categories && !array_key_exists('categories', $firstRow)) {
    +            throw new ApplicationException('Please specify a match for the Categories column.');
    +        }
    +
    +        // Import
    +        foreach ($results as $row => $data) {
    +            try {
    +
    +                if (!$title = array_get($data, 'title')) {
    +                    $this->logSkipped($row, 'Missing post title');
    +                    continue;
    +                }
    +
    +                // Find or create
    +                $post = Post::make();
    +
    +                if ($this->update_existing) {
    +                    $post = $this->findDuplicatePost($data) ?: $post;
    +                }
    +
    +                $postExists = $post->exists;
    +
    +                // Set attributes
    +                $except = ['id', 'categories', 'author_email'];
    +
    +                foreach (array_except($data, $except) as $attribute => $value) {
    +                    if (in_array($attribute, $post->getDates()) && empty($value)) {
    +                        continue;
    +                    }
    +                    $post->{$attribute} = isset($value) ? $value : null;
    +                }
    +
    +                if ($author = $this->findAuthorFromEmail($data)) {
    +                    $post->user_id = $author->id;
    +                }
    +
    +                $post->forceSave();
    +
    +                if ($categoryIds = $this->getCategoryIdsForPost($data)) {
    +                    $post->categories()->sync($categoryIds, false);
    +                }
    +
    +                // Log results
    +                if ($postExists) {
    +                    $this->logUpdated();
    +                }
    +                else {
    +                    $this->logCreated();
    +                }
    +            }
    +            catch (Exception $ex) {
    +                $this->logError($row, $ex->getMessage());
    +            }
    +        }
    +    }
    +
    +    protected function findAuthorFromEmail($data)
    +    {
    +        if (!$email = array_get($data, 'email', $this->default_author)) {
    +            return null;
    +        }
    +
    +        if (isset($this->authorEmailCache[$email])) {
    +            return $this->authorEmailCache[$email];
    +        }
    +
    +        $author = AuthorModel::where('email', $email)->first();
    +        return $this->authorEmailCache[$email] = $author;
    +    }
    +
    +    protected function findDuplicatePost($data)
    +    {
    +        if ($id = array_get($data, 'id')) {
    +            return Post::find($id);
    +        }
    +
    +        $title = array_get($data, 'title');
    +        $post = Post::where('title', $title);
    +
    +        if ($slug = array_get($data, 'slug')) {
    +            $post->orWhere('slug', $slug);
    +        }
    +
    +        return $post->first();
    +    }
    +
    +    protected function getCategoryIdsForPost($data)
    +    {
    +        $ids = [];
    +
    +        if ($this->auto_create_categories) {
    +            $categoryNames = $this->decodeArrayValue(array_get($data, 'categories'));
    +
    +            foreach ($categoryNames as $name) {
    +                if (!$name = trim($name)) {
    +                    continue;
    +                }
    +
    +                if (isset($this->categoryNameCache[$name])) {
    +                    $ids[] = $this->categoryNameCache[$name];
    +                }
    +                else {
    +                    $newCategory = Category::firstOrCreate(['name' => $name]);
    +                    $ids[] = $this->categoryNameCache[$name] = $newCategory->id;
    +                }
    +            }
    +        }
    +        elseif ($this->categories) {
    +            $ids = (array) $this->categories;
    +        }
    +
    +        return $ids;
    +    }
    +}
    diff --git a/plugins/rainlab/blog/models/Settings.php b/plugins/rainlab/blog/models/Settings.php
    new file mode 100644
    index 0000000..2c27a53
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/Settings.php
    @@ -0,0 +1,18 @@
    + ['boolean'],
    +    ];
    +}
    diff --git a/plugins/rainlab/blog/models/category/columns.yaml b/plugins/rainlab/blog/models/category/columns.yaml
    new file mode 100644
    index 0000000..578d846
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/category/columns.yaml
    @@ -0,0 +1,13 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    name:
    +        label: rainlab.blog::lang.category.name
    +        searchable: true
    +
    +    post_count:
    +        label: rainlab.blog::lang.category.posts
    +        sortable: false
    diff --git a/plugins/rainlab/blog/models/category/fields.yaml b/plugins/rainlab/blog/models/category/fields.yaml
    new file mode 100644
    index 0000000..adcf1bc
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/category/fields.yaml
    @@ -0,0 +1,23 @@
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    name:
    +        label: rainlab.blog::lang.category.name
    +        placeholder: rainlab.blog::lang.category.name_placeholder
    +        span: left
    +
    +    slug:
    +        label: rainlab.blog::lang.category.slug
    +        span: right
    +        placeholder: rainlab.blog::lang.category.slug_placeholder
    +        preset: name
    +
    +    description:
    +        label: 'rainlab.blog::lang.category.description'
    +        size: large
    +        oc.commentPosition: ''
    +        span: full
    +        type: textarea
    diff --git a/plugins/rainlab/blog/models/post/columns.yaml b/plugins/rainlab/blog/models/post/columns.yaml
    new file mode 100644
    index 0000000..6567fcc
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/columns.yaml
    @@ -0,0 +1,36 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    title:
    +        label: rainlab.blog::lang.post.title
    +        searchable: true
    +
    +    # author:
    +    #   label: Author
    +    #   relation: user
    +    #   select: login
    +    #   searchable: true
    +
    +    categories:
    +        label: rainlab.blog::lang.post.categories
    +        relation: categories
    +        select: name
    +        searchable: true
    +        sortable: false
    +
    +    created_at:
    +        label: rainlab.blog::lang.post.created
    +        type: date
    +        invisible: true
    +
    +    updated_at:
    +        label: rainlab.blog::lang.post.updated
    +        type: date
    +        invisible: true
    +
    +    published_at:
    +        label: rainlab.blog::lang.post.published
    +        type: date
    diff --git a/plugins/rainlab/blog/models/post/fields.yaml b/plugins/rainlab/blog/models/post/fields.yaml
    new file mode 100644
    index 0000000..ed7f590
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/fields.yaml
    @@ -0,0 +1,77 @@
    +# ===================================
    +#  Field Definitions
    +# ===================================
    +
    +fields:
    +    title:
    +        label: rainlab.blog::lang.post.title
    +        span: left
    +        placeholder: rainlab.blog::lang.post.title_placeholder
    +
    +    slug:
    +        label: rainlab.blog::lang.post.slug
    +        span: right
    +        placeholder: rainlab.blog::lang.post.slug_placeholder
    +        preset:
    +            field: title
    +            type: slug
    +
    +    toolbar:
    +        type: partial
    +        path: post_toolbar
    +        cssClass: collapse-visible
    +
    +secondaryTabs:
    +    stretch: true
    +    fields:
    +        content:
    +            tab: rainlab.blog::lang.post.tab_edit
    +            type: RainLab\Blog\FormWidgets\BlogMarkdown
    +            cssClass: field-slim blog-post-preview
    +            stretch: true
    +            mode: split
    +
    +        categories:
    +            tab: rainlab.blog::lang.post.tab_categories
    +            type: relation
    +            commentAbove: rainlab.blog::lang.post.categories_comment
    +            placeholder: rainlab.blog::lang.post.categories_placeholder
    +
    +        published:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published
    +            span: left
    +            type: checkbox
    +
    +        user:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published_by
    +            span: right
    +            type: dropdown
    +            emptyOption: rainlab.blog::lang.post.current_user
    +
    +        published_at:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.published_on
    +            span: left
    +            cssClass: checkbox-align
    +            type: datepicker
    +            mode: datetime
    +            trigger:
    +                action: enable
    +                field: published
    +                condition: checked
    +
    +        excerpt:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.excerpt
    +            type: textarea
    +            size: small
    +
    +        featured_images:
    +            tab: rainlab.blog::lang.post.tab_manage
    +            label: rainlab.blog::lang.post.featured_images
    +            type: fileupload
    +            mode: image
    +            imageWidth: 200
    +            imageHeight: 200
    diff --git a/plugins/rainlab/blog/models/post/scopes.yaml b/plugins/rainlab/blog/models/post/scopes.yaml
    new file mode 100644
    index 0000000..6ddfc4c
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/post/scopes.yaml
    @@ -0,0 +1,43 @@
    +# ===================================
    +# Filter Scope Definitions
    +# ===================================
    +
    +scopes:
    +
    +    category:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_category
    +
    +        # Model Class name
    +        modelClass: RainLab\Blog\Models\Category
    +
    +        # Model attribute to display for the name
    +        nameFrom: name
    +
    +        # Apply query scope
    +        scope: FilterCategories
    +
    +    published:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_published
    +
    +        # Filter type
    +        type: switch
    +
    +        # SQL Conditions
    +        conditions:
    +            - published <> '1'
    +            - published = '1'
    +
    +    created_at:
    +
    +        # Filter name
    +        label: rainlab.blog::lang.posts.filter_date
    +
    +        # Filter type
    +        type: daterange
    +
    +        # SQL Conditions
    +        conditions: created_at >= ':after' AND created_at <= ':before'
    diff --git a/plugins/rainlab/blog/models/postexport/columns.yaml b/plugins/rainlab/blog/models/postexport/columns.yaml
    new file mode 100644
    index 0000000..b500a8f
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postexport/columns.yaml
    @@ -0,0 +1,19 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    id: ID
    +    title: rainlab.blog::lang.post.title
    +    content: rainlab.blog::lang.post.content
    +    content_html: rainlab.blog::lang.post.content_html
    +    excerpt: rainlab.blog::lang.post.excerpt
    +    slug: rainlab.blog::lang.post.slug
    +    categories: rainlab.blog::lang.post.categories
    +    author_email: rainlab.blog::lang.post.author_email
    +    featured_image_urls: rainlab.blog::lang.post.featured_images
    +    created_at: rainlab.blog::lang.post.created_date
    +    updated_at: rainlab.blog::lang.post.updated_date
    +    published: rainlab.blog::lang.post.published
    +    published_at: rainlab.blog::lang.post.published_date
    diff --git a/plugins/rainlab/blog/models/postimport/columns.yaml b/plugins/rainlab/blog/models/postimport/columns.yaml
    new file mode 100644
    index 0000000..a2584ea
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postimport/columns.yaml
    @@ -0,0 +1,17 @@
    +# ===================================
    +#  Column Definitions
    +# ===================================
    +
    +columns:
    +
    +    id: ID
    +    title: rainlab.blog::lang.post.title
    +    content: rainlab.blog::lang.post.content
    +    excerpt: rainlab.blog::lang.post.excerpt
    +    slug: rainlab.blog::lang.post.slug
    +    categories: rainlab.blog::lang.post.categories
    +    author_email: rainlab.blog::lang.post.author_email
    +    created_at: rainlab.blog::lang.post.created_date
    +    updated_at: rainlab.blog::lang.post.updated_date
    +    published: rainlab.blog::lang.post.published
    +    published_at: rainlab.blog::lang.post.published_date
    diff --git a/plugins/rainlab/blog/models/postimport/fields.yaml b/plugins/rainlab/blog/models/postimport/fields.yaml
    new file mode 100644
    index 0000000..3e7600a
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/postimport/fields.yaml
    @@ -0,0 +1,37 @@
    +# ===================================
    +#  Form Field Definitions
    +# ===================================
    +
    +fields:
    +
    +    update_existing:
    +        label: rainlab.blog::lang.import.update_existing_label
    +        comment: rainlab.blog::lang.import.update_existing_comment
    +        type: checkbox
    +        default: true
    +        span: left
    +
    +    auto_create_categories:
    +        label: rainlab.blog::lang.import.auto_create_categories_label
    +        comment: rainlab.blog::lang.import.auto_create_categories_comment
    +        type: checkbox
    +        default: true
    +        span: right
    +
    +    categories:
    +        label: rainlab.blog::lang.import.categories_label
    +        commentAbove: rainlab.blog::lang.import.categories_comment
    +        type: checkboxlist
    +        span: right
    +        cssClass: field-indent
    +        trigger:
    +            action: hide
    +            field: auto_create_categories
    +            condition: checked
    +
    +    default_author:
    +        label: rainlab.blog::lang.import.default_author_label
    +        comment: rainlab.blog::lang.import.default_author_comment
    +        type: dropdown
    +        placeholder: rainlab.blog::lang.import.default_author_placeholder
    +        span: left
    diff --git a/plugins/rainlab/blog/models/settings/fields.yaml b/plugins/rainlab/blog/models/settings/fields.yaml
    new file mode 100644
    index 0000000..3138a59
    --- /dev/null
    +++ b/plugins/rainlab/blog/models/settings/fields.yaml
    @@ -0,0 +1,17 @@
    +tabs:
    +    fields:
    +        show_all_posts:
    +            span: auto
    +            label: rainlab.blog::lang.blog.show_all_posts_label
    +            comment: rainlab.blog::lang.blog.show_all_posts_comment
    +            type: switch
    +            default: 1
    +            tab: rainlab.blog::lang.blog.tab_general
    +
    +        use_legacy_editor:
    +            span: auto
    +            label: rainlab.blog::lang.blog.use_legacy_editor_label
    +            comment: rainlab.blog::lang.blog.use_legacy_editor_comment
    +            type: switch
    +            default: 0
    +            tab: rainlab.blog::lang.blog.tab_general
    diff --git a/plugins/rainlab/blog/updates/categories_add_nested_fields.php b/plugins/rainlab/blog/updates/categories_add_nested_fields.php
    new file mode 100644
    index 0000000..c5a2bd2
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/categories_add_nested_fields.php
    @@ -0,0 +1,34 @@
    +integer('parent_id')->unsigned()->index()->nullable();
    +            $table->integer('nest_left')->nullable();
    +            $table->integer('nest_right')->nullable();
    +            $table->integer('nest_depth')->nullable();
    +        });
    +
    +        foreach (CategoryModel::all() as $category) {
    +            $category->setDefaultLeftAndRight();
    +            $category->save();
    +        }
    +    }
    +
    +    public function down()
    +    {
    +    }
    +
    +}
    \ No newline at end of file
    diff --git a/plugins/rainlab/blog/updates/create_categories_table.php b/plugins/rainlab/blog/updates/create_categories_table.php
    new file mode 100644
    index 0000000..988a9d4
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/create_categories_table.php
    @@ -0,0 +1,41 @@
    +engine = 'InnoDB';
    +            $table->increments('id');
    +            $table->string('name')->nullable();
    +            $table->string('slug')->nullable()->index();
    +            $table->string('code')->nullable();
    +            $table->text('description')->nullable();
    +            $table->integer('parent_id')->unsigned()->index()->nullable();
    +            $table->integer('nest_left')->nullable();
    +            $table->integer('nest_right')->nullable();
    +            $table->integer('nest_depth')->nullable();
    +            $table->timestamps();
    +        });
    +
    +        Schema::create('rainlab_blog_posts_categories', function($table)
    +        {
    +            $table->engine = 'InnoDB';
    +            $table->integer('post_id')->unsigned();
    +            $table->integer('category_id')->unsigned();
    +            $table->primary(['post_id', 'category_id']);
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        Schema::dropIfExists('rainlab_blog_categories');
    +        Schema::dropIfExists('rainlab_blog_posts_categories');
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/create_posts_table.php b/plugins/rainlab/blog/updates/create_posts_table.php
    new file mode 100644
    index 0000000..8ec081b
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/create_posts_table.php
    @@ -0,0 +1,32 @@
    +engine = 'InnoDB';
    +            $table->increments('id');
    +            $table->integer('user_id')->unsigned()->nullable()->index();
    +            $table->string('title')->nullable();
    +            $table->string('slug')->index();
    +            $table->text('excerpt')->nullable();
    +            $table->longText('content')->nullable();
    +            $table->longText('content_html')->nullable();
    +            $table->timestamp('published_at')->nullable();
    +            $table->boolean('published')->default(false);
    +            $table->timestamps();
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        Schema::dropIfExists('rainlab_blog_posts');
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/posts_add_metadata.php b/plugins/rainlab/blog/updates/posts_add_metadata.php
    new file mode 100644
    index 0000000..ccd96bb
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/posts_add_metadata.php
    @@ -0,0 +1,31 @@
    +mediumText('metadata')->nullable();
    +        });
    +    }
    +
    +    public function down()
    +    {
    +        if (Schema::hasColumn('rainlab_blog_posts', 'metadata')) {
    +            Schema::table('rainlab_blog_posts', function ($table) {
    +                $table->dropColumn('metadata');
    +            });
    +        }
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/seed_all_tables.php b/plugins/rainlab/blog/updates/seed_all_tables.php
    new file mode 100644
    index 0000000..7be9344
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/seed_all_tables.php
    @@ -0,0 +1,34 @@
    + 'First blog post',
    +            'slug' => 'first-blog-post',
    +            'content' => '
    +This is your first ever **blog post**! It might be a good idea to update this post with some more relevant content.
    +
    +You can edit this content by selecting **Blog** from the administration back-end menu.
    +
    +*Enjoy the good times!*
    +            ',
    +            'excerpt' => 'The first ever blog post is here. It might be a good idea to update this post with some more relevant content.',
    +            'published_at' => Carbon::now(),
    +            'published' => true
    +        ]);
    +
    +        Category::create([
    +            'name' => trans('rainlab.blog::lang.categories.uncategorized'),
    +            'slug' => 'uncategorized',
    +        ]);
    +    }
    +
    +}
    diff --git a/plugins/rainlab/blog/updates/update_timestamp_nullable.php b/plugins/rainlab/blog/updates/update_timestamp_nullable.php
    new file mode 100644
    index 0000000..7de7961
    --- /dev/null
    +++ b/plugins/rainlab/blog/updates/update_timestamp_nullable.php
    @@ -0,0 +1,20 @@
    + "Builder",
    +            'description' => "Provides visual tools for building October plugins.",
    +            'author' => 'Alexey Bobkov, Samuel Georges',
    +            'icon' => 'icon-wrench',
    +            'homepage' => 'https://github.com/rainlab/builder-plugin'
    +        ];
    +    }
    +
    +    /**
    +     * registerComponents
    +     */
    +    public function registerComponents()
    +    {
    +        return [
    +            \RainLab\Builder\Components\RecordList::class => 'builderList',
    +            \RainLab\Builder\Components\RecordDetails::class => 'builderDetails'
    +        ];
    +    }
    +
    +    /**
    +     * registerPermissions
    +     */
    +    public function registerPermissions()
    +    {
    +        return [
    +            'rainlab.builder.manage_plugins' => [
    +                'tab' => "Builder",
    +                'label' => 'rainlab.builder::lang.plugin.manage_plugins'
    +            ]
    +        ];
    +    }
    +
    +    /**
    +     * registerNavigation
    +     */
    +    public function registerNavigation()
    +    {
    +        return [
    +            'builder' => [
    +                'label' => "Builder",
    +                'url' => Backend::url('rainlab/builder'),
    +                'icon' => 'icon-wrench',
    +                'iconSvg' => 'plugins/rainlab/builder/assets/images/builder-icon.svg',
    +                'permissions' => ['rainlab.builder.manage_plugins'],
    +                'order' => 400,
    +                'useDropdown' => false,
    +
    +                'sideMenu' => [
    +                    'database' => [
    +                        'label' => 'rainlab.builder::lang.database.menu_label',
    +                        'icon' => 'icon-hdd-o',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'database'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'models' => [
    +                        'label' => 'rainlab.builder::lang.model.menu_label',
    +                        'icon' => 'icon-random',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'models'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'permissions' => [
    +                        'label' => 'rainlab.builder::lang.permission.menu_label',
    +                        'icon' => 'icon-unlock-alt',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-no-side-panel' => 'true', 'data-builder-command' => 'permission:cmdOpenPermissions', 'data-menu-item' => 'permissions'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'menus' => [
    +                        'label' => 'rainlab.builder::lang.menu.menu_label',
    +                        'icon' => 'icon-location-arrow',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-no-side-panel' => 'true', 'data-builder-command' => 'menus:cmdOpenMenus', 'data-menu-item' => 'menus'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'controllers' => [
    +                        'label' => 'rainlab.builder::lang.controller.menu_label',
    +                        'icon' => 'icon-asterisk',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'controllers'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'versions' => [
    +                        'label' => 'rainlab.builder::lang.version.menu_label',
    +                        'icon' => 'icon-code-fork',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'version'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'localization' => [
    +                        'label' => 'rainlab.builder::lang.localization.menu_label',
    +                        'icon' => 'icon-globe',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'localization'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'code' => [
    +                        'label' => 'Code',
    +                        'icon' => 'icon-file-code-o',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-menu-item' => 'code'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ],
    +                    'imports' => [
    +                        'label' => 'Import',
    +                        'icon' => 'icon-arrow-circle-down',
    +                        'url' => 'javascript:;',
    +                        'attributes' => ['data-no-side-panel' => 'true', 'data-builder-command' => 'imports:cmdOpenImports', 'data-menu-item' => 'imports'],
    +                        'permissions' => ['rainlab.builder.manage_plugins']
    +                    ]
    +                ]
    +
    +            ]
    +        ];
    +    }
    +
    +    /**
    +     * registerSettings
    +     */
    +    public function registerSettings()
    +    {
    +        return [
    +            'config' => [
    +                'label' => 'Builder',
    +                'icon' => 'icon-wrench',
    +                'description' => 'Set your author name and namespace for plugin creation.',
    +                'class' => 'RainLab\Builder\Models\Settings',
    +                'permissions' => ['rainlab.builder.manage_plugins'],
    +                'order' => 600
    +            ]
    +        ];
    +    }
    +
    +    /**
    +     * boot
    +     */
    +    public function boot()
    +    {
    +        Event::listen('pages.builder.registerControls', function ($controlLibrary) {
    +            new StandardControlsRegistry($controlLibrary);
    +        });
    +
    +        Event::listen('pages.builder.registerControllerBehaviors', function ($behaviorLibrary) {
    +            new StandardBehaviorsRegistry($behaviorLibrary);
    +        });
    +
    +        Event::listen('pages.builder.registerTailorBlueprints', function ($blueprintLibrary) {
    +            new StandardBlueprintsRegistry($blueprintLibrary);
    +        });
    +
    +        // Register reserved keyword validation
    +        Event::listen('translator.beforeResolve', function ($key, $replaces, $locale) {
    +            if ($key === 'validation.reserved') {
    +                return Lang::get('rainlab.builder::lang.validation.reserved');
    +            }
    +        });
    +
    +        $this->callAfterResolving('validator', function ($validator) {
    +            $validator->extend('reserved', Reserved::class);
    +            $validator->replacer('reserved', function ($message, $attribute, $rule, $parameters) {
    +                // Fixes lowercase attribute names in the new plugin modal form
    +                return ucfirst($message);
    +            });
    +        });
    +
    +        // Register doctrine types
    +        if (!DoctrineType::hasType('timestamp')) {
    +            DoctrineType::addType('timestamp', \RainLab\Builder\Classes\Doctrine\TimestampType::class);
    +        }
    +    }
    +}
    diff --git a/plugins/rainlab/builder/README.md b/plugins/rainlab/builder/README.md
    new file mode 100644
    index 0000000..7596674
    --- /dev/null
    +++ b/plugins/rainlab/builder/README.md
    @@ -0,0 +1,391 @@
    +Builder is a visual development tool. It shortens plugin development time by automating common development tasks and makes programming fun again. With Builder you can create a fully functional plugin scaffold in a matter of minutes.
    +
    +Builder makes the learning curve less steep by providing a visual interface that naturally incorporates October's design patterns and documentation. Here’s an example, instead of looking into the documentation for a list of supported form controls and their features, you can just open the Form Builder, find a suitable control in the Control Palette, add the control to the form and explore its properties with the visual inspector.
    +
    +Builder implements a Rapid Application Development process that automates the boring activities without sacrificing complete control. With this tool you can spend more time implementing the plugin's business logic in your favorite code editor rather than dealing with the more mundane tasks, like building forms or managing plugin versions.
    +
    +Plugins created with the help of Builder are no different to plugins that you would usually create by hand. That means that you can continue to use your usual “hands on” workflow for updating your servers, managing the code versions and sharing work with your teammates.
    +
    +**What's New!** Builder has been revamped to support October CMS v3, including dark mode, updated specifications, a Tailor import tool, and a new inline code editor.
    +
    +## Requirements
    +
    +- October CMS 3.3 or above
    +
    +### Installation
    +
    +Run the following to install this plugin:
    +
    +```bash
    +php artisan plugin:install RainLab.Builder
    +```
    +
    +To uninstall this plugin:
    +
    +```bash
    +php artisan plugin:remove RainLab.Builder
    +```
    +
    +If you are using October CMS v1 or v2, install v1.2 with the following commands:
    +
    +```bash
    +composer require rainlab/builder-plugin "^1.2"
    +```
    +
    +## Video Tutorial
    +
    +We also recorded a video tutorial showing how to use the plugin to build a simple library plugin: [watch the video](https://vimeo.com/154415433).
    +
    +## What You Can Do with Builder
    +
    +This tool includes multiple features that cover almost all aspects of creating a plugin.
    +
    +- Initializing a new plugin - this creates the plugin directory along with any necessary files.
    +- Creating and editing plugin database tables. All schema changes are saved as regular migration files, so you can easily update the plugin on other servers using your regular workflow.
    +- Creating model classes.
    +- Creating backend forms with the visual Form Builder.
    +- Creating backend lists.
    +- Managing a list of user permissions provided by the plugin.
    +- Creating plugin backend navigation - in the form of main menu items and sidebar items.
    +- Creating backend controllers and configuring their behaviors with a visual tool.
    +- Managing plugin versions and updates.
    +- Managing plugin localization files.
    +- A set of universal components - used for displaying data from the plugin on the front-end in form of lists and single record details.
    +- Managing raw code files directly in the backend (**New in v2**).
    +- Converting [Tailor blueprints](https://docs.octobercms.com/3.x/cms/tailor/blueprints.html) to plugin files (**New in v2**).
    +
    +Put simply, you can create a multilingual plugin, that installs database tables, has backend lists and forms protected with user permissions, and adds CMS pages for displaying data managed with the plugin. After learning how Builder works, this process takes just a few minutes.
    +
    +Builder is a productivity tool, it doesn't completely replace coding by hand and doesn't include a code editor for editing PHP files (the only exception is the version management interface). Builder never overwrites or deletes plugin PHP files, so you can rest assured knowing that the code you write never gets touched by Builder. However, Builder can create new PHP files, like models and controllers.
    +
    +Most of the visual editors in Builder work with YAML configuration files, which are a native concept in October CMS. For example, after creating a model class in Builder, you can choose to add a form to the model. This operation creates a YAML file in the model's directory.
    +
    +There are currently some limitations when using Builder. Some of them are missing features which will be added later. Others are ideas intentionally omitted to keep the things simple. As mentioned above, Builder doesn't want to replace coding, while at the same time, it doesn't go too far with visual programming either. The limitations are explained and described in the corresponding sections of the plugin documentation. Those limitations don't mean you can't create any plugin you want - the good old approach to writing the code manually is always applicable for plugins developed with Builder. Builder’s aim is to be a modest, yet powerful tool that is used to accelerate your development cycle.
    +
    +## Getting Started
    +
    +Before you create your first plugin with Builder you should configure it. Open the Settings page in October CMS backend and find Builder in the side menu. Enter your author name and namespace. The author name and namespace are required fields and should not change if you wish to publish your plugins on October CMS Marketplace.
    +
    +If you already have a Marketplace account, use your existing author name and namespace.
    +
    +## Initializing a New Plugin
    +
    +On the Builder page in October CMS backend click the small arrow icon in the sidebar to expose the plugin list. After clicking the "Create plugin" button, enter the plugin name and namespace. The default author name and namespace can be pre-filled from the plugin settings. Select the plugin icon, enter the description text and plugin homepage URL (optional).
    +
    +Please note that you cannot change the namespaces after you create the plugin.
    +
    +When Builder initializes a plugin, it creates the following files and directories in October's **plugins** directory:
    +
    +```
    +authornamespace
    +    pluginnamespace
    +       classes
    +       lang
    +           en
    +               lang.php
    +       updates
    +           version.yaml
    +       Plugin.php
    +       plugin.yaml
    +```
    +
    +The file **plugin.yaml** contains the basic plugin information - name, description, permissions and backend navigation. This file is managed by the Builder user interface.
    +
    +The initial contents of the **lang.php** localization file is the plugin name and description. The localization file is created in the default locale of your October installation.
    +
    +When a new plugin is created, it's automatically selected as the current plugin that Builder works with. You can select another plugin in the plugin list if you need.
    +
    +## Managing Plugin Database Tables
    +
    +Tables are managed on the Database tab of Builder. You can create tables, update their structure and delete tables with the visual interface.
    +
    +Click Add button to open the Create Table tab. Builder automatically generates prefixes for plugin tables. The prefixes are compliant with the [Developer Guidelines](http://octobercms.com/docs/help/developer-guide) for the Marketplace plugins.
    +
    +Every time when you save changes in a table, Builder shows a popup window with the automatically generated migration PHP code. You can't edit the code in the popup, but you can inspect it or copy to the clipboard. After reviewing the migration, click Save & Apply button. Builder executes the migration immediately and saves the migration file to the plugin's **updates** directory. Afterwards you can find all plugin migrations on the Versions tab of Builder.
    +
    +> **Important**: Although Builder generates migration files automatically, it can't prevent data loss in some cases when you significantly change the table structure. In some cases it's possible - for example, when you alter length of a string column. Always check the migration PHP code generated by Builder before applying the migration and consider possible consequences of running the migration in a production database.
    +
    +Currently Builder doesn't allow to manage table indexes with the visual user interface. Unique column management is not supported yet as well. Please use the Version Management feature to manually create migration files.
    +
    +Please note that the `enum` data type is not currently supported by the Builder due to limitations in the underlying Doctrine classes.
    +
    +## Managing Models
    +
    +You can edit models on the Models tab of Builder. Click the Add button, enter the model class name and select a database table from the drop-down list.
    +
    +The model class name should not contain the namespace. Some examples: Post, Product, Category.
    +
    +Please note that you cannot delete model files with builder because it would contradict the idea of not deleting or overwriting PHP files with the visual tool. If you need to delete a model, remove its files manually.
    +
    +## Managing Backend Forms
    +
    +In October CMS forms belong to models. For every model you can create as many backend forms as you need, but in most cases there is a single form per model.
    +
    +> **Note**: When you create a form, it's not displayed in October CMS backend until you create a backend controller which uses the form. Read more about controllers below.
    +
    +On the Models tab in Builder find a model you want to create a form for. Expand the model if needed, hover the **Forms** section and click the plus sign.
    +
    +Forms in October CMS are defined with YAML files. The default form file name is **fields.yaml**. In the Form Builder, click a placeholder and select a control from the popup list. After that you can click the control and edit it parameters in Inspector.
    +
    +Almost all form controls have these common properties:
    +
    +* Field name - a model field name. It's an autocomplete field in Inspector, which allows you to select column names from the underlying database table. Currently relations are not displayed in the autocomplete hints, this will be implemented later. You can enter any model property manually.
    +* Label - The control label. You can enter a static text string to the field, or create a new localization string by clicking the plus sign in the input field. Almost all editors in Builder support this feature.
    +* Comment - the comment text - fixed text or localization string key.
    +* Span - position of the control on the form - left, right, full or automatic placement.
    +
    +Most of the properties have descriptive names or have a description in Inspector. If you need more information about control properties please refer to the [Documentation](https://docs.octobercms.com/3.x/element/form-fields.html).
    +
    +You can drag controls in the Form Builder to rearrange them or to move them to/from form tabs. A form tab should have at least one control, otherwise it will be ignored when Form Builder saves the YAML file.
    +
    +Some form controls, for example the file upload control, require a relation to be created in the model class manually. The relation name should be entered in the Field name property. Please read the [Forms Documentation](https://docs.octobercms.com/3.x/element/form-fields.html) for details about specific form controls.
    +
    +## Managing Backend Lists
    +
    +Similarly to forms, backend lists in October CMS belong to models.
    +
    +> **Note**: When you create a list, it's not displayed in October CMS backend until you create a backend controller which uses the list. Read about controllers below.
    +
    +On the Models tab in Builder find a model you want to create a list for. Expand the model if needed, hover the **Lists** section and click the plus sign.
    +
    +Lists in October CMS are defined with YAML files. The default list file name is **columns.yaml**. The grid in the list editor contains list column definitions. Column property names are self descriptive, although some of them require some explanations. Refer to the [Lists documentation](https://docs.octobercms.com/3.x/element/list-columns.html) for details about each property.
    +
    +For the Label property you can either enter a static string or create a new localization string.
    +
    +The Field property column has an autocompletion feature attached. It allows you to select columns from the database table that is bound to the model. At the moment it doesn't show relation properties, but you can still type them in manually.
    +
    +## Managing Plugin Permissions
    +
    +[Plugin permissions](https://docs.octobercms.com/3.x/extend/backend/users.html) define the features and backend plugin pages a user can access. You can manage permissions on the Permissions tab in Builder. For each permission you should specify a unique permission code, permission tab title and permission label. The tab title and label are displayed in the user management interface on the System page in October backend.
    +
    +For the tab title and label you can either enter a static string or create a new localization string.
    +
    +Later, when you create controllers and menu items, you can select what permissions users should have in order to access or see those objects.
    +
    +## Managing Backend Menus
    +
    +The [plugin navigation](https://docs.octobercms.com/3.x/extend/backend/navigation.html) is managed on the Backend Menus tab of the Builder. The user interface allows to create top level menu items and sidebar items.
    +
    +To create a menu item click the placeholder rectangle and then click the new item to open Inspector. In the inspector you can enter the item label, select icon and assign user permissions. The **code** property is required for referring menu items from the controllers code (for marking menu items active).
    +
    +> **Note**: When you create menu items for backend pages which don't exist yet in the plugin, it makes sense to leave the **URL** property empty until you create the plugin controllers. This property supports autocompletion, so you can just select your controller URLs from the drop-down list.
    +
    +## Managing Backend Controllers, Forms and Lists
    +
    +Back-end pages in October CMS are provided with backend controllers. Usually backend pages contain lists and forms for managing plugin records, although you can create any custom controller.
    +
    +Please refer to the [backend forms](https://docs.octobercms.com/3.x/extend/forms/form-controller.html) and [lists](https://docs.octobercms.com/3.x/extend/lists/list-controller.html) documentation pages for more information about controller behaviors. Currently only List and Form behaviors can be configured with the Builder. If your controller contains other behaviors they won't be removed by the Builder, you just won't be able to edit them with the visual interface.
    +
    +Builder also allows you to create empty controller classes which don't implement any behaviors and customize them manually.
    +
    +> **Note**: Some behaviors require specific model features to be implemented. For example, the Reorder Controller behavior requires the model to implement Sortable or NestedTree traits. Always refer to the specific behavior documentation for the implementation details.
    +
    +To create a controller, click the Add button list on the Controllers tab. Enter the controller class name, for example Posts.
    +
    +If the controller is going to provide backend lists or forms, select a base model in the drop-down list and select behaviors you want to add. You can also select a top and sidebar menu items that should be active on the controller pages. If needed, choose permissions that users must have to access the controller pages.
    +
    +> Please note that the settings you enter in the Create Controller popup cannot be changed with Builder. However you can update them manually by editing controller classes.
    +
    +After creating a controller you can configure its behaviors. Click the controller in list and then click a behavior you want to configure. When Builder creates a controller it tries to apply default configuration to the behaviors, however you might want to change it. Inspector lists displays all supported behavior properties. URL properties (like the list records URLs) are autocomplete fields and populated with URLs of the existing plugin controllers.
    +
    +## Managing Plugin Versions
    +
    +Please read the [Version History](https://docs.octobercms.com/3.x/extend/system/plugins.html#version-history) documentation page to understand how versioning works in October CMS.
    +
    +Basically there are 3 types of version updates:
    +
    +1. Updates which change the database structure - migrations. Builder can generate migration files automatically when you make changes in the DB schema on the Database tab.
    +2. Seeding updates, which populate the database contents.
    +3. Version updates, which do not update anything in the database but are often used for releasing code changes.
    +
    +Plugin versions are managed on the Versions tab of the Builder. This tab displays a list of existing plugin versions their status. Applied versions have a green checkbox marker. Pending versions have a grey clock marker.
    +
    +You can create a new version with clicking the Add button and selecting the update time. The user interface automatically generates scaffold PHP code for the "Migration" and "Seeder" updates. The "Increase the version number" updates won’t contain any PHP code.
    +
    +For every version you should specify the new version number and description. Builder generates the version number automatically by increasing the last digit in the existing version. You might want to change it if you're releasing a major version update and want to change the first or second digit.
    +
    +When a version file is saved, Builder doesn't apply it immediately. You should click the "Apply version" button in the toolbar in order to apply the version and execute the update code (if applicable). You can also rollback already applied version updates, change their code and apply again. This allows you to edit database schema updates generated by Builder if you don't like the default code.
    +
    +> **Note**: Your migration files should provide correct rollback code in the `down` method in order to use the rollback feature.
    +
    +When you rollback a version, it automatically rolls back all newer versions. When you apply a version, it automatically applies all pending older versions. Please remember that when a user logs into the backend, October automatically applies all pending updates. Never edit versions on a production server or on a server with multiple backend users - it could cause unpredictable consequences.
    +
    +Migrations that contain multiple scripts are not supported. They can't be created or edited with Builder.
    +
    +## Managing Localization
    +
    +Localization files are managed on the Localization tab of the Builder. When a new plugin is initialized, a single language file is created. This file is created in the default system locale specified in October CMS configuration scripts.
    +
    +You can create as many language files as you want. Builder UI always displays strings in the October CMS locale, so you might want to update your configuration files to see your plugin in another language.
    +
    +Please note that although [localization files in October CMS](https://docs.octobercms.com/3.x/extend/system/localization.html) are PHP scripts, they are translated to YAML to simplify the editing in the Builder user interface. When language files are saved, they are translated back to PHP again.
    +
    +Builder tries to keep the user interface synchronized with your default language file. This means that when you save the language file, Builder automatically updates all localized strings in all editors. In some cases you might need to close and open Inspector in order to re-initialize the autocomplete fields.
    +
    +In many cases you can create new localization strings on-the-fly from the Builder editors - the Form Builder, Menu Builder, etc. The localization input field has the plus icon on the right side. Clicking the plus icon opens a popup window that allows you to enter the localization string key and value. The string key can contain dots to mark the localization file sections. For example - if you add a string with the key `plugin.posts.category` and value "Enter a category name", Builder will create the following structure in the language file:
    +
    +```
    +plugin:
    +    posts:
    +        category: Enter a category name
    +```
    +
    +If you create a new localization string from the Inspector or other editor while you have the default language file tab open in the Builder, it will try to update the tab contents or merge the updated file contents from the server. It's a good idea to keep the default localization file always saved in the Builder to avoid possible content conflicts when you edit localization from another place.
    +
    +> **Tip**: In YAML a single quote is escaped with two single quotes (http://yaml.org/spec/current.html#id2534365).
    +
    +## Editing Code Files (New in v2)
    +
    +Raw code and other files can be managed directly in the Code tab of the Builder. You can navigate to any file within the context of the selected plugin.
    +
    +Use this to make code adjustments without the need for a code editor, which includes creating, moving, renaming and deleting files.
    +
    +## Importing Tailor Blueprints (New in v2)
    +
    +The Import tab of the Builder can generate scaffold files using [Tailor blueprints](https://docs.octobercms.com/3.x/cms/tailor/blueprints.html) as a source. In combination, Tailor and Builder work together to create a super-scaffolding tool since it can generate multiple files in one process. First, design your fields and preview them using Tailor, and then when you are ready, import them in to Builder to start working directly with the files.
    +
    +It important to note that importing blueprints is a one-way function, and in some cases it is better to leave content within Tailor for the benefits that come with its dynamic models, especially for content. This design decision is up to you.
    +
    +When visiting the Import tab, use the **Add Blueprint** button to select the Tailor blueprints you wish to import. You can select multiple blueprints and it is recommended to include related blueprints so the relationships between blueprints are preserved.
    +
    +Once added, each blueprint can be customized, which includes the Controller Class, Model Class, Table Name, Permission Code and Menu Code names. When these fields are modified, they will adjust the filenames of the generated files.
    +
    +Clicking the Import button will begin the conversion process. Some import options are shown to control how the import should proceed.
    +
    +- **Migrate Database** performs a database migration after the import is finished. This optional and you can migrate the database later.
    +
    +- **Disable Blueprints** will rename the blueprint files to use a backup extension (.bak) to disable them.
    +
    +- **Delete Blueprint Data** will delete any existing data and tables for the selected blueprints found within Tailor.
    +
    +Before clicking **Import**, be sure to double check the selected blueprints. The import process creates multiple scaffold files for the selected plugin. This process can be difficult to undo, so it is a good idea to practice on a test plugin first, without migrating the database or disabling the blueprints.
    +
    +When everything is done, you should see the controller, model and migration files generated for your selected plugin.
    +
    +## Displaying Plugin Records on CMS Pages
    +
    +Builder provides universal CMS components that you can use for displaying records from your plugins on the front-end website pages. The components provide only basic functionality, for example they don't support a record search feature.
    +
    +Please read the [CMS documentation](https://docs.octobercms.com/3.x/cms/themes/components.html) to learn more about the CMS components concept.
    +
    +### Record List Component
    +
    +The Record list component outputs a list of records provided by a plugin's model. The component supports the following optional features: pagination, links to the record details page, using a [model scope](https://docs.octobercms.com/3.x/extend/database/model.html#query-scopes) for the list filtering. The list can be sorted by any column, but the sorting cannot be changed by website visitors - it's set in the component configuration.
    +
    +Add this component to a CMS page by dragging it to the page code from the component list and click it to configure its properties:
    +
    +* `Model class` - select a model class you want to use to fetch data from the database.
    +* `Scope` - optional, select the model scope method used to filter the results
    +* `Scope Value` - optional, the value to provide to the selected scope method. URL parameters can be provided in the form of `{{ :nameOfParam }}`
    +* `Display column` - select the model column to display in the list. It's an autocomplete field that displays columns from the underlying database table. You can enter any value in this field. The value is used in the default component partial, you can customize the component by providing custom markup instead of the default partial.
    +* `Details page` - a drop-down list of CMS pages you want to create links to.
    +* `Details key column` - select a column you want to use as a record identifier in the record links. You can link your records by the primary identifier (id), slug column or any other - it depends on the your database structure.
    +* `URL parameter name` - enter the details page URL parameter name, which takes the record identifier. For example, if the record details page has a URL like "/blog/post/:slug", the URL parameter would be "slug".
    +* `Records per page` - enter a number to enable pagination for the records.
    +* `Page number` - specify the fixed page number or use the **external parameter editor** to enter a name of the URL parameter which holds the page number. For example - if your record list page URL was "/record-list-test/:page?", the Page number property value would be ":page".
    +* `Sorting` - select a database column name to use for sorting the list.
    +* `Direction` - select whether the sorting should be ascending or descending.
    +
    +After configuring the component save and preview the page. Most likely you will want to customize the [default component markup](https://docs.octobercms.com/3.x/cms/themes/components.html#customizing-default-markup) to output more details about each record.
    +
    +> **Tip**: In the CMS editor, you can right-click on the `{% component %}` tag and select "Expand Markup".
    +
    +## Record Details Component
    +
    +The Record details component loads a model from the database and outputs its details on a page. If the requested record cannot be found, the component outputs the "record not found" message.
    +
    +Add this component to a CMS page by dragging it to the page code from the component list and click it to configure its properties:
    +
    +* `Model class` - select a model class you want to use to fetch data from the database.
    +* `Identifier value` - specify a fixed value or use the **external parameter editor** to enter a name of the URL parameter. If the details page had the URL like "/blog/post/:slug", the identifier value would be ":slug".
    +* `Key column` - specify a name of the database table column to use for looking up the record. This is an autocomplete field that displays columns from the underlying database table.
    +* `Display column` - enter a name of the database table column to display on the details page. The value is used in the default component partial, you can customize the component by providing custom markup instead of the default partial.
    +* `Not found message` - a message to display if the record is not found. Used in the default partial.
    +
    +After configuring the component save and preview the page. You will likely want to customize the [default component markup](https://docs.octobercms.com/3.x/cms/themes/components.html#customizing-default-markup) to output more details from the loaded model.
    +
    +## Notes About Autocompletion
    +
    +Builder updates the Inspector autocompletion fields every time when the underlying data is updated. For example, the “Field name” property of the Form Builder controls is populated with the database table column names. If you update the table structure with Builder, the autocompletion cache updates automatically. However you may need to reopen Inspector so that it can update its editors.
    +
    +If you edit your plugin files or database structure with an external editor, Builder won’t be able to pick up those changes automatically. You might want to reload the Builder page after you add a database column with an external tool in order to refresh the autocompletion features.
    +
    +## Editing Other Plugins
    +
    +Although Builder allows you to edit plugins created by other authors, remember that you do it at your own risk. Plugins could be updated by their authors, which will eliminate your changes or break the plugin. In many cases, if you make updates to plugins developed by another author, you lose any technical support provided by the author.
    +
    +## Adding Support for a Custom Form Widget
    +
    +To add a custom widget to the Builder plugin, you must first [register a backend form widget](https://docs.octobercms.com/3.x/extend/forms/form-widgets.html#form-widget-registration) for your plugin.
    +
    +Once it is registered, define a list of [inspector properties](https://docs.octobercms.com/3.x/element/inspector-types.html) within your plugin in the Plugin registration class `boot()` method and register the custom control. For example:
    +
    +```php
    +public function boot()
    +{
    +    $properties = [
    +        'max_value' => [
    +            'title' => 'The maximum allowed',
    +            'type' => 'builderLocalization',
    +            'validation' => [
    +                'required' => [
    +                    'message' => 'Maxium value is required'
    +                ]
    +            ]
    +        ],
    +         'mode' => [
    +            'title' => 'The plugin mode',
    +            'type' => 'dropdown',
    +                'options' => [
    +                    'single' => 'Single',
    +                    'multiple' => 'Multiple',
    +                ],
    +            'ignoreIfEmpty' => true,
    +        ]
    +    ];
    +
    +    Event::listen('pages.builder.registerControls', function($controlLibrary) {
    +        $controlLibrary->registerControl(
    +            'yourwidgetname',
    +            'My Widget',
    +            'Widget description',
    +            ControlLibrary::GROUP_WIDGETS,
    +            'icon-file-image-o',
    +            $controlLibrary->getStandardProperties([], $properties),
    +            'Acme\Blog\Classes\ControlDesignTimeProvider'
    +        );
    +    });
    +}
    +```
    +
    +> **Note**: See the `getStandardProperties()` method in the `rainlab/builder/classes/ControlLibrary.php` file for more examples.
    +
    +Now, we need the `ControlDesignTimeProvider` class referenced above. Save the following as `classes/ControlDesignTimeProvider.php` within your plugin's directory (replacing `'yourwidgetname'` with what you used in your Plugin registration class `boot()` method).
    +
    +```php
    +namespace Acme\Blog\Classes;
    +
    +use RainLab\Builder\Widgets\DefaultControlDesignTimeProvider;
    +
    +class ControlDesignTimeProvider extends DefaultControlDesignTimeProvider
    +{
    +    public function __construct()
    +    {
    +        $this->defaultControlsTypes[] = 'yourwidgetname';
    +    }
    +}
    +```
    +
    +Then save the following as `class/controldesigntimeprovider/_control-yourwidgetname.htm` within your plugin's directory, and customize it how you like. Again, `yourwidgetname` in the file name must match:
    +
    +```html
    +
    + +
    +``` + +You should now be able to add and configure your custom widget within the Builder plugin just like any other plugin. + +### License + +This plugin is an official extension of the October CMS platform and is free to use if you have a platform license. See [EULA license](LICENSE.md) for more details. diff --git a/plugins/rainlab/builder/assets/css/builder.css b/plugins/rainlab/builder/assets/css/builder.css new file mode 100644 index 0000000..f5d173a --- /dev/null +++ b/plugins/rainlab/builder/assets/css/builder.css @@ -0,0 +1 @@ +:root,[data-bs-theme=light]{--oc-builder-control-color:#555}[data-bs-theme=dark]{--oc-builder-control-color:#888}.builder-building-area{background:var(--bs-body-bg,#fff)}.builder-building-area ul.builder-control-list{list-style:none;margin-bottom:0;padding:20px}.builder-building-area ul.builder-control-list:after,.builder-building-area ul.builder-control-list:before{content:" ";display:table}.builder-building-area ul.builder-control-list:after{clear:both}.builder-building-area ul.builder-control-list>li.control{cursor:pointer;margin-bottom:20px;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.builder-building-area ul.builder-control-list>li.control[data-unknown]{cursor:default}.builder-building-area ul.builder-control-list>li.control.loading-control,.builder-building-area ul.builder-control-list>li.control.oc-placeholder{border:2px dotted var(--bs-border-color,#dae0e0);border-radius:4px;color:var(--bs-emphasis-color,#dae0e0);margin-top:20px;padding:10px 12px;position:relative;text-align:center}.builder-building-area ul.builder-control-list>li.control.loading-control i,.builder-building-area ul.builder-control-list>li.control.oc-placeholder i{margin-right:8px}.builder-building-area ul.builder-control-list>li.control.loading-control{background:var(--oc-secondary-bg)}.builder-building-area ul.builder-control-list>li.control.clear-row{display:none;margin-bottom:0}.builder-building-area ul.builder-control-list>li.control.loading-control{border-color:#bdc3c7;text-align:left}.builder-building-area ul.builder-control-list>li.control.loading-control:before,.builder-building-area ul.builder-control-list>li.control.updating-control:after{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite;background-image:url(../images/loader-transparent.svg);background-position:50% 50%;background-size:15px 15px;content:" ";display:inline-block;height:15px;margin-right:13px;position:relative;top:2px;width:15px}.builder-building-area ul.builder-control-list>li.control.loading-control:after{content:attr(data-builder-loading-text);display:inline-block}.builder-building-area ul.builder-control-list>li.control.updating-control:after{position:absolute;right:-8px;top:5px}.builder-building-area ul.builder-control-list>li.control.updating-control:before{background:hsla(0,0%,50%,.1);border-radius:4px;content:"";height:25px;position:absolute;right:0;top:0;width:25px}.builder-building-area ul.builder-control-list>li.control.drag-over{border-color:var(--oc-selection);color:var(--oc-selection)}.builder-building-area ul.builder-control-list>li.control.span-full{float:left;width:100%}.builder-building-area ul.builder-control-list>li.control.span-left{clear:left;float:left;width:48.5%}.builder-building-area ul.builder-control-list>li.control.span-right{clear:right;float:right;width:48.5%}.builder-building-area ul.builder-control-list>li.control.span-right+li.clear-row{clear:both;display:block}.builder-building-area ul.builder-control-list>li.control>div.remove-control{display:none}.builder-building-area ul.builder-control-list>li.control:not(.oc-placeholder):not(.loading-control):not(.updating-control):hover>div.remove-control{background:var(--oc-toolbar-border,#ecf0f1);border-radius:20px;color:var(--oc-toolbar-color,#95a5a6)!important;cursor:pointer;display:block;font-family:sans-serif;font-size:16px;font-weight:700;height:21px;line-height:21px;padding-left:6px;position:absolute;right:0;top:0;width:21px}.builder-building-area ul.builder-control-list>li.control:not(.oc-placeholder):not(.loading-control):not(.updating-control):hover>div.remove-control:hover{background:#c03f31;color:#fff!important}.builder-building-area ul.builder-control-list>li.control:not(.oc-placeholder):not(.loading-control):not(.updating-control):hover[data-control-type=hint]>div.remove-control,.builder-building-area ul.builder-control-list>li.control:not(.oc-placeholder):not(.loading-control):not(.updating-control):hover[data-control-type=partial]>div.remove-control{right:12px;top:12px}.builder-building-area ul.builder-control-list>li.control[data-control-type=hint].updating-control:before,.builder-building-area ul.builder-control-list>li.control[data-control-type=partial].updating-control:before{right:12px;top:7}.builder-building-area ul.builder-control-list>li.control[data-control-type=hint].updating-control:after,.builder-building-area ul.builder-control-list>li.control[data-control-type=partial].updating-control:after{right:4px;top:13px}.builder-building-area ul.builder-control-list>li.control>.control-static-contents,.builder-building-area ul.builder-control-list>li.control>.control-wrapper{position:relative;transition:margin .1s}.builder-building-area ul.builder-control-list>li.oc-placeholder.control-palette-open,.builder-building-area ul.builder-control-list>li.oc-placeholder.popover-highlight,.builder-building-area ul.builder-control-list>li.oc-placeholder:hover{background-color:var(--oc-selection)!important;border-color:var(--oc-selection);border-style:solid;color:#fff!important;opacity:1}.builder-building-area ul.builder-control-list>li.control.inspector-open:not(.oc-placeholder):not(.loading-control)>.control-wrapper *,.builder-building-area ul.builder-control-list>li.control:not(.oc-placeholder):not(.loading-control):not([data-unknown]):hover>.control-wrapper *{color:var(--oc-selection)!important}.builder-building-area ul.builder-control-list>li.control.drag-over:not(.oc-placeholder):before{background-color:var(--oc-selection);border-radius:5px;content:"";height:100%;left:0;position:absolute;top:0;width:10px}.builder-building-area ul.builder-control-list>li.control.drag-over:not(.oc-placeholder)>.control-static-contents,.builder-building-area ul.builder-control-list>li.control.drag-over:not(.oc-placeholder)>.control-wrapper{margin-left:20px;margin-right:-20px}.builder-building-area .control-body.field-disabled,.builder-building-area .control-body.field-hidden{opacity:.5}.builder-building-area .builder-control-label{color:var(--oc-builder-control-color);font-size:14px;font-weight:600;margin-bottom:10px}.builder-building-area .builder-control-label.required:after{content:" *";font-size:60%;vertical-align:super}.builder-building-area .builder-control-label:empty{margin-bottom:0}.builder-building-area .builder-control-comment-above{margin-bottom:8px;margin-top:-3px}.builder-building-area .builder-control-comment-below{margin-top:6px}.builder-building-area .builder-control-comment-above,.builder-building-area .builder-control-comment-below{color:#737373;font-size:12px}.builder-building-area .builder-control-comment-above:empty,.builder-building-area .builder-control-comment-below:empty{display:none}html.gecko.mac .builder-building-area div[data-root-control-wrapper]{margin-right:17px}[data-bs-theme=dark] .builder-building-area{background:var(--bs-body-bg)}.builder-building-area .builder-blueprint-control-dropdown,.builder-building-area .builder-blueprint-control-partial,.builder-building-area .builder-blueprint-control-text,.builder-building-area .builder-blueprint-control-textarea,.builder-building-area .builder-blueprint-control-unknown{border:2px solid var(--oc-document-ruler-tick,#bdc3c7);border-radius:4px;color:#95a5a6;padding:10px 12px}.builder-building-area .builder-blueprint-control-dropdown i,.builder-building-area .builder-blueprint-control-partial i,.builder-building-area .builder-blueprint-control-text i,.builder-building-area .builder-blueprint-control-textarea i,.builder-building-area .builder-blueprint-control-unknown i{margin-right:5px}.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-dropdown,.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-text,.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-textarea,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-dropdown,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-text,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-textarea{border-color:var(--oc-selection)}.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-dropdown:before,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-dropdown:before{background-color:var(--oc-selection)}.builder-building-area .builder-blueprint-control-textarea.size-tiny{min-height:50px}.builder-building-area .builder-blueprint-control-textarea.size-small{min-height:100px}.builder-building-area .builder-blueprint-control-textarea.size-large{min-height:200px}.builder-building-area .builder-blueprint-control-textarea.size-huge{min-height:250px}.builder-building-area .builder-blueprint-control-textarea.size-giant{min-height:350px}.builder-building-area .builder-blueprint-control-section{border-bottom:1px solid var(--oc-document-ruler-tick,#bdc3c7);padding-bottom:4px}.builder-building-area .builder-blueprint-control-section .builder-control-label{font-size:16px;margin-bottom:6px}.builder-building-area .builder-blueprint-control-partial,.builder-building-area .builder-blueprint-control-unknown{background:#eee;border-color:#eee}.builder-building-area .builder-blueprint-control-dropdown{position:relative}.builder-building-area .builder-blueprint-control-dropdown:after,.builder-building-area .builder-blueprint-control-dropdown:before{content:"";position:absolute}.builder-building-area .builder-blueprint-control-dropdown:before{background:var(--oc-document-ruler-tick,#bdc3c7);height:100%;right:40px;top:0;width:2px}.builder-building-area .builder-blueprint-control-dropdown:after{-webkit-font-smoothing:antialiased;color:inherit;content:"\f107";font-family:FontAwesome;font-size:20px;font-style:normal;font-weight:400;line-height:20px;right:15px;text-decoration:inherit;top:12px}.builder-building-area .builder-blueprint-control-checkbox:before{border:2px solid var(--oc-document-ruler-tick,#bdc3c7);border-radius:4px;content:" ";float:left;height:17px;position:relative;top:2px;width:17px}.builder-building-area .builder-blueprint-control-checkbox .builder-control-label{font-weight:400;margin-left:25px}.builder-building-area .builder-blueprint-control-checkbox .builder-control-comment-below{margin-left:25px}.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-checkbox:before,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-checkbox:before{border-color:var(--oc-selection)}.builder-building-area .builder-blueprint-control-switch{position:relative}.builder-building-area .builder-blueprint-control-switch:after,.builder-building-area .builder-blueprint-control-switch:before{border-radius:30px;content:" ";position:absolute}.builder-building-area .builder-blueprint-control-switch:before{background-color:var(--oc-document-ruler-tick,#bdc3c7);height:18px;left:2px;top:2px;width:34px}.builder-building-area .builder-blueprint-control-switch:after{background-color:#fff;height:14px;left:4px;margin-left:16px;top:4px;width:14px}.builder-building-area .builder-blueprint-control-switch .builder-control-label{font-weight:400;margin-left:45px}.builder-building-area .builder-blueprint-control-switch .builder-control-comment-below{margin-left:45px}.builder-building-area li.control:hover>.control-wrapper .builder-blueprint-control-switch:before,.builder-building-area li.inspector-open>.control-wrapper .builder-blueprint-control-switch:before{background-color:var(--oc-selection)}.builder-building-area .builder-blueprint-control-repeater-body>.repeater-button{background:var(--oc-document-ruler-tick,#bdc3c7);border-radius:2px;color:#fff;display:inline-block;margin-bottom:10px;padding:8px 13px}.builder-building-area ul.builder-control-list>li.control:hover>.control-wrapper>.control-body .builder-blueprint-control-repeater-body>.repeater-button,.builder-building-area ul.builder-control-list>li.inspector-open>.control-wrapper>.control-body .builder-blueprint-control-repeater-body>.repeater-button{background:var(--oc-selection);color:#fff!important}.builder-building-area ul.builder-control-list>li.control:hover>.control-wrapper>.control-body .builder-blueprint-control-repeater-body>.repeater-button span,.builder-building-area ul.builder-control-list>li.inspector-open>.control-wrapper>.control-body .builder-blueprint-control-repeater-body>.repeater-button span{color:#fff!important}.builder-building-area .builder-blueprint-control-repeater{position:relative}.builder-building-area .builder-blueprint-control-repeater:before{background:var(--oc-document-ruler-tick,#bdc3c7);content:"";height:100%;left:2px;position:absolute;top:0;width:2px}.builder-building-area .builder-blueprint-control-repeater:after{background:var(--oc-document-ruler-tick,#bdc3c7);border-radius:6px;content:"";height:6px;left:0;position:absolute;top:14px;width:6px}.builder-building-area .builder-blueprint-control-repeater>ul.builder-control-list{padding-bottom:0;padding-right:0;padding-top:10px}.builder-building-area li.control:hover>.builder-blueprint-control-repeater:after,.builder-building-area li.control:hover>.builder-blueprint-control-repeater:before,.builder-building-area li.inspector-open>.builder-blueprint-control-repeater:after,.builder-building-area li.inspector-open>.builder-blueprint-control-repeater:before{background-color:var(--oc-selection)}.builder-building-area .builder-blueprint-control-checkboxlist ul,.builder-building-area .builder-blueprint-control-radiolist ul{color:#95a5a6;list-style:none;padding:0}.builder-building-area .builder-blueprint-control-checkboxlist ul li,.builder-building-area .builder-blueprint-control-radiolist ul li{margin-bottom:3px}.builder-building-area .builder-blueprint-control-checkboxlist ul li:last-child,.builder-building-area .builder-blueprint-control-radiolist ul li:last-child{margin-bottom:0}.builder-building-area .builder-blueprint-control-checkboxlist ul li i,.builder-building-area .builder-blueprint-control-radiolist ul li i{margin-right:5px}.builder-building-area .builder-blueprint-control-text.fileupload.image{height:100px;text-align:center;width:100px}.builder-building-area .builder-blueprint-control-text.fileupload.image i{line-height:77px;margin-right:0}.builder-controllers-builder-area{background:var(--bs-body-bg,#fff)}.builder-controllers-builder-area ul.controller-behavior-list{list-style:none;margin-bottom:0;padding:20px}.builder-controllers-builder-area ul.controller-behavior-list:after,.builder-controllers-builder-area ul.controller-behavior-list:before{content:" ";display:table}.builder-controllers-builder-area ul.controller-behavior-list:after{clear:both}.builder-controllers-builder-area ul.controller-behavior-list li h4{border-bottom:1px dotted var(--oc-document-ruler-tick,#bdc3c7);margin:0 -20px 40px;text-align:center}.builder-controllers-builder-area ul.controller-behavior-list li h4 span{background:#72809d;border-radius:8px;color:#fff;display:inline-block;font-size:13px;line-height:100%;margin:0 auto;padding:7px 10px;position:relative;top:14px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container{cursor:pointer;margin-bottom:40px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container:after,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container:before{content:" ";display:table}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container:after{clear:both}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior{border:2px solid var(--oc-document-ruler-tick,#bdc3c7);border-radius:4px;padding:25px 10px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior table{border-collapse:collapse;width:100%}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table td,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior table td{border-right:1px solid var(--oc-document-ruler-tick,#bdc3c7);padding:0 15px 15px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table td:last-child,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior table td:last-child{border-right:none}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table .oc-placeholder,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior table .oc-placeholder{background:var(--oc-secondary-bg,#eef2f4);height:25px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table tbody tr:last-child td,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .list-behavior table tbody tr:last-child td{padding-bottom:0}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table .oc-placeholder,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table i.icon-bars{float:left}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .import-export-behavior table i.icon-bars{color:#d6dde0;font-size:28px;line-height:28px;margin-right:15px;position:relative;top:-2px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.form{border:2px solid var(--oc-document-ruler-tick,#bdc3c7);border-radius:4px;margin-bottom:20px;padding:25px 25px 0}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.form:after,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.form:before{content:" ";display:table}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.form:after{clear:both}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field.left{float:left;width:48%}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field.right{float:right;width:45%}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field div.label{background:var(--oc-secondary-bg,#eef2f4);height:25px;margin-bottom:10px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field div.label.size-3{width:100px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field div.label.size-5{width:150px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field div.label.size-2{width:60px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.field div.control{background:var(--oc-secondary-bg,#eef2f4);height:35px;margin-bottom:25px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.button{background:var(--oc-secondary-bg,#eef2f4);border-radius:4px;height:35px;margin-right:20px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.button.size-5{width:100px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.button.size-3{width:60px}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container .form-behavior div.button:first-child{margin-right:0}.builder-controllers-builder-area ul.controller-behavior-list .behavior-container.inspector-open *,.builder-controllers-builder-area ul.controller-behavior-list .behavior-container:hover *{border-color:var(--oc-selection)!important}html.gecko.mac .builder-controllers-builder-area ul.controller-behavior-list{padding-right:40px}.builder-tabs>.tabs{position:relative}.builder-tabs>.tabs .tab-control{display:block;position:absolute}.builder-tabs>.tabs .tab-control.inspector-trigger{cursor:pointer;font-size:14px;padding-left:5px;padding-right:5px}.builder-tabs>.tabs .tab-control.inspector-trigger span{background:#95a5a6;display:block;height:3px;margin-bottom:2px;width:3px}.builder-tabs>.tabs .tab-control.inspector-trigger span:last-child{margin-bottom:0}.builder-tabs>.tabs .tab-control.inspector-trigger.inspector-open span,.builder-tabs>.tabs .tab-control.inspector-trigger:hover span{background:var(--bs-link-color)}.builder-tabs>.tabs .tab-control.inspector-trigger.global{background:var(--bs-body-bg);border-radius:3px;padding-right:10px;right:0;top:5px;z-index:110}.builder-tabs>.tabs .tab-control.inspector-trigger.global>div{background:var(--oc-toolbar-bg);border-radius:3px;height:24px;padding-left:10px;padding-top:5px;width:24px}.builder-tabs>.tabs .tab-control.inspector-trigger.global>div:active{background:var(--oc-toolbar-hover-bg)}.builder-tabs>.tabs>ul.tabs{font-size:0;list-style:none;margin:0;overflow:hidden;padding-right:50px;position:relative;white-space:nowrap}.builder-tabs>.tabs>ul.tabs>li{cursor:pointer;display:inline-block;font-size:13px;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;white-space:nowrap}.builder-tabs>.tabs>ul.tabs>li>div.tab-container{color:var(--oc-tab-color)!important;position:relative}.builder-tabs>.tabs>ul.tabs>li>div.tab-container>div{position:relative;transition:padding .1s}.builder-tabs>.tabs>ul.tabs>li:hover>div{color:var(--oc-tab-active-color)!important}.builder-tabs>.tabs>ul.tabs>li .tab-control{display:none}.builder-tabs>.tabs>ul.tabs>li .tab-control.close-btn{color:#95a5a6;cursor:pointer;font-size:15px;height:15px;line-height:15px;right:18px;text-align:center;top:7px;width:15px}.builder-tabs>.tabs>ul.tabs>li .tab-control.close-btn:hover{color:var(--bs-link-color)!important}.builder-tabs>.tabs>ul.tabs>li .tab-control.inspector-trigger{right:34px;top:10px}.builder-tabs>.tabs>ul.tabs>li.active>div.tab-container{color:var(--oc-tab-active-color)!important}.builder-tabs>.tabs>ul.tabs>li.active .tab-control{display:block}.builder-tabs>.tabs>ul.panels{list-style:none;padding:0}.builder-tabs>.tabs>ul.panels>li{display:none}.builder-tabs>.tabs>ul.panels>li.active{display:block}.builder-tabs.primary>.tabs>ul.tabs{height:31px;padding:0 40px}.builder-tabs.primary>.tabs>ul.tabs:after{background:transparent linear-gradient(90deg,#bdc3c7 90%,transparent);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;width:100%;z-index:106}.builder-tabs.primary>.tabs>ul.tabs>li{bottom:-3px;margin-left:-20px;z-index:105}.builder-tabs.primary>.tabs>ul.tabs>li>div.tab-container{height:27px;padding:0 21px}.builder-tabs.primary>.tabs>ul.tabs>li>div.tab-container>div{background:#fff;padding:5px 5px 0}.builder-tabs.primary>.tabs>ul.tabs>li>div.tab-container>div>span{position:relative;top:-4px;transition:top .1s}.builder-tabs.primary>.tabs>ul.tabs>li.active{color:var(--oc-tab-active-color);z-index:107}.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:after,.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:before{background:transparent url(../images/tab.png) no-repeat;content:"";display:block;height:27px;position:absolute;top:0;width:21px}.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:before{background-position:0 0;left:0}.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:after{background-position:-75px 0;right:0}.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container>div{border-top:2px solid #bdc3c7;padding-right:30px}.builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container>div>span{top:0}.builder-tabs.primary>.tabs>ul.tabs>li.active:before{background:#fff;bottom:0;content:"";display:block;height:3px;left:0;position:absolute;width:100%}.builder-tabs.primary>.tabs>ul.tabs>li.new-tab{background:transparent url(../images/tab.png) no-repeat;background-position:-24px 0;cursor:pointer;height:22px;margin-left:-11px;position:relative;top:4px;width:27px}.builder-tabs.primary>.tabs>ul.tabs>li.new-tab:hover{background-position:-24px -32px}.builder-tabs.secondary>.tabs ul.tabs{margin-left:12px;padding-left:0}.builder-tabs.secondary>.tabs ul.tabs>li{border-right:1px solid #bdc3c7;padding-right:1px}.builder-tabs.secondary>.tabs ul.tabs>li>div.tab-container>div{padding:4px 10px}.builder-tabs.secondary>.tabs ul.tabs>li>div.tab-container>div span{font-size:14px}.builder-tabs.secondary>.tabs ul.tabs>li .tab-control{right:23px;top:7px}.builder-tabs.secondary>.tabs ul.tabs>li .tab-control.close-btn{right:6px;top:5px}.builder-tabs.secondary>.tabs ul.tabs>li.new-tab{background:transparent;border:2px solid #e4e4e4;border-radius:4px;cursor:pointer;height:22px;left:9px;position:relative;top:7px;width:27px}.builder-tabs.secondary>.tabs ul.tabs>li.new-tab:hover{background-color:#2581b8;border-color:#2581b8}.builder-tabs.secondary>.tabs ul.tabs>li.active{padding-right:10px}.builder-tabs.secondary>.tabs ul.tabs>li.active>div.tab-container>div{color:var(--oc-builder-control-color);padding-right:30px}[data-bs-theme=dark] .builder-tabs.primary>.tabs>ul.tabs>li.active:before,[data-bs-theme=dark] .builder-tabs.primary>.tabs>ul.tabs>li>div.tab-container>div{background:#202124}[data-bs-theme=dark] .builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:after,[data-bs-theme=dark] .builder-tabs.primary>.tabs>ul.tabs>li.active>div.tab-container:before{background-image:url(../images/tab-dark.png)}.builder-menu-editor{background:var(--bs-body-bg,#fff)}.builder-menu-editor .builder-menu-editor-workspace{padding:30px}.builder-menu-editor ul.builder-menu{cursor:pointer;font-size:0;padding:0}.builder-menu-editor ul.builder-menu>li{border-radius:4px}.builder-menu-editor ul.builder-menu>li div.item-container:hover,.builder-menu-editor ul.builder-menu>li.inspector-open>div.item-container{background:var(--oc-selection)!important;color:#fff!important}.builder-menu-editor ul.builder-menu>li div.item-container:hover a,.builder-menu-editor ul.builder-menu>li.inspector-open>div.item-container a{color:#fff!important}.builder-menu-editor ul.builder-menu>li div.item-container{position:relative}.builder-menu-editor ul.builder-menu>li div.item-container .close-btn{color:#fff;display:none;font-size:14px;height:15px;line-height:14px;position:absolute;right:5px;text-align:center;top:5px;width:15px}.builder-menu-editor ul.builder-menu>li div.item-container:hover .close-btn{display:block;opacity:.5;text-decoration:none}.builder-menu-editor ul.builder-menu>li div.item-container:hover .close-btn:hover{opacity:1}.builder-menu-editor ul.builder-menu>li.add{border:2px dotted var(--oc-dropdown-trigger-border,#dde0e2);font-size:16px;text-align:center}.builder-menu-editor ul.builder-menu>li.add a{color:var(--oc-dropdown-trigger-color,#bdc3c7);text-decoration:none}.builder-menu-editor ul.builder-menu>li.add span.title{font-size:14px}.builder-menu-editor ul.builder-menu>li.add:hover{background:var(--oc-selection)!important;border:2px dotted var(--oc-selection)}.builder-menu-editor ul.builder-menu>li.add:hover a{color:#fff}.builder-menu-editor ul.builder-menu>li.list-sortable-placeholder{background:transparent;border:2px dotted var(--oc-selection);height:10px}.builder-menu-editor ul.builder-menu.builder-main-menu>li{display:inline-block;vertical-align:top}.builder-menu-editor ul.builder-menu.builder-main-menu>li.item{margin:0 20px 20px 0}.builder-menu-editor ul.builder-menu.builder-main-menu>li>div.item-container{background:var(--bs-secondary-bg,#ecf0f1);color:var(--bs-secondary-color,#708080);height:64px;padding:20px 25px;white-space:nowrap}.builder-menu-editor ul.builder-menu.builder-main-menu>li>div.item-container i{font-size:24px;margin-right:10px}.builder-menu-editor ul.builder-menu.builder-main-menu>li>div.item-container span.title{font-size:14px;line-height:100%;position:relative;top:-3px}.builder-menu-editor ul.builder-menu.builder-main-menu>li.add{height:64px}.builder-menu-editor ul.builder-menu.builder-main-menu>li.add a{display:block;height:60px;padding:20px 15px}.builder-menu-editor ul.builder-menu.builder-main-menu>li.add a i{margin-right:5px}.builder-menu-editor ul.builder-menu.builder-main-menu>li.add a span{position:relative;top:-1px}.builder-menu-editor ul.builder-menu.builder-submenu{margin-top:1px}.builder-menu-editor ul.builder-menu.builder-submenu>li{display:block;width:120px}.builder-menu-editor ul.builder-menu.builder-submenu>li i{display:block;margin-bottom:7px}.builder-menu-editor ul.builder-menu.builder-submenu>li span.title{display:block;font-size:12px}.builder-menu-editor ul.builder-menu.builder-submenu>li.item{margin:0 0 1px}.builder-menu-editor ul.builder-menu.builder-submenu>li>div.item-container{background:var(--bs-tertiary-bg,#f3f5f5);color:var(--bs-tertiary-color,#94a5a6);padding:18px 13px;text-align:center}.builder-menu-editor ul.builder-menu.builder-submenu>li>div.item-container i{font-size:24px}.builder-menu-editor ul.builder-menu.builder-submenu>li.add{margin-top:20px}.builder-menu-editor ul.builder-menu.builder-submenu>li.add a{display:block;padding:10px 20px}.builder-tailor-builder-area{background:var(--bs-body-bg,#fff)}.builder-tailor-builder-area ul.tailor-blueprint-list{cursor:pointer;list-style:none;margin-bottom:0;padding:20px}.builder-tailor-builder-area ul.tailor-blueprint-list:after,.builder-tailor-builder-area ul.tailor-blueprint-list:before{content:" ";display:table}.builder-tailor-builder-area ul.tailor-blueprint-list:after{clear:both}.builder-tailor-builder-area ul.tailor-blueprint-list li{position:relative}.builder-tailor-builder-area ul.tailor-blueprint-list li h4{border-bottom:1px dotted var(--oc-document-ruler-tick,#bdc3c7);margin:0 -20px 30px;text-align:center}.builder-tailor-builder-area ul.tailor-blueprint-list li h4 span{background:#72809d;border-radius:8px;color:#fff;display:inline-block;font-size:13px;line-height:100%;margin:0 auto;padding:7px 10px;position:relative;top:14px}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table{margin:0}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table td{font-size:.875em}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table td>span{word-wrap:break-word;color:var(--bs-secondary-color);font-family:var(--bs-font-monospace);word-break:break-word}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table th{font-size:.875em;text-align:right}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table th:not(.table-danger){color:var(--bs-body-color)}.builder-tailor-builder-area ul.tailor-blueprint-list li table.table tr:last-child td,.builder-tailor-builder-area ul.tailor-blueprint-list li table.table tr:last-child th{border-bottom:none}.builder-tailor-builder-area ul.tailor-blueprint-list li div.remove-blueprint{background:var(--oc-toolbar-border,#ecf0f1);border-radius:20px;color:var(--oc-toolbar-color,#95a5a6)!important;cursor:pointer;display:none;font-family:sans-serif;font-size:16px;font-weight:700;height:21px;line-height:21px;padding-left:6px;position:absolute;right:0;top:20px;width:21px}.builder-tailor-builder-area ul.tailor-blueprint-list li div.remove-blueprint:hover{background:#c03f31;color:#fff!important}.builder-tailor-builder-area ul.tailor-blueprint-list li:hover div.remove-blueprint{display:block}.builder-tailor-builder-area ul.tailor-blueprint-list li.updating-blueprint:after{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite;background-image:url(../images/loader-transparent.svg);background-position:50% 50%;background-size:15px 15px;content:" ";display:inline-block;height:15px;margin-right:13px;position:relative;position:absolute;right:-8px;top:2px;top:35px;width:15px}.builder-tailor-builder-area ul.tailor-blueprint-list li.updating-blueprint:before{background:hsla(0,0%,50%,.1);border-radius:4px;content:"";height:25px;position:absolute;right:0;top:30px;width:25px}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container:after,.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container:before{content:" ";display:table}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container:after{clear:both}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container .tailor-blueprint div.form{border:2px solid var(--oc-document-ruler-tick,#bdc3c7);border-radius:4px;margin-bottom:20px}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container .tailor-blueprint div.form:after,.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container .tailor-blueprint div.form:before{content:" ";display:table}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container .tailor-blueprint div.form:after{clear:both}.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container.inspector-open *,.builder-tailor-builder-area ul.tailor-blueprint-list .blueprint-container:hover *{border-color:var(--oc-selection)!important}.builder-tailor-builder-area .add-blueprint-button{border:2px dotted var(--oc-dropdown-trigger-border,#dde0e2);font-size:16px;height:64px;margin:0 20px 40px;text-align:center}.builder-tailor-builder-area .add-blueprint-button a{color:var(--oc-dropdown-trigger-color,#bdc3c7);display:block;height:60px;padding:20px 15px;text-decoration:none}.builder-tailor-builder-area .add-blueprint-button i{margin-right:5px}.builder-tailor-builder-area .add-blueprint-button span{position:relative;top:-1px}.builder-tailor-builder-area .add-blueprint-button span.title{font-size:14px}.builder-tailor-builder-area .add-blueprint-button:hover{background:var(--oc-selection)!important;border:2px dotted var(--oc-selection)}.builder-tailor-builder-area .add-blueprint-button:hover a{color:#fff}html.gecko.mac .builder-tailor-builder-area ul.tailor-blueprint-list{padding-right:40px}.localization-input-container input[type=text].string-editor{padding-right:20px!important}.localization-input-container .localization-trigger{color:#95a5a6;display:none;font-size:14px;height:10px;outline:none;position:absolute;width:10px}.localization-input-container .localization-trigger:active,.localization-input-container .localization-trigger:focus,.localization-input-container .localization-trigger:hover{color:#2581b8;text-decoration:none}table.data td.active .localization-input-container .localization-trigger,table.inspector-fields td.active .localization-input-container .localization-trigger{display:block}table.data td.active .localization-input-container .localization-trigger{right:7px!important;top:5px!important}.control-table td[data-column-type=builderLocalization] input[type=text]{border:none;display:block;height:100%;outline:none;padding-right:20px!important;padding:6px 10px;width:100%}html.chrome .control-table td[data-column-type=builderLocalization] input[type=text]{padding:6px 10px 7px!important}html.gecko .control-table td[data-column-type=builderLocalization] input[type=text],html.safari .control-table td[data-column-type=builderLocalization] input[type=text]{padding:5px 10px}.autocomplete.dropdown-menu.table-widget-autocomplete.localization li a{word-wrap:break-word;white-space:normal}table.data td[data-column-type=builderLocalization] .loading-indicator-container.size-small .loading-indicator{padding-bottom:0!important}table.data td[data-column-type=builderLocalization] .loading-indicator-container.size-small .loading-indicator span{left:auto;right:6px}[data-entity=code] .secondary-content-tabs .nav-tabs{display:none}.control-codelist p.no-data{border-radius:4px;color:var(--bs-secondary-color);font-size:14px;font-weight:400;margin:0;padding:22px;text-align:center}.control-codelist p.parent,.control-codelist ul li{font-weight:300;line-height:150%;margin-bottom:0}.control-codelist p.parent.active a,.control-codelist ul li.active a{background:#ddd;position:relative}.control-codelist p.parent.active a:after,.control-codelist ul li.active a:after{background:var(--bs-primary);content:" ";display:block;height:100%;left:0;position:absolute;top:0;width:4px}.control-codelist p.parent a.link,.control-codelist ul li a.link{word-wrap:break-word;color:var(--bs-body-color);display:block;font-size:14px;font-weight:400;outline:none;padding:10px 50px 10px 20px;position:relative}.control-codelist p.parent a.link:active,.control-codelist p.parent a.link:focus,.control-codelist p.parent a.link:hover,.control-codelist ul li a.link:active,.control-codelist ul li a.link:focus,.control-codelist ul li a.link:hover{text-decoration:none}.control-codelist p.parent a.link span,.control-codelist ul li a.link span{display:block}.control-codelist p.parent a.link span.description,.control-codelist ul li a.link span.description{word-wrap:break-word;color:var(--oc-primary-color);font-size:12px;font-weight:400}.control-codelist p.parent a.link span.description strong,.control-codelist ul li a.link span.description strong{color:var(--bs-body-color);font-weight:400}.control-codelist p.parent.directory a.link,.control-codelist p.parent.parent a.link,.control-codelist ul li.directory a.link,.control-codelist ul li.parent a.link{padding-left:40px}.control-codelist p.parent.directory a.link:after,.control-codelist p.parent.parent a.link:after,.control-codelist ul li.directory a.link:after,.control-codelist ul li.parent a.link:after{-webkit-font-smoothing:antialiased;color:#a1aab1;content:"\f07b";display:block;font-family:FontAwesome;font-size:14px;font-style:normal;font-weight:400;height:10px;left:20px;position:absolute;text-decoration:inherit;top:10px;width:10px}.control-codelist p.parent.parent a.link,.control-codelist ul li.parent a.link{word-wrap:break-word;background-color:var(--oc-primary-bg);color:var(--oc-primary-color);padding-left:41px}.control-codelist p.parent.parent a.link:before,.control-codelist ul li.parent a.link:before{background:var(--oc-primary-border);content:"";display:block;height:1px;left:0;position:absolute;top:0;width:100%}.control-codelist p.parent.parent a.link:after,.control-codelist ul li.parent a.link:after{-webkit-font-smoothing:antialiased;color:var(--oc-primary-color);content:"\f053";font-family:FontAwesome;font-size:13px;font-style:normal;font-weight:400;height:18px;left:22px;opacity:.5;text-decoration:inherit;top:11px;width:18px}.control-codelist p.parent a.link:hover{background:var(--oc-editor-section-bg)!important;color:var(--oc-editor-section-color)!important}.control-codelist p.parent a.link:hover:after{opacity:1}.control-codelist p.parent a.link:hover:before{display:none}.control-codelist ul{margin:0;padding:0}.control-codelist ul li{font-weight:300;line-height:150%;list-style:none;position:relative}.control-codelist ul li a.link:hover,.control-codelist ul li.active a.link{background:var(--oc-editor-section-bg);color:var(--oc-editor-section-color)}.control-codelist ul li.active a.link{position:relative}.control-codelist ul li.active a.link:after{background:var(--oc-primary-border);content:" ";display:block;height:100%;left:0;position:absolute;top:0;width:4px}.control-codelist ul li div.controls{position:absolute;right:45px;top:10px}.control-codelist ul li div.controls .dropdown{height:21px;width:14px}.control-codelist ul li div.controls .dropdown.open a.control{display:block!important}.control-codelist ul li div.controls .dropdown.open a.control:before{display:block;visibility:visible}.control-codelist ul li div.controls a.control{color:var(--bs-body-color);cursor:pointer;display:none;font-size:14px;height:21px;opacity:.5;overflow:hidden;text-decoration:none;visibility:hidden;width:14px}.control-codelist ul li div.controls a.control:before{display:block;margin-right:0;visibility:visible}.control-codelist ul li div.controls a.control:hover{opacity:1}.control-codelist ul li:hover{background:var(--oc-editor-section-bg);color:var(--oc-editor-section-color)}.control-codelist ul li:hover a.control,.control-codelist ul li:hover a.control>a.control,.control-codelist ul li:hover div.controls,.control-codelist ul li:hover div.controls>a.control{display:block!important}.control-codelist ul li .form-check{position:absolute;right:5px;top:10px}.control-codelist ul li .form-check label{margin-right:0}.control-codelist div.list-container{position:relative;transform:translate(0)}.control-codelist div.list-container.animate ul{transition:all .2s ease}.control-codelist div.list-container.goForward ul{transform:translate(-350px)}.control-codelist div.list-container.goBackward ul{transform:translate(350px)}.control-filelist ul li.group.model>h4 a:after{content:"\f074";top:10px}.control-filelist ul li.group.form>h4 a:after{content:"\f14a"}.control-filelist ul li.group.list>h4 a:after{content:"\f00b";top:10px}.control-filelist ul li.group>ul>li.group>ul>li>a{margin-left:-20px;padding-left:73px}.control-filelist ul li.with-icon span.description,.control-filelist ul li.with-icon span.title{padding-left:22px}.control-filelist ul li.with-icon i.list-icon{color:#405261;left:20px;position:absolute;top:12px}.control-filelist ul li.with-icon i.list-icon.mute{color:#8f8f8f}.control-filelist ul li.with-icon i.list-icon.icon-check-square{color:#8da85e}html.gecko .control-filelist ul li.group{margin-right:10px}.builder-inspector-container{border-left:1px solid var(--bs-border-color,#d9d9d9);width:350px}.builder-inspector-container:empty{display:none!important}form.hide-secondary-tabs div.control-tabs.secondary-tabs ul.nav.nav-tabs{display:none}.form-group.size-quarter{width:23.5%}.form-group.size-three-quarter{width:73.5%}form[data-entity=database] div.field-datatable,form[data-entity=database] div.field-datatable div[data-control=table],form[data-entity=database] div.field-datatable div[data-control=table] div.table-container,form[data-entity=models] div.field-datatable,form[data-entity=models] div.field-datatable div[data-control=table],form[data-entity=models] div.field-datatable div[data-control=table] div.table-container{height:100%;position:absolute;width:100%}form[data-entity=database] div.field-datatable div[data-control=table] div.table-container div.control-scrollbar,form[data-entity=models] div.field-datatable div[data-control=table] div.table-container div.control-scrollbar{bottom:0;height:auto!important;max-height:none!important;position:absolute;top:70px}.control-tabs.auxiliary-tabs{background:#fff}.control-tabs.auxiliary-tabs>div>ul.nav-tabs,.control-tabs.auxiliary-tabs>ul.nav-tabs{background:#fff;padding-bottom:2px;padding-left:20px;position:relative}.control-tabs.auxiliary-tabs>div>ul.nav-tabs:before,.control-tabs.auxiliary-tabs>ul.nav-tabs:before{background:#95a5a6;content:" ";display:block;height:1px;left:0;position:absolute;top:0;width:100%}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li,.control-tabs.auxiliary-tabs>ul.nav-tabs>li{margin-right:2px}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li>a,.control-tabs.auxiliary-tabs>ul.nav-tabs>li>a{background:#fff;border-bottom:1px solid #ecf0f1!important;border-left:1px solid #ecf0f1!important;border-radius:0 0 4px 4px;border-right:1px solid #ecf0f1!important;color:#bdc3c7;line-height:100%;padding:4px 10px}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li>a>span.title>span,.control-tabs.auxiliary-tabs>ul.nav-tabs>li>a>span.title>span{font-size:13px;height:auto;margin-bottom:0}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li.active,.control-tabs.auxiliary-tabs>ul.nav-tabs>li.active{top:0}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li.active:before,.control-tabs.auxiliary-tabs>ul.nav-tabs>li.active:before{background:#fff;content:" ";display:block;height:1px;left:0;position:absolute;top:0;top:-1px;width:100%}.control-tabs.auxiliary-tabs>div>ul.nav-tabs>li.active a,.control-tabs.auxiliary-tabs>ul.nav-tabs>li.active a{border-bottom:1px solid #95a5a6!important;border-left:1px solid #95a5a6!important;border-right:1px solid #95a5a6!important;color:#95a5a6;padding-top:5px}.control-tabs.auxiliary-tabs>div.tab-content>.tab-pane{background:#fff} diff --git a/plugins/rainlab/builder/assets/images/builder-icon.svg b/plugins/rainlab/builder/assets/images/builder-icon.svg new file mode 100644 index 0000000..99c5de6 --- /dev/null +++ b/plugins/rainlab/builder/assets/images/builder-icon.svg @@ -0,0 +1,17 @@ + + + + Group + Created with Sketch. + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/images/loader-transparent.svg b/plugins/rainlab/builder/assets/images/loader-transparent.svg new file mode 100644 index 0000000..cf84589 --- /dev/null +++ b/plugins/rainlab/builder/assets/images/loader-transparent.svg @@ -0,0 +1,20 @@ + + + +]> + + + + + + + + diff --git a/plugins/rainlab/builder/assets/images/tab-dark.png b/plugins/rainlab/builder/assets/images/tab-dark.png new file mode 100644 index 0000000..0862f05 Binary files /dev/null and b/plugins/rainlab/builder/assets/images/tab-dark.png differ diff --git a/plugins/rainlab/builder/assets/images/tab.png b/plugins/rainlab/builder/assets/images/tab.png new file mode 100644 index 0000000..9227f65 Binary files /dev/null and b/plugins/rainlab/builder/assets/images/tab.png differ diff --git a/plugins/rainlab/builder/assets/js/build-min.js b/plugins/rainlab/builder/assets/js/build-min.js new file mode 100644 index 0000000..849ef07 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/build-min.js @@ -0,0 +1 @@ +!function($){"use strict";void 0===$.oc.builder&&($.oc.builder={});var Base=$.oc.foundation.base,DataRegistry=(Base.prototype,function(){this.data={},this.requestCache={},this.callbackCache={},Base.call(this)});DataRegistry.prototype.set=function(plugin,type,subtype,data,params){this.storeData(plugin,type,subtype,data),"localization"!=type||subtype||this.localizationUpdated(plugin,params)},DataRegistry.prototype.get=function($formElement,plugin,type,subtype,callback){if(void 0===this.data[plugin]||void 0===this.data[plugin][type]||void 0===this.data[plugin][type][subtype]||this.isCacheObsolete(this.data[plugin][type][subtype].timestamp))return this.loadDataFromServer($formElement,plugin,type,subtype,callback);callback(this.data[plugin][type][subtype].data)},DataRegistry.prototype.makeCacheKey=function(plugin,type,subtype){var key=plugin+"-"+type;return subtype&&(key+="-"+subtype),key},DataRegistry.prototype.isCacheObsolete=function(timestamp){return Date.now()-timestamp>3e5},DataRegistry.prototype.loadDataFromServer=function($formElement,plugin,type,subtype,callback){var self=this,cacheKey=this.makeCacheKey(plugin,type,subtype);return void 0===this.requestCache[cacheKey]&&(this.requestCache[cacheKey]=$formElement.request("onPluginDataRegistryGetData",{data:{registry_plugin_code:plugin,registry_data_type:type,registry_data_subtype:subtype}}).done((function(data){if(void 0===data.registryData)throw new Error("Invalid data registry response.");self.storeData(plugin,type,subtype,data.registryData),self.applyCallbacks(cacheKey,data.registryData),self.requestCache[cacheKey]=void 0}))),this.addCallbackToQueue(callback,cacheKey),this.requestCache[cacheKey]},DataRegistry.prototype.addCallbackToQueue=function(callback,key){void 0===this.callbackCache[key]&&(this.callbackCache[key]=[]),this.callbackCache[key].push(callback)},DataRegistry.prototype.applyCallbacks=function(key,registryData){if(void 0!==this.callbackCache[key]){for(var i=this.callbackCache[key].length-1;i>=0;i--)this.callbackCache[key][i](registryData);delete this.callbackCache[key]}},DataRegistry.prototype.storeData=function(plugin,type,subtype,data){void 0===this.data[plugin]&&(this.data[plugin]={}),void 0===this.data[plugin][type]&&(this.data[plugin][type]={});var dataItem={timestamp:Date.now(),data:data};this.data[plugin][type][subtype]=dataItem},DataRegistry.prototype.clearCache=function(plugin,type){void 0!==this.data[plugin]&&void 0!==this.data[plugin][type]&&(this.data[plugin][type]=void 0)},DataRegistry.prototype.getLocalizationString=function($formElement,plugin,key,callback){this.get($formElement,plugin,"localization",null,(function(data){void 0===data[key]?callback(key):callback(data[key])}))},DataRegistry.prototype.localizationUpdated=function(plugin,params){$.oc.builder.localizationInput.updatePluginInputs(plugin),void 0!==params&¶ms.suppressLanguageEditorUpdate||$.oc.builder.indexController.entityControllers.localization.languageUpdated(plugin),$.oc.builder.indexController.entityControllers.localization.updateOnScreenStrings(plugin)},$.oc.builder.dataRegistry=new DataRegistry}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.foundation.base,BaseProto=Base.prototype,EntityBase=function(typeName,indexController){if(void 0===typeName)throw new Error("The Builder entity type name should be set in the base constructor call.");if(void 0===indexController)throw new Error("The Builder index controller should be set when creating an entity controller.");this.typeName=typeName,this.indexController=indexController,Base.call(this)};(EntityBase.prototype=Object.create(BaseProto)).constructor=EntityBase,EntityBase.prototype.registerHandlers=function(){},EntityBase.prototype.invokeCommand=function(command,ev){if(!/^cmd[a-zA-Z0-9]+$/.test(command))throw new Error("Invalid command: "+command);if(void 0===this[command])throw new Error("Unknown command: "+command);this[command].apply(this,[ev])},EntityBase.prototype.newTabId=function(){return this.typeName+Math.random()},EntityBase.prototype.makeTabId=function(objectName){return this.typeName+"-"+objectName},EntityBase.prototype.getMasterTabsActivePane=function(){return this.indexController.getMasterTabActivePane()},EntityBase.prototype.getMasterTabsObject=function(){return this.indexController.masterTabsObj},EntityBase.prototype.getSelectedPlugin=function(){return $("#PluginList-pluginList-plugin-list > ul > li.active").data("id")},EntityBase.prototype.getIndexController=function(){return this.indexController},EntityBase.prototype.updateMasterTabIdAndTitle=function($tabPane,responseData){var tabsObject=this.getMasterTabsObject();tabsObject.updateIdentifier($tabPane,responseData.tabId),tabsObject.updateTitle($tabPane,responseData.tabTitle)},EntityBase.prototype.unhideFormDeleteButton=function($tabPane){$("[data-control=delete-button]",$tabPane).removeClass("hide oc-hide")},EntityBase.prototype.forceCloseTab=function($tabPane){$tabPane.trigger("close.oc.tab",[{force:!0}])},EntityBase.prototype.unmodifyTab=function($tabPane){this.indexController.unchangeTab($tabPane)},$.oc.builder.entityControllers.base=EntityBase}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Plugin=function(indexController){Base.call(this,"plugin",indexController),this.popupZIndex=5050};(Plugin.prototype=Object.create(BaseProto)).constructor=Plugin,Plugin.prototype.cmdMakePluginActive=function(ev){var selectedPluginCode=$(ev.currentTarget).data("pluginCode");this.makePluginActive(selectedPluginCode)},Plugin.prototype.cmdCreatePlugin=function(ev){var $target=$(ev.currentTarget);$target.one("shown.oc.popup",this.proxy(this.onPluginPopupShown)),$target.popup({handler:"onPluginLoadPopup",zIndex:this.popupZIndex})},Plugin.prototype.cmdApplyPluginSettings=function(ev){var $form=$(ev.currentTarget),self=this;$.oc.stripeLoadIndicator.show(),$form.request("onPluginSave").always($.oc.builder.indexController.hideStripeIndicatorProxy).done((function(data){$form.trigger("close.oc.popup"),self.applyPluginSettingsDone(data)}))},Plugin.prototype.cmdEditPluginSettings=function(ev){var $target=$(ev.currentTarget);$target.one("shown.oc.popup",this.proxy(this.onPluginPopupShown)),$target.popup({handler:"onPluginLoadPopup",zIndex:this.popupZIndex,extraData:{pluginCode:$target.data("pluginCode")}})},Plugin.prototype.onPluginPopupShown=function(ev,button,popup){$(popup).find("input[name=name]").focus()},Plugin.prototype.applyPluginSettingsDone=function(data){void 0!==data.responseData&&void 0!==data.responseData.isNewPlugin&&this.makePluginActive(data.responseData.pluginCode,!0)},Plugin.prototype.makePluginActive=function(pluginCode,updatePluginList){var $form=$("#builder-plugin-selector-panel form").first();$.oc.stripeLoadIndicator.show(),$form.request("onPluginSetActive",{data:{pluginCode:pluginCode,updatePluginList:updatePluginList?1:0}}).always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.makePluginActiveDone))},Plugin.prototype.makePluginActiveDone=function(data){var pluginCode=data.responseData.pluginCode;$("#builder-plugin-selector-panel [data-control=filelist]").fileList("markActive",pluginCode)},$.oc.builder.entityControllers.plugin=Plugin}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,DatabaseTable=function(indexController){Base.call(this,"databaseTable",indexController)};(DatabaseTable.prototype=Object.create(BaseProto)).constructor=DatabaseTable,DatabaseTable.prototype.cmdCreateTable=function(ev){var result=this.indexController.openOrLoadMasterTab($(ev.target),"onDatabaseTableCreateOrOpen",this.newTabId());!1!==result&&result.done(this.proxy(this.onTableLoaded,this))},DatabaseTable.prototype.cmdOpenTable=function(ev){var table=$(ev.currentTarget).data("id"),result=this.indexController.openOrLoadMasterTab($(ev.target),"onDatabaseTableCreateOrOpen",this.makeTabId(table),{table_name:table});!1!==result&&result.done(this.proxy(this.onTableLoaded,this))},DatabaseTable.prototype.cmdSaveTable=function(ev){var $target=$(ev.currentTarget);if(this.validateTable($target)){var data={columns:this.getTableData($target)};$target.popup({extraData:data,handler:"onDatabaseTableValidateAndShowPopup"})}},DatabaseTable.prototype.cmdSaveMigration=function(ev){var $target=$(ev.currentTarget);$.oc.stripeLoadIndicator.show(),$target.request("onDatabaseTableMigrationApply").always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.saveMigrationDone))},DatabaseTable.prototype.cmdDeleteTable=function(ev){var $target=$(ev.currentTarget);$.oc.confirm($target.data("confirm"),this.proxy(this.deleteConfirmed))},DatabaseTable.prototype.cmdUnModifyForm=function(){var $masterTabPane=this.getMasterTabsActivePane();this.unmodifyTab($masterTabPane)},DatabaseTable.prototype.cmdAddIdColumn=function(ev){var $target=$(ev.currentTarget);this.addIdColumn($target)||alert($target.closest("form").attr("data-lang-id-exists"))},DatabaseTable.prototype.cmdAddTimestamps=function(ev){var $target=$(ev.currentTarget);this.addTimeStampColumns($target,["created_at","updated_at"])||alert($target.closest("form").attr("data-lang-timestamps-exist"))},DatabaseTable.prototype.cmdAddSoftDelete=function(ev){var $target=$(ev.currentTarget);this.addTimeStampColumns($target,["deleted_at"])||alert($target.closest("form").attr("data-lang-soft-deleting-exist"))},DatabaseTable.prototype.onTableCellChanged=function(ev,column,value,rowIndex){var $target=$(ev.target);if("columns"==$target.data("alias")&&"database"==$target.closest("form").data("entity")){var updatedRow={};"auto_increment"==column&&value&&(updatedRow.unsigned=1,updatedRow.primary_key=1),"unsigned"!=column||value||(updatedRow.auto_increment=0),"primary_key"==column&&value&&(updatedRow.allow_null=0),"allow_null"==column&&value&&(updatedRow.primary_key=0),"primary_key"!=column||value||(updatedRow.auto_increment=0),$target.table("setRowValues",rowIndex,updatedRow)}},DatabaseTable.prototype.onTableLoaded=function(){$(document).trigger("render");var $masterTabPane=this.getMasterTabsActivePane(),$form=$masterTabPane.find("form"),$toolbar=$masterTabPane.find("div[data-control=table] div.toolbar"),$addIdButton=$(''),$addTimestampsButton=$(''),$addSoftDeleteButton=$('');$addIdButton.text($form.attr("data-lang-add-id")),$toolbar.append($addIdButton),$addTimestampsButton.text($form.attr("data-lang-add-timestamps")),$toolbar.append($addTimestampsButton),$addSoftDeleteButton.text($form.attr("data-lang-add-soft-delete")),$toolbar.append($addSoftDeleteButton)},DatabaseTable.prototype.registerHandlers=function(){this.indexController.$masterTabs.on("oc.tableCellChanged",this.proxy(this.onTableCellChanged))},DatabaseTable.prototype.validateTable=function($target){var tableObj=this.getTableControlObject($target);return tableObj.unfocusTable(),tableObj.validate()},DatabaseTable.prototype.getTableData=function($target){return this.getTableControlObject($target).dataSource.getAllData()},DatabaseTable.prototype.getTableControlObject=function($target){var tableObj=$target.closest("form").find("[data-control=table]").data("oc.table");if(!tableObj)throw new Error("Table object is not found on the database table tab");return tableObj},DatabaseTable.prototype.saveMigrationDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");$("#builderTableMigrationPopup").trigger("close.oc.popup");var $masterTabPane=this.getMasterTabsActivePane();this.getMasterTabsObject();"delete"!=data.builderResponseData.operation?($masterTabPane.find("input[name=table_name]").val(data.builderResponseData.builderObjectName),this.updateMasterTabIdAndTitle($masterTabPane,data.builderResponseData),this.unhideFormDeleteButton($masterTabPane),this.getTableList().fileList("markActive",data.builderResponseData.tabId),this.getIndexController().unchangeTab($masterTabPane),this.updateTable(data.builderResponseData)):this.forceCloseTab($masterTabPane),$.oc.builder.dataRegistry.clearCache(data.builderResponseData.pluginCode,"model-columns")},DatabaseTable.prototype.getTableList=function(){return $("#layout-side-panel form[data-content-id=database] [data-control=filelist]")},DatabaseTable.prototype.deleteConfirmed=function(){this.getMasterTabsActivePane().find("form").popup({handler:"onDatabaseTableShowDeletePopup"})},DatabaseTable.prototype.getColumnNames=function($target){this.getTableControlObject($target).unfocusTable();var data=this.getTableData($target),result=[];for(var index in data)void 0!==data[index].name&&result.push($.trim(data[index].name));return result},DatabaseTable.prototype.addIdColumn=function($target){var added=!1;if(-1===this.getColumnNames($target).indexOf("id")){var tableObj=this.getTableControlObject($target),currentData=this.getTableData($target);(currentData.length-1||currentData[0].name||currentData[0].type||currentData[0].length||currentData[0].unsigned||currentData[0].nullable||currentData[0].auto_increment||currentData[0].primary_key||currentData[0].default)&&tableObj.addRecord("bottom",!0),tableObj.setRowValues(currentData.length-1,{name:"id",type:"integer",unsigned:!0,auto_increment:!0,primary_key:!0}),tableObj.addRecord("bottom",!1),tableObj.deleteRecord(),added=!0}return added&&$target.trigger("change"),added},DatabaseTable.prototype.addTimeStampColumns=function($target,columns){var existingColumns=this.getColumnNames($target),added=!1;for(var index in columns){var column=columns[index];-1===existingColumns.indexOf(column)&&(this.addTimeStampColumn($target,column),added=!0)}return added&&$target.trigger("change"),added},DatabaseTable.prototype.addTimeStampColumn=function($target,column){var tableObj=this.getTableControlObject($target),currentData=this.getTableData($target),rowData={name:column,type:"timestamp",default:null,allow_null:!0};tableObj.addRecord("bottom",!0),tableObj.setRowValues(currentData.length-1,rowData),tableObj.addRecord("bottom",!1),tableObj.deleteRecord()},DatabaseTable.prototype.updateTable=function(data){var tabsObject=this.getMasterTabsObject(),tab=$("#builder-master-tabs").data("oc.tab").findByIdentifier(data.tabId);tabsObject.updateTab(tab,data.tableName,data.tab),this.onTableLoaded()},$.oc.builder.entityControllers.databaseTable=DatabaseTable}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Model=function(indexController){Base.call(this,"model",indexController)};(Model.prototype=Object.create(BaseProto)).constructor=Model,Model.prototype.cmdCreateModel=function(ev){var $target=$(ev.currentTarget);$target.one("shown.oc.popup",this.proxy(this.onModelPopupShown)),$target.popup({handler:"onModelLoadPopup"})},Model.prototype.cmdApplyModelSettings=function(ev){var $form=$(ev.currentTarget),self=this;$.oc.stripeLoadIndicator.show(),$form.request("onModelSave").always($.oc.builder.indexController.hideStripeIndicatorProxy).done((function(data){$form.trigger("close.oc.popup"),self.applyModelSettingsDone(data)}))},Model.prototype.onModelPopupShown=function(ev,button,popup){$(popup).find("input[name=className]").focus()},Model.prototype.applyModelSettingsDone=function(data){if(void 0!==data.builderResponseData.registryData){var registryData=data.builderResponseData.registryData;$.oc.builder.dataRegistry.set(registryData.pluginCode,"model-classes",null,registryData.models)}},$.oc.builder.entityControllers.model=Model}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,ModelForm=function(indexController){Base.call(this,"modelForm",indexController)};(ModelForm.prototype=Object.create(BaseProto)).constructor=ModelForm,ModelForm.prototype.cmdCreateForm=function(ev){var $link=$(ev.currentTarget),data={model_class:$link.data("modelClass")};this.indexController.openOrLoadMasterTab($link,"onModelFormCreateOrOpen",this.newTabId(),data)},ModelForm.prototype.cmdSaveForm=function(ev){var $target=$(ev.currentTarget),$form=$target.closest("form"),$rootContainer=$("[data-root-control-wrapper] > [data-control-container]",$form),$inspectorContainer=$form.find(".inspector-container"),controls=$.oc.builder.formbuilder.domToPropertyJson.convert($rootContainer.get(0));if($.oc.inspector.manager.applyValuesFromContainer($inspectorContainer))if(!1!==controls){var data={controls:controls};$target.request("onModelFormSave",{data:data}).done(this.proxy(this.saveFormDone))}else $.oc.flashMsg({text:$.oc.builder.formbuilder.domToPropertyJson.getLastError(),class:"error",interval:5})},ModelForm.prototype.cmdAddDatabaseFields=function(ev){var $target=$(ev.currentTarget),$placeholder=this.getMasterTabsActivePane().find(".builder-control-list .control.oc-placeholder:first")[0],fields=$target.find(".control-table").data("oc.table").dataSource.data.filter((function(column){return column.add})).reverse();$target.closest(".control-popup").data("oc.popup").hide(),$.oc.stripeLoadIndicator.show();var allFields=$.when({});$.each(fields,(function(index,field){allFields=allFields.then(function(field){return function(){var defer=$.Deferred();return $.oc.builder.formbuilder.controller.addControlToPlaceholder($placeholder,field.type,field.label?field.label:field.column,!1,field.column).always((function(){defer.resolve()})),defer.promise()}}(field))})),$.when(allFields).always($.oc.builder.indexController.hideStripeIndicatorProxy)},ModelForm.prototype.cmdOpenForm=function(ev){var form=$(ev.currentTarget).data("form"),model=$(ev.currentTarget).data("modelClass");this.indexController.openOrLoadMasterTab($(ev.target),"onModelFormCreateOrOpen",this.makeTabId(model+"-"+form),{file_name:form,model_class:model})},ModelForm.prototype.cmdDeleteForm=function(ev){var $target=$(ev.currentTarget);$.oc.confirm($target.data("confirm"),this.proxy(this.deleteConfirmed))},ModelForm.prototype.cmdAddControl=function(ev){$.oc.builder.formbuilder.controlPalette.addControl(ev)},ModelForm.prototype.cmdUndockControlPalette=function(ev){$.oc.builder.formbuilder.controlPalette.undockFromContainer(ev)},ModelForm.prototype.cmdDockControlPalette=function(ev){$.oc.builder.formbuilder.controlPalette.dockToContainer(ev)},ModelForm.prototype.cmdCloseControlPalette=function(ev){$.oc.builder.formbuilder.controlPalette.closeInContainer(ev)},ModelForm.prototype.saveFormDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();$masterTabPane.find("input[name=file_name]").val(data.builderResponseData.builderObjectName),this.updateMasterTabIdAndTitle($masterTabPane,data.builderResponseData),this.unhideFormDeleteButton($masterTabPane),this.getModelList().fileList("markActive",data.builderResponseData.tabId),this.getIndexController().unchangeTab($masterTabPane),this.updateDataRegistry(data)},ModelForm.prototype.updateDataRegistry=function(data){if(void 0!==data.builderResponseData.registryData){var registryData=data.builderResponseData.registryData;$.oc.builder.dataRegistry.set(registryData.pluginCode,"model-forms",registryData.modelClass,registryData.forms)}},ModelForm.prototype.deleteConfirmed=function(){var $form=this.getMasterTabsActivePane().find("form");$.oc.stripeLoadIndicator.show(),$form.request("onModelFormDelete").always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.deleteDone))},ModelForm.prototype.deleteDone=function(data){var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),this.forceCloseTab($masterTabPane),this.updateDataRegistry(data)},ModelForm.prototype.getModelList=function(){return $("#layout-side-panel form[data-content-id=models] [data-control=filelist]")},$.oc.builder.entityControllers.modelForm=ModelForm}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,ModelList=function(indexController){this.cachedModelFieldsPromises={},Base.call(this,"modelList",indexController)};(ModelList.prototype=Object.create(BaseProto)).constructor=ModelList,ModelList.prototype.registerHandlers=function(){$(document).on("autocompleteitems.oc.table",'form[data-sub-entity="model-list"] [data-control=table]',this.proxy(this.onAutocompleteItems))},ModelList.prototype.cmdCreateList=function(ev){var $link=$(ev.currentTarget),data={model_class:$link.data("modelClass")},result=this.indexController.openOrLoadMasterTab($link,"onModelListCreateOrOpen",this.newTabId(),data);!1!==result&&result.done(this.proxy(this.onListLoaded,this))},ModelList.prototype.cmdSaveList=function(ev){var $target=$(ev.currentTarget);$target.closest("form");this.validateTable($target)&&$target.request("onModelListSave",{data:{columns:this.getTableData($target)}}).done(this.proxy(this.saveListDone))},ModelList.prototype.cmdOpenList=function(ev){var list=$(ev.currentTarget).data("list"),model=$(ev.currentTarget).data("modelClass"),result=this.indexController.openOrLoadMasterTab($(ev.target),"onModelListCreateOrOpen",this.makeTabId(model+"-"+list),{file_name:list,model_class:model});!1!==result&&result.done(this.proxy(this.onListLoaded,this))},ModelList.prototype.cmdDeleteList=function(ev){var $target=$(ev.currentTarget);$.oc.confirm($target.data("confirm"),this.proxy(this.deleteConfirmed))},ModelList.prototype.cmdAddDatabaseColumns=function(ev){var $target=$(ev.currentTarget);$.oc.stripeLoadIndicator.show(),$target.request("onModelListLoadDatabaseColumns").done(this.proxy(this.databaseColumnsLoaded)).always($.oc.builder.indexController.hideStripeIndicatorProxy)},ModelList.prototype.saveListDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();$masterTabPane.find("input[name=file_name]").val(data.builderResponseData.builderObjectName),this.updateMasterTabIdAndTitle($masterTabPane,data.builderResponseData),this.unhideFormDeleteButton($masterTabPane),this.getModelList().fileList("markActive",data.builderResponseData.tabId),this.getIndexController().unchangeTab($masterTabPane),this.updateDataRegistry(data)},ModelList.prototype.deleteConfirmed=function(){var $form=this.getMasterTabsActivePane().find("form");$.oc.stripeLoadIndicator.show(),$form.request("onModelListDelete").always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.deleteDone))},ModelList.prototype.deleteDone=function(data){var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),this.forceCloseTab($masterTabPane),this.updateDataRegistry(data)},ModelList.prototype.getTableControlObject=function($target){var tableObj=$target.closest("form").find("[data-control=table]").data("oc.table");if(!tableObj)throw new Error("Table object is not found on the model list tab");return tableObj},ModelList.prototype.getModelList=function(){return $("#layout-side-panel form[data-content-id=models] [data-control=filelist]")},ModelList.prototype.validateTable=function($target){var tableObj=this.getTableControlObject($target);return tableObj.unfocusTable(),tableObj.validate()},ModelList.prototype.getTableData=function($target){return this.getTableControlObject($target).dataSource.getAllData()},ModelList.prototype.loadModelFields=function(table,callback){var $form=$(table).closest("form"),modelClass=$form.find("input[name=model_class]").val(),cachedFields=$form.data("oc.model-field-cache");void 0===cachedFields?(void 0===this.cachedModelFieldsPromises[modelClass]&&(this.cachedModelFieldsPromises[modelClass]=$form.request("onModelFormGetModelFields",{data:{as_plain_list:1}})),void 0!==callback&&this.cachedModelFieldsPromises[modelClass].done((function(data){$form.data("oc.model-field-cache",data.responseData.options),callback(data.responseData.options)}))):callback(cachedFields)},ModelList.prototype.updateDataRegistry=function(data){if(void 0!==data.builderResponseData.registryData){var registryData=data.builderResponseData.registryData;$.oc.builder.dataRegistry.set(registryData.pluginCode,"model-lists",registryData.modelClass,registryData.lists),$.oc.builder.dataRegistry.clearCache(registryData.pluginCode,"plugin-lists")}},ModelList.prototype.databaseColumnsLoaded=function(data){$.isArray(data.responseData.columns)||alert("Invalid server response");var $form=this.getMasterTabsActivePane().find("form"),existingColumns=this.getColumnNames($form),columnsAdded=!1;for(var i in data.responseData.columns){var column=data.responseData.columns[i],type=this.mapType(column.type);-1===$.inArray(column.name,existingColumns)&&(this.addColumn($form,column.name,type),columnsAdded=!0)}columnsAdded?$form.trigger("change"):alert($form.attr("data-lang-all-database-columns-exist"))},ModelList.prototype.mapType=function(type){switch(type){case"integer":return"number";case"timestamp":return"datetime";default:return"text"}},ModelList.prototype.addColumn=function($target,column,type){var tableObj=this.getTableControlObject($target),currentData=this.getTableData($target),rowData={field:column,label:column,type:type};tableObj.addRecord("bottom",!0),tableObj.setRowValues(currentData.length-1,rowData),tableObj.addRecord("bottom",!1),tableObj.deleteRecord()},ModelList.prototype.getColumnNames=function($target){this.getTableControlObject($target).unfocusTable();var data=this.getTableData($target),result=[];for(var index in data)void 0!==data[index].field&&result.push($.trim(data[index].field));return result},ModelList.prototype.onAutocompleteItems=function(ev,data){if("model-fields"===data.columnConfiguration.fillFrom)return ev.preventDefault(),this.loadModelFields(ev.target,data.callback),!1},ModelList.prototype.onListLoaded=function(){$(document).trigger("render");var $masterTabPane=this.getMasterTabsActivePane(),$form=$masterTabPane.find("form"),$toolbar=$masterTabPane.find("div[data-control=table] div.toolbar"),$button=$('');$button.text($form.attr("data-lang-add-database-columns")),$toolbar.append($button)},$.oc.builder.entityControllers.modelList=ModelList}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Permission=function(indexController){Base.call(this,"permissions",indexController)};(Permission.prototype=Object.create(BaseProto)).constructor=Permission,Permission.prototype.registerHandlers=function(){this.indexController.$masterTabs.on("oc.tableNewRow",this.proxy(this.onTableRowCreated))},Permission.prototype.cmdOpenPermissions=function(ev){var currentPlugin=this.getSelectedPlugin();currentPlugin?this.indexController.openOrLoadMasterTab($(ev.target),"onPermissionsOpen",this.makeTabId(currentPlugin)):alert("Please select a plugin first")},Permission.prototype.cmdSavePermissions=function(ev){var $target=$(ev.currentTarget);$target.closest("form");this.validateTable($target)&&$target.request("onPermissionsSave",{data:{permissions:this.getTableData($target)}}).done(this.proxy(this.savePermissionsDone))},Permission.prototype.getTableControlObject=function($target){var tableObj=$target.closest("form").find("[data-control=table]").data("oc.table");if(!tableObj)throw new Error("Table object is not found on permissions tab");return tableObj},Permission.prototype.validateTable=function($target){var tableObj=this.getTableControlObject($target);return tableObj.unfocusTable(),tableObj.validate()},Permission.prototype.getTableData=function($target){return this.getTableControlObject($target).dataSource.getAllData()},Permission.prototype.savePermissionsDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),$.oc.builder.dataRegistry.clearCache(data.builderResponseData.pluginCode,"permissions")},Permission.prototype.onTableRowCreated=function(ev,recordData){var $target=$(ev.target);if("permissions"==$target.data("alias")){var $form=$target.closest("form");if("permissions"==$form.data("entity")){var pluginCode=$form.find("input[name=plugin_code]").val();recordData.permission=pluginCode.toLowerCase()+"."}}},$.oc.builder.entityControllers.permission=Permission}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Menus=function(indexController){Base.call(this,"menus",indexController)};(Menus.prototype=Object.create(BaseProto)).constructor=Menus,Menus.prototype.cmdOpenMenus=function(ev){var currentPlugin=this.getSelectedPlugin();currentPlugin?this.indexController.openOrLoadMasterTab($(ev.target),"onMenusOpen",this.makeTabId(currentPlugin)):alert("Please select a plugin first")},Menus.prototype.cmdSaveMenus=function(ev){var $target=$(ev.currentTarget),$form=$target.closest("form"),$inspectorContainer=$form.find(".inspector-container");if($.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)){var menus=$.oc.builder.menubuilder.controller.getJson($form.get(0));$target.request("onMenusSave",{data:{menus:menus}}).done(this.proxy(this.saveMenusDone))}},Menus.prototype.cmdAddMainMenuItem=function(ev){$.oc.builder.menubuilder.controller.addMainMenuItem(ev)},Menus.prototype.cmdAddSideMenuItem=function(ev){$.oc.builder.menubuilder.controller.addSideMenuItem(ev)},Menus.prototype.cmdDeleteMenuItem=function(ev){$.oc.builder.menubuilder.controller.deleteMenuItem(ev)},Menus.prototype.saveMenusDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();$.oc.mainMenu&&data.mainMenu&&data.mainMenuLeft&&$.oc.mainMenu.reload(data.mainMenu,data.mainMenuLeft),this.getIndexController().unchangeTab($masterTabPane)},$.oc.builder.entityControllers.menus=Menus}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Imports=function(indexController){Base.call(this,"imports",indexController)};(Imports.prototype=Object.create(BaseProto)).constructor=Imports,Imports.prototype.cmdOpenImports=function(ev){var currentPlugin=this.getSelectedPlugin();currentPlugin?this.indexController.openOrLoadMasterTab($(ev.target),"onImportsOpen",this.makeTabId(currentPlugin)):alert("Please select a plugin first")},Imports.prototype.cmdConfirmImports=function(ev){$(ev.currentTarget).popup({handler:"onImportsShowConfirmPopup"})},Imports.prototype.cmdSaveImports=function(ev){var $form=this.getMasterTabsActivePane().find("form"),$popup=$(ev.currentTarget).closest(".control-popup");$popup.removeClass("show").popup("setLoading",!0),$form.request("onImportsSave",{data:oc.serializeJSON($popup.get(0))}).done((data=>{$popup.trigger("close.oc.popup"),this.saveImportsDone(data)})).fail((()=>{$popup.addClass("show").popup("setLoading",!1).popup("setShake")}))},Imports.prototype.cmdMigrateDatabase=function(ev){$(ev.currentTarget).request("onMigrateDatabase")},Imports.prototype.cmdAddBlueprintItem=function(ev){},Imports.prototype.cmdRemoveBlueprintItem=function(ev){},Imports.prototype.saveImportsDone=function(data){this.hideInspector(),$("#blueprintList").html(""),$.oc.mainMenu&&data&&data.mainMenu&&data.mainMenuLeft&&$.oc.mainMenu.reload(data.mainMenu,data.mainMenuLeft);var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane)},Imports.prototype.hideInspector=function(){var $container=$(".blueprint-container.inspector-open:first");if($container.length){var $inspectorContainer=this.findInspectorContainer($container);$.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0))}},Imports.prototype.findInspectorContainer=function($element){return $element.closest("[data-inspector-container]").find(".inspector-container")},$.oc.builder.entityControllers.imports=Imports}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Code=function(indexController){Base.call(this,"code",indexController)};(Code.prototype=Object.create(BaseProto)).constructor=Code,Code.prototype.registerHandlers=function(){},Code.prototype.cmdCreateCode=function(ev){this.indexController.openOrLoadMasterTab($(ev.target),"onCodeOpen",this.newTabId())},Code.prototype.cmdOpenCode=function(ev){var path=$(ev.currentTarget).data("path"),pluginCode=$(ev.currentTarget).data("pluginCode"),result=this.indexController.openOrLoadMasterTab($(ev.target),"onCodeOpen",this.makeTabId(pluginCode+"-"+path),{fileName:path});!1!==result&&result.done(this.proxy(this.updateFormEditorMode,this))},Code.prototype.cmdSaveCode=function(ev){var $target=$(ev.currentTarget),$inspectorContainer=$target.closest("form").find(".inspector-container");$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)&&$target.request("onCodeSave").done(this.proxy(this.saveCodeDone))},Code.prototype.saveCodeDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),this.updateFormEditorMode()},Code.prototype.getCodeList=function(){return $("#layout-side-panel form[data-content-id=code] .control-codelist")},Code.prototype.updateFormEditorMode=function(){var $masterTabPane=this.getMasterTabsActivePane(),modes={css:"css",htm:"html",html:"html",js:"javascript",json:"json",less:"less",md:"markdown",sass:"sass",scss:"scss",txt:"plain_text",yaml:"yaml",xml:"xml",php:"php"},parts=$("input[name=fileName]",$masterTabPane).val().split("."),extension="txt",mode="plain_text",editor=$("[data-control=codeeditor]",$masterTabPane);parts.length>=2&&(extension=parts.pop().toLowerCase()),void 0!==modes[extension]&&(mode=modes[extension]);window.setTimeout((function(){editor.data("oc.codeEditor").editor.getSession().setMode({path:"ace/mode/"+mode})}),200)},$.oc.builder.entityControllers.code=Code}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Version=function(indexController){Base.call(this,"version",indexController),this.hiddenHints={}};(Version.prototype=Object.create(BaseProto)).constructor=Version,Version.prototype.cmdCreateVersion=function(ev){var $link=$(ev.currentTarget),versionType=$link.data("versionType");this.indexController.openOrLoadMasterTab($link,"onVersionCreateOrOpen",this.newTabId(),{version_type:versionType})},Version.prototype.cmdSaveVersion=function(ev){var $target=$(ev.currentTarget);$target.closest("form");$target.request("onVersionSave").done(this.proxy(this.saveVersionDone))},Version.prototype.cmdOpenVersion=function(ev){var versionNumber=$(ev.currentTarget).data("id"),pluginCode=$(ev.currentTarget).data("pluginCode");this.indexController.openOrLoadMasterTab($(ev.target),"onVersionCreateOrOpen",this.makeTabId(pluginCode+"-"+versionNumber),{original_version:versionNumber})},Version.prototype.cmdDeleteVersion=function(ev){var $target=$(ev.currentTarget);$.oc.confirm($target.data("confirm"),this.proxy(this.deleteConfirmed))},Version.prototype.cmdApplyVersion=function(ev){var $target=$(ev.currentTarget),$pane=$target.closest("div.tab-pane"),self=this;this.showHintPopup($pane,"builder-version-apply",(function(){$target.request("onVersionApply").done(self.proxy(self.applyVersionDone))}))},Version.prototype.cmdRollbackVersion=function(ev){var $target=$(ev.currentTarget),$pane=$target.closest("div.tab-pane"),self=this;this.showHintPopup($pane,"builder-version-rollback",(function(){$target.request("onVersionRollback").done(self.proxy(self.rollbackVersionDone))}))},Version.prototype.saveVersionDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.updateUiAfterSave($masterTabPane,data),data.builderResponseData.isApplied||this.showSavedNotAppliedHint($masterTabPane)},Version.prototype.showSavedNotAppliedHint=function($masterTabPane){this.showHintPopup($masterTabPane,"builder-version-save-unapplied")},Version.prototype.showHintPopup=function($masterTabPane,code,callback){this.getDontShowHintAgain(code,$masterTabPane)?callback&&callback.apply(this):($masterTabPane.one("hide.oc.popup",this.proxy(this.onHintPopupHide)),callback&&$masterTabPane.one("shown.oc.popup",(function(ev,$element,$modal){$modal.find("form").one("submit",(function(ev){return callback.apply(this),ev.preventDefault(),$(ev.target).trigger("close.oc.popup"),!1}))})),$masterTabPane.popup({content:this.getPopupContent($masterTabPane,code)}))},Version.prototype.onHintPopupHide=function(ev,$element,$modal){var cbValue=$modal.find("input[type=checkbox][name=dont_show_again]").is(":checked"),code=$modal.find("input[type=hidden][name=hint_code]").val();($modal.find("form").off("submit"),cbValue)&&(this.getMasterTabsActivePane().find('form[data-entity="versions"]').request("onHideBackendHint",{data:{name:code}}),this.setDontShowHintAgain(code))},Version.prototype.setDontShowHintAgain=function(code){this.hiddenHints[code]=!0},Version.prototype.getDontShowHintAgain=function(code,$pane){return void 0!==this.hiddenHints[code]?this.hiddenHints[code]:"true"==$pane.find('input[type=hidden][data-hint-hidden="'+code+'"]').val()},Version.prototype.getPopupContent=function($pane,code){var template=$pane.find('script[data-version-hint-template="'+code+'"]');if(0===template.length)throw new Error("Version popup template not found: "+code);return template.html()},Version.prototype.updateUiAfterSave=function($masterTabPane,data){$masterTabPane.find("input[name=original_version]").val(data.builderResponseData.savedVersion),this.updateMasterTabIdAndTitle($masterTabPane,data.builderResponseData),this.unhideFormDeleteButton($masterTabPane),this.getVersionList().fileList("markActive",data.builderResponseData.tabId),this.getIndexController().unchangeTab($masterTabPane)},Version.prototype.deleteConfirmed=function(){var $form=this.getMasterTabsActivePane().find("form");$.oc.stripeLoadIndicator.show(),$form.request("onVersionDelete").always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.deleteDone))},Version.prototype.deleteDone=function(){var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),this.forceCloseTab($masterTabPane)},Version.prototype.applyVersionDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.updateUiAfterSave($masterTabPane,data),this.updateVersionsButtons()},Version.prototype.rollbackVersionDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.updateUiAfterSave($masterTabPane,data),this.updateVersionsButtons()},Version.prototype.getVersionList=function(){return $("#layout-side-panel form[data-content-id=version] [data-control=filelist]")},Version.prototype.updateVersionsButtons=function(){for(var tabsObject=this.getMasterTabsObject(),$tabs=tabsObject.$tabsContainer.find("> li"),$versionList=this.getVersionList(),i=$tabs.length-1;i>=0;i--){var $tab=$($tabs[i]),tabId=$tab.data("tabId");if(tabId&&0!=String(tabId).length){var $versionLi=$versionList.find('li[data-id="'+tabId+'"]');if($versionLi.length){var isApplied=$versionLi.data("applied"),$pane=tabsObject.findPaneFromTab($tab);isApplied?($pane.find('[data-builder-command="version:cmdApplyVersion"]').addClass("hide oc-hide"),$pane.find('[data-builder-command="version:cmdRollbackVersion"]').removeClass("hide oc-hide")):($pane.find('[data-builder-command="version:cmdApplyVersion"]').removeClass("hide oc-hide"),$pane.find('[data-builder-command="version:cmdRollbackVersion"]').addClass("hide oc-hide"))}}}},$.oc.builder.entityControllers.version=Version}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Localization=function(indexController){Base.call(this,"localization",indexController)};(Localization.prototype=Object.create(BaseProto)).constructor=Localization,Localization.prototype.cmdCreateLanguage=function(ev){this.indexController.openOrLoadMasterTab($(ev.target),"onLanguageCreateOrOpen",this.newTabId())},Localization.prototype.cmdOpenLanguage=function(ev){var language=$(ev.currentTarget).data("id"),pluginCode=$(ev.currentTarget).data("pluginCode");this.indexController.openOrLoadMasterTab($(ev.target),"onLanguageCreateOrOpen",this.makeTabId(pluginCode+"-"+language),{original_language:language})},Localization.prototype.cmdSaveLanguage=function(ev){var $target=$(ev.currentTarget);$target.closest("form");$target.request("onLanguageSave").done(this.proxy(this.saveLanguageDone))},Localization.prototype.cmdDeleteLanguage=function(ev){var $target=$(ev.currentTarget);$.oc.confirm($target.data("confirm"),this.proxy(this.deleteConfirmed))},Localization.prototype.cmdCopyMissingStrings=function(ev){var $form=$(ev.currentTarget),language=$form.find("select[name=language]").val(),$masterTabPane=this.getMasterTabsActivePane();$form.trigger("close.oc.popup"),$.oc.stripeLoadIndicator.show(),$masterTabPane.find("form").request("onLanguageCopyStringsFrom",{data:{copy_from:language}}).always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.copyStringsFromDone))},Localization.prototype.languageUpdated=function(plugin){var languageForm=this.findDefaultLanguageForm(plugin);if(languageForm){var $languageForm=$(languageForm);$languageForm.hasClass("oc-data-changed")?this.mergeLanguageFromServer($languageForm):this.updateLanguageFromServer($languageForm)}},Localization.prototype.updateOnScreenStrings=function(plugin){var stringElements=document.body.querySelectorAll('span[data-localization-key][data-plugin="'+plugin+'"]');$.oc.builder.dataRegistry.get($("#builder-plugin-selector-panel form"),plugin,"localization",null,(function(data){for(var i=stringElements.length-1;i>=0;i--){var stringElement=stringElements[i],stringKey=stringElement.getAttribute("data-localization-key");void 0!==data[stringKey]?stringElement.textContent=data[stringKey]:stringElement.textContent=stringKey}}))},Localization.prototype.saveLanguageDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();if($masterTabPane.find("input[name=original_language]").val(data.builderResponseData.language),this.updateMasterTabIdAndTitle($masterTabPane,data.builderResponseData),this.unhideFormDeleteButton($masterTabPane),this.getLanguageList().fileList("markActive",data.builderResponseData.tabId),this.getIndexController().unchangeTab($masterTabPane),void 0!==data.builderResponseData.registryData){var registryData=data.builderResponseData.registryData;$.oc.builder.dataRegistry.set(registryData.pluginCode,"localization",null,registryData.strings,{suppressLanguageEditorUpdate:!0}),$.oc.builder.dataRegistry.set(registryData.pluginCode,"localization","sections",registryData.sections)}},Localization.prototype.getLanguageList=function(){return $("#layout-side-panel form[data-content-id=localization] [data-control=filelist]")},Localization.prototype.getCodeEditor=function($tab){return $tab.find("div[data-field-name=strings] div[data-control=codeeditor]").data("oc.codeEditor").editor},Localization.prototype.deleteConfirmed=function(){var $form=this.getMasterTabsActivePane().find("form");$.oc.stripeLoadIndicator.show(),$form.request("onLanguageDelete").always($.oc.builder.indexController.hideStripeIndicatorProxy).done(this.proxy(this.deleteDone))},Localization.prototype.deleteDone=function(){var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane),this.forceCloseTab($masterTabPane)},Localization.prototype.copyStringsFromDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var responseData=data.builderResponseData,$masterTabPane=this.getMasterTabsActivePane(),$form=$masterTabPane.find("form"),codeEditor=this.getCodeEditor($masterTabPane),newStringMessage=$form.data("newStringMessage"),mismatchMessage=$form.data("structureMismatch");codeEditor.getSession().setValue(responseData.strings);for(var annotations=[],i=responseData.updatedLines.length-1;i>=0;i--){var line=responseData.updatedLines[i];annotations.push({row:line,column:0,text:newStringMessage,type:"warning"})}codeEditor.getSession().setAnnotations(annotations),responseData.mismatch&&$.oc.alert(mismatchMessage)},Localization.prototype.findDefaultLanguageForm=function(plugin){for(var forms=document.body.querySelectorAll("form[data-entity=localization]"),i=forms.length-1;i>=0;i--){var form=forms[i],pluginInput=form.querySelector("input[name=plugin_code]"),languageInput=form.querySelector("input[name=original_language]");if(pluginInput&&pluginInput.value==plugin&&(languageInput&&form.getAttribute("data-default-language")==languageInput.value))return form}return null},Localization.prototype.updateLanguageFromServer=function($languageForm){var self=this;$languageForm.request("onLanguageGetStrings").done((function(data){self.updateLanguageFromServerDone($languageForm,data)}))},Localization.prototype.updateLanguageFromServerDone=function($languageForm,data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var responseData=data.builderResponseData,$tabPane=$languageForm.closest(".tab-pane"),codeEditor=this.getCodeEditor($tabPane);responseData.strings&&(codeEditor.getSession().setValue(responseData.strings),this.unmodifyTab($tabPane))},Localization.prototype.mergeLanguageFromServer=function($languageForm){var language=$languageForm.find("input[name=original_language]").val(),self=this;$languageForm.request("onLanguageCopyStringsFrom",{data:{copy_from:language}}).done((function(data){self.mergeLanguageFromServerDone($languageForm,data)}))},Localization.prototype.mergeLanguageFromServerDone=function($languageForm,data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var responseData=data.builderResponseData,$tabPane=$languageForm.closest(".tab-pane"),codeEditor=this.getCodeEditor($tabPane);codeEditor.getSession().setValue(responseData.strings),codeEditor.getSession().setAnnotations([])},$.oc.builder.entityControllers.localization=Localization}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={}),void 0===$.oc.builder.entityControllers&&($.oc.builder.entityControllers={});var Base=$.oc.builder.entityControllers.base,BaseProto=Base.prototype,Controller=function(indexController){Base.call(this,"controller",indexController)};(Controller.prototype=Object.create(BaseProto)).constructor=Controller,Controller.prototype.cmdCreateController=function(ev){var $form=$(ev.currentTarget),self=this,pluginCode=$form.data("pluginCode");($form.find('input[name="behaviors[]"]:checked').length?this.indexController.openOrLoadMasterTab($form,"onControllerCreate",this.makeTabId(pluginCode+"-new-controller"),{}):$form.request("onControllerCreate")).done((function(data){$form.trigger("close.oc.popup"),self.updateDataRegistry(data)})).always($.oc.builder.indexController.hideStripeIndicatorProxy)},Controller.prototype.cmdOpenController=function(ev){var controller=$(ev.currentTarget).data("id"),pluginCode=$(ev.currentTarget).data("pluginCode");this.indexController.openOrLoadMasterTab($(ev.target),"onControllerOpen",this.makeTabId(pluginCode+"-"+controller),{controller:controller})},Controller.prototype.cmdSaveController=function(ev){var $target=$(ev.currentTarget),$inspectorContainer=$target.closest("form").find(".inspector-container");$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)&&$target.request("onControllerSave").done(this.proxy(this.saveControllerDone))},Controller.prototype.saveControllerDone=function(data){if(void 0===data.builderResponseData)throw new Error("Invalid response data");var $masterTabPane=this.getMasterTabsActivePane();this.getIndexController().unchangeTab($masterTabPane)},Controller.prototype.updateDataRegistry=function(data){if(void 0!==data.builderResponseData.registryData){var registryData=data.builderResponseData.registryData;$.oc.builder.dataRegistry.set(registryData.pluginCode,"controller-urls",null,registryData.urls)}},Controller.prototype.getControllerList=function(){return $("#layout-side-panel form[data-content-id=controller] [data-control=filelist]")},$.oc.builder.entityControllers.controller=Controller}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={});var Base=$.oc.foundation.base,BaseProto=Base.prototype,Builder=function(){Base.call(this),this.$masterTabs=null,this.masterTabsObj=null,this.hideStripeIndicatorProxy=null,this.entityControllers={},this.init()};(Builder.prototype=Object.create(BaseProto)).constructor=Builder,Builder.prototype.dispose=function(){BaseProto.dispose.call(this)},Builder.prototype.openOrLoadMasterTab=function($form,serverHandlerName,tabId,data){if(this.masterTabsObj.goTo(tabId))return!1;var requestData=void 0===data?{}:data;return $.oc.stripeLoadIndicator.show(),$form.request(serverHandlerName,{data:requestData}).done(this.proxy(this.addMasterTab)).always(this.hideStripeIndicatorProxy)},Builder.prototype.getMasterTabActivePane=function(){return this.$masterTabs.find("> .tab-content > .tab-pane.active")},Builder.prototype.unchangeTab=function($pane){$pane.find("form").trigger("unchange.oc.changeMonitor")},Builder.prototype.triggerCommand=function(command,ev){var commandParts=command.split(":");if(2===commandParts.length){var entity=commandParts[0],commandToExecute=commandParts[1];if(void 0===this.entityControllers[entity])throw new Error("Unknown entity type: "+entity);this.entityControllers[entity].invokeCommand(commandToExecute,ev)}},Builder.prototype.init=function(){this.$masterTabs=$("#builder-master-tabs"),this.$sidePanel=$("#builder-side-panel"),this.masterTabsObj=this.$masterTabs.data("oc.tab"),this.hideStripeIndicatorProxy=this.proxy(this.hideStripeIndicator),new $.oc.tabFormExpandControls(this.$masterTabs),this.createEntityControllers(),this.registerHandlers()},Builder.prototype.createEntityControllers=function(){for(var controller in $.oc.builder.entityControllers)"base"!=controller&&(this.entityControllers[controller]=new $.oc.builder.entityControllers[controller](this))},Builder.prototype.registerHandlers=function(){for(var controller in $(document).on("click","[data-builder-command]",this.proxy(this.onCommand)),$(document).on("submit","[data-builder-command]",this.proxy(this.onCommand)),this.$masterTabs.on("changed.oc.changeMonitor",this.proxy(this.onFormChanged)),this.$masterTabs.on("unchanged.oc.changeMonitor",this.proxy(this.onFormUnchanged)),this.$masterTabs.on("shown.bs.tab",this.proxy(this.onTabShown)),this.$masterTabs.on("afterAllClosed.oc.tab",this.proxy(this.onAllTabsClosed)),this.$masterTabs.on("closed.oc.tab",this.proxy(this.onTabClosed)),this.$masterTabs.on("autocompleteitems.oc.inspector",this.proxy(this.onDataRegistryItems)),this.$masterTabs.on("dropdownoptions.oc.inspector",this.proxy(this.onDataRegistryItems)),this.entityControllers)void 0!==this.entityControllers[controller].registerHandlers&&this.entityControllers[controller].registerHandlers()},Builder.prototype.hideStripeIndicator=function(){$.oc.stripeLoadIndicator.hide()},Builder.prototype.addMasterTab=function(data){this.masterTabsObj.addTab(data.tabTitle,data.tab,data.tabId,"oc-"+data.tabIcon);var $masterTabPane=this.getMasterTabActivePane();data.isNewRecord&&$masterTabPane.find("form").one("ready.oc.changeMonitor",this.proxy(this.onChangeMonitorReady)),$("[data-builder-tabs]",$masterTabPane).dragScroll()},Builder.prototype.updateModifiedCounter=function(){var counters={database:{menu:"database",count:0},models:{menu:"models",count:0},permissions:{menu:"permissions",count:0},menus:{menu:"menus",count:0},imports:{menu:"imports",count:0},versions:{menu:"versions",count:0},localization:{menu:"localization",count:0},controller:{menu:"controllers",count:0},code:{menu:"code",count:0}};$("> div.tab-content > div.tab-pane[data-modified] > form",this.$masterTabs).each((function(){var entity=$(this).data("entity");counters[entity].count++})),$.each(counters,(function(type,data){$.oc.sideNav.setCounter("builder/"+data.menu,data.count)}))},Builder.prototype.getFormPluginCode=function(formElement){var code=$(formElement).closest("form").find('input[name="plugin_code"]').val();if(!code)throw new Error("Plugin code input is not found in the form.");return code},Builder.prototype.setPageTitle=function(title){$.oc.layout.setPageTitle(title.length?title+" | ":title)},Builder.prototype.getFileLists=function(){return $("[data-control=filelist]",this.$sidePanel)},Builder.prototype.dataToInspectorArray=function(data){var result=[];for(var key in data){var item={title:data[key],value:key};result.push(item)}return result},Builder.prototype.onCommand=function(ev){if("FORM"!=ev.currentTarget.tagName||"click"!=ev.type){var command=$(ev.currentTarget).data("builderCommand");this.triggerCommand(command,ev);var $target=$(ev.currentTarget);if("A"!==ev.currentTarget.tagName||"menuitem"!=$target.attr("role")||"javascript:;"!=$target.attr("href"))return ev.preventDefault(),!1}},Builder.prototype.onFormChanged=function(ev){$(".form-tabless-fields",ev.target).trigger("modified.oc.tab"),this.updateModifiedCounter()},Builder.prototype.onFormUnchanged=function(ev){$(".form-tabless-fields",ev.target).trigger("unmodified.oc.tab"),this.updateModifiedCounter()},Builder.prototype.onTabShown=function(ev){if($(ev.target).closest("[data-control=tab]").attr("id")==this.$masterTabs.attr("id")){var dataId=$(ev.target).closest("li").attr("data-tab-id"),title=$(ev.target).attr("title");title&&this.setPageTitle(title),this.getFileLists().fileList("markActive",dataId),$(window).trigger("resize")}},Builder.prototype.onAllTabsClosed=function(ev){this.setPageTitle(""),this.getFileLists().fileList("markActive",null)},Builder.prototype.onTabClosed=function(ev,tab,pane){$(pane).find("form").off("ready.oc.changeMonitor",this.proxy(this.onChangeMonitorReady)),this.updateModifiedCounter()},Builder.prototype.onChangeMonitorReady=function(ev){$(ev.target).trigger("change")},Builder.prototype.onDataRegistryItems=function(ev,data){var self=this;if("model-classes"==data.propertyDefinition.fillFrom||"model-forms"==data.propertyDefinition.fillFrom||"model-lists"==data.propertyDefinition.fillFrom||"controller-urls"==data.propertyDefinition.fillFrom||"model-columns"==data.propertyDefinition.fillFrom||"plugin-lists"==data.propertyDefinition.fillFrom||"permissions"==data.propertyDefinition.fillFrom){ev.preventDefault();var subtype=null,subtypeProperty=data.propertyDefinition.subtypeFrom;void 0!==subtypeProperty&&(subtype=data.values[subtypeProperty]),$.oc.builder.dataRegistry.get($(ev.target),this.getFormPluginCode(ev.target),data.propertyDefinition.fillFrom,subtype,(function(response){data.callback({options:self.dataToInspectorArray(response)})}))}},$(document).ready((function(){$.oc.builder.indexController=new Builder}))}(window.jQuery),function($){"use strict";void 0===$.oc.builder&&($.oc.builder={});var Base=$.oc.foundation.base,BaseProto=Base.prototype,LocalizationInput=function(input,form,options){this.input=input,this.form=form,this.options=$.extend({},LocalizationInput.DEFAULTS,options),this.disposed=!1,this.initialized=!1,this.newStringPopupMarkup=null,Base.call(this),this.init()};LocalizationInput.prototype=Object.create(BaseProto),LocalizationInput.prototype.constructor=LocalizationInput,LocalizationInput.prototype.dispose=function(){this.unregisterHandlers(),this.form=null,this.options.beforePopupShowCallback=null,this.options.afterPopupHideCallback=null,this.options=null,this.disposed=!0,this.newStringPopupMarkup=null,this.initialized&&$(this.input).autocomplete("destroy"),$(this.input).removeData("localization-input"),this.input=null,BaseProto.dispose.call(this)},LocalizationInput.prototype.init=function(){if(!this.options.plugin)throw new Error("The options.plugin value should be set in the localization input object.");var $input=$(this.input);$input.data("localization-input",this),$input.attr("data-builder-localization-input","true"),$input.attr("data-builder-localization-plugin",this.options.plugin),this.getContainer().addClass("localization-input-container"),this.registerHandlers(),this.loadDataAndBuild()},LocalizationInput.prototype.buildAddLink=function(){var $container=this.getContainer();if(!($container.find("a.localization-trigger").length>0)){var trigger=document.createElement("a");trigger.setAttribute("class","oc-icon-plus localization-trigger"),trigger.setAttribute("href","#");var pos=$container.position();$(trigger).css({top:pos.top+4,right:7}),$container.append(trigger)}},LocalizationInput.prototype.loadDataAndBuild=function(){this.showLoadingIndicator();var result=$.oc.builder.dataRegistry.get(this.form,this.options.plugin,"localization",null,this.proxy(this.dataLoaded)),self=this;result&&result.always((function(){self.hideLoadingIndicator()}))},LocalizationInput.prototype.reload=function(){$.oc.builder.dataRegistry.get(this.form,this.options.plugin,"localization",null,this.proxy(this.dataLoaded))},LocalizationInput.prototype.dataLoaded=function(data){if(!this.disposed){var autocomplete=$(this.input).data("autocomplete");if(autocomplete)autocomplete.source=this.preprocessData(data);else{this.hideLoadingIndicator();var autocompleteOptions={source:this.preprocessData(data),matchWidth:!0};autocompleteOptions=$.extend(autocompleteOptions,this.options.autocompleteOptions),$(this.input).autocomplete(autocompleteOptions),this.initialized=!0}}},LocalizationInput.prototype.preprocessData=function(data){var dataClone=$.extend({},data);for(var key in dataClone)dataClone[key]=key+" - "+dataClone[key];return dataClone},LocalizationInput.prototype.getContainer=function(){return $(this.input).closest(".autocomplete-container")},LocalizationInput.prototype.showLoadingIndicator=function(){var $container=this.getContainer();$container.addClass("loading-indicator-container size-small"),$container.loadIndicator()},LocalizationInput.prototype.hideLoadingIndicator=function(){var $container=this.getContainer();$container.loadIndicator("hide"),$container.loadIndicator("destroy"),$container.removeClass("loading-indicator-container")},LocalizationInput.prototype.loadAndShowPopup=function(){null===this.newStringPopupMarkup?($.oc.stripeLoadIndicator.show(),$(this.input).request("onLanguageLoadAddStringForm").done(this.proxy(this.popupMarkupLoaded)).always((function(){$.oc.stripeLoadIndicator.hide()}))):this.showPopup()},LocalizationInput.prototype.popupMarkupLoaded=function(responseData){this.newStringPopupMarkup=responseData.markup,this.showPopup()},LocalizationInput.prototype.showPopup=function(){var $input=$(this.input);$input.popup({content:this.newStringPopupMarkup});var $content=$input.data("oc.popup").$content,$keyInput=$content.find("#language_string_key");$.oc.builder.dataRegistry.get(this.form,this.options.plugin,"localization","sections",(function(data){$keyInput.autocomplete({source:data,matchWidth:!0})})),$content.find("form").on("submit",this.proxy(this.onSubmitPopupForm))},LocalizationInput.prototype.stringCreated=function(data){if(void 0===data.localizationData||void 0===data.registryData)throw new Error("Invalid server response.");var $input=$(this.input);$input.val(data.localizationData.key),$.oc.builder.dataRegistry.set(this.options.plugin,"localization",null,data.registryData.strings),$.oc.builder.dataRegistry.set(this.options.plugin,"localization","sections",data.registryData.sections),$input.data("oc.popup").hide(),$input.trigger("change")},LocalizationInput.prototype.onSubmitPopupForm=function(ev){var $form=$(ev.target);return $.oc.stripeLoadIndicator.show(),$form.request("onLanguageCreateString",{data:{plugin_code:this.options.plugin}}).done(this.proxy(this.stringCreated)).always((function(){$.oc.stripeLoadIndicator.hide()})),ev.preventDefault(),!1},LocalizationInput.prototype.onPopupHidden=function(ev,link,popup){$(popup).find("#language_string_key").autocomplete("destroy"),$(popup).find("form").on("submit",this.proxy(this.onSubmitPopupForm)),this.options.afterPopupHideCallback&&this.options.afterPopupHideCallback()},LocalizationInput.updatePluginInputs=function(plugin){for(var inputs=document.body.querySelectorAll('input[data-builder-localization-input][data-builder-localization-plugin="'+plugin+'"]'),i=inputs.length-1;i>=0;i--)$(inputs[i]).data("localization-input").reload()},LocalizationInput.prototype.unregisterHandlers=function(){this.input.removeEventListener("focus",this.proxy(this.onInputFocus)),this.getContainer().off("click","a.localization-trigger",this.proxy(this.onTriggerClick)),$(this.input).off("hidden.oc.popup",this.proxy(this.onPopupHidden))},LocalizationInput.prototype.registerHandlers=function(){this.input.addEventListener("focus",this.proxy(this.onInputFocus)),this.getContainer().on("click","a.localization-trigger",this.proxy(this.onTriggerClick)),$(this.input).on("hidden.oc.popup",this.proxy(this.onPopupHidden))},LocalizationInput.prototype.onInputFocus=function(){this.buildAddLink()},LocalizationInput.prototype.onTriggerClick=function(ev){return this.options.beforePopupShowCallback&&this.options.beforePopupShowCallback(),this.loadAndShowPopup(),ev.preventDefault(),!1},LocalizationInput.DEFAULTS={plugin:null,autocompleteOptions:{},beforePopupShowCallback:null,afterPopupHideCallback:null},$.oc.builder.localizationInput=LocalizationInput}(window.jQuery),function($){"use strict";var Base=$.oc.inspector.propertyEditors.string,BaseProto=Base.prototype,LocalizationEditor=function(inspector,propertyDefinition,containerCell,group){this.localizationInput=null,Base.call(this,inspector,propertyDefinition,containerCell,group)};(LocalizationEditor.prototype=Object.create(BaseProto)).constructor=Base,LocalizationEditor.prototype.dispose=function(){this.removeLocalizationInput(),BaseProto.dispose.call(this)},LocalizationEditor.prototype.build=function(){var container=document.createElement("div"),editor=document.createElement("input"),placeholder=void 0!==this.propertyDefinition.placeholder?this.propertyDefinition.placeholder:"",value=this.inspector.getPropertyValue(this.propertyDefinition.property);editor.setAttribute("type","text"),editor.setAttribute("class","string-editor"),editor.setAttribute("placeholder",placeholder),container.setAttribute("class","autocomplete-container"),void 0===value&&(value=this.propertyDefinition.default),void 0===value&&(value=""),editor.value=value,$.oc.foundation.element.addClass(this.containerCell,"text autocomplete"),container.appendChild(editor),this.containerCell.appendChild(container),this.buildLocalizationEditor()},LocalizationEditor.prototype.buildLocalizationEditor=function(){this.localizationInput=new $.oc.builder.localizationInput(this.getInput(),this.getForm(),{plugin:this.getPluginCode(),beforePopupShowCallback:this.proxy(this.onPopupShown,this),afterPopupHideCallback:this.proxy(this.onPopupHidden,this)})},LocalizationEditor.prototype.removeLocalizationInput=function(){this.localizationInput.dispose(),this.localizationInput=null},LocalizationEditor.prototype.supportsExternalParameterEditor=function(){return!1},LocalizationEditor.prototype.registerHandlers=function(){BaseProto.registerHandlers.call(this),$(this.getInput()).on("change",this.proxy(this.onInputKeyUp))},LocalizationEditor.prototype.unregisterHandlers=function(){BaseProto.unregisterHandlers.call(this),$(this.getInput()).off("change",this.proxy(this.onInputKeyUp))},LocalizationEditor.prototype.getForm=function(){var inspectableElement=this.getRootSurface().getInspectableElement();if(!inspectableElement)throw new Error("Cannot determine inspectable element in the Builder localization editor.");return $(inspectableElement).closest("form")},LocalizationEditor.prototype.getPluginCode=function(){var $input=this.getForm().find("input[name=plugin_code]");if(!$input.length)throw new Error('The input "plugin_code" should be defined in the form in order to use the localization Inspector editor.');return $input.val()},LocalizationEditor.prototype.onPopupShown=function(){this.getRootSurface().popupDisplayed()},LocalizationEditor.prototype.onPopupHidden=function(){this.getRootSurface().popupHidden()},$.oc.inspector.propertyEditors.builderLocalization=LocalizationEditor}(window.jQuery),function($){"use strict";if(void 0===$.oc.table)throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded.");if(void 0===$.oc.table.processor)throw new Error("The $.oc.table.processor namespace is not defined. Make sure that the table.processor.base.js script is loaded.");var Base=$.oc.table.processor.string,BaseProto=Base.prototype,LocalizationProcessor=function(tableObj,columnName,columnConfiguration){this.localizationInput=null,this.popupDisplayed=!1,Base.call(this,tableObj,columnName,columnConfiguration)};(LocalizationProcessor.prototype=Object.create(BaseProto)).constructor=LocalizationProcessor,LocalizationProcessor.prototype.dispose=function(){this.removeLocalizationInput(),BaseProto.dispose.call(this)},LocalizationProcessor.prototype.onUnfocus=function(){this.activeCell&&!this.popupDisplayed&&(this.removeLocalizationInput(),BaseProto.onUnfocus.call(this))},LocalizationProcessor.prototype.onBeforePopupShow=function(){this.popupDisplayed=!0},LocalizationProcessor.prototype.onAfterPopupHide=function(){this.popupDisplayed=!1},LocalizationProcessor.prototype.renderCell=function(value,cellContentContainer){BaseProto.renderCell.call(this,value,cellContentContainer)},LocalizationProcessor.prototype.buildEditor=function(cellElement,cellContentContainer,isClick){BaseProto.buildEditor.call(this,cellElement,cellContentContainer,isClick),$.oc.foundation.element.addClass(cellContentContainer,"autocomplete-container"),this.buildLocalizationEditor()},LocalizationProcessor.prototype.buildLocalizationEditor=function(){var input=this.getInput();this.localizationInput=new $.oc.builder.localizationInput(input,$(input),{plugin:this.getPluginCode(input),beforePopupShowCallback:$.proxy(this.onBeforePopupShow,this),afterPopupHideCallback:$.proxy(this.onAfterPopupHide,this),autocompleteOptions:{menu:'',bodyContainer:!0}})},LocalizationProcessor.prototype.getInput=function(){return this.activeCell?this.activeCell.querySelector(".string-input"):null},LocalizationProcessor.prototype.getPluginCode=function(input){var $input=$(input).closest("form").find("input[name=plugin_code]");if(!$input.length)throw new Error('The input "plugin_code" should be defined in the form in order to use the localization table processor.');return $input.val()},LocalizationProcessor.prototype.removeLocalizationInput=function(){this.localizationInput&&(this.localizationInput.dispose(),this.localizationInput=null)},$.oc.table.processor.builderLocalization=LocalizationProcessor}(window.jQuery),function($){"use strict";var CodeList=function(form,alias){this.$form=$(form),this.alias=alias,this.$form.on("ajaxSuccess",$.proxy(this.onAjaxSuccess,this)),this.$form.on("click","ul.list > li.directory > a",$.proxy(this.onDirectoryClick,this)),this.$form.on("click","ul.list > li.file > a",$.proxy(this.onFileClick,this)),this.$form.on("click","p.parent > a",$.proxy(this.onDirectoryClick,this)),this.$form.on("click","a[data-control=delete-asset]",$.proxy(this.onDeleteClick,this)),this.$form.on("oc.list.setActiveItem",$.proxy(this.onSetActiveItem,this)),this.setupUploader()};CodeList.prototype.onDirectoryClick=function(e){return this.gotoDirectory($(e.currentTarget).data("path"),$(e.currentTarget).parent().hasClass("parent")),!1},CodeList.prototype.gotoDirectory=function(path,gotoParent){var $container=$("div.list-container",this.$form),self=this;void 0!==gotoParent&&gotoParent?$container.addClass("goBackward"):$container.addClass("goForward"),$.oc.stripeLoadIndicator.show(),this.$form.request(this.alias+"::onOpenDirectory",{data:{path:path,d:.2},complete:function(){self.updateUi(),$container.trigger("oc.scrollbar.gotoStart")},error:function(jqXHR,textStatus,errorThrown){$container.removeClass("goForward goBackward"),alert(jqXHR.responseText.length?jqXHR.responseText:jqXHR.statusText)}}).always((function(){$.oc.stripeLoadIndicator.hide()}))},CodeList.prototype.onDeleteClick=function(e){var $el=$(e.currentTarget),self=this;return!!confirm($el.data("confirmation"))&&(this.$form.request(this.alias+"::onDeleteFiles",{success:function(data){void 0!==data.error&&"string"===$.type(data.error)&&data.error.length&&$.oc.flashMsg({text:data.error,class:"error"})},complete:function(){self.refresh()}}),!1)},CodeList.prototype.onAjaxSuccess=function(){this.updateUi()},CodeList.prototype.onUploadFail=function(file,message){413===file.xhr.status&&(message="Server rejected the file because it was too large, try increasing post_max_size"),message||(message="Error uploading file"),$.oc.alert(message),this.refresh()},CodeList.prototype.onUploadSuccess=function(file,data){"success"!==data&&$.oc.alert(data)},CodeList.prototype.onUploadComplete=function(file,data){$.oc.stripeLoadIndicator.hide(),this.refresh()},CodeList.prototype.onUploadStart=function(){$.oc.stripeLoadIndicator.show()},CodeList.prototype.onFileClick=function(event){var $li=$(event.currentTarget).parent(),e=$.Event("open.oc.list",{relatedTarget:$li.get(0),clickEvent:event});if(this.$form.trigger(e,this),e.isDefaultPrevented())return!1},CodeList.prototype.onSetActiveItem=function(event,dataId){$("ul li.file",this.$form).removeClass("active"),dataId&&$('ul li.file[data-id="'+dataId+'"]',this.$form).addClass("active")},CodeList.prototype.updateUi=function(){$("button[data-control=asset-tools]",self.$form).trigger("oc.triggerOn.update")},CodeList.prototype.refresh=function(){var self=this;this.$form.request(this.alias+"::onRefresh",{complete:function(){self.updateUi()}})},CodeList.prototype.setupUploader=function(){var self=this,$link=$('[data-control="upload-assets"]',this.$form),uploaderOptions={method:"POST",url:window.location,paramName:"file_data",previewsContainer:$("
    ").get(0),clickable:$link.get(0),timeout:0,headers:{}},token=$('meta[name="csrf-token"]').attr("content");token&&(uploaderOptions.headers["X-CSRF-TOKEN"]=token);var dropzone=new Dropzone($("
    ").get(0),uploaderOptions);dropzone.on("error",$.proxy(self.onUploadFail,self)),dropzone.on("success",$.proxy(self.onUploadSuccess,self)),dropzone.on("complete",$.proxy(self.onUploadComplete,self)),dropzone.on("sending",(function(file,xhr,formData){$.each(self.$form.serializeArray(),(function(index,field){formData.append(field.name,field.value)})),xhr.setRequestHeader("X-OCTOBER-REQUEST-HANDLER",self.alias+"::onUpload"),self.onUploadStart()}))},$(document).on("render",(function(){var $container=$("#code-list-container");!0!==$container.data("oc.codeListAttached")&&($container.data("oc.codeListAttached",!0),new CodeList($container.closest("form"),$container.data("alias")))}))}(window.jQuery); diff --git a/plugins/rainlab/builder/assets/js/builder.codelist.js b/plugins/rainlab/builder/assets/js/builder.codelist.js new file mode 100644 index 0000000..43064e7 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.codelist.js @@ -0,0 +1,197 @@ +/* + * Code List + */ ++function ($) { "use strict"; + + var CodeList = function (form, alias) { + this.$form = $(form); + this.alias = alias; + + this.$form.on('ajaxSuccess', $.proxy(this.onAjaxSuccess, this)); + this.$form.on('click', 'ul.list > li.directory > a', $.proxy(this.onDirectoryClick, this)); + this.$form.on('click', 'ul.list > li.file > a', $.proxy(this.onFileClick, this)); + this.$form.on('click', 'p.parent > a', $.proxy(this.onDirectoryClick, this)); + this.$form.on('click', 'a[data-control=delete-asset]', $.proxy(this.onDeleteClick, this)); + this.$form.on('oc.list.setActiveItem', $.proxy(this.onSetActiveItem, this)); + + this.setupUploader(); + } + + // Event handlers + // ================= + + CodeList.prototype.onDirectoryClick = function(e) { + this.gotoDirectory( + $(e.currentTarget).data('path'), + $(e.currentTarget).parent().hasClass('parent') + ); + + return false; + } + + CodeList.prototype.gotoDirectory = function(path, gotoParent) { + var $container = $('div.list-container', this.$form), + self = this; + + if (gotoParent !== undefined && gotoParent) { + $container.addClass('goBackward'); + } + else { + $container.addClass('goForward'); + } + + $.oc.stripeLoadIndicator.show(); + this.$form.request(this.alias+'::onOpenDirectory', { + data: { + path: path, + d: 0.2 + }, + complete: function() { + self.updateUi() + $container.trigger('oc.scrollbar.gotoStart') + }, + error: function(jqXHR, textStatus, errorThrown) { + $container.removeClass('goForward goBackward') + alert(jqXHR.responseText.length ? jqXHR.responseText : jqXHR.statusText) + } + }).always(function(){ + $.oc.stripeLoadIndicator.hide() + }) + } + + CodeList.prototype.onDeleteClick = function(e) { + var $el = $(e.currentTarget), + self = this; + + if (!confirm($el.data('confirmation'))) { + return false; + } + + this.$form.request(this.alias+'::onDeleteFiles', { + success: function(data) { + if (data.error !== undefined && $.type(data.error) === 'string' && data.error.length) { + $.oc.flashMsg({text: data.error, 'class': 'error'}); + } + }, + complete: function() { + self.refresh(); + } + }); + + return false; + } + + CodeList.prototype.onAjaxSuccess = function() { + this.updateUi(); + } + + CodeList.prototype.onUploadFail = function(file, message) { + if (file.xhr.status === 413) { + message = 'Server rejected the file because it was too large, try increasing post_max_size'; + } + if (!message) { + message = 'Error uploading file'; + } + + $.oc.alert(message); + + this.refresh(); + } + + CodeList.prototype.onUploadSuccess = function(file, data) { + if (data !== 'success') { + $.oc.alert(data); + } + } + + CodeList.prototype.onUploadComplete = function(file, data) { + $.oc.stripeLoadIndicator.hide(); + this.refresh(); + } + + CodeList.prototype.onUploadStart = function() { + $.oc.stripeLoadIndicator.show(); + } + + CodeList.prototype.onFileClick = function(event) { + var $link = $(event.currentTarget), + $li = $link.parent(); + + var e = $.Event('open.oc.list', {relatedTarget: $li.get(0), clickEvent: event}); + this.$form.trigger(e, this); + + if (e.isDefaultPrevented()) { + return false; + } + } + + CodeList.prototype.onSetActiveItem = function(event, dataId) { + $('ul li.file', this.$form).removeClass('active'); + if (dataId) { + $('ul li.file[data-id="'+dataId+'"]', this.$form).addClass('active'); + } + } + + // Service functions + // ================= + + CodeList.prototype.updateUi = function() { + $('button[data-control=asset-tools]', self.$form).trigger('oc.triggerOn.update'); + } + + CodeList.prototype.refresh = function() { + var self = this; + + this.$form.request(this.alias+'::onRefresh', { + complete: function() { + self.updateUi(); + } + }); + } + + CodeList.prototype.setupUploader = function() { + var self = this, + $link = $('[data-control="upload-assets"]', this.$form), + uploaderOptions = { + method: 'POST', + url: window.location, + paramName: 'file_data', + previewsContainer: $('
    ').get(0), + clickable: $link.get(0), + timeout: 0, + headers: {} + }; + + // Add CSRF token to headers + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) { + uploaderOptions.headers['X-CSRF-TOKEN'] = token; + } + + var dropzone = new Dropzone($('
    ').get(0), uploaderOptions); + dropzone.on('error', $.proxy(self.onUploadFail, self)); + dropzone.on('success', $.proxy(self.onUploadSuccess, self)); + dropzone.on('complete', $.proxy(self.onUploadComplete, self)); + dropzone.on('sending', function(file, xhr, formData) { + $.each(self.$form.serializeArray(), function (index, field) { + formData.append(field.name, field.value); + }); + xhr.setRequestHeader('X-OCTOBER-REQUEST-HANDLER', self.alias + '::onUpload'); + self.onUploadStart(); + }); + } + + $(document).on('render', function() { + var $container = $('#code-list-container'); + if ($container.data('oc.codeListAttached') === true) { + return; + } + + $container.data('oc.codeListAttached', true); + new CodeList( + $container.closest('form'), + $container.data('alias') + ); + }); + +}(window.jQuery); diff --git a/plugins/rainlab/builder/assets/js/builder.dataregistry.js b/plugins/rainlab/builder/assets/js/builder.dataregistry.js new file mode 100644 index 0000000..bb39ba4 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.dataregistry.js @@ -0,0 +1,170 @@ +/* + * Builder client-side plugin data registry + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var DataRegistry = function() { + this.data = {} + this.requestCache = {} + this.callbackCache = {} + + Base.call(this) + } + + /* + * Example: + * $.oc.builder.dataRegistry.set('rainlab.blog', 'model.forms', 'Categories', formsArray) + * $.oc.builder.dataRegistry.set('rainlab.blog', 'localization', null, stringsArray) // The registry contains only default language + */ + DataRegistry.prototype.set = function(plugin, type, subtype, data, params) { + this.storeData(plugin, type, subtype, data) + + if (type == 'localization' && !subtype) { + this.localizationUpdated(plugin, params) + } + } + + /* + * Example: + * $.oc.builder.dataRegistry.get($form, 'rainlab.blog', 'model.forms', 'Categories', function(data){ ... }) + */ + DataRegistry.prototype.get = function($formElement, plugin, type, subtype, callback) { + if (this.data[plugin] === undefined + || this.data[plugin][type] === undefined + || this.data[plugin][type][subtype] === undefined + || this.isCacheObsolete(this.data[plugin][type][subtype].timestamp)) { + + return this.loadDataFromServer($formElement, plugin, type, subtype, callback) + } + + callback(this.data[plugin][type][subtype].data) + } + + // INTERNAL METHODS + // ============================ + + DataRegistry.prototype.makeCacheKey = function(plugin, type, subtype) { + var key = plugin + '-' + type + + if (subtype) { + key += '-' + subtype + } + + return key + } + + DataRegistry.prototype.isCacheObsolete = function(timestamp) { + return (Date.now() - timestamp) > 60000*5 // 5 minutes cache TTL + } + + DataRegistry.prototype.loadDataFromServer = function($formElement, plugin, type, subtype, callback) { + var self = this, + cacheKey = this.makeCacheKey(plugin, type, subtype) + + if (this.requestCache[cacheKey] === undefined) { + this.requestCache[cacheKey] = $formElement.request('onPluginDataRegistryGetData', { + data: { + registry_plugin_code: plugin, + registry_data_type: type, + registry_data_subtype: subtype + } + }).done( + function(data) { + if (data.registryData === undefined) { + throw new Error('Invalid data registry response.') + } + + self.storeData(plugin, type, subtype, data.registryData) + self.applyCallbacks(cacheKey, data.registryData) + + self.requestCache[cacheKey] = undefined + } + ) + } + + this.addCallbackToQueue(callback, cacheKey) + + return this.requestCache[cacheKey] + } + + DataRegistry.prototype.addCallbackToQueue = function(callback, key) { + if (this.callbackCache[key] === undefined) { + this.callbackCache[key] = [] + } + + this.callbackCache[key].push(callback) + } + + DataRegistry.prototype.applyCallbacks = function(key, registryData) { + if (this.callbackCache[key] === undefined) { + return + } + + for (var i=this.callbackCache[key].length-1; i>=0; i--) { + this.callbackCache[key][i](registryData); + } + + delete this.callbackCache[key] + } + + DataRegistry.prototype.storeData = function(plugin, type, subtype, data) { + if (this.data[plugin] === undefined) { + this.data[plugin] = {} + } + + if (this.data[plugin][type] === undefined) { + this.data[plugin][type] = {} + } + + var dataItem = { + timestamp: Date.now(), + data: data + } + + this.data[plugin][type][subtype] = dataItem + } + + DataRegistry.prototype.clearCache = function(plugin, type) { + if (this.data[plugin] === undefined) { + return + } + + if (this.data[plugin][type] === undefined) { + return + } + + this.data[plugin][type] = undefined + } + + // LOCALIZATION-SPECIFIC METHODS + // ============================ + + DataRegistry.prototype.getLocalizationString = function($formElement, plugin, key, callback) { + this.get($formElement, plugin, 'localization', null, function(data){ + if (data[key] !== undefined) { + callback(data[key]) + return + } + + callback(key) + }) + } + + DataRegistry.prototype.localizationUpdated = function(plugin, params) { + $.oc.builder.localizationInput.updatePluginInputs(plugin) + + if (params === undefined || !params.suppressLanguageEditorUpdate) { + $.oc.builder.indexController.entityControllers.localization.languageUpdated(plugin) + } + + $.oc.builder.indexController.entityControllers.localization.updateOnScreenStrings(plugin) + } + + $.oc.builder.dataRegistry = new DataRegistry() +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.base.js b/plugins/rainlab/builder/assets/js/builder.index.entity.base.js new file mode 100644 index 0000000..1caac0b --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.base.js @@ -0,0 +1,100 @@ +/* + * Base class for Builder Index entity controllers + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var EntityBase = function(typeName, indexController) { + if (typeName === undefined) { + throw new Error('The Builder entity type name should be set in the base constructor call.') + } + + if (indexController === undefined) { + throw new Error('The Builder index controller should be set when creating an entity controller.') + } + + // The type name is used mostly for referring to + // DOM objects. + this.typeName = typeName + + this.indexController = indexController + + Base.call(this) + } + + EntityBase.prototype = Object.create(BaseProto) + EntityBase.prototype.constructor = EntityBase + + EntityBase.prototype.registerHandlers = function() { + + } + + EntityBase.prototype.invokeCommand = function(command, ev) { + if (/^cmd[a-zA-Z0-9]+$/.test(command)) { + if (this[command] !== undefined) { + this[command].apply(this, [ev]) + } + else { + throw new Error('Unknown command: '+command) + } + } + else { + throw new Error('Invalid command: '+command) + } + } + + EntityBase.prototype.newTabId = function() { + return this.typeName + Math.random() + } + + EntityBase.prototype.makeTabId = function(objectName) { + return this.typeName + '-' + objectName + } + + EntityBase.prototype.getMasterTabsActivePane = function() { + return this.indexController.getMasterTabActivePane() + } + + EntityBase.prototype.getMasterTabsObject = function() { + return this.indexController.masterTabsObj + } + + EntityBase.prototype.getSelectedPlugin = function() { + var activeItem = $('#PluginList-pluginList-plugin-list > ul > li.active') + + return activeItem.data('id') + } + + EntityBase.prototype.getIndexController = function() { + return this.indexController + } + + EntityBase.prototype.updateMasterTabIdAndTitle = function($tabPane, responseData) { + var tabsObject = this.getMasterTabsObject() + + tabsObject.updateIdentifier($tabPane, responseData.tabId) + tabsObject.updateTitle($tabPane, responseData.tabTitle) + } + + EntityBase.prototype.unhideFormDeleteButton = function($tabPane) { + $('[data-control=delete-button]', $tabPane).removeClass('hide oc-hide') + } + + EntityBase.prototype.forceCloseTab = function($tabPane) { + $tabPane.trigger('close.oc.tab', [{force: true}]) + } + + EntityBase.prototype.unmodifyTab = function($tabPane) { + this.indexController.unchangeTab($tabPane) + } + + $.oc.builder.entityControllers.base = EntityBase; +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.code.js b/plugins/rainlab/builder/assets/js/builder.index.entity.code.js new file mode 100644 index 0000000..4544233 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.code.js @@ -0,0 +1,124 @@ +/* + * Builder Index controller Code entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) { + $.oc.builder = {}; + } + + if ($.oc.builder.entityControllers === undefined) { + $.oc.builder.entityControllers = {}; + } + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype; + + var Code = function(indexController) { + Base.call(this, 'code', indexController); + } + + Code.prototype = Object.create(BaseProto); + Code.prototype.constructor = Code; + + // PUBLIC METHODS + // ============================ + + Code.prototype.registerHandlers = function() { + } + + Code.prototype.cmdCreateCode = function(ev) { + this.indexController.openOrLoadMasterTab($(ev.target), 'onCodeOpen', this.newTabId()); + } + + Code.prototype.cmdOpenCode = function(ev) { + var path = $(ev.currentTarget).data('path'), + pluginCode = $(ev.currentTarget).data('pluginCode'); + + var result = this.indexController.openOrLoadMasterTab($(ev.target), 'onCodeOpen', this.makeTabId(pluginCode+'-'+path), { + fileName: path + }); + + if (result !== false) { + result.done(this.proxy(this.updateFormEditorMode, this)); + } + } + + Code.prototype.cmdSaveCode = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form'), + $inspectorContainer = $form.find('.inspector-container') + + if (!$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)) { + return + } + + $target.request('onCodeSave').done( + this.proxy(this.saveCodeDone) + ) + } + + Code.prototype.saveCodeDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data'); + } + + var $masterTabPane = this.getMasterTabsActivePane(); + + this.getIndexController().unchangeTab($masterTabPane); + + this.updateFormEditorMode(); + } + + Code.prototype.getCodeList = function() { + return $('#layout-side-panel form[data-content-id=code] .control-codelist') + } + + Code.prototype.updateFormEditorMode = function() { + var $masterTabPane = this.getMasterTabsActivePane(); + + var modes = { + css: "css", + htm: "html", + html: "html", + js: "javascript", + json: "json", + less: "less", + md: "markdown", + sass: "sass", + scss: "scss", + txt: "plain_text", + yaml: "yaml", + xml: "xml", + php: "php" + }; + + var fileName = $('input[name=fileName]', $masterTabPane).val(), + parts = fileName.split('.'), + extension = 'txt', + mode = 'plain_text', + editor = $('[data-control=codeeditor]', $masterTabPane); + + if (parts.length >= 2) { + extension = parts.pop().toLowerCase(); + } + + if (modes[extension] !== undefined) { + mode = modes[extension]; + } + + var setEditorMode = function() { + window.setTimeout(function() { + editor.data('oc.codeEditor').editor.getSession().setMode({path: 'ace/mode/'+mode}) + }, 200); + }; + + setEditorMode(); + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.code = Code; + +}(window.jQuery); diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.controller.js b/plugins/rainlab/builder/assets/js/builder.index.entity.controller.js new file mode 100644 index 0000000..5e21a28 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.controller.js @@ -0,0 +1,109 @@ +/* + * Builder Index controller Controller entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Controller = function(indexController) { + Base.call(this, 'controller', indexController); + } + + Controller.prototype = Object.create(BaseProto); + Controller.prototype.constructor = Controller; + + // PUBLIC METHODS + // ============================ + + Controller.prototype.cmdCreateController = function(ev) { + var $form = $(ev.currentTarget), + self = this, + pluginCode = $form.data('pluginCode'), + behaviorsSelected = $form.find('input[name="behaviors[]"]:checked').length, + promise = null; + + // If behaviors were selected, open a new tab after the + // controller is saved. Otherwise just update the controller + // list. + if (behaviorsSelected) { + promise = this.indexController.openOrLoadMasterTab( + $form, + 'onControllerCreate', + this.makeTabId(pluginCode+'-new-controller'), + {} + ); + } + else { + promise = $form.request('onControllerCreate'); + } + + promise.done(function(data){ + $form.trigger('close.oc.popup') + self.updateDataRegistry(data) + }).always($.oc.builder.indexController.hideStripeIndicatorProxy); + } + + Controller.prototype.cmdOpenController = function(ev) { + var controller = $(ev.currentTarget).data('id'), + pluginCode = $(ev.currentTarget).data('pluginCode'); + + this.indexController.openOrLoadMasterTab($(ev.target), 'onControllerOpen', this.makeTabId(pluginCode+'-'+controller), { + controller: controller + }); + } + + Controller.prototype.cmdSaveController = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form'), + $inspectorContainer = $form.find('.inspector-container'); + + if (!$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)) { + return; + } + + $target.request('onControllerSave').done( + this.proxy(this.saveControllerDone) + ); + } + + // EVENT HANDLERS + // ============================ + + // INTERNAL METHODS + // ============================ + + Controller.prototype.saveControllerDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + } + + Controller.prototype.updateDataRegistry = function(data) { + if (data.builderResponseData.registryData !== undefined) { + var registryData = data.builderResponseData.registryData + + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'controller-urls', null, registryData.urls) + } + } + + Controller.prototype.getControllerList = function() { + return $('#layout-side-panel form[data-content-id=controller] [data-control=filelist]') + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.controller = Controller; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.databasetable.js b/plugins/rainlab/builder/assets/js/builder.index.entity.databasetable.js new file mode 100644 index 0000000..474707e --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.databasetable.js @@ -0,0 +1,363 @@ +/* + * Builder Index controller Database Table entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var DatabaseTable = function(indexController) { + Base.call(this, 'databaseTable', indexController) + } + + DatabaseTable.prototype = Object.create(BaseProto) + DatabaseTable.prototype.constructor = DatabaseTable + + // PUBLIC METHODS + // ============================ + + DatabaseTable.prototype.cmdCreateTable = function(ev) { + var result = this.indexController.openOrLoadMasterTab($(ev.target), 'onDatabaseTableCreateOrOpen', this.newTabId()) + + if (result !== false) { + result.done(this.proxy(this.onTableLoaded, this)) + } + } + + DatabaseTable.prototype.cmdOpenTable = function(ev) { + var table = $(ev.currentTarget).data('id'), + result = this.indexController.openOrLoadMasterTab($(ev.target), 'onDatabaseTableCreateOrOpen', this.makeTabId(table), { + table_name: table + }) + + if (result !== false) { + result.done(this.proxy(this.onTableLoaded, this)) + } + } + + DatabaseTable.prototype.cmdSaveTable = function(ev) { + var $target = $(ev.currentTarget) + + // The process of saving a database table: + // - validate client-side + // - validate columns on the server + // - display a popup asking to enter the migration text + // - generate the migration on the server and execute it + // - drop the form modified flag + + if (!this.validateTable($target)) { + return + } + + var data = { + 'columns': this.getTableData($target) + } + + $target.popup({ + extraData: data, + handler: 'onDatabaseTableValidateAndShowPopup' + }) + } + + DatabaseTable.prototype.cmdSaveMigration = function(ev) { + var $target = $(ev.currentTarget) + + $.oc.stripeLoadIndicator.show() + $target.request('onDatabaseTableMigrationApply').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.saveMigrationDone) + ) + } + + DatabaseTable.prototype.cmdDeleteTable = function(ev) { + var $target = $(ev.currentTarget) + $.oc.confirm($target.data('confirm'), this.proxy(this.deleteConfirmed)) + } + + DatabaseTable.prototype.cmdUnModifyForm = function() { + var $masterTabPane = this.getMasterTabsActivePane() + this.unmodifyTab($masterTabPane) + } + + DatabaseTable.prototype.cmdAddIdColumn = function(ev) { + var $target = $(ev.currentTarget), + added = this.addIdColumn($target) + + if (!added) { + alert($target.closest('form').attr('data-lang-id-exists')) + } + } + + DatabaseTable.prototype.cmdAddTimestamps = function(ev) { + var $target = $(ev.currentTarget), + added = this.addTimeStampColumns($target, ['created_at', 'updated_at']) + + if (!added) { + alert($target.closest('form').attr('data-lang-timestamps-exist')) + } + } + + DatabaseTable.prototype.cmdAddSoftDelete = function(ev) { + var $target = $(ev.currentTarget), + added = this.addTimeStampColumns($target, ['deleted_at']) + + if (!added) { + alert($target.closest('form').attr('data-lang-soft-deleting-exist')) + } + } + + // EVENT HANDLERS + // ============================ + + DatabaseTable.prototype.onTableCellChanged = function(ev, column, value, rowIndex) { + var $target = $(ev.target) + + if ($target.data('alias') != 'columns') { + return + } + + if ($target.closest('form').data('entity') != 'database') { + return + } + + // Some migration-related rules are enforced here: + // + // 1. Checking Autoincrement checkbox automatically checks the Unsigned checkbox (this corresponds to the + // logic internally implemented in Laravel schema builder) and PK + // 2. Unchecking Unsigned unchecks Autoincrement + // 3. Checking the PK column unchecks Nullable + // 4. Checking Nullable unchecks PK + // 6. Unchecking the PK unchecks Autoincrement + + var updatedRow = {} + + if (column == 'auto_increment' && value) { + updatedRow.unsigned = 1 + updatedRow.primary_key = 1 + } + + if (column == 'unsigned' && !value) { + updatedRow.auto_increment = 0 + } + + if (column == 'primary_key' && value) { + updatedRow.allow_null = 0 + } + + if (column == 'allow_null' && value) { + updatedRow.primary_key = 0 + } + + if (column == 'primary_key' && !value) { + updatedRow.auto_increment = 0 + } + + $target.table('setRowValues', rowIndex, updatedRow) + } + + DatabaseTable.prototype.onTableLoaded = function() { + $(document).trigger('render') + + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form'), + $toolbar = $masterTabPane.find('div[data-control=table] div.toolbar'), + $addIdButton = $(''), + $addTimestampsButton = $(''), + $addSoftDeleteButton = $('') + + $addIdButton.text($form.attr('data-lang-add-id')); + $toolbar.append($addIdButton) + + $addTimestampsButton.text($form.attr('data-lang-add-timestamps')); + $toolbar.append($addTimestampsButton) + + $addSoftDeleteButton.text($form.attr('data-lang-add-soft-delete')); + $toolbar.append($addSoftDeleteButton) + } + + // INTERNAL METHODS + // ============================ + + DatabaseTable.prototype.registerHandlers = function() { + this.indexController.$masterTabs.on('oc.tableCellChanged', this.proxy(this.onTableCellChanged)); + } + + DatabaseTable.prototype.validateTable = function($target) { + var tableObj = this.getTableControlObject($target); + + tableObj.unfocusTable(); + return tableObj.validate(); + } + + DatabaseTable.prototype.getTableData = function($target) { + var tableObj = this.getTableControlObject($target); + + return tableObj.dataSource.getAllData(); + } + + DatabaseTable.prototype.getTableControlObject = function($target) { + var $form = $target.closest('form'), + $table = $form.find('[data-control=table]'), + tableObj = $table.data('oc.table') + + if (!tableObj) { + throw new Error('Table object is not found on the database table tab') + } + + return tableObj + } + + DatabaseTable.prototype.saveMigrationDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + $('#builderTableMigrationPopup').trigger('close.oc.popup') + + var $masterTabPane = this.getMasterTabsActivePane(), + tabsObject = this.getMasterTabsObject() + + if (data.builderResponseData.operation != 'delete') { + $masterTabPane.find('input[name=table_name]').val(data.builderResponseData.builderObjectName) + this.updateMasterTabIdAndTitle($masterTabPane, data.builderResponseData) + this.unhideFormDeleteButton($masterTabPane) + + this.getTableList().fileList('markActive', data.builderResponseData.tabId) + this.getIndexController().unchangeTab($masterTabPane) + + this.updateTable(data.builderResponseData) + } + else { + this.forceCloseTab($masterTabPane) + } + + $.oc.builder.dataRegistry.clearCache(data.builderResponseData.pluginCode, 'model-columns') + } + + DatabaseTable.prototype.getTableList = function() { + return $('#layout-side-panel form[data-content-id=database] [data-control=filelist]') + } + + DatabaseTable.prototype.deleteConfirmed = function() { + var $masterTabPane = this.getMasterTabsActivePane() + + $masterTabPane.find('form').popup({ + handler: 'onDatabaseTableShowDeletePopup' + }) + } + + DatabaseTable.prototype.getColumnNames = function($target) { + var tableObj = this.getTableControlObject($target) + + tableObj.unfocusTable() + + var data = this.getTableData($target), + result = [] + + for (var index in data) { + if (data[index].name !== undefined) { + result.push($.trim(data[index].name)) + } + } + + return result + } + + DatabaseTable.prototype.addIdColumn = function($target) { + var existingColumns = this.getColumnNames($target), + added = false + + if (existingColumns.indexOf('id') === -1) { + var tableObj = this.getTableControlObject($target), + currentData = this.getTableData($target), + rowData = { + name: 'id', + type: 'integer', + unsigned: true, + auto_increment: true, + primary_key: true, + } + + if (currentData.length - 1 || currentData[0].name || currentData[0].type || currentData[0].length || currentData[0].unsigned || currentData[0].nullable || currentData[0].auto_increment || currentData[0].primary_key || currentData[0].default) { + tableObj.addRecord('bottom', true) + } + + tableObj.setRowValues(currentData.length - 1, rowData) + + // Forces the table to apply values + // from the data source + tableObj.addRecord('bottom', false) + tableObj.deleteRecord() + + added = true + } + + if (added) { + $target.trigger('change') + } + + return added + } + + DatabaseTable.prototype.addTimeStampColumns = function($target, columns) + { + var existingColumns = this.getColumnNames($target), + added = false + + for (var index in columns) { + var column = columns[index] + + if (existingColumns.indexOf(column) === -1) { + this.addTimeStampColumn($target, column) + added = true + } + } + + if (added) { + $target.trigger('change') + } + + return added + } + + DatabaseTable.prototype.addTimeStampColumn = function($target, column) { + var tableObj = this.getTableControlObject($target), + currentData = this.getTableData($target), + rowData = { + name: column, + type: 'timestamp', + 'default': null, + allow_null: true // Simplifies the case when a timestamp is added to a table with data + } + + tableObj.addRecord('bottom', true) + tableObj.setRowValues(currentData.length - 1, rowData) + + // Forces the table to apply values + // from the data source + tableObj.addRecord('bottom', false) + tableObj.deleteRecord() + } + + DatabaseTable.prototype.updateTable = function(data) { + var tabsObject = this.getMasterTabsObject(), + tabs = $('#builder-master-tabs').data('oc.tab'), + tab = tabs.findByIdentifier(data.tabId) + + tabsObject.updateTab(tab, data.tableName, data.tab) + this.onTableLoaded() + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.databaseTable = DatabaseTable; + +}(window.jQuery); diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.imports.js b/plugins/rainlab/builder/assets/js/builder.index.entity.imports.js new file mode 100644 index 0000000..b09bdd3 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.imports.js @@ -0,0 +1,112 @@ +/* + * Builder Index controller Imports entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) { + $.oc.builder = {}; + } + + if ($.oc.builder.entityControllers === undefined) { + $.oc.builder.entityControllers = {}; + } + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype; + + var Imports = function(indexController) { + Base.call(this, 'imports', indexController); + } + + Imports.prototype = Object.create(BaseProto); + Imports.prototype.constructor = Imports; + + // PUBLIC METHODS + // ============================ + + Imports.prototype.cmdOpenImports = function(ev) { + var currentPlugin = this.getSelectedPlugin(); + + if (!currentPlugin) { + alert('Please select a plugin first'); + return; + } + + this.indexController.openOrLoadMasterTab($(ev.target), 'onImportsOpen', this.makeTabId(currentPlugin)); + } + + Imports.prototype.cmdConfirmImports = function(ev) { + var $target = $(ev.currentTarget); + + $target.popup({ + handler: 'onImportsShowConfirmPopup' + }); + } + + Imports.prototype.cmdSaveImports = function(ev) { + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form'), + $popup = $(ev.currentTarget).closest('.control-popup'); + + $popup.removeClass('show').popup('setLoading', true); + + $form.request('onImportsSave', { + data: oc.serializeJSON($popup.get(0)) + }) + .done((data) => { + $popup.trigger('close.oc.popup'); + this.saveImportsDone(data); + }) + .fail(() => { + $popup.addClass('show').popup('setLoading', false).popup('setShake'); + }); + } + + Imports.prototype.cmdMigrateDatabase = function(ev) { + var $target = $(ev.currentTarget); + $target.request('onMigrateDatabase'); + } + + Imports.prototype.cmdAddBlueprintItem = function(ev) { + // $.oc.builder.blueprintbuilder.controller.addBlueprintItem(ev) + } + + Imports.prototype.cmdRemoveBlueprintItem = function(ev) { + // $.oc.builder.blueprintbuilder.controller.removeBlueprint(ev) + } + + // INTERNAL METHODS + // ============================ + + Imports.prototype.saveImportsDone = function(data) { + this.hideInspector(); + $('#blueprintList').html(''); + + if ($.oc.mainMenu && data && data.mainMenu && data.mainMenuLeft) { + $.oc.mainMenu.reload(data.mainMenu, data.mainMenuLeft); + } + + var $masterTabPane = this.getMasterTabsActivePane(); + this.getIndexController().unchangeTab($masterTabPane); + } + + Imports.prototype.hideInspector = function() { + var $container = $('.blueprint-container.inspector-open:first'); + + if ($container.length) { + var $inspectorContainer = this.findInspectorContainer($container); + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)); + } + } + + Imports.prototype.findInspectorContainer = function($element) { + var $containerRoot = $element.closest('[data-inspector-container]') + return $containerRoot.find('.inspector-container') + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.imports = Imports; + +}(window.jQuery); diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.localization.js b/plugins/rainlab/builder/assets/js/builder.index.entity.localization.js new file mode 100644 index 0000000..40f92fe --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.localization.js @@ -0,0 +1,282 @@ +/* + * Builder Index controller Localization entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Localization = function(indexController) { + Base.call(this, 'localization', indexController) + } + + Localization.prototype = Object.create(BaseProto) + Localization.prototype.constructor = Localization + + // PUBLIC METHODS + // ============================ + + Localization.prototype.cmdCreateLanguage = function(ev) { + this.indexController.openOrLoadMasterTab($(ev.target), 'onLanguageCreateOrOpen', this.newTabId()) + } + + Localization.prototype.cmdOpenLanguage = function(ev) { + var language = $(ev.currentTarget).data('id'), + pluginCode = $(ev.currentTarget).data('pluginCode') + + this.indexController.openOrLoadMasterTab($(ev.target), 'onLanguageCreateOrOpen', this.makeTabId(pluginCode+'-'+language), { + original_language: language + }) + } + + Localization.prototype.cmdSaveLanguage = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form') + + $target.request('onLanguageSave').done( + this.proxy(this.saveLanguageDone) + ) + } + + Localization.prototype.cmdDeleteLanguage = function(ev) { + var $target = $(ev.currentTarget) + $.oc.confirm($target.data('confirm'), this.proxy(this.deleteConfirmed)) + } + + Localization.prototype.cmdCopyMissingStrings = function(ev) { + var $form = $(ev.currentTarget), + language = $form.find('select[name=language]').val(), + $masterTabPane = this.getMasterTabsActivePane() + + $form.trigger('close.oc.popup') + + $.oc.stripeLoadIndicator.show() + $masterTabPane.find('form').request('onLanguageCopyStringsFrom', { + data: { + copy_from: language + } + }).always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.copyStringsFromDone) + ) + } + + // EVENT HANDLERS + // ============================ + + // INTERNAL BUILDER API + // ============================ + + Localization.prototype.languageUpdated = function(plugin) { + var languageForm = this.findDefaultLanguageForm(plugin) + + if (!languageForm) { + return + } + + var $languageForm = $(languageForm) + + if (!$languageForm.hasClass('oc-data-changed')) { + this.updateLanguageFromServer($languageForm) + } + else { + // If there are changes - merge language from server + // in the background. As this operation is not 100% + // reliable, it could be a good idea to display a + // warning when the user navigates to the tab. + + this.mergeLanguageFromServer($languageForm) + } + } + + Localization.prototype.updateOnScreenStrings = function(plugin) { + var stringElements = document.body.querySelectorAll('span[data-localization-key][data-plugin="'+plugin+'"]') + + $.oc.builder.dataRegistry.get($('#builder-plugin-selector-panel form'), plugin, 'localization', null, function(data){ + for (var i=stringElements.length-1; i>=0; i--) { + var stringElement = stringElements[i], + stringKey = stringElement.getAttribute('data-localization-key') + + if (data[stringKey] !== undefined) { + stringElement.textContent = data[stringKey] + } + else { + stringElement.textContent = stringKey + } + } + }) + } + + // INTERNAL METHODS + // ============================ + + Localization.prototype.saveLanguageDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + $masterTabPane.find('input[name=original_language]').val(data.builderResponseData.language) + this.updateMasterTabIdAndTitle($masterTabPane, data.builderResponseData) + this.unhideFormDeleteButton($masterTabPane) + + this.getLanguageList().fileList('markActive', data.builderResponseData.tabId) + this.getIndexController().unchangeTab($masterTabPane) + + if (data.builderResponseData.registryData !== undefined) { + var registryData = data.builderResponseData.registryData + + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'localization', null, registryData.strings, {suppressLanguageEditorUpdate: true}) + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'localization', 'sections', registryData.sections) + } + } + + Localization.prototype.getLanguageList = function() { + return $('#layout-side-panel form[data-content-id=localization] [data-control=filelist]') + } + + Localization.prototype.getCodeEditor = function($tab) { + return $tab.find('div[data-field-name=strings] div[data-control=codeeditor]').data('oc.codeEditor').editor + } + + Localization.prototype.deleteConfirmed = function() { + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form') + + $.oc.stripeLoadIndicator.show() + $form.request('onLanguageDelete').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.deleteDone) + ) + } + + Localization.prototype.deleteDone = function() { + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + this.forceCloseTab($masterTabPane) + } + + Localization.prototype.copyStringsFromDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var responseData = data.builderResponseData, + $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form'), + codeEditor = this.getCodeEditor($masterTabPane), + newStringMessage = $form.data('newStringMessage'), + mismatchMessage = $form.data('structureMismatch') + + codeEditor.getSession().setValue(responseData.strings) + + var annotations = [] + for (var i=responseData.updatedLines.length-1; i>=0; i--) { + var line = responseData.updatedLines[i] + + annotations.push({ + row: line, + column: 0, + text: newStringMessage, + type: 'warning' + }) + } + + codeEditor.getSession().setAnnotations(annotations) + + if (responseData.mismatch) { + $.oc.alert(mismatchMessage) + } + } + + Localization.prototype.findDefaultLanguageForm = function(plugin) { + var forms = document.body.querySelectorAll('form[data-entity=localization]') + + for (var i=forms.length-1; i>=0; i--) { + var form = forms[i], + pluginInput = form.querySelector('input[name=plugin_code]'), + languageInput = form.querySelector('input[name=original_language]') + + if (!pluginInput || pluginInput.value != plugin) { + continue + } + + if (!languageInput) { + continue + } + + if (form.getAttribute('data-default-language') == languageInput.value) { + return form + } + } + + return null + } + + Localization.prototype.updateLanguageFromServer = function($languageForm) { + var self = this + + $languageForm.request('onLanguageGetStrings').done(function(data) { + self.updateLanguageFromServerDone($languageForm, data) + }) + } + + Localization.prototype.updateLanguageFromServerDone = function($languageForm, data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var responseData = data.builderResponseData, + $tabPane = $languageForm.closest('.tab-pane'), + codeEditor = this.getCodeEditor($tabPane) + + if (!responseData.strings) { + return + } + + codeEditor.getSession().setValue(responseData.strings) + this.unmodifyTab($tabPane) + } + + Localization.prototype.mergeLanguageFromServer = function($languageForm) { + var language = $languageForm.find('input[name=original_language]').val(), + self = this + + $languageForm.request('onLanguageCopyStringsFrom', { + data: { + copy_from: language + } + }).done(function(data) { + self.mergeLanguageFromServerDone($languageForm, data) + }) + } + + Localization.prototype.mergeLanguageFromServerDone = function($languageForm, data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var responseData = data.builderResponseData, + $tabPane = $languageForm.closest('.tab-pane'), + codeEditor = this.getCodeEditor($tabPane) + + codeEditor.getSession().setValue(responseData.strings) + codeEditor.getSession().setAnnotations([]) + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.localization = Localization; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.menus.js b/plugins/rainlab/builder/assets/js/builder.index.entity.menus.js new file mode 100644 index 0000000..18753a3 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.menus.js @@ -0,0 +1,90 @@ +/* + * Builder Index controller Menus entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Menus = function(indexController) { + Base.call(this, 'menus', indexController) + } + + Menus.prototype = Object.create(BaseProto) + Menus.prototype.constructor = Menus + + // PUBLIC METHODS + // ============================ + + Menus.prototype.cmdOpenMenus = function(ev) { + var currentPlugin = this.getSelectedPlugin() + + if (!currentPlugin) { + alert('Please select a plugin first') + return + } + + this.indexController.openOrLoadMasterTab($(ev.target), 'onMenusOpen', this.makeTabId(currentPlugin)) + } + + Menus.prototype.cmdSaveMenus = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form'), + $inspectorContainer = $form.find('.inspector-container') + + if (!$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)) { + return + } + + var menus = $.oc.builder.menubuilder.controller.getJson($form.get(0)) + + $target.request('onMenusSave', { + data: { + menus: menus + } + }).done( + this.proxy(this.saveMenusDone) + ) + } + + Menus.prototype.cmdAddMainMenuItem = function(ev) { + $.oc.builder.menubuilder.controller.addMainMenuItem(ev) + } + + Menus.prototype.cmdAddSideMenuItem = function(ev) { + $.oc.builder.menubuilder.controller.addSideMenuItem(ev) + } + + Menus.prototype.cmdDeleteMenuItem = function(ev) { + $.oc.builder.menubuilder.controller.deleteMenuItem(ev) + } + + // INTERNAL METHODS + // ============================ + + Menus.prototype.saveMenusDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data'); + } + + var $masterTabPane = this.getMasterTabsActivePane(); + + if ($.oc.mainMenu && data.mainMenu && data.mainMenuLeft) { + $.oc.mainMenu.reload(data.mainMenu, data.mainMenuLeft); + } + + this.getIndexController().unchangeTab($masterTabPane); + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.menus = Menus; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.model.js b/plugins/rainlab/builder/assets/js/builder.index.entity.model.js new file mode 100644 index 0000000..10a1cd9 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.model.js @@ -0,0 +1,72 @@ +/* + * Builder Index controller Model entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Model = function(indexController) { + Base.call(this, 'model', indexController) + } + + Model.prototype = Object.create(BaseProto) + Model.prototype.constructor = Model + + // PUBLIC METHODS + // ============================ + + Model.prototype.cmdCreateModel = function(ev) { + var $target = $(ev.currentTarget) + + $target.one('shown.oc.popup', this.proxy(this.onModelPopupShown)) + + $target.popup({ + handler: 'onModelLoadPopup' + }) + } + + Model.prototype.cmdApplyModelSettings = function(ev) { + var $form = $(ev.currentTarget), + self = this + + $.oc.stripeLoadIndicator.show() + $form.request('onModelSave').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done(function(data){ + $form.trigger('close.oc.popup') + + self.applyModelSettingsDone(data) + }) + } + + // EVENT HANDLERS + // ============================ + + Model.prototype.onModelPopupShown = function(ev, button, popup) { + $(popup).find('input[name=className]').focus() + } + + // INTERNAL METHODS + // ============================ + + Model.prototype.applyModelSettingsDone = function(data) { + if (data.builderResponseData.registryData !== undefined) { + var registryData = data.builderResponseData.registryData + + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'model-classes', null, registryData.models) + } + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.model = Model; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.modelform.js b/plugins/rainlab/builder/assets/js/builder.index.entity.modelform.js new file mode 100644 index 0000000..af03406 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.modelform.js @@ -0,0 +1,201 @@ +/* + * Builder Index controller Model Form entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var ModelForm = function(indexController) { + Base.call(this, 'modelForm', indexController) + } + + ModelForm.prototype = Object.create(BaseProto) + ModelForm.prototype.constructor = ModelForm + + // PUBLIC METHODS + // ============================ + + ModelForm.prototype.cmdCreateForm = function(ev) { + var $link = $(ev.currentTarget), + data = { + model_class: $link.data('modelClass') + } + + this.indexController.openOrLoadMasterTab($link, 'onModelFormCreateOrOpen', this.newTabId(), data) + } + + ModelForm.prototype.cmdSaveForm = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form'), + $rootContainer = $('[data-root-control-wrapper] > [data-control-container]', $form), + $inspectorContainer = $form.find('.inspector-container'), + controls = $.oc.builder.formbuilder.domToPropertyJson.convert($rootContainer.get(0)) + + if (!$.oc.inspector.manager.applyValuesFromContainer($inspectorContainer)) { + return + } + + if (controls === false) { + $.oc.flashMsg({ + 'text': $.oc.builder.formbuilder.domToPropertyJson.getLastError(), + 'class': 'error', + 'interval': 5 + }) + + return + } + + var data = { + controls: controls + } + + $target.request('onModelFormSave', { + data: data + }).done( + this.proxy(this.saveFormDone) + ) + } + + ModelForm.prototype.cmdAddDatabaseFields = function (ev) { + var $target = $(ev.currentTarget) + + // Always use the first placeholder to add controls + var $placeholder = this.getMasterTabsActivePane().find('.builder-control-list .control.oc-placeholder:first')[0] + + // Filter all fields from the DataTable that have the "add" checkbox checked. + var fields = $target.find('.control-table').data('oc.table').dataSource.data.filter(function (column) { + return column.add + }).reverse() + + // Hide the popup and initialize the load indicator. + $target.closest('.control-popup').data('oc.popup').hide() + $.oc.stripeLoadIndicator.show() + + // When a control is added, an AJAX request is made which returns the widget's markup. + // We need to wait for each request to finish before we can add another field, since the + // addControlToPlaceholder requires a proper reflow of the whole form layout before + // a new field can be added. This addField helper function makes sure that all + // Promises are run in sequence to achieve this. + function addField(field) { + return function () { + var defer = $.Deferred() + $.oc.builder.formbuilder.controller.addControlToPlaceholder( + $placeholder, + field.type, + field.label ? field.label : field.column, + false, + field.column + ).always(function () { + defer.resolve() + }) + return defer.promise() + }; + } + + /// Add all fields in sequence. + var allFields = $.when({}) + $.each(fields, function (index, field) { + allFields = allFields.then(addField(field)) + }); + + // Once everything is done, hide the load indicator. + $.when(allFields).always($.oc.builder.indexController.hideStripeIndicatorProxy) + } + + ModelForm.prototype.cmdOpenForm = function(ev) { + var form = $(ev.currentTarget).data('form'), + model = $(ev.currentTarget).data('modelClass') + + this.indexController.openOrLoadMasterTab($(ev.target), 'onModelFormCreateOrOpen', this.makeTabId(model+'-'+form), { + file_name: form, + model_class: model + }) + } + + ModelForm.prototype.cmdDeleteForm = function(ev) { + var $target = $(ev.currentTarget) + $.oc.confirm($target.data('confirm'), this.proxy(this.deleteConfirmed)) + } + + ModelForm.prototype.cmdAddControl = function(ev) { + $.oc.builder.formbuilder.controlPalette.addControl(ev) + } + + ModelForm.prototype.cmdUndockControlPalette = function(ev) { + $.oc.builder.formbuilder.controlPalette.undockFromContainer(ev) + } + + ModelForm.prototype.cmdDockControlPalette = function(ev) { + $.oc.builder.formbuilder.controlPalette.dockToContainer(ev) + } + + ModelForm.prototype.cmdCloseControlPalette = function(ev) { + $.oc.builder.formbuilder.controlPalette.closeInContainer(ev) + } + + // INTERNAL METHODS + // ============================ + + ModelForm.prototype.saveFormDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + $masterTabPane.find('input[name=file_name]').val(data.builderResponseData.builderObjectName) + this.updateMasterTabIdAndTitle($masterTabPane, data.builderResponseData) + this.unhideFormDeleteButton($masterTabPane) + + this.getModelList().fileList('markActive', data.builderResponseData.tabId) + this.getIndexController().unchangeTab($masterTabPane) + + this.updateDataRegistry(data) + } + + ModelForm.prototype.updateDataRegistry = function(data) { + if (data.builderResponseData.registryData !== undefined) { + var registryData = data.builderResponseData.registryData + + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'model-forms', registryData.modelClass, registryData.forms) + } + } + + ModelForm.prototype.deleteConfirmed = function() { + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form') + + $.oc.stripeLoadIndicator.show() + $form.request('onModelFormDelete').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.deleteDone) + ) + } + + ModelForm.prototype.deleteDone = function(data) { + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + this.forceCloseTab($masterTabPane) + + this.updateDataRegistry(data) + } + + ModelForm.prototype.getModelList = function() { + return $('#layout-side-panel form[data-content-id=models] [data-control=filelist]') + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.modelForm = ModelForm; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.modellist.js b/plugins/rainlab/builder/assets/js/builder.index.entity.modellist.js new file mode 100644 index 0000000..7f8be68 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.modellist.js @@ -0,0 +1,304 @@ +/* + * Builder Index controller Model List entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var ModelList = function(indexController) { + this.cachedModelFieldsPromises = {} + + Base.call(this, 'modelList', indexController) + } + + ModelList.prototype = Object.create(BaseProto) + ModelList.prototype.constructor = ModelList + + ModelList.prototype.registerHandlers = function() { + $(document).on('autocompleteitems.oc.table', 'form[data-sub-entity="model-list"] [data-control=table]', this.proxy(this.onAutocompleteItems)) + } + + // PUBLIC METHODS + // ============================ + + ModelList.prototype.cmdCreateList = function(ev) { + var $link = $(ev.currentTarget), + data = { + model_class: $link.data('modelClass') + } + + var result = this.indexController.openOrLoadMasterTab($link, 'onModelListCreateOrOpen', this.newTabId(), data) + + if (result !== false) { + result.done(this.proxy(this.onListLoaded, this)) + } + } + + ModelList.prototype.cmdSaveList = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form') + + if (!this.validateTable($target)) { + return + } + + $target.request('onModelListSave', { + data: { + columns: this.getTableData($target) + } + }).done( + this.proxy(this.saveListDone) + ) + } + + ModelList.prototype.cmdOpenList = function(ev) { + var list = $(ev.currentTarget).data('list'), + model = $(ev.currentTarget).data('modelClass') + + var result = this.indexController.openOrLoadMasterTab($(ev.target), 'onModelListCreateOrOpen', this.makeTabId(model+'-'+list), { + file_name: list, + model_class: model + }) + + if (result !== false) { + result.done(this.proxy(this.onListLoaded, this)) + } + } + + ModelList.prototype.cmdDeleteList = function(ev) { + var $target = $(ev.currentTarget) + $.oc.confirm($target.data('confirm'), this.proxy(this.deleteConfirmed)) + } + + ModelList.prototype.cmdAddDatabaseColumns = function(ev) { + var $target = $(ev.currentTarget) + + $.oc.stripeLoadIndicator.show() + $target.request('onModelListLoadDatabaseColumns').done( + this.proxy(this.databaseColumnsLoaded) + ).always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ) + } + + // INTERNAL METHODS + // ============================ + + ModelList.prototype.saveListDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + $masterTabPane.find('input[name=file_name]').val(data.builderResponseData.builderObjectName) + this.updateMasterTabIdAndTitle($masterTabPane, data.builderResponseData) + this.unhideFormDeleteButton($masterTabPane) + + this.getModelList().fileList('markActive', data.builderResponseData.tabId) + this.getIndexController().unchangeTab($masterTabPane) + + this.updateDataRegistry(data) + } + + ModelList.prototype.deleteConfirmed = function() { + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form') + + $.oc.stripeLoadIndicator.show() + $form.request('onModelListDelete').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.deleteDone) + ) + } + + ModelList.prototype.deleteDone = function(data) { + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + this.forceCloseTab($masterTabPane) + + this.updateDataRegistry(data) + } + + ModelList.prototype.getTableControlObject = function($target) { + var $form = $target.closest('form'), + $table = $form.find('[data-control=table]'), + tableObj = $table.data('oc.table') + + if (!tableObj) { + throw new Error('Table object is not found on the model list tab') + } + + return tableObj + } + + ModelList.prototype.getModelList = function() { + return $('#layout-side-panel form[data-content-id=models] [data-control=filelist]') + } + + ModelList.prototype.validateTable = function($target) { + var tableObj = this.getTableControlObject($target) + + tableObj.unfocusTable() + return tableObj.validate() + } + + ModelList.prototype.getTableData = function($target) { + var tableObj = this.getTableControlObject($target) + + return tableObj.dataSource.getAllData() + } + + ModelList.prototype.loadModelFields = function(table, callback) { + var $form = $(table).closest('form'), + modelClass = $form.find('input[name=model_class]').val(), + cachedFields = $form.data('oc.model-field-cache') + + if (cachedFields !== undefined) { + callback(cachedFields) + + return + } + + if (this.cachedModelFieldsPromises[modelClass] === undefined) { + this.cachedModelFieldsPromises[modelClass] = $form.request('onModelFormGetModelFields', { + data: { + 'as_plain_list': 1 + } + }) + } + + if (callback === undefined) { + return + } + + this.cachedModelFieldsPromises[modelClass].done(function(data){ + $form.data('oc.model-field-cache', data.responseData.options) + + callback(data.responseData.options) + }) + } + + ModelList.prototype.updateDataRegistry = function(data) { + if (data.builderResponseData.registryData !== undefined) { + var registryData = data.builderResponseData.registryData + + $.oc.builder.dataRegistry.set(registryData.pluginCode, 'model-lists', registryData.modelClass, registryData.lists) + + $.oc.builder.dataRegistry.clearCache(registryData.pluginCode, 'plugin-lists') + } + } + + ModelList.prototype.databaseColumnsLoaded = function(data) { + if (!$.isArray(data.responseData.columns)) { + alert('Invalid server response') + } + + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form'), + existingColumns = this.getColumnNames($form), + columnsAdded = false + + for (var i in data.responseData.columns) { + var column = data.responseData.columns[i], + type = this.mapType(column.type) + + if ($.inArray(column.name, existingColumns) !== -1) { + continue + } + + this.addColumn($form, column.name, type) + columnsAdded = true + } + + if (!columnsAdded) { + alert($form.attr('data-lang-all-database-columns-exist')) + } + else { + $form.trigger('change') + } + } + + ModelList.prototype.mapType = function(type) { + switch (type) { + case 'integer' : return 'number' + case 'timestamp' : return 'datetime' + default: return 'text' + } + } + + ModelList.prototype.addColumn = function($target, column, type) { + var tableObj = this.getTableControlObject($target), + currentData = this.getTableData($target), + rowData = { + field: column, + label: column, + type: type + } + + tableObj.addRecord('bottom', true) + tableObj.setRowValues(currentData.length-1, rowData) + + // Forces the table to apply values + // from the data source + tableObj.addRecord('bottom', false) + tableObj.deleteRecord() + } + + ModelList.prototype.getColumnNames = function($target) { + var tableObj = this.getTableControlObject($target) + + tableObj.unfocusTable() + + var data = this.getTableData($target), + result = [] + + for (var index in data) { + if (data[index].field !== undefined) { + result.push($.trim(data[index].field)) + } + } + + return result + } + + // EVENT HANDLERS + // ============================ + + ModelList.prototype.onAutocompleteItems = function(ev, data) { + if (data.columnConfiguration.fillFrom === 'model-fields') { + ev.preventDefault() + + this.loadModelFields(ev.target, data.callback) + + return false + } + } + + ModelList.prototype.onListLoaded = function() { + $(document).trigger('render') + + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form'), + $toolbar = $masterTabPane.find('div[data-control=table] div.toolbar'), + $button = $('') + + $button.text($form.attr('data-lang-add-database-columns')); + $toolbar.append($button) + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.modelList = ModelList; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.permission.js b/plugins/rainlab/builder/assets/js/builder.index.entity.permission.js new file mode 100644 index 0000000..a74732b --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.permission.js @@ -0,0 +1,122 @@ +/* + * Builder Index controller Permission entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Permission = function(indexController) { + Base.call(this, 'permissions', indexController) + } + + Permission.prototype = Object.create(BaseProto) + Permission.prototype.constructor = Permission + + Permission.prototype.registerHandlers = function() { + this.indexController.$masterTabs.on('oc.tableNewRow', this.proxy(this.onTableRowCreated)) + } + + // PUBLIC METHODS + // ============================ + + Permission.prototype.cmdOpenPermissions = function(ev) { + var currentPlugin = this.getSelectedPlugin() + + if (!currentPlugin) { + alert('Please select a plugin first') + return + } + + this.indexController.openOrLoadMasterTab($(ev.target), 'onPermissionsOpen', this.makeTabId(currentPlugin)) + } + + Permission.prototype.cmdSavePermissions = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form') + + if (!this.validateTable($target)) { + return + } + + $target.request('onPermissionsSave', { + data: { + permissions: this.getTableData($target) + } + }).done( + this.proxy(this.savePermissionsDone) + ) + } + + // INTERNAL METHODS + // ============================ + + Permission.prototype.getTableControlObject = function($target) { + var $form = $target.closest('form'), + $table = $form.find('[data-control=table]'), + tableObj = $table.data('oc.table') + + if (!tableObj) { + throw new Error('Table object is not found on permissions tab') + } + + return tableObj + } + + Permission.prototype.validateTable = function($target) { + var tableObj = this.getTableControlObject($target) + + tableObj.unfocusTable() + return tableObj.validate() + } + + Permission.prototype.getTableData = function($target) { + var tableObj = this.getTableControlObject($target) + + return tableObj.dataSource.getAllData() + } + + Permission.prototype.savePermissionsDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + $.oc.builder.dataRegistry.clearCache(data.builderResponseData.pluginCode, 'permissions') + } + + // EVENT HANDLERS + // ============================ + + Permission.prototype.onTableRowCreated = function(ev, recordData) { + var $target = $(ev.target) + + if ($target.data('alias') != 'permissions') { + return + } + + var $form = $target.closest('form') + + if ($form.data('entity') != 'permissions') { + return + } + + var pluginCode = $form.find('input[name=plugin_code]').val() + + recordData.permission = pluginCode.toLowerCase() + '.'; + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.permission = Permission; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.plugin.js b/plugins/rainlab/builder/assets/js/builder.index.entity.plugin.js new file mode 100644 index 0000000..7468e3e --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.plugin.js @@ -0,0 +1,116 @@ +/* + * Builder Index controller Plugin entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Plugin = function(indexController) { + Base.call(this, 'plugin', indexController) + + this.popupZIndex = 5050 // This popup should be above the flyout overlay, which z-index is 5000 + } + + Plugin.prototype = Object.create(BaseProto) + Plugin.prototype.constructor = Plugin + + // PUBLIC METHODS + // ============================ + + Plugin.prototype.cmdMakePluginActive = function(ev) { + var $target = $(ev.currentTarget), + selectedPluginCode = $target.data('pluginCode') + + this.makePluginActive(selectedPluginCode) + } + + Plugin.prototype.cmdCreatePlugin = function(ev) { + var $target = $(ev.currentTarget) + + $target.one('shown.oc.popup', this.proxy(this.onPluginPopupShown)) + + $target.popup({ + handler: 'onPluginLoadPopup', + zIndex: this.popupZIndex + }) + } + + Plugin.prototype.cmdApplyPluginSettings = function(ev) { + var $form = $(ev.currentTarget), + self = this + + $.oc.stripeLoadIndicator.show() + $form.request('onPluginSave').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done(function(data){ + $form.trigger('close.oc.popup') + + self.applyPluginSettingsDone(data) + }) + } + + Plugin.prototype.cmdEditPluginSettings = function(ev) { + var $target = $(ev.currentTarget) + + $target.one('shown.oc.popup', this.proxy(this.onPluginPopupShown)) + + $target.popup({ + handler: 'onPluginLoadPopup', + zIndex: this.popupZIndex, + extraData: { + pluginCode: $target.data('pluginCode') + } + }) + } + + // EVENT HANDLERS + // ============================ + + Plugin.prototype.onPluginPopupShown = function(ev, button, popup) { + $(popup).find('input[name=name]').focus() + } + + // INTERNAL METHODS + // ============================ + + Plugin.prototype.applyPluginSettingsDone = function(data) { + if (data.responseData !== undefined && data.responseData.isNewPlugin !== undefined) { + this.makePluginActive(data.responseData.pluginCode, true) + } + } + + Plugin.prototype.makePluginActive = function(pluginCode, updatePluginList) { + var $form = $('#builder-plugin-selector-panel form').first() + + $.oc.stripeLoadIndicator.show() + $form.request('onPluginSetActive', { + data: { + pluginCode: pluginCode, + updatePluginList: (updatePluginList ? 1 : 0) + } + }).always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.makePluginActiveDone) + ) + } + + Plugin.prototype.makePluginActiveDone = function(data) { + var pluginCode = data.responseData.pluginCode + + $('#builder-plugin-selector-panel [data-control=filelist]').fileList('markActive', pluginCode) + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.plugin = Plugin; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.entity.version.js b/plugins/rainlab/builder/assets/js/builder.index.entity.version.js new file mode 100644 index 0000000..0b33133 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.entity.version.js @@ -0,0 +1,272 @@ +/* + * Builder Index controller Version entity controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.entityControllers === undefined) + $.oc.builder.entityControllers = {} + + var Base = $.oc.builder.entityControllers.base, + BaseProto = Base.prototype + + var Version = function(indexController) { + Base.call(this, 'version', indexController) + + this.hiddenHints = {} + } + + Version.prototype = Object.create(BaseProto) + Version.prototype.constructor = Version + + // PUBLIC METHODS + // ============================ + + Version.prototype.cmdCreateVersion = function(ev) { + var $link = $(ev.currentTarget), + versionType = $link.data('versionType') + + this.indexController.openOrLoadMasterTab($link, 'onVersionCreateOrOpen', this.newTabId(), { + version_type: versionType + }) + } + + Version.prototype.cmdSaveVersion = function(ev) { + var $target = $(ev.currentTarget), + $form = $target.closest('form') + + $target.request('onVersionSave').done( + this.proxy(this.saveVersionDone) + ) + } + + Version.prototype.cmdOpenVersion = function(ev) { + var versionNumber = $(ev.currentTarget).data('id'), + pluginCode = $(ev.currentTarget).data('pluginCode') + + this.indexController.openOrLoadMasterTab($(ev.target), 'onVersionCreateOrOpen', this.makeTabId(pluginCode+'-'+versionNumber), { + original_version: versionNumber + }) + } + + Version.prototype.cmdDeleteVersion = function(ev) { + var $target = $(ev.currentTarget) + $.oc.confirm($target.data('confirm'), this.proxy(this.deleteConfirmed)) + } + + Version.prototype.cmdApplyVersion = function(ev) { + var $target = $(ev.currentTarget), + $pane = $target.closest('div.tab-pane'), + self = this + + this.showHintPopup($pane, 'builder-version-apply', function(){ + $target.request('onVersionApply').done( + self.proxy(self.applyVersionDone) + ) + }) + } + + Version.prototype.cmdRollbackVersion = function(ev) { + var $target = $(ev.currentTarget), + $pane = $target.closest('div.tab-pane'), + self = this + + + this.showHintPopup($pane, 'builder-version-rollback', function(){ + $target.request('onVersionRollback').done( + self.proxy(self.rollbackVersionDone) + ) + }) + } + + // INTERNAL METHODS + // ============================ + + Version.prototype.saveVersionDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + this.updateUiAfterSave($masterTabPane, data) + + if (!data.builderResponseData.isApplied) { + this.showSavedNotAppliedHint($masterTabPane) + } + } + + Version.prototype.showSavedNotAppliedHint = function($masterTabPane) { + this.showHintPopup($masterTabPane, 'builder-version-save-unapplied') + } + + Version.prototype.showHintPopup = function($masterTabPane, code, callback) { + if (this.getDontShowHintAgain(code, $masterTabPane)) { + if (callback) { + callback.apply(this) + } + + return + } + + $masterTabPane.one('hide.oc.popup', this.proxy(this.onHintPopupHide)) + + if (callback) { + $masterTabPane.one('shown.oc.popup', function(ev, $element, $modal) { + $modal.find('form').one('submit', function(ev) { + callback.apply(this) + ev.preventDefault() + + $(ev.target).trigger('close.oc.popup') + + return false + }) + }) + } + + $masterTabPane.popup({ + content: this.getPopupContent($masterTabPane, code) + }) + } + + Version.prototype.onHintPopupHide = function(ev, $element, $modal) { + var cbValue = $modal.find('input[type=checkbox][name=dont_show_again]').is(':checked'), + code = $modal.find('input[type=hidden][name=hint_code]').val() + + $modal.find('form').off('submit') + + if (!cbValue) { + return + } + + var $form = this.getMasterTabsActivePane().find('form[data-entity="versions"]') + + $form.request('onHideBackendHint', { + data: { + name: code + } + }) + + this.setDontShowHintAgain(code) + } + + Version.prototype.setDontShowHintAgain = function(code) { + this.hiddenHints[code] = true + } + + Version.prototype.getDontShowHintAgain = function(code, $pane) { + if (this.hiddenHints[code] !== undefined) { + return this.hiddenHints[code] + } + + return $pane.find('input[type=hidden][data-hint-hidden="'+code+'"]').val() == "true" + } + + Version.prototype.getPopupContent = function($pane, code) { + var template = $pane.find('script[data-version-hint-template="'+code+'"]') + + if (template.length === 0) { + throw new Error('Version popup template not found: '+code) + } + + return template.html() + } + + Version.prototype.updateUiAfterSave = function($masterTabPane, data) { + $masterTabPane.find('input[name=original_version]').val(data.builderResponseData.savedVersion) + this.updateMasterTabIdAndTitle($masterTabPane, data.builderResponseData) + this.unhideFormDeleteButton($masterTabPane) + + this.getVersionList().fileList('markActive', data.builderResponseData.tabId) + this.getIndexController().unchangeTab($masterTabPane) + } + + Version.prototype.deleteConfirmed = function() { + var $masterTabPane = this.getMasterTabsActivePane(), + $form = $masterTabPane.find('form') + + $.oc.stripeLoadIndicator.show() + $form.request('onVersionDelete').always( + $.oc.builder.indexController.hideStripeIndicatorProxy + ).done( + this.proxy(this.deleteDone) + ) + } + + Version.prototype.deleteDone = function() { + var $masterTabPane = this.getMasterTabsActivePane() + + this.getIndexController().unchangeTab($masterTabPane) + this.forceCloseTab($masterTabPane) + } + + Version.prototype.applyVersionDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + this.updateUiAfterSave($masterTabPane, data) + + this.updateVersionsButtons() + } + + Version.prototype.rollbackVersionDone = function(data) { + if (data['builderResponseData'] === undefined) { + throw new Error('Invalid response data') + } + + var $masterTabPane = this.getMasterTabsActivePane() + + this.updateUiAfterSave($masterTabPane, data) + + this.updateVersionsButtons() + } + + Version.prototype.getVersionList = function() { + return $('#layout-side-panel form[data-content-id=version] [data-control=filelist]') + } + + Version.prototype.updateVersionsButtons = function() { + var tabsObject = this.getMasterTabsObject(), + $tabs = tabsObject.$tabsContainer.find('> li'), + $versionList = this.getVersionList() + + // Find all version tabs and update Apply and Rollback buttons + // basing on the version statuses in the version list. + for (var i=$tabs.length-1; i>=0; i--) { + var $tab = $($tabs[i]), + tabId = $tab.data('tabId') + + if (!tabId || String(tabId).length == 0) { + continue + } + + var $versionLi = $versionList.find('li[data-id="'+tabId+'"]') + if (!$versionLi.length) { + continue + } + + var isApplied = $versionLi.data('applied'), + $pane = tabsObject.findPaneFromTab($tab) + + if (isApplied) { + $pane.find('[data-builder-command="version:cmdApplyVersion"]').addClass('hide oc-hide') + $pane.find('[data-builder-command="version:cmdRollbackVersion"]').removeClass('hide oc-hide') + } + else { + $pane.find('[data-builder-command="version:cmdApplyVersion"]').removeClass('hide oc-hide') + $pane.find('[data-builder-command="version:cmdRollbackVersion"]').addClass('hide oc-hide') + } + } + + } + + // REGISTRATION + // ============================ + + $.oc.builder.entityControllers.version = Version; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.index.js b/plugins/rainlab/builder/assets/js/builder.index.js new file mode 100644 index 0000000..d3cd3fc --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.index.js @@ -0,0 +1,299 @@ +/* + * Builder client-side Index page controller + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) { + $.oc.builder = {}; + } + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype; + + var Builder = function() { + Base.call(this); + + this.$masterTabs = null; + this.masterTabsObj = null; + this.hideStripeIndicatorProxy = null; + this.entityControllers = {}; + + this.init(); + } + + Builder.prototype = Object.create(BaseProto) + Builder.prototype.constructor = Builder + + Builder.prototype.dispose = function() { + // We don't really care about disposing the + // index controller, as it's used only once + // and always exists during the page life. + BaseProto.dispose.call(this) + } + + // PUBLIC METHODS + // ============================ + + Builder.prototype.openOrLoadMasterTab = function($form, serverHandlerName, tabId, data) { + if (this.masterTabsObj.goTo(tabId)) { + return false; + } + + var requestData = data === undefined ? {} : data; + + $.oc.stripeLoadIndicator.show(); + + var promise = $form + .request(serverHandlerName, { + data: requestData + }) + .done(this.proxy(this.addMasterTab)) + .always(this.hideStripeIndicatorProxy); + + return promise; + } + + Builder.prototype.getMasterTabActivePane = function() { + return this.$masterTabs.find('> .tab-content > .tab-pane.active'); + } + + Builder.prototype.unchangeTab = function($pane) { + $pane.find('form').trigger('unchange.oc.changeMonitor'); + } + + Builder.prototype.triggerCommand = function(command, ev) { + var commandParts = command.split(':') + + if (commandParts.length === 2) { + var entity = commandParts[0], + commandToExecute = commandParts[1] + + if (this.entityControllers[entity] === undefined) { + throw new Error('Unknown entity type: ' + entity) + } + + this.entityControllers[entity].invokeCommand(commandToExecute, ev) + } + } + + // INTERNAL METHODS + // ============================ + + Builder.prototype.init = function() { + this.$masterTabs = $('#builder-master-tabs') + this.$sidePanel = $('#builder-side-panel') + + this.masterTabsObj = this.$masterTabs.data('oc.tab') + this.hideStripeIndicatorProxy = this.proxy(this.hideStripeIndicator) + new $.oc.tabFormExpandControls(this.$masterTabs) + + this.createEntityControllers() + this.registerHandlers() + } + + Builder.prototype.createEntityControllers = function() { + for (var controller in $.oc.builder.entityControllers) { + if (controller == "base") { + continue; + } + + this.entityControllers[controller] = new $.oc.builder.entityControllers[controller](this); + } + } + + Builder.prototype.registerHandlers = function() { + $(document).on('click', '[data-builder-command]', this.proxy(this.onCommand)); + $(document).on('submit', '[data-builder-command]', this.proxy(this.onCommand)); + + this.$masterTabs.on('changed.oc.changeMonitor', this.proxy(this.onFormChanged)); + this.$masterTabs.on('unchanged.oc.changeMonitor', this.proxy(this.onFormUnchanged)); + this.$masterTabs.on('shown.bs.tab', this.proxy(this.onTabShown)); + this.$masterTabs.on('afterAllClosed.oc.tab', this.proxy(this.onAllTabsClosed)); + this.$masterTabs.on('closed.oc.tab', this.proxy(this.onTabClosed)); + this.$masterTabs.on('autocompleteitems.oc.inspector', this.proxy(this.onDataRegistryItems)); + this.$masterTabs.on('dropdownoptions.oc.inspector', this.proxy(this.onDataRegistryItems)); + + for (var controller in this.entityControllers) { + if (this.entityControllers[controller].registerHandlers !== undefined) { + this.entityControllers[controller].registerHandlers(); + } + } + } + + Builder.prototype.hideStripeIndicator = function() { + $.oc.stripeLoadIndicator.hide(); + } + + Builder.prototype.addMasterTab = function(data) { + this.masterTabsObj.addTab(data.tabTitle, data.tab, data.tabId, 'oc-' + data.tabIcon) + + var $masterTabPane = this.getMasterTabActivePane(); + + if (data.isNewRecord) { + $masterTabPane.find('form').one('ready.oc.changeMonitor', this.proxy(this.onChangeMonitorReady)); + } + + $('[data-builder-tabs]', $masterTabPane).dragScroll(); + } + + Builder.prototype.updateModifiedCounter = function() { + var counters = { + database: { menu: 'database', count: 0 }, + models: { menu: 'models', count: 0 }, + permissions: { menu: 'permissions', count: 0 }, + menus: { menu: 'menus', count: 0 }, + imports: { menu: 'imports', count: 0 }, + versions: { menu: 'versions', count: 0 }, + localization: { menu: 'localization', count: 0 }, + controller: { menu: 'controllers', count: 0 }, + code: { menu: 'code', count: 0 } + } + + $('> div.tab-content > div.tab-pane[data-modified] > form', this.$masterTabs).each(function(){ + var entity = $(this).data('entity') + counters[entity].count++ + }) + + $.each(counters, function(type, data){ + $.oc.sideNav.setCounter('builder/' + data.menu, data.count); + }) + } + + Builder.prototype.getFormPluginCode = function(formElement) { + var $form = $(formElement).closest('form'), + $input = $form.find('input[name="plugin_code"]'), + code = $input.val() + + if (!code) { + throw new Error('Plugin code input is not found in the form.') + } + + return code + } + + Builder.prototype.setPageTitle = function(title) { + $.oc.layout.setPageTitle(title.length ? (title + ' | ') : title) + } + + Builder.prototype.getFileLists = function() { + return $('[data-control=filelist]', this.$sidePanel) + } + + Builder.prototype.dataToInspectorArray = function(data) { + var result = [] + + for (var key in data) { + var item = { + title: data[key], + value: key + } + result.push(item) + } + + return result + } + + // EVENT HANDLERS + // ============================ + + Builder.prototype.onCommand = function(ev) { + if (ev.currentTarget.tagName == 'FORM' && ev.type == 'click') { + // The form elements could have data-builder-command attribute, + // but for them we only handle the submit event and ignore clicks. + return; + } + + var command = $(ev.currentTarget).data('builderCommand'); + this.triggerCommand(command, ev); + + // Prevent default for everything except drop-down menu items + // + var $target = $(ev.currentTarget); + if (ev.currentTarget.tagName === 'A' && $target.attr('role') == 'menuitem' && $target.attr('href') == 'javascript:;') { + return; + } + + ev.preventDefault(); + return false; + } + + Builder.prototype.onFormChanged = function(ev) { + $('.form-tabless-fields', ev.target).trigger('modified.oc.tab') + this.updateModifiedCounter() + } + + Builder.prototype.onFormUnchanged = function(ev) { + $('.form-tabless-fields', ev.target).trigger('unmodified.oc.tab') + this.updateModifiedCounter() + } + + Builder.prototype.onTabShown = function(ev) { + var $tabControl = $(ev.target).closest('[data-control=tab]') + + if ($tabControl.attr('id') != this.$masterTabs.attr('id')) { + return + } + + var dataId = $(ev.target).closest('li').attr('data-tab-id'), + title = $(ev.target).attr('title') + + if (title) { + this.setPageTitle(title) + } + + this.getFileLists().fileList('markActive', dataId) + + $(window).trigger('resize') + } + + Builder.prototype.onAllTabsClosed = function(ev) { + this.setPageTitle('') + this.getFileLists().fileList('markActive', null) + } + + Builder.prototype.onTabClosed = function(ev, tab, pane) { + $(pane).find('form').off('ready.oc.changeMonitor', this.proxy(this.onChangeMonitorReady)) + + this.updateModifiedCounter() + } + + Builder.prototype.onChangeMonitorReady = function(ev) { + $(ev.target).trigger('change') + } + + Builder.prototype.onDataRegistryItems = function(ev, data) { + var self = this + + if (data.propertyDefinition.fillFrom == 'model-classes' || + data.propertyDefinition.fillFrom == 'model-forms' || + data.propertyDefinition.fillFrom == 'model-lists' || + data.propertyDefinition.fillFrom == 'controller-urls' || + data.propertyDefinition.fillFrom == 'model-columns' || + data.propertyDefinition.fillFrom == 'plugin-lists' || + data.propertyDefinition.fillFrom == 'permissions' + ) { + ev.preventDefault() + + var subtype = null, + subtypeProperty = data.propertyDefinition.subtypeFrom + + if (subtypeProperty !== undefined) { + subtype = data.values[subtypeProperty] + } + + $.oc.builder.dataRegistry.get($(ev.target), this.getFormPluginCode(ev.target), data.propertyDefinition.fillFrom, subtype, function(response){ + data.callback({ + options: self.dataToInspectorArray(response) + }) + }) + } + } + + // INITIALIZATION + // ============================ + + $(document).ready(function(){ + $.oc.builder.indexController = new Builder() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.inspector.editor.localization.js b/plugins/rainlab/builder/assets/js/builder.inspector.editor.localization.js new file mode 100644 index 0000000..1dfc831 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.inspector.editor.localization.js @@ -0,0 +1,114 @@ +/* + * Inspector localization editor class. + */ ++function ($) { "use strict"; + + var Base = $.oc.inspector.propertyEditors.string, + BaseProto = Base.prototype + + var LocalizationEditor = function(inspector, propertyDefinition, containerCell, group) { + this.localizationInput = null + + Base.call(this, inspector, propertyDefinition, containerCell, group) + } + + LocalizationEditor.prototype = Object.create(BaseProto) + LocalizationEditor.prototype.constructor = Base + + LocalizationEditor.prototype.dispose = function() { + this.removeLocalizationInput() + + BaseProto.dispose.call(this) + } + + LocalizationEditor.prototype.build = function() { + var container = document.createElement('div'), + editor = document.createElement('input'), + placeholder = this.propertyDefinition.placeholder !== undefined ? this.propertyDefinition.placeholder : '', + value = this.inspector.getPropertyValue(this.propertyDefinition.property) + + editor.setAttribute('type', 'text') + editor.setAttribute('class', 'string-editor') + editor.setAttribute('placeholder', placeholder) + + container.setAttribute('class', 'autocomplete-container') + + if (value === undefined) { + value = this.propertyDefinition.default + } + + if (value === undefined) { + value = '' + } + + editor.value = value + + $.oc.foundation.element.addClass(this.containerCell, 'text autocomplete') + + container.appendChild(editor) + this.containerCell.appendChild(container) + + this.buildLocalizationEditor() + } + + LocalizationEditor.prototype.buildLocalizationEditor = function() { + this.localizationInput = new $.oc.builder.localizationInput(this.getInput(), this.getForm(), { + plugin: this.getPluginCode(), + beforePopupShowCallback: this.proxy(this.onPopupShown, this), + afterPopupHideCallback: this.proxy(this.onPopupHidden, this) + }) + } + + LocalizationEditor.prototype.removeLocalizationInput = function() { + this.localizationInput.dispose() + + this.localizationInput = null + } + + LocalizationEditor.prototype.supportsExternalParameterEditor = function() { + return false + } + + LocalizationEditor.prototype.registerHandlers = function() { + BaseProto.registerHandlers.call(this) + + $(this.getInput()).on('change', this.proxy(this.onInputKeyUp)) + } + + LocalizationEditor.prototype.unregisterHandlers = function() { + BaseProto.unregisterHandlers.call(this) + + $(this.getInput()).off('change', this.proxy(this.onInputKeyUp)) + } + + LocalizationEditor.prototype.getForm = function() { + var inspectableElement = this.getRootSurface().getInspectableElement() + + if (!inspectableElement) { + throw new Error('Cannot determine inspectable element in the Builder localization editor.') + } + + return $(inspectableElement).closest('form') + } + + LocalizationEditor.prototype.getPluginCode = function() { + var $form = this.getForm(), + $input = $form.find('input[name=plugin_code]') + + if (!$input.length) { + throw new Error('The input "plugin_code" should be defined in the form in order to use the localization Inspector editor.') + } + + return $input.val() + } + + LocalizationEditor.prototype.onPopupShown = function() { + this.getRootSurface().popupDisplayed() + } + + LocalizationEditor.prototype.onPopupHidden = function() { + this.getRootSurface().popupHidden() + } + + $.oc.inspector.propertyEditors.builderLocalization = LocalizationEditor +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.localizationinput.js b/plugins/rainlab/builder/assets/js/builder.localizationinput.js new file mode 100644 index 0000000..c2ec839 --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.localizationinput.js @@ -0,0 +1,299 @@ +/* + * Builder localization input control + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var LocalizationInput = function(input, form, options) { + this.input = input + this.form = form + this.options = $.extend({}, LocalizationInput.DEFAULTS, options) + this.disposed = false + this.initialized = false + this.newStringPopupMarkup = null + + Base.call(this) + + this.init() + } + + LocalizationInput.prototype = Object.create(BaseProto) + LocalizationInput.prototype.constructor = LocalizationInput + + LocalizationInput.prototype.dispose = function() { + this.unregisterHandlers() + + this.form = null + this.options.beforePopupShowCallback = null + this.options.afterPopupHideCallback = null + this.options = null + this.disposed = true + this.newStringPopupMarkup = null + + if (this.initialized) { + $(this.input).autocomplete('destroy') + } + + $(this.input).removeData('localization-input') + + this.input = null + + BaseProto.dispose.call(this) + } + + LocalizationInput.prototype.init = function() { + if (!this.options.plugin) { + throw new Error('The options.plugin value should be set in the localization input object.') + } + + var $input = $(this.input) + + $input.data('localization-input', this) + $input.attr('data-builder-localization-input', 'true') + $input.attr('data-builder-localization-plugin', this.options.plugin) + + this.getContainer().addClass('localization-input-container') + + this.registerHandlers() + this.loadDataAndBuild() + } + + LocalizationInput.prototype.buildAddLink = function() { + var $container = this.getContainer() + + if ($container.find('a.localization-trigger').length > 0) { + return + } + + var trigger = document.createElement('a') + + trigger.setAttribute('class', 'oc-icon-plus localization-trigger') + trigger.setAttribute('href', '#') + + var pos = $container.position() + $(trigger).css({ + top: pos.top + 4, + right: 7 + }) + + $container.append(trigger) + } + + LocalizationInput.prototype.loadDataAndBuild = function() { + this.showLoadingIndicator() + + var result = $.oc.builder.dataRegistry.get(this.form, this.options.plugin, 'localization', null, this.proxy(this.dataLoaded)), + self = this + + if (result) { + result.always(function(){ + self.hideLoadingIndicator() + }) + } + } + + LocalizationInput.prototype.reload = function() { + $.oc.builder.dataRegistry.get(this.form, this.options.plugin, 'localization', null, this.proxy(this.dataLoaded)) + } + + LocalizationInput.prototype.dataLoaded = function(data) { + if (this.disposed) { + return + } + + var $input = $(this.input), + autocomplete = $input.data('autocomplete') + + if (!autocomplete) { + this.hideLoadingIndicator() + + var autocompleteOptions = { + source: this.preprocessData(data), + matchWidth: true + } + + autocompleteOptions = $.extend(autocompleteOptions, this.options.autocompleteOptions) + + $(this.input).autocomplete(autocompleteOptions) + + this.initialized = true + } + else { + autocomplete.source = this.preprocessData(data) + } + } + + LocalizationInput.prototype.preprocessData = function(data) { + var dataClone = $.extend({}, data) + + for (var key in dataClone) { + dataClone[key] = key + ' - ' + dataClone[key] + } + + return dataClone + } + + LocalizationInput.prototype.getContainer = function() { + return $(this.input).closest('.autocomplete-container') + } + + LocalizationInput.prototype.showLoadingIndicator = function() { + var $container = this.getContainer() + + $container.addClass('loading-indicator-container size-small') + $container.loadIndicator() + } + + LocalizationInput.prototype.hideLoadingIndicator = function() { + var $container = this.getContainer() + + $container.loadIndicator('hide') + $container.loadIndicator('destroy') + + $container.removeClass('loading-indicator-container') + } + + // POPUP + // ============================ + + LocalizationInput.prototype.loadAndShowPopup = function() { + if (this.newStringPopupMarkup === null) { + $.oc.stripeLoadIndicator.show() + $(this.input).request('onLanguageLoadAddStringForm') + .done( + this.proxy(this.popupMarkupLoaded) + ).always(function(){ + $.oc.stripeLoadIndicator.hide() + }) + } + else { + this.showPopup() + } + } + + LocalizationInput.prototype.popupMarkupLoaded = function(responseData) { + this.newStringPopupMarkup = responseData.markup + + this.showPopup() + } + + LocalizationInput.prototype.showPopup = function() { + var $input = $(this.input) + + $input.popup({ + content: this.newStringPopupMarkup + }) + + var $content = $input.data('oc.popup').$content, + $keyInput = $content.find('#language_string_key') + + $.oc.builder.dataRegistry.get(this.form, this.options.plugin, 'localization', 'sections', function(data){ + $keyInput.autocomplete({ + source: data, + matchWidth: true + }) + }) + + $content.find('form').on('submit', this.proxy(this.onSubmitPopupForm)) + } + + LocalizationInput.prototype.stringCreated = function(data) { + if (data.localizationData === undefined || data.registryData === undefined) { + throw new Error('Invalid server response.') + } + + var $input = $(this.input) + + $input.val(data.localizationData.key) + + $.oc.builder.dataRegistry.set(this.options.plugin, 'localization', null, data.registryData.strings) + $.oc.builder.dataRegistry.set(this.options.plugin, 'localization', 'sections', data.registryData.sections) + + $input.data('oc.popup').hide() + + $input.trigger('change') + } + + LocalizationInput.prototype.onSubmitPopupForm = function(ev) { + var $form = $(ev.target) + + $.oc.stripeLoadIndicator.show() + $form.request('onLanguageCreateString', { + data: { + plugin_code: this.options.plugin + } + }) + .done( + this.proxy(this.stringCreated) + ).always(function(){ + $.oc.stripeLoadIndicator.hide() + }) + + ev.preventDefault() + return false + } + + LocalizationInput.prototype.onPopupHidden = function(ev, link, popup) { + $(popup).find('#language_string_key').autocomplete('destroy') + $(popup).find('form').on('submit', this.proxy(this.onSubmitPopupForm)) + + if (this.options.afterPopupHideCallback) { + this.options.afterPopupHideCallback() + } + } + + LocalizationInput.updatePluginInputs = function(plugin) { + var inputs = document.body.querySelectorAll('input[data-builder-localization-input][data-builder-localization-plugin="'+plugin+'"]') + + for (var i=inputs.length-1; i>=0; i--) { + $(inputs[i]).data('localization-input').reload() + } + } + + // EVENT HANDLERS + // ============================ + + LocalizationInput.prototype.unregisterHandlers = function() { + this.input.removeEventListener('focus', this.proxy(this.onInputFocus)) + + this.getContainer().off('click', 'a.localization-trigger', this.proxy(this.onTriggerClick)) + $(this.input).off('hidden.oc.popup', this.proxy(this.onPopupHidden)) + } + + LocalizationInput.prototype.registerHandlers = function() { + this.input.addEventListener('focus', this.proxy(this.onInputFocus)) + + this.getContainer().on('click', 'a.localization-trigger', this.proxy(this.onTriggerClick)) + $(this.input).on('hidden.oc.popup', this.proxy(this.onPopupHidden)) + } + + LocalizationInput.prototype.onInputFocus = function() { + this.buildAddLink() + } + + LocalizationInput.prototype.onTriggerClick = function(ev) { + if (this.options.beforePopupShowCallback) { + this.options.beforePopupShowCallback() + } + + this.loadAndShowPopup() + + ev.preventDefault() + return false + } + + LocalizationInput.DEFAULTS = { + plugin: null, + autocompleteOptions: {}, + beforePopupShowCallback: null, + afterPopupHideCallback: null + } + + $.oc.builder.localizationInput = LocalizationInput + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/js/builder.table.processor.localization.js b/plugins/rainlab/builder/assets/js/builder.table.processor.localization.js new file mode 100644 index 0000000..f54a89e --- /dev/null +++ b/plugins/rainlab/builder/assets/js/builder.table.processor.localization.js @@ -0,0 +1,126 @@ +/* + * Localiztion cell processor for the table control. + */ + ++function ($) { "use strict"; + + // NAMESPACE CHECK + // ============================ + + if ($.oc.table === undefined) + throw new Error("The $.oc.table namespace is not defined. Make sure that the table.js script is loaded."); + + if ($.oc.table.processor === undefined) + throw new Error("The $.oc.table.processor namespace is not defined. Make sure that the table.processor.base.js script is loaded."); + + // CLASS DEFINITION + // ============================ + + var Base = $.oc.table.processor.string, + BaseProto = Base.prototype + + var LocalizationProcessor = function(tableObj, columnName, columnConfiguration) { + // + // State properties + // + + this.localizationInput = null + this.popupDisplayed = false + + // + // Parent constructor + // + + Base.call(this, tableObj, columnName, columnConfiguration) + } + + LocalizationProcessor.prototype = Object.create(BaseProto) + LocalizationProcessor.prototype.constructor = LocalizationProcessor + + LocalizationProcessor.prototype.dispose = function() { + this.removeLocalizationInput() + + BaseProto.dispose.call(this) + } + + /* + * Forces the processor to hide the editor when the user navigates + * away from the cell. Processors can update the sell value in this method. + * Processors must clear the reference to the active cell in this method. + */ + LocalizationProcessor.prototype.onUnfocus = function() { + if (!this.activeCell || this.popupDisplayed) + return + + this.removeLocalizationInput() + + BaseProto.onUnfocus.call(this) + } + + LocalizationProcessor.prototype.onBeforePopupShow = function() { + this.popupDisplayed = true + } + + LocalizationProcessor.prototype.onAfterPopupHide = function() { + this.popupDisplayed = false + } + + /* + * Renders the cell in the normal (no edit) mode + */ + LocalizationProcessor.prototype.renderCell = function(value, cellContentContainer) { + BaseProto.renderCell.call(this, value, cellContentContainer) + } + + LocalizationProcessor.prototype.buildEditor = function(cellElement, cellContentContainer, isClick) { + BaseProto.buildEditor.call(this, cellElement, cellContentContainer, isClick) + + $.oc.foundation.element.addClass(cellContentContainer, 'autocomplete-container') + this.buildLocalizationEditor() + } + + LocalizationProcessor.prototype.buildLocalizationEditor = function() { + var input = this.getInput() + + this.localizationInput = new $.oc.builder.localizationInput(input, $(input), { + plugin: this.getPluginCode(input), + beforePopupShowCallback: $.proxy(this.onBeforePopupShow, this), + afterPopupHideCallback: $.proxy(this.onAfterPopupHide, this), + autocompleteOptions: { + menu: '', + bodyContainer: true + } + }) + } + + LocalizationProcessor.prototype.getInput = function() { + if (!this.activeCell) { + return null + } + + return this.activeCell.querySelector('.string-input') + } + + LocalizationProcessor.prototype.getPluginCode = function(input) { + var $form = $(input).closest('form'), + $input = $form.find('input[name=plugin_code]') + + if (!$input.length) { + throw new Error('The input "plugin_code" should be defined in the form in order to use the localization table processor.') + } + + return $input.val() + } + + LocalizationProcessor.prototype.removeLocalizationInput = function() { + if (!this.localizationInput) { + return + } + + this.localizationInput.dispose() + + this.localizationInput = null + } + + $.oc.table.processor.builderLocalization = LocalizationProcessor; +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/less/behaviors.less b/plugins/rainlab/builder/assets/less/behaviors.less new file mode 100644 index 0000000..02af3da --- /dev/null +++ b/plugins/rainlab/builder/assets/less/behaviors.less @@ -0,0 +1,167 @@ +.builder-controllers-builder-area { + background: var(--bs-body-bg, white); + + ul.controller-behavior-list { + .clearfix(); + + padding: 20px; + margin-bottom: 0; + list-style: none; + + li { + h4 { + text-align: center; + border-bottom: 1px dotted @builder-control-border-color; + margin: 0 -20px 40px; + + span { + display: inline-block; + background: @builder-control-tooltip-color; + color: white; + margin: 0 auto; + border-radius: 8px; + padding: 7px 10px; + font-size: 13px; + line-height: 100%; + position: relative; + top: 14px; + } + } + } + + .behavior-container { + margin-bottom: 40px; + .clearfix(); + cursor: pointer; + + .list-behavior, .import-export-behavior { + border-radius: 4px; + border: 2px solid @builder-control-border-color; + padding: 25px 10px 25px 10px; + + table { + border-collapse: collapse; + width: 100%; + + td { + padding: 0 15px 15px 15px; + + border-right: 1px solid @builder-control-border-color; + + &:last-child { + border-right: none; + } + } + + .oc-placeholder { + background: var(--oc-secondary-bg, #EEF2F4); + height: 25px; + } + + tbody tr:last-child td { + padding-bottom: 0; + } + } + } + + .import-export-behavior { + table { + i.icon-bars, .oc-placeholder { + float: left; + } + + i.icon-bars { + margin-right: 15px; + color: #D6DDE0; + font-size: 28px; + line-height: 28px; + position: relative; + top: -2px; + } + } + } + + .form-behavior { + div.form { + .clearfix(); + + padding: 25px 25px 0 25px; + border: 2px solid @builder-control-border-color; + margin-bottom: 20px; + .border-radius(4px); + } + + div.field { + &.left { + float: left; + width: 48%; + } + + &.right { + float: right; + width: 45%; + } + + div.label { + background: var(--oc-secondary-bg, #EEF2F4); + height: 25px; + margin-bottom: 10px; + + &.size-3 { + width: 100px; + } + + &.size-5 { + width: 150px; + } + + &.size-2 { + width: 60px; + } + } + + div.control { + background: var(--oc-secondary-bg, #EEF2F4); + height: 35px; + margin-bottom: 25px; + } + } + + div.button { + background: var(--oc-secondary-bg, #EEF2F4); + height: 35px; + margin-right: 20px; + .border-radius(4px); + + &.size-5 { + width: 100px; + } + + &.size-3 { + width: 60px; + } + + &:first-child { + margin-right: 0; + } + } + } + + &:hover, &.inspector-open { + * { + border-color: @builder-hover-color!important; + } + } + } + } +} + +// Fix for the Mac firefox + +html.gecko.mac { + .builder-controllers-builder-area { + ul.controller-behavior-list { + padding-right: 40px; + } + } +} \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/less/builder.less b/plugins/rainlab/builder/assets/less/builder.less new file mode 100644 index 0000000..44f0e98 --- /dev/null +++ b/plugins/rainlab/builder/assets/less/builder.less @@ -0,0 +1,197 @@ +@import "../../../../../modules/backend/assets/less/core/boot.less"; + +@builder-control-border-color: var(--oc-document-ruler-tick, #bdc3c7); +@builder-control-tooltip-color: #72809d; +@builder-control-text-color: #95a5a6; +@builder-hover-color: var(--oc-selection); + +:root, [data-bs-theme="light"] { + --oc-builder-control-color: #555; +} + +[data-bs-theme="dark"] { + --oc-builder-control-color: #888; +} + +@import "buildingarea.less"; +@import "controlblueprint.less"; +@import "behaviors.less"; +@import "tabs.less"; +@import "menus.less"; +@import "imports.less"; +@import "localization.less"; +@import "codelist.less"; + +.control-filelist ul li.group.model > h4 a:after { + content: @random; + top: 10px; +} + +.control-filelist ul li.group.form > h4 a:after { + content: @check-square; +} + +.control-filelist ul li.group.list > h4 a:after { + content: @th-list; + top: 10px; +} + +.control-filelist ul li.group > ul > li.group > ul > li > a { + padding-left: 73px; + margin-left: -20px; +} + +.control-filelist ul li.with-icon { + span.title, span.description { + padding-left: 22px; + } + + i.list-icon { + position: absolute; + left: 20px; + top: 12px; + color: #405261; + + &.mute { + color: #8f8f8f; + } + + &.icon-check-square { + color: #8da85e; + } + } +} + +html.gecko .control-filelist ul li.group { + margin-right: 10px; +} + +.builder-inspector-container { + width: 350px; + border-left: 1px solid var(--bs-border-color, #d9d9d9); + + &:empty { + display: none!important; + } +} + +form.hide-secondary-tabs { + div.control-tabs.secondary-tabs { + ul.nav.nav-tabs { + display: none; + } + } +} + +.form-group { + &.size-quarter { + width: 23.5%; + } + + &.size-three-quarter { + width: 73.5%; + } +} + +// Full height database columns table + +form[data-entity=database], +form[data-entity=models] { + div.field-datatable { + position: absolute; + width: 100%; + height: 100%; + + div[data-control=table] { + position: absolute; + width: 100%; + height: 100%; + + div.table-container { + position: absolute; + width: 100%; + height: 100%; + + div.control-scrollbar { + top: 70px; + bottom: 0; + position: absolute; + max-height: none!important; + height: auto!important; + } + } + } + } +} + +// TODO: Move the aux tab styles to the tab.less + +.control-tabs { + &.auxiliary-tabs { + background: white; + + .border-top(@color) { + content: ' '; + display: block; + position: absolute; + width: 100%; + height: 1px; + background: @color; + top: 0; + left: 0; + } + + > ul.nav-tabs, > div > ul.nav-tabs { + padding-left: 20px; + padding-bottom: 2px; + background: white; + position: relative; + + &:before { + .border-top(#95a5a6); + } + + > li { + margin-right: 2px; + + > a { + background: white; + color: #bdc3c7; + border-left: 1px solid #ecf0f1!important; + border-right: 1px solid #ecf0f1!important; + border-bottom: 1px solid #ecf0f1!important; + padding: 4px 10px; + line-height: 100%; + .border-radius(0 0 4px 4px); + + > span.title > span { + margin-bottom: 0; + font-size: 13px; + height: auto; + } + } + + &.active{ + top: 0; + + &:before { + .border-top(white); + top: -1px; + } + + a { + padding-top: 5px; + border-left: 1px solid #95a5a6!important; + border-right: 1px solid #95a5a6!important; + border-bottom: 1px solid #95a5a6!important; + color: #95a5a6; + } + } + } + } + + > div.tab-content > .tab-pane { + background: white; + } + } +} diff --git a/plugins/rainlab/builder/assets/less/buildingarea.less b/plugins/rainlab/builder/assets/less/buildingarea.less new file mode 100644 index 0000000..234b43f --- /dev/null +++ b/plugins/rainlab/builder/assets/less/buildingarea.less @@ -0,0 +1,262 @@ +.builder-building-area { + background: var(--bs-body-bg, white); + + ul.builder-control-list { + .clearfix(); + + padding: 20px; + margin-bottom: 0; + list-style: none; + > li.control { + position: relative; + margin-bottom: 20px; + cursor: pointer; + user-select: none; + + &[data-unknown] { + cursor: default; + } + + &.oc-placeholder, &.loading-control { + padding: 10px 12px; + position: relative; + text-align: center; + border: 2px dotted var(--bs-border-color, #dae0e0); + margin-top: 20px; + border-radius: 4px; + color: var(--bs-emphasis-color, #dae0e0); + + i { + margin-right: 8px; + } + } + + &.loading-control { + background: var(--oc-secondary-bg); + } + + &.clear-row { + display: none; + margin-bottom: 0; + } + + &.loading-control { + border-color: #bdc3c7; + text-align: left; + } + + &.updating-control:after, + &.loading-control:before { + background-image:url(../images/loader-transparent.svg); + background-size: 15px 15px; + background-position: 50% 50%; + display: inline-block; + width: 15px; + height: 15px; + content: ' '; + margin-right: 13px; + position: relative; + top: 2px; + animation: spin 1s linear infinite; + } + + &.loading-control:after { + content: attr(data-builder-loading-text); + display: inline-block; + } + + &.updating-control:after { + position: absolute; + right: -8px; + top: 5px; + } + + &.updating-control:before { + content: ''; + position: absolute; + right: 0; + top: 0; + width: 25px; + height: 25px; + background: rgba(127, 127, 127, 0.1); + border-radius: 4px; + } + + &.drag-over { + color: @builder-hover-color; + border-color: @builder-hover-color; + } + + &.span-full { + width: 100%; + float: left; + } + + &.span-left { + float: left; + width: 48.5%; + clear: left; + } + + &.span-right { + float: right; + width: 48.5%; + clear: right; + } + + &.span-right + li.clear-row { + display: block; + clear: both; + } + + > div.remove-control { + display: none; + } + + &:not(.oc-placeholder):not(.loading-control):not(.updating-control):hover > { + > div.remove-control { + font-family: sans-serif; + display: block; + position: absolute; + right: 0; + top: 0; + cursor: pointer; + width: 21px; + height: 21px; + padding-left: 6px; + font-size: 16px; + font-weight: bold; + line-height: 21px; + border-radius: 20px; + background: var(--oc-toolbar-border, #ecf0f1); + color: var(--oc-toolbar-color, #95a5a6) !important; + + &:hover { + color: white !important; + background: #c03f31; + } + } + + &[data-control-type=hint], &[data-control-type=partial] { + > div.remove-control { + top: 12px; + right: 12px; + } + } + } + + &[data-control-type=hint], &[data-control-type=partial] { + &.updating-control { + &:before { + right: 12px; + top: 7; + } + + &:after { + right: 4px; + top: 13px; + } + } + } + + > .control-wrapper, + > .control-static-contents { + position: relative; + transition: margin 0.1s; + } + } + + > li.oc-placeholder:hover, + > li.oc-placeholder.popover-highlight, + > li.oc-placeholder.control-palette-open { + background-color: @builder-hover-color!important; + color: white!important; + border-style: solid; + border-color: @builder-hover-color; + opacity: 1; + } + + > li.control:not(.oc-placeholder):not(.loading-control):not([data-unknown]):hover > .control-wrapper *, + > li.control.inspector-open:not(.oc-placeholder):not(.loading-control) > .control-wrapper * { + color: @builder-hover-color!important; + } + + > li.control.drag-over:not(.oc-placeholder) { + &:before { + position: absolute; + content: ''; + top: 0; + left: 0; + width: 10px; + height: 100%; + border-radius: 5px; + background-color: @builder-hover-color; + } + + > .control-wrapper, + > .control-static-contents { + margin-left: 20px; + margin-right: -20px; + } + } + } + + .control-body { + &.field-disabled, + &.field-hidden { + opacity: 0.5; + } + } + + .builder-control-label { + margin-bottom: 10px; + color: var(--oc-builder-control-color); + font-size: 14px; + font-weight: 600; + + &.required:after { + vertical-align: super; + font-size: 60%; + content: " *"; + } + } + + .builder-control-label:empty { + margin-bottom: 0; + } + + .builder-control-comment-above { + margin-bottom: 8px; + margin-top: -3px; + } + + .builder-control-comment-below { + margin-top: 6px; + } + + .builder-control-comment-above, + .builder-control-comment-below { + color: #737373; + font-size: 12px; + + &:empty { + display: none; + } + } +} + +html.gecko.mac { + // Fixes a quirk in FireFox on mac + + .builder-building-area { + div[data-root-control-wrapper] { + margin-right: 17px; + } + } +} + +[data-bs-theme="dark"] { + .builder-building-area { + background: @body-bg; + } +} diff --git a/plugins/rainlab/builder/assets/less/codelist.less b/plugins/rainlab/builder/assets/less/codelist.less new file mode 100644 index 0000000..2c6f63a --- /dev/null +++ b/plugins/rainlab/builder/assets/less/codelist.less @@ -0,0 +1,249 @@ + + +@color-text-title: @text-color; +@color-text-description: @primary-color; + +[data-entity="code"] .secondary-content-tabs .nav-tabs { + display: none; +} + +.control-codelist { + p.no-data { + padding: 22px; + margin: 0; + color: @text-muted; + font-size: 14px; + text-align: center; + font-weight: 400; + border-radius: @border-radius-base; + } + + p.parent, ul li { + font-weight: 300; + line-height: 150%; + margin-bottom: 0; + + &.active a { + background: @color-list-active; + position: relative; + &:after { + position: absolute; + height: 100%; + width: 4px; + left: 0; + top: 0; + background: @color-list-active-border; + display: block; + content: ' '; + } + } + + a.link { + display: block; + position: relative; + word-wrap: break-word; + padding: 10px 50px 10px 20px; + outline: none; + font-weight: 400; + color: @color-text-title; + font-size: 14px; + + &:hover, &:focus, &:active { + text-decoration: none; + } + + span { + display: block; + + &.description { + color: @color-text-description; + font-size: 12px; + font-weight: 400; + word-wrap: break-word; + + strong { + color: @color-text-title; + font-weight: 400; + } + } + } + } + + &.directory, &.parent { + a.link { + padding-left: 40px; + + &:after { + display: block; + position: absolute; + width: 10px; + height: 10px; + top: 10px; + left: 20px; + .icon(@folder); + color: @color-list-icon; + font-size: 14px; + } + } + } + + &.parent { + a.link { + padding-left: 41px; + background-color: @primary-bg; + color: @primary-color; + word-wrap: break-word; + + &:before { + content: ''; + height: 1px; + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 1px; + background: @primary-border; + } + + + &:after { + font-size: 13px; + color: @primary-color; + width: 18px; + height: 18px; + top: 11px; + left: 22px; + opacity: 0.5; + .icon(@chevron-left); + } + } + } + } + + p.parent a.link:hover { + background: @editor-section-bg !important; + color: @editor-section-color !important; + + &:after { + opacity: 1; + } + + &:before { + display: none; + } + } + + ul { + padding: 0; + margin: 0; + + li { + font-weight: 300; + line-height: 150%; + position: relative; + list-style: none; + + &.active a.link, a.link:hover { + background: @editor-section-bg; + color: @editor-section-color; + } + + &.active a.link { + position: relative; + &:after { + position: absolute; + height: 100%; + width: 4px; + left: 0; + top: 0; + background: @primary-border; + display: block; + content: ' '; + } + } + + div.controls { + position: absolute; + right: 45px; + top: 10px; + + .dropdown { + width: 14px; + height: 21px; + + &.open a.control { + display: block!important; + &:before { + visibility: visible; + display: block; + } + } + } + + a.control { + color: @color-text-title; + font-size: 14px; + visibility: hidden; + overflow: hidden; + width: 14px; + height: 21px; + display: none; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + &:before { + visibility: visible; + display: block; + margin-right: 0; + } + + &:hover { + opacity: 1; + } + } + } + + &:hover { + background: @editor-section-bg; + color: @editor-section-color; + + div.controls, a.control { + display: block !important; + + > a.control { + display: block !important; + } + } + } + + .form-check { + position: absolute; + top: 10px; + right: 5px; + + label { + margin-right: 0; + } + } + } + } + + div.list-container { + position: relative; + .translate(0, 0); + + &.animate ul { + .transition(all 0.2s ease); + } + + &.goForward ul { + .translate(-350px, 0); + } + + &.goBackward ul { + .translate(350px, 0); + } + + } +} diff --git a/plugins/rainlab/builder/assets/less/controlblueprint.less b/plugins/rainlab/builder/assets/less/controlblueprint.less new file mode 100644 index 0000000..3f2f786 --- /dev/null +++ b/plugins/rainlab/builder/assets/less/controlblueprint.less @@ -0,0 +1,260 @@ +.builder-building-area { + .builder-blueprint-control-text, + .builder-blueprint-control-textarea, + .builder-blueprint-control-partial, + .builder-blueprint-control-unknown, + .builder-blueprint-control-dropdown { + padding: 10px 12px; + border: 2px solid @builder-control-border-color; + color: @builder-control-text-color; + .border-radius(4px); + + i { + margin-right: 5px; + } + } + + li.control:hover, li.inspector-open { + > .control-wrapper { + .builder-blueprint-control-text, + .builder-blueprint-control-textarea, + .builder-blueprint-control-dropdown { + border-color: @builder-hover-color; + } + + .builder-blueprint-control-dropdown:before { + background-color: @builder-hover-color; + } + } + } + + .builder-blueprint-control-textarea { + &.size-tiny { min-height: @size-tiny; } + &.size-small { min-height: @size-small; } + &.size-large { min-height: @size-large; } + &.size-huge { min-height: @size-huge; } + &.size-giant { min-height: @size-giant; } + } + + .builder-blueprint-control-section { + border-bottom: 1px solid @builder-control-border-color; + padding-bottom: 4px; + + .builder-control-label { + font-size: 16px; + margin-bottom: 6px; + } + } + + .builder-blueprint-control-unknown { + border-color: #eee; + background: #eee; + } + + .builder-blueprint-control-partial { + border-color: #eee; + background: #eee; + } + + .builder-blueprint-control-dropdown { + position: relative; + + &:before, &:after { + position: absolute; + content: ''; + } + + &:before { + top: 0; + width: 2px; + background: @builder-control-border-color; + right: 40px; + height: 100%; + } + + &:after { + .icon(@angle-down); + color: inherit; + right: 15px; + top: 12px; + font-size: 20px; + line-height: 20px; + } + } + + .builder-blueprint-control-checkbox { + &:before { + float: left; + content: ' '; + border: 2px solid @builder-control-border-color; + .border-radius(4px); + width: 17px; + height: 17px; + position: relative; + top: 2px; + } + + .builder-control-label { + margin-left: 25px; + font-weight: normal; + } + + .builder-control-comment-below { + margin-left: 25px; + } + } + + li.control:hover, li.inspector-open { + > .control-wrapper { + .builder-blueprint-control-checkbox:before { + border-color: @builder-hover-color; + } + } + } + + .builder-blueprint-control-switch { + position: relative; + + &:before, &:after { + position: absolute; + content: ' '; + .border-radius(30px); + } + + &:before { + background-color: @builder-control-border-color; + + width: 34px; + height: 18px; + top: 2px; + left: 2px; + } + + &:after { + background-color: white; + + width: 14px; + height: 14px; + top: 4px; + left: 4px; + margin-left: 16px; + } + + .builder-control-label { + margin-left: 45px; + font-weight: normal; + } + + .builder-control-comment-below { + margin-left: 45px; + } + } + + li.control:hover, li.inspector-open { + > .control-wrapper { + .builder-blueprint-control-switch:before { + background-color: @builder-hover-color; + } + } + } + + .builder-blueprint-control-repeater-body { + > .repeater-button { + padding: 8px 13px; + background: @builder-control-border-color; + color: white; + display: inline-block; + margin-bottom: 10px; + .border-radius(2px); + } + } + + ul.builder-control-list > li.control:hover, + ul.builder-control-list > li.inspector-open { + > .control-wrapper > .control-body { + .builder-blueprint-control-repeater-body { + > .repeater-button { + background: @builder-hover-color; + color: white!important; + span { + color: white!important; + } + } + } + } + } + + .builder-blueprint-control-repeater { + position: relative; + + &:before { + content: ''; + position: absolute; + width: 2px; + top: 0; + left: 2px; + height: 100%; + background: @builder-control-border-color; + } + + &:after { + content: ''; + position: absolute; + width: 6px; + height: 6px; + top: 14px; + left: 0; + .border-radius(6px); + background: @builder-control-border-color; + } + + > ul.builder-control-list { + padding-right: 0; + padding-bottom: 0; + padding-top: 10px; + } + } + + li.control:hover, li.inspector-open { + > .builder-blueprint-control-repeater { + &:before, &:after { + background-color: @builder-hover-color; + } + } + } + + .builder-blueprint-control-radiolist, + .builder-blueprint-control-checkboxlist { + ul { + list-style: none; + padding: 0; + color: @builder-control-text-color; + + li { + margin-bottom: 3px; + + &:last-child { + margin-bottom: 0; + } + + i { + margin-right: 5px; + } + } + } + } + + .builder-blueprint-control-text { + &.fileupload.image { + width: 100px; + height: 100px; + text-align: center; + + i { + line-height: 77px; + margin-right: 0; + } + } + } + +} \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/less/imports.less b/plugins/rainlab/builder/assets/less/imports.less new file mode 100644 index 0000000..7c48ddc --- /dev/null +++ b/plugins/rainlab/builder/assets/less/imports.less @@ -0,0 +1,186 @@ +.builder-tailor-builder-area { + background: var(--bs-body-bg, white); + + ul.tailor-blueprint-list { + .clearfix(); + cursor: pointer; + padding: 20px; + margin-bottom: 0; + list-style: none; + + li { + position: relative; + + h4 { + text-align: center; + border-bottom: 1px dotted @builder-control-border-color; + margin: 0 -20px 30px; + + span { + display: inline-block; + color: white; + margin: 0 auto; + border-radius: 8px; + background: @builder-control-tooltip-color; + padding: 7px 10px; + font-size: 13px; + line-height: 100%; + position: relative; + top: 14px; + } + } + + table.table { + margin: 0; + td { + font-size: 0.875em; + + > span { + font-family: var(--bs-font-monospace); + color: @secondary-color; + word-wrap: break-word; + word-break: break-word; + } + } + th { + font-size: 0.875em; + text-align: right; + } + th:not(.table-danger) { + color: @text-color; + } + tr:last-child { + td, th { + border-bottom: none; + } + } + } + + div.remove-blueprint { + font-family: sans-serif; + display: none; + position: absolute; + right: 0; + top: 20px; + cursor: pointer; + width: 21px; + height: 21px; + padding-left: 6px; + font-size: 16px; + font-weight: bold; + line-height: 21px; + border-radius: 20px; + background: var(--oc-toolbar-border, #ecf0f1); + color: var(--oc-toolbar-color, #95a5a6) !important; + + &:hover { + color: white !important; + background: #c03f31; + } + } + + &:hover { + div.remove-blueprint { + display: block; + } + } + + &.updating-blueprint:after { + background-image:url(../images/loader-transparent.svg); + background-size: 15px 15px; + background-position: 50% 50%; + display: inline-block; + width: 15px; + height: 15px; + content: ' '; + margin-right: 13px; + position: relative; + top: 2px; + animation: spin 1s linear infinite; + } + + &.updating-blueprint:after { + position: absolute; + right: -8px; + top: 35px; + } + + &.updating-blueprint:before { + content: ''; + position: absolute; + right: 0; + top: 30px; + width: 25px; + height: 25px; + background: rgba(127, 127, 127, 0.1); + border-radius: 4px; + } + } + + .blueprint-container { + .clearfix(); + + .tailor-blueprint { + div.form { + .clearfix(); + border: 2px solid @builder-control-border-color; + margin-bottom: 20px; + border-radius: 4px; + } + } + + &:hover, &.inspector-open { + * { + border-color: @builder-hover-color!important; + } + } + } + } + + .add-blueprint-button { + font-size: 16px; + text-align: center; + border: 2px dotted var(--oc-dropdown-trigger-border, #dde0e2); + height: 64px; + margin: 0 20px 40px; + + a { + padding: 20px 15px; + height: 60px; + display: block; + text-decoration: none; + color: var(--oc-dropdown-trigger-color, #bdc3c7); + } + + i { + margin-right: 5px; + } + + span { + position: relative; + top: -1px; + } + + span.title { + font-size: 14px; + } + + &:hover { + border: 2px dotted @builder-hover-color; + background: @builder-hover-color!important; + + a { + color: white; + } + } + } +} + +// Fix for the Mac firefox +html.gecko.mac { + .builder-tailor-builder-area { + ul.tailor-blueprint-list { + padding-right: 40px; + } + } +} diff --git a/plugins/rainlab/builder/assets/less/localization.less b/plugins/rainlab/builder/assets/less/localization.less new file mode 100644 index 0000000..a8637ff --- /dev/null +++ b/plugins/rainlab/builder/assets/less/localization.less @@ -0,0 +1,86 @@ +.localization-input-container { + // position: relative; + + input[type=text].string-editor { + padding-right: 20px!important; + } + + .localization-trigger { + position: absolute; + display: none; + width: 10px; + height: 10px; + font-size: 14px; + color: #95a5a6; + + outline: none; + + &:hover, &:active, &:focus { + color: #2581b8; + text-decoration: none; + } + } +} + +table.inspector-fields td.active, +table.data td.active { + .localization-input-container .localization-trigger { + display: block; + } +} + +table.data td.active .localization-input-container .localization-trigger { + top: 5px!important; + right: 7px!important; +} + +.control-table td[data-column-type=builderLocalization] input[type=text] { + padding-right: 20px!important; +} + +.control-table { + td[data-column-type=builderLocalization] { + input[type=text] { + width: 100%; + height: 100%; + display: block; + outline: none; + border: none; + padding: 6px 10px 6px; + } + } +} + +html.chrome { + .control-table { + td[data-column-type=builderLocalization] { + input[type=text] { + padding: 6px 10px 7px!important; + } + } + } +} + +html.safari, html.gecko { + .control-table { + td[data-column-type=builderLocalization] { + input[type=text] { + padding: 5px 10px 5px; + } + } + } +} + +.autocomplete.dropdown-menu.table-widget-autocomplete.localization li a { + white-space: normal; + word-wrap: break-word; +} + +table.data td[data-column-type=builderLocalization] .loading-indicator-container.size-small .loading-indicator { + padding-bottom: 0!important; + + span { + left: auto; + right: 6px; + } +} \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/less/menus.less b/plugins/rainlab/builder/assets/less/menus.less new file mode 100644 index 0000000..f0e96f1 --- /dev/null +++ b/plugins/rainlab/builder/assets/less/menus.less @@ -0,0 +1,178 @@ +.builder-menu-editor { + background: var(--bs-body-bg, white); + + .builder-menu-editor-workspace { + padding: 30px; + } + + ul.builder-menu { + font-size: 0; + padding: 0; + cursor: pointer; + + > li { + border-radius: 4px; + + div.item-container:hover, &.inspector-open > div.item-container { + background: @builder-hover-color!important; + color: white!important; + + a { + color: white!important; + } + } + + div.item-container { + position: relative; + + .close-btn { + color: white; + position: absolute; + display: none; + width: 15px; + height: 15px; + right: 5px; + top: 5px; + font-size: 14px; + text-align: center; + line-height: 14px; + } + + &:hover .close-btn { + display: block; + text-decoration: none; + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + } + + &.add { + font-size: 16px; + text-align: center; + border: 2px dotted var(--oc-dropdown-trigger-border, #dde0e2); + + a { + text-decoration: none; + color: var(--oc-dropdown-trigger-color, #bdc3c7); + } + + span.title { + font-size: 14px; + } + + &:hover { + border: 2px dotted @builder-hover-color; + background: @builder-hover-color!important; + + a { + color: white; + } + } + } + + &.list-sortable-placeholder { + border: 2px dotted @builder-hover-color; + height: 10px; + background: transparent; + } + } + + &.builder-main-menu { + > li { + display: inline-block; + vertical-align: top; + + &.item { + margin: 0 20px 20px 0; + } + + > div.item-container { + background: var(--bs-secondary-bg, #ecf0f1); + color: var(--bs-secondary-color, #708080); + padding: 20px 25px; + height: 64px; + white-space: nowrap; + + i { + font-size: 24px; + margin-right: 10px; + } + + span.title { + font-size: 14px; + line-height: 100%; + position: relative; + top: -3px; + } + } + + &.add { + height: 64px; + + a { + padding: 20px 15px; + height: 60px; + display: block; + + i { + margin-right: 5px; + } + + span { + position: relative; + top: -1px; + } + } + } + } + } + + &.builder-submenu { + margin-top: 1px; + + > li { + display: block; + width: 120px; + + i { + display: block; + margin-bottom: 7px; + } + + span.title { + display: block; + font-size: 12px; + } + + &.item { + margin: 0 0 1px 0; + } + + > div.item-container { + background: var(--bs-tertiary-bg, #f3f5f5); + color: var(--bs-tertiary-color, #94a5a6); + + padding: 18px 13px; + text-align: center; + + i { + font-size: 24px; + } + } + + &.add { + margin-top: 20px; + + a { + padding: 10px 20px; + display: block; + } + + } + } + } + } +} \ No newline at end of file diff --git a/plugins/rainlab/builder/assets/less/tabs.less b/plugins/rainlab/builder/assets/less/tabs.less new file mode 100644 index 0000000..a743dad --- /dev/null +++ b/plugins/rainlab/builder/assets/less/tabs.less @@ -0,0 +1,331 @@ +.builder-tabs { + > .tabs { + position: relative; + + .tab-control { + position: absolute; + display: block; + + &.inspector-trigger { + font-size: 14px; + padding-left: 5px; + padding-right: 5px; + cursor: pointer; + + span { + display: block; + width: 3px; + height: 3px; + margin-bottom: 2px; + background: #95a5a6; + + &:last-child { + margin-bottom: 0; + } + } + + &:hover, &.inspector-open { + span { + background: @link-color; + } + } + + &.global { + top: 5px; + right: 0; + padding-right: 10px; + background: @body-bg; + z-index: 110; + border-radius: 3px; + + > div { + width: 24px; + height: 24px; + border-radius: 3px; + background: @toolbar-bg; + padding-left: 10px; + padding-top: 5px; + + &:active { + background: @toolbar-focus-bg; + } + } + } + } + } + + > ul.tabs { + margin: 0; + padding-right: 50px; + list-style: none; + font-size: 0; + white-space: nowrap; + overflow: hidden; + position: relative; + + > li { + user-select: none; + display: inline-block; + font-size: 13px; + white-space: nowrap; + position: relative; + cursor: pointer; + + > div.tab-container { + position: relative; + color: @tab-color !important; + + > div { + transition: padding .1s; + position: relative; + } + } + + &:hover > div { + color: @tab-active-color !important; + } + + .tab-control { + display: none; + + &.close-btn { + font-size: 15px; + top: 7px; + right: 18px; + line-height: 15px; + height: 15px; + width: 15px; + text-align: center; + cursor: pointer; + color: #95a5a6; + + &:hover { + color: @link-color !important; + } + } + + &.inspector-trigger{ + right: 34px; + top: 10px; + } + } + + &.active { + > div.tab-container { + color: @tab-active-color !important; + } + + .tab-control { + display: block; + } + } + } + } + + > ul.panels { + padding: 0; + list-style: none; + + > li { + display: none; + + &.active { + display: block; + } + } + } + } + + &.primary { + > .tabs { + > ul.tabs { + padding: 0 40px 0 40px; + height: 31px; + + &:after { + position: absolute; + content: ''; + display: block; + height: 2px; + left: 0; + bottom: 0; + width: 100%; + // background: #bdc3c7; + background: transparent linear-gradient(90deg, #bdc3c7 90%, transparent 100%); + z-index: 106; + } + + > li { + bottom: -3px; + margin-left: -20px; + z-index: 105; + + > div.tab-container { + padding: 0 21px 0 21px; + height: 27px; + + > div { + padding: 5px 5px 0 5px; + background: white; + + > span { + position: relative; + top: -4px; + transition: top .1s; + } + } + } + + &.active { + z-index: 107; + color: var(--oc-tab-active-color); + + > div.tab-container { + + &:before, &:after { + content: ''; + display: block; + position: absolute; + top: 0; + height: 27px; + width: 21px; + background: transparent url(../images/tab.png) no-repeat; + } + + &:before { + left: 0; + background-position: 0 0; + } + + &:after { + right: 0; + background-position: -75px 0; + } + + > div { + padding-right: 30px; + border-top: 2px solid #bdc3c7; + + > span { + top: 0; + } + } + } + + &:before { + position: absolute; + content: ''; + display: block; + height: 3px; + left: 0; + bottom: 0; + width: 100%; + background: white; + } + } + + &.new-tab { + background: transparent url(../images/tab.png) no-repeat; + background-position: -24px 0; + width: 27px; + height: 22px; + + margin-left: -11px; + top: 4px; + position: relative; + cursor: pointer; + + &:hover { + background-position: -24px -32px; + } + } + } + } + } + } + + &.secondary { + > .tabs { + ul.tabs { + margin-left: 12px; + padding-left: 0; + + > li { + border-right: 1px solid #bdc3c7; + padding-right: 1px; + + > div.tab-container { + > div { + padding: 4px 10px; + + span { + font-size: 14px; + } + } + } + + .tab-control { + right: 23px; + top: 7px; + + &.close-btn { + right: 6px; + top: 5px; + } + } + + &.new-tab { + background: transparent; + border: 2px solid #e4e4e4; + width: 27px; + height: 22px; + left: 9px; + top: 7px; + position: relative; + cursor: pointer; + .border-radius(4px); + + &:hover { + background-color: #2581b8; + border-color: #2581b8; + } + } + + &.active { + padding-right: 10px; + + > div.tab-container { + > div { + color: var(--oc-builder-control-color); + padding-right: 30px; + } + } + } + } + } + } + } +} + +// html.gecko { +// .builder-tabs.primary > .tabs > ul.tabs > li { +// // Fixes a visual glitch in FireFox, noticed in v. 42 on Mac. +// bottom: -3px; +// > div.tab-container > div { +// padding-top: 5px; +// } +// } +// } + + +[data-bs-theme="dark"] { + .builder-tabs.primary > .tabs > ul.tabs > li.active:before, + .builder-tabs.primary > .tabs > ul.tabs > li > div.tab-container > div { + background: #202124; + } + + .builder-tabs.primary > .tabs > ul.tabs > li.active > div.tab-container:before, + .builder-tabs.primary > .tabs > ul.tabs > li.active > div.tab-container:after { + background-image: url(../images/tab-dark.png); + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexCodeOperations.php b/plugins/rainlab/builder/behaviors/IndexCodeOperations.php new file mode 100644 index 0000000..aee256c --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexCodeOperations.php @@ -0,0 +1,135 @@ +getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $widget = $this->makeBaseFormWidget($fileName, $options); + $this->vars['fileName'] = $fileName; + + $result = [ + 'tabTitle' => $this->getTabName($widget->model), + 'tabIcon' => 'icon-file-code-o', + 'tabId' => $this->getTabId($pluginCodeObj->toCode(), $fileName), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + return $result; + } + + /** + * onCodeSave + */ + public function onCodeSave() + { + $pluginCodeObj = new PluginCode(post('plugin_code')); + $pluginCode = $pluginCodeObj->toCode(); + + $fileName = post('fileName'); + + $data = array_only(post(), ['fileName', 'content']); + + $model = $this->loadModelFromPost(); + $model->fill($data); + $model->save(); + + Flash::success(Lang::get('rainlab.builder::lang.controller.saved')); + + $result = $this->controller->widget->codeList->onRefresh(); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($pluginCode, $fileName), + 'tabTitle' => $this->getTabName($model), + ]; + + return $result; + } + + /** + * getTabName + */ + protected function getTabName($model) + { + $pluginName = Lang::get($model->getModelPluginName()); + + return $pluginName.'/'.$model->fileName; + } + + /** + * getTabId + */ + protected function getTabId($pluginCode, $fileName) + { + return 'code-'.$pluginCode.'-'.$fileName; + } + + /** + * loadModelFromPost + */ + protected function loadModelFromPost() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $fileName = Input::get('fileName'); + + return $this->loadOrCreateBaseModel($fileName, $options); + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($fileName, $options = []) + { + $model = new CodeFileModel(); + + if (isset($options['pluginCode'])) { + $model->setPluginCode($options['pluginCode']); + } + + if (!$fileName) { + if ($currentPath = $this->controller->widget->codeList->getCurrentRelativePath()) { + $model->fileName = $currentPath . '/'; + } + return $model; + } + + $model->load($fileName); + + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexControllerOperations.php b/plugins/rainlab/builder/behaviors/IndexControllerOperations.php new file mode 100644 index 0000000..094bfaf --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexControllerOperations.php @@ -0,0 +1,198 @@ +getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $widget = $this->makeBaseFormWidget($controller, $options); + $this->vars['controller'] = $controller; + + $result = [ + 'tabTitle' => $this->getTabName($widget->model), + 'tabIcon' => 'icon-asterisk', + 'tabId' => $this->getTabId($pluginCodeObj->toCode(), $controller), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + return $result; + } + + /** + * onControllerCreate + */ + public function onControllerCreate() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $model = $this->loadOrCreateBaseModel(null, $options); + $model->fill(post()); + $model->save(); + + $this->vars['controller'] = $model->controller; + + $result = $this->controller->widget->controllerList->updateList(); + + if ($model->behaviors) { + // Create a new tab only for controllers with behaviors. + $widget = $this->makeBaseFormWidget($model->controller, $options); + + $tab = [ + 'tabTitle' => $this->getTabName($widget->model), + 'tabIcon' => 'icon-asterisk', + 'tabId' => $this->getTabId($pluginCodeObj->toCode(), $model->controller), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + $result = array_merge($result, $tab); + } + + $this->mergeRegistryDataIntoResult($result, $pluginCodeObj); + + return $result; + } + + /** + * onControllerSave + */ + public function onControllerSave() + { + $controller = Input::get('controller'); + + $model = $this->loadModelFromPost(); + $model->fill(post()); + $model->save(); + + Flash::success(Lang::get('rainlab.builder::lang.controller.saved')); + + $result['builderResponseData'] = []; + + return $result; + } + + /** + * onControllerShowCreatePopup + */ + public function onControllerShowCreatePopup() + { + $pluginCodeObj = $this->getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $this->baseFormConfigFile = '~/plugins/rainlab/builder/models/controllermodel/fields_new_controller.yaml'; + $widget = $this->makeBaseFormWidget(null, $options); + + return $this->makePartial('create-controller-popup-form', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]); + } + + /** + * getTabName + */ + protected function getTabName($model) + { + $pluginName = Lang::get($model->getModelPluginName()); + + return $pluginName.'/'.$model->controller; + } + + /** + * getTabId + */ + protected function getTabId($pluginCode, $controller) + { + return 'controller-'.$pluginCode.'-'.$controller; + } + + /** + * loadModelFromPost + */ + protected function loadModelFromPost() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $controller = Input::get('controller'); + + return $this->loadOrCreateBaseModel($controller, $options); + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($controller, $options = []) + { + $model = new ControllerModel(); + + if (isset($options['pluginCode'])) { + $model->setPluginCode($options['pluginCode']); + } + + if (!$controller) { + return $model; + } + + $model->load($controller); + return $model; + } + + /** + * mergeRegistryDataIntoResult + */ + protected function mergeRegistryDataIntoResult(&$result, $pluginCodeObj) + { + if (!array_key_exists('builderResponseData', $result)) { + $result['builderResponseData'] = []; + } + + $pluginCode = $pluginCodeObj->toCode(); + $result['builderResponseData']['registryData'] = [ + 'urls' => ControllerModel::getPluginRegistryData($pluginCode, null), + 'pluginCode' => $pluginCode + ]; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexDataRegistry.php b/plugins/rainlab/builder/behaviors/IndexDataRegistry.php new file mode 100644 index 0000000..3ca1a35 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexDataRegistry.php @@ -0,0 +1,60 @@ + $result]; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexDatabaseTableOperations.php b/plugins/rainlab/builder/behaviors/IndexDatabaseTableOperations.php new file mode 100644 index 0000000..4d301ab --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexDatabaseTableOperations.php @@ -0,0 +1,273 @@ +use_table_comments) { + return $config; + } + + $configMod = (array) $config; + + array_forget($configMod, 'tabs.fields.columns.columns.comment'); + + return (object) $configMod; + } + + /** + * onDatabaseTableCreateOrOpen + */ + public function onDatabaseTableCreateOrOpen() + { + $tableName = Input::get('table_name'); + $pluginCodeObj = $this->getPluginCode(); + + $widget = $this->makeBaseFormWidget($tableName); + + $this->vars['tableName'] = $tableName; + + $result = [ + 'tabTitle' => $this->getTabTitle($tableName), + 'tabIcon' => 'icon-hdd-o', + 'tabId' => $this->getTabId($tableName), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode(), + 'tableName' => $tableName + ]) + ]; + + return $result; + } + + /** + * onDatabaseTableValidateAndShowPopup + */ + public function onDatabaseTableValidateAndShowPopup() + { + $tableName = Input::get('table_name'); + + $model = $this->loadOrCreateBaseModel($tableName); + $model->fill($this->processColumnData(post())); + + $pluginCode = Request::input('plugin_code'); + $model->setPluginCode($pluginCode); + try { + $model->validate(); + } + catch (Exception $ex) { + throw new ApplicationException($ex->getMessage()); + } + + $migration = $model->generateCreateOrUpdateMigration(); + + if (!$migration) { + return $this->makePartial('migration-popup-form', [ + 'noChanges' => true + ]); + } + + return $this->makePartial('migration-popup-form', [ + 'form' => $this->makeMigrationFormWidget($migration), + 'operation' => $model->isNewModel() ? 'create' : 'update', + 'table' => $model->name, + 'pluginCode' => $pluginCode + ]); + } + + /** + * onDatabaseTableMigrationApply + */ + public function onDatabaseTableMigrationApply() + { + $pluginCode = new PluginCode(Request::input('plugin_code')); + $model = new MigrationModel(); + $model->setPluginCodeObj($pluginCode); + + $model->fill(post()); + + $operation = Input::get('operation'); + $table = Input::get('table'); + + $model->scriptFileName = 'builder_table_'.$operation.'_'.$table; + $model->makeScriptFileNameUnique(); + + $codeGenerator = new TableMigrationCodeGenerator(); + $model->code = $codeGenerator->wrapMigrationCode($model->scriptFileName, $model->code, $pluginCode); + + try { + $model->save(); + } + catch (Exception $ex) { + throw new ApplicationException($ex->getMessage()); + } + + $result = $this->controller->widget->databaseTableList->updateList(); + + $result = array_merge( + $result, + $this->controller->widget->versionList->refreshActivePlugin() + ); + + if ($operation === 'delete') { + $result['builderResponseData'] = [ + 'builderObjectName' => $table, + 'tabId' => $this->getTabId($table), + 'tabTitle' => $table, + 'tableName' => $table, + 'operation' => $operation, + 'pluginCode' => $pluginCode->toCode() + ]; + } + else { + $widget = $this->makeBaseFormWidget($table); + $this->vars['tableName'] = $table; + + $result['builderResponseData'] = [ + 'builderObjectName' => $table, + 'tabId' => $this->getTabId($table), + 'tabTitle' => $table, + 'tableName' => $table, + 'operation' => $operation, + 'pluginCode' => $pluginCode->toCode(), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $this->getPluginCode()->toCode(), + 'tableName' => $table + ]) + ]; + } + + return $result; + } + + /** + * onDatabaseTableShowDeletePopup + */ + public function onDatabaseTableShowDeletePopup() + { + $tableName = Input::get('table_name'); + + $model = $this->loadOrCreateBaseModel($tableName); + $pluginCode = Request::input('plugin_code'); + $model->setPluginCode($pluginCode); + + $migration = $model->generateDropMigration(); + + return $this->makePartial('migration-popup-form', [ + 'form' => $this->makeMigrationFormWidget($migration), + 'operation' => 'delete', + 'table' => $model->name, + 'pluginCode' => $pluginCode + ]); + } + + /** + * getTabTitle + */ + protected function getTabTitle($tableName) + { + if (!strlen($tableName)) { + return Lang::get('rainlab.builder::lang.database.tab_new_table'); + } + + return $tableName; + } + + /** + * getTabId + */ + protected function getTabId($tableName) + { + if (!strlen($tableName)) { + return 'databaseTable-'.uniqid(time()); + } + + return 'databaseTable-'.$tableName; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($tableName, $options = []) + { + $model = new DatabaseTableModel(); + + if (!$tableName) { + $model->name = $this->getPluginCode()->toDatabasePrefix().'_'; + return $model; + } + + $model->load($tableName); + return $model; + } + + /** + * makeMigrationFormWidget + */ + protected function makeMigrationFormWidget($migration) + { + $widgetConfig = $this->makeConfig($this->migrationFormConfigFile); + $widgetConfig->model = $migration; + $widgetConfig->alias = 'form_migration_'.uniqid(); + + $form = $this->makeWidget(\Backend\Widgets\Form::class, $widgetConfig); + $form->context = 'create'; + + return $form; + } + + /** + * processColumnData + */ + protected function processColumnData($postData) + { + if (!array_key_exists('columns', $postData)) { + return $postData; + } + + $booleanColumns = ['unsigned', 'allow_null', 'auto_increment', 'primary_key']; + foreach ($postData['columns'] as &$row) { + foreach ($row as $column => $value) { + if (in_array($column, $booleanColumns) && $value == 'false') { + $row[$column] = false; + } + } + } + + return $postData; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexImportsOperations.php b/plugins/rainlab/builder/behaviors/IndexImportsOperations.php new file mode 100644 index 0000000..20faf05 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexImportsOperations.php @@ -0,0 +1,192 @@ +getPluginCode(); + $pluginCode = $pluginCodeObj->toCode(); + $widget = $this->makeSelectionFormWidget($pluginCode); + + $result = [ + 'tabTitle' => $widget->model->getPluginName().'/'.__("Import"), + 'tabIcon' => 'icon-arrow-circle-down', + 'tabId' => $this->getTabId($pluginCode), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + return $result; + } + + /** + * onImportsSave + */ + public function onImportsShowConfirmPopup() + { + if (!post('blueprints')) { + throw new ApplicationException(__("There are no blueprints to import, please select a blueprint and try again.")); + } + + $pluginCodeObj = $this->getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $this->baseFormConfigFile = '~/plugins/rainlab/builder/models/importsmodel/fields_import.yaml'; + $widget = $this->makeBaseFormWidget(null, $options); + + return $this->makePartial('import-blueprints-popup-form', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]); + } + + /** + * onImportsSave + */ + public function onImportsSave() + { + if (post('delete_blueprint_data')) { + $confirmText = trim(strtolower(post('delete_blueprint_data_confirm'))); + if ($confirmText !== 'ok') { + throw new ApplicationException(__("Type OK in the field to confirm you want to destroy the existing blueprint data.")); + } + } + + $pluginCodeObj = new PluginCode(post('plugin_code')); + $pluginCode = $pluginCodeObj->toCode(); + + // Validate plugin code matches + $vectorCode = $this->controller->getBuilderActivePluginVector()->pluginCodeObj->toCode(); + if ($pluginCode !== $vectorCode) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.not_match')); + } + + $model = $this->loadOrCreateBaseModel($pluginCodeObj->toCode()); + + // Disable blueprints when finished + if (post('disable_blueprints')) { + $model->disableBlueprints = true; + } + + // Disable blueprints when finished + if (post('delete_blueprint_data')) { + $model->deleteBlueprintData = true; + } + + // Perform import + $model->setPluginCodeObj($pluginCodeObj); + $model->fill(post()); + $model->import(); + + // Migrate database + if (post('migrate_database')) { + VersionManager::instance()->updatePlugin($pluginCode); + } + + CacheHelper::instance()->clearBlueprintCache(); + + Flash::success(__("Import Complete")); + + $builderResponseData = [ + 'tabId' => $this->getTabId($pluginCode), + 'tabTitle' => $model->getPluginName().'/'.__("Import"), + ]; + + // Refresh everything + $result = $this->controller->setBuilderActivePlugin($pluginCode); + $result['builderResponseData'] = $builderResponseData; + + // Feature is nice to have, only supported in >3.3.9 + try { + PluginManager::instance()->reloadPlugins(); + BackendMenu::resetCache(); + + $result['mainMenu'] = $this->controller->makeLayoutPartial('mainmenu'); + $result['mainMenuLeft'] = $this->controller->makeLayoutPartial('mainmenu', ['isVerticalMenu'=>true]); + } + catch (Throwable $ex) {} + + return $result; + } + + /** + * onMigrateDatabase + */ + public function onMigrateDatabase() + { + $pluginCodeObj = new PluginCode(post('plugin_code')); + + VersionManager::instance()->updatePlugin($pluginCodeObj->toCode()); + + Flash::success(__("Migration Complete")); + } + + /** + * getTabId + */ + protected function getTabId($pluginCode) + { + return 'imports-'.$pluginCode; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($pluginCode, $options = []) + { + $model = new ImportsModel; + $model->loadPlugin($pluginCode); + return $model; + } + + /** + * makeBaseFormWidget + */ + protected function makeSelectionFormWidget($modelCode, $options = []) + { + if (!strlen($this->selectFormConfigFile)) { + throw new ApplicationException(sprintf('Base form configuration file is not specified for %s behavior', get_class($this))); + } + + $widgetConfig = $this->makeConfig($this->selectFormConfigFile); + $widgetConfig->model = $this->loadOrCreateBaseModel($modelCode, $options); + + return $this->makeWidget(\Backend\Widgets\Form::class, $widgetConfig); + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexLocalizationOperations.php b/plugins/rainlab/builder/behaviors/IndexLocalizationOperations.php new file mode 100644 index 0000000..eed43c7 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexLocalizationOperations.php @@ -0,0 +1,218 @@ +getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $widget = $this->makeBaseFormWidget($language, $options); + $this->vars['originalLanguage'] = $language; + + if ($widget->model->isNewModel()) { + $widget->model->initContent(); + } + + $result = [ + 'tabTitle' => $this->getTabName($widget->model), + 'tabIcon' => 'icon-globe', + 'tabId' => $this->getTabId($pluginCodeObj->toCode(), $language), + 'isNewRecord' => $widget->model->isNewModel(), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode(), + 'language' => $language, + 'defaultLanguage' => LocalizationModel::getDefaultLanguage() + ]) + ]; + + return $result; + } + + public function onLanguageSave() + { + $model = $this->loadOrCreateLocalizationFromPost(); + $model->fill(post()); + $model->save(false); + + Flash::success(Lang::get('rainlab.builder::lang.localization.saved')); + $result = $this->controller->widget->languageList->updateList(); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($model->getPluginCodeObj()->toCode(), $model->language), + 'tabTitle' => $this->getTabName($model), + 'language' => $model->language + ]; + + if ($model->language === LocalizationModel::getDefaultLanguage()) { + $pluginCode = $model->getPluginCodeObj()->toCode(); + + $registryData = [ + 'strings' => LocalizationModel::getPluginRegistryData($pluginCode, null), + 'sections' => LocalizationModel::getPluginRegistryData($pluginCode, 'sections'), + 'pluginCode' => $pluginCode + ]; + + $result['builderResponseData']['registryData'] = $registryData; + } + + return $result; + } + + public function onLanguageDelete() + { + $model = $this->loadOrCreateLocalizationFromPost(); + + $model->deleteModel(); + + return $this->controller->widget->languageList->updateList(); + } + + public function onLanguageShowCopyStringsPopup() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $language = trim(Input::get('original_language')); + + $languages = LocalizationModel::listPluginLanguages($pluginCodeObj); + + if (strlen($language)) { + $languages = array_diff($languages, [$language]); + } + + return $this->makePartial('copy-strings-popup-form', ['languages'=>$languages]); + } + + public function onLanguageCopyStringsFrom() + { + $sourceLanguage = Request::input('copy_from'); + $destinationText = Request::input('strings'); + + $model = new LocalizationModel(); + $model->setPluginCode(Request::input('plugin_code')); + + $responseData = $model->copyStringsFrom($destinationText, $sourceLanguage); + + return ['builderResponseData' => $responseData]; + } + + public function onLanguageLoadAddStringForm() + { + return [ + 'markup' => $this->makePartial('new-string-popup') + ]; + } + + public function onLanguageCreateString() + { + $stringKey = trim(Request::input('key')); + $stringValue = trim(Request::input('value')); + + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $pluginCode = $pluginCodeObj->toCode(); + $options = [ + 'pluginCode' => $pluginCode + ]; + + $defaultLanguage = LocalizationModel::getDefaultLanguage(); + if (LocalizationModel::languageFileExists($pluginCode, $defaultLanguage)) { + $model = $this->loadOrCreateBaseModel($defaultLanguage, $options); + } + else { + $model = LocalizationModel::initModel($pluginCode, $defaultLanguage); + } + + $newStringKey = $model->createStringAndSave($stringKey, $stringValue); + $pluginCode = $pluginCodeObj->toCode(); + + return [ + 'localizationData' => [ + 'key' => $newStringKey, + 'value' => $stringValue + ], + 'registryData' => [ + 'strings' => LocalizationModel::getPluginRegistryData($pluginCode, null), + 'sections' => LocalizationModel::getPluginRegistryData($pluginCode, 'sections') + ] + ]; + } + + public function onLanguageGetStrings() + { + $model = $this->loadOrCreateLocalizationFromPost(); + + return ['builderResponseData' => [ + 'strings' => $model ? $model->strings : null + ]]; + } + + protected function loadOrCreateLocalizationFromPost() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $originalLanguage = Input::get('original_language'); + + return $this->loadOrCreateBaseModel($originalLanguage, $options); + } + + protected function getTabName($model) + { + $pluginName = Lang::get($model->getModelPluginName()); + + if (!strlen($model->language)) { + return $pluginName.'/'.Lang::get('rainlab.builder::lang.localization.tab_new_language'); + } + + return $pluginName.'/'.$model->language; + } + + protected function getTabId($pluginCode, $language) + { + if (!strlen($language)) { + return 'localization-'.$pluginCode.'-'.uniqid(time()); + } + + return 'localization-'.$pluginCode.'-'.$language; + } + + protected function loadOrCreateBaseModel($language, $options = []) + { + $model = new LocalizationModel(); + + if (isset($options['pluginCode'])) { + $model->setPluginCode($options['pluginCode']); + } + + if (!$language) { + return $model; + } + + $model->load($language); + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexMenusOperations.php b/plugins/rainlab/builder/behaviors/IndexMenusOperations.php new file mode 100644 index 0000000..df09732 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexMenusOperations.php @@ -0,0 +1,101 @@ +getPluginCode(); + + $pluginCode = $pluginCodeObj->toCode(); + $widget = $this->makeBaseFormWidget($pluginCode); + + $result = [ + 'tabTitle' => $widget->model->getPluginName().'/'.Lang::get('rainlab.builder::lang.menu.tab'), + 'tabIcon' => 'icon-location-arrow', + 'tabId' => $this->getTabId($pluginCode), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + return $result; + } + + /** + * onMenusSave + */ + public function onMenusSave() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + + $pluginCode = $pluginCodeObj->toCode(); + $model = $this->loadOrCreateBaseModel($pluginCodeObj->toCode()); + $model->setPluginCodeObj($pluginCodeObj); + $model->fill(post()); + $model->save(); + + Flash::success(Lang::get('rainlab.builder::lang.menu.saved')); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($pluginCode), + 'tabTitle' => $model->getPluginName().'/'.Lang::get('rainlab.builder::lang.menu.tab'), + ]; + + // Feature is nice to have, only supported in >3.3.9 + try { + PluginManager::instance()->reloadPlugins(); + BackendMenu::resetCache(); + + $result['mainMenu'] = $this->controller->makeLayoutPartial('mainmenu'); + $result['mainMenuLeft'] = $this->controller->makeLayoutPartial('mainmenu', ['isVerticalMenu'=>true]); + } + catch (Throwable $ex) {} + + return $result; + } + + /** + * getTabId + */ + protected function getTabId($pluginCode) + { + return 'menus-'.$pluginCode; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($pluginCode, $options = []) + { + $model = new MenusModel(); + + $model->loadPlugin($pluginCode); + + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexModelFormOperations.php b/plugins/rainlab/builder/behaviors/IndexModelFormOperations.php new file mode 100644 index 0000000..1695c3b --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexModelFormOperations.php @@ -0,0 +1,313 @@ +alias = 'defaultFormBuilder'; + $formBuilder->bindToController(); + } + + /** + * onModelFormCreateOrOpen + */ + public function onModelFormCreateOrOpen() + { + $fileName = Input::get('file_name'); + $modelClass = Input::get('model_class'); + + $pluginCodeObj = $this->getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode(), + 'modelClass' => $modelClass + ]; + + $widget = $this->makeBaseFormWidget($fileName, $options); + $this->vars['fileName'] = $fileName; + + $result = [ + 'tabTitle' => $widget->model->getDisplayName(Lang::get('rainlab.builder::lang.form.tab_new_form')), + 'tabIcon' => 'icon-check-square', + 'tabId' => $this->getTabId($modelClass, $fileName), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode(), + 'fileName' => $fileName, + 'modelClass' => $modelClass + ]) + ]; + + return $result; + } + + /** + * onModelFormSave + */ + public function onModelFormSave() + { + $model = $this->loadOrCreateFormFromPost(); + + $model->fill(post()); + $model->save(); + + $result = $this->controller->widget->modelList->updateList(); + + Flash::success(Lang::get('rainlab.builder::lang.form.saved')); + + $modelClass = Input::get('model_class'); + $result['builderResponseData'] = [ + 'builderObjectName' => $model->fileName, + 'tabId' => $this->getTabId($modelClass, $model->fileName), + 'tabTitle' => $model->getDisplayName(Lang::get('rainlab.builder::lang.form.tab_new_form')) + ]; + + $this->mergeRegistryDataIntoResult($result, $model, $modelClass); + + return $result; + } + + /** + * onModelFormDelete + */ + public function onModelFormDelete() + { + $model = $this->loadOrCreateFormFromPost(); + + $model->deleteModel(); + + $result = $this->controller->widget->modelList->updateList(); + + $modelClass = Input::get('model_class'); + $this->mergeRegistryDataIntoResult($result, $model, $modelClass); + + return $result; + } + + /** + * onModelFormGetModelFields + */ + public function onModelFormGetModelFields() + { + $columnNames = ModelModel::getModelFields($this->getPluginCode(), Input::get('model_class')); + $asPlainList = Input::get('as_plain_list'); + + $result = []; + foreach ($columnNames as $columnName) { + if (!$asPlainList) { + $result[] = [ + 'title' => $columnName, + 'value' => $columnName + ]; + } + else { + $result[$columnName] = $columnName; + } + } + + return [ + 'responseData' => [ + 'options' => $result + ] + ]; + } + + /** + * onModelShowAddDatabaseFieldsPopup + */ + public function onModelShowAddDatabaseFieldsPopup() + { + $columns = ModelModel::getModelColumnsAndTypes($this->getPluginCode(), Input::get('model_class')); + $config = $this->makeConfig($this->getAddDatabaseFieldsDataTableConfig()); + + $field = new FormField('add_database_fields_datatable', 'add_database_fields_datatable'); + $field->value = $this->getAddDatabaseFieldsDataTableValue($columns); + + $datatable = new DataTable($this->controller, $field, $config); + $datatable->alias = 'add_database_fields_datatable'; + $datatable->bindToController(); + + return $this->makePartial('add-database-fields-popup-form', [ + 'datatable' => $datatable, + 'pluginCode' => $this->getPluginCode()->toCode(), + ]); + } + + /** + * loadOrCreateFormFromPost + */ + protected function loadOrCreateFormFromPost() + { + $pluginCode = Request::input('plugin_code'); + $modelClass = Input::get('model_class'); + $fileName = Input::get('file_name'); + + $options = [ + 'pluginCode' => $pluginCode, + 'modelClass' => $modelClass + ]; + + return $this->loadOrCreateBaseModel($fileName, $options); + } + + /** + * getTabId + */ + protected function getTabId($modelClass, $fileName) + { + if (!strlen($fileName)) { + return 'modelForm-'.uniqid(time()); + } + + return 'modelForm-'.$modelClass.'-'.$fileName; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($fileName, $options = []) + { + $model = new ModelFormModel(); + + if (isset($options['pluginCode']) && isset($options['modelClass'])) { + $model->setPluginCode($options['pluginCode']); + $model->setModelClassName($options['modelClass']); + } + + if (!$fileName) { + $model->initDefaults(); + + return $model; + } + + $model->loadForm($fileName); + return $model; + } + + /** + * mergeRegistryDataIntoResult + */ + protected function mergeRegistryDataIntoResult(&$result, $model, $modelClass) + { + if (!array_key_exists('builderResponseData', $result)) { + $result['builderResponseData'] = []; + } + + $fullClassName = $model->getPluginCodeObj()->toPluginNamespace().'\\Models\\'.$modelClass; + $pluginCode = $model->getPluginCodeObj()->toCode(); + $result['builderResponseData']['registryData'] = [ + 'forms' => ModelFormModel::getPluginRegistryData($pluginCode, $modelClass), + 'pluginCode' => $pluginCode, + 'modelClass' => $fullClassName + ]; + } + + /** + * getAddDatabaseFieldsDataTableConfig returns the configuration for the DataTable widget + * that is used in the "add database fields" popup. + * + * @return array + */ + protected function getAddDatabaseFieldsDataTableConfig() + { + // Get all registered controls and build an array that uses the control types as key and value for each entry. + $controls = ControlLibrary::instance()->listControls(); + + // Fix for error throwing when using non-english language + $standard = trans('rainlab.builder::lang.form.control_group_standard'); + $widgets = trans('rainlab.builder::lang.form.control_group_widgets'); + $fieldTypes = array_merge(array_keys($controls[$standard]), array_keys($controls[$widgets])); + $options = array_combine($fieldTypes, $fieldTypes); + + return [ + 'toolbar' => false, + 'columns' => [ + 'add' => [ + 'title' => 'rainlab.builder::lang.common.add', + 'type' => 'checkbox', + 'width' => '50px', + ], + 'column' => [ + 'title' => 'rainlab.builder::lang.database.column_name_name', + 'readOnly' => true, + ], + 'label' => [ + 'title' => 'rainlab.builder::lang.list.column_name_label', + ], + 'type' => [ + 'title' => 'rainlab.builder::lang.form.control_widget_type', + 'type' => 'dropdown', + 'options' => $options, + ], + ], + ]; + } + + /** + * getAddDatabaseFieldsDataTableValue returns the initial value for the DataTable widget that + * is used in the "add database columns" popup. + * + * @param array $columns + * @return array + */ + protected function getAddDatabaseFieldsDataTableValue(array $columns) + { + // Map database column types to widget types. + $typeMap = [ + 'string' => 'text', + 'integer' => 'number', + 'text' => 'textarea', + 'timestamp' => 'datepicker', + 'smallInteger' => 'number', + 'bigInteger' => 'number', + 'date' => 'datepicker', + 'time' => 'datepicker', + 'dateTime' => 'datepicker', + 'binary' => 'checkbox', + 'boolean' => 'checkbox', + 'decimal' => 'number', + 'double' => 'number', + ]; + + return array_map(function($column) use ($typeMap) { + return [ + 'column' => $column['name'], + 'label' => str_replace('_', ' ', ucfirst($column['name'])), + 'type' => $typeMap[$column['type']] ?? $column['type'], + 'add' => false, + ]; + }, $columns); + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexModelListOperations.php b/plugins/rainlab/builder/behaviors/IndexModelListOperations.php new file mode 100644 index 0000000..95d8a2e --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexModelListOperations.php @@ -0,0 +1,223 @@ +tabs, 'fields.columns.columns.type.options'); + + $pluginColumns = PluginManager::instance()->getRegistrationMethodValues('registerListColumnTypes'); + foreach ($pluginColumns as $customColumns) { + foreach (array_keys($customColumns) as $customColumn) { + $typeOptions[$customColumn] = __(Str::studly($customColumn)); + } + } + + array_set($config->tabs, 'fields.columns.columns.type.options', $typeOptions); + return $config; + } + + /** + * onModelListCreateOrOpen + */ + public function onModelListCreateOrOpen() + { + $fileName = Input::get('file_name'); + $modelClass = Input::get('model_class'); + + $pluginCodeObj = $this->getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode(), + 'modelClass' => $modelClass + ]; + + $widget = $this->makeBaseFormWidget($fileName, $options); + $this->vars['fileName'] = $fileName; + + $result = [ + 'tabTitle' => $widget->model->getDisplayName(Lang::get('rainlab.builder::lang.list.tab_new_list')), + 'tabIcon' => 'icon-list', + 'tabId' => $this->getTabId($modelClass, $fileName), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode(), + 'fileName' => $fileName, + 'modelClass' => $modelClass + ]) + ]; + + return $result; + } + + /** + * onModelListSave + */ + public function onModelListSave() + { + $model = $this->loadOrCreateListFromPost(); + $model->fill(post()); + $model->save(); + + $result = $this->controller->widget->modelList->updateList(); + + Flash::success(Lang::get('rainlab.builder::lang.list.saved')); + + $modelClass = Input::get('model_class'); + $result['builderResponseData'] = [ + 'builderObjectName' => $model->fileName, + 'tabId' => $this->getTabId($modelClass, $model->fileName), + 'tabTitle' => $model->getDisplayName(Lang::get('rainlab.builder::lang.list.tab_new_list')) + ]; + + $this->mergeRegistryDataIntoResult($result, $model, $modelClass); + + return $result; + } + + /** + * onModelListDelete + */ + public function onModelListDelete() + { + $model = $this->loadOrCreateListFromPost(); + + $model->deleteModel(); + + $result = $this->controller->widget->modelList->updateList(); + + $modelClass = Input::get('model_class'); + $this->mergeRegistryDataIntoResult($result, $model, $modelClass); + + return $result; + } + + /** + * onModelListGetModelFields + */ + public function onModelListGetModelFields() + { + $columnNames = ModelModel::getModelFields($this->getPluginCode(), Input::get('model_class')); + + $result = []; + foreach ($columnNames as $columnName) { + $result[] = [ + 'title' => $columnName, + 'value' => $columnName + ]; + } + + return [ + 'responseData' => [ + 'options' => $result + ] + ]; + } + + /** + * onModelListLoadDatabaseColumns + */ + public function onModelListLoadDatabaseColumns() + { + $columns = ModelModel::getModelColumnsAndTypes($this->getPluginCode(), Input::get('model_class')); + + return [ + 'responseData' => [ + 'columns' => $columns + ] + ]; + } + + /** + * loadOrCreateListFromPost + */ + protected function loadOrCreateListFromPost() + { + $pluginCode = Request::input('plugin_code'); + $modelClass = Input::get('model_class'); + $fileName = Input::get('file_name'); + + $options = [ + 'pluginCode' => $pluginCode, + 'modelClass' => $modelClass + ]; + + return $this->loadOrCreateBaseModel($fileName, $options); + } + + /** + * getTabId + */ + protected function getTabId($modelClass, $fileName) + { + if (!strlen($fileName)) { + return 'modelForm-'.uniqid(time()); + } + + return 'modelList-'.$modelClass.'-'.$fileName; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($fileName, $options = []) + { + $model = new ModelListModel(); + + if (isset($options['pluginCode']) && isset($options['modelClass'])) { + $model->setPluginCode($options['pluginCode']); + $model->setModelClassName($options['modelClass']); + } + + if (!$fileName) { + $model->initDefaults(); + + return $model; + } + + $model->loadForm($fileName); + return $model; + } + + /** + * mergeRegistryDataIntoResult + */ + protected function mergeRegistryDataIntoResult(&$result, $model, $modelClass) + { + if (!array_key_exists('builderResponseData', $result)) { + $result['builderResponseData'] = []; + } + + $fullClassName = $model->getPluginCodeObj()->toPluginNamespace().'\\Models\\'.$modelClass; + $pluginCode = $model->getPluginCodeObj()->toCode(); + $result['builderResponseData']['registryData'] = [ + 'lists' => ModelListModel::getPluginRegistryData($pluginCode, $modelClass), + 'pluginCode' => $pluginCode, + 'modelClass' => $fullClassName + ]; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexModelOperations.php b/plugins/rainlab/builder/behaviors/IndexModelOperations.php new file mode 100644 index 0000000..3ea346b --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexModelOperations.php @@ -0,0 +1,78 @@ +getPluginCode(); + + try { + $widget = $this->makeBaseFormWidget(null); + $this->vars['form'] = $widget; + $widget->model->setPluginCodeObj($pluginCodeObj); + $this->vars['pluginCode'] = $pluginCodeObj->toCode(); + } + catch (ApplicationException $ex) { + $this->vars['errorMessage'] = $ex->getMessage(); + } + + return $this->makePartial('model-popup-form'); + } + + /** + * onModelSave + */ + public function onModelSave() + { + $pluginCode = Request::input('plugin_code'); + + $model = $this->loadOrCreateBaseModel(null); + $model->setPluginCode($pluginCode); + + $model->fill(post()); + $model->save(); + + $result = $this->controller->widget->modelList->updateList(); + + $builderResponseData = [ + 'registryData' => [ + 'models' => ModelModel::getPluginRegistryData($pluginCode, null), + 'pluginCode' => $pluginCode + ] + ]; + + $result['builderResponseData'] = $builderResponseData; + + return $result; + } + + /** + * loadOrCreateBaseModel + */ + protected function loadOrCreateBaseModel($className, $options = []) + { + // Editing model is not supported, always return + // a new object. + + return new ModelModel(); + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexPermissionsOperations.php b/plugins/rainlab/builder/behaviors/IndexPermissionsOperations.php new file mode 100644 index 0000000..b0a9778 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexPermissionsOperations.php @@ -0,0 +1,76 @@ +getPluginCode(); + + $pluginCode = $pluginCodeObj->toCode(); + $widget = $this->makeBaseFormWidget($pluginCode); + + $result = [ + 'tabTitle' => Lang::get($widget->model->getPluginName()).'/'.Lang::get('rainlab.builder::lang.permission.tab'), + 'tabIcon' => 'icon-unlock-alt', + 'tabId' => $this->getTabId($pluginCode), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode() + ]) + ]; + + return $result; + } + + public function onPermissionsSave() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + + $pluginCode = $pluginCodeObj->toCode(); + $model = $this->loadOrCreateBaseModel($pluginCodeObj->toCode()); + $model->setPluginCodeObj($pluginCodeObj); + $model->fill(post()); + $model->save(); + + Flash::success(Lang::get('rainlab.builder::lang.permission.saved')); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($pluginCode), + 'tabTitle' => $model->getPluginName().'/'.Lang::get('rainlab.builder::lang.permission.tab'), + 'pluginCode' => $pluginCode + ]; + + return $result; + } + + protected function getTabId($pluginCode) + { + return 'permissions-'.$pluginCode; + } + + protected function loadOrCreateBaseModel($pluginCode, $options = []) + { + $model = new PermissionsModel(); + + $model->loadPlugin($pluginCode); + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexPluginOperations.php b/plugins/rainlab/builder/behaviors/IndexPluginOperations.php new file mode 100644 index 0000000..5f52207 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexPluginOperations.php @@ -0,0 +1,89 @@ +vars['form'] = $this->makeBaseFormWidget($pluginCode); + $this->vars['pluginCode'] = $pluginCode; + } + catch (ApplicationException $ex) { + $this->vars['errorMessage'] = $ex->getMessage(); + } + + return $this->makePartial('plugin-popup-form'); + } + + public function onPluginSave() + { + $pluginCode = Input::get('pluginCode'); + + $model = $this->loadOrCreateBaseModel($pluginCode); + $model->fill(post()); + $model->save(); + + if (!$pluginCode) { + $result = []; + + $result['responseData'] = [ + 'pluginCode' => $model->getPluginCode(), + 'isNewPlugin' => 1 + ]; + + return $result; + } else { + $result = []; + + $result['responseData'] = [ + 'pluginCode' => $model->getPluginCode() + ]; + + return array_merge($result, $this->controller->updatePluginList()); + } + } + + public function onPluginSetActive() + { + $pluginCode = Input::get('pluginCode'); + $updatePluginList = Input::get('updatePluginList'); + + $result = $this->controller->setBuilderActivePlugin($pluginCode, false); + + if ($updatePluginList) { + $result = array_merge($result, $this->controller->updatePluginList()); + } + + $result['responseData'] = ['pluginCode'=>$pluginCode]; + + return $result; + } + + protected function loadOrCreateBaseModel($pluginCode, $options = []) + { + $model = new PluginBaseModel; + + if (!$pluginCode) { + $model->initDefaults(); + return $model; + } + + $model->loadPlugin($pluginCode); + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/IndexVersionsOperations.php b/plugins/rainlab/builder/behaviors/IndexVersionsOperations.php new file mode 100644 index 0000000..30fc8b0 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/IndexVersionsOperations.php @@ -0,0 +1,178 @@ +getPluginCode(); + + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $widget = $this->makeBaseFormWidget($versionNumber, $options); + $this->vars['originalVersion'] = $versionNumber; + + if ($widget->model->isNewModel()) { + $versionType = Input::get('version_type'); + $widget->model->initVersion($versionType); + } + + $result = [ + 'tabTitle' => $this->getTabName($versionNumber, $widget->model), + 'tabIcon' => 'icon-code-fork', + 'tabId' => $this->getTabId($pluginCodeObj->toCode(), $versionNumber), + 'isNewRecord' => $widget->model->isNewModel(), + 'tab' => $this->makePartial('tab', [ + 'form' => $widget, + 'pluginCode' => $pluginCodeObj->toCode(), + 'originalVersion' => $versionNumber + ]) + ]; + + return $result; + } + + public function onVersionSave() + { + $model = $this->loadOrCreateListFromPost(); + $model->fill(post()); + $model->save(false); + + Flash::success(Lang::get('rainlab.builder::lang.version.saved')); + $result = $this->controller->widget->versionList->updateList(); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($model->getPluginCodeObj()->toCode(), $model->version), + 'tabTitle' => $this->getTabName($model->version, $model), + 'savedVersion' => $model->version, + 'isApplied' => $model->isApplied() + ]; + + return $result; + } + + public function onVersionDelete() + { + $model = $this->loadOrCreateListFromPost(); + + $model->deleteModel(); + + return $this->controller->widget->versionList->updateList(); + } + + public function onVersionApply() + { + // Save the version before applying it + // + $model = $this->loadOrCreateListFromPost(); + $model->fill(post()); + $model->save(false); + + // Apply the version + // + $model->apply(); + + Flash::success(Lang::get('rainlab.builder::lang.version.applied')); + $result = $this->controller->widget->versionList->updateList(); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($model->getPluginCodeObj()->toCode(), $model->version), + 'tabTitle' => $this->getTabName($model->version, $model), + 'savedVersion' => $model->version + ]; + + return $result; + } + + public function onVersionRollback() + { + // Save the version before rolling it back + // + $model = $this->loadOrCreateListFromPost(); + $model->fill(post()); + $model->save(false); + + // Rollback the version + // + $model->rollback(); + + Flash::success(Lang::get('rainlab.builder::lang.version.rolled_back')); + $result = $this->controller->widget->versionList->updateList(); + + $result['builderResponseData'] = [ + 'tabId' => $this->getTabId($model->getPluginCodeObj()->toCode(), $model->version), + 'tabTitle' => $this->getTabName($model->version, $model), + 'savedVersion' => $model->version + ]; + + return $result; + } + + protected function loadOrCreateListFromPost() + { + $pluginCodeObj = new PluginCode(Request::input('plugin_code')); + $options = [ + 'pluginCode' => $pluginCodeObj->toCode() + ]; + + $versionNumber = Input::get('original_version'); + + return $this->loadOrCreateBaseModel($versionNumber, $options); + } + + protected function getTabName($version, $model) + { + $pluginName = Lang::get($model->getModelPluginName()); + + if (!strlen($version)) { + return $pluginName.'/'.Lang::get('rainlab.builder::lang.version.tab_new_version'); + } + + return $pluginName.'/v'.$version; + } + + protected function getTabId($pluginCode, $version) + { + if (!strlen($version)) { + return 'version-'.$pluginCode.'-'.uniqid(time()); + } + + return 'version-'.$pluginCode.'-'.$version; + } + + protected function loadOrCreateBaseModel($versionNumber, $options = []) + { + $model = new MigrationModel(); + + if (isset($options['pluginCode'])) { + $model->setPluginCode($options['pluginCode']); + } + + if (!$versionNumber) { + return $model; + } + + $model->load($versionNumber); + return $model; + } +} diff --git a/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_tab.php new file mode 100644 index 0000000..397bda3 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_tab.php @@ -0,0 +1,10 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'code', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + diff --git a/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_toolbar.php new file mode 100644 index 0000000..bc46bbd --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexcodeoperations/partials/_toolbar.php @@ -0,0 +1,10 @@ + diff --git a/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_create-controller-popup-form.php b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_create-controller-popup-form.php new file mode 100644 index 0000000..0f2cfb1 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_create-controller-popup-form.php @@ -0,0 +1,26 @@ +'controller:cmdCreateController', + 'data-plugin-code' => $pluginCode +]) ?> + + + + + diff --git a/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_tab.php new file mode 100644 index 0000000..7e10672 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_tab.php @@ -0,0 +1,11 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'controller', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_toolbar.php new file mode 100644 index 0000000..c520b12 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexcontrolleroperations/partials/_toolbar.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_migration-popup-form.php b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_migration-popup-form.php new file mode 100644 index 0000000..c0f3675 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_migration-popup-form.php @@ -0,0 +1,49 @@ +'databaseTable:cmdSaveMigration', + 'id'=>'builderTableMigrationPopup' +]) ?> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_tab.php new file mode 100644 index 0000000..2cb010c --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_tab.php @@ -0,0 +1,17 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'database', + 'onsubmit' => 'return false', + 'data-lang-add-id' => e(trans('rainlab.builder::lang.database.btn_add_id')), + 'data-lang-add-timestamps' => e(trans('rainlab.builder::lang.database.btn_add_timestamps')), + 'data-lang-add-soft-delete' => e(trans('rainlab.builder::lang.database.btn_add_soft_deleting')), + 'data-lang-id-exists' => e(trans('rainlab.builder::lang.database.id_exists')), + 'data-lang-timestamps-exist' => e(trans('rainlab.builder::lang.database.timestamps_exist')), + 'data-lang-soft-deleting-exist' => e(trans('rainlab.builder::lang.database.soft_deleting_exist')), +]) ?> + render() ?> + + + diff --git a/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_toolbar.php new file mode 100644 index 0000000..51cb08e --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_toolbar.php @@ -0,0 +1,17 @@ +
    + + + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_import-blueprints-popup-form.php b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_import-blueprints-popup-form.php new file mode 100644 index 0000000..e094388 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_import-blueprints-popup-form.php @@ -0,0 +1,26 @@ +'imports:cmdSaveImports', + 'data-plugin-code' => $pluginCode +]) ?> + + + + + diff --git a/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_tab.php new file mode 100644 index 0000000..2361f9e --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_tab.php @@ -0,0 +1,15 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'imports', + 'onsubmit' => 'return false' +]) ?> + + render() ?> + + + + + + diff --git a/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_toolbar.php new file mode 100644 index 0000000..e15fba8 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indeximportsoperations/partials/_toolbar.php @@ -0,0 +1,17 @@ + diff --git a/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_copy-strings-popup-form.php b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_copy-strings-popup-form.php new file mode 100644 index 0000000..faca9d7 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_copy-strings-popup-form.php @@ -0,0 +1,41 @@ +'localization:cmdCopyMissingStrings' +]) ?> + + + + + diff --git a/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_new-string-popup.php b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_new-string-popup.php new file mode 100644 index 0000000..55c7437 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_new-string-popup.php @@ -0,0 +1,42 @@ +'return false' +]) ?> + + + + + diff --git a/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_tab.php new file mode 100644 index 0000000..7b68bb2 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_tab.php @@ -0,0 +1,15 @@ + 'layout hide-secondary-tabs', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-new-string-message' => e(trans('rainlab.builder::lang.localization.new_string_warning')), + 'data-structure-mismatch' => e(trans('rainlab.builder::lang.localization.structure_mismatch')), + 'data-entity' => 'localization', + 'data-default-language' => e($defaultLanguage), + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_toolbar.php new file mode 100644 index 0000000..85b3e7c --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexlocalizationoperations/partials/_toolbar.php @@ -0,0 +1,27 @@ +
    + + + + + + + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_tab.php new file mode 100644 index 0000000..db93b35 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_tab.php @@ -0,0 +1,11 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'menus', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_toolbar.php new file mode 100644 index 0000000..ba880c5 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmenusoperations/partials/_toolbar.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_add-database-fields-popup-form.php b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_add-database-fields-popup-form.php new file mode 100644 index 0000000..7f7d672 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_add-database-fields-popup-form.php @@ -0,0 +1,28 @@ +'modelForm:cmdAddDatabaseFields' +]) ?> + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_tab.php new file mode 100644 index 0000000..ae1ba27 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_tab.php @@ -0,0 +1,13 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'models', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_toolbar.php new file mode 100644 index 0000000..5729ea0 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodelformoperations/partials/_toolbar.php @@ -0,0 +1,27 @@ +
    + + + + + + + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_tab.php new file mode 100644 index 0000000..aa1139f --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_tab.php @@ -0,0 +1,16 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'models', + 'onsubmit' => 'return false', + 'data-sub-entity' => 'model-list', + 'data-lang-add-database-columns' => e(trans('rainlab.builder::lang.list.btn_add_database_columns')), + 'data-lang-all-database-columns-exist' => e(trans('rainlab.builder::lang.list.all_database_columns_exist')), +]) ?> + render() ?> + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_toolbar.php new file mode 100644 index 0000000..eb8ecdf --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodellistoperations/partials/_toolbar.php @@ -0,0 +1,17 @@ +
    + + + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexmodeloperations/partials/_model-popup-form.php b/plugins/rainlab/builder/behaviors/indexmodeloperations/partials/_model-popup-form.php new file mode 100644 index 0000000..f244d31 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexmodeloperations/partials/_model-popup-form.php @@ -0,0 +1,32 @@ +'model:cmdApplyModelSettings' +]) ?> + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_tab.php new file mode 100644 index 0000000..bea6379 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_tab.php @@ -0,0 +1,11 @@ + 'layout', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'permissions', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_toolbar.php new file mode 100644 index 0000000..6e416a5 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexpermissionsoperations/partials/_toolbar.php @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-popup-form.php b/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-popup-form.php new file mode 100644 index 0000000..54492cd --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-popup-form.php @@ -0,0 +1,33 @@ +'plugin:cmdApplyPluginSettings' +]) ?> + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-update-hint.php b/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-update-hint.php new file mode 100644 index 0000000..0feedb0 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-update-hint.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_tab.php b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_tab.php new file mode 100644 index 0000000..ceed210 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_tab.php @@ -0,0 +1,40 @@ + 'layout hide-secondary-tabs', + 'data-change-monitor' => 'true', + 'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')), + 'data-entity' => 'versions', + 'onsubmit' => 'return false' +]) ?> + render() ?> + + + + + + + + + + + + + diff --git a/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_toolbar.php b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_toolbar.php new file mode 100644 index 0000000..a44d9ee --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_toolbar.php @@ -0,0 +1,33 @@ +
    + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_version-hint-block.php b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_version-hint-block.php new file mode 100644 index 0000000..e9eff67 --- /dev/null +++ b/plugins/rainlab/builder/behaviors/indexversionsoperations/partials/_version-hint-block.php @@ -0,0 +1,28 @@ + +
    + + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/BehaviorDesignTimeProviderBase.php b/plugins/rainlab/builder/classes/BehaviorDesignTimeProviderBase.php new file mode 100644 index 0000000..2121cff --- /dev/null +++ b/plugins/rainlab/builder/classes/BehaviorDesignTimeProviderBase.php @@ -0,0 +1,33 @@ +sourceModel = $source; + } + + /** + * generate + */ + public function inspect($blueprint) + { + $this->setBlueprintContext($blueprint); + + $result = [ + 'controllerFile' => null, + 'modelFiles' => [], + 'migrationFiles' => [], + 'errorMessage' => null + ]; + + try { + if ($model = $this->makeControllerModel()) { + $result['controllerFile'] = $model->getControllerFilePath(); + } + + if ($model = $this->makeModelModel()) { + $result['modelFiles'][] = $model->getModelFilePath(); + } + + foreach ($this->makeExpandoModels(true) as $model) { + $result['modelFiles'][] = $model->getModelFilePath(); + } + + $result['migrationFiles'] = array_keys($this->inspectMigrations()); + } + catch (Throwable $ex) { + $result['errorMessage'] = $ex->getMessage(); + } + + return $result; + } + + /** + * generate + */ + public function generate() + { + $this->templateVars = []; + $this->filesGenerated = []; + $this->filesValidated = []; + $this->blueprintFiles = []; + $this->migrationScripts = []; + $this->sourceBlueprints = []; + + $this->loadSourceBlueprints(); + $this->validateNavigation(); + + // Validate + foreach ($this->sourceBlueprints as $blueprint) { + $this->setBlueprintContext($blueprint); + $this->validateModel(); + $this->validateExpandoModels(); + $this->validateController(); + $this->validatePermission(); + } + + // Generate + try { + foreach ($this->sourceBlueprints as $blueprint) { + $this->setBlueprintContext($blueprint); + $this->generateMigrations(); + $this->generateModel(); + $this->generateExpandoModels(); + $this->generateController(); + $this->generatePermission(); + + $this->blueprintFiles[] = $blueprint->getFilePath(); + } + } + catch (Throwable $ex) { + $this->rollback(); + throw $ex; + } + + $this->generateNavigation(); + $this->generateVersionUpdate(); + + if ($this->sourceModel->deleteBlueprintData) { + $this->deleteGeneratedBlueprintData(); + } + + if ($this->sourceModel->disableBlueprints) { + $this->disableGeneratedBlueprints(); + } + } + + /** + * loadSourceBlueprints + */ + protected function loadSourceBlueprints() + { + $blueprintLib = TailorBlueprintLibrary::instance(); + + foreach ($this->sourceModel->blueprints as $uuid => $config) { + $blueprint = $blueprintLib->getBlueprintObject($uuid); + if ($blueprint) { + $this->sourceBlueprints[$uuid] = $blueprint; + } + } + } + + /** + * setBlueprintContext + */ + protected function setBlueprintContext($blueprint) + { + $config = $this->sourceModel->blueprints[$blueprint->uuid] ?? []; + + $this->sourceModel->setBlueprintContext($blueprint, $config); + + $this->setTemplateVars(); + } + + /** + * disableGeneratedBlueprints + */ + protected function disableGeneratedBlueprints() + { + foreach ($this->blueprintFiles as $filePath) { + File::move( + $filePath, + str_replace('.yaml', '.yaml.bak', $filePath) + ); + } + } + + /** + * deleteGeneratedBlueprintData + */ + protected function deleteGeneratedBlueprintData() + { + foreach ($this->sourceBlueprints as $blueprint) { + $contentTable = $blueprint->getContentTableName(); + Schema::dropIfExists($contentTable); + + $joinTable = $blueprint->getJoinTableName(); + Schema::dropIfExists($joinTable); + + $repeaterTable = $blueprint->getRepeaterTableName(); + Schema::dropIfExists($repeaterTable); + } + } + + /** + * setTemplateVars + */ + protected function setTemplateVars() + { + $pluginCodeObj = $this->sourceModel->getPluginCodeObj(); + + $this->templateVars = $this->getConfig(); + $this->templateVars['pluginNamespace'] = $pluginCodeObj->toPluginNamespace(); + $this->templateVars['pluginCode'] = $pluginCodeObj->toCode(); + } + + /** + * getTemplatePath + */ + protected function getTemplatePath($template) + { + return __DIR__.'/blueprintgenerator/templates/'.$template; + } + + /** + * parseTemplate + */ + protected function parseTemplate($templatePath, $vars = []) + { + $template = File::get($templatePath); + + $vars = array_merge($this->templateVars, $vars); + $code = Twig::parse($template, $vars); + + return $code; + } + + /** + * writeFile + */ + protected function writeFile($path, $data) + { + $fileDirectory = dirname($path); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', [ + 'name' => $fileDirectory + ])); + } + } + + if (@File::put($path, $data) === false) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_save_file', [ + 'file' => basename($path) + ])); + } + + @File::chmod($path); + $this->filesGenerated[] = $path; + } + + /** + * rollback + */ + protected function rollback() + { + foreach ($this->filesGenerated as $path) { + @unlink($path); + } + } + + /** + * makeTabs + */ + protected function makeTabs($str) + { + return str_replace('\t', ' ', $str); + } + + /** + * getConfig + */ + protected function getConfig($key = null, $default = null) + { + return $this->sourceModel->getBlueprintConfig($key, $default); + } + + /** + * validateUniqueFiles + */ + protected function validateUniqueFiles(array $files) + { + if (!is_array($this->filesValidated)) { + $this->filesValidated = []; + } + + foreach ($files as $path) { + if (File::isFile($path) || in_array($path, $this->filesValidated)) { + throw new ValidationException([ + 'modelClass' => __("File [:file] already exists when trying to import [:blueprint]", [ + 'file' => basename(dirname($path)) . '/' . basename($path), + 'blueprint' => $this->sourceModel->getBlueprintObject()->handle ?? 'unknown' + ]) + ]); + } + } + + $this->filesValidated = array_merge($this->filesValidated, $files); + } +} diff --git a/plugins/rainlab/builder/classes/ComponentHelper.php b/plugins/rainlab/builder/classes/ComponentHelper.php new file mode 100644 index 0000000..12ae887 --- /dev/null +++ b/plugins/rainlab/builder/classes/ComponentHelper.php @@ -0,0 +1,123 @@ +modelListCache !== null) { + return $this->modelListCache; + } + + $key = 'builder-global-model-list'; + $cached = Cache::get($key, false); + + if ($cached !== false && ($cached = @unserialize($cached)) !== false) { + return $this->modelListCache = $cached; + } + + $plugins = PluginBaseModel::listAllPluginCodes(); + + $result = []; + foreach ($plugins as $pluginCode) { + try { + $pluginCodeObj = new PluginCode($pluginCode); + + $models = ModelModel::listPluginModels($pluginCodeObj); + + $pluginCodeStr = $pluginCodeObj->toCode(); + $pluginModelsNamespace = $pluginCodeObj->toPluginNamespace().'\\Models\\'; + foreach ($models as $model) { + $fullClassName = $pluginModelsNamespace.$model->className; + + $result[$fullClassName] = $pluginCodeStr.' - '.$model->className; + } + } + catch (Exception $ex) { + // Ignore invalid plugins and models + } + } + + $expiresAt = now()->addMinutes(1); + Cache::put($key, serialize($result), $expiresAt); + + return $this->modelListCache = $result; + } + + /** + * getModelClassDesignTime + */ + public function getModelClassDesignTime() + { + $modelClass = trim(Input::get('modelClass')); + + if ($modelClass && !is_scalar($modelClass)) { + throw new ApplicationException('Model class name should be a string.'); + } + + if (!strlen($modelClass)) { + $models = $this->listGlobalModels(); + $modelClass = key($models); + } + + if (!ModelModel::validateModelClassName($modelClass)) { + throw new ApplicationException('Invalid model class name.'); + } + + return $modelClass; + } + + /** + * listModelColumnNames + */ + public function listModelColumnNames() + { + $modelClass = $this->getModelClassDesignTime(); + + $key = md5('builder-global-model-list-'.$modelClass); + $cached = Cache::get($key, false); + + if ($cached !== false && ($cached = @unserialize($cached)) !== false) { + return $cached; + } + + $pluginCodeObj = PluginCode::createFromNamespace($modelClass); + + $modelClassParts = explode('\\', $modelClass); // The full class name is already validated in PluginCode::createFromNamespace() + $modelClass = array_pop($modelClassParts); + + $columnNames = ModelModel::getModelFields($pluginCodeObj, $modelClass); + + $result = []; + foreach ($columnNames as $columnName) { + $result[$columnName] = $columnName; + } + + $expiresAt = now()->addMinutes(1); + Cache::put($key, serialize($result), $expiresAt); + + return $result; + } +} diff --git a/plugins/rainlab/builder/classes/ControlDesignTimeProviderBase.php b/plugins/rainlab/builder/classes/ControlDesignTimeProviderBase.php new file mode 100644 index 0000000..5264a96 --- /dev/null +++ b/plugins/rainlab/builder/classes/ControlDesignTimeProviderBase.php @@ -0,0 +1,42 @@ +groupedControls !== null) { + return $returnGrouped ? $this->groupedControls : $this->controls; + } + + $this->groupedControls = [ + $this->resolveControlGroupName(self::GROUP_STANDARD) => [], + $this->resolveControlGroupName(self::GROUP_WIDGETS) => [], + $this->resolveControlGroupName(self::GROUP_UI) => [] + ]; + + Event::fire('pages.builder.registerControls', [$this]); + + foreach ($this->controls as $controlType => $controlInfo) { + $controlGroup = $this->resolveControlGroupName($controlInfo['group']); + + if (!array_key_exists($controlGroup, $this->groupedControls)) { + $this->groupedControls[$controlGroup] = []; + } + + $this->groupedControls[$controlGroup][$controlType] = $controlInfo; + } + + return $returnGrouped ? $this->groupedControls : $this->controls; + } + + /** + * getControlInfo returns information about a control by its code. + * @param string $code Specifies the control code. + * @return array Returns an associative array or null if the control is not registered. + */ + public function getControlInfo($code) + { + $controls = $this->listControls(false); + + if (array_key_exists($code, $controls)) { + return $controls[$code]; + } + + return [ + 'properties' => [], + 'designTimeProvider' => self::DEFAULT_DESIGN_TIME_PROVIDER, + 'name' => $code, + 'description' => null, + 'unknownControl' => true + ]; + } + + /** + * registerControl registers a control. + * @param string $code Specifies the control code, for example "codeeditor". + * @param string $name Specifies the control name, for example "Code editor". + * @param string $description Specifies the control descritpion, can be empty. + * @param string|integer $controlGroup Specifies the control group. + * Control groups are used to create tabs in the Control Palette in Form Builder. + * The group could one of the ControlLibrary::GROUP_ constants or a string. + * @param string $icon Specifies the control icon for the Control Palette. + * @see http://octobercms.com/docs/ui/icon + * @param array $properties Specifies the control properties. + * The property definitions should be compatible with Inspector properties, similarly + * to the Component properties: http://octobercms.com/docs/plugin/components#component-properties + * Use the getStandardProperties() of the ControlLibrary to get the standard control properties. + * @param string $designTimeProviderClass Specifies the control design-time provider class name. + * The class should extend RainLab\Builder\Classes\ControlDesignTimeProviderBase. If the class is not provided, + * the default control design and design settings will be used. + */ + public function registerControl($code, $name, $description, $controlGroup, $icon, $properties, $designTimeProviderClass) + { + if (!$designTimeProviderClass) { + $designTimeProviderClass = self::DEFAULT_DESIGN_TIME_PROVIDER; + } + + $this->controls[$code] = [ + 'group' => $controlGroup, + 'name' => $name, + 'description' => $description, + 'icon' => $icon, + 'properties' => $properties, + 'designTimeProvider' => $designTimeProviderClass + ]; + } + + /** + * getStandardProperties + */ + public function getStandardProperties($excludeProperties = [], $addProperties = []) + { + $result = [ + 'label' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_label_title'), + 'type' => 'builderLocalization', + ], + 'oc.comment' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_comment_title'), + 'type' => 'builderLocalization', + ], + 'oc.commentPosition' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_comment_position'), + 'type' => 'dropdown', + 'options' => [ + 'above' => Lang::get('rainlab.builder::lang.form.property_comment_position_above'), + 'below' => Lang::get('rainlab.builder::lang.form.property_comment_position_below') + ], + 'ignoreIfEmpty' => true, + ], + 'span' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_span_title'), + 'type' => 'dropdown', + 'default' => 'full', + 'options' => [ + 'left' => Lang::get('rainlab.builder::lang.form.span_left'), + 'right' => Lang::get('rainlab.builder::lang.form.span_right'), + 'full' => Lang::get('rainlab.builder::lang.form.span_full'), + 'auto' => Lang::get('rainlab.builder::lang.form.span_auto') + ] + ], + 'placeholder' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_placeholder_title'), + 'type' => 'builderLocalization', + ], + 'default' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_default_title'), + 'type' => 'builderLocalization', + ], + 'cssClass' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_css_class_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_css_class_description'), + 'type' => 'string' + ], + 'disabled' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_disabled_title'), + 'type' => 'checkbox' + ], + 'readOnly' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_read_only_title'), + 'type' => 'checkbox' + ], + 'hidden' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_hidden_title'), + 'type' => 'checkbox' + ], + 'required' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_required_title'), + 'type' => 'checkbox' + ], + 'stretch' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_stretch_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_stretch_description'), + 'type' => 'checkbox' + ], + 'context' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_context_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_context_description'), + 'type' => 'set', + 'items' => [ + 'create' => Lang::get('rainlab.builder::lang.form.property_context_create'), + 'update' => Lang::get('rainlab.builder::lang.form.property_context_update'), + 'preview' => Lang::get('rainlab.builder::lang.form.property_context_preview') + ], + 'default' => ['create', 'update', 'preview'], + 'ignoreIfDefault' => true + ] + ]; + + $result = array_merge($result, $addProperties); + + $advancedProperties = [ + 'defaultFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_default_from_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_default_from_description'), + 'type' => 'dropdown', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + 'ignoreIfEmpty' => true, + 'fillFrom' => 'form-controls' + ], + 'dependsOn' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_dependson_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_dependson_description'), + 'type' => 'stringList', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + ], + 'trigger' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_trigger_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_trigger_description'), + 'type' => 'object', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + 'ignoreIfPropertyEmpty' => 'field', + 'properties' => [ + [ + 'property' => 'action', + 'title' => Lang::get('rainlab.builder::lang.form.property_trigger_action'), + 'type' => 'dropdown', + 'options' => [ + 'show' => Lang::get('rainlab.builder::lang.form.property_trigger_show'), + 'hide' => Lang::get('rainlab.builder::lang.form.property_trigger_hide'), + 'enable' => Lang::get('rainlab.builder::lang.form.property_trigger_enable'), + 'disable' => Lang::get('rainlab.builder::lang.form.property_trigger_disable'), + 'empty' => Lang::get('rainlab.builder::lang.form.property_trigger_empty') + ] + ], + [ + 'property' => 'field', + 'title' => Lang::get('rainlab.builder::lang.form.property_trigger_field'), + 'description' => Lang::get('rainlab.builder::lang.form.property_trigger_field_description'), + 'type' => 'dropdown', + 'fillFrom' => 'form-controls' + ], + [ + 'property' => 'condition', + 'title' => Lang::get('rainlab.builder::lang.form.property_trigger_condition'), + 'description' => Lang::get('rainlab.builder::lang.form.property_trigger_condition_description'), + 'type' => 'autocomplete', + 'items' => [ + 'checked' => Lang::get('rainlab.builder::lang.form.property_trigger_condition_checked'), + 'unchecked' => Lang::get('rainlab.builder::lang.form.property_trigger_condition_unchecked'), + 'value[somevalue]' => Lang::get('rainlab.builder::lang.form.property_trigger_condition_somevalue'), + ] + ] + ] + ], + 'preset' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_preset_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_preset_description'), + 'type' => 'object', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + 'ignoreIfPropertyEmpty' => 'field', + 'properties' => [ + [ + 'property' => 'field', + 'title' => Lang::get('rainlab.builder::lang.form.property_preset_field'), + 'description' => Lang::get('rainlab.builder::lang.form.property_preset_field_description'), + 'type' => 'dropdown', + 'fillFrom' => 'form-controls' + ], + [ + 'property' => 'type', + 'title' => Lang::get('rainlab.builder::lang.form.property_preset_type'), + 'description' => Lang::get('rainlab.builder::lang.form.property_preset_type_description'), + 'type' => 'dropdown', + 'options' => [ + 'url' => 'URL', + 'file' => 'File', + 'slug' => 'Slug', + 'camel' => 'Camel' + ] + ] + ] + ], + 'attributes' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_attributes_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_attributes_description'), + 'type' => 'dictionary', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + ], + 'containerAttributes' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_container_attributes_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_container_attributes_description'), + 'type' => 'dictionary', + 'group' => Lang::get('rainlab.builder::lang.form.property_group_advanced'), + ] + ]; + + $result = array_merge($result, $advancedProperties); + + foreach ($excludeProperties as $property) { + if (array_key_exists($property, $result)) { + unset($result[$property]); + } + } + + return $result; + } + + /** + * resolveControlGroupName + */ + protected function resolveControlGroupName($group) + { + if ($group === self::GROUP_STANDARD) { + return Lang::get('rainlab.builder::lang.form.control_group_standard'); + } + + if ($group === self::GROUP_WIDGETS) { + return Lang::get('rainlab.builder::lang.form.control_group_widgets'); + } + + if ($group === self::GROUP_UI) { + return Lang::get('rainlab.builder::lang.form.control_group_ui'); + } + + return Lang::get($group); + } +} diff --git a/plugins/rainlab/builder/classes/ControllerBehaviorLibrary.php b/plugins/rainlab/builder/classes/ControllerBehaviorLibrary.php new file mode 100644 index 0000000..7db3ff6 --- /dev/null +++ b/plugins/rainlab/builder/classes/ControllerBehaviorLibrary.php @@ -0,0 +1,84 @@ +listBehaviors(); + + if (!array_key_exists($behaviorClassName, $behaviors)) { + return null; + } + + return $behaviors[$behaviorClassName]; + } + + /** + * Registers a controller behavior. + * @param string $class Specifies the behavior class name. + * @param string $name Specifies the behavior name, for example "Form behavior". + * @param string $description Specifies the behavior description. + * @param array $properties Specifies the behavior properties. + * The property definitions should be compatible with Inspector properties, similarly + * to the Component properties: http://octobercms.com/docs/plugin/components#component-properties + * @param string $configFilePropertyName Specifies the name of the controller property that contains the configuration file name for the behavior. + * @param string $designTimeProviderClass Specifies the behavior design-time provider class name. + * The class should extend RainLab\Builder\Classes\BehaviorDesignTimeProviderBase. If the class is not provided, + * the default control design and design settings will be used. + * @param string $configFileName Default behavior configuration file name, for example config_form.yaml. + * @param array $viewTemplates An array of view templates that are required for the behavior. + * The templates are used when a new controller is created. The templates should be specified as paths + * to Twig files in the format ['~/plugins/author/plugin/behaviors/behaviorname/templates/view.htm.tpl']. + */ + public function registerBehavior($class, $name, $description, $properties, $configFilePropertyName, $designTimeProviderClass, $configFileName, $viewTemplates = []) + { + if (!$designTimeProviderClass) { + $designTimeProviderClass = self::DEFAULT_DESIGN_TIME_PROVIDER; + } + + $this->behaviors[$class] = [ + 'class' => $class, + 'name' => Lang::get($name), + 'description' => Lang::get($description), + 'properties' => $properties, + 'designTimeProvider' => $designTimeProviderClass, + 'viewTemplates' => $viewTemplates, + 'configFileName' => $configFileName, + 'configPropertyName' => $configFilePropertyName + ]; + } + + /** + * listBehaviors + */ + public function listBehaviors() + { + if ($this->behaviors !== null) { + return $this->behaviors; + } + + $this->behaviors = []; + + Event::fire('pages.builder.registerControllerBehaviors', [$this]); + + return $this->behaviors; + } +} diff --git a/plugins/rainlab/builder/classes/ControllerFileParser.php b/plugins/rainlab/builder/classes/ControllerFileParser.php new file mode 100644 index 0000000..e158dc2 --- /dev/null +++ b/plugins/rainlab/builder/classes/ControllerFileParser.php @@ -0,0 +1,146 @@ +stream = new PhpSourceStream($fileContents); + } + + /** + * listBehaviors + */ + public function listBehaviors() + { + $this->stream->reset(); + + while ($this->stream->forward()) { + $tokenCode = $this->stream->getCurrentCode(); + + if ($tokenCode == T_PUBLIC) { + $behaviors = $this->extractBehaviors(); + if ($behaviors !== false) { + return $behaviors; + } + } + } + } + + /** + * getStringPropertyValue + */ + public function getStringPropertyValue($property) + { + $this->stream->reset(); + + while ($this->stream->forward()) { + $tokenCode = $this->stream->getCurrentCode(); + + if ($tokenCode == T_PUBLIC) { + $value = $this->extractPropertyValue($property); + if ($value !== false) { + return $value; + } + } + } + } + + /** + * extractBehaviors + */ + protected function extractBehaviors() + { + if ($this->stream->getNextExpected(T_WHITESPACE) === null) { + return false; + } + + if ($this->stream->getNextExpected(T_VARIABLE) === null) { + return false; + } + + if ($this->stream->getCurrentText() != '$implement') { + return false; + } + + if ($this->stream->getNextExpectedTerminated(['=', T_WHITESPACE], ['[', T_ARRAY]) === null) { + return false; + } + + if ($this->stream->getCurrentText() === 'array') { + // For the array syntax 'array(' - forward to the next + // character after the opening bracket + + if ($this->stream->getNextExpectedTerminated(['(', T_WHITESPACE], [T_CONSTANT_ENCAPSED_STRING]) === null) { + return false; + } + + $this->stream->back(); + } + + $result = []; + while ($line = $this->stream->getNextExpectedTerminated([T_CONSTANT_ENCAPSED_STRING, T_NAME_FULLY_QUALIFIED, T_WHITESPACE], [',', ']', ')'], [T_DOUBLE_COLON, T_CLASS])) { + $line = $this->stream->unquotePhpString(trim($line), $line); + if (!strlen($line)) { + continue; + } + + $result[] = $this->normalizeBehaviorClassName($line); + } + + return $result; + } + + /** + * extractPropertyValue + */ + protected function extractPropertyValue($property) + { + if ($this->stream->getNextExpected(T_WHITESPACE) === null) { + return false; + } + + if ($this->stream->getNextExpected(T_VARIABLE) === null) { + return false; + } + + if ($this->stream->getCurrentText() != '$'.$property) { + return false; + } + + if ($this->stream->getNextExpectedTerminated(['=', T_WHITESPACE], [T_CONSTANT_ENCAPSED_STRING]) === null) { + return null; + } + + $value = trim($this->stream->getCurrentText()); + $value = $this->stream->unquotePhpString($value); + + if ($value === false) { + return null; + } + + return $value; + } + + /** + * normalizeBehaviorClassName + */ + protected function normalizeBehaviorClassName($className) + { + $className = str_replace('.', '\\', trim($className)); + return ltrim($className, '\\'); + } +} diff --git a/plugins/rainlab/builder/classes/ControllerGenerator.php b/plugins/rainlab/builder/classes/ControllerGenerator.php new file mode 100644 index 0000000..2f18ff9 --- /dev/null +++ b/plugins/rainlab/builder/classes/ControllerGenerator.php @@ -0,0 +1,411 @@ +sourceModel = $source; + } + + /** + * generate + */ + public function generate() + { + $this->filesGenerated = []; + $this->templateVars = []; + + try { + $this->validateBehaviorViewTemplates(); + $this->validateBehaviorConfigSettings(); + $this->validateControllerUnique(); + + $this->setTemplateVars(); + $this->generateControllerFile(); + $this->generateConfigFiles(); + $this->generateViews(); + } + catch (Exception $ex) { + $this->rollback(); + + throw $ex; + } + } + + /** + * setTemplateVariable + */ + public function setTemplateVariable($var, $value) + { + $this->templateVars[$var] = $value; + } + + /** + * validateBehaviorViewTemplates + */ + protected function validateBehaviorViewTemplates() + { + if (!$this->sourceModel->behaviors) { + return; + } + + $this->templateFiles = []; + + $controllerPath = $this->sourceModel->getControllerFilePath(true); + $behaviorLibrary = ControllerBehaviorLibrary::instance(); + + $knownTemplates = []; + foreach ($this->sourceModel->behaviors as $behaviorClass => $behaviorConfig) { + $behaviorInfo = $behaviorLibrary->getBehaviorInfo($behaviorClass); + if (!$behaviorInfo) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_unknown_behavior', [ + 'class' => $behaviorClass + ]) + ]); + } + + foreach ($behaviorInfo['viewTemplates'] as $viewTemplate) { + $templateFileName = basename($viewTemplate); + $templateBaseName = pathinfo($templateFileName, PATHINFO_FILENAME); + + if (in_array($templateFileName, $knownTemplates)) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_behavior_view_conflict', [ + 'view' => $templateBaseName + ]) + ]); + } + + $knownTemplates[] = $templateFileName; + + $filePath = File::symbolizePath($viewTemplate); + if (!File::isFile($filePath)) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_behavior_view_file_not_found', [ + 'class' => $behaviorClass, + 'view' => $templateFileName + ]) + ]); + } + + $destFilePath = $controllerPath.'/'.$templateBaseName; + if (File::isFile($destFilePath)) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_behavior_view_file_exists', [ + 'view' => $destFilePath + ]) + ]); + } + + $this->templateFiles[$filePath] = $destFilePath; + } + } + } + + /** + * validateBehaviorConfigSettings + */ + protected function validateBehaviorConfigSettings() + { + if (!$this->sourceModel->behaviors) { + return; + } + + $this->configTemplateProperties = []; + + $controllerPath = $this->sourceModel->getControllerFilePath(true); + $behaviorLibrary = ControllerBehaviorLibrary::instance(); + + $knownConfigFiles = []; + foreach ($this->sourceModel->behaviors as $behaviorClass => $behaviorConfig) { + $behaviorInfo = $behaviorLibrary->getBehaviorInfo($behaviorClass); + $configFileName = $behaviorInfo['configFileName']; + + if (!strlen($configFileName)) { + continue; + } + + if (in_array($configFileName, $knownConfigFiles)) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_behavior_config_conflict', [ + 'file' => $configFileName + ]) + ]); + } + + $knownConfigFiles[] = $configFileName; + + $destFilePath = $controllerPath.'/'.$configFileName; + if (File::isFile($destFilePath)) { + throw new ValidationException([ + 'behaviors' => Lang::get('rainlab.builder::lang.controller.error_behavior_config_file_exists', [ + 'file' => $destFilePath + ]) + ]); + } + + $configPropertyName = $behaviorInfo['configPropertyName']; + $this->configTemplateProperties[$configPropertyName] = $configFileName; + } + } + + /** + * validateControllerUnique + */ + protected function validateControllerUnique() + { + $controllerFilePath = $this->sourceModel->getControllerFilePath(); + + if (File::isFile($controllerFilePath)) { + throw new ValidationException([ + 'controller' => Lang::get('rainlab.builder::lang.controller.error_controller_exists', [ + 'file' => basename($controllerFilePath) + ]) + ]); + } + } + + /** + * setTemplateVars + */ + protected function setTemplateVars() + { + $pluginCodeObj = $this->sourceModel->getPluginCodeObj(); + + $this->templateVars['pluginNamespace'] = $pluginCodeObj->toPluginNamespace(); + $this->templateVars['pluginCode'] = $pluginCodeObj->toCode(); + $this->templateVars['permissions'] = $this->sourceModel->permissions; + $this->templateVars['controller'] = $this->sourceModel->controller; + $this->templateVars['controllerName'] = $this->sourceModel->controllerName; + $this->templateVars['baseModelClassName'] = $this->sourceModel->baseModelClassName; + + $this->templateVars['controllerUrl'] = $pluginCodeObj->toUrl().'/'.strtolower($this->sourceModel->controller); + + $menuItem = $this->sourceModel->menuItem; + if ($menuItem) { + $itemParts = explode('||', $menuItem); + $this->templateVars['menuItem'] = $itemParts[0]; + + if (count($itemParts) > 1) { + $this->templateVars['sideMenuItem'] = $itemParts[1]; + } + } + + if ($this->sourceModel->behaviors) { + $this->templateVars['behaviors'] = array_keys($this->sourceModel->behaviors); + } + else { + $this->templateVars['behaviors'] = []; + } + + $this->templateVars['behaviorConfigVars'] = $this->configTemplateProperties; + } + + /** + * getTemplatePath + */ + protected function getTemplatePath($template) + { + return __DIR__.'/controllergenerator/templates/'.$template; + } + + /** + * parseTemplate + */ + protected function parseTemplate($templatePath, $vars = []) + { + $template = File::get($templatePath); + + $vars = array_merge($this->templateVars, $vars); + $code = Twig::parse($template, $vars); + + return $code; + } + + /** + * writeFile + */ + protected function writeFile($path, $data) + { + $fileDirectory = dirname($path); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', [ + 'name' => $fileDirectory + ])); + } + } + + if (@File::put($path, $data) === false) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_save_file', [ + 'file' => basename($path) + ])); + } + + @File::chmod($path); + $this->filesGenerated[] = $path; + } + + /** + * rollback + */ + protected function rollback() + { + foreach ($this->filesGenerated as $path) { + @unlink($path); + } + } + + /** + * generateControllerFile + */ + protected function generateControllerFile() + { + $templateParts = []; + $code = $this->parseTemplate($this->getTemplatePath('controller-config-vars.php.tpl')); + if (strlen($code)) { + $templateParts[] = $code; + } + + $code = $this->parseTemplate($this->getTemplatePath('controller-permissions.php.tpl')); + if (strlen($code)) { + $templateParts[] = $code; + } + + if (count($templateParts)) { + $templateParts = "\n".implode("\n", $templateParts); + } + else { + $templateParts = ""; + } + + $noListTemplate = ""; + if ( + !array_key_exists(\Backend\Behaviors\ListController::class, $this->sourceModel->behaviors) && + array_key_exists(\Backend\Behaviors\FormController::class, $this->sourceModel->behaviors) + ) { + $noListTemplate = $this->parseTemplate($this->getTemplatePath('controller-no-list.php.tpl')); + } + + $code = $this->parseTemplate($this->getTemplatePath('controller.php.tpl'), [ + 'templateParts' => $templateParts, + 'noListTemplate' => $noListTemplate, + ]); + + $controllerFilePath = $this->sourceModel->getControllerFilePath(); + + $this->writeFile($controllerFilePath, $code); + } + + /** + * getBehaviorDesignTimeProvider + */ + protected function getBehaviorDesignTimeProvider($providerClass) + { + if (array_key_exists($providerClass, $this->designTimeProviders)) { + return $this->designTimeProviders[$providerClass]; + } + + return $this->designTimeProviders[$providerClass] = new $providerClass(null, []); + } + + /** + * generateConfigFiles + */ + protected function generateConfigFiles() + { + if (!$this->sourceModel->behaviors) { + return; + } + + $controllerPath = $this->sourceModel->getControllerFilePath(true); + $behaviorLibrary = ControllerBehaviorLibrary::instance(); + $dumper = new YamlDumper(); + + foreach ($this->sourceModel->behaviors as $behaviorClass => $behaviorConfig) { + $behaviorInfo = $behaviorLibrary->getBehaviorInfo($behaviorClass); + $configFileName = $behaviorInfo['configFileName']; + + if (!strlen($configFileName)) { + continue; + } + + $provider = $this->getBehaviorDesignTimeProvider($behaviorInfo['designTimeProvider']); + + $destFilePath = $controllerPath.'/'.$configFileName; + + try { + $configArray = $provider->getDefaultConfiguration($behaviorClass, $this->sourceModel, $this); + } + catch (Exception $ex) { + throw new ValidationException(['baseModelClassName' => $ex->getMessage()]); + } + + $configArray = array_merge($configArray, $behaviorConfig); + + $code = $dumper->dump($configArray, 20, 0, false, true); + + $this->writeFile($destFilePath, $code); + } + } + + /** + * generateViews + */ + protected function generateViews() + { + foreach ($this->templateFiles as $templatePath => $destPath) { + $code = $this->parseTemplate($templatePath); + + $this->writeFile($destPath, $code); + } + } +} diff --git a/plugins/rainlab/builder/classes/DatabaseTableSchemaCreator.php b/plugins/rainlab/builder/classes/DatabaseTableSchemaCreator.php new file mode 100644 index 0000000..b99a4c1 --- /dev/null +++ b/plugins/rainlab/builder/classes/DatabaseTableSchemaCreator.php @@ -0,0 +1,66 @@ +formatOptions($type, $column); + + $schema->addColumn($column['name'], $typeName, $options); + if ($column['primary_key']) { + $primaryKeyColumns[] = $column['name']; + } + } + + if ($primaryKeyColumns) { + $schema->setPrimaryKey($primaryKeyColumns); + } + + return $schema; + } + + /** + * Converts column options to a format supported by Doctrine\DBAL\Schema\Column + */ + protected function formatOptions($type, $options) + { + $result = MigrationColumnType::lengthToPrecisionAndScale($type, $options['length']); + + $result['unsigned'] = !!$options['unsigned']; + $result['notnull'] = !$options['allow_null']; + $result['autoincrement'] = !!$options['auto_increment']; + $result['comment'] = trim($options['comment'] ?? ''); + + // Note - this code doesn't allow to set empty string as default. + // But converting empty strings to NULLs is required for the further + // work with Doctrine types. As an option - empty strings could be specified + // as '' in the editor UI (table column editor). + $default = trim($options['default']); + $result['default'] = $default === '' ? null : $default; + + return $result; + } +} diff --git a/plugins/rainlab/builder/classes/EnumDbType.php b/plugins/rainlab/builder/classes/EnumDbType.php new file mode 100644 index 0000000..43777e6 --- /dev/null +++ b/plugins/rainlab/builder/classes/EnumDbType.php @@ -0,0 +1,38 @@ + 'plugin.php.tpl' + * ]; + * $generator = new FilesystemGenerator('$', $structure, '$/Author/Plugin/templates/plugin'); + * + * $variables = [ + * 'namespace' => 'Author/Plugin' + * ]; + * $generator->setVariables($variables); + * $generator->generate(); + * + * @package rainlab\builder + * @author Alexey Bobkov, Samuel Georges + */ +class FilesystemGenerator +{ + protected $destinationPath; + + protected $structure; + + protected $variables = []; + + protected $templatesPath; + + /** + * Initializes the object. + * @param string $destinationPath Destination path to create the filesystem objects in. + * The path can contain filesystem symbols. + * @param array $structure Specifies the structure as array. + * @param string $templatesPath Path to the directory that contains file templates. + * The parameter is required only in case any files should be created. The path can + * contain filesystem symbols. + */ + public function __construct($destinationPath, array $structure, $templatesPath = null) + { + $this->destinationPath = File::symbolizePath($destinationPath); + $this->structure = $structure; + + if ($templatesPath) { + $this->templatesPath = File::symbolizePath($templatesPath); + } + } + + public function setVariables($variables) + { + foreach ($variables as $key => $value) { + $this->setVariable($key, $value); + } + } + + public function setVariable($key, $value) + { + $this->variables[$key] = $value; + } + + public function generate() + { + if (!File::isDirectory($this->destinationPath)) { + throw new SystemException(Lang::get('rainlab.builder::lang.common.destination_dir_not_exists', ['path'=>$this->destinationPath])); + } + + foreach ($this->structure as $key => $value) { + if (is_numeric($key)) { + $this->makeDirectory($value); + } + else { + $this->makeFile($key, $value); + } + } + } + + public function getTemplateContents($templateName) + { + $templatePath = $this->templatesPath.DIRECTORY_SEPARATOR.$templateName; + if (!File::isFile($templatePath)) { + throw new SystemException(Lang::get('rainlab.builder::lang.common.template_not_found', ['name'=>$templateName])); + } + + $fileContents = File::get($templatePath); + + return TextParser::parse($fileContents, $this->variables); + } + + protected function makeDirectory($dirPath) + { + $path = $this->destinationPath.DIRECTORY_SEPARATOR.$dirPath; + + if (File::isDirectory($path)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_dir_exists', ['path'=>$path])); + } + + if (!File::makeDirectory($path, 0777, true, true)) { + throw new SystemException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$path])); + } + } + + protected function makeFile($filePath, $templateName) + { + $path = $this->destinationPath.DIRECTORY_SEPARATOR.$filePath; + + if (File::isFile($path)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_file_exists', ['path'=>$path])); + } + + $fileDirectory = dirname($path); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new SystemException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory])); + } + } + + $fileContents = $this->getTemplateContents($templateName); + if (@File::put($path, $fileContents) === false) { + throw new SystemException(Lang::get('rainlab.builder::lang.common.error_generating_file', ['path'=>$path])); + } + + @File::chmod($path); + } +} diff --git a/plugins/rainlab/builder/classes/IconList.php b/plugins/rainlab/builder/classes/IconList.php new file mode 100644 index 0000000..ab2eace --- /dev/null +++ b/plugins/rainlab/builder/classes/IconList.php @@ -0,0 +1,607 @@ + ['adjust', 'oc-icon-adjust'], + 'oc-icon-adn' => ['adn', 'oc-icon-adn'], + 'oc-icon-align-center' => ['align-center', 'oc-icon-align-center'], + 'oc-icon-align-justify' => ['align-justify', 'oc-icon-align-justify'], + 'oc-icon-align-left' => ['align-left', 'oc-icon-align-left'], + 'oc-icon-align-right' => ['align-right', 'oc-icon-align-right'], + 'oc-icon-ambulance' => ['ambulance', 'oc-icon-ambulance'], + 'oc-icon-anchor' => ['anchor', 'oc-icon-anchor'], + 'oc-icon-android' => ['android', 'oc-icon-android'], + 'oc-icon-angellist' => ['angellist', 'oc-icon-angellist'], + 'oc-icon-angle-double-down' => ['angle-double-down', 'oc-icon-angle-double-down'], + 'oc-icon-angle-double-left' => ['angle-double-left', 'oc-icon-angle-double-left'], + 'oc-icon-angle-double-right' => ['angle-double-right', 'oc-icon-angle-double-right'], + 'oc-icon-angle-double-up' => ['angle-double-up', 'oc-icon-angle-double-up'], + 'oc-icon-angle-down' => ['angle-down', 'oc-icon-angle-down'], + 'oc-icon-angle-left' => ['angle-left', 'oc-icon-angle-left'], + 'oc-icon-angle-right' => ['angle-right', 'oc-icon-angle-right'], + 'oc-icon-angle-up' => ['angle-up', 'oc-icon-angle-up'], + 'oc-icon-apple' => ['apple', 'oc-icon-apple'], + 'oc-icon-archive' => ['archive', 'oc-icon-archive'], + 'oc-icon-area-chart' => ['area-chart', 'oc-icon-area-chart'], + 'oc-icon-arrow-circle-down' => ['arrow-circle-down', 'oc-icon-arrow-circle-down'], + 'oc-icon-arrow-circle-left' => ['arrow-circle-left', 'oc-icon-arrow-circle-left'], + 'oc-icon-arrow-circle-o-down' => ['arrow-circle-o-down', 'oc-icon-arrow-circle-o-down'], + 'oc-icon-arrow-circle-o-left' => ['arrow-circle-o-left', 'oc-icon-arrow-circle-o-left'], + 'oc-icon-arrow-circle-o-right' => ['arrow-circle-o-right', 'oc-icon-arrow-circle-o-right'], + 'oc-icon-arrow-circle-o-up' => ['arrow-circle-o-up', 'oc-icon-arrow-circle-o-up'], + 'oc-icon-arrow-circle-right' => ['arrow-circle-right', 'oc-icon-arrow-circle-right'], + 'oc-icon-arrow-circle-up' => ['arrow-circle-up', 'oc-icon-arrow-circle-up'], + 'oc-icon-arrow-down' => ['arrow-down', 'oc-icon-arrow-down'], + 'oc-icon-arrow-left' => ['arrow-left', 'oc-icon-arrow-left'], + 'oc-icon-arrow-right' => ['arrow-right', 'oc-icon-arrow-right'], + 'oc-icon-arrow-up' => ['arrow-up', 'oc-icon-arrow-up'], + 'oc-icon-arrows' => ['arrows', 'oc-icon-arrows'], + 'oc-icon-arrows-alt' => ['arrows-alt', 'oc-icon-arrows-alt'], + 'oc-icon-arrows-h' => ['arrows-h', 'oc-icon-arrows-h'], + 'oc-icon-arrows-v' => ['arrows-v', 'oc-icon-arrows-v'], + 'oc-icon-asterisk' => ['asterisk', 'oc-icon-asterisk'], + 'oc-icon-at' => ['at', 'oc-icon-at'], + 'oc-icon-automobile' => ['automobile', 'oc-icon-automobile'], + 'oc-icon-backward' => ['backward', 'oc-icon-backward'], + 'oc-icon-ban' => ['ban', 'oc-icon-ban'], + 'oc-icon-bank' => ['bank', 'oc-icon-bank'], + 'oc-icon-bar-chart' => ['bar-chart', 'oc-icon-bar-chart'], + 'oc-icon-bar-chart-o' => ['bar-chart-o', 'oc-icon-bar-chart-o'], + 'oc-icon-barcode' => ['barcode', 'oc-icon-barcode'], + 'oc-icon-bars' => ['bars', 'oc-icon-bars'], + 'oc-icon-bed' => ['bed', 'oc-icon-bed'], + 'oc-icon-beer' => ['beer', 'oc-icon-beer'], + 'oc-icon-behance' => ['behance', 'oc-icon-behance'], + 'oc-icon-behance-square' => ['behance-square', 'oc-icon-behance-square'], + 'oc-icon-bell' => ['bell', 'oc-icon-bell'], + 'oc-icon-bell-o' => ['bell-o', 'oc-icon-bell-o'], + 'oc-icon-bell-slash' => ['bell-slash', 'oc-icon-bell-slash'], + 'oc-icon-bell-slash-o' => ['bell-slash-o', 'oc-icon-bell-slash-o'], + 'oc-icon-bicycle' => ['bicycle', 'oc-icon-bicycle'], + 'oc-icon-binoculars' => ['binoculars', 'oc-icon-binoculars'], + 'oc-icon-birthday-cake' => ['birthday-cake', 'oc-icon-birthday-cake'], + 'oc-icon-bitbucket' => ['bitbucket', 'oc-icon-bitbucket'], + 'oc-icon-bitbucket-square' => ['bitbucket-square', 'oc-icon-bitbucket-square'], + 'oc-icon-bitcoin' => ['bitcoin', 'oc-icon-bitcoin'], + 'oc-icon-bold' => ['bold', 'oc-icon-bold'], + 'oc-icon-bolt' => ['bolt', 'oc-icon-bolt'], + 'oc-icon-bomb' => ['bomb', 'oc-icon-bomb'], + 'oc-icon-book' => ['book', 'oc-icon-book'], + 'oc-icon-bookmark' => ['bookmark', 'oc-icon-bookmark'], + 'oc-icon-bookmark-o' => ['bookmark-o', 'oc-icon-bookmark-o'], + 'oc-icon-briefcase' => ['briefcase', 'oc-icon-briefcase'], + 'oc-icon-btc' => ['btc', 'oc-icon-btc'], + 'oc-icon-bug' => ['bug', 'oc-icon-bug'], + 'oc-icon-building' => ['building', 'oc-icon-building'], + 'oc-icon-building-o' => ['building-o', 'oc-icon-building-o'], + 'oc-icon-bullhorn' => ['bullhorn', 'oc-icon-bullhorn'], + 'oc-icon-bullseye' => ['bullseye', 'oc-icon-bullseye'], + 'oc-icon-bus' => ['bus', 'oc-icon-bus'], + 'oc-icon-buysellads' => ['buysellads', 'oc-icon-buysellads'], + 'oc-icon-cab' => ['cab', 'oc-icon-cab'], + 'oc-icon-calculator' => ['calculator', 'oc-icon-calculator'], + 'oc-icon-calendar' => ['calendar', 'oc-icon-calendar'], + 'oc-icon-calendar-o' => ['calendar-o', 'oc-icon-calendar-o'], + 'oc-icon-camera' => ['camera', 'oc-icon-camera'], + 'oc-icon-camera-retro' => ['camera-retro', 'oc-icon-camera-retro'], + 'oc-icon-car' => ['car', 'oc-icon-car'], + 'oc-icon-caret-down' => ['caret-down', 'oc-icon-caret-down'], + 'oc-icon-caret-left' => ['caret-left', 'oc-icon-caret-left'], + 'oc-icon-caret-right' => ['caret-right', 'oc-icon-caret-right'], + 'oc-icon-caret-square-o-down' => ['caret-square-o-down', 'oc-icon-caret-square-o-down'], + 'oc-icon-caret-square-o-left' => ['caret-square-o-left', 'oc-icon-caret-square-o-left'], + 'oc-icon-caret-square-o-right' => ['caret-square-o-right', 'oc-icon-caret-square-o-right'], + 'oc-icon-caret-square-o-up' => ['caret-square-o-up', 'oc-icon-caret-square-o-up'], + 'oc-icon-caret-up' => ['caret-up', 'oc-icon-caret-up'], + 'oc-icon-cart-arrow-down' => ['cart-arrow-down', 'oc-icon-cart-arrow-down'], + 'oc-icon-cart-plus' => ['cart-plus', 'oc-icon-cart-plus'], + 'oc-icon-cc' => ['cc', 'oc-icon-cc'], + 'oc-icon-cc-amex' => ['cc-amex', 'oc-icon-cc-amex'], + 'oc-icon-cc-discover' => ['cc-discover', 'oc-icon-cc-discover'], + 'oc-icon-cc-mastercard' => ['cc-mastercard', 'oc-icon-cc-mastercard'], + 'oc-icon-cc-paypal' => ['cc-paypal', 'oc-icon-cc-paypal'], + 'oc-icon-cc-stripe' => ['cc-stripe', 'oc-icon-cc-stripe'], + 'oc-icon-cc-visa' => ['cc-visa', 'oc-icon-cc-visa'], + 'oc-icon-certificate' => ['certificate', 'oc-icon-certificate'], + 'oc-icon-chain' => ['chain', 'oc-icon-chain'], + 'oc-icon-chain-broken' => ['chain-broken', 'oc-icon-chain-broken'], + 'oc-icon-check' => ['check', 'oc-icon-check'], + 'oc-icon-check-circle' => ['check-circle', 'oc-icon-check-circle'], + 'oc-icon-check-circle-o' => ['check-circle-o', 'oc-icon-check-circle-o'], + 'oc-icon-check-square' => ['check-square', 'oc-icon-check-square'], + 'oc-icon-check-square-o' => ['check-square-o', 'oc-icon-check-square-o'], + 'oc-icon-chevron-circle-down' => ['chevron-circle-down', 'oc-icon-chevron-circle-down'], + 'oc-icon-chevron-circle-left' => ['chevron-circle-left', 'oc-icon-chevron-circle-left'], + 'oc-icon-chevron-circle-right' => ['chevron-circle-right', 'oc-icon-chevron-circle-right'], + 'oc-icon-chevron-circle-up' => ['chevron-circle-up', 'oc-icon-chevron-circle-up'], + 'oc-icon-chevron-down' => ['chevron-down', 'oc-icon-chevron-down'], + 'oc-icon-chevron-left' => ['chevron-left', 'oc-icon-chevron-left'], + 'oc-icon-chevron-right' => ['chevron-right', 'oc-icon-chevron-right'], + 'oc-icon-chevron-up' => ['chevron-up', 'oc-icon-chevron-up'], + 'oc-icon-child' => ['child', 'oc-icon-child'], + 'oc-icon-circle' => ['circle', 'oc-icon-circle'], + 'oc-icon-circle-o' => ['circle-o', 'oc-icon-circle-o'], + 'oc-icon-circle-o-notch' => ['circle-o-notch', 'oc-icon-circle-o-notch'], + 'oc-icon-circle-thin' => ['circle-thin', 'oc-icon-circle-thin'], + 'oc-icon-clipboard' => ['clipboard', 'oc-icon-clipboard'], + 'oc-icon-clock-o' => ['clock-o', 'oc-icon-clock-o'], + 'oc-icon-close' => ['close', 'oc-icon-close'], + 'oc-icon-cloud' => ['cloud', 'oc-icon-cloud'], + 'oc-icon-cloud-download' => ['cloud-download', 'oc-icon-cloud-download'], + 'oc-icon-cloud-upload' => ['cloud-upload', 'oc-icon-cloud-upload'], + 'oc-icon-cny' => ['cny', 'oc-icon-cny'], + 'oc-icon-code' => ['code', 'oc-icon-code'], + 'oc-icon-code-fork' => ['code-fork', 'oc-icon-code-fork'], + 'oc-icon-codepen' => ['codepen', 'oc-icon-codepen'], + 'oc-icon-coffee' => ['coffee', 'oc-icon-coffee'], + 'oc-icon-cog' => ['cog', 'oc-icon-cog'], + 'oc-icon-cogs' => ['cogs', 'oc-icon-cogs'], + 'oc-icon-columns' => ['columns', 'oc-icon-columns'], + 'oc-icon-comment' => ['comment', 'oc-icon-comment'], + 'oc-icon-comment-o' => ['comment-o', 'oc-icon-comment-o'], + 'oc-icon-comments' => ['comments', 'oc-icon-comments'], + 'oc-icon-comments-o' => ['comments-o', 'oc-icon-comments-o'], + 'oc-icon-compass' => ['compass', 'oc-icon-compass'], + 'oc-icon-compress' => ['compress', 'oc-icon-compress'], + 'oc-icon-connectdevelop' => ['connectdevelop', 'oc-icon-connectdevelop'], + 'oc-icon-copy' => ['copy', 'oc-icon-copy'], + 'oc-icon-copyright' => ['copyright', 'oc-icon-copyright'], + 'oc-icon-credit-card' => ['credit-card', 'oc-icon-credit-card'], + 'oc-icon-crop' => ['crop', 'oc-icon-crop'], + 'oc-icon-crosshairs' => ['crosshairs', 'oc-icon-crosshairs'], + 'oc-icon-css3' => ['css3', '|'], + 'oc-icon-cube' => ['cube', 'oc-icon-cube'], + 'oc-icon-cubes' => ['cubes', 'oc-icon-cubes'], + 'oc-icon-cut' => ['cut', 'oc-icon-cut'], + 'oc-icon-cutlery' => ['cutlery', 'oc-icon-cutlery'], + 'oc-icon-dashboard' => ['dashboard', 'oc-icon-dashboard'], + 'oc-icon-dashcube' => ['dashcube', 'oc-icon-dashcube'], + 'oc-icon-database' => ['database', 'oc-icon-database'], + 'oc-icon-dedent' => ['dedent', 'oc-icon-dedent'], + 'oc-icon-delicious' => ['delicious', 'oc-icon-delicious'], + 'oc-icon-desktop' => ['desktop', 'oc-icon-desktop'], + 'oc-icon-deviantart' => ['deviantart', 'oc-icon-deviantart'], + 'oc-icon-diamond' => ['diamond', 'oc-icon-diamond'], + 'oc-icon-digg' => ['digg', 'oc-icon-digg'], + 'oc-icon-dollar' => ['dollar', 'oc-icon-dollar'], + 'oc-icon-dot-circle-o' => ['dot-circle-o', 'oc-icon-dot-circle-o'], + 'oc-icon-download' => ['download', 'oc-icon-download'], + 'oc-icon-dribbble' => ['dribbble', 'oc-icon-dribbble'], + 'oc-icon-dropbox' => ['dropbox', 'oc-icon-dropbox'], + 'oc-icon-drupal' => ['drupal', 'oc-icon-drupal'], + 'oc-icon-edit' => ['edit', 'oc-icon-edit'], + 'oc-icon-eject' => ['eject', 'oc-icon-eject'], + 'oc-icon-ellipsis-h' => ['ellipsis-h', 'oc-icon-ellipsis-h'], + 'oc-icon-ellipsis-v' => ['ellipsis-v', 'oc-icon-ellipsis-v'], + 'oc-icon-empire' => ['empire', 'oc-icon-empire'], + 'oc-icon-envelope' => ['envelope', 'oc-icon-envelope'], + 'oc-icon-envelope-o' => ['envelope-o', 'oc-icon-envelope-o'], + 'oc-icon-envelope-square' => ['envelope-square', 'oc-icon-envelope-square'], + 'oc-icon-eraser' => ['eraser', 'oc-icon-eraser'], + 'oc-icon-eur' => ['eur', 'oc-icon-eur'], + 'oc-icon-euro' => ['euro', 'oc-icon-euro'], + 'oc-icon-exchange' => ['exchange', 'oc-icon-exchange'], + 'oc-icon-exclamation' => ['exclamation', 'oc-icon-exclamation'], + 'oc-icon-exclamation-circle' => ['exclamation-circle', 'oc-icon-exclamation-circle'], + 'oc-icon-exclamation-triangle' => ['exclamation-triangle', 'oc-icon-exclamation-triangle'], + 'oc-icon-expand' => ['expand', 'oc-icon-expand'], + 'oc-icon-external-link' => ['external-link', 'oc-icon-external-link'], + 'oc-icon-external-link-square' => ['external-link-square', 'oc-icon-external-link-square'], + 'oc-icon-eye' => ['eye', 'oc-icon-eye'], + 'oc-icon-eye-slash' => ['eye-slash', 'oc-icon-eye-slash'], + 'oc-icon-eyedropper' => ['eyedropper', 'oc-icon-eyedropper'], + 'oc-icon-facebook' => ['facebook', 'oc-icon-facebook'], + 'oc-icon-facebook-f' => ['facebook-f', 'oc-icon-facebook-f'], + 'oc-icon-facebook-official' => ['facebook-official', 'oc-icon-facebook-official'], + 'oc-icon-facebook-square' => ['facebook-square', 'oc-icon-facebook-square'], + 'oc-icon-fast-backward' => ['fast-backward', 'oc-icon-fast-backward'], + 'oc-icon-fast-forward' => ['fast-forward', 'oc-icon-fast-forward'], + 'oc-icon-fax' => ['fax', 'oc-icon-fax'], + 'oc-icon-female' => ['female', 'oc-icon-female'], + 'oc-icon-fighter-jet' => ['fighter-jet', 'oc-icon-fighter-jet'], + 'oc-icon-file' => ['file', 'oc-icon-file'], + 'oc-icon-file-archive-o' => ['file-archive-o', 'oc-icon-file-archive-o'], + 'oc-icon-file-audio-o' => ['file-audio-o', 'oc-icon-file-audio-o'], + 'oc-icon-file-code-o' => ['file-code-o', 'oc-icon-file-code-o'], + 'oc-icon-file-excel-o' => ['file-excel-o', 'oc-icon-file-excel-o'], + 'oc-icon-file-image-o' => ['file-image-o', 'oc-icon-file-image-o'], + 'oc-icon-file-movie-o' => ['file-movie-o', 'oc-icon-file-movie-o'], + 'oc-icon-file-o' => ['file-o', 'oc-icon-file-o'], + 'oc-icon-file-pdf-o' => ['file-pdf-o', 'oc-icon-file-pdf-o'], + 'oc-icon-file-photo-o' => ['file-photo-o', 'oc-icon-file-photo-o'], + 'oc-icon-file-picture-o' => ['file-picture-o', 'oc-icon-file-picture-o'], + 'oc-icon-file-powerpoint-o' => ['file-powerpoint-o', 'oc-icon-file-powerpoint-o'], + 'oc-icon-file-sound-o' => ['file-sound-o', 'oc-icon-file-sound-o'], + 'oc-icon-file-text' => ['file-text', 'oc-icon-file-text'], + 'oc-icon-file-text-o' => ['file-text-o', 'oc-icon-file-text-o'], + 'oc-icon-file-video-o' => ['file-video-o', 'oc-icon-file-video-o'], + 'oc-icon-file-word-o' => ['file-word-o', 'oc-icon-file-word-o'], + 'oc-icon-file-zip-o' => ['file-zip-o', 'oc-icon-file-zip-o'], + 'oc-icon-files-o' => ['files-o', 'oc-icon-files-o'], + 'oc-icon-film' => ['film', 'oc-icon-film'], + 'oc-icon-filter' => ['filter', 'oc-icon-filter'], + 'oc-icon-fire' => ['fire', 'oc-icon-fire'], + 'oc-icon-fire-extinguisher' => ['fire-extinguisher', 'oc-icon-fire-extinguisher'], + 'oc-icon-flag' => ['flag', 'oc-icon-flag'], + 'oc-icon-flag-checkered' => ['flag-checkered', 'oc-icon-flag-checkered'], + 'oc-icon-flag-o' => ['flag-o', 'oc-icon-flag-o'], + 'oc-icon-flash' => ['flash', 'oc-icon-flash'], + 'oc-icon-flask' => ['flask', 'oc-icon-flask'], + 'oc-icon-flickr' => ['flickr', 'oc-icon-flickr'], + 'oc-icon-floppy-o' => ['floppy-o', 'oc-icon-floppy-o'], + 'oc-icon-folder' => ['folder', 'oc-icon-folder'], + 'oc-icon-folder-o' => ['folder-o', 'oc-icon-folder-o'], + 'oc-icon-folder-open' => ['folder-open', 'oc-icon-folder-open'], + 'oc-icon-folder-open-o' => ['folder-open-o', 'oc-icon-folder-open-o'], + 'oc-icon-font' => ['font', 'oc-icon-font'], + 'oc-icon-forumbee' => ['forumbee', 'oc-icon-forumbee'], + 'oc-icon-forward' => ['forward', 'oc-icon-forward'], + 'oc-icon-foursquare' => ['foursquare', 'oc-icon-foursquare'], + 'oc-icon-frown-o' => ['frown-o', 'oc-icon-frown-o'], + 'oc-icon-futbol-o' => ['futbol-o', 'oc-icon-futbol-o'], + 'oc-icon-gamepad' => ['gamepad', 'oc-icon-gamepad'], + 'oc-icon-gavel' => ['gavel', 'oc-icon-gavel'], + 'oc-icon-gbp' => ['gbp', 'oc-icon-gbp'], + 'oc-icon-ge' => ['ge', 'oc-icon-ge'], + 'oc-icon-gear' => ['gear', 'oc-icon-gear'], + 'oc-icon-gears' => ['gears', 'oc-icon-gears'], + 'oc-icon-genderless' => ['genderless', 'oc-icon-genderless'], + 'oc-icon-gift' => ['gift', 'oc-icon-gift'], + 'oc-icon-git' => ['git', 'oc-icon-git'], + 'oc-icon-git-square' => ['git-square', 'oc-icon-git-square'], + 'oc-icon-github' => ['github', 'oc-icon-github'], + 'oc-icon-github-alt' => ['github-alt', 'oc-icon-github-alt'], + 'oc-icon-github-square' => ['github-square', 'oc-icon-github-square'], + 'oc-icon-gittip' => ['gittip', 'oc-icon-gittip'], + 'oc-icon-glass' => ['glass', 'oc-icon-glass'], + 'oc-icon-globe' => ['globe', 'oc-icon-globe'], + 'oc-icon-google' => ['google', 'oc-icon-google'], + 'oc-icon-google-plus' => ['google-plus', 'oc-icon-google-plus'], + 'oc-icon-google-plus-square' => ['google-plus-square', 'oc-icon-google-plus-square'], + 'oc-icon-google-wallet' => ['google-wallet', 'oc-icon-google-wallet'], + 'oc-icon-graduation-cap' => ['graduation-cap', 'oc-icon-graduation-cap'], + 'oc-icon-gratipay' => ['gratipay', 'oc-icon-gratipay'], + 'oc-icon-group' => ['group', 'oc-icon-group'], + 'oc-icon-h-square' => ['h-square', 'oc-icon-h-square'], + 'oc-icon-hacker-news' => ['hacker-news', 'oc-icon-hacker-news'], + 'oc-icon-hand-o-down' => ['hand-o-down', 'oc-icon-hand-o-down'], + 'oc-icon-hand-o-left' => ['hand-o-left', 'oc-icon-hand-o-left'], + 'oc-icon-hand-o-right' => ['hand-o-right', 'oc-icon-hand-o-right'], + 'oc-icon-hand-o-up' => ['hand-o-up', 'oc-icon-hand-o-up'], + 'oc-icon-hdd-o' => ['hdd-o', 'oc-icon-hdd-o'], + 'oc-icon-header' => ['header', 'oc-icon-header'], + 'oc-icon-headphones' => ['headphones', 'oc-icon-headphones'], + 'oc-icon-heart' => ['heart', 'oc-icon-heart'], + 'oc-icon-heart-o' => ['heart-o', 'oc-icon-heart-o'], + 'oc-icon-heartbeat' => ['heartbeat', 'oc-icon-heartbeat'], + 'oc-icon-history' => ['history', 'oc-icon-history'], + 'oc-icon-home' => ['home', 'oc-icon-home'], + 'oc-icon-hospital-o' => ['hospital-o', 'oc-icon-hospital-o'], + 'oc-icon-hotel' => ['hotel', 'oc-icon-hotel'], + 'oc-icon-html5' => ['html5', '|'], + 'oc-icon-ils' => ['ils', 'oc-icon-ils'], + 'oc-icon-image' => ['image', 'oc-icon-image'], + 'oc-icon-inbox' => ['inbox', 'oc-icon-inbox'], + 'oc-icon-indent' => ['indent', 'oc-icon-indent'], + 'oc-icon-info' => ['info', 'oc-icon-info'], + 'oc-icon-info-circle' => ['info-circle', 'oc-icon-info-circle'], + 'oc-icon-inr' => ['inr', 'oc-icon-inr'], + 'oc-icon-instagram' => ['instagram', 'oc-icon-instagram'], + 'oc-icon-institution' => ['institution', 'oc-icon-institution'], + 'oc-icon-ioxhost' => ['ioxhost', 'oc-icon-ioxhost'], + 'oc-icon-italic' => ['italic', 'oc-icon-italic'], + 'oc-icon-joomla' => ['joomla', 'oc-icon-joomla'], + 'oc-icon-jpy' => ['jpy', 'oc-icon-jpy'], + 'oc-icon-jsfiddle' => ['jsfiddle', 'oc-icon-jsfiddle'], + 'oc-icon-key' => ['key', 'oc-icon-key'], + 'oc-icon-keyboard-o' => ['keyboard-o', 'oc-icon-keyboard-o'], + 'oc-icon-krw' => ['krw', 'oc-icon-krw'], + 'oc-icon-language' => ['language', 'oc-icon-language'], + 'oc-icon-laptop' => ['laptop', 'oc-icon-laptop'], + 'oc-icon-lastfm' => ['lastfm', 'oc-icon-lastfm'], + 'oc-icon-lastfm-square' => ['lastfm-square', 'oc-icon-lastfm-square'], + 'oc-icon-leaf' => ['leaf', 'oc-icon-leaf'], + 'oc-icon-leanpub' => ['leanpub', 'oc-icon-leanpub'], + 'oc-icon-legal' => ['legal', 'oc-icon-legal'], + 'oc-icon-lemon-o' => ['lemon-o', 'oc-icon-lemon-o'], + 'oc-icon-level-down' => ['level-down', 'oc-icon-level-down'], + 'oc-icon-level-up' => ['level-up', 'oc-icon-level-up'], + 'oc-icon-life-bouy' => ['life-bouy', 'oc-icon-life-bouy'], + 'oc-icon-lightbulb-o' => ['lightbulb-o', 'oc-icon-lightbulb-o'], + 'oc-icon-line-chart' => ['line-chart', 'oc-icon-line-chart'], + 'oc-icon-link' => ['link', 'oc-icon-link'], + 'oc-icon-linkedin' => ['linkedin', 'oc-icon-linkedin'], + 'oc-icon-linkedin-square' => ['linkedin-square', 'oc-icon-linkedin-square'], + 'oc-icon-linux' => ['linux', 'oc-icon-linux'], + 'oc-icon-list' => ['list', 'oc-icon-list'], + 'oc-icon-list-alt' => ['list-alt', 'oc-icon-list-alt'], + 'oc-icon-list-ol' => ['list-ol', 'oc-icon-list-ol'], + 'oc-icon-list-ul' => ['list-ul', 'oc-icon-list-ul'], + 'oc-icon-location-arrow' => ['location-arrow', 'oc-icon-location-arrow'], + 'oc-icon-lock' => ['lock', 'oc-icon-lock'], + 'oc-icon-long-arrow-down' => ['long-arrow-down', 'oc-icon-long-arrow-down'], + 'oc-icon-long-arrow-left' => ['long-arrow-left', 'oc-icon-long-arrow-left'], + 'oc-icon-long-arrow-right' => ['long-arrow-right', 'oc-icon-long-arrow-right'], + 'oc-icon-long-arrow-up' => ['long-arrow-up', 'oc-icon-long-arrow-up'], + 'oc-icon-magic' => ['magic', 'oc-icon-magic'], + 'oc-icon-magnet' => ['magnet', 'oc-icon-magnet'], + 'oc-icon-mail-forward' => ['mail-forward', 'oc-icon-mail-forward'], + 'oc-icon-mail-reply' => ['mail-reply', 'oc-icon-mail-reply'], + 'oc-icon-mail-reply-all' => ['mail-reply-all', 'oc-icon-mail-reply-all'], + 'oc-icon-male' => ['male', 'oc-icon-male'], + 'oc-icon-map-marker' => ['map-marker', 'oc-icon-map-marker'], + 'oc-icon-mars' => ['mars', 'oc-icon-mars'], + 'oc-icon-mars-double' => ['mars-double', 'oc-icon-mars-double'], + 'oc-icon-mars-stroke' => ['mars-stroke', 'oc-icon-mars-stroke'], + 'oc-icon-mars-stroke-h' => ['mars-stroke-h', 'oc-icon-mars-stroke-h'], + 'oc-icon-mars-stroke-v' => ['mars-stroke-v', 'oc-icon-mars-stroke-v'], + 'oc-icon-maxcdn' => ['maxcdn', 'oc-icon-maxcdn'], + 'oc-icon-meanpath' => ['meanpath', 'oc-icon-meanpath'], + 'oc-icon-medium' => ['medium', 'oc-icon-medium'], + 'oc-icon-medkit' => ['medkit', 'oc-icon-medkit'], + 'oc-icon-meh-o' => ['meh-o', 'oc-icon-meh-o'], + 'oc-icon-mercury' => ['mercury', 'oc-icon-mercury'], + 'oc-icon-microphone' => ['microphone', 'oc-icon-microphone'], + 'oc-icon-microphone-slash' => ['microphone-slash', 'oc-icon-microphone-slash'], + 'oc-icon-minus' => ['minus', 'oc-icon-minus'], + 'oc-icon-minus-circle' => ['minus-circle', 'oc-icon-minus-circle'], + 'oc-icon-minus-square' => ['minus-square', 'oc-icon-minus-square'], + 'oc-icon-minus-square-o' => ['minus-square-o', 'oc-icon-minus-square-o'], + 'oc-icon-mobile' => ['mobile', 'oc-icon-mobile'], + 'oc-icon-mobile-phone' => ['mobile-phone', 'oc-icon-mobile-phone'], + 'oc-icon-money' => ['money', 'oc-icon-money'], + 'oc-icon-moon-o' => ['moon-o', 'oc-icon-moon-o'], + 'oc-icon-mortar-board' => ['mortar-board', 'oc-icon-mortar-board'], + 'oc-icon-motorcycle' => ['motorcycle', 'oc-icon-motorcycle'], + 'oc-icon-music' => ['music', 'oc-icon-music'], + 'oc-icon-navicon' => ['navicon', 'oc-icon-navicon'], + 'oc-icon-neuter' => ['neuter', 'oc-icon-neuter'], + 'oc-icon-newspaper-o' => ['newspaper-o', 'oc-icon-newspaper-o'], + 'oc-icon-openid' => ['openid', 'oc-icon-openid'], + 'oc-icon-outdent' => ['outdent', 'oc-icon-outdent'], + 'oc-icon-pagelines' => ['pagelines', 'oc-icon-pagelines'], + 'oc-icon-paint-brush' => ['paint-brush', 'oc-icon-paint-brush'], + 'oc-icon-paper-plane' => ['paper-plane', 'oc-icon-paper-plane'], + 'oc-icon-paper-plane-o' => ['paper-plane-o', 'oc-icon-paper-plane-o'], + 'oc-icon-paperclip' => ['paperclip', 'oc-icon-paperclip'], + 'oc-icon-paragraph' => ['paragraph', 'oc-icon-paragraph'], + 'oc-icon-paste' => ['paste', 'oc-icon-paste'], + 'oc-icon-pause' => ['pause', 'oc-icon-pause'], + 'oc-icon-paw' => ['paw', 'oc-icon-paw'], + 'oc-icon-paypal' => ['paypal', 'oc-icon-paypal'], + 'oc-icon-pencil' => ['pencil', 'oc-icon-pencil'], + 'oc-icon-pencil-square' => ['pencil-square', 'oc-icon-pencil-square'], + 'oc-icon-pencil-square-o' => ['pencil-square-o', 'oc-icon-pencil-square-o'], + 'oc-icon-phone' => ['phone', 'oc-icon-phone'], + 'oc-icon-phone-square' => ['phone-square', 'oc-icon-phone-square'], + 'oc-icon-photo' => ['photo', 'oc-icon-photo'], + 'oc-icon-picture-o' => ['picture-o', 'oc-icon-picture-o'], + 'oc-icon-pie-chart' => ['pie-chart', 'oc-icon-pie-chart'], + 'oc-icon-pied-piper' => ['pied-piper', 'oc-icon-pied-piper'], + 'oc-icon-pied-piper-alt' => ['pied-piper-alt', 'oc-icon-pied-piper-alt'], + 'oc-icon-pinterest' => ['pinterest', 'oc-icon-pinterest'], + 'oc-icon-pinterest-p' => ['pinterest-p', 'oc-icon-pinterest-p'], + 'oc-icon-pinterest-square' => ['pinterest-square', 'oc-icon-pinterest-square'], + 'oc-icon-plane' => ['plane', 'oc-icon-plane'], + 'oc-icon-play' => ['play', 'oc-icon-play'], + 'oc-icon-play-circle' => ['play-circle', 'oc-icon-play-circle'], + 'oc-icon-play-circle-o' => ['play-circle-o', 'oc-icon-play-circle-o'], + 'oc-icon-plug' => ['plug', 'oc-icon-plug'], + 'oc-icon-plus' => ['plus', 'oc-icon-plus'], + 'oc-icon-plus-circle' => ['plus-circle', 'oc-icon-plus-circle'], + 'oc-icon-plus-square' => ['plus-square', 'oc-icon-plus-square'], + 'oc-icon-plus-square-o' => ['plus-square-o', 'oc-icon-plus-square-o'], + 'oc-icon-power-off' => ['power-off', 'oc-icon-power-off'], + 'oc-icon-print' => ['print', 'oc-icon-print'], + 'oc-icon-puzzle-piece' => ['puzzle-piece', 'oc-icon-puzzle-piece'], + 'oc-icon-qq' => ['qq', 'oc-icon-qq'], + 'oc-icon-qrcode' => ['qrcode', 'oc-icon-qrcode'], + 'oc-icon-question' => ['question', 'oc-icon-question'], + 'oc-icon-question-circle' => ['question-circle', 'oc-icon-question-circle'], + 'oc-icon-quote-left' => ['quote-left', 'oc-icon-quote-left'], + 'oc-icon-quote-right' => ['quote-right', 'oc-icon-quote-right'], + 'oc-icon-ra' => ['ra', 'oc-icon-ra'], + 'oc-icon-random' => ['random', 'oc-icon-random'], + 'oc-icon-rebel' => ['rebel', 'oc-icon-rebel'], + 'oc-icon-recycle' => ['recycle', 'oc-icon-recycle'], + 'oc-icon-reddit' => ['reddit', 'oc-icon-reddit'], + 'oc-icon-reddit-square' => ['reddit-square', 'oc-icon-reddit-square'], + 'oc-icon-refresh' => ['refresh', 'oc-icon-refresh'], + 'oc-icon-remove' => ['remove', 'oc-icon-remove'], + 'oc-icon-renren' => ['renren', 'oc-icon-renren'], + 'oc-icon-reorder' => ['reorder', 'oc-icon-reorder'], + 'oc-icon-repeat' => ['repeat', 'oc-icon-repeat'], + 'oc-icon-reply' => ['reply', 'oc-icon-reply'], + 'oc-icon-reply-all' => ['reply-all', 'oc-icon-reply-all'], + 'oc-icon-retweet' => ['retweet', 'oc-icon-retweet'], + 'oc-icon-rmb' => ['rmb', 'oc-icon-rmb'], + 'oc-icon-road' => ['road', 'oc-icon-road'], + 'oc-icon-rocket' => ['rocket', 'oc-icon-rocket'], + 'oc-icon-rotate-left' => ['rotate-left', 'oc-icon-rotate-left'], + 'oc-icon-rotate-right' => ['rotate-right', 'oc-icon-rotate-right'], + 'oc-icon-rouble' => ['rouble', 'oc-icon-rouble'], + 'oc-icon-rss' => ['rss', 'oc-icon-rss'], + 'oc-icon-rss-square' => ['rss-square', 'oc-icon-rss-square'], + 'oc-icon-rub' => ['rub', 'oc-icon-rub'], + 'oc-icon-ruble' => ['ruble', 'oc-icon-ruble'], + 'oc-icon-rupee' => ['rupee', 'oc-icon-rupee'], + 'oc-icon-save' => ['save', 'oc-icon-save'], + 'oc-icon-scissors' => ['scissors', 'oc-icon-scissors'], + 'oc-icon-search' => ['search', 'oc-icon-search'], + 'oc-icon-search-minus' => ['search-minus', 'oc-icon-search-minus'], + 'oc-icon-search-plus' => ['search-plus', 'oc-icon-search-plus'], + 'oc-icon-sellsy' => ['sellsy', 'oc-icon-sellsy'], + 'oc-icon-send' => ['send', 'oc-icon-send'], + 'oc-icon-send-o' => ['send-o', 'oc-icon-send-o'], + 'oc-icon-server' => ['server', 'oc-icon-server'], + 'oc-icon-share' => ['share', 'oc-icon-share'], + 'oc-icon-share-alt' => ['share-alt', 'oc-icon-share-alt'], + 'oc-icon-share-alt-square' => ['share-alt-square', 'oc-icon-share-alt-square'], + 'oc-icon-share-square' => ['share-square', 'oc-icon-share-square'], + 'oc-icon-share-square-o' => ['share-square-o', 'oc-icon-share-square-o'], + 'oc-icon-shekel' => ['shekel', 'oc-icon-shekel'], + 'oc-icon-sheqel' => ['sheqel', 'oc-icon-sheqel'], + 'oc-icon-shield' => ['shield', 'oc-icon-shield'], + 'oc-icon-ship' => ['ship', 'oc-icon-ship'], + 'oc-icon-shirtsinbulk' => ['shirtsinbulk', 'oc-icon-shirtsinbulk'], + 'oc-icon-shopping-cart' => ['shopping-cart', 'oc-icon-shopping-cart'], + 'oc-icon-sign-in' => ['sign-in', 'oc-icon-sign-in'], + 'oc-icon-sign-out' => ['sign-out', 'oc-icon-sign-out'], + 'oc-icon-signal' => ['signal', 'oc-icon-signal'], + 'oc-icon-simplybuilt' => ['simplybuilt', 'oc-icon-simplybuilt'], + 'oc-icon-sitemap' => ['sitemap', 'oc-icon-sitemap'], + 'oc-icon-skyatlas' => ['skyatlas', 'oc-icon-skyatlas'], + 'oc-icon-skype' => ['skype', 'oc-icon-skype'], + 'oc-icon-slack' => ['slack', 'oc-icon-slack'], + 'oc-icon-sliders' => ['sliders', 'oc-icon-sliders'], + 'oc-icon-slideshare' => ['slideshare', 'oc-icon-slideshare'], + 'oc-icon-smile-o' => ['smile-o', 'oc-icon-smile-o'], + 'oc-icon-soccer-ball-o' => ['soccer-ball-o', 'oc-icon-soccer-ball-o'], + 'oc-icon-sort' => ['sort', 'oc-icon-sort'], + 'oc-icon-sort-alpha-asc' => ['sort-alpha-asc', 'oc-icon-sort-alpha-asc'], + 'oc-icon-sort-alpha-desc' => ['sort-alpha-desc', 'oc-icon-sort-alpha-desc'], + 'oc-icon-sort-amount-asc' => ['sort-amount-asc', 'oc-icon-sort-amount-asc'], + 'oc-icon-sort-amount-desc' => ['sort-amount-desc', 'oc-icon-sort-amount-desc'], + 'oc-icon-sort-asc' => ['sort-asc', 'oc-icon-sort-asc'], + 'oc-icon-sort-desc' => ['sort-desc', 'oc-icon-sort-desc'], + 'oc-icon-sort-down' => ['sort-down', 'oc-icon-sort-down'], + 'oc-icon-sort-numeric-asc' => ['sort-numeric-asc', 'oc-icon-sort-numeric-asc'], + 'oc-icon-sort-numeric-desc' => ['sort-numeric-desc', 'oc-icon-sort-numeric-desc'], + 'oc-icon-sort-up' => ['sort-up', 'oc-icon-sort-up'], + 'oc-icon-soundcloud' => ['soundcloud', 'oc-icon-soundcloud'], + 'oc-icon-space-shuttle' => ['space-shuttle', 'oc-icon-space-shuttle'], + 'oc-icon-spinner' => ['spinner', 'oc-icon-spinner'], + 'oc-icon-spoon' => ['spoon', 'oc-icon-spoon'], + 'oc-icon-spotify' => ['spotify', 'oc-icon-spotify'], + 'oc-icon-square' => ['square', 'oc-icon-square'], + 'oc-icon-square-o' => ['square-o', 'oc-icon-square-o'], + 'oc-icon-stack-exchange' => ['stack-exchange', 'oc-icon-stack-exchange'], + 'oc-icon-stack-overflow' => ['stack-overflow', 'oc-icon-stack-overflow'], + 'oc-icon-star' => ['star', 'oc-icon-star'], + 'oc-icon-star-half' => ['star-half', 'oc-icon-star-half'], + 'oc-icon-star-half-empty' => ['star-half-empty', 'oc-icon-star-half-empty'], + 'oc-icon-star-half-full' => ['star-half-full', 'oc-icon-star-half-full'], + 'oc-icon-star-half-o' => ['star-half-o', 'oc-icon-star-half-o'], + 'oc-icon-star-o' => ['star-o', 'oc-icon-star-o'], + 'oc-icon-steam' => ['steam', 'oc-icon-steam'], + 'oc-icon-steam-square' => ['steam-square', 'oc-icon-steam-square'], + 'oc-icon-step-backward' => ['step-backward', 'oc-icon-step-backward'], + 'oc-icon-step-forward' => ['step-forward', 'oc-icon-step-forward'], + 'oc-icon-stethoscope' => ['stethoscope', 'oc-icon-stethoscope'], + 'oc-icon-stop' => ['stop', 'oc-icon-stop'], + 'oc-icon-street-view' => ['street-view', 'oc-icon-street-view'], + 'oc-icon-strikethrough' => ['strikethrough', 'oc-icon-strikethrough'], + 'oc-icon-stumbleupon' => ['stumbleupon', 'oc-icon-stumbleupon'], + 'oc-icon-stumbleupon-circle' => ['stumbleupon-circle', 'oc-icon-stumbleupon-circle'], + 'oc-icon-subscript' => ['subscript', 'oc-icon-subscript'], + 'oc-icon-subway' => ['subway', 'oc-icon-subway'], + 'oc-icon-suitcase' => ['suitcase', 'oc-icon-suitcase'], + 'oc-icon-sun-o' => ['sun-o', 'oc-icon-sun-o'], + 'oc-icon-superscript' => ['superscript', 'oc-icon-superscript'], + 'oc-icon-support' => ['support', 'oc-icon-support'], + 'oc-icon-table' => ['table', 'oc-icon-table'], + 'oc-icon-tablet' => ['tablet', 'oc-icon-tablet'], + 'oc-icon-tachometer' => ['tachometer', 'oc-icon-tachometer'], + 'oc-icon-tag' => ['tag', 'oc-icon-tag'], + 'oc-icon-tags' => ['tags', 'oc-icon-tags'], + 'oc-icon-tasks' => ['tasks', 'oc-icon-tasks'], + 'oc-icon-taxi' => ['taxi', 'oc-icon-taxi'], + 'oc-icon-tencent-weibo' => ['tencent-weibo', 'oc-icon-tencent-weibo'], + 'oc-icon-terminal' => ['terminal', 'oc-icon-terminal'], + 'oc-icon-text-height' => ['text-height', 'oc-icon-text-height'], + 'oc-icon-text-width' => ['text-width', 'oc-icon-text-width'], + 'oc-icon-th' => ['th', 'oc-icon-th'], + 'oc-icon-th-large' => ['th-large', 'oc-icon-th-large'], + 'oc-icon-th-list' => ['th-list', 'oc-icon-th-list'], + 'oc-icon-thumb-tack' => ['thumb-tack', 'oc-icon-thumb-tack'], + 'oc-icon-thumbs-down' => ['thumbs-down', 'oc-icon-thumbs-down'], + 'oc-icon-thumbs-o-down' => ['thumbs-o-down', 'oc-icon-thumbs-o-down'], + 'oc-icon-thumbs-o-up' => ['thumbs-o-up', 'oc-icon-thumbs-o-up'], + 'oc-icon-thumbs-up' => ['thumbs-up', 'oc-icon-thumbs-up'], + 'oc-icon-ticket' => ['ticket', 'oc-icon-ticket'], + 'oc-icon-times' => ['times', 'oc-icon-times'], + 'oc-icon-times-circle' => ['times-circle', 'oc-icon-times-circle'], + 'oc-icon-times-circle-o' => ['times-circle-o', 'oc-icon-times-circle-o'], + 'oc-icon-tint' => ['tint', 'oc-icon-tint'], + 'oc-icon-toggle-down' => ['toggle-down', 'oc-icon-toggle-down'], + 'oc-icon-toggle-left' => ['toggle-left', 'oc-icon-toggle-left'], + 'oc-icon-toggle-off' => ['toggle-off', 'oc-icon-toggle-off'], + 'oc-icon-toggle-on' => ['toggle-on', 'oc-icon-toggle-on'], + 'oc-icon-toggle-right' => ['toggle-right', 'oc-icon-toggle-right'], + 'oc-icon-toggle-up' => ['toggle-up', 'oc-icon-toggle-up'], + 'oc-icon-train' => ['train', 'oc-icon-train'], + 'oc-icon-transgender' => ['transgender', 'oc-icon-transgender'], + 'oc-icon-transgender-alt' => ['transgender-alt', 'oc-icon-transgender-alt'], + 'oc-icon-trash' => ['trash', 'oc-icon-trash'], + 'oc-icon-trash-o' => ['trash-o', 'oc-icon-trash-o'], + 'oc-icon-tree' => ['tree', 'oc-icon-tree'], + 'oc-icon-trello' => ['trello', 'oc-icon-trello'], + 'oc-icon-trophy' => ['trophy', 'oc-icon-trophy'], + 'oc-icon-truck' => ['truck', 'oc-icon-truck'], + 'oc-icon-try' => ['try', 'oc-icon-try'], + 'oc-icon-tty' => ['tty', 'oc-icon-tty'], + 'oc-icon-tumblr' => ['tumblr', 'oc-icon-tumblr'], + 'oc-icon-tumblr-square' => ['tumblr-square', 'oc-icon-tumblr-square'], + 'oc-icon-turkish-lira' => ['turkish-lira', 'oc-icon-turkish-lira'], + 'oc-icon-twitch' => ['twitch', 'oc-icon-twitch'], + 'oc-icon-twitter' => ['twitter', 'oc-icon-twitter'], + 'oc-icon-twitter-square' => ['twitter-square', 'oc-icon-twitter-square'], + 'oc-icon-umbrella' => ['umbrella', 'oc-icon-umbrella'], + 'oc-icon-underline' => ['underline', 'oc-icon-underline'], + 'oc-icon-undo' => ['undo', 'oc-icon-undo'], + 'oc-icon-university' => ['university', 'oc-icon-university'], + 'oc-icon-unlink' => ['unlink', 'oc-icon-unlink'], + 'oc-icon-unlock' => ['unlock', 'oc-icon-unlock'], + 'oc-icon-unlock-alt' => ['unlock-alt', 'oc-icon-unlock-alt'], + 'oc-icon-unsorted' => ['unsorted', 'oc-icon-unsorted'], + 'oc-icon-upload' => ['upload', 'oc-icon-upload'], + 'oc-icon-usd' => ['usd', 'oc-icon-usd'], + 'oc-icon-user' => ['user', 'oc-icon-user'], + 'oc-icon-user-md' => ['user-md', 'oc-icon-user-md'], + 'oc-icon-user-plus' => ['user-plus', 'oc-icon-user-plus'], + 'oc-icon-user-secret' => ['user-secret', 'oc-icon-user-secret'], + 'oc-icon-user-times' => ['user-times', 'oc-icon-user-times'], + 'oc-icon-users' => ['users', 'oc-icon-users'], + 'oc-icon-venus' => ['venus', 'oc-icon-venus'], + 'oc-icon-venus-double' => ['venus-double', 'oc-icon-venus-double'], + 'oc-icon-venus-mars' => ['venus-mars', 'oc-icon-venus-mars'], + 'oc-icon-viacoin' => ['viacoin', 'oc-icon-viacoin'], + 'oc-icon-video-camera' => ['video-camera', 'oc-icon-video-camera'], + 'oc-icon-vimeo-square' => ['vimeo-square', 'oc-icon-vimeo-square'], + 'oc-icon-vine' => ['vine', 'oc-icon-vine'], + 'oc-icon-vk' => ['vk', 'oc-icon-vk'], + 'oc-icon-volume-down' => ['volume-down', 'oc-icon-volume-down'], + 'oc-icon-volume-off' => ['volume-off', 'oc-icon-volume-off'], + 'oc-icon-volume-up' => ['volume-up', 'oc-icon-volume-up'], + 'oc-icon-warning' => ['warning', 'oc-icon-warning'], + 'oc-icon-wechat' => ['wechat', 'oc-icon-wechat'], + 'oc-icon-weibo' => ['weibo', 'oc-icon-weibo'], + 'oc-icon-weixin' => ['weixin', 'oc-icon-weixin'], + 'oc-icon-whatsapp' => ['whatsapp', 'oc-icon-whatsapp'], + 'oc-icon-wheelchair' => ['wheelchair', 'oc-icon-wheelchair'], + 'oc-icon-wifi' => ['wifi', 'oc-icon-wifi'], + 'oc-icon-windows' => ['windows', 'oc-icon-windows'], + 'oc-icon-won' => ['won', 'oc-icon-won'], + 'oc-icon-wordpress' => ['wordpress', 'oc-icon-wordpress'], + 'oc-icon-wrench' => ['wrench', 'oc-icon-wrench'], + 'oc-icon-xing' => ['xing', 'oc-icon-xing'], + 'oc-icon-xing-square' => ['xing-square', 'oc-icon-xing-square'], + 'oc-icon-yahoo' => ['yahoo', 'oc-icon-yahoo'], + 'oc-icon-yelp' => ['yelp', 'oc-icon-yelp'], + 'oc-icon-yen' => ['yen', 'oc-icon-yen'], + 'oc-icon-youtube' => ['youtube', 'oc-icon-youtube'], + 'oc-icon-youtube-play' => ['youtube-play', 'oc-icon-youtube-play'], + 'oc-icon-youtube-square' => ['youtube-square', 'oc-icon-youtube-square'] + ]; + } +} diff --git a/plugins/rainlab/builder/classes/IndexOperationsBehaviorBase.php b/plugins/rainlab/builder/classes/IndexOperationsBehaviorBase.php new file mode 100644 index 0000000..8929d86 --- /dev/null +++ b/plugins/rainlab/builder/classes/IndexOperationsBehaviorBase.php @@ -0,0 +1,78 @@ +getPluginCode(); + $pluginCode = $pluginCodeObj->toCode(); + $widget = $this->makeBaseFormWidget($pluginCode, ['alias' => $alias]); + $widget->bindToController(); + return $widget; + } + + /** + * makeBaseFormWidget + */ + protected function makeBaseFormWidget($modelCode, $options = []) + { + if (!strlen($this->baseFormConfigFile)) { + throw new ApplicationException(sprintf('Base form configuration file is not specified for %s behavior', get_class($this))); + } + + $widgetConfig = $this->makeConfig($this->baseFormConfigFile); + $widgetConfig->model = $this->loadOrCreateBaseModel($modelCode, $options); + $widgetConfig->alias = $options['alias'] ?? 'form_'.md5(get_class($this)).uniqid(); + + $widgetConfig = $this->extendBaseFormWidgetConfig($widgetConfig); + + $form = $this->makeWidget(\Backend\Widgets\Form::class, $widgetConfig); + $form->context = strlen($modelCode) ? 'update' : 'create'; + + return $form; + } + + /** + * extendBaseFormWidgetConfig + */ + protected function extendBaseFormWidgetConfig($config) + { + return $config; + } + + /** + * getPluginCode + */ + protected function getPluginCode() + { + $vector = $this->controller->getBuilderActivePluginVector(); + + if (!$vector) { + throw new ApplicationException('Cannot determine the currently active plugin.'); + } + + return $vector->pluginCodeObj; + } + + /** + * loadOrCreateBaseModel + */ + abstract protected function loadOrCreateBaseModel($modelCode, $options = []); +} diff --git a/plugins/rainlab/builder/classes/LanguageMixer.php b/plugins/rainlab/builder/classes/LanguageMixer.php new file mode 100644 index 0000000..70a2294 --- /dev/null +++ b/plugins/rainlab/builder/classes/LanguageMixer.php @@ -0,0 +1,196 @@ + '', + 'mismatch' => false, + 'updatedLines' => [], + ]; + + try { + $destArray = Yaml::parse($destContents); + } + catch (Exception $ex) { + throw new ApplicationException(sprintf('Cannot parse the YAML content: %s', $ex->getMessage())); + } + + if (!$destArray) { + $result['strings'] = $this->arrayToYaml($srcArray); + return $result; + } + + $mismatch = false; + $missingPaths = $this->findMissingPaths($destArray, $srcArray, $mismatch); + $mergedArray = self::arrayMergeRecursive($srcArray, $destArray); + + $destStrings = $this->arrayToYaml($mergedArray); + $addedLines = $this->getAddedLines($destStrings, $missingPaths); + + $result['strings'] = $destStrings; + $result['updatedLines'] = $addedLines['lines']; + $result['mismatch'] = $mismatch || $addedLines['mismatch']; + + return $result; + } + + public static function arrayMergeRecursive(&$array1, &$array2) + { + // The native PHP implementation of array_merge_recursive + // generates unexpected results when two scalar elements with a + // same key is found, so we use a custom one. + + $result = $array1; + + foreach ($array2 as $key => &$value) { + if (is_array($value) && isset($result[$key]) && is_array($result[$key])) { + $result[$key] = self::arrayMergeRecursive($result[$key], $value); + } else { + $result[$key] = $value; + } + } + + return $result; + } + + protected function findMissingPaths($destArray, $srcArray, &$mismatch) + { + $result = []; + $mismatch = false; + $this->findMissingPathsRecursive($destArray, $srcArray, $result, [], $mismatch); + + return $result; + } + + protected function findMissingPathsRecursive($destArray, $srcArray, &$result, $currentPath, &$mismatch) + { + foreach ($srcArray as $key => $value) { + $newPath = array_merge($currentPath, [$key]); + $pathValue = null; + $pathExists = $this->pathExistsInArray($destArray, $newPath, $pathValue); + + if (!$pathExists) { + $result[] = $newPath; + } + + if (is_array($value)) { + $this->findMissingPathsRecursive($destArray, $value, $result, $newPath, $mismatch); + } + else { + // Detect the case when the value in the destination file + // is an array, when the value in the source file a is a string. + if ($pathExists && is_array($pathValue)) { + $mismatch = true; + } + } + } + } + + protected function pathExistsInArray($array, $path, &$value) + { + $currentArray = $array; + + while ($path) { + $currentPath = array_shift($path); + + if (!is_array($currentArray)) { + return false; + } + + if (!array_key_exists($currentPath, $currentArray)) { + return false; + } + + $currentArray = $currentArray[$currentPath]; + } + + $value = $currentArray; + return true; + } + + protected function arrayToYaml($array) + { + $dumper = new YamlDumper(); + return $dumper->dump($array, 20, 0, false, true); + } + + protected function getAddedLines($strings, $paths) + { + $result = [ + 'lines' => [], + 'mismatch' => false + ]; + + foreach ($paths as $path) { + $line = $this->getLineForPath($strings, $path); + + if ($line !== false) { + $result['lines'][] = $line; + } + else { + $result['mismatch'] = true; + } + } + + return $result; + } + + protected function getLineForPath($strings, $path) + { + $strings = str_replace("\n\r", "\n", trim($strings)); + $lines = explode("\n", $strings); + + $lineCount = count($lines); + $currentLineIndex = 0; + foreach ($path as $indentaion => $key) { + $expectedKeyDefinition = str_repeat(' ', $indentaion).$key.':'; + + $firstLineAfterKey = true; + for ($lineIndex = $currentLineIndex; $lineIndex < $lineCount; $lineIndex++) { + $line = $lines[$lineIndex]; + + if (!$firstLineAfterKey) { + $lineIndentation = 0; + if (preg_match('/^\s+/', $line, $matches)) { + $lineIndentation = strlen($matches[0])/4; + } + + if ($lineIndentation < $indentaion) { + continue; // Don't allow entering wrong branches + } + } + + $firstLineAfterKey = false; + + if (strpos($line, $expectedKeyDefinition) === 0) { + $currentLineIndex = $lineIndex; + continue 2; + } + } + + // If the key wasn't found in the text, there is + // a structure difference between the source an destination + // languages - for example when a string key was replaced + // with an array of strings. + return false; + } + + return $currentLineIndex; + } +} diff --git a/plugins/rainlab/builder/classes/MigrationColumnType.php b/plugins/rainlab/builder/classes/MigrationColumnType.php new file mode 100644 index 0000000..2e7da46 --- /dev/null +++ b/plugins/rainlab/builder/classes/MigrationColumnType.php @@ -0,0 +1,234 @@ + DoctrineType::INTEGER, + self::TYPE_SMALLINTEGER => DoctrineType::SMALLINT, + self::TYPE_BIGINTEGER => DoctrineType::BIGINT, + self::TYPE_DATE => DoctrineType::DATE_MUTABLE, + self::TYPE_TIME => DoctrineType::TIME_MUTABLE, + self::TYPE_DATETIME => DoctrineType::DATETIME_MUTABLE, + self::TYPE_TIMESTAMP => DoctrineType::DATETIME_MUTABLE, + self::TYPE_STRING => DoctrineType::STRING, + self::TYPE_TEXT => DoctrineType::TEXT, + self::TYPE_BINARY => DoctrineType::BLOB, + self::TYPE_BOOLEAN => DoctrineType::BOOLEAN, + self::TYPE_DECIMAL => DoctrineType::DECIMAL, + self::TYPE_DOUBLE => DoctrineType::FLOAT + ]; + } + + /** + * Converts a migration column type to a corresponding Doctrine mapping type name. + */ + public static function toDoctrineTypeName($type) + { + $typeMap = self::getDoctrineTypeMap(); + + if (!array_key_exists($type, $typeMap)) { + throw new SystemException(sprintf('Unknown column type: %s', $type)); + } + + return $typeMap[$type]; + } + + /** + * Converts Doctrine mapping type name to a migration column method name + */ + public static function toMigrationMethodName($type, $columnName) + { + $typeMap = self::getDoctrineTypeMap(); + + if (!in_array($type, $typeMap)) { + throw new SystemException(sprintf('Unknown column type: %s', $type)); + } + + // Some Doctrine types map to multiple migration types, for example + // Doctrine boolean could be boolean and tinyInteger in migrations. + // Some guessing could be required in this method. The method is not + // 100% reliable. + + if ($type == DoctrineType::DATETIME_MUTABLE) { + // The datetime type maps to datetime and timestamp. Use the name + // guessing as the only possible solution. + + if (in_array($columnName, ['created_at', 'updated_at', 'deleted_at', 'published_at', 'deleted_at'])) { + return self::TYPE_TIMESTAMP; + } + + return self::TYPE_DATETIME; + } + + $typeMap = array_flip($typeMap); + return $typeMap[$type]; + } + + /** + * Validates the column length parameter basing on the column type + */ + public static function validateLength($type, $value) + { + $value = trim($value); + + if (!strlen($value)) { + return; + } + + if (in_array($type, self::getDecimalTypes())) { + if (!preg_match(self::REGEX_LENGTH_DOUBLE, $value)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_table_decimal_length', [ + 'type' => $type + ])); + } + } else { + if (!preg_match(self::REGEX_LENGTH_SINGLE, $value)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_table_length', [ + 'type' => $type + ])); + } + } + } + + /** + * Returns an array containing a column length, precision and scale, basing on the column type. + */ + public static function lengthToPrecisionAndScale($type, $length) + { + $length = trim($length); + + if (!strlen($length)) { + return []; + } + + $result = [ + 'length' => null, + 'precision' => null, + 'scale' => null + ]; + + if (in_array($type, self::getDecimalTypes())) { + $matches = []; + + if (!preg_match(self::REGEX_LENGTH_DOUBLE, $length, $matches)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_table_length', [ + 'type' => $type + ])); + } + + $result['precision'] = $matches[1]; + $result['scale'] = $matches[2]; + + return $result; + } + + if (in_array($type, self::getIntegerTypes())) { + if (!preg_match(self::REGEX_LENGTH_SINGLE, $length)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_table_length', [ + 'type' => $type + ])); + } + + $result['precision'] = $length; + $result['scale'] = 0; + + return $result; + } + + $result['length'] = $length; + return $result; + } + + /** + * doctrineLengthToMigrationLength converts Doctrine length, precision and scale to migration-compatible length string + * @return string + */ + public static function doctrineLengthToMigrationLength($column) + { + $typeName = $column->getType()->getName(); + $migrationTypeName = self::toMigrationMethodName($typeName, $column->getName()); + + if (in_array($migrationTypeName, self::getDecimalTypes())) { + return $column->getPrecision().','.$column->getScale(); + } + + if (in_array($migrationTypeName, self::getIntegerTypes())) { + return $column->getPrecision(); + } + + return $column->getLength(); + } +} diff --git a/plugins/rainlab/builder/classes/MigrationFileParser.php b/plugins/rainlab/builder/classes/MigrationFileParser.php new file mode 100644 index 0000000..6efb900 --- /dev/null +++ b/plugins/rainlab/builder/classes/MigrationFileParser.php @@ -0,0 +1,77 @@ +forward()) { + $tokenCode = $stream->getCurrentCode(); + + if ($tokenCode == T_NAMESPACE) { + $namespace = $this->extractNamespace($stream); + if ($namespace === null) { + return null; + } + + $result['namespace'] = $namespace; + } + + if ($tokenCode == T_CLASS) { + $className = $this->extractClassName($stream); + if ($className === null) { + return null; + } + + $result['class'] = $className; + } + } + + if (!$result) { + return null; + } + + return $result; + } + + protected function extractClassName($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return null; + } + + return $stream->getNextExpectedTerminated([T_STRING], [T_WHITESPACE, ';']); + } + + protected function extractNamespace($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return null; + } + + $expected = [T_STRING, T_NS_SEPARATOR]; + + // Namespace string on PHP 8.0 returns code 314 (T_NAME_QUALIFIED) + // @deprecated combine when min req > php 8 + if (defined('T_NAME_QUALIFIED') && T_NAME_QUALIFIED > 0) { + $expected[] = T_NAME_QUALIFIED; + } + + return $stream->getNextExpectedTerminated($expected, [T_WHITESPACE, ';']); + } +} diff --git a/plugins/rainlab/builder/classes/ModelFileParser.php b/plugins/rainlab/builder/classes/ModelFileParser.php new file mode 100644 index 0000000..8770760 --- /dev/null +++ b/plugins/rainlab/builder/classes/ModelFileParser.php @@ -0,0 +1,200 @@ +forward()) { + $tokenCode = $stream->getCurrentCode(); + + if ($tokenCode == T_NAMESPACE) { + $namespace = $this->extractNamespace($stream); + if ($namespace === null) { + return null; + } + + $result['namespace'] = $namespace; + } + + if ($tokenCode == T_CLASS && !isset($result['class'])) { + $className = $this->extractClassName($stream); + if ($className === null) { + return null; + } + + $result['class'] = $className; + } + + if ($tokenCode == T_PUBLIC || $tokenCode == T_PROTECTED) { + $tableName = $this->extractTableName($stream); + if ($tableName === false) { + continue; + } + + if ($tableName === null) { + return null; + } + + $result['table'] = $tableName; + } + } + + if (!$result) { + return null; + } + + return $result; + } + + /** + * Extracts names and types of model relations. + * @param string $fileContents Specifies the file contents. + * @return array|null Returns an array with keys matching the relation types and values containing relation names as array. + * Returns null if the parsing fails. + */ + public function extractModelRelationsFromSource($fileContents) + { + $result = []; + + $stream = new PhpSourceStream($fileContents); + + while ($stream->forward()) { + $tokenCode = $stream->getCurrentCode(); + + if ($tokenCode == T_PUBLIC) { + $relations = $this->extractRelations($stream); + if ($relations === false) { + continue; + } + } + } + + if (!$result) { + return null; + } + + return $result; + } + + /** + * extractNamespace from model info + */ + protected function extractNamespace($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return null; + } + + $expected = [T_STRING, T_NS_SEPARATOR]; + + // Namespace string on PHP 8.0 returns code 314 (T_NAME_QUALIFIED) + // @deprecated combine when min req > php 8 + if (defined('T_NAME_QUALIFIED') && T_NAME_QUALIFIED > 0) { + $expected[] = T_NAME_QUALIFIED; + } + + return $stream->getNextExpectedTerminated($expected, [T_WHITESPACE, ';']); + } + + /** + * extractClassName from model info + */ + protected function extractClassName($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return null; + } + + return $stream->getNextExpectedTerminated([T_STRING], [T_WHITESPACE, ';']); + } + + /** + * Returns the table name. This method would return null in case if the + * $table variable was found, but it value cannot be read. If the variable + * is not found, the method returns false, allowing the outer loop to go to + * the next token. + */ + protected function extractTableName($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return false; + } + + if ($stream->getNextExpected(T_VARIABLE) === null) { + return false; + } + + if ($stream->getCurrentText() != '$table') { + return false; + } + + if ($stream->getNextExpectedTerminated(['=', T_WHITESPACE], [T_CONSTANT_ENCAPSED_STRING]) === null) { + return null; + } + + $tableName = $stream->getCurrentText(); + $tableName = trim($tableName, '\''); + $tableName = trim($tableName, '"'); + + return $tableName; + } + + protected function extractRelations($stream) + { + if ($stream->getNextExpected(T_WHITESPACE) === null) { + return false; + } + + if ($stream->getNextExpected(T_VARIABLE) === null) { + return false; + } + + $relationTypes = [ + 'belongsTo', + 'belongsToMany', + 'attachMany', + 'hasMany', + 'morphToMany', + 'morphedByMany', + 'morphMany', + 'hasManyThrough' + ]; + + $relationType = null; + $currentText = $stream->getCurrentText(); + + foreach ($relationTypes as $type) { + if ($currentText == '$'.$type) { + $relationType = $type; + break; + } + } + + if (!$relationType) { + return false; + } + + if ($stream->getNextExpectedTerminated(['=', T_WHITESPACE], ['[']) === null) { + return null; + } + + // The implementation is not finished and postponed. Relation definition could + // be quite complex and contain nested arrays. + } +} diff --git a/plugins/rainlab/builder/classes/PhpSourceStream.php b/plugins/rainlab/builder/classes/PhpSourceStream.php new file mode 100644 index 0000000..1412fb8 --- /dev/null +++ b/plugins/rainlab/builder/classes/PhpSourceStream.php @@ -0,0 +1,259 @@ +tokens = token_get_all($fileContents); + } + + /** + * Moves head to the beginning and cleans the internal bookmarks. + */ + public function reset() + { + $this->head = 0; + $this->headBookmarks = []; + } + + public function getHead() + { + return $this->head; + } + + /** + * Updates the head position. + * @return boolean Returns true if the head was successfully updated. Returns false otherwise. + */ + public function setHead($head) + { + if ($head < 0) { + return false; + } + + if ($head > (count($this->tokens) - 1)) { + return false; + } + + $this->head = $head; + return true; + } + + /** + * Bookmarks the head position in the internal bookmark stack. + */ + public function bookmarkHead() + { + array_push($this->headBookmarks, $this->head); + } + + /** + * Restores the head position from the last stored bookmark. + */ + public function restoreBookmark() + { + $head = array_pop($this->headBookmarks); + if ($head === null) { + throw new SystemException("Can't restore PHP token stream bookmark - the bookmark doesn't exist"); + } + + return $this->setHead($head); + } + + /** + * Discards the last stored bookmark without changing the head position. + */ + public function discardBookmark() + { + $head = array_pop($this->headBookmarks); + if ($head === null) { + throw new SystemException("Can't discard PHP token stream bookmark - the bookmark doesn't exist"); + } + } + + /** + * Returns the current token and doesn't move the head. + */ + public function getCurrent() + { + return $this->tokens[$this->head]; + } + + /** + * Returns the current token's text and doesn't move the head. + */ + public function getCurrentText() + { + $token = $this->getCurrent(); + if (!is_array($token)) { + return $token; + } + + return $token[1]; + } + + /** + * Returns the current token's code and doesn't move the head. + */ + public function getCurrentCode() + { + $token = $this->getCurrent(); + if (!is_array($token)) { + return null; + } + + return $token[0]; + } + + /** + * Returns the next token and moves the head forward. + */ + public function getNext() + { + $nextIndex = $this->head + 1; + if (!array_key_exists($nextIndex, $this->tokens)) { + return null; + } + + $this->head = $nextIndex; + return $this->tokens[$nextIndex]; + } + + /** + * Reads the next token, updates the head and and returns the token if it has the expected code. + * @param integer $expectedCode Specifies the code to expect. + * @return mixed Returns the token or null if the token code was not expected. + */ + public function getNextExpected($expectedCode) + { + $token = $this->getNext(); + if ($this->getCurrentCode() != $expectedCode) { + return null; + } + + return $token; + } + + /** + * Reads expected tokens, until the termination token is found. + * If any unexpected token is found before the termination token, returns null. + * If the method succeeds, the head is positioned on the termination token. + * @param array $expectedCodesOrValues Specifies the expected codes or token values. + * @param integer|string|array $terminationToken Specifies the termination token text or code. + * The termination tokens could be specified as array. + * @return string|null Returns the tokens text or null + */ + public function getNextExpectedTerminated($expectedCodesOrValues, $terminationToken, $skipToken = []) + { + $buffer = null; + + if (!is_array($skipToken)) { + $skipToken = [$skipToken]; + } + + if (!is_array($terminationToken)) { + $terminationToken = [$terminationToken]; + } + + while (($nextToken = $this->getNext()) !== null) { + $code = $this->getCurrentCode(); + $text = $this->getCurrentText(); + + if (in_array($code, $expectedCodesOrValues) || in_array($text, $expectedCodesOrValues)) { + $buffer .= $text; + continue; + } + + if (in_array($code, $terminationToken) || in_array($text, $terminationToken)) { + return $buffer; + } + + if (in_array($code, $skipToken) || in_array($text, $skipToken)) { + continue; + } + + // The token should be either expected or termination. + // If something else is found, return null. + return null; + } + + return $buffer; + } + + /** + * Moves the head forward. + * @return boolean Returns true if the head was successfully moved. + * Returns false if the head can't be moved because it has reached the end of the steam. + */ + public function forward() + { + return $this->setHead($this->getHead()+1); + } + + /** + * Moves the head backward. + * @return boolean Returns true if the head was successfully moved. + * Returns false if the head can't be moved because it has reached the beginning of the steam. + */ + public function back() + { + return $this->setHead($this->getHead()-1); + } + + /** + * getTextToSemicolon returns the stream text from the head position to the next + * semicolon and updates the head. If the method succeeds, the head is positioned + * on the semicolon. + */ + public function getTextToSemicolon() + { + $buffer = null; + + while (($nextToken = $this->getNext()) !== null) { + if ($nextToken == ';') { + return $buffer; + } + + $buffer .= $this->getCurrentText(); + } + + // The semicolon wasn't found. + return null; + } + + /** + * unquotePhpString + */ + public function unquotePhpString($string, $default = false) + { + if ((substr($string, 0, 1) === '\'' && substr($string, -1) === '\'') || + (substr($string, 0, 1) === '"' && substr($string, -1) === '"')) { + return substr($string, 1, -1); + } + + return $default; + } +} diff --git a/plugins/rainlab/builder/classes/PluginCode.php b/plugins/rainlab/builder/classes/PluginCode.php new file mode 100644 index 0000000..1eef2b3 --- /dev/null +++ b/plugins/rainlab/builder/classes/PluginCode.php @@ -0,0 +1,170 @@ +validateCodeWord($authorCode) || !$this->validateCodeWord($pluginCode)) { + throw new ApplicationException(sprintf('Invalid plugin code: %s', $pluginCodeStr)); + } + + $this->authorCode = trim($authorCode); + $this->pluginCode = trim($pluginCode); + } + + /** + * createFromNamespace + */ + public static function createFromNamespace($namespace) + { + $namespaceParts = explode('\\', $namespace); + if (count($namespaceParts) < 2) { + throw new ApplicationException('Invalid plugin namespace value.'); + } + + $authorCode = $namespaceParts[0]; + $pluginCode = $namespaceParts[1]; + + return new self($authorCode.'.'.$pluginCode); + } + + /** + * toPluginNamespace + */ + public function toPluginNamespace() + { + return $this->authorCode.'\\'.$this->pluginCode; + } + + /** + * toUrl + */ + public function toUrl() + { + return strtolower($this->authorCode).'/'.strtolower($this->pluginCode); + } + + /** + * toUpdatesNamespace + */ + public function toUpdatesNamespace() + { + return $this->toPluginNamespace().'\\Updates'; + } + + /** + * toFilesystemPath + */ + public function toFilesystemPath() + { + return strtolower($this->authorCode.'/'.$this->pluginCode); + } + + /** + * toCode + */ + public function toCode() + { + return $this->authorCode.'.'.$this->pluginCode; + } + + /** + * toPluginFilePath + */ + public function toPluginFilePath() + { + return '$/'.$this->toFilesystemPath().'/plugin.yaml'; + } + + /** + * toPluginInformationFilePath + */ + public function toPluginInformationFilePath() + { + return '$/'.$this->toFilesystemPath().'/Plugin.php'; + } + + /** + * toPluginDirectoryPath + */ + public function toPluginDirectoryPath() + { + return '$/'.$this->toFilesystemPath(); + } + + /** + * toDatabasePrefix + */ + public function toDatabasePrefix($dbPrefix = false) + { + $builderPrefix = strtolower($this->authorCode.'_'.$this->pluginCode); + + if ($dbPrefix) { + return Db::getTablePrefix() . $builderPrefix; + } + + return $builderPrefix; + } + + /** + * toPermissionPrefix + */ + public function toPermissionPrefix() + { + return strtolower($this->authorCode.'.'.$this->pluginCode); + } + + /** + * getAuthorCode + */ + public function getAuthorCode() + { + return $this->authorCode; + } + + /** + * getPluginCode + */ + public function getPluginCode() + { + return $this->pluginCode; + } + + /** + * validateCodeWord + */ + protected function validateCodeWord($str) + { + $str = trim($str); + return strlen($str) && preg_match('/^[a-z]+[a-z0-9]+$/i', $str); + } +} diff --git a/plugins/rainlab/builder/classes/PluginVector.php b/plugins/rainlab/builder/classes/PluginVector.php new file mode 100644 index 0000000..433b6ec --- /dev/null +++ b/plugins/rainlab/builder/classes/PluginVector.php @@ -0,0 +1,67 @@ +plugin = $plugin; + $this->pluginCodeObj = $pluginCodeObj; + } + + /** + * createFromPluginCode + */ + public static function createFromPluginCode($pluginCode) + { + $pluginCodeObj = new PluginCode($pluginCode); + + $plugins = PluginManager::instance()->getPlugins(); + + foreach ($plugins as $code => $plugin) { + if ($code == $pluginCode) { + return new PluginVector($plugin, $pluginCodeObj); + } + } + + return null; + } + + /** + * getPluginName + */ + public function getPluginName() + { + if (!$this->plugin) { + return null; + } + + $pluginInfo = $this->plugin->pluginDetails(); + if (!isset($pluginInfo['name'])) { + return null; + } + + return $pluginInfo['name']; + } +} diff --git a/plugins/rainlab/builder/classes/PluginVersion.php b/plugins/rainlab/builder/classes/PluginVersion.php new file mode 100644 index 0000000..1726c8c --- /dev/null +++ b/plugins/rainlab/builder/classes/PluginVersion.php @@ -0,0 +1,71 @@ +getPluginUpdatesPath($pluginCodeObj, 'version.yaml'); + + if (!File::isFile($filePath)) { + throw new SystemException('Plugin version.yaml file is not found.'); + } + + $versionInfo = Yaml::parseFile($filePath); + + if (!is_array($versionInfo)) { + $versionInfo = []; + } + + if ($versionInfo) { + uksort($versionInfo, function ($a, $b) { + return version_compare($a, $b); + }); + } + + // Normalize result + $result = []; + + foreach ($versionInfo as $version => $info) { + $result[$this->normalizeVersion($version)] = $info; + } + + return $result; + } + + /** + * getPluginUpdatesPath + */ + protected function getPluginUpdatesPath($pluginCodeObj, $fileName = null) + { + $filePath = '$/'.$pluginCodeObj->toFilesystemPath().'/updates'; + $filePath = File::symbolizePath($filePath); + + if ($fileName !== null) { + return $filePath .= '/'.$fileName; + } + + return $filePath; + } + + /** + * normalizeVersion checks some versions start with v and others not + */ + protected function normalizeVersion($version): string + { + return rtrim(ltrim((string) $version, 'v'), '.'); + } +} diff --git a/plugins/rainlab/builder/classes/StandardBehaviorsRegistry.php b/plugins/rainlab/builder/classes/StandardBehaviorsRegistry.php new file mode 100644 index 0000000..66a5367 --- /dev/null +++ b/plugins/rainlab/builder/classes/StandardBehaviorsRegistry.php @@ -0,0 +1,530 @@ +behaviorLibrary = $behaviorLibrary; + + $this->registerBehaviors(); + } + + /** + * registerBehaviors + */ + protected function registerBehaviors() + { + $this->registerFormBehavior(); + $this->registerListBehavior(); + $this->registerImportExportBehavior(); + } + + /** + * registerFormBehavior + */ + protected function registerFormBehavior() + { + $properties = [ + 'name' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_name'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_name_description'), + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_name_required') + ] + ], + ], + 'modelClass' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_model_class'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_model_class_description'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_model_class_placeholder'), + 'type' => 'dropdown', + 'fillFrom' => 'model-classes', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_model_class_required') + ] + ], + ], + 'form' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_file'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_file_description'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_placeholder'), + 'type' => 'autocomplete', + 'fillFrom' => 'model-forms', + 'subtypeFrom' => 'modelClass', + 'depends' => ['modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_file_required') + ] + ], + ], + 'defaultRedirect' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_default_redirect'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_default_redirect_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'ignoreIfEmpty' => true + ], + 'create' => [ + 'type' => 'object', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_create'), + 'ignoreIfEmpty' => true, + 'properties' => [ + [ + 'property' => 'title', + 'type' => 'builderLocalization', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_page_title'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'redirect', + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_description'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'redirectClose', + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_close'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_close_description'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'flashSave', + 'type' => 'builderLocalization', + 'ignoreIfEmpty' => true, + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_save'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_save_description'), + ] + ] + ], + 'update' => [ + 'type' => 'object', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_update'), + 'ignoreIfEmpty' => true, + 'properties' => [ + [ + 'property' => 'title', + 'type' => 'builderLocalization', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_page_title'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'redirect', + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_description'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'redirectClose', + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_close'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_redirect_close_description'), + 'ignoreIfEmpty' => true + ], + [ + 'property' => 'flashSave', + 'type' => 'builderLocalization', + 'ignoreIfEmpty' => true, + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_save'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_save_description'), + ], + [ + 'property' => 'flashDelete', + 'type' => 'builderLocalization', + 'ignoreIfEmpty' => true, + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_delete'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_flash_delete_description'), + ] + ] + ], + 'preview' => [ + 'type' => 'object', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_preview'), + 'ignoreIfEmpty' => true, + 'properties' => [ + [ + 'property' => 'title', + 'type' => 'builderLocalization', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_page_title'), + 'ignoreIfEmpty' => true + ] + ] + ] + ]; + + $templates = [ + '$/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/create.php.tpl', + '$/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/update.php.tpl', + '$/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/preview.php.tpl' + ]; + + $this->behaviorLibrary->registerBehavior( + \Backend\Behaviors\FormController::class, + 'rainlab.builder::lang.controller.behavior_form_controller', + 'rainlab.builder::lang.controller.behavior_form_controller_description', + $properties, + 'formConfig', + null, + 'config_form.yaml', + $templates + ); + } + + /** + * registerListBehavior + */ + protected function registerListBehavior() + { + $properties = [ + 'title' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_title'), + 'type' => 'builderLocalization', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_title_required') + ] + ], + ], + 'modelClass' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_model_class'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_model_class_description'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_model_placeholder'), + 'type' => 'dropdown', + 'fillFrom' => 'model-classes', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_model_class_required') + ] + ], + ], + 'list' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_placeholder'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'model-lists', + 'subtypeFrom' => 'modelClass', + 'depends' => ['modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_required') + ] + ], + ], + 'recordUrl' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_record_url'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_record_url_description'), + 'ignoreIfEmpty' => true, + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + ], + 'noRecordsMessage' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_no_records_message'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_no_records_message_description'), + 'ignoreIfEmpty' => true, + 'type' => 'builderLocalization', + ], + 'recordsPerPage' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_recs_per_page'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_recs_per_page_description'), + 'ignoreIfEmpty' => true, + 'type' => 'string', + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_recs_per_page_regex') + ] + ], + ], + 'showSetup' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_setup'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'showCheckboxes' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_checkboxes'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'structure' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_structure'), + 'ignoreIfEmpty' => true, + 'type' => 'object', + 'ignoreIfPropertyEmpty' => 'maxDepth', + 'properties' => [ + [ + 'property' => 'maxDepth', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_max_depth'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_max_depth_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_max_depth_regex') + ] + ], + ], + [ + 'property' => 'showTree', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_tree'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_tree_description'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfDefault' => true, + ], + [ + 'property' => 'treeExpanded', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_tree_expanded'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_tree_expanded_description'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfDefault' => true, + ], + [ + 'property' => 'showReorder', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_reorder'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfDefault' => true, + ], + [ + 'property' => 'showSorting', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_show_sorting'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfDefault' => true, + ], + [ + 'property' => 'dragRow', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_drag_row'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfDefault' => true, + ], + ], + ], + 'defaultSort' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_default_sort'), + 'ignoreIfEmpty' => true, + 'type' => 'object', + 'ignoreIfPropertyEmpty' => 'column', + 'properties' => [ + [ + 'property' => 'column', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_ds_column'), + 'type' => 'autocomplete', + 'fillFrom' => 'model-columns', + 'subtypeFrom' => 'modelClass', + 'depends' => ['modelClass'] + ], + [ + 'property' => 'direction', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_ds_direction'), + 'type' => 'dropdown', + 'options' => [ + 'asc' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_ds_asc'), + 'desc' => Lang::get('rainlab.builder::lang.controller.property_behavior_form_ds_desc'), + ], + ] + ] + ], + 'toolbar' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_toolbar'), + 'type' => 'object', + 'ignoreIfEmpty' => true, + 'properties' => [ + [ + 'property' => 'buttons', + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_toolbar_buttons'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_toolbar_buttons_description'), + ], + [ + 'property' => 'search', + 'type' => 'object', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_search'), + 'properties' => [ + [ + 'property' => 'prompt', + 'type' => 'builderLocalization', + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_search_prompt'), + ] + ] + ] + ] + ], + 'recordOnClick' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_onclick'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_onclick_description'), + 'ignoreIfEmpty' => true, + 'type' => 'string' + ], + 'filter' => [ + 'type' => 'string', // Should be configurable in place later + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_filter'), + 'ignoreIfEmpty' => true + ] + ]; + + $templates = [ + '$/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/index.php.tpl', + '$/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/_list_toolbar.php.tpl' + ]; + + $this->behaviorLibrary->registerBehavior( + \Backend\Behaviors\ListController::class, + 'rainlab.builder::lang.controller.behavior_list_controller', + 'rainlab.builder::lang.controller.behavior_list_controller_description', + $properties, + 'listConfig', + null, + 'config_list.yaml', + $templates + ); + } + + /** + * registerImportExportBehavior + */ + protected function registerImportExportBehavior() + { + $properties = [ + 'import.title' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_title'), + 'type' => 'builderLocalization', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_title_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_import'), + ], + 'import.modelClass' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class_description'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class_placeholder'), + 'type' => 'dropdown', + 'fillFrom' => 'model-classes', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_import'), + ], + 'import.list' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_placeholder'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'model-lists', + 'subtypeFrom' => 'import.modelClass', + 'depends' => ['import.modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_import'), + ], + 'import.redirect' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_redirect'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_redirect_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'ignoreIfEmpty' => true, + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_import'), + ], + 'export.title' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_export_title'), + 'type' => 'builderLocalization', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_title_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_export'), + ], + 'export.modelClass' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_export_model_class'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_export_model_class_description'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class_placeholder'), + 'type' => 'dropdown', + 'fillFrom' => 'model-classes', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_model_class_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_export'), + ], + 'export.list' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file'), + 'placeholder' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_placeholder'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'model-lists', + 'subtypeFrom' => 'export.modelClass', + 'depends' => ['export.modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.controller.property_behavior_list_file_required') + ] + ], + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_export'), + ], + 'export.redirect' => [ + 'title' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_redirect'), + 'description' => Lang::get('rainlab.builder::lang.controller.property_behavior_import_redirect_description'), + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'ignoreIfEmpty' => true, + 'group' => Lang::get('rainlab.builder::lang.controller.property_group_export'), + ], + ]; + + $templates = [ + '$/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/import.php.tpl', + '$/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/export.php.tpl', + ]; + + $this->behaviorLibrary->registerBehavior( + \Backend\Behaviors\ImportExportController::class, + 'rainlab.builder::lang.controller.behavior_import_export_controller', + 'rainlab.builder::lang.controller.behavior_import_export_controller_description', + $properties, + 'importExportConfig', + null, + 'config_import_export.yaml', + $templates + ); + } +} diff --git a/plugins/rainlab/builder/classes/StandardBlueprintsRegistry.php b/plugins/rainlab/builder/classes/StandardBlueprintsRegistry.php new file mode 100644 index 0000000..1498026 --- /dev/null +++ b/plugins/rainlab/builder/classes/StandardBlueprintsRegistry.php @@ -0,0 +1,204 @@ +blueprintLibrary = $blueprintLibrary; + + $this->registerBlueprints(); + } + + /** + * registerBlueprints + */ + protected function registerBlueprints() + { + $this->registerEntryBlueprint(); + $this->registerGlobalBlueprint(); + } + + /** + * registerEntryBlueprint + */ + protected function registerEntryBlueprint() + { + $properties = [ + 'name' => [ + 'title' => "Name", + 'description' => "The name to use for this blueprint in the user interface", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A name is required" + ] + ], + ], + 'controllerClass' => [ + 'title' => "Controller Class", + 'description' => "Controller name defines the class name and URL of the controller's back-end pages. Standard PHP variable naming conventions apply. The first symbol should be a capital Latin letter. Examples: Categories, Posts, Products.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A controller name is required" + ] + ], + ], + 'modelClass' => [ + 'title' => "Model Class", + 'description' => "Model name defines the class name of the model. Standard PHP variable naming conventions apply. The first symbol should be a capital Latin letter. Examples: Category, Post, Product.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A model name is required" + ] + ], + ], + 'tableName' => [ + 'title' => "Table Name", + 'description' => "Table name defines the table name in the database.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A table name is required" + ] + ], + ], + 'permissionCode' => [ + 'title' => "Permission Code", + 'description' => "Permission code used to manage this item.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A permission code is required" + ] + ], + ], + 'menuCode' => [ + 'title' => "Menu Code", + 'description' => "Menu code used to include navigation for this item.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A menu code is required" + ] + ], + ] + ]; + + $this->blueprintLibrary->registerBlueprint( + \Tailor\Classes\Blueprint\EntryBlueprint::class, + 'Entry Blueprint', + 'The standard content structure that supports drafts.', + $properties, + ); + + $this->blueprintLibrary->registerBlueprint( + \Tailor\Classes\Blueprint\StreamBlueprint::class, + 'Stream Blueprint', + 'A stream of time stamped entries.', + $properties, + ); + + $this->blueprintLibrary->registerBlueprint( + \Tailor\Classes\Blueprint\SingleBlueprint::class, + 'Single Blueprint', + 'A single entry with dedicated fields.', + $properties, + ); + + $this->blueprintLibrary->registerBlueprint( + \Tailor\Classes\Blueprint\StructureBlueprint::class, + 'Structure Blueprint', + 'A defined structure of entries.', + $properties, + ); + } + + protected function registerGlobalBlueprint() + { + $properties = [ + 'name' => [ + 'title' => "Name", + 'description' => "The name to use for this blueprint in the user interface", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A name is required" + ] + ], + ], + 'controllerClass' => [ + 'title' => "Controller Class", + 'description' => "Controller name defines the class name and URL of the controller's back-end pages. Standard PHP variable naming conventions apply. The first symbol should be a capital Latin letter. Examples: Categories, Posts, Products.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A controller name is required" + ] + ], + ], + 'modelClass' => [ + 'title' => "Model Class", + 'description' => "Model name defines the class name of the model. Standard PHP variable naming conventions apply. The first symbol should be a capital Latin letter. Examples: Category, Post, Product.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A model name is required" + ] + ], + ], + 'tableName' => [ + 'title' => "Table Name", + 'description' => "Table name defines the table name in the database.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A table name is required" + ] + ], + ], + 'permissionCode' => [ + 'title' => "Permission Code", + 'description' => "Permission code used to manage this item.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A permission code is required" + ] + ], + ], + 'menuCode' => [ + 'title' => "Menu Code", + 'description' => "Menu code used to include navigation for this item.", + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => "A menu code is required" + ] + ], + ] + ]; + + $this->blueprintLibrary->registerBlueprint( + \Tailor\Classes\Blueprint\GlobalBlueprint::class, + 'Global Blueprint', + 'A single record in the database and is often used for settings and configuration.', + $properties, + ); + } +} diff --git a/plugins/rainlab/builder/classes/StandardControlsRegistry.php b/plugins/rainlab/builder/classes/StandardControlsRegistry.php new file mode 100644 index 0000000..d06d71c --- /dev/null +++ b/plugins/rainlab/builder/classes/StandardControlsRegistry.php @@ -0,0 +1,73 @@ +controlLibrary = $controlLibrary; + + $this->registerControls(); + } + + /** + * registerControls + */ + protected function registerControls() + { + // UI + $this->registerSectionControl(); + $this->registerHintControl(); + $this->registerRulerControl(); + $this->registerPartialControl(); + + // Fields + $this->registerTextControl(); + $this->registerNumberControl(); + $this->registerPasswordControl(); + $this->registerEmailControl(); + $this->registerTextareaControl(); + $this->registerDropdownControl(); + $this->registerRadioListControl(); + $this->registerBalloonSelectorControl(); + $this->registerCheckboxControl(); + $this->registerCheckboxListControl(); + $this->registerSwitchControl(); + + // Widgets + $this->registerCodeEditorWidget(); + $this->registerColorPickerWidget(); + $this->registerDataTableWidget(); + $this->registerDatepickerWidget(); + $this->registerFileUploadWidget(); + $this->registerMarkdownWidget(); + $this->registerMediaFinderWidget(); + $this->registerNestedFormWidget(); + $this->registerRecordFinderWidget(); + $this->registerRelationWidget(); + $this->registerRepeaterWidget(); + $this->registerRichEditorWidget(); + $this->registerPageFinderWidget(); + $this->registerSensitiveWidget(); + $this->registerTagListWidget(); + } +} diff --git a/plugins/rainlab/builder/classes/TableMigrationCodeGenerator.php b/plugins/rainlab/builder/classes/TableMigrationCodeGenerator.php new file mode 100644 index 0000000..710688b --- /dev/null +++ b/plugins/rainlab/builder/classes/TableMigrationCodeGenerator.php @@ -0,0 +1,654 @@ +diffTable($existingTable, $updatedTable); + + // Remove database prefix + $existingTableName = substr($existingTable->getName(), mb_strlen(Db::getTablePrefix())); + + if ($newTableName !== $existingTableName) { + if (!$tableDiff) { + $tableDiff = new TableDiff($existingTableName); + } + + $tableDiff->newName = $newTableName; + } + } + // The table doesn't exist + else { + $tableDiff = new TableDiff( + $updatedTable->getName(), + $updatedTable->getColumns(), + [], // Changed columns + [], // Removed columns + $updatedTable->getIndexes() // Added indexes + ); + + $tableDiff->fromTable = $updatedTable; + } + + if (!$tableDiff) { + return false; + } + + if (!$this->tableHasNameOrColumnChanges($tableDiff) && !$this->tableHasPrimaryKeyChanges($tableDiff)) { + return false; + } + + return $this->generateCreateOrUpdateCode($tableDiff, !$existingTable, $updatedTable); + } + + /** + * Wrap migration's up() and down() functions into a complete migration class declaration + * @param string $scriptFilename Specifies the migration script file name + * @param string $code Specifies the migration code + * @param PluginCode $pluginCodeObj The plugin code object + * @return TextParser + */ + public function wrapMigrationCode($scriptFilename, $code, $pluginCodeObj) + { + $templatePath = '$/rainlab/builder/models/databasetablemodel/templates/full-migration-code.php.tpl'; + $templatePath = File::symbolizePath($templatePath); + + $fileContents = File::get($templatePath); + + return TextParser::parse($fileContents, [ + 'className' => Str::studly($scriptFilename), + 'migrationCode' => $this->indent($code), + 'namespace' => $pluginCodeObj->toUpdatesNamespace() + ]); + } + + /** + * Generates code for dropping a database table. + * @param \Doctrine\DBAL\Schema\Table $existingTable Specifies the existing table schema. + * @return string Returns the migration up() and down() methods code. + */ + public function dropTable($existingTable) + { + return $this->generateMigrationCode( + $this->generateDropUpCode($existingTable), + $this->generateDropDownCode($existingTable) + ); + } + + protected function generateCreateOrUpdateCode($tableDiff, $isNewTable, $newOrUpdatedTable) + { + /* + * Although it might seem that a reverse diff could be used + * for the down() method, that's not so. The up and down operations + * are not fully symmetrical. + */ + + return $this->generateMigrationCode( + $this->generateCreateOrUpdateUpCode($tableDiff, $isNewTable, $newOrUpdatedTable), + $this->generateCreateOrUpdateDownCode($tableDiff, $isNewTable, $newOrUpdatedTable) + ); + } + + protected function generateMigrationCode($upCode, $downCode) + { + $templatePath = '$/rainlab/builder/models/databasetablemodel/templates/migration-code.php.tpl'; + $templatePath = File::symbolizePath($templatePath); + + $fileContents = File::get($templatePath); + + return TextParser::parse($fileContents, [ + 'upCode' => $upCode, + 'downCode' => $downCode + ]); + } + + protected function generateCreateOrUpdateUpCode($tableDiff, $isNewTable, $newOrUpdatedTable) + { + $result = null; + + $hasColumnChanges = $this->tableHasNameOrColumnChanges($tableDiff, true); + $changedPrimaryKey = $this->getChangedOrRemovedPrimaryKey($tableDiff); + $addedPrimaryKey = $this->findPrimaryKeyIndex($tableDiff->addedIndexes, $newOrUpdatedTable); + + if ($tableDiff->getNewName()) { + $result .= $this->generateTableRenameCode($tableDiff->name, $tableDiff->newName); + + if ($hasColumnChanges || $changedPrimaryKey) { + $result .= $this->eol; + } + } + + if (!$hasColumnChanges && !$changedPrimaryKey && !$addedPrimaryKey) { + return $this->makeTabs($result); + } + + $tableName = $tableDiff->getNewName() ? $tableDiff->newName : $tableDiff->name; + $result .= $this->generateSchemaTableMethodStart($tableName, $isNewTable); + + if ($changedPrimaryKey) { + $result .= $this->generatePrimaryKeyDrop($tableDiff->fromTable); + } + + foreach ($tableDiff->addedColumns as $column) { + $result .= $this->generateColumnCode($column, self::COLUMN_MODE_CREATE); + } + + foreach ($tableDiff->changedColumns as $columnDiff) { + $result .= $this->generateColumnCode($columnDiff, self::COLUMN_MODE_CHANGE); + } + + foreach ($tableDiff->renamedColumns as $oldName => $column) { + $result .= $this->generateColumnRenameCode($oldName, $column->getName()); + } + + foreach ($tableDiff->removedColumns as $name => $column) { + $result .= $this->generateColumnRemoveCode($name); + } + + $primaryKey = $changedPrimaryKey ? + $this->findPrimaryKeyIndex($tableDiff->changedIndexes, $newOrUpdatedTable) : + $this->findPrimaryKeyIndex($tableDiff->addedIndexes, $newOrUpdatedTable); + + if ($primaryKey) { + $result .= $this->generatePrimaryKeyCode($primaryKey, self::COLUMN_MODE_CREATE); + } + + $result .= $this->generateSchemaTableMethodEnd(); + + return $this->makeTabs($result); + } + + protected function generateCreateOrUpdateDownCode($tableDiff, $isNewTable, $newOrUpdatedTable) + { + $result = ''; + + if ($isNewTable) { + $result = $this->generateTableDropCode($tableDiff->name); + } else { + $changedPrimaryKey = $this->getChangedOrRemovedPrimaryKey($tableDiff); + $addedPrimaryKey = $this->findPrimaryKeyIndex($tableDiff->addedIndexes, $newOrUpdatedTable); + + if ($this->tableHasNameOrColumnChanges($tableDiff) || $changedPrimaryKey || $addedPrimaryKey) { + $hasColumnChanges = $this->tableHasNameOrColumnChanges($tableDiff, true); + + if ($tableDiff->getNewName()) { + $result .= $this->generateTableRenameCode($tableDiff->newName, $tableDiff->name); + + if ($hasColumnChanges || $changedPrimaryKey || $addedPrimaryKey) { + $result .= $this->eol; + } + } + + if (!$hasColumnChanges && !$changedPrimaryKey && !$addedPrimaryKey) { + return $this->makeTabs($result); + } + + $result .= $this->generateSchemaTableMethodStart($tableDiff->name, $isNewTable); + + if ($changedPrimaryKey || $addedPrimaryKey) { + $result .= $this->generatePrimaryKeyDrop($newOrUpdatedTable); + } + + foreach ($tableDiff->addedColumns as $column) { + $result .= $this->generateColumnDrop($column); + } + + foreach ($tableDiff->changedColumns as $columnDiff) { + $result .= $this->generateColumnCode($columnDiff, self::COLUMN_MODE_REVERT); + } + + foreach ($tableDiff->renamedColumns as $oldName => $column) { + $result .= $this->generateColumnRenameCode($column->getName(), $oldName); + } + + foreach ($tableDiff->removedColumns as $name => $column) { + $result .= $this->generateColumnCode($column, self::COLUMN_MODE_CREATE); + } + + if ($changedPrimaryKey || $addedPrimaryKey) { + $primaryKey = $this->findPrimaryKeyIndex($tableDiff->fromTable->getIndexes(), $tableDiff->fromTable); + if ($primaryKey) { + $result .= $this->generatePrimaryKeyCode($primaryKey, self::COLUMN_MODE_CREATE); + } + } + + $result .= $this->generateSchemaTableMethodEnd(); + } + } + + return $this->makeTabs($result); + } + + protected function generateDropUpCode($table) + { + $result = $this->generateTableDropCode($table->getName()); + return $this->makeTabs($result); + } + + protected function generateDropDownCode($table) + { + $tableDiff = new TableDiff( + $table->getName(), + $table->getColumns(), + [], // Changed columns + [], // Removed columns + $table->getIndexes() // Added indexes + ); + + return $this->generateCreateOrUpdateUpCode($tableDiff, true, $table); + } + + protected function formatLengthParameters($column, $method) + { + $length = $column->getLength(); + $precision = $column->getPrecision(); + $scale = $column->getScale(); + + if (!strlen($length) && !strlen($precision)) { + return null; + } + + if ($method == MigrationColumnType::TYPE_STRING) { + if (!strlen($length)) { + return null; + } + + return ', '.$length; + } + + if ($method == MigrationColumnType::TYPE_DECIMAL || $method == MigrationColumnType::TYPE_DOUBLE) { + if (!strlen($precision)) { + return null; + } + + if (strlen($scale)) { + return ', '.$precision.', '.$scale; + } + + return ', '.$precision; + } + } + + protected function applyMethodIncrements($method, $column) + { + if (!$column->getAutoincrement()) { + return $method; + } + + if ($method == MigrationColumnType::TYPE_BIGINTEGER) { + return 'bigIncrements'; + } + + return 'increments'; + } + + protected function generateSchemaTableMethodStart($tableName, $isNewTable) + { + $tableFunction = $isNewTable ? 'create' : 'table'; + $result = sprintf('\tSchema::%s(\'%s\', function($table)', $tableFunction, $tableName).$this->eol; + $result .= '\t{'.$this->eol; + return $result; + } + + protected function generateSchemaTableMethodEnd() + { + return '\t});'; + } + + protected function generateColumnDrop($column) + { + return sprintf('\t\t$table->dropColumn(\'%s\');', $column->getName()).$this->eol; + } + + protected function generateIndexDrop($index) + { + return sprintf('\t\t$table->dropIndex(\'%s\');', $index->getName()).$this->eol; + } + + protected function generatePrimaryKeyDrop($table) + { + $index = $this->findPrimaryKeyIndex($table->getIndexes(), $table); + if (!$index) { + return; + } + + $indexColumns = $index->getColumns(); + return sprintf('\t\t$table->dropPrimary([%s]);', $this->implodeColumnList($indexColumns)).$this->eol; + } + + protected function generateColumnCode($columnData, $mode) + { + $forceFlagsChange = false; + + switch ($mode) { + case self::COLUMN_MODE_CREATE: + $column = $columnData; + $changeMode = false; + break; + case self::COLUMN_MODE_CHANGE: + $column = $columnData->column; + $changeMode = true; + + $forceFlagsChange = in_array('type', $columnData->changedProperties); + break; + case self::COLUMN_MODE_REVERT: + $column = $columnData->fromColumn; + $changeMode = true; + + $forceFlagsChange = in_array('type', $columnData->changedProperties); + break; + } + + $result = $this->generateColumnMethodCall($column); + $result .= $this->generateNullable($column, $changeMode, $columnData, $forceFlagsChange); + $result .= $this->generateUnsigned($column, $changeMode, $columnData, $forceFlagsChange); + $result .= $this->generateDefault($column, $changeMode, $columnData, $forceFlagsChange); + $result .= $this->generateComment($column, $changeMode, $columnData, $forceFlagsChange); + + if ($changeMode) { + $result .= '->change()'; + } + + $result .= ';'.$this->eol; + + return $result; + } + + protected function generateColumnRenameCode($fromName, $toName) + { + return sprintf('\t\t$table->renameColumn(\'%s\', \'%s\');', $fromName, $toName).$this->eol; + } + + protected function generateTableRenameCode($fromName, $toName) + { + return sprintf('\tSchema::rename(\'%s\', \'%s\');', $fromName, $toName); + } + + protected function generateTableDropCode($name) + { + return sprintf('\tSchema::dropIfExists(\'%s\');', $name); + } + + protected function generateColumnRemoveCode($name) + { + return sprintf('\t\t$table->dropColumn(\'%s\');', $name).$this->eol; + } + + protected function generateColumnMethodCall($column) + { + $columnName = $column->getName(); + $typeName = $column->getType()->getName(); + + $method = MigrationColumnType::toMigrationMethodName($typeName, $columnName); + $method = $this->applyMethodIncrements($method, $column); + + $lengthStr = $this->formatLengthParameters($column, $method); + + return sprintf('\t\t$table->%s(\'%s\'%s)', $method, $columnName, $lengthStr); + } + + protected function generateNullable($column, $changeMode, $columnData, $forceFlagsChange) + { + $result = null; + + if (!$changeMode) { + if (!$column->getNotnull()) { + $result = $this->generateBooleanMethod('nullable', true); + } + } + elseif (in_array('notnull', $columnData->changedProperties) || $forceFlagsChange) { + $result = $this->generateBooleanMethod('nullable', !$column->getNotnull()); + } + + return $result; + } + + protected function generateUnsigned($column, $changeMode, $columnData, $forceFlagsChange) + { + $result = null; + + if (!$changeMode) { + if ($column->getUnsigned()) { + $result = $this->generateBooleanMethod('unsigned', true); + } + } + elseif (in_array('unsigned', $columnData->changedProperties) || $forceFlagsChange) { + $result = $this->generateBooleanMethod('unsigned', $column->getUnsigned()); + } + + return $result; + } + + protected function generateDefault($column, $changeMode, $columnData, $forceFlagsChange) + { + /* + * See a note about empty strings as default values in + * DatabaseTableSchemaCreator::formatOptions() method. + */ + $result = null; + $default = $column->getDefault(); + + if (!$changeMode) { + if (strlen($default)) { + $result = $this->generateDefaultMethodCall($default, $column); + } + } elseif (in_array('default', $columnData->changedProperties) || $forceFlagsChange) { + if (strlen($default)) { + $result = $this->generateDefaultMethodCall($default, $column); + } elseif ($changeMode) { + $result = sprintf('->default(null)'); + } + } + + return $result; + } + + protected function generateDefaultMethodCall($default, $column) + { + $columnName = $column->getName(); + $typeName = $column->getType()->getName(); + + $type = MigrationColumnType::toMigrationMethodName($typeName, $columnName); + + if (in_array($type, MigrationColumnType::getIntegerTypes()) || + in_array($type, MigrationColumnType::getDecimalTypes()) || + $type == MigrationColumnType::TYPE_BOOLEAN) { + return sprintf('->default(%s)', $default); + } + + return sprintf('->default(\'%s\')', $this->quoteParameter($default)); + } + + protected function generateComment($column, $changeMode, $columnData, $forceFlagsChange) + { + $result = null; + $default = $column->getComment(); + + if (!$changeMode) { + if (strlen($default)) { + $result = $this->generateCommentMethodCall($default, $column); + } + } + elseif (in_array('comment', $columnData->changedProperties) || $forceFlagsChange) { + if (strlen($default)) { + $result = $this->generateCommentMethodCall($default, $column); + } + elseif ($changeMode) { + $result = sprintf('->comment(null)'); + } + } + + return $result; + } + + protected function generateCommentMethodCall($default, $column) + { + return sprintf('->comment(\'%s\')', $this->quoteParameter($default)); + } + + protected function generatePrimaryKeyCode($index) + { + $columns = $index->getColumns(); + + return sprintf('\t\t$table->primary([%s]);', $this->implodeColumnList($columns)).$this->eol; + } + + protected function generateBooleanString($value) + { + $result = $value ? 'true' : 'false'; + + return $result; + } + + protected function generateBooleanMethod($methodName, $value) + { + if ($value) { + return '->'.$methodName.'()'; + } + + return '->'.$methodName.'('.$this->generateBooleanString($value).')'; + } + + protected function quoteParameter($str) + { + return str_replace("'", "\'", $str); + } + + protected function makeTabs($str) + { + return str_replace('\t', ' ', $str); + } + + protected function indent($str) + { + return $this->indent . str_replace($this->eol, $this->eol . $this->indent, $str); + } + + protected function implodeColumnList($columnNames) + { + foreach ($columnNames as &$columnName) { + $columnName = '\''.$columnName.'\''; + } + + return implode(',', $columnNames); + } + + protected function tableHasNameOrColumnChanges($tableDiff, $columnChangesOnly = false) + { + $result = $tableDiff->addedColumns + || $tableDiff->changedColumns + || $tableDiff->removedColumns + || $tableDiff->renamedColumns; + + if ($columnChangesOnly) { + return $result; + } + + return $result || $tableDiff->getNewName(); + } + + protected function tableHasPrimaryKeyChanges($tableDiff) + { + return $this->findPrimaryKeyIndex($tableDiff->addedIndexes, $tableDiff->fromTable) || + $this->findPrimaryKeyIndex($tableDiff->changedIndexes, $tableDiff->fromTable) || + $this->findPrimaryKeyIndex($tableDiff->removedIndexes, $tableDiff->fromTable); + } + + protected function getChangedOrRemovedPrimaryKey($tableDiff) + { + foreach ($tableDiff->changedIndexes as $index) { + if ($index->isPrimary()) { + return $index; + } + } + + foreach ($tableDiff->removedIndexes as $index) { + if ($index->isPrimary()) { + return $index; + } + } + + return null; + } + + protected function findPrimaryKeyIndex($indexes, $table) + { + /* + * This method ignores auto-increment primary keys + * as they are managed with the increments() method + * instead of the primary(). + */ + foreach ($indexes as $index) { + if (!$index->isPrimary()) { + continue; + } + + if ($this->indexHasAutoincrementColumns($index, $table)) { + continue; + } + + return $index; + } + + return null; + } + + protected function indexHasAutoincrementColumns($index, $table) + { + $indexColumns = $index->getColumns(); + + foreach ($indexColumns as $indexColumn) { + if (!$table->hasColumn($indexColumn)) { + continue; + } + + $tableColumn = $table->getColumn($indexColumn); + if ($tableColumn->getAutoincrement()) { + return true; + } + } + + return false; + } +} diff --git a/plugins/rainlab/builder/classes/TailorBlueprintLibrary.php b/plugins/rainlab/builder/classes/TailorBlueprintLibrary.php new file mode 100644 index 0000000..c9099c6 --- /dev/null +++ b/plugins/rainlab/builder/classes/TailorBlueprintLibrary.php @@ -0,0 +1,154 @@ +getBlueprintObject($blueprintUuid); + if (!$blueprintObj) { + return null; + } + + $blueprintClassName = get_class($blueprintObj); + + $blueprints = $this->listBlueprints(); + if (!array_key_exists($blueprintClassName, $blueprints)) { + return null; + } + + return [ + 'blueprintObj' => $blueprintObj, + 'blueprintClass' => get_class($blueprintObj) + ] + $blueprints[$blueprintClassName]; + } + + /** + * getRelatedBlueprintUuids returns blueprints related to the supplied blueprint UUID + */ + public function getRelatedBlueprintUuids($blueprintUuid) + { + $indexer = BlueprintIndexer::instance(); + $fieldset = $indexer->findContentFieldset($blueprintUuid); + + $relatedFieldTypes = ['entries']; + + $result = []; + foreach ($fieldset->getAllFields() as $name => $field) { + if (!in_array($field->type, $relatedFieldTypes)) { + continue; + } + + $bp = $this->getBlueprintObject($field->source, $field->source); + if (!$bp) { + continue; + } + + $result[$name] = $bp->uuid; + } + + return $result; + } + + /** + * registerBlueprint + * + * @param string $class Specifies the blueprint class name. + * @param string $name Specifies the blueprint name, for example "Form blueprint". + * @param string $description Specifies the blueprint description. + * @param array $properties Specifies the blueprint properties. + * The property definitions should be compatible with Inspector properties, similarly + * to the Component properties: http://octobercms.com/docs/plugin/components#component-properties + * @param string $designTimeProviderClass Specifies the blueprint design-time provider class name. + * The class should extend RainLab\Builder\Classes\BlueprintDesignTimeProviderBase. If the class is not provided, + * the default control design and design settings will be used. + * The templates are used when a new controller is created. The templates should be specified as paths + * to Twig files in the format ['~/plugins/author/plugin/blueprints/blueprintname/templates/view.htm.tpl']. + */ + public function registerBlueprint($class, $name, $description, $properties, $designTimeProviderClass = null) + { + if (!$designTimeProviderClass) { + $designTimeProviderClass = self::DEFAULT_DESIGN_TIME_PROVIDER; + } + + $this->blueprints[$class] = [ + 'class' => $class, + 'name' => Lang::get($name), + 'description' => Lang::get($description), + 'properties' => $properties, + 'designTimeProvider' => $designTimeProviderClass, + ]; + } + + /** + * listBlueprints + */ + public function listBlueprints() + { + if ($this->blueprints !== null) { + return $this->blueprints; + } + + $this->blueprints = []; + + Event::fire('pages.builder.registerTailorBlueprints', [$this]); + + return $this->blueprints; + } + + /** + * getBlueprintObject + */ + public function getBlueprintObject($uuid, $handle = null) + { + if (isset($this->blueprintUuidCache[$uuid])) { + return $this->blueprintUuidCache[$uuid]; + } + + foreach (EntryBlueprint::listInProject() as $blueprint) { + if ($blueprint->uuid === $uuid) { + return $this->blueprintUuidCache[$blueprint->uuid] = $blueprint; + } + if ($handle && $blueprint->handle === $handle) { + return $this->blueprintUuidCache[$blueprint->uuid] = $blueprint; + } + } + + foreach (GlobalBlueprint::listInProject() as $blueprint) { + if ($blueprint->uuid === $uuid) { + return $this->blueprintUuidCache[$blueprint->uuid] = $blueprint; + } + if ($handle && $blueprint->handle === $handle) { + return $this->blueprintUuidCache[$blueprint->uuid] = $blueprint; + } + } + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/ContainerUtils.php b/plugins/rainlab/builder/classes/blueprintgenerator/ContainerUtils.php new file mode 100644 index 0000000..739f6d7 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/ContainerUtils.php @@ -0,0 +1,71 @@ +sourceModel = $sourceModel; + } + + /** + * getBlueprintDefinition + */ + public function getBlueprintDefinition() + { + return $this->sourceModel->getBlueprintObject(); + } + + /** + * findUuidFromSource + */ + protected function findUuidFromSource($uuidOrHandle): ?string + { + if (!$this->sourceModel) { + return null; + } + + $blueprint = TailorBlueprintLibrary::instance()->getBlueprintObject($uuidOrHandle, $uuidOrHandle); + if (!$blueprint) { + return null; + } + + return $blueprint->uuid; + } + + /** + * findRelatedModelClass + */ + protected function findRelatedModelClass($uuidOrHandle): ?string + { + $uuid = $this->findUuidFromSource($uuidOrHandle); + if (!$uuid) { + return null; + } + + $modelClass = $this->sourceModel->blueprints[$uuid]['modelClass'] ?? null; + if (!$modelClass) { + return null; + } + + $pluginCodeObj = $this->sourceModel->getPluginCodeObj(); + return $pluginCodeObj->toPluginNamespace().'\\Models\\'.$modelClass; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/ExpandoModelContainer.php b/plugins/rainlab/builder/classes/blueprintgenerator/ExpandoModelContainer.php new file mode 100644 index 0000000..1fbb393 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/ExpandoModelContainer.php @@ -0,0 +1,56 @@ +repeaterFieldset) { + throw new ApplicationException('Missing repeater fieldset'); + } + + // Process join table entries specifically + $fieldset = $this->repeaterFieldset; + foreach ($fieldset->getAllFields() as $name => $field) { + if ($field->type === 'entries') { + $this->processEntryRelationDefinitions($definitions, $name, $field); + } + if ($field->type === 'repeater' || $field->type === 'nestedform') { + $this->processRepeaterRelationDefinitions($definitions, $name, $field); + } + } + + return $definitions; + } + + /** + * processRepeaterRelationDefinitions + */ + protected function processRepeaterRelationDefinitions(&$definitions, $fieldName, $fieldObj) + { + foreach ($definitions as $type => &$relations) { + foreach ($relations as $name => &$props) { + if ($name === $fieldName && isset($props[0]) && $props[0] === \Tailor\Models\RepeaterItem::class) { + // Field to be jsonable (nested) + $this->addJsonable($name); + unset($relations[$name]); + break; + } + } + } + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/FilterElementContainer.php b/plugins/rainlab/builder/classes/blueprintgenerator/FilterElementContainer.php new file mode 100644 index 0000000..cee02d7 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/FilterElementContainer.php @@ -0,0 +1,77 @@ +label($label)->displayAs('text'); + + $this->scopes[$scopeName] = $scope; + + return $scope; + } + + /** + * getControls + */ + public function getControls(): array + { + $result = []; + $index = 0; + + foreach ($this->scopes as $name => $field) { + $result[$name] = $this->parseFieldConfig($index, $name, $field->config); + $index++; + } + + return $result; + } + + /** + * parseFieldConfig + */ + protected function parseFieldConfig($index, $name, $config): array + { + // Remove tailor values + $ignoreConfig = [ + 'scopeName', + 'source', + 'externalToolbarAppState', + 'externalToolbarEventBus' + ]; + + $parsedConfig = array_except((array) $config, $ignoreConfig); + + $parsedConfig['id'] = $index; + $parsedConfig['field'] = $name; + + // Remove default values + $keepDefaults = [ + 'type', + ]; + + $defaultField = new FilterScope; + foreach ($parsedConfig as $key => $value) { + if (!in_array($key, $keepDefaults) && $defaultField->$key === $value) { + unset($parsedConfig[$key]); + } + } + + return $parsedConfig; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/FormElementContainer.php b/plugins/rainlab/builder/classes/blueprintgenerator/FormElementContainer.php new file mode 100644 index 0000000..24a3a19 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/FormElementContainer.php @@ -0,0 +1,135 @@ +label($label)->displayAs('text'); + + $this->addField($fieldName, $field); + + return $field; + } + + /** + * getFormFieldset returns the current fieldset definition + */ + public function getFormFieldset(): FieldsetDefinition + { + return $this; + } + + /** + * getFormContext returns the current form context, e.g. create, update + */ + public function getFormContext() + { + return ''; + } + + /** + * getPrimaryControls + */ + public function getPrimaryControls() + { + $host = new self; + + $host->addFormField('title', 'Title')->span('auto'); + $host->addFormField('slug', 'Slug')->preset(['field' => 'title', 'type' => 'slug'])->span('auto'); + + return $host->getControls(); + } + + /** + * getControls + */ + public function getControls(): array + { + $result = []; + + foreach ($this->getAllFields() as $name => $field) { + $result[$name] = $this->parseFieldConfig($name, $field); + } + + return $result; + } + + /** + * parseFieldConfig + */ + protected function parseFieldConfig($fieldName, $fieldObj): array + { + // Apply mutations to field object + if ($fieldObj->span === 'adaptive') { + $fieldObj->span('full'); + } + + if ($fieldObj->type === 'recordfinder') { + $relatedModelClass = $this->findRelatedModelClass($fieldObj->source); + if ($relatedModelClass) { + $baseClass = mb_strtolower(class_basename($relatedModelClass)); + $path = $this->sourceModel->getPluginCodeObj()->toPluginDirectoryPath().'/models/'.$baseClass; + $fieldObj->list($path.'/columns.yaml'); + } + } + + if ($fieldObj->type === 'repeater') { + $modelClass = $this->sourceModel->getBlueprintConfig('modelClass'); + $baseClass = mb_strtolower(class_basename($modelClass)).mb_strtolower(Str::studly($fieldName)).'item'; + $path = $this->sourceModel->getPluginCodeObj()->toPluginDirectoryPath().'/models/'.$baseClass; + if ($fieldObj->groups) { + $newGroups = []; + foreach ($fieldObj->groups as $groupName => $groupConfig) { + $newGroups[$groupName] = $path."/fields_{$groupName}.yaml"; + } + $fieldObj->groups($newGroups); + } + else { + $fieldObj->form($path.'/fields.yaml'); + } + } + + // Remove tailor values + $ignoreConfig = [ + 'fieldName', + 'source', + 'column', + 'scope', + 'inverse', + 'validation', + 'externalToolbarAppState', + 'externalToolbarEventBus' + ]; + + $parsedConfig = array_except((array) $fieldObj->config, $ignoreConfig); + + // Remove default values + $keepDefaults = [ + 'type', + 'span', + ]; + + $defaultField = new FormField; + foreach ($parsedConfig as $key => $value) { + if (!in_array($key, $keepDefaults) && $defaultField->$key === $value) { + unset($parsedConfig[$key]); + } + } + + return $parsedConfig; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasControllers.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasControllers.php new file mode 100644 index 0000000..1740d05 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasControllers.php @@ -0,0 +1,71 @@ +makeControllerModel()) { + $files[] = $model->getControllerFilePath(); + } + + $this->validateUniqueFiles($files); + + $model && $model->validate(); + } + + /** + * generateController + */ + protected function generateController() + { + if ($controller = $this->makeControllerModel()) { + $controller->save(); + } + } + + /** + * makeControllerModel + */ + protected function makeControllerModel() + { + $controller = new ControllerModel; + + $controller->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $controller->baseModelClassName = $this->getConfig('modelClass'); + + $controller->controllerName = $this->getConfig('name'); + + $controller->controller = $this->getConfig('controllerClass'); + + $controller->menuItem = $this->getActiveMenuItemCode(); + + $controller->permissions = [$this->getConfig('permissionCode')]; + + $controller->behaviors = []; + + if ($this->sourceModel->useListController()) { + $listConfig = []; + + if ($this->sourceModel->useStructure()) { + $listConfig['structure'] = $this->sourceModel->getBlueprintObject()->structure ?? true; + } + + $controller->behaviors[\Backend\Behaviors\ListController::class] = $listConfig; + } + + $controller->behaviors[\Backend\Behaviors\FormController::class] = []; + + return $controller; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasExpandoModels.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasExpandoModels.php new file mode 100644 index 0000000..55b50e3 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasExpandoModels.php @@ -0,0 +1,203 @@ +makeExpandoModels(true) as $model) { + $this->validateUniqueFiles([$model->getModelFilePath()]); + $model->validate(); + } + } + + /** + * generateExpandoModels + */ + protected function generateExpandoModels() + { + foreach ($this->makeExpandoModels() as $model) { + $model->save(); + $this->filesGenerated[] = $model->getModelFilePath(); + } + } + + /** + * makeExpandoModels + */ + protected function makeExpandoModels($isValidate = false) + { + $models = []; + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + foreach ($fieldset->getAllFields() as $name => $field) { + if ($field->type !== 'repeater') { + continue; + } + + $container = new ExpandoModelContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = $container->repeaterFieldset = $this->makeExpandoRepeaterFieldset($field); + + $fieldset->applyModelExtensions($container); + + // Generate form fields + $this->generateExpandoModelFormFields($container, $name, $field, $isValidate); + + // Generate model + $models[] = $this->makeExpandoModelModel($container, $name, $field); + } + + return $models; + } + + /** + * generateExpandoModelModel + */ + protected function makeExpandoModelModel($container, $name, $field) + { + $repeaterInfo = $container->getRepeaterTableInfoFor($name, $field); + + $model = new ModelModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->className = $repeaterInfo['modelClass']; + + $model->databaseTable = $repeaterInfo['tableName']; + + $model->addTimestamps = true; + + $model->skipDbValidation = true; + + $model->traits[] = \Tailor\Traits\BlueprintRelationModel::class; + + $model->baseClassName = \October\Rain\Database\ExpandoModel::class; + + $model->relationDefinitions = (array) $container->getProcessedRelationDefinitions(); + + $model->validationDefinitions = (array) $container->getValidationDefinitions(); + + $model->addRawContentToModel(<<getJsonable()) { + $jsonableStr = ''; + foreach ($jsonable as $j) { + $jsonableStr = "'".$j."', "; + } + $jsonableStr = trim($jsonableStr, ', '); + $model->addRawContentToModel(<<groups) { + return FieldManager::instance()->makeFieldset((array) $field->form); + } + + // Create a merged fieldset for groups to acquire relations + $fieldsets = []; + foreach ($field->groups as $config) { + $fieldsets[] = FieldManager::instance()->makeFieldset($config); + } + + $fieldset = array_shift($fieldsets); + foreach ($fieldsets as $otherFieldset) { + foreach ($otherFieldset->getAllFields() as $name => $field) { + $fieldset->addField($name, $field); + } + } + + return $fieldset; + } + + /** + * generateExpandoModelFormFields generates form fields YAML files + */ + protected function generateExpandoModelFormFields($container, $name, $field, $isValidate = false) + { + $repeaterInfo = $container->getRepeaterTableInfoFor($name, $field); + + $forms = []; + if ($field->groups) { + foreach ($field->groups as $groupName => $groupConfig) { + $forms[] = $this->makeExpandoModelFormFields($repeaterInfo, $groupConfig, $groupName); + } + } + elseif ($field->form) { + $forms[] = $this->makeExpandoModelFormFields($repeaterInfo, $field->form); + } + + foreach ($forms as $form) { + if ($isValidate) { + $this->validateUniqueFiles([$form->getYamlFilePath()]); + $form->validate(); + } + else { + $form->save(); + $this->filesGenerated[] = $form->getYamlFilePath(); + } + } + } + + /** + * makeExpandoModelFormFields + */ + protected function makeExpandoModelFormFields($repeaterInfo, $formConfig, $groupPrefix = '') + { + $model = new ModelFormModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->setModelClassName($repeaterInfo['modelClass']); + + $model->fileName = $groupPrefix ? "fields_{$groupPrefix}.yaml" : 'fields.yaml'; + + $container = new FormElementContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = FieldManager::instance()->makeFieldset($formConfig); + + $fieldset->defineAllFormFields($container, ['context' => '*']); + + $model->controls = array_except($formConfig, 'fields') + [ + 'fields' => $container->getControls(), + ]; + + return $model; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasMigrations.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasMigrations.php new file mode 100644 index 0000000..1643eda --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasMigrations.php @@ -0,0 +1,230 @@ +dryRunMigrations = true; + + $this->generateMigrations(); + + return $this->migrationScripts; + } + + /** + * generateMigrations for a blueprint + */ + protected function generateMigrations() + { + if ($this->sourceModel->wantsDatabaseMigration()) { + $this->generateContentTable(); + } + + $this->generateJoinTables(); + $this->generateRepeaterTables(); + } + + /** + * generateContentTable + */ + protected function generateContentTable() + { + $tableName = $this->getConfig('tableName'); + if (!$tableName) { + throw new ApplicationException('Missing a table name for migrations'); + } + + [$proposedFile, $migrationFilePath] = $this->findAvailableMigrationFile($tableName); + + $this->migrationScripts[$proposedFile] = __("Create :name Content Table", ['name' => $this->getConfig('name')]); + + if ($this->dryRunMigrations) { + return; + } + + // Prepare the schema from the fieldset + $table = $this->makeSchemaBlueprint($tableName); + + // Write migration to disk + $migrationCode = ''; + foreach ($table->getColumns() as $column) { + $migrationCode .= '\t\t\t'.$this->makeTableDefinition($column).PHP_EOL; + } + + $code = $this->parseTemplate($this->getTemplatePath('migration.php.tpl'), [ + 'migrationCode' => $this->makeTabs(trim($migrationCode, PHP_EOL)), + 'useStructure' => $this->sourceModel->useStructure() + ]); + + $this->writeFile($migrationFilePath, $code); + } + + /** + * makeTableDefinition + */ + protected function makeTableDefinition($column) + { + $defaultStrLength = \Illuminate\Database\Schema\Builder::$defaultStringLength; + + $code = '$table->'.$column['type'].'(\''.$column['name'].'\')'; + + foreach ($column->getAttributes() as $attribute => $value) { + if (in_array($attribute, ['name', 'type'])) { + continue; + } + + if ($attribute === 'length' && $value === $defaultStrLength) { + continue; + } + + $exportValue = $value !== true ? var_export($value, true) : ''; + $code .= '->'.$attribute.'('.$exportValue.')'; + } + + $code .= ';'; + + return $code; + } + + /** + * makeSchemaBlueprint + */ + protected function makeSchemaBlueprint($tableName) + { + $table = App::make(\October\Rain\Database\Schema\Blueprint::class, ['table' => $tableName]); + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + foreach ($fieldset->getAllFields() as $fieldObj) { + $fieldObj->extendDatabaseTable($table); + } + + return $table; + } + + /** + * generateJoinTables + */ + protected function generateJoinTables() + { + $container = new ModelContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->applyModelExtensions($container); + + foreach ($fieldset->getAllFields() as $name => $field) { + if ($field->type === 'entries' && !$field->inverse && $field->maxItems !== 1) { + if ($joinInfo = $container->getJoinTableInfoFor($name, $field)) { + $this->generateJoinTableForEntries($joinInfo); + } + } + } + } + + /** + * generateJoinTableForEntries + */ + protected function generateJoinTableForEntries($joinInfo) + { + $tableName = $joinInfo['tableName']; + + [$proposedFile, $migrationFilePath] = $this->findAvailableMigrationFile($tableName); + + $this->migrationScripts[$proposedFile] = __("Create :name Pivot Table for :field", [ + 'name' => $this->getConfig('name'), + 'field' => $joinInfo['fieldName'] ?? '??' + ]); + + if ($this->dryRunMigrations) { + return; + } + + $code = $this->parseTemplate($this->getTemplatePath('migration-join.php.tpl'), $joinInfo); + + $this->writeFile($migrationFilePath, $code); + } + + /** + * generateRepeaterTables + */ + protected function generateRepeaterTables() + { + $container = new ExpandoModelContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->applyModelExtensions($container); + + foreach ($fieldset->getAllFields() as $name => $field) { + if ($field->type === 'repeater') { + if ($repeaterInfo = $container->getRepeaterTableInfoFor($name, $field)) { + $this->generateRepeaterTableForEntries($repeaterInfo); + } + } + } + } + + /** + * generateJoinTableForEntries + */ + protected function generateRepeaterTableForEntries($repeaterInfo) + { + $tableName = $repeaterInfo['tableName']; + + [$proposedFile, $migrationFilePath] = $this->findAvailableMigrationFile($tableName); + + $this->migrationScripts[$proposedFile] = __("Create :name Repeater Table for :field", [ + 'name' => $this->getConfig('name'), + 'field' => $repeaterInfo['fieldName'] ?? '??' + ]); + + if ($this->dryRunMigrations) { + return; + } + + $code = $this->parseTemplate($this->getTemplatePath('migration-repeater.php.tpl'), $repeaterInfo); + + $this->writeFile($migrationFilePath, $code); + } + + /** + * findAvailableMigrationFile + */ + protected function findAvailableMigrationFile(string $tableName): array + { + // Shorten table name + $tableName = trim(str_replace($this->sourceModel->getPluginCodeObj()->toDatabasePrefix(), '', $tableName), "_"); + + $proposedFile = "create_{$tableName}_table.php"; + $migrationFilePath = $this->sourceModel->getPluginFilePath('updates/'.$proposedFile); + + // Find an available file name + $counter = 2; + while (File::isFile($migrationFilePath)) { + $proposedFile = "create_{$tableName}_table_{$counter}.php"; + $migrationFilePath = $this->sourceModel->getPluginFilePath('updates/'.$proposedFile); + $counter++; + } + + return [$proposedFile, $migrationFilePath]; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasModels.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasModels.php new file mode 100644 index 0000000..7571045 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasModels.php @@ -0,0 +1,224 @@ +makeModelModel()) { + $files[] = $model->getModelFilePath(); + } + + if ($form = $this->makeModelFormFields()) { + $files[] = $form->getYamlFilePath(); + } + + if ($lists = $this->makeModelListColumns()) { + $files[] = $lists->getYamlFilePath(); + } + + if ($filter = $this->makeModelFilterScopes()) { + $files[] = $filter->getYamlFilePath(); + } + + $this->validateUniqueFiles($files); + + $model && $model->validate(); + $form && $form->validate(); + $lists && $lists->validate(); + $filter && $filter->validate(); + } + + /** + * generateModel + */ + protected function generateModel() + { + if ($filter = $this->makeModelFilterScopes()) { + $filter->save(); + $this->filesGenerated[] = $filter->getYamlFilePath(); + } + + if ($lists = $this->makeModelListColumns()) { + $lists->save(); + $this->filesGenerated[] = $lists->getYamlFilePath(); + } + + if ($form = $this->makeModelFormFields()) { + $form->save(); + $this->filesGenerated[] = $form->getYamlFilePath(); + } + + if ($model = $this->makeModelModel()) { + $model->save(); + $this->filesGenerated[] = $model->getModelFilePath(); + } + } + + /** + * makeModelModel + */ + protected function makeModelModel() + { + $model = new ModelModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->className = $this->getConfig('modelClass'); + + $model->databaseTable = $this->getConfig('tableName'); + + $model->addTimestamps = true; + + $model->addSoftDeleting = true; + + $model->skipDbValidation = true; + + $model->traits[] = \Tailor\Traits\BlueprintRelationModel::class; + + // Custom logic for settings models + if ($this->sourceModel->useSettingModel()) { + $model->baseClassName = \System\Models\SettingModel::class; + + $model->addSoftDeleting = false; + } + + $this->extendModelWithModelSpecs($model); + + return $model; + } + + /** + * extendModelWithModelSpecs + */ + protected function extendModelWithModelSpecs($model) + { + $container = new ModelContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->applyModelExtensions($container); + + $model->relationDefinitions = (array) $container->getProcessedRelationDefinitions(); + + $model->validationDefinitions = (array) $container->getValidationDefinitions(); + + $model->validationDefinitions['rules'] += ['title' => 'required']; + + if ($this->sourceModel->useMultisite()) { + $model->traits[] = \October\Rain\Database\Traits\Multisite::class; + + $model->multisiteDefinition = (array) $container->getMultisiteDefinition(); + } + + if ($this->sourceModel->useStructure()) { + $model->traits[] = \October\Rain\Database\Traits\NestedTree::class; + } + } + + /** + * makeModelFormFields + */ + protected function makeModelFormFields() + { + $model = new ModelFormModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->setModelClassName($this->getConfig('modelClass')); + + $model->fileName = 'fields.yaml'; + + $container = new FormElementContainer; + + $container->setSourceModel($this->sourceModel); + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->defineAllFormFields($container, ['context' => '*']); + + $model->controls = [ + 'fields' => $container->getPrimaryControls(), + 'tabs' => ['fields' => $container->getControls()] + ]; + + return $model; + } + + /** + * makeModelListColumns + */ + protected function makeModelListColumns() + { + $model = new ModelListModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->setModelClassName($this->getConfig('modelClass')); + + $model->fileName = 'columns.yaml'; + + $container = new ListElementContainer; + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->defineAllListColumns($container); + + $container->postProcessControls(); + + $model->columns = $container->getPrimaryControls() + $container->getControls(); + + if (!$model->columns) { + return null; + } + + return $model; + } + + /** + * makeModelFilterScopes + */ + protected function makeModelFilterScopes() + { + $model = new ModelFilterModel; + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + $model->setModelClassName($this->getConfig('modelClass')); + + $model->fileName = 'scopes.yaml'; + + $container = new FilterElementContainer; + + $fieldset = $this->sourceModel->getBlueprintFieldset(); + + $fieldset->defineAllFilterScopes($container); + + $model->scopes = $container->getControls(); + + if (!$model->scopes) { + return null; + } + + return $model; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasNavigation.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasNavigation.php new file mode 100644 index 0000000..c5caa9b --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasNavigation.php @@ -0,0 +1,152 @@ + menu_code + */ + protected $seenMenuItems = []; + + /** + * validateNavigation + */ + protected function validateNavigation() + { + $this->seenMenuItems = []; + $model = $this->loadOrCreateMenusModel(); + $model->menus = array_merge($model->menus, $this->makeNavigationItems()); + $model->validate(); + } + + /** + * generateNavigation + */ + protected function generateNavigation() + { + $model = $this->loadOrCreateMenusModel(); + $model->menus = array_merge($model->menus, $this->makeNavigationItems()); + $model->save(); + } + + /** + * makeNavigationItems + */ + protected function makeNavigationItems() + { + $indexer = BlueprintIndexer::instance(); + + $menus = []; + + $parentCodes = []; + + // Primary navigation + foreach ($this->sourceBlueprints as $blueprint) { + $this->setBlueprintContext($blueprint); + + $primaryNav = $indexer->findPrimaryNavigation($blueprint->uuid); + if (!$primaryNav) { + continue; + } + + $parentCodes[$primaryNav->code] = $blueprint->uuid; + $menuItem = $primaryNav->toBackendMenuArray(); + $menuItem['url'] = $this->getControllerUrl(); + $menuItem['code'] = $this->getNavigationCodeForUuid($blueprint->uuid); + $menuItem['sideMenu'] = []; + + $secondaryNav = $indexer->findSecondaryNavigation($blueprint->uuid); + if ($secondaryNav && $secondaryNav->hasPrimary) { + $subItem = $secondaryNav->toBackendMenuArray(); + $subItem['url'] = $this->getControllerUrl(); + $subItem['code'] = $this->getNavigationCodeForUuid($blueprint->uuid); + $subItem['permissions'] = [$this->getConfig('permissionCode')]; + $menuItem['sideMenu'][$secondaryNav->code] = $subItem; + $this->seenMenuItems[$blueprint->uuid] = $menuItem['code'].'||'.$subItem['code']; + } + + $menus[$primaryNav->code] = $menuItem; + } + + // Secondary navigation + foreach ($this->sourceBlueprints as $blueprint) { + $this->setBlueprintContext($blueprint); + + $secondaryNav = $indexer->findSecondaryNavigation($blueprint->uuid); + if (!$secondaryNav || $secondaryNav->hasPrimary) { + continue; + } + + if (!$secondaryNav->parentCode || !isset($menus[$secondaryNav->parentCode])) { + continue; + } + + $subItem = $secondaryNav->toBackendMenuArray(); + $subItem['url'] = $this->getControllerUrl(); + $subItem['code'] = $this->getNavigationCodeForUuid($blueprint->uuid); + $subItem['permissions'] = [$this->getConfig('permissionCode')]; + + $parentUuid = $parentCodes[$secondaryNav->parentCode] ?? null; + $this->seenMenuItems[$blueprint->uuid] = $parentUuid + ? $this->getNavigationCodeForUuid($parentUuid).'||'.$subItem['code'] + : $subItem['code']; + + $menus[$secondaryNav->parentCode]['sideMenu'][$secondaryNav->code] = $subItem; + } + + foreach ($menus as &$menu) { + $parentPermissions = []; + foreach ($menu['sideMenu'] as $item) { + $parentPermissions = array_merge($parentPermissions, $item['permissions']); + } + $menu['permissions'] = $parentPermissions; + } + + return $menus; + } + + /** + * getNavigationCodeForUuid + */ + protected function getNavigationCodeForUuid($uuid) + { + return $this->sourceModel->blueprints[$uuid]['menuCode'] ?? 'unknown'; + } + + /** + * loadOrCreateMenusModel + */ + protected function loadOrCreateMenusModel() + { + $model = new MenusModel; + + $model->loadPlugin($this->sourceModel->getPluginCodeObj()->toCode()); + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + return $model; + } + + /** + * getControllerUrl + */ + protected function getControllerUrl() + { + return $this->sourceModel->getPluginCodeObj()->toUrl().'/'.strtolower($this->getConfig('controllerClass')); + } + + /** + * getActiveMenuItemCode + */ + protected function getActiveMenuItemCode() + { + $uuid = $this->sourceModel->getBlueprintObject()->uuid; + + return $this->seenMenuItems[$uuid] ?? 'unknown'; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasPermissions.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasPermissions.php new file mode 100644 index 0000000..a4f7666 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasPermissions.php @@ -0,0 +1,61 @@ +getConfig('permissionCode')) { + $blueprint = $this->sourceModel->getBlueprintObject(); + $model = $this->loadOrCreatePermissionsModel(); + $model->permissions[] = $this->makePermissionItem($blueprint, $permissionCode); + $model->validate(); + } + } + + /** + * generatePermission + */ + protected function generatePermission() + { + if ($permissionCode = $this->getConfig('permissionCode')) { + $blueprint = $this->sourceModel->getBlueprintObject(); + $model = $this->loadOrCreatePermissionsModel(); + $model->permissions[] = $this->makePermissionItem($blueprint, $permissionCode); + $model->save(); + } + } + + /** + * makePermissionItem + */ + protected function makePermissionItem($blueprint, $code) + { + return [ + 'permission' => $code, + 'tab' => $this->sourceModel->getPluginName(), + 'label' => __("Manage :name Items", ['name' => $blueprint->name]), + ]; + } + + /** + * loadOrCreatePermissionsModel + */ + protected function loadOrCreatePermissionsModel() + { + $model = new PermissionsModel; + + $model->loadPlugin($this->sourceModel->getPluginCodeObj()->toCode()); + + $model->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + return $model; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/HasVersionFile.php b/plugins/rainlab/builder/classes/blueprintgenerator/HasVersionFile.php new file mode 100644 index 0000000..95a8a38 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/HasVersionFile.php @@ -0,0 +1,77 @@ +sourceModel->getPluginFilePath('updates/version.yaml'); + + $versionInformation = $this->sourceModel->getPluginVersionInformation(); + + $nextVersion = $this->getNextVersion(); + + foreach ($this->migrationScripts as $scriptName => $comment) { + $versionInformation[$nextVersion] = [ + $comment, + $scriptName + ]; + + $nextVersion = $this->getNextVersion($nextVersion); + } + + // Add "v" to the version information + $versionInformation = $this->normalizeVersions((array) $versionInformation); + + $yamlData = Yaml::render($versionInformation); + + if (!File::put($versionFilePath, $yamlData)) { + throw new ApplicationException(sprintf('Error saving file %s', $versionFilePath)); + } + + @File::chmod($versionFilePath); + } + + /** + * getNextVersion returns the next version for this plugin + */ + protected function getNextVersion($fromVersion = null) + { + if ($fromVersion) { + $parts = explode('.', $fromVersion); + + $parts[count($parts)-1]++; + + return implode('.', $parts); + } + + $migration = new MigrationModel; + + $migration->setPluginCodeObj($this->sourceModel->getPluginCodeObj()); + + return $migration->getNextVersion(); + } + + /** + * normalizeVersions checks some versions start with v and others not + */ + protected function normalizeVersions(array $versions): array + { + $result = []; + foreach ($versions as $key => $value) { + $version = rtrim(ltrim((string) $key, 'v'), '.'); + $result['v'.$version] = $value; + } + return $result; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/ListElementContainer.php b/plugins/rainlab/builder/classes/blueprintgenerator/ListElementContainer.php new file mode 100644 index 0000000..9fb7578 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/ListElementContainer.php @@ -0,0 +1,103 @@ +label($label)->displayAs('text'); + + $this->columns[$columnName] = $column; + + return $column; + } + + /** + * postProcessControls + */ + public function postProcessControls() + { + foreach ($this->columns as $name => $field) { + if ($field->type === 'partial' && starts_with($field->path, '~/modules/tailor/contentfields/')) { + $field->displayAs('text')->path(null)->relation($name)->select('title'); + } + } + } + + /** + * getPrimaryControls + */ + public function getPrimaryControls() + { + $host = new self; + + $host->defineColumn('id', 'ID')->invisible(); + $host->defineColumn('title', 'Title')->searchable(true); + $host->defineColumn('slug', 'Slug')->searchable(true)->invisible(); + + return $host->getControls(); + } + + /** + * getControls + */ + public function getControls(): array + { + $result = []; + $index = 0; + + foreach ($this->columns as $name => $field) { + $result[$name] = $this->parseFieldConfig($index, $name, $field->config); + $index++; + } + + return $result; + } + + /** + * parseFieldConfig + */ + protected function parseFieldConfig($index, $name, $config): array + { + // Remove tailor values + $ignoreConfig = [ + 'columnName', + 'source', + 'externalToolbarAppState', + 'externalToolbarEventBus' + ]; + + $parsedConfig = array_except((array) $config, $ignoreConfig); + + $parsedConfig['id'] = $index; + $parsedConfig['field'] = $name; + + // Remove default values + $keepDefaults = [ + 'type', + ]; + + $defaultField = new ListColumn; + foreach ($parsedConfig as $key => $value) { + if (!in_array($key, $keepDefaults) && $defaultField->$key === $value) { + unset($parsedConfig[$key]); + } + } + + return $parsedConfig; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/ModelContainer.php b/plugins/rainlab/builder/classes/blueprintgenerator/ModelContainer.php new file mode 100644 index 0000000..294c628 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/ModelContainer.php @@ -0,0 +1,272 @@ +propagatable = array_merge($this->propagatable, $attributes); + } + + /** + * getBlueprintAttribute + */ + public function getBlueprintAttribute() + { + return $this->getBlueprintDefinition(); + } + + /** + * getProcessedRelationDefinitions + */ + public function getProcessedRelationDefinitions() + { + $definitions = parent::getRelationDefinitions(); + + // Process specific field types + $fieldset = $this->sourceModel->getBlueprintFieldset(); + foreach ($fieldset->getAllFields() as $name => $field) { + if ($field->type === 'entries') { + $this->processEntryRelationDefinitions($definitions, $name, $field); + } + if ($field->type === 'repeater') { + $this->processRepeaterRelationDefinitions($definitions, $name, $field); + } + } + + return $definitions; + } + + /** + * processRepeaterRelationDefinitions + */ + protected function processRepeaterRelationDefinitions(&$definitions, $fieldName, $fieldObj) + { + foreach ($definitions as $type => &$relations) { + foreach ($relations as $name => &$props) { + if ($name === $fieldName && isset($props[0]) && $props[0] === \Tailor\Models\RepeaterItem::class) { + $repeaterInfo = $this->getRepeaterTableInfoFor($fieldName, $fieldObj); + $pluginCodeObj = $this->sourceModel->getPluginCodeObj(); + $props[0] = $pluginCodeObj->toPluginNamespace().'\\Models\\'.$repeaterInfo['modelClass']; + $props['key'] = 'parent_id'; + unset($props['relationClass']); + break; + } + } + } + } + + /** + * getRepeaterTableInfoFor + */ + public function getRepeaterTableInfoFor($fieldName, $fieldObj): ?array + { + $tableName = $this->sourceModel->getBlueprintConfig('tableName'); + if (!$tableName) { + throw new ApplicationException('Missing a table name for repeaters'); + } + + $modelClass = $this->sourceModel->getBlueprintConfig('modelClass'); + $repeaterTable = $tableName .= '_' . mb_strtolower($fieldName) . '_items'; + $repeaterModelClass = $modelClass . Str::studly($fieldName) . 'Item'; + + return [ + 'fieldName' => $fieldName, + 'tableName' => $repeaterTable, + 'modelClass' => $repeaterModelClass + ]; + } + + /** + * processEntryRelationDefinitions + */ + protected function processEntryRelationDefinitions(&$definitions, $fieldName, $fieldObj) + { + $foundDefinition = null; + $foundAsType = null; + foreach ($definitions as $type => &$relations) { + foreach ($relations as $name => &$props) { + if ($name === $fieldName) { + // (╯°□°)╯︵ ┻━┻ + $foundDefinition = array_pull($relations, $name); + $foundAsType = $type; + break; + } + } + } + + if (!$foundDefinition) { + return; + } + + // Clean up and replace class + if ($overrideClass = $this->findRelatedModelClass($fieldObj->source)) { + $foundDefinition[0] = $overrideClass; + } + elseif ($overrideBlueprint = $this->findUuidFromSource($fieldObj->source)) { + $foundDefinition['blueprint'] = $overrideBlueprint; + } + + unset($foundDefinition['relationClass']); + + // This converts custom tailor relations to standard belongs to many + if (isset($foundDefinition['table'])) { + $joinInfo = $fieldObj->inverse + ? $this->getInverseJoinTableInfoFor($fieldName, $fieldObj, $foundDefinition) + : $this->getJoinTableInfoFor($fieldName, $fieldObj); + + if ($joinInfo) { + $foundAsType = 'belongsToMany'; + $foundDefinition['table'] = $joinInfo['tableName']; + unset($foundDefinition['name']); + + // Generic key for blueprints + if ($joinInfo['isBlueprint']) { + $foundDefinition['otherKey'] = $joinInfo['relatedKey']; + } + + // Swap keys + if ($fieldObj->inverse) { + $foundDefinition['key'] = $joinInfo['relatedKey']; + $foundDefinition['otherKey'] = $joinInfo['parentKey']; + } + } + } + + // ┬─┬ノ( º _ ºノ) + $definitions[$foundAsType][$fieldName] = $foundDefinition; + } + + /** + * getJoinTableInfoFor + */ + public function getJoinTableInfoFor($fieldName, $fieldObj): ?array + { + $tableName = $this->sourceModel->getBlueprintConfig('tableName'); + if (!$tableName) { + throw new ApplicationException('Missing a table name for joins'); + } + + $joinTable = $tableName .= '_' . mb_strtolower($fieldName) . '_join'; + + $modelClass = $this->sourceModel->getBlueprintConfig('modelClass'); + $relatedModelClass = $this->findRelatedModelClass($fieldObj->source); + if (!$modelClass) { + return null; + } + + $parentKey = Str::snake(class_basename($modelClass)).'_id'; + $relatedKey = $relatedModelClass + ? Str::snake(class_basename($relatedModelClass)).'_id' + : 'relation_id'; + + return [ + 'fieldName' => $fieldName, + 'tableName' => $joinTable, + 'parentKey' => $parentKey, + 'relatedKey' => $relatedKey, + 'isBlueprint' => !$relatedModelClass + ]; + } + + /** + * getInverseJoinTableInfoFor + */ + public function getInverseJoinTableInfoFor($fieldName, $fieldObj, $foundDefinition): ?array + { + $relatedUuid = $this->findUuidFromSource($fieldObj->source); + if (!$relatedUuid) { + return null; + } + + // Determine join table + $tableName = $this->sourceModel->blueprints[$relatedUuid]['tableName'] ?? null; + if ($tableName) { + $joinTable = $tableName .= '_' . mb_strtolower($fieldObj->inverse) . '_join'; + } + else { + $joinTable = $foundDefinition['table'] ?? 'unknown'; + } + + $modelClass = $this->sourceModel->getBlueprintConfig('modelClass'); + $relatedModelClass = $this->findRelatedModelClass($fieldObj->source); + if (!$modelClass) { + return null; + } + + $parentKey = Str::snake(class_basename($modelClass)).'_id'; + $relatedKey = $relatedModelClass + ? Str::snake(class_basename($relatedModelClass)).'_id' + : 'relation_id'; + + return [ + 'tableName' => $joinTable, + 'parentKey' => $parentKey, + 'relatedKey' => $relatedKey, + 'isBlueprint' => !$relatedModelClass + ]; + } + + /** + * getValidationDefinitions + */ + public function getValidationDefinitions() + { + return [ + 'rules' => $this->rules, + 'attributeNames' => $this->attributeNames, + 'customMessages' => $this->customMessages, + ]; + } + + /** + * getMultisiteDefinition + */ + public function getMultisiteDefinition() + { + $multisiteSync = $this->blueprint instanceof \Tailor\Classes\Blueprint\EntryBlueprint && + $this->blueprint->useMultisiteSync(); + + return [ + 'fields' => $this->propagatable, + 'sync' => $multisiteSync + ]; + } +} diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-join.php.tpl b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-join.php.tpl new file mode 100644 index 0000000..9cbe949 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-join.php.tpl @@ -0,0 +1,24 @@ +integer('{{ parentKey }}')->unsigned(); + $table->integer('{{ relatedKey }}')->unsigned(); + $table->primary(['{{ parentKey }}', '{{ relatedKey }}']); + }); + } + + public function down() + { + Schema::dropIfExists('{{ tableName }}'); + } +}; diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-repeater.php.tpl b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-repeater.php.tpl new file mode 100644 index 0000000..b26ec1d --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration-repeater.php.tpl @@ -0,0 +1,25 @@ +increments('id'); + $table->integer('parent_id')->unsigned()->nullable()->index(); + $table->mediumText('value')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('{{ tableName }}'); + } +}; diff --git a/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration.php.tpl b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration.php.tpl new file mode 100644 index 0000000..7f42e88 --- /dev/null +++ b/plugins/rainlab/builder/classes/blueprintgenerator/templates/migration.php.tpl @@ -0,0 +1,35 @@ +increments('id'); + $table->integer('site_id')->nullable()->index(); + $table->integer('site_root_id')->nullable()->index(); + $table->string('title')->nullable(); + $table->string('slug')->nullable()->index(); +{{ migrationCode|raw }} +{% if useStructure %} + $table->integer('parent_id')->nullable(); + $table->integer('nest_left')->nullable(); + $table->integer('nest_right')->nullable(); + $table->integer('nest_depth')->nullable(); +{% endif %} + $table->softDeletes(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('{{ tableName }}'); + } +}; diff --git a/plugins/rainlab/builder/classes/controllergenerator/templates/controller-config-vars.php.tpl b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-config-vars.php.tpl new file mode 100644 index 0000000..62c2600 --- /dev/null +++ b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-config-vars.php.tpl @@ -0,0 +1,3 @@ +{% for configVar, varValue in behaviorConfigVars %} + public ${{ configVar }} = '{{ varValue }}'; +{% endfor %} \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/controllergenerator/templates/controller-no-list.php.tpl b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-no-list.php.tpl new file mode 100644 index 0000000..c77f05f --- /dev/null +++ b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-no-list.php.tpl @@ -0,0 +1,12 @@ + + public function index() + { + $model = $this->formCreateModelObject()->first(); + + if (!$model) { + $model = $this->formCreateModelObject(); + $model->forceSave(); + } + + return Backend::redirect("{{ controllerUrl }}/update/{$model->id}"); + } \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/controllergenerator/templates/controller-permissions.php.tpl b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-permissions.php.tpl new file mode 100644 index 0000000..e1395dd --- /dev/null +++ b/plugins/rainlab/builder/classes/controllergenerator/templates/controller-permissions.php.tpl @@ -0,0 +1,7 @@ +{% if permissions %} + public $requiredPermissions = [ +{% for permission in permissions %} + '{{ permission }}'{% if not loop.last %},{% endif %} +{% endfor %} + ]; +{% endif %} \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/controllergenerator/templates/controller.php.tpl b/plugins/rainlab/builder/classes/controllergenerator/templates/controller.php.tpl new file mode 100644 index 0000000..f6482ef --- /dev/null +++ b/plugins/rainlab/builder/classes/controllergenerator/templates/controller.php.tpl @@ -0,0 +1,27 @@ +getName(); + + if (in_array($name, ['mysql', 'sqlite'])) { + $method = 'get'.ucfirst($name).'PlatformSQLDeclaration'; + + return $this->$method($fieldDeclaration); + } + + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets the SQL declaration snippet for a field of this type for the MySQL Platform. + * + * @param array $fieldDeclaration The field declaration. + * + * @return string + */ + protected function getMysqlPlatformSQLDeclaration(array $fieldDeclaration) + { + $columnType = $fieldDeclaration['precision'] ? "TIMESTAMP({$fieldDeclaration['precision']})" : 'TIMESTAMP'; + + if (isset($fieldDeclaration['notnull']) && $fieldDeclaration['notnull'] == true) { + return $columnType; + } + + return "$columnType NULL"; + } + + /** + * Gets the SQL declaration snippet for a field of this type for the Sqlite Platform. + * + * @param array $fieldDeclaration The field declaration. + * + * @return string + */ + protected function getSqlitePlatformSQLDeclaration(array $fieldDeclaration) + { + return $this->getMysqlPlatformSQLDeclaration($fieldDeclaration); + } +} diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/create.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/create.php.tpl new file mode 100644 index 0000000..5a9b5e7 --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/create.php.tpl @@ -0,0 +1,46 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + +
    +
    + + + + +

    fatalError)) ?>

    +

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/preview.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/preview.php.tpl new file mode 100644 index 0000000..8236c2f --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/preview.php.tpl @@ -0,0 +1,22 @@ + + + + +fatalError): ?> + +
    + formRenderPreview() ?> +
    + + +

    fatalError) ?>

    + + +

    + + + +

    \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/update.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/update.php.tpl new file mode 100644 index 0000000..544f44a --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/formcontroller/templates/update.php.tpl @@ -0,0 +1,54 @@ + + + + +fatalError): ?> + + 'layout']) ?> + +
    + formRender() ?> +
    + +
    +
    + + + + + + + +
    +
    + + + +

    fatalError)) ?>

    +

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/export.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/export.php.tpl new file mode 100644 index 0000000..352e5bf --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/export.php.tpl @@ -0,0 +1,18 @@ + 'layout']) ?> + +
    + exportRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/import.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/import.php.tpl new file mode 100644 index 0000000..10122a0 --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/importexportcontroller/templates/import.php.tpl @@ -0,0 +1,18 @@ + 'layout']) ?> + +
    + importRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/_list_toolbar.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/_list_toolbar.php.tpl new file mode 100644 index 0000000..6a78847 --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/_list_toolbar.php.tpl @@ -0,0 +1,26 @@ +
    +{% if hasFormBehavior %} + + + +{% endif %} +{% if hasImportExportBehavior %} + + + + + + +{% endif %} + +
    diff --git a/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/index.php.tpl b/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/index.php.tpl new file mode 100644 index 0000000..ea43a36 --- /dev/null +++ b/plugins/rainlab/builder/classes/standardbehaviorsregistry/listcontroller/templates/index.php.tpl @@ -0,0 +1 @@ +listRender() ?> diff --git a/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormFields.php b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormFields.php new file mode 100644 index 0000000..ade2531 --- /dev/null +++ b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormFields.php @@ -0,0 +1,347 @@ +controlLibrary->registerControl( + 'text', + 'rainlab.builder::lang.form.control_text', + 'rainlab.builder::lang.form.control_text_description', + ControlLibrary::GROUP_STANDARD, + 'icon-terminal', + $this->controlLibrary->getStandardProperties(['stretch']), + null + ); + } + + /** + * registerNumberControl + */ + protected function registerNumberControl() + { + // Extra properties + $extraProps = [ + 'min' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_min'), + 'description' => Lang::get('rainlab.builder::lang.form.property_min_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_min_number') + ] + ], + ], + 'max' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_max'), + 'description' => Lang::get('rainlab.builder::lang.form.property_max_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_max_number') + ] + ], + ], + 'step' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_step'), + 'description' => Lang::get('rainlab.builder::lang.form.property_step_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_step_number') + ] + ], + ], + ]; + + $this->controlLibrary->registerControl( + 'number', + 'rainlab.builder::lang.form.control_number', + 'rainlab.builder::lang.form.control_number_description', + ControlLibrary::GROUP_STANDARD, + 'icon-superscript', + $this->controlLibrary->getStandardProperties(['stretch'], $extraProps), + null + ); + } + + /** + * registerPasswordControl + */ + protected function registerPasswordControl() + { + $this->controlLibrary->registerControl( + 'password', + 'rainlab.builder::lang.form.control_password', + 'rainlab.builder::lang.form.control_password_description', + ControlLibrary::GROUP_STANDARD, + 'icon-lock', + $this->controlLibrary->getStandardProperties(['stretch']), + null + ); + } + + /** + * registerEmailControl + */ + protected function registerEmailControl() + { + $this->controlLibrary->registerControl( + 'email', + 'rainlab.builder::lang.form.control_email', + 'rainlab.builder::lang.form.control_email_description', + ControlLibrary::GROUP_STANDARD, + 'icon-envelope', + $this->controlLibrary->getStandardProperties(['stretch']), + null + ); + } + + /** + * registerTextareaControl + */ + protected function registerTextareaControl() + { + $properties = $this->getFieldSizeProperties(); + + $this->controlLibrary->registerControl( + 'textarea', + 'rainlab.builder::lang.form.control_textarea', + 'rainlab.builder::lang.form.control_textarea_description', + ControlLibrary::GROUP_STANDARD, + 'icon-pencil-square-o', + $this->controlLibrary->getStandardProperties(['stretch'], $properties), + null + ); + } + + /** + * registerDropdownControl + */ + protected function registerDropdownControl() + { + $properties = [ + 'options' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options'), + 'type' => 'dictionary', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + 'optionsMethod' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options_method'), + 'description' => Lang::get('rainlab.builder::lang.form.property_options_method_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + 'emptyOption' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_empty_option'), + 'description' => Lang::get('rainlab.builder::lang.form.property_empty_option_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 83 + ], + 'showSearch' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_show_search'), + 'description' => Lang::get('rainlab.builder::lang.form.property_show_search_description'), + 'type' => 'checkbox', + 'sortOrder' => 84, + 'default' => true + ] + ]; + + $this->controlLibrary->registerControl( + 'dropdown', + 'rainlab.builder::lang.form.control_dropdown', + 'rainlab.builder::lang.form.control_dropdown_description', + ControlLibrary::GROUP_STANDARD, + 'icon-angle-double-down', + $this->controlLibrary->getStandardProperties(['stretch'], $properties), + null + ); + } + + /** + * registerRadioListControl + */ + protected function registerRadioListControl() + { + $properties = [ + 'options' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options'), + 'type' => 'dictionary', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + 'optionsMethod' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options_method'), + 'description' => Lang::get('rainlab.builder::lang.form.property_options_method_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + ]; + + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'preset' + ]; + + $this->controlLibrary->registerControl( + 'radio', + 'rainlab.builder::lang.form.control_radio', + 'rainlab.builder::lang.form.control_radio_description', + ControlLibrary::GROUP_STANDARD, + 'icon-dot-circle-o', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerBalloonSelectorControl + */ + protected function registerBalloonSelectorControl() + { + $properties = [ + 'options' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options'), + 'type' => 'dictionary', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + 'optionsMethod' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options_method'), + 'description' => Lang::get('rainlab.builder::lang.form.property_options_method_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + ]; + + $this->controlLibrary->registerControl( + 'balloon-selector', + 'rainlab.builder::lang.form.control_balloon-selector', + 'rainlab.builder::lang.form.control_balloon-selector_description', + ControlLibrary::GROUP_STANDARD, + 'icon-ellipsis-h', + $this->controlLibrary->getStandardProperties(['stretch'], $properties), + null + ); + } + + /** + * registerCheckboxControl + */ + protected function registerCheckboxControl() + { + $this->controlLibrary->registerControl( + 'checkbox', + 'rainlab.builder::lang.form.control_checkbox', + 'rainlab.builder::lang.form.control_checkbox_description', + ControlLibrary::GROUP_STANDARD, + 'icon-check-square-o', + $this->controlLibrary->getStandardProperties(['oc.commentPosition', 'stretch'], $this->getCheckboxTypeProperties()), + null + ); + } + + /** + * registerCheckboxListControl + */ + protected function registerCheckboxListControl() + { + $properties = [ + 'options' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options'), + 'type' => 'dictionary', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + 'optionsMethod' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options_method'), + 'description' => Lang::get('rainlab.builder::lang.form.property_options_method_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + 'quickselect' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_quickselect'), + 'description' => Lang::get('rainlab.builder::lang.form.property_quickselect_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'inlineOptions' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_inline_options'), + 'description' => Lang::get('rainlab.builder::lang.form.property_inline_options_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ] + ]; + + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'preset' + ]; + + $this->controlLibrary->registerControl( + 'checkboxlist', + 'rainlab.builder::lang.form.control_checkboxlist', + 'rainlab.builder::lang.form.control_checkboxlist_description', + ControlLibrary::GROUP_STANDARD, + 'icon-list', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerSwitchControl + */ + protected function registerSwitchControl() + { + $this->controlLibrary->registerControl( + 'switch', + 'rainlab.builder::lang.form.control_switch', + 'rainlab.builder::lang.form.control_switch_description', + ControlLibrary::GROUP_STANDARD, + 'icon-toggle-on', + $this->controlLibrary->getStandardProperties(['oc.commentPosition', 'stretch'], $this->getCheckboxTypeProperties()), + null + ); + } + + /** + * getCheckboxTypeProperties + */ + protected function getCheckboxTypeProperties() + { + return [ + 'default' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_checked_default_title'), + 'type' => 'checkbox' + ] + ]; + } +} diff --git a/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormUi.php b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormUi.php new file mode 100644 index 0000000..60d7c9a --- /dev/null +++ b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormUi.php @@ -0,0 +1,166 @@ +controlLibrary->registerControl( + 'section', + 'rainlab.builder::lang.form.control_section', + 'rainlab.builder::lang.form.control_section_description', + ControlLibrary::GROUP_UI, + 'icon-minus', + $this->controlLibrary->getStandardProperties($excludeProperties), + null + ); + } + + /** + * registerHintControl + */ + protected function registerHintControl() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'required', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'oc.commentPosition', + 'disabled' + ]; + + $properties = [ + 'path' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_hint_path'), + 'description' => Lang::get('rainlab.builder::lang.form.property_hint_path_description'), + 'type' => 'string', + 'sortOrder' => 81 + ], + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_display_mode'), + 'description' => Lang::get('rainlab.builder::lang.form.property_display_mode_description'), + 'type' => 'dropdown', + 'default' => 'info', + 'options' => [ + 'tip' => Lang::get('rainlab.builder::lang.form.class_mode_tip'), + 'info' => Lang::get('rainlab.builder::lang.form.class_mode_info'), + 'warning' => Lang::get('rainlab.builder::lang.form.class_mode_warning'), + 'danger' => Lang::get('rainlab.builder::lang.form.class_mode_danger'), + 'success' => Lang::get('rainlab.builder::lang.form.class_mode_success'), + ], + 'sortOrder' => 82 + ] + ]; + + $this->controlLibrary->registerControl( + 'hint', + 'rainlab.builder::lang.form.control_hint', + 'rainlab.builder::lang.form.control_hint_description', + ControlLibrary::GROUP_UI, + 'icon-question-circle', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerRulerControl + */ + protected function registerRulerControl() + { + $excludeProperties = [ + 'label', + 'stretch', + 'default', + 'placeholder', + 'required', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'oc.comment', + 'oc.commentPosition', + 'disabled' + ]; + + $this->controlLibrary->registerControl( + 'ruler', + 'rainlab.builder::lang.form.control_ruler', + 'rainlab.builder::lang.form.control_ruler_description', + ControlLibrary::GROUP_UI, + 'icon-minus', + $this->controlLibrary->getStandardProperties($excludeProperties), + null + ); + } + + /** + * registerPartialControl + */ + protected function registerPartialControl() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'required', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'oc.commentPosition', + 'oc.comment', + 'disabled' + ]; + + $properties = [ + 'path' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_partial_path'), + 'description' => Lang::get('rainlab.builder::lang.form.property_partial_path_description'), + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_partial_path_required') + ] + ], + 'sortOrder' => 81 + ] + ]; + + $this->controlLibrary->registerControl( + 'partial', + 'rainlab.builder::lang.form.control_partial', + 'rainlab.builder::lang.form.control_partial_description', + ControlLibrary::GROUP_UI, + 'icon-file-text-o', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } +} diff --git a/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormWidgets.php b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormWidgets.php new file mode 100644 index 0000000..c67b96f --- /dev/null +++ b/plugins/rainlab/builder/classes/standardcontrolsregistry/HasFormWidgets.php @@ -0,0 +1,1269 @@ +getFieldSizeProperties() + [ + 'language' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_code_language'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => 'php', + 'options' => [ + 'css' => 'CSS', + 'html' => 'HTML', + 'javascript' => 'JavaScript', + 'less' => 'LESS', + 'markdown' => 'Markdown', + 'php' => 'PHP', + 'plain_text' => 'Plain text', + 'sass' => 'SASS', + 'scss' => 'SCSS', + 'twig' => 'Twig' + ], + 'sortOrder' => 82 + ], + 'theme' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_code_theme'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_theme_use_default'), + 'ambiance' => 'Ambiance', + 'chaos' => 'Chaos', + 'chrome' => 'Chrome', + 'clouds' => 'Clouds', + 'clouds_midnight' => 'Clouds midnight', + 'cobalt' => 'Cobalt', + 'crimson_editor' => 'Crimson editor', + 'dawn' => 'Dawn', + 'dreamweaver' => 'Dreamweaver', + 'eclipse' => 'Eclipse', + 'github' => 'Github', + 'idle_fingers' => 'Idle fingers', + 'iplastic' => 'IPlastic', + 'katzenmilch' => 'Katzenmilch', + 'kr_theme' => 'krTheme', + 'kuroir' => 'Kuroir', + 'merbivore' => 'Merbivore', + 'merbivore_soft' => 'Merbivore soft', + 'mono_industrial' => 'Mono industrial', + 'monokai' => 'Monokai', + 'pastel_on_dark' => 'Pastel on dark', + 'solarized_dark' => 'Solarized dark', + 'solarized_light' => 'Solarized light', + 'sqlserver' => 'SQL server', + 'terminal' => 'Terminal', + 'textmate' => 'Textmate', + 'tomorrow' => 'Tomorrow', + 'tomorrow_night' => 'Tomorrow night', + 'tomorrow_night_blue' => 'Tomorrow night blue', + 'tomorrow_night_bright' => 'Tomorrow night bright', + 'tomorrow_night_eighties' => 'Tomorrow night eighties', + 'twilight' => 'Twilight', + 'vibrant_ink' => 'Vibrant ink', + 'xcode' => 'XCode' + ], + 'sortOrder' => 83 + ], + 'showGutter' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_gutter'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'booleanValues' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 'true' => Lang::get('rainlab.builder::lang.form.property_gutter_show'), + 'false' => Lang::get('rainlab.builder::lang.form.property_gutter_hide'), + ], + 'sortOrder' => 84 + ], + 'wordWrap' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_wordwrap'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'booleanValues' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 'true' => Lang::get('rainlab.builder::lang.form.property_wordwrap_wrap'), + 'false' => Lang::get('rainlab.builder::lang.form.property_wordwrap_nowrap'), + ], + 'sortOrder' => 85 + ], + 'fontSize' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fontsize'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + '10' => '10px', + '11' => '11px', + '12' => '11px', + '13' => '13px', + '14' => '14px', + '16' => '16px', + '18' => '18px', + '20' => '20px', + '24' => '24px' + ], + 'sortOrder' => 86 + ], + 'codeFolding' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_codefolding'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 'manual' => Lang::get('rainlab.builder::lang.form.property_codefolding_manual'), + 'markbegin' => Lang::get('rainlab.builder::lang.form.property_codefolding_markbegin'), + 'markbeginend' => Lang::get('rainlab.builder::lang.form.property_codefolding_markbeginend'), + ], + 'sortOrder' => 87 + ], + 'autoClosing' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_autoclosing'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'booleanValues' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 'true' => Lang::get('rainlab.builder::lang.form.property_enabled'), + 'false' => Lang::get('rainlab.builder::lang.form.property_disabled') + ], + 'sortOrder' => 88 + ], + 'useSoftTabs' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_soft_tabs'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'booleanValues' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 'true' => Lang::get('rainlab.builder::lang.form.property_enabled'), + 'false' => Lang::get('rainlab.builder::lang.form.property_disabled') + ], + 'sortOrder' => 89 + ], + 'tabSize' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_tab_size'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'type' => 'dropdown', + 'default' => '', + 'ignoreIfEmpty' => true, + 'options' => [ + '' => Lang::get('rainlab.builder::lang.form.property_use_default'), + 2 => 2, + 4 => 4, + 8 => 8 + ], + 'sortOrder' => 90 + ], + 'readOnly' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_readonly'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_code_editor'), + 'default' => 0, + 'ignoreIfEmpty' => true, + 'type' => 'checkbox' + ] + ]; + + $this->controlLibrary->registerControl( + 'codeeditor', + 'rainlab.builder::lang.form.control_codeeditor', + 'rainlab.builder::lang.form.control_codeeditor_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-code', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerColorPickerWidget + */ + protected function registerColorPickerWidget() + { + $excludeProperties = [ + 'stretch' + ]; + + $properties = [ + 'availableColors' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_available_colors'), + 'description' => Lang::get('rainlab.builder::lang.form.property_available_colors_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_colorpicker'), + 'type' => 'stringList', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + 'allowEmpty' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_allow_empty'), + 'description' => Lang::get('rainlab.builder::lang.form.property_allow_empty_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_colorpicker'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfEmpty' => true, + ], + 'allowCustom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_allow_custom'), + 'description' => Lang::get('rainlab.builder::lang.form.property_allow_custom_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_colorpicker'), + 'type' => 'checkbox', + 'default' => true, + 'ignoreIfEmpty' => true, + ], + 'showAlpha' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_show_alpha'), + 'description' => Lang::get('rainlab.builder::lang.form.property_show_alpha_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_colorpicker'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'showInput' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_show_input'), + 'description' => Lang::get('rainlab.builder::lang.form.property_show_input_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_colorpicker'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + ]; + + $this->controlLibrary->registerControl( + 'colorpicker', + 'rainlab.builder::lang.form.control_colorpicker', + 'rainlab.builder::lang.form.control_colorpicker_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-eyedropper', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerDataTableWidget + */ + protected function registerDataTableWidget() + { + $excludeProperties = [ + 'stretch' + ]; + + $properties = [ + 'oc.columns' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_columns'), + 'description' => Lang::get('rainlab.builder::lang.form.property_columns_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datatable'), + 'type' => 'objectList', + 'ignoreIfEmpty' => true, + 'titleProperty' => 'title', + 'itemProperties' => [ + [ + 'property' => 'type', + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_type'), + 'type' => 'dropdown', + 'default' => 'string', + 'options' => [ + 'string' => "String", + 'checkbox' => "Checkbox", + 'dropdown' => "Dropdown", + 'autocomplete' => "Autocomplete", + ], + ], + [ + 'property' => 'code', + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_code'), + 'type' => 'string', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_datatable_code_regex'), + ] + ], + ], + [ + 'property' => 'title', + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_title'), + 'type' => 'string' + ], + [ + 'property' => 'options', + 'title' => Lang::get('rainlab.builder::lang.form.property_options'), + 'type' => 'dictionary', + 'ignoreIfEmpty' => true, + ], + [ + 'property' => 'width', + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_width'), + 'type' => 'string', + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_datatable_width_regex') + ] + ], + ] + ], + 'sortOrder' => 81 + ], + 'adding' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_adding'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datatable_adding_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datatable'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'deleting' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_deleting'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datatable_deleting_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datatable'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + 'searching' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datatable_searching'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datatable_searching_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datatable'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + ], + ]; + + $this->controlLibrary->registerControl( + 'datatable', + 'rainlab.builder::lang.form.control_datatable', + 'rainlab.builder::lang.form.control_datatable_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-table', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerDatepickerWidget + */ + protected function registerDatepickerWidget() + { + $excludeProperties = [ + 'stretch' + ]; + + $properties = [ + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_mode'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'dropdown', + 'default' => 'datetime', + 'options' => [ + 'date' => Lang::get('rainlab.builder::lang.form.property_datepicker_mode_date'), + 'datetime' => Lang::get('rainlab.builder::lang.form.property_datepicker_mode_datetime'), + 'time' => Lang::get('rainlab.builder::lang.form.property_datepicker_mode_time') + ], + 'sortOrder' => 81 + ], + 'format' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_format'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_year_format_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + 'minDate' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_min_date'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_min_date_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 83 + ], + 'maxDate' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_max_date'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_max_date_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 84 + ], + 'yearRange' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_year_range'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_year_range_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^([0-9]+|\[[0-9]{4},[0-9]{4}\])$', + 'message' => Lang::get('rainlab.builder::lang.form.property_datepicker_year_range_invalid_format') + ] + ], + 'sortOrder' => 85 + ], + 'firstDay' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_first_day'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_first_day_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'string', + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_datepicker_first_day_regex') + ] + ], + 'ignoreIfEmpty' => true, + 'sortOrder' => 86 + ], + 'twelveHour' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_twelve_hour'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_twelve_hour_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 87 + ], + 'showWeekNumber' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_datepicker_show_week_number'), + 'description' => Lang::get('rainlab.builder::lang.form.property_datepicker_show_week_number_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_datepicker'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 88 + ], + ]; + + $this->controlLibrary->registerControl( + 'datepicker', + 'rainlab.builder::lang.form.control_datepicker', + 'rainlab.builder::lang.form.control_datepicker_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-calendar', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerFileUploadWidget + */ + protected function registerFileUploadWidget() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes' + ]; + + $properties = [ + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_mode'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'dropdown', + 'default' => 'file', + 'options' => [ + 'file' => Lang::get('rainlab.builder::lang.form.property_fileupload_mode_file'), + 'image' => Lang::get('rainlab.builder::lang.form.property_fileupload_mode_image') + ], + 'sortOrder' => 81 + ], + 'imageWidth' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_width'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_width_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_dimension') + ] + ], + 'sortOrder' => 83 + ], + 'imageHeight' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_height'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_height_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_dimension') + ] + ], + 'sortOrder' => 84 + ], + 'fileTypes' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_file_types'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_file_types_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 85 + ], + 'mimeTypes' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_mime_types'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_mime_types_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 86 + ], + 'useCaption' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_use_caption'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_use_caption_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'checkbox', + 'default' => true, + 'sortOrder' => 87 + ], + 'thumbOptions' => $this->getFieldThumbOptionsProperties()['thumbOptions'], + 'maxFilesize' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_maxfilesize'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_maxfilesize_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'sortOrder' => 89, + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9\.]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_maxfilesize') + ] + ], + ], + 'maxFiles' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_maxfiles'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_maxfiles_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'sortOrder' => 90, + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_maxfiles') + ] + ], + ] + ]; + + $this->controlLibrary->registerControl( + 'fileupload', + 'rainlab.builder::lang.form.control_fileupload', + 'rainlab.builder::lang.form.control_fileupload_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-upload', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerMarkdownWidget + */ + protected function registerMarkdownWidget() + { + $properties = $this->getFieldSizeProperties() + [ + 'sideBySide' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_side_by_side'), + 'description' => Lang::get('rainlab.builder::lang.form.property_side_by_side_description'), + 'type' => 'checkbox', + 'sortOrder' => 81 + ] + ]; + + $this->controlLibrary->registerControl( + 'markdown', + 'rainlab.builder::lang.form.control_markdown', + 'rainlab.builder::lang.form.control_markdown_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-columns', + $this->controlLibrary->getStandardProperties([], $properties), + null + ); + } + + /** + * registerMediaFinderWidget + */ + protected function registerMediaFinderWidget() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'disabled' + ]; + + $properties = [ + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_mediafinder_mode'), + 'type' => 'dropdown', + 'default' => 'file', + 'options' => [ + 'file' => Lang::get('rainlab.builder::lang.form.property_mediafinder_mode_file'), + 'image' => Lang::get('rainlab.builder::lang.form.property_mediafinder_mode_image') + ], + 'sortOrder' => 81 + ], + 'imageWidth' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_width'), + 'description' => Lang::get('rainlab.builder::lang.form.property_mediafinder_image_width_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_dimension') + ] + ], + 'sortOrder' => 82 + ], + 'imageHeight' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_image_height'), + 'description' => Lang::get('rainlab.builder::lang.form.property_mediafinder_image_height_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.form.property_fileupload_invalid_dimension') + ] + ], + 'sortOrder' => 83 + ], + 'maxItems' => $this->getFieldMaxItemsProperties()['maxItems'], + 'thumbOptions' => $this->getFieldThumbOptionsProperties()['thumbOptions'], + ]; + + $this->controlLibrary->registerControl( + 'mediafinder', + 'rainlab.builder::lang.form.control_mediafinder', + 'rainlab.builder::lang.form.control_mediafinder_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-picture-o', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerNestedFormWidget + */ + protected function registerNestedFormWidget() + { + $properties = [ + 'form' => [ + 'type' => 'control-container' + ], + 'showPanel' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_nestedform_show_panel'), + 'description' => Lang::get('rainlab.builder::lang.form.property_nestedform_show_panel_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 87, + ], + 'defaultCreate' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_nestedform_default_create'), + 'description' => Lang::get('rainlab.builder::lang.form.property_nestedform_default_create_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 88, + ], + ]; + + $excludeProperties = [ + 'stretch', + 'placeholder', + 'default', + 'required', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes' + ]; + + $this->controlLibrary->registerControl( + 'nestedform', + 'rainlab.builder::lang.form.control_nestedform', + 'rainlab.builder::lang.form.control_nestedform_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-object-group', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerRecordFinderWidget + */ + protected function registerRecordFinderWidget() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'disabled' + ]; + + $properties = [ + 'nameFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_name_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_name_from_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_recordfinder'), + 'type' => 'string', + 'default' => 'name', + 'sortOrder' => 81 + ], + 'descriptionFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_description_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_description_from_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_recordfinder'), + 'type' => 'string', + 'default' => 'description', + 'sortOrder' => 82 + ], + 'title' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_recordfinder_title'), + 'description' => Lang::get('rainlab.builder::lang.form.property_recordfinder_title_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_recordfinder'), + 'type' => 'builderLocalization', + 'ignoreIfEmpty' => true, + 'sortOrder' => 83 + ], + 'list' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_recordfinder_list'), + 'description' => Lang::get('rainlab.builder::lang.form.property_recordfinder_list_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_recordfinder'), + 'type' => 'autocomplete', + 'fillFrom' => 'plugin-lists', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_recordfinder_list_required'), + ] + ], + 'sortOrder' => 83 + ], + 'scope' => $this->getFieldConditionsProperties()['scope'], + ]; + + $this->controlLibrary->registerControl( + 'recordfinder', + 'rainlab.builder::lang.form.control_recordfinder', + 'rainlab.builder::lang.form.control_recordfinder_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-search', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerRelationWidget + */ + protected function registerRelationWidget() + { + $excludeProperties = [ + 'stretch', + 'default', + 'placeholder', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes', + 'trigger', + 'disabled' + ]; + + $properties = [ + 'nameFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_name_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_name_from_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_relation'), + 'type' => 'string', + 'default' => 'name', + 'sortOrder' => 81 + ], + 'descriptionFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_description_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_description_from_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_relation'), + 'type' => 'string', + 'default' => 'description', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + 'emptyOption' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_relation_prompt'), + 'description' => Lang::get('rainlab.builder::lang.form.property_relation_prompt_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_relation'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 83 + ], + 'select' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_relation_select'), + 'description' => Lang::get('rainlab.builder::lang.form.property_relation_select_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_relation'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 84 + ], + 'scope' => $this->getFieldConditionsProperties()['scope'], + ]; + + $this->controlLibrary->registerControl( + 'relation', + 'rainlab.builder::lang.form.control_relation', + 'rainlab.builder::lang.form.control_relation_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-code-fork', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerRepeaterWidget + */ + protected function registerRepeaterWidget() + { + $properties = [ + 'prompt' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_prompt'), + 'description' => Lang::get('rainlab.builder::lang.form.property_prompt_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'default' => Lang::get('rainlab.builder::lang.form.property_prompt_default'), + 'sortOrder' => 81 + ], + 'titleFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_title_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_title_from_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 82 + ], + 'form' => [ + 'type' => 'control-container' + ], + 'minItems' => $this->getFieldMaxItemsProperties()['minItems'], + 'maxItems' => $this->getFieldMaxItemsProperties()['maxItems'], + 'displayMode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_display_mode'), + 'description' => Lang::get('rainlab.builder::lang.form.property_display_mode_description'), + 'type' => 'dropdown', + 'default' => 'accordion', + 'options' => [ + 'builder' => Lang::get('rainlab.builder::lang.form.display_mode_builder'), + 'accordion' => Lang::get('rainlab.builder::lang.form.display_mode_accordion'), + ], + 'sortOrder' => 85, + ], + // @todo this needs work, the control container doesn't support tabs + // 'useTabs' => [ + // 'title' => "Use Tabs", + // 'description' => "Shows tabs when enabled, allowing fields to specify a tab property.", + // 'type' => 'checkbox', + // 'ignoreIfEmpty' => true, + // 'sortOrder' => 86, + // ], + 'showReorder' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_repeater_show_reorder'), + 'description' => Lang::get('rainlab.builder::lang.form.property_repeater_show_reorder_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 87, + ], + 'showDuplicate' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_repeater_show_duplicate'), + 'description' => Lang::get('rainlab.builder::lang.form.property_repeater_show_duplicate_description'), + 'type' => 'checkbox', + 'ignoreIfEmpty' => true, + 'sortOrder' => 88, + ], + ]; + + $excludeProperties = [ + 'stretch', + 'placeholder', + 'default', + 'required', + 'defaultFrom', + 'dependsOn', + 'preset', + 'attributes' + ]; + + $this->controlLibrary->registerControl( + 'repeater', + 'rainlab.builder::lang.form.control_repeater', + 'rainlab.builder::lang.form.control_repeater_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-server', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * registerRichEditorWidget + */ + protected function registerRichEditorWidget() + { + $properties = $this->getFieldSizeProperties() + [ + 'toolbarButtons' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_richeditor_toolbar_buttons'), + 'description' => Lang::get('rainlab.builder::lang.form.property_richeditor_toolbar_buttons_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_rich_editor'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 81 + ], + ]; + + $this->controlLibrary->registerControl( + 'richeditor', + 'rainlab.builder::lang.form.control_richeditor', + 'rainlab.builder::lang.form.control_richeditor_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-indent', + $this->controlLibrary->getStandardProperties([], $properties), + null + ); + } + + /** + * registerPageFinderWidget + */ + protected function registerPageFinderWidget() + { + $properties = [ + 'singleMode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_pagefinder_single_mode'), + 'description' => Lang::get('rainlab.builder::lang.form.property_pagefinder_single_mode_description'), + 'type' => 'checkbox', + 'sortOrder' => 81 + ] + ]; + + $this->controlLibrary->registerControl( + 'pagefinder', + 'rainlab.builder::lang.form.control_pagefinder', + 'rainlab.builder::lang.form.control_pagefinder_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-paperclip', + $this->controlLibrary->getStandardProperties(['stretch'], $properties), + null + ); + } + + /** + * registerSensitiveWidget + */ + protected function registerSensitiveWidget() + { + $properties = [ + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_display_mode'), + 'description' => Lang::get('rainlab.builder::lang.form.property_display_mode_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_sensitive'), + 'type' => 'dropdown', + 'options' => [ + 'text' => "Text", + 'textarea' => "Textarea", + ], + 'ignoreIfDefault' => true, + 'default' => 'text', + 'sortOrder' => 83 + ], + 'allowCopy' => [ + 'title' => Lang::get('rainlab.builder::lang.form.allow_copy'), + 'description' => Lang::get('rainlab.builder::lang.form.allow_copy_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_sensitive'), + 'type' => 'checkbox', + 'ignoreIfDefault' => true, + 'sortOrder' => 84, + 'default' => true + ], + 'hiddenPlaceholder' => [ + 'title' => Lang::get('rainlab.builder::lang.form.hidden_placeholder'), + 'description' => Lang::get('rainlab.builder::lang.form.hidden_placeholder_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_sensitive'), + 'type' => 'string', + 'default' => '__hidden__', + 'ignoreIfDefault' => true, + 'sortOrder' => 85 + ], + 'hideOnTabChange' => [ + 'title' => Lang::get('rainlab.builder::lang.form.hide_on_tab_change'), + 'description' => Lang::get('rainlab.builder::lang.form.hide_on_tab_change_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_sensitive'), + 'type' => 'checkbox', + 'ignoreIfDefault' => true, + 'sortOrder' => 86, + 'default' => true + ], + ]; + + $this->controlLibrary->registerControl( + 'sensitive', + 'rainlab.builder::lang.form.control_sensitive', + 'rainlab.builder::lang.form.control_sensitive_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-eye-slash', + $this->controlLibrary->getStandardProperties(['stretch'], $properties), + null + ); + } + + /** + * registerTagListWidget + */ + protected function registerTagListWidget() + { + $excludeProperties = [ + 'stretch', + 'readOnly' + ]; + + $properties = [ + 'mode' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_mode'), + 'description' => Lang::get('rainlab.builder::lang.form.property_taglist_mode_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'dropdown', + 'options' => [ + 'string' => Lang::get('rainlab.builder::lang.form.property_taglist_mode_string'), + 'array' => Lang::get('rainlab.builder::lang.form.property_taglist_mode_array'), + 'relation' => Lang::get('rainlab.builder::lang.form.property_taglist_mode_relation') + ], + 'default' => 'string', + 'sortOrder' => 83 + ], + 'separator' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_separator'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'dropdown', + 'options' => [ + 'comma' => Lang::get('rainlab.builder::lang.form.property_taglist_separator_comma'), + 'space' => Lang::get('rainlab.builder::lang.form.property_taglist_separator_space') + ], + 'default' => 'comma', + 'sortOrder' => 84 + ], + 'customTags' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_custom_tags'), + 'description' => Lang::get('rainlab.builder::lang.form.property_taglist_custom_tags_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'checkbox', + 'default' => true, + 'sortOrder' => 86 + ], + 'options' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_options'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'stringList', + 'ignoreIfEmpty' => true, + 'sortOrder' => 85 + ], + 'optionsMethod' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_options_method'), + 'description' => Lang::get('rainlab.builder::lang.form.property_options_method_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 86 + ], + 'nameFrom' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_name_from'), + 'description' => Lang::get('rainlab.builder::lang.form.property_taglist_name_from_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 87 + ], + 'useKey' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_taglist_use_key'), + 'description' => Lang::get('rainlab.builder::lang.form.property_taglist_use_key_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_taglist'), + 'type' => 'checkbox', + 'default' => false, + 'ignoreIfEmpty' => true, + 'sortOrder' => 88 + ] + ]; + + $this->controlLibrary->registerControl( + 'taglist', + 'rainlab.builder::lang.form.control_taglist', + 'rainlab.builder::lang.form.control_taglist_description', + ControlLibrary::GROUP_WIDGETS, + 'icon-tags', + $this->controlLibrary->getStandardProperties($excludeProperties, $properties), + null + ); + } + + /** + * getFieldThumbOptionsProperties + */ + protected function getFieldThumbOptionsProperties(): array + { + return [ + 'thumbOptions' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_options'), + 'description' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_options_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_fileupload'), + 'type' => 'object', + 'properties' => [ + [ + 'property' => 'mode', + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_mode'), + 'type' => 'dropdown', + 'default' => 'crop', + 'options' => [ + 'auto' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_auto'), + 'exact' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_exact'), + 'portrait' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_portrait'), + 'landscape' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_landscape'), + 'crop' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_crop') + ] + ], + [ + 'property' => 'extension', + 'title' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_extension'), + 'type' => 'dropdown', + 'default' => 'auto', + 'options' => [ + 'auto' => Lang::get('rainlab.builder::lang.form.property_fileupload_thumb_auto'), + 'jpg' => 'jpg', + 'gif' => 'gif', + 'png' => 'png' + ] + ] + ], + 'sortOrder' => 88 + ] + ]; + } + + /** + * getFieldMaxItemsProperties + */ + protected function getFieldMaxItemsProperties(): array + { + return [ + 'minItems' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_min_items'), + 'description' => Lang::get('rainlab.builder::lang.form.property_min_items_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 83, + 'validation' => [ + 'integer' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_min_items_integer'), + 'allowNegative' => false, + ] + ], + ], + 'maxItems' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_max_items'), + 'description' => Lang::get('rainlab.builder::lang.form.property_max_items_description'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 84, + 'validation' => [ + 'integer' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_max_items_integer'), + 'allowNegative' => false, + ] + ], + ], + ]; + } + + /** + * getFieldConditionsProperties + */ + protected function getFieldConditionsProperties(): array + { + return [ + 'scope' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_relation_scope'), + 'description' => Lang::get('rainlab.builder::lang.form.property_relation_scope_description'), + 'group' => Lang::get('rainlab.builder::lang.form.property_group_relation'), + 'type' => 'string', + 'ignoreIfEmpty' => true, + 'sortOrder' => 85 + ] + ]; + } + + /** + * getFieldSizeProperties + */ + protected function getFieldSizeProperties(): array + { + return [ + 'size' => [ + 'title' => Lang::get('rainlab.builder::lang.form.property_attributes_size'), + 'type' => 'dropdown', + 'options' => [ + 'tiny' => Lang::get('rainlab.builder::lang.form.property_attributes_size_tiny'), + 'small' => Lang::get('rainlab.builder::lang.form.property_attributes_size_small'), + 'large' => Lang::get('rainlab.builder::lang.form.property_attributes_size_large'), + 'huge' => Lang::get('rainlab.builder::lang.form.property_attributes_size_huge'), + 'giant' => Lang::get('rainlab.builder::lang.form.property_attributes_size_giant') + ], + 'sortOrder' => 51 + ] + ]; + } +} diff --git a/plugins/rainlab/builder/components/RecordDetails.php b/plugins/rainlab/builder/components/RecordDetails.php new file mode 100644 index 0000000..c2ddb68 --- /dev/null +++ b/plugins/rainlab/builder/components/RecordDetails.php @@ -0,0 +1,161 @@ + 'rainlab.builder::lang.components.details_title', + 'description' => 'rainlab.builder::lang.components.details_description' + ]; + } + + // + // Properties + // + + public function defineProperties() + { + return [ + 'modelClass' => [ + 'title' => 'rainlab.builder::lang.components.details_model', + 'type' => 'dropdown', + 'showExternalParam' => false + ], + 'identifierValue' => [ + 'title' => 'rainlab.builder::lang.components.details_identifier_value', + 'description' => 'rainlab.builder::lang.components.details_identifier_value_description', + 'type' => 'string', + 'default' => '{{ :id }}', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.components.details_identifier_value_required') + ] + ] + ], + 'modelKeyColumn' => [ + 'title' => 'rainlab.builder::lang.components.details_key_column', + 'description' => 'rainlab.builder::lang.components.details_key_column_description', + 'type' => 'autocomplete', + 'default' => 'id', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.components.details_key_column_required') + ] + ], + 'showExternalParam' => false + ], + 'displayColumn' => [ + 'title' => 'rainlab.builder::lang.components.details_display_column', + 'description' => 'rainlab.builder::lang.components.details_display_column_description', + 'type' => 'autocomplete', + 'depends' => ['modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.components.details_display_column_required') + ] + ], + 'showExternalParam' => false + ], + 'notFoundMessage' => [ + 'title' => 'rainlab.builder::lang.components.details_not_found_message', + 'description' => 'rainlab.builder::lang.components.details_not_found_message_description', + 'default' => Lang::get('rainlab.builder::lang.components.details_not_found_message_default'), + 'type' => 'string', + 'showExternalParam' => false + ] + ]; + } + + public function getModelClassOptions() + { + return ComponentHelper::instance()->listGlobalModels(); + } + + public function getDisplayColumnOptions() + { + return ComponentHelper::instance()->listModelColumnNames(); + } + + public function getModelKeyColumnOptions() + { + return ComponentHelper::instance()->listModelColumnNames(); + } + + // + // Rendering and processing + // + + public function onRun() + { + $this->prepareVars(); + + $this->record = $this->page['record'] = $this->loadRecord(); + } + + protected function prepareVars() + { + $this->notFoundMessage = $this->page['notFoundMessage'] = Lang::get($this->property('notFoundMessage')); + $this->displayColumn = $this->page['displayColumn'] = $this->property('displayColumn'); + $this->modelKeyColumn = $this->page['modelKeyColumn'] = $this->property('modelKeyColumn'); + $this->identifierValue = $this->page['identifierValue'] = $this->property('identifierValue'); + + if (!strlen($this->displayColumn)) { + throw new SystemException('The display column name is not set.'); + } + + if (!strlen($this->modelKeyColumn)) { + throw new SystemException('The model key column name is not set.'); + } + } + + protected function loadRecord() + { + if (!strlen($this->identifierValue)) { + return; + } + + $modelClassName = $this->property('modelClass'); + if (!strlen($modelClassName) || !class_exists($modelClassName)) { + throw new SystemException('Invalid model class name'); + } + + $model = new $modelClassName(); + return $model->where($this->modelKeyColumn, '=', $this->identifierValue)->first(); + } +} diff --git a/plugins/rainlab/builder/components/RecordList.php b/plugins/rainlab/builder/components/RecordList.php new file mode 100644 index 0000000..e57c0a3 --- /dev/null +++ b/plugins/rainlab/builder/components/RecordList.php @@ -0,0 +1,343 @@ + 'rainlab.builder::lang.components.list_title', + 'description' => 'rainlab.builder::lang.components.list_description' + ]; + } + + // + // Properties + // + + public function defineProperties() + { + return [ + 'modelClass' => [ + 'title' => 'rainlab.builder::lang.components.list_model', + 'type' => 'dropdown', + 'showExternalParam' => false + ], + 'scope' => [ + 'title' => 'rainlab.builder::lang.components.list_scope', + 'description' => 'rainlab.builder::lang.components.list_scope_description', + 'type' => 'dropdown', + 'depends' => ['modelClass'], + 'showExternalParam' => false + ], + 'scopeValue' => [ + 'title' => 'rainlab.builder::lang.components.list_scope_value', + 'description' => 'rainlab.builder::lang.components.list_scope_value_description', + 'type' => 'string', + 'default' => '{{ :scope }}', + ], + 'displayColumn' => [ + 'title' => 'rainlab.builder::lang.components.list_display_column', + 'description' => 'rainlab.builder::lang.components.list_display_column_description', + 'type' => 'autocomplete', + 'depends' => ['modelClass'], + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.components.list_display_column_required') + ] + ] + ], + 'noRecordsMessage' => [ + 'title' => 'rainlab.builder::lang.components.list_no_records', + 'description' => 'rainlab.builder::lang.components.list_no_records_description', + 'type' => 'string', + 'default' => Lang::get('rainlab.builder::lang.components.list_no_records_default'), + 'showExternalParam' => false, + ], + 'detailsPage' => [ + 'title' => 'rainlab.builder::lang.components.list_details_page', + 'description' => 'rainlab.builder::lang.components.list_details_page_description', + 'type' => 'dropdown', + 'showExternalParam' => false, + 'group' => 'rainlab.builder::lang.components.list_details_page_link' + ], + 'detailsKeyColumn' => [ + 'title' => 'rainlab.builder::lang.components.list_details_key_column', + 'description' => 'rainlab.builder::lang.components.list_details_key_column_description', + 'type' => 'autocomplete', + 'depends' => ['modelClass'], + 'showExternalParam' => false, + 'group' => 'rainlab.builder::lang.components.list_details_page_link' + ], + 'detailsUrlParameter' => [ + 'title' => 'rainlab.builder::lang.components.list_details_url_parameter', + 'description' => 'rainlab.builder::lang.components.list_details_url_parameter_description', + 'type' => 'string', + 'default' => 'id', + 'showExternalParam' => false, + 'group' => 'rainlab.builder::lang.components.list_details_page_link' + ], + 'recordsPerPage' => [ + 'title' => 'rainlab.builder::lang.components.list_records_per_page', + 'description' => 'rainlab.builder::lang.components.list_records_per_page_description', + 'type' => 'string', + 'validationPattern' => '^[0-9]*$', + 'validationMessage' => 'rainlab.builder::lang.components.list_records_per_page_validation', + 'group' => 'rainlab.builder::lang.components.list_pagination' + ], + 'pageNumber' => [ + 'title' => 'rainlab.builder::lang.components.list_page_number', + 'description' => 'rainlab.builder::lang.components.list_page_number_description', + 'type' => 'string', + 'default' => '{{ :page }}', + 'group' => 'rainlab.builder::lang.components.list_pagination' + ], + 'sortColumn' => [ + 'title' => 'rainlab.builder::lang.components.list_sort_column', + 'description' => 'rainlab.builder::lang.components.list_sort_column_description', + 'type' => 'autocomplete', + 'depends' => ['modelClass'], + 'group' => 'rainlab.builder::lang.components.list_sorting', + 'showExternalParam' => false + ], + 'sortDirection' => [ + 'title' => 'rainlab.builder::lang.components.list_sort_direction', + 'type' => 'dropdown', + 'showExternalParam' => false, + 'group' => 'rainlab.builder::lang.components.list_sorting', + 'options' => [ + 'asc' => 'rainlab.builder::lang.components.list_order_direction_asc', + 'desc' => 'rainlab.builder::lang.components.list_order_direction_desc' + ] + ] + ]; + } + + public function getDetailsPageOptions() + { + $pages = Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); + + $pages = [ + '-' => Lang::get('rainlab.builder::lang.components.list_details_page_no') + ] + $pages; + + return $pages; + } + + public function getModelClassOptions() + { + return ComponentHelper::instance()->listGlobalModels(); + } + + public function getDisplayColumnOptions() + { + return ComponentHelper::instance()->listModelColumnNames(); + } + + public function getDetailsKeyColumnOptions() + { + return ComponentHelper::instance()->listModelColumnNames(); + } + + public function getSortColumnOptions() + { + return ComponentHelper::instance()->listModelColumnNames(); + } + + public function getScopeOptions() + { + $modelClass = ComponentHelper::instance()->getModelClassDesignTime(); + + $result = [ + '-' => Lang::get('rainlab.builder::lang.components.list_scope_default') + ]; + try { + $model = new $modelClass; + $methods = $model->getClassMethods(); + + foreach ($methods as $method) { + if (preg_match('/scope[A-Z].*/', $method)) { + $result[$method] = $method; + } + } + } + catch (Exception $ex) { + // Ignore invalid models + } + + return $result; + } + + // + // Rendering and processing + // + + public function onRun() + { + $this->prepareVars(); + + $this->records = $this->page['records'] = $this->listRecords(); + } + + protected function prepareVars() + { + $this->noRecordsMessage = $this->page['noRecordsMessage'] = Lang::get($this->property('noRecordsMessage')); + $this->displayColumn = $this->page['displayColumn'] = $this->property('displayColumn'); + $this->pageParam = $this->page['pageParam'] = $this->paramName('pageNumber'); + + $this->detailsKeyColumn = $this->page['detailsKeyColumn'] = $this->property('detailsKeyColumn'); + $this->detailsUrlParameter = $this->page['detailsUrlParameter'] = $this->property('detailsUrlParameter'); + + $detailsPage = $this->property('detailsPage'); + if ($detailsPage == '-') { + $detailsPage = null; + } + + $this->detailsPage = $this->page['detailsPage'] = $detailsPage; + + if (!strlen($this->displayColumn)) { + throw new SystemException('The display column name is not set.'); + } + + if (strlen($this->detailsPage)) { + if (!strlen($this->detailsKeyColumn)) { + throw new SystemException('The details key column should be set to generate links to the details page.'); + } + + if (!strlen($this->detailsUrlParameter)) { + throw new SystemException('The details page URL parameter name should be set to generate links to the details page.'); + } + } + } + + protected function listRecords() + { + $modelClassName = $this->property('modelClass'); + if (!strlen($modelClassName) || !class_exists($modelClassName)) { + throw new SystemException('Invalid model class name'); + } + + $model = new $modelClassName(); + $scope = $this->getScopeName($model); + $scopeValue = $this->property('scopeValue'); + + if ($scope !== null) { + $model = $model->$scope($scopeValue); + } + + $model = $this->sort($model); + $records = $this->paginate($model); + + return $records; + } + + protected function getScopeName($model) + { + $scopeMethod = trim($this->property('scope')); + if (!strlen($scopeMethod) || $scopeMethod == '-') { + return null; + } + + if (!preg_match('/scope[A-Z].+/', $scopeMethod)) { + throw new SystemException('Invalid scope method name.'); + } + + if (!$model->methodExists($scopeMethod)) { + throw new SystemException('Scope method not found.'); + } + + return lcfirst(substr($scopeMethod, 5)); + } + + protected function paginate($model) + { + $recordsPerPage = trim($this->property('recordsPerPage')); + if (!strlen($recordsPerPage)) { + // Pagination is disabled - return all records + return $model->get(); + } + + if (!preg_match('/^[0-9]+$/', $recordsPerPage)) { + throw new SystemException('Invalid records per page value.'); + } + + $pageNumber = trim($this->property('pageNumber')); + if (!strlen($pageNumber) || !preg_match('/^[0-9]+$/', $pageNumber)) { + $pageNumber = 1; + } + + return $model->paginate($recordsPerPage, $pageNumber); + } + + protected function sort($model) + { + $sortColumn = trim($this->property('sortColumn')); + if (!strlen($sortColumn)) { + return $model; + } + + $sortDirection = trim($this->property('sortDirection')); + + if ($sortDirection !== 'desc') { + $sortDirection = 'asc'; + } + + // Note - no further validation of the sort column + // value is performed here, relying to the ORM sanitizing. + return $model->orderBy($sortColumn, $sortDirection); + } +} diff --git a/plugins/rainlab/builder/components/recorddetails/default.htm b/plugins/rainlab/builder/components/recorddetails/default.htm new file mode 100644 index 0000000..3d56a45 --- /dev/null +++ b/plugins/rainlab/builder/components/recorddetails/default.htm @@ -0,0 +1,9 @@ +{% set record = __SELF__.record %} +{% set displayColumn = __SELF__.displayColumn %} +{% set notFoundMessage = __SELF__.notFoundMessage %} + +{% if record %} + {{ attribute(record, displayColumn) }} +{% else %} + {{ notFoundMessage }} +{% endif %} \ No newline at end of file diff --git a/plugins/rainlab/builder/components/recordlist/default.htm b/plugins/rainlab/builder/components/recordlist/default.htm new file mode 100644 index 0000000..4e01590 --- /dev/null +++ b/plugins/rainlab/builder/components/recordlist/default.htm @@ -0,0 +1,36 @@ +{% set records = __SELF__.records %} +{% set displayColumn = __SELF__.displayColumn %} +{% set noRecordsMessage = __SELF__.noRecordsMessage %} +{% set detailsPage = __SELF__.detailsPage %} +{% set detailsKeyColumn = __SELF__.detailsKeyColumn %} +{% set detailsUrlParameter = __SELF__.detailsUrlParameter %} + + + +{% if records.lastPage > 1 %} +
      + {% if records.currentPage > 1 %} +
    • ← Prev
    • + {% endif %} + + {% for page in 1..records.lastPage %} +
    • + {{ page }} +
    • + {% endfor %} + + {% if records.lastPage > records.currentPage %} +
    • Next →
    • + {% endif %} +
    +{% endif %} \ No newline at end of file diff --git a/plugins/rainlab/builder/composer.json b/plugins/rainlab/builder/composer.json new file mode 100644 index 0000000..5a39a0b --- /dev/null +++ b/plugins/rainlab/builder/composer.json @@ -0,0 +1,29 @@ +{ + "name": "rainlab/builder-plugin", + "type": "october-plugin", + "description": "Builder plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-builder", + "keywords": ["october", "octobercms", "builder"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": "^8.0.2", + "october/rain": ">=3.0", + "composer/installers": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/builder/controllers/Index.php b/plugins/rainlab/builder/controllers/Index.php new file mode 100644 index 0000000..c7b5e51 --- /dev/null +++ b/plugins/rainlab/builder/controllers/Index.php @@ -0,0 +1,153 @@ +bodyClass = 'compact-container sidenav-responsive'; + $this->pageTitle = "Builder"; + } + + /** + * beforeDisplay + */ + public function beforeDisplay() + { + new PluginList($this, 'pluginList'); + new DatabaseTableList($this, 'databaseTableList'); + new ModelList($this, 'modelList'); + new VersionList($this, 'versionList'); + new LanguageList($this, 'languageList'); + new ControllerList($this, 'controllerList'); + new CodeList($this, 'codeList'); + + $this->bindFormWidgetToController(); + } + + /** + * bindFormWidgetToController + */ + protected function bindFormWidgetToController() + { + if (!Request::ajax() || !post('operationClass') || !post('formWidgetAlias')) { + return; + } + + $extension = $this->asExtension(post('operationClass')); + if (!$extension) { + return; + } + + $extension->bindFormWidgetToController(post('formWidgetAlias')); + } + + /** + * index + */ + public function index() + { + $this->addCss('/plugins/rainlab/builder/assets/css/builder.css', 'RainLab.Builder'); + + // The table widget scripts should be preloaded + $this->addJs('/modules/backend/widgets/table/assets/js/build-min.js', 'core'); + $this->addJs('/plugins/rainlab/builder/assets/js/build-min.js', 'RainLab.Builder'); + + $this->pageTitleTemplate = '%s Builder'; + } + + /** + * setBuilderActivePlugin + */ + public function setBuilderActivePlugin($pluginCode, $refreshPluginList = false) + { + $this->widget->pluginList->setActivePlugin($pluginCode); + + $result = []; + if ($refreshPluginList) { + $result = $this->widget->pluginList->updateList(); + } + + $result = array_merge( + $result, + $this->widget->databaseTableList->refreshActivePlugin(), + $this->widget->modelList->refreshActivePlugin(), + $this->widget->versionList->refreshActivePlugin(), + $this->widget->languageList->refreshActivePlugin(), + $this->widget->controllerList->refreshActivePlugin(), + $this->widget->codeList->refreshActivePlugin() + ); + + return $result; + } + + /** + * getBuilderActivePluginVector + */ + public function getBuilderActivePluginVector() + { + return $this->widget->pluginList->getActivePluginVector(); + } + + /** + * updatePluginList + */ + public function updatePluginList() + { + return $this->widget->pluginList->updateList(); + } +} diff --git a/plugins/rainlab/builder/controllers/index/_plugin-selector.php b/plugins/rainlab/builder/controllers/index/_plugin-selector.php new file mode 100644 index 0000000..a3fdda0 --- /dev/null +++ b/plugins/rainlab/builder/controllers/index/_plugin-selector.php @@ -0,0 +1,9 @@ +
    +
    +
    +
    + widget->pluginList->render() ?> +
    +
    +
    +
    diff --git a/plugins/rainlab/builder/controllers/index/_sidepanel.php b/plugins/rainlab/builder/controllers/index/_sidepanel.php new file mode 100644 index 0000000..8cacf37 --- /dev/null +++ b/plugins/rainlab/builder/controllers/index/_sidepanel.php @@ -0,0 +1,65 @@ +
    +
    +
    + +
    + widget->databaseTableList->render() ?> +
    + + +
    + widget->modelList->render() ?> +
    + + +
    + widget->controllerList->render() ?> +
    + + +
    + widget->versionList->render() ?> +
    + + +
    + widget->languageList->render() ?> +
    + + +
    + widget->codeList->render() ?> +
    +
    +
    +
    diff --git a/plugins/rainlab/builder/controllers/index/index.php b/plugins/rainlab/builder/controllers/index/index.php new file mode 100644 index 0000000..f1e3bd7 --- /dev/null +++ b/plugins/rainlab/builder/controllers/index/index.php @@ -0,0 +1,38 @@ + + + + fatalError): ?> + makePartial('sidepanel') ?> + + + + + makePartial('plugin-selector') ?> + + + + fatalError): ?> +
    + +
    +
    + +
    +
    +
    +
    + +
    + + +

    fatalError)) ?>

    + + \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/BlueprintBuilder.php b/plugins/rainlab/builder/formwidgets/BlueprintBuilder.php new file mode 100644 index 0000000..2850e4c --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/BlueprintBuilder.php @@ -0,0 +1,320 @@ +getSelectFormWidget(); + } + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareVars(); + return $this->makePartial('body'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['model'] = $this->model; + $this->vars['items'] = $this->model->blueprints; + $this->vars['selectWidget'] = $this->getSelectFormWidget(); + $this->vars['pluginCode'] = $this->getPluginCode(); + + $this->vars['emptyItem'] = [ + 'label' => __("Add Blueprint"), + 'icon' => 'icon-life-ring', + 'code' => 'newitemcode', + 'url' => '/' + ]; + } + + /** + * onRefreshBlueprintContainer + */ + public function onRefreshBlueprintContainer() + { + $uuid = post('blueprint_uuid'); + $blueprintInfo = $this->getBlueprintInfo($uuid); + $blueprintConfig = (array) post('properties'); + + return [ + 'markup' => $this->renderBlueprintBody($blueprintInfo, $blueprintConfig), + 'blueprintUuid' => $uuid + ]; + } + + /** + * onShowSelectBlueprintForm + */ + public function onShowSelectBlueprintForm() + { + $this->prepareVars(); + + $selectedBlueprints = (array) post('blueprints') ?: []; + if ($selectedBlueprints) { + $model = $this->getSelectFormWidget()->getModel(); + $model->blueprints = $selectedBlueprints; + } + + return $this->makePartial('select_blueprint_form'); + } + + /** + * onSelectBlueprint + */ + public function onSelectBlueprint() + { + $widget = $this->getSelectFormWidget(); + $data = $widget->getSaveData(); + $uuids = (array) ($data['blueprint_uuid'] ?? []); + if (!$uuids) { + throw new ApplicationException(__("There are no blueprints to import, please select a blueprint and try again.")); + } + + $result = []; + $availableUuids = $this->getSelectFormWidget()->getModel()->getBlueprintUuidOptions(); + foreach ($uuids as $uuid) { + $blueprintInfo = $this->getBlueprintInfo($uuid); + $blueprintConfig = $this->generateBlueprintConfiguration($blueprintInfo); + + $result[] = $this->makePartial('blueprint', [ + 'blueprintUuid' => $uuid, + 'blueprintConfig' => $blueprintConfig + ]); + + unset($availableUuids[$uuid]); + } + + $includeRelated = (bool) ($data['include_related'] ?? false); + if ($includeRelated) { + foreach ($uuids as $uuid) { + $this->appendRelatedBlueprintsToOutput($uuid, $result, $availableUuids); + } + } + + return ['@#blueprintList' => implode(PHP_EOL, $result)]; + } + + /** + * appendRelatedBlueprintsToOutput + */ + protected function appendRelatedBlueprintsToOutput($parentUuid, &$result, &$available) + { + $library = TailorBlueprintLibrary::instance(); + $relatedUuids = $library->getRelatedBlueprintUuids($parentUuid); + + foreach ($relatedUuids as $uuid) { + if (!isset($available[$uuid])) { + continue; + } + + $blueprintInfo = $this->getBlueprintInfo($uuid); + $blueprintConfig = $this->generateBlueprintConfiguration($blueprintInfo); + + $result[] = $this->makePartial('blueprint', [ + 'blueprintUuid' => $uuid, + 'blueprintConfig' => $blueprintConfig + ]); + + unset($available[$uuid]); + + // Recursion + $this->appendRelatedBlueprintsToOutput($uuid, $result, $available); + } + } + + /** + * {@inheritDoc} + */ + public function loadAssets() + { + $this->addJs('js/blueprintbuilder.js', 'builder'); + } + + /** + * getPluginCode + */ + public function getPluginCode() + { + $pluginCode = post('plugin_code'); + if (strlen($pluginCode)) { + return $pluginCode; + } + + $pluginVector = $this->controller->getBuilderActivePluginVector(); + + return $pluginVector->pluginCodeObj->toCode(); + } + + + /** + * getSelectFormWidget + */ + protected function getSelectFormWidget() + { + if ($this->selectFormWidget) { + return $this->selectFormWidget; + } + + $config = $this->makeConfig('~/plugins/rainlab/builder/models/importsmodel/fields_select.yaml'); + $config->model = $this->makeImportsModelInstance(); + $config->alias = $this->alias . 'Select'; + $config->arrayName = 'BlueprintBuilder'; + + $form = $this->makeWidget(\Backend\Widgets\Form::class, $config); + $form->bindToController(); + + return $this->selectFormWidget = $form; + } + + /** + * makeImportsModelInstance + */ + protected function makeImportsModelInstance() + { + $model = new ImportsModel; + $model->setPluginCode($this->getPluginCode()); + return $model; + } + + // + // Methods for the internal use + // + + /** + * getBlueprintDesignTimeProvider + */ + protected function getBlueprintDesignTimeProvider($providerClass) + { + if (array_key_exists($providerClass, $this->designTimeProviders)) { + return $this->designTimeProviders[$providerClass]; + } + + return $this->designTimeProviders[$providerClass] = new $providerClass($this->controller); + } + + /** + * getPropertyValue + */ + protected function getPropertyValue($properties, $property) + { + if (array_key_exists($property, $properties)) { + return $properties[$property]; + } + + return null; + } + + /** + * propertiesToInspectorSchema + */ + protected function propertiesToInspectorSchema($propertyConfiguration) + { + $result = []; + + foreach ($propertyConfiguration as $property => $propertyData) { + $propertyData['property'] = $property; + + $result[] = $propertyData; + } + + return $result; + } + + /** + * getBlueprintInfo + */ + protected function getBlueprintInfo($uuid) + { + if (array_key_exists($uuid, $this->blueprintInfoCache)) { + return $this->blueprintInfoCache[$uuid]; + } + + $library = TailorBlueprintLibrary::instance(); + $blueprintInfo = $library->getBlueprintInfo($uuid); + + if (!$blueprintInfo) { + throw new ApplicationException('The requested blueprint class information is not found.'); + } + + return $this->blueprintInfoCache[$uuid] = $blueprintInfo; + } + + /** + * renderBlueprintBody + */ + protected function renderBlueprintBody($blueprintInfo, $blueprintConfig) + { + $blueprintClass = $blueprintInfo['blueprintClass']; + + $blueprintObj = $blueprintInfo['blueprintObj']; + + $provider = $this->getBlueprintDesignTimeProvider($blueprintInfo['designTimeProvider']); + + // Inspect the generated output files + $importsModel = $this->makeImportsModelInstance(); + + $importsModel->fill(post()); + + $importsModel->blueprints[$blueprintObj->uuid] = $blueprintConfig; + + $inspectedOutput = $importsModel->inspect($blueprintObj); + + $blueprintConfig['inspectedOutput'] = $inspectedOutput; + + return $provider->renderBlueprintBody($blueprintClass, $blueprintConfig, $blueprintObj); + } + + /** + * generateBlueprintConfiguration + */ + protected function generateBlueprintConfiguration($blueprintInfo): array + { + $blueprintClass = $blueprintInfo['blueprintClass']; + + $blueprintObj = $blueprintInfo['blueprintObj']; + + $provider = $this->getBlueprintDesignTimeProvider($blueprintInfo['designTimeProvider']); + + $model = $this->makeImportsModelInstance(); + + return $provider->getDefaultConfiguration($blueprintClass, $blueprintObj, $model); + } +} diff --git a/plugins/rainlab/builder/formwidgets/ControllerBuilder.php b/plugins/rainlab/builder/formwidgets/ControllerBuilder.php new file mode 100644 index 0000000..9bd7b9c --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/ControllerBuilder.php @@ -0,0 +1,132 @@ +prepareVars(); + return $this->makePartial('body'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['model'] = $this->model; + } + + /** + * {@inheritDoc} + */ + public function loadAssets() + { + $this->addJs('js/controllerbuilder.js', 'builder'); + } + + /* + * Event handlers + */ + + // + // Methods for the internal use + // + + /** + * getBehaviorDesignTimeProvider + */ + protected function getBehaviorDesignTimeProvider($providerClass) + { + if (array_key_exists($providerClass, $this->designTimeProviders)) { + return $this->designTimeProviders[$providerClass]; + } + + return $this->designTimeProviders[$providerClass] = new $providerClass($this->controller); + } + + /** + * getPropertyValue + */ + protected function getPropertyValue($properties, $property) + { + if (array_key_exists($property, $properties)) { + return $properties[$property]; + } + + return null; + } + + /** + * propertiesToInspectorSchema + */ + protected function propertiesToInspectorSchema($propertyConfiguration) + { + $result = []; + + foreach ($propertyConfiguration as $property => $propertyData) { + $propertyData['property'] = $property; + + $result[] = $propertyData; + } + + return $result; + } + + /** + * getBehaviorInfo + */ + protected function getBehaviorInfo($class) + { + if (array_key_exists($class, $this->behaviorInfoCache)) { + return $this->behaviorInfoCache[$class]; + } + + $library = ControllerBehaviorLibrary::instance(); + $behaviorInfo = $library->getBehaviorInfo($class); + + if (!$behaviorInfo) { + throw new ApplicationException('The requested behavior class information is not found.'); + } + + return $this->behaviorInfoCache[$class] = $behaviorInfo; + } + + /** + * renderBehaviorBody + */ + protected function renderBehaviorBody($behaviorClass, $behaviorInfo, $behaviorConfig) + { + $provider = $this->getBehaviorDesignTimeProvider($behaviorInfo['designTimeProvider']); + + return $provider->renderBehaviorBody($behaviorClass, $behaviorConfig, $this); + } +} diff --git a/plugins/rainlab/builder/formwidgets/FormBuilder.php b/plugins/rainlab/builder/formwidgets/FormBuilder.php new file mode 100644 index 0000000..1bdbfe2 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/FormBuilder.php @@ -0,0 +1,488 @@ +prepareVars(); + return $this->makePartial('body'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['model'] = $this->model; + } + + /** + * {@inheritDoc} + */ + public function loadAssets() + { + $this->addJs('js/formbuilder.js', 'builder'); + $this->addJs('js/formbuilder.domtopropertyjson.js', 'builder'); + $this->addJs('js/formbuilder.tabs.js', 'builder'); + $this->addJs('js/formbuilder.controlpalette.js', 'builder'); + } + + /** + * renderControlList + */ + public function renderControlList($controls, $listName = '') + { + return $this->makePartial('controllist', [ + 'controls' => $controls, + 'listName' => $listName + ]); + } + + /** + * onModelFormRenderControlWrapper + */ + public function onModelFormRenderControlWrapper() + { + $type = Input::get('controlType'); + $controlId = Input::get('controlId'); + $properties = Input::get('properties'); + + $controlInfo = $this->getControlInfo($type); + + return [ + 'markup' => $this->renderControlWrapper($type, $properties), + 'controlId' => $controlId, + 'controlTitle' => Lang::get($controlInfo['name']), + 'description' => Lang::get($controlInfo['description']), + 'type' => $type + ]; + } + + /** + * onModelFormRenderControlBody + */ + public function onModelFormRenderControlBody() + { + $type = Input::get('controlType'); + $controlId = Input::get('controlId'); + $properties = Input::get('properties'); + + return [ + 'markup' => $this->renderControlBody($type, $properties, $this), + 'controlId' => $controlId + ]; + } + + /** + * onModelFormLoadControlPalette + */ + public function onModelFormLoadControlPalette() + { + $controlId = Input::get('controlId'); + + $library = ControlLibrary::instance(); + $controls = $library->listControls(); + $this->vars['registeredControls'] = $controls; + $this->vars['controlGroups'] = array_keys($controls); + + return [ + 'markup' => $this->makePartial('controlpalette'), + 'controlId' => $controlId + ]; + } + + /** + * getPluginCode + */ + public function getPluginCode() + { + $pluginCode = Input::get('plugin_code'); + if (strlen($pluginCode)) { + return $pluginCode; + } + + return $this->model->getPluginCodeObj()->toCode(); + } + + // + // Methods for the internal use + // + + /** + * getControlDesignTimeProvider + */ + protected function getControlDesignTimeProvider($providerClass) + { + if (array_key_exists($providerClass, $this->designTimeProviders)) { + return $this->designTimeProviders[$providerClass]; + } + + return $this->designTimeProviders[$providerClass] = new $providerClass($this->controller); + } + + /** + * getPropertyValue + */ + protected function getPropertyValue($properties, $property) + { + if (array_key_exists($property, $properties)) { + return $properties[$property]; + } + + return null; + } + + /** + * propertiesToInspectorSchema + */ + protected function propertiesToInspectorSchema($propertyConfiguration) + { + $result = []; + + $fieldNameProperty = [ + 'title' => Lang::get('rainlab.builder::lang.form.property_field_name_title'), + 'property' => 'oc.fieldName', + 'type' => 'autocomplete', + 'fillFrom' => 'model-fields', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_field_name_required') + ], + 'regex' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_field_name_regex'), + 'pattern' => '^[a-zA-Z\_]+[0-9a-z\_\[\]]*$' + ] + ] + ]; + + $result[] = $fieldNameProperty; + + foreach ($propertyConfiguration as $property => $propertyData) { + $propertyData['property'] = $property; + + if ($propertyData['type'] === 'control-container') { + // Control container type properties are handled with the form builder UI and + // should not be available in Inspector. + // + continue; + } + + $result[] = $propertyData; + } + + return $result; + } + + /** + * getControlInfo + */ + protected function getControlInfo($type) + { + if (array_key_exists($type, $this->controlInfoCache)) { + return $this->controlInfoCache[$type]; + } + + $library = ControlLibrary::instance(); + $controlInfo = $library->getControlInfo($type); + + if (!$controlInfo) { + throw new ApplicationException('The requested control type is not found.'); + } + + return $this->controlInfoCache[$type] = $controlInfo; + } + + /** + * renderControlBody + */ + protected function renderControlBody($type, $properties) + { + $controlInfo = $this->getControlInfo($type); + $provider = $this->getControlDesignTimeProvider($controlInfo['designTimeProvider']); + + return $this->makePartial('controlbody', [ + 'hasLabels' => $provider->controlHasLabels($type), + 'body' => $provider->renderControlBody($type, $properties, $this), + 'properties' => $properties + ]); + } + + /** + * renderControlStaticBody + */ + protected function renderControlStaticBody($type, $properties, $controlConfiguration) + { + // The control body footer is never updated with AJAX and currently + // used only by the Repeater widget to display its controls. + + $controlInfo = $this->getControlInfo($type); + $provider = $this->getControlDesignTimeProvider($controlInfo['designTimeProvider']); + + return $provider->renderControlStaticBody($type, $properties, $controlConfiguration, $this); + } + + /** + * renderControlWrapper + */ + protected function renderControlWrapper($type, $properties = [], $controlConfiguration = []) + { + // This method renders the entire control, including + // the wrapping element. + + $controlInfo = $this->getControlInfo($type); + + // Builder UI displays Comment and Comment Above properties + // as Comment and Comment Position properties. + + if (array_key_exists('comment', $properties) && strlen($properties['comment'])) { + $properties['oc.comment'] = $properties['comment']; + $properties['oc.commentPosition'] = 'below'; + } + + if (array_key_exists('commentAbove', $properties) && strlen($properties['commentAbove'])) { + $properties['oc.comment'] = $properties['commentAbove']; + $properties['oc.commentPosition'] = 'above'; + } + + // Data table columns (TODO: move to design time provider? -sg 2023) + if ($type === 'datatable' && isset($properties['columns']) && is_array($properties['columns'])) { + $ocColumns = []; + foreach ($properties['columns'] as $key => $config) { + $ocColumns[] = ['code' => $key] + $config; + } + $properties['oc.columns'] = $ocColumns; + } + + $provider = $this->getControlDesignTimeProvider($controlInfo['designTimeProvider']); + return $this->makePartial('controlwrapper', [ + 'fieldsConfiguration' => $this->propertiesToInspectorSchema($controlInfo['properties']), + 'controlConfiguration' => $controlConfiguration, + 'type' => $type, + 'properties' => $properties + ]); + } + + /** + * getSpan + */ + protected function getSpan($currentSpan, $prevSpan, $isPlaceholder = false) + { + if ($currentSpan == 'auto' || !strlen($currentSpan)) { + if ($prevSpan == 'left') { + return 'right'; + } + else { + return $isPlaceholder ? 'full' : 'left'; + } + } + + return $currentSpan; + } + + /** + * preprocessPropertyValues + */ + protected function preprocessPropertyValues($controlName, $properties, $controlInfo) + { + $properties['oc.fieldName'] = $controlName; + + // Remove the control container type property values. + // + if (isset($controlInfo['properties'])) { + foreach ($controlInfo['properties'] as $property => $propertyConfig) { + if (isset($propertyConfig['type']) && $propertyConfig['type'] === 'control-container' && isset($properties[$property])) { + unset($properties[$property]); + } + } + } + + return $properties; + } + + /** + * getControlRenderingInfo + */ + protected function getControlRenderingInfo($controlName, $properties, $prevProperties) + { + $type = isset($properties['type']) ? $properties['type'] : 'text'; + $spanFixed = isset($properties['span']) ? $properties['span'] : 'auto'; + $prevSpan = isset($prevProperties['span']) ? $prevProperties['span'] : 'auto'; + + $span = $this->getSpan($spanFixed, $prevSpan); + $spanClass = 'span-'.$span; + + $controlInfo = $this->getControlInfo($type); + + $properties = $this->preprocessPropertyValues($controlName, $properties, $controlInfo); + + return [ + 'title' => Lang::get($controlInfo['name']), + 'description' => Lang::get($controlInfo['description']), + 'type' => $type, + 'span' => $span, + 'spanFixed' => $spanFixed, + 'spanClass' => $spanClass, + 'properties' => $properties, + 'unknownControl' => isset($controlInfo['unknownControl']) && $controlInfo['unknownControl'] + ]; + } + + /** + * getTabConfigurationSchema + */ + protected function getTabConfigurationSchema() + { + if ($this->tabConfigurationSchema !== null) { + return $this->tabConfigurationSchema; + } + + $result = [ + [ + 'title' => Lang::get('rainlab.builder::lang.form.tab_title'), + 'property' => 'title', + 'type' => 'builderLocalization', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.form.property_tab_title_required') + ] + ] + ] + ]; + + return $this->tabConfigurationSchema = json_encode($result); + } + + /** + * getTabsConfigurationSchema + */ + protected function getTabsConfigurationSchema() + { + if ($this->tabsConfigurationSchema !== null) { + return $this->tabsConfigurationSchema; + } + + $result = [ + [ + 'title' => Lang::get('rainlab.builder::lang.form.tab_stretch'), + 'description' => Lang::get('rainlab.builder::lang.form.tab_stretch_description'), + 'property' => 'stretch', + 'type' => 'checkbox' + ], + [ + 'title' => Lang::get('rainlab.builder::lang.form.tab_css_class'), + 'description' => Lang::get('rainlab.builder::lang.form.tab_css_class_description'), + 'property' => 'cssClass', + 'type' => 'string' + ] + ]; + + return $this->tabsConfigurationSchema = json_encode($result); + } + + /** + * getTabConfigurationValues + */ + protected function getTabConfigurationValues($values) + { + if (!count($values)) { + return '{}'; + } + + return json_encode($values); + } + + /** + * getTabsConfigurationValues + */ + protected function getTabsConfigurationValues($values) + { + if (!count($values)) { + return '{}'; + } + + return json_encode($values); + } + + /** + * getTabsFields + */ + protected function getTabsFields($tabsName, $fields) + { + $result = []; + + if (!is_array($fields)) { + return $result; + } + + if (!array_key_exists($tabsName, $fields) || !array_key_exists('fields', $fields[$tabsName])) { + return $result; + } + + $defaultTab = Lang::get('backend::lang.form.undefined_tab'); + if (array_key_exists('defaultTab', $fields[$tabsName])) { + $defaultTab = Lang::get($fields[$tabsName]['defaultTab']); + } + + foreach ($fields[$tabsName]['fields'] as $fieldName => $fieldConfiguration) { + if (!isset($fieldConfiguration['tab'])) { + $fieldConfiguration['tab'] = $defaultTab; + } + + $tab = $fieldConfiguration['tab']; + if (!array_key_exists($tab, $result)) { + $result[$tab] = []; + } + + $result[$tab][$fieldName] = $fieldConfiguration; + } + + return $result; + } +} diff --git a/plugins/rainlab/builder/formwidgets/MenuEditor.php b/plugins/rainlab/builder/formwidgets/MenuEditor.php new file mode 100644 index 0000000..923e46f --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/MenuEditor.php @@ -0,0 +1,241 @@ +prepareVars(); + return $this->makePartial('body'); + } + + /** + * Prepares the list data + */ + public function prepareVars() + { + $this->vars['model'] = $this->model; + $this->vars['items'] = $this->model->menus; + + $this->vars['emptyItem'] = [ + 'label' => Lang::get('rainlab.builder::lang.menu.new_menu_item'), + 'icon' => 'icon-life-ring', + 'code' => 'newitemcode', + 'url' => '/' + ]; + + $this->vars['emptySubItem'] = [ + 'label' => Lang::get('rainlab.builder::lang.menu.new_menu_item'), + 'icon' => 'icon-sitemap', + 'code' => 'newitemcode', + 'url' => '/' + ]; + } + + /** + * {@inheritDoc} + */ + public function loadAssets() + { + $this->addJs('js/menubuilder.js', 'builder'); + } + + public function getPluginCode() + { + $pluginCode = Input::get('plugin_code'); + if (strlen($pluginCode)) { + return $pluginCode; + } + + $pluginVector = $this->controller->getBuilderActivePluginVector(); + + return $pluginVector->pluginCodeObj->toCode(); + } + + // + // Event handlers + // + + // + // Methods for the internal use + // + + protected function getItemArrayProperty($item, $property) + { + if (array_key_exists($property, $item)) { + return $item[$property]; + } + + return null; + } + + protected function getIconList() + { + if ($this->iconList !== null) { + return $this->iconList; + } + + $icons = IconList::getList(); + $this->iconList = []; + + foreach ($icons as $iconCode => $iconInfo) { + $iconCode = preg_replace('/^oc\-/', '', $iconCode); + + $this->iconList[$iconCode] = $iconInfo; + } + + return $this->iconList; + } + + protected function getCommonMenuItemConfigurationSchema() + { + $result = [ + [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_code'), + 'property' => 'code', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.menu.property_code_required') + ] + ] + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_label'), + 'type' => 'builderLocalization', + 'property' => 'label', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.menu.property_label_required') + ] + ] + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_url'), + 'property' => 'url', + 'type' => 'autocomplete', + 'fillFrom' => 'controller-urls', + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.menu.property_url_required') + ] + ] + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_icon'), + 'property' => 'icon', + 'type' => 'dropdown', + 'options' => $this->getIconList(), + 'validation' => [ + 'required' => [ + 'message' => Lang::get('rainlab.builder::lang.menu.property_icon_required') + ] + ], + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.icon_svg'), + 'description' => Lang::get('rainlab.builder::lang.menu.icon_svg_description'), + 'property' => 'iconSvg', + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_permissions'), + 'property' => 'permissions', + 'type' => 'stringListAutocomplete', + 'fillFrom' => 'permissions' + ], + [ + 'title' => Lang::get('rainlab.builder::lang.menu.counter'), + 'description' => Lang::get('rainlab.builder::lang.menu.counter_description'), + 'property' => 'counter', + 'group' => Lang::get('rainlab.builder::lang.menu.counter_group'), + + ], + // Removed in OCv2 + // [ + // 'title' => Lang::get('rainlab.builder::lang.menu.counter_label'), + // 'description' => Lang::get('rainlab.builder::lang.menu.counter_label_description'), + // 'property' => 'counterLabel', + // 'group' => Lang::get('rainlab.builder::lang.menu.counter_group'), + // ], + ]; + + return $result; + } + + protected function getSideMenuConfigurationSchema() + { + $result = $this->getCommonMenuItemConfigurationSchema(); + + $result[] = [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_attributes'), + 'property' => 'attributes', + 'type' => 'stringList' + ]; + + return json_encode($result); + } + + protected function getSideMenuConfiguration($item) + { + if (!count($item)) { + return '{}'; + } + + return json_encode($item); + } + + + protected function getMainMenuConfigurationSchema() + { + $result = $this->getCommonMenuItemConfigurationSchema(); + + $result[] = [ + 'title' => Lang::get('rainlab.builder::lang.menu.property_order'), + 'description' => Lang::get('rainlab.builder::lang.menu.property_order_description'), + 'property' => 'order', + 'validation' => [ + 'regex' => [ + 'pattern' => '^[0-9]+$', + 'message' => Lang::get('rainlab.builder::lang.menu.property_order_invalid') + ] + ] + ]; + + return json_encode($result); + } + + protected function getMainMenuConfiguration($item) + { + if (!count($item)) { + return '{}'; + } + + return json_encode($item); + } +} diff --git a/plugins/rainlab/builder/formwidgets/blueprintbuilder/assets/js/blueprintbuilder.js b/plugins/rainlab/builder/formwidgets/blueprintbuilder/assets/js/blueprintbuilder.js new file mode 100644 index 0000000..437f5b5 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/blueprintbuilder/assets/js/blueprintbuilder.js @@ -0,0 +1,145 @@ +/* + * Blueprint Importer widget class. + * + * There is only a single instance of the Blueprint Importer class and it handles + * as many import builder user interfaces as needed. + * + */ ++function ($) { "use strict"; + + if ($.oc.builder.blueprintbuilder === undefined) { + $.oc.builder.blueprintbuilder = {}; + } + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype; + + var BlueprintBuilder = function() { + Base.call(this); + + this.updateBlueprintTimer = null; + + this.init(); + } + + BlueprintBuilder.prototype = Object.create(BaseProto) + BlueprintBuilder.prototype.constructor = BlueprintBuilder + + // INTERNAL METHODS + // ============================ + + BlueprintBuilder.prototype.init = function() { + this.registerHandlers(); + } + + BlueprintBuilder.prototype.registerHandlers = function() { + $(document).on('click', '.tailor-blueprint-list > li div[data-builder-remove-blueprint]', this.proxy(this.onRemoveBlueprint)) + $(document).on('livechange', '.tailor-blueprint-list > li.blueprint', this.proxy(this.onBlueprintLiveChange)) + } + + // BUILDER API METHODS + // ============================ + + BlueprintBuilder.prototype.onBlueprintLiveChange = function(ev) { + var $li = $(ev.currentTarget).closest('li'); + + this.startUpdateBlueprintBody($li.data('blueprint-uuid')); + + ev.stopPropagation(); + return false; + } + + BlueprintBuilder.prototype.startUpdateBlueprintBody = function(uuid) { + this.clearUpdateBlueprintBodyTimer(); + + var self = this; + this.updateBlueprintTimer = window.setTimeout(function(){ + self.updateBlueprintBody(uuid); + }, 300); + } + + BlueprintBuilder.prototype.clearUpdateBlueprintBodyTimer = function() { + if (this.updateBlueprintTimer === null) { + return; + } + + clearTimeout(this.updateBlueprintTimer); + this.updateBlueprintTimer = null; + } + + BlueprintBuilder.prototype.updateBlueprintBody = function(uuid) { + var $blueprint = $('li[data-blueprint-uuid="'+uuid+'"]'); + if (!$blueprint.length) { + return; + } + + this.clearUpdateBlueprintBodyTimer(); + $blueprint.addClass('updating-blueprint'); + + var properties = this.getBlueprintProperties($blueprint), + data = { + blueprint_uuid: uuid, + properties: properties + }; + + $blueprint.request('onRefreshBlueprintContainer', { + data: data + }).done( + this.proxy(this.blueprintMarkupLoaded) + ).always(function(){ + $blueprint.removeClass('updating-blueprint'); + }); + } + + BlueprintBuilder.prototype.blueprintMarkupLoaded = function(responseData) { + var $li = $('li[data-blueprint-uuid="'+responseData.blueprintUuid+'"]'); + if (!$li.length) { + return; + } + + $('.blueprint-body:first', $li).html(responseData.markup); + } + + BlueprintBuilder.prototype.getBlueprintProperties = function($blueprint) { + var value = $('input[data-inspector-values]', $blueprint).val(); + + if (value) { + return $.parseJSON(value); + } + + throw new Error('Inspector values element is not found in control.'); + } + + BlueprintBuilder.prototype.onRemoveBlueprint = function(ev) { + this.removeBlueprint($(ev.target).closest('li')); + + ev.preventDefault(); + ev.stopPropagation(); + + return false; + } + + BlueprintBuilder.prototype.removeBlueprint = function($control) { + var $container = $('.blueprint-container:first', $control); + + if ($container.hasClass('inspector-open')) { + var $inspectorContainer = this.findInspectorContainer($container); + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)); + } + + $control.remove(); + } + + BlueprintBuilder.prototype.findInspectorContainer = function($element) { + var $containerRoot = $element.closest('[data-inspector-container]') + + return $containerRoot.find('.inspector-container') + } + + $(document).ready(function(){ + // There is a single instance of the form builder. All operations + // are stateless, so instance properties or DOM references are not needed. + $.oc.builder.blueprintbuilder.controller = new BlueprintBuilder(); + }) + +}(window.jQuery); diff --git a/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_blueprint.php b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_blueprint.php new file mode 100644 index 0000000..51f9369 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_blueprint.php @@ -0,0 +1,18 @@ +getBlueprintInfo($blueprintUuid); + $fieldsConfiguration = $this->propertiesToInspectorSchema($blueprintInfo['properties']); +?> +
  • +

    + +
    +
    + renderBlueprintBody($blueprintInfo, $blueprintConfig) ?> +
    + + + +
    + +
    ×
    +
  • diff --git a/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_body.php b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_body.php new file mode 100644 index 0000000..acfc94a --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_body.php @@ -0,0 +1,11 @@ +
    +
    + +
    + makePartial('buildingarea') ?> +
    + + +
    +
    +
    diff --git a/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_buildingarea.php b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_buildingarea.php new file mode 100644 index 0000000..d4abe87 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_buildingarea.php @@ -0,0 +1,24 @@ +
    +
    +
    +
      + blueprints as $blueprintUuid => $blueprintConfig): ?> + makePartial('blueprint', [ + 'blueprintUuid' => $blueprintUuid, + 'blueprintConfig' => $blueprintConfig + ]) ?> + +
    + +
    +
    +
    diff --git a/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_select_blueprint_form.php b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_select_blueprint_form.php new file mode 100644 index 0000000..3364f52 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/blueprintbuilder/partials/_select_blueprint_form.php @@ -0,0 +1,36 @@ +
    + getEventHandler('onSelectBlueprint'), [ + 'data-popup-load-indicator' => true, + ]) ?> + + + + + + + + + + + +
    diff --git a/plugins/rainlab/builder/formwidgets/controllerbuilder/assets/js/controllerbuilder.js b/plugins/rainlab/builder/formwidgets/controllerbuilder/assets/js/controllerbuilder.js new file mode 100644 index 0000000..e69de29 diff --git a/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_behavior.php b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_behavior.php new file mode 100644 index 0000000..e5839b0 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_behavior.php @@ -0,0 +1,16 @@ +getBehaviorInfo($behaviorClass); + + $fieldsConfiguration = $this->propertiesToInspectorSchema($behaviorInfo['properties']); +?> + +
  • +

    + +
    + renderBehaviorBody($behaviorClass, $behaviorInfo, $behaviorConfig) ?> + + + +
    +
  • diff --git a/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_body.php b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_body.php new file mode 100644 index 0000000..ad34004 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_body.php @@ -0,0 +1,10 @@ +
    +
    +
    + makePartial('buildingarea') ?> +
    + + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_buildingarea.php b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_buildingarea.php new file mode 100644 index 0000000..5fae3d5 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/controllerbuilder/partials/_buildingarea.php @@ -0,0 +1,11 @@ +
    +
    +
    +
      + behaviors as $behaviorClass=>$behaviorConfig): ?> + makePartial('behavior', ['behaviorClass'=>$behaviorClass, 'behaviorConfig'=>$behaviorConfig]) ?> + +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.controlpalette.js b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.controlpalette.js new file mode 100644 index 0000000..f5c9ec6 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.controlpalette.js @@ -0,0 +1,263 @@ +/* + * Manages the control palette loading and displaying + */ ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var ControlPalette = function() { + Base.call(this) + + this.controlPaletteMarkup = null + this.popoverMarkup = null + this.containerMarkup = null + this.$popoverContainer = null + } + + ControlPalette.prototype = Object.create(BaseProto) + ControlPalette.prototype.constructor = ControlPalette + + // INTERNAL METHODS + // ============================ + + ControlPalette.prototype.loadControlPalette = function(element, controlId) { + if (this.controlPaletteMarkup === null) { + var data = { + controlId: controlId + } + + $.oc.stripeLoadIndicator.show() + $(element).request('onModelFormLoadControlPalette', { + data: data + }).done( + this.proxy(this.controlPaletteMarkupLoaded) + ).always(function(){ + $.oc.stripeLoadIndicator.hide() + }) + } + else { + this.showControlPalette(controlId, true) + } + } + + ControlPalette.prototype.controlPaletteMarkupLoaded = function(responseData) { + this.controlPaletteMarkup = responseData.markup + + this.showControlPalette(responseData.controlId) + } + + ControlPalette.prototype.getControlById = function(controlId) { + return document.body.querySelector('li[data-builder-control-id="'+controlId+'"]') + } + + ControlPalette.prototype.showControlPalette = function(controlId, initControls) { + if (this.getContainerPreference()) { + this.showControlPalletteInContainer(controlId, initControls) + } + else { + this.showControlPalletteInPopup(controlId, initControls) + } + } + + ControlPalette.prototype.assignControlIdToTemplate = function(template, controlId) { + return template.replace('%c', controlId) + } + + ControlPalette.prototype.markPlaceholderPaletteOpen = function(control) { + $(control).addClass('control-palette-open') + } + + ControlPalette.prototype.markPlaceholderPaletteNotOpen = function(control) { + $(control).removeClass('control-palette-open') + } + + ControlPalette.prototype.getContainerPreference = function() { + return $.oc.inspector.manager.getContainerPreference() + } + + ControlPalette.prototype.setContainerPreference = function(value) { + return $.oc.inspector.manager.setContainerPreference(value) + } + + ControlPalette.prototype.addControl = function(ev) { + var $target = $(ev.currentTarget), + controlId = $target.closest('[data-control-palette-controlid]').attr('data-control-palette-controlid') + + ev.preventDefault() + ev.stopPropagation() + + if (!controlId) { + return false; + } + + var control = this.getControlById(controlId) + if (!control) { + return false + } + + if ($(control).hasClass('loading-control')) { + return false + } + + $target.trigger('close.oc.popover') + + var promise = $.oc.builder.formbuilder.controller.addControlFromControlPalette(controlId, + $target.data('builderControlType'), + $target.data('builderControlName')) + + promise.done(function() { + $.oc.inspector.manager.createInspector(control) + $(control).trigger('change') // Set modified state for the form + }) + + return false + } + + // + // Popover wrapper + // + + ControlPalette.prototype.showControlPalletteInPopup = function(controlId, initControls) { + var control = this.getControlById(controlId) + + if (!control) { + return + } + + var $control = $(control) + + $control.ocPopover({ + content: this.assignControlIdToTemplate(this.getPopoverMarkup(), controlId), + highlightModalTarget: true, + modal: true, + placement: 'below', + containerClass: 'control-inspector', + offset: 15, + width: 400 + }) + + var $popoverContainer = $control.data('oc.popover').$container + + if (initControls) { + // Initialize the scrollpad control in the popup only when the + // popup is created from the cached markup string + $popoverContainer.trigger('render') + } + } + + ControlPalette.prototype.getPopoverMarkup = function() { + if (this.popoverMarkup !== null) { + return this.popoverMarkup + } + + var outerMarkup = $('script[data-template=control-palette-popover]').html() + + this.popoverMarkup = outerMarkup.replace('%s', this.controlPaletteMarkup) + + return this.popoverMarkup + } + + ControlPalette.prototype.dockToContainer = function(ev) { + var $popoverBody = $(ev.target).closest('.control-popover'), + $controlIdContainer = $popoverBody.find('[data-control-palette-controlid]'), + controlId = $controlIdContainer.attr('data-control-palette-controlid'), + control = this.getControlById(controlId) + + $popoverBody.trigger('close.oc.popover') + + this.setContainerPreference(true) + + if (control) { + this.loadControlPalette($(control), controlId) + } + } + + // + // Container wrapper + // + + ControlPalette.prototype.showControlPalletteInContainer = function(controlId, initControls) { + var control = this.getControlById(controlId) + + if (!control) { + return + } + + var inspectorManager = $.oc.inspector.manager, + $container = inspectorManager.getContainerElement($(control)) + + // If the container is already in use, apply values to the inspectable elements + if (!inspectorManager.applyValuesFromContainer($container) || !inspectorManager.containerHidingAllowed($container)) { + return + } + + // Dispose existing Inspector + $.oc.foundation.controlUtils.disposeControls($container.get(0)) + + this.markPlaceholderPaletteOpen(control) + + var template = this.assignControlIdToTemplate(this.getContainerMarkup(), controlId) + $container.append(template) + + $container.find('[data-control-palette-controlid]').one('dispose-control', this.proxy(this.onRemovePaletteFromContainer)) + + if (initControls) { + // Initialize the scrollpad control in the container only when the + // palette is created from the cached markup string + $container.trigger('render') + } + } + + ControlPalette.prototype.onRemovePaletteFromContainer = function(ev) { + this.removePaletteFromContainer($(ev.target)) + } + + ControlPalette.prototype.removePaletteFromContainer = function($container) { + var controlId = $container.attr('data-control-palette-controlid'), + control = this.getControlById(controlId) + + if (control) { + this.markPlaceholderPaletteNotOpen(control) + } + + var $parent = $container.parent() + $container.remove() + $parent.html('') + } + + ControlPalette.prototype.getContainerMarkup = function() { + if (this.containerMarkup !== null) { + return this.containerMarkup + } + + var outerMarkup = $('script[data-template=control-palette-container]').html() + + this.containerMarkup = outerMarkup.replace('%s', this.controlPaletteMarkup) + + return this.containerMarkup + } + + ControlPalette.prototype.closeInContainer = function(ev) { + this.removePaletteFromContainer($(ev.target).closest('[data-control-palette-controlid]')) + } + + ControlPalette.prototype.undockFromContainer = function(ev) { + var $container = $(ev.target).closest('[data-control-palette-controlid]'), + controlId = $container.attr('data-control-palette-controlid'), + control = this.getControlById(controlId) + + this.removePaletteFromContainer($container) + this.setContainerPreference(false) + + if (control) { + this.loadControlPalette($(control), controlId) + } + } + + $(document).ready(function(){ + // There is a single instance of the control palette manager. + $.oc.builder.formbuilder.controlPalette = new ControlPalette() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.domtopropertyjson.js b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.domtopropertyjson.js new file mode 100644 index 0000000..95e530e --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.domtopropertyjson.js @@ -0,0 +1,350 @@ +/* + * Converts control properties from DOM elements to JSON format. + */ ++function ($) { "use strict"; + + if ($.oc.builder === undefined) + $.oc.builder = {} + + if ($.oc.builder.formbuilder === undefined) + $.oc.builder.formbuilder = {} + + function getControlPropertyValues(item) { + for (var i=0, len=item.children.length; i 0 && properties['oc.commentPosition'] == 'above') { + properties['commentAbove'] = properties['oc.comment'] + + if (properties['comment'] !== undefined) { + delete properties['comment'] + } + + delete properties['oc.comment'] + delete properties['oc.commentPosition'] + } + + if (String(properties['oc.comment']).length > 0 && properties['oc.commentPosition'] == 'below') { + properties['comment'] = properties['oc.comment'] + + if (properties['comentAbove'] !== undefined) { + delete properties['comentAbove'] + } + + delete properties['oc.comment'] + delete properties['oc.commentPosition'] + } + + if (properties['oc.comment'] !== undefined) { + if (String(properties['oc.comment']).length > 0) { + properties['comment'] = properties['oc.comment'] + } + + delete properties['oc.comment'] + } + } + + function parseControlControlContainer(control) { + var children = control.children, + result = {} + + for (var i=0, len=children.length; i 0) { + if (objectHasProperties(controls)) { + if (result[listName].fields === undefined) { + result[listName].fields = {} + } + + result[listName].fields = $.extend(result[listName].fields, controls) + } + } + else { + if (objectHasProperties(controls)) { + if (result.fields === undefined) { + result.fields = {} + } + + result.fields = $.extend(result.fields, controls) + } + } + } + + function containerToJson(container) { + var containerElements = container.children, + result = {} + + for (var i=0, len=containerElements.length; i li.control'), + result = [] + + for (var i=controls.length-1; i>=0; i--) { + var properties = getControlPropertyValues(controls[i]) + + if (typeof properties !== 'object') { + continue + } + + if (properties['oc.fieldName'] === undefined) { + continue + } + + var name = properties['oc.fieldName'] + + if (result.indexOf(name) === -1) { + result.push(name) + } + } + + result.sort() + + return result + } + + + $.oc.builder.formbuilder.domToPropertyJson = DomToJson + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.js b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.js new file mode 100644 index 0000000..1f7d38b --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.js @@ -0,0 +1,803 @@ +/* + * Form Builder widget class. + * + * There is only a single instance of the Form Builder class and it handles + * as many form builder user interfaces as needed. + * + */ ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var FormBuilder = function() { + Base.call(this); + + this.placeholderIdIndex = 0; + this.updateControlBodyTimer = null; + + this.init(); + } + + FormBuilder.prototype = Object.create(BaseProto) + FormBuilder.prototype.constructor = FormBuilder + + // INTERNAL METHODS + // ============================ + + FormBuilder.prototype.init = function() { + this.registerHandlers() + } + + FormBuilder.prototype.registerHandlers = function() { + document.addEventListener('dragstart', this.proxy(this.onDragStart)) + document.addEventListener('dragover', this.proxy(this.onDragOver)) + document.addEventListener('dragenter', this.proxy(this.onDragEnter)) + document.addEventListener('dragleave', this.proxy(this.onDragLeave)) + document.addEventListener('drop', this.proxy(this.onDragDrop), false); + + $(document).on('change', '.builder-control-list > li.control', this.proxy(this.onControlChange)) + $(document).on('click', '.builder-control-list > li.control div[data-builder-remove-control]', this.proxy(this.onRemoveControl)) + $(document).on('click', '.builder-control-list > li.oc-placeholder', this.proxy(this.onPlaceholderClick)) + $(document).on('showing.oc.inspector', '.builder-control-list > li.control', this.proxy(this.onInspectorShowing)) + $(document).on('livechange', '.builder-control-list > li.control', this.proxy(this.onControlLiveChange)) + $(document).on('autocompleteitems.oc.inspector', '.builder-control-list > li.control', this.proxy(this.onAutocompleteItems)) + $(document).on('dropdownoptions.oc.inspector', '.builder-control-list > li.control', this.proxy(this.onDropdownOptions)) + } + + FormBuilder.prototype.getControlId = function(li) { + if (li.hasAttribute('data-builder-control-id')) { + return li.getAttribute('data-builder-control-id') + } + + this.placeholderIdIndex++ + li.setAttribute('data-builder-control-id', this.placeholderIdIndex) + + return this.placeholderIdIndex + } + + // PROPERTY HELPERS + // ============================ + + FormBuilder.prototype.getControlProperties = function(li) { + var children = li.children + + for (var i=children.length-1; i>=0; i--) { + var element = children[i] + + if (element.tagName === 'INPUT' && element.hasAttribute('data-inspector-values')) { + return $.parseJSON(element.value) + } + } + + throw new Error('Inspector values element is not found in control.') + } + + FormBuilder.prototype.setControlProperties = function(li, propertiesObj) { + var propertiesStr = JSON.stringify(propertiesObj), + valuesInput = li.querySelector('[data-inspector-values]') + + valuesInput.value = propertiesStr + } + + FormBuilder.prototype.loadModelFields = function(control, callback) { + var $form = $(this.findForm(control)), + pluginCode = $.oc.builder.indexController.getFormPluginCode($form), + modelClass = $form.find('input[name=model_class]').val() + + $.oc.builder.dataRegistry.get($form, pluginCode, 'model-columns', modelClass, function(response){ + callback({ + options: $.oc.builder.indexController.dataToInspectorArray(response) + }) + }) + } + + FormBuilder.prototype.getContainerFieldNames = function(control, callback) { + var controlWrapper = this.findRootControlWrapper(control), + fieldNames = $.oc.builder.formbuilder.domToPropertyJson.getAllControlNames(controlWrapper), + options = [] + + options.push({ + title: '---', + value: '' + }) + + for (var i=0, len=fieldNames.length; i=0; i--) { + var value = String(valueInputs[i].value) + + if (value.length === 0) { + continue + } + + var properties = $.parseJSON(value) + + if (properties['oc.fieldName'] == fieldName) { + return true + } + } + + return false + } + + // FLOW MANAGEMENT + // ============================ + + FormBuilder.prototype.reflow = function(li, listElement) { + if (!li && !listElement) { + throw new Error('Invalid call of the reflow method. Either li or list parameter should be not empty.') + } + + var list = listElement ? listElement : li.parentNode, + items = list.children, + prevSpan = null + + for (var i=0, len = items.length; i < len; i++) { + var item = items[i], + itemSpan = item.getAttribute('data-builder-span') + + if ($.oc.foundation.element.hasClass(item, 'clear-row')) { + continue + } + + if (itemSpan == 'auto') { + $.oc.foundation.element.removeClass(item, 'span-left') + $.oc.foundation.element.removeClass(item, 'span-full') + $.oc.foundation.element.removeClass(item, 'span-right') + + if (prevSpan == 'left') { + $.oc.foundation.element.addClass(item, 'span-right') + prevSpan = 'right' + } + else { + if (!$.oc.foundation.element.hasClass(item, 'oc-placeholder')) { + $.oc.foundation.element.addClass(item, 'span-left') + } + else { + $.oc.foundation.element.addClass(item, 'span-full') + } + + prevSpan = 'left' + } + } + else { + $.oc.foundation.element.removeClass(item, 'span-left') + $.oc.foundation.element.removeClass(item, 'span-full') + $.oc.foundation.element.removeClass(item, 'span-right') + $.oc.foundation.element.addClass(item, 'span-' + itemSpan) + + prevSpan = itemSpan + } + } + } + + FormBuilder.prototype.setControlSpanFromProperties = function(li, properties) { + if (properties.span === undefined) { + return + } + + li.setAttribute('data-builder-span', properties.span) + this.reflow(li) + } + + FormBuilder.prototype.appendClearRowElement = function(li) { + li.insertAdjacentHTML('afterend', '
  • '); + } + + FormBuilder.prototype.patchControlSpan = function(li, span) { + li.setAttribute('data-builder-span', span) + + var properties = this.getControlProperties(li) + properties.span = span + this.setControlProperties(li, properties) + } + + // DRAG AND DROP + // ============================ + + FormBuilder.prototype.targetIsPlaceholder = function(ev) { + if (!ev.target.getAttribute) { + return false // In Gecko ev.target could be a text node + } + + return ev.target.getAttribute('data-builder-placeholder') + } + + FormBuilder.prototype.dataTransferContains = function(ev, element) { + if (ev.dataTransfer.types.indexOf !== undefined){ + return ev.dataTransfer.types.indexOf(element) >= 0 + } + + return ev.dataTransfer.types.contains(element) + } + + FormBuilder.prototype.sourceIsContainer = function(ev) { + return this.dataTransferContains(ev, 'builder/source/container') + } + + FormBuilder.prototype.startDragFromContainer = function(ev) { + ev.dataTransfer.effectAllowed = 'move' + + var controlId = this.getControlId(ev.target) + ev.dataTransfer.setData('builder/source/container', 'true') + ev.dataTransfer.setData('builder/control/id', controlId) + ev.dataTransfer.setData(controlId, controlId) + } + + FormBuilder.prototype.dropTargetIsChildOf = function(target, ev) { + var current = target + + while (current) { + if (this.elementIsControl(current) && this.dataTransferContains(ev, this.getControlId(current))) { + return true + } + + current = current.parentNode + } + + return false + } + + FormBuilder.prototype.dropFromContainerToPlaceholderOrControl = function(ev, targetControl) { + var targetElement = targetControl ? targetControl : ev.target + + $.oc.foundation.event.stop(ev) + this.stopHighlightingTargets(targetElement) + + var controlId = ev.dataTransfer.getData('builder/control/id'), + originalControl = document.body.querySelector('li[data-builder-control-id="'+controlId+'"]') + + if (!originalControl) { + return + } + + var isSameList = originalControl.parentNode === targetElement.parentNode, + originalList = originalControl.parentNode, + $originalClearRow = $(originalControl).next() + + targetElement.parentNode.insertBefore(originalControl, targetElement) + + this.appendClearRowElement(originalControl) + if ($originalClearRow.hasClass('clear-row')) { + $originalClearRow.remove() + } + + if (!$.oc.foundation.element.hasClass(originalControl, 'inspector-open')) { + this.patchControlSpan(originalControl, 'auto') + } + + this.reflow(targetElement) + + if (!isSameList) { + this.reflow(null, originalList) + } + + $(targetElement).closest('form').trigger('change') + } + + FormBuilder.prototype.elementContainsPoint = function(point, element) { + var elementPosition = $.oc.foundation.element.absolutePosition(element), + elementRight = elementPosition.left + element.offsetWidth, + elementBottom = elementPosition.top + element.offsetHeight + + return point.x >= elementPosition.left && point.x <= elementRight + && point.y >= elementPosition.top && point.y <= elementBottom + } + + FormBuilder.prototype.stopHighlightingTargets = function(target, excludeTarget) { + var rootWrapper = this.findRootControlWrapper(target), + controls = rootWrapper.querySelectorAll('li.control.drag-over') + + for (var i=controls.length-1; i>= 0; i--) { + if (!excludeTarget || target !== controls[i]) { + $.oc.foundation.element.removeClass(controls[i], 'drag-over') + } + } + } + + // UPDATING CONTROLS + // ============================ + + FormBuilder.prototype.startUpdateControlBody = function(controlId) { + this.clearUpdateControlBodyTimer() + + var self = this + this.updateControlBodyTimer = window.setTimeout(function(){ + self.updateControlBody(controlId) + }, 300) + } + + FormBuilder.prototype.clearUpdateControlBodyTimer = function() { + if (this.updateControlBodyTimer === null) { + return + } + + clearTimeout(this.updateControlBodyTimer) + this.updateControlBodyTimer = null + } + + FormBuilder.prototype.updateControlBody = function(controlId) { + var control = document.body.querySelector('li[data-builder-control-id="'+controlId+'"]') + if (!control) { + return + } + + this.clearUpdateControlBodyTimer() + + var rootWrapper = this.findRootControlWrapper(control), + controls = rootWrapper.querySelectorAll('li.control.updating-control') + + for (var i=controls.length-1; i>=0; i--) { + $.oc.foundation.element.removeClass(controls[i], 'updating-control') + } + + $.oc.foundation.element.addClass(control, 'updating-control') + + var controlType = control.getAttribute('data-control-type'), + properties = this.getControlProperties(control), + data = { + controlType: controlType, + controlId: controlId, + properties: properties + } + + $(control).request('onModelFormRenderControlBody', { + data: data + }).done( + this.proxy(this.controlBodyMarkupLoaded) + ).always(function(){ + $.oc.foundation.element.removeClass(control, 'updating-control') + }) + } + + FormBuilder.prototype.controlBodyMarkupLoaded = function(responseData) { + var li = document.body.querySelector('li[data-builder-control-id="'+responseData.controlId+'"]') + if (!li) { + return + } + + var wrapper = li.querySelector('.control-wrapper') + + wrapper.innerHTML = responseData.markup + } + + // ADDING CONTROLS + // ============================ + + FormBuilder.prototype.generateFieldName = function(controlType, placeholder) { + var controlContainer = this.findControlContainer(placeholder) + + if (!controlContainer) { + throw new Error('Cannot find control container for a placeholder.') + } + + // Replace any banned characters + controlType = controlType.replace(/[^a-zA-Z0-9_\[\]]/g, '') + + var counter = 1, + fieldName = controlType + counter + + while (this.fieldNameExistsInContainer(controlContainer, fieldName)) { + counter ++ + fieldName = controlType + counter + } + + return fieldName + } + + FormBuilder.prototype.addControlToPlaceholder = function(placeholder, controlType, controlName, noNewPlaceholder, fieldName) { + // Duplicate the placeholder and place it after + // the existing one + if (!noNewPlaceholder) { + var newPlaceholder = $(placeholder.outerHTML) + + newPlaceholder.removeAttr('data-builder-control-id') + newPlaceholder.removeClass('control-palette-open') + + placeholder.insertAdjacentHTML('afterend', newPlaceholder.get(0).outerHTML) + } + + // Create the clear-row element after the current placeholder + this.appendClearRowElement(placeholder) + + // Replace the placeholder class with control + // loading indicator + $.oc.foundation.element.removeClass(placeholder, 'oc-placeholder') + $.oc.foundation.element.addClass(placeholder, 'loading-control') + $.oc.foundation.element.removeClass(placeholder, 'control-palette-open') + placeholder.innerHTML = '' + placeholder.removeAttribute('data-builder-placeholder') + + if (!fieldName) { + fieldName = this.generateFieldName(controlType, placeholder) + } + + // Send request to the server to load the + // control markup, Inspector data schema, inspector title, etc. + var data = { + controlType: controlType, + controlId: this.getControlId(placeholder), + properties: { + 'label': controlName, + 'span': 'auto', + 'oc.fieldName': fieldName + } + } + this.reflow(placeholder) + + return $(placeholder).request('onModelFormRenderControlWrapper', { + data: data + }).done(this.proxy(this.controlWrapperMarkupLoaded)) + } + + FormBuilder.prototype.controlWrapperMarkupLoaded = function(responseData) { + var placeholder = document.body.querySelector('li[data-builder-control-id="'+responseData.controlId+'"]') + if (!placeholder) { + return + } + + placeholder.setAttribute('draggable', true) + placeholder.setAttribute('data-inspectable', true) + placeholder.setAttribute('data-control-type', responseData.type) + + placeholder.setAttribute('data-inspector-title', responseData.controlTitle) + placeholder.setAttribute('data-inspector-description', responseData.description) + + placeholder.innerHTML = responseData.markup + $.oc.foundation.element.removeClass(placeholder, 'loading-control') + } + + FormBuilder.prototype.displayControlPaletteForPlaceholder = function(element) { + $.oc.builder.formbuilder.controlPalette.loadControlPalette(element, this.getControlId(element)) + } + + FormBuilder.prototype.addControlFromControlPalette = function(placeholderId, controlType, controlName) { + var placeholder = document.body.querySelector('li[data-builder-control-id="'+placeholderId+'"]') + if (!placeholder) { + return + } + + return this.addControlToPlaceholder(placeholder, controlType, controlName) + } + + // REMOVING CONTROLS + // ============================ + + FormBuilder.prototype.removeControl = function($control) { + if ($control.hasClass('inspector-open')) { + var $inspectorContainer = this.findInspectorContainer($control) + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)) + } + + var $nextControl = $control.next() // Even if the removed element was alone, there's always a placeholder element + $control.remove() + + this.reflow($nextControl.get(0)) + $nextControl.trigger('change') + } + + // DOM HELPERS + // ============================ + + FormBuilder.prototype.findControlContainer = function(element) { + var current = element + + while (current) { + if (current.hasAttribute && current.hasAttribute('data-control-container') ) { + return current + } + + current = current.parentNode + } + + return null + } + + FormBuilder.prototype.findForm = function(element) { + var current = element + + while (current) { + if (current.tagName === 'FORM') { + return current + } + + current = current.parentNode + } + + return null + } + + FormBuilder.prototype.findControlList = function(element) { + var current = element + + while (current) { + if (current.hasAttribute('data-control-list')) { + return current + } + + current = current.parentNode + } + + throw new Error('Cannot find control list for an element.') + } + + FormBuilder.prototype.findPlaceholder = function(controlList) { + var children = controlList.children + + for (var i=children.length-1; i>=0; i--) { + var element = children[i] + + if (element.tagName === 'LI' && $.oc.foundation.element.hasClass(element, 'oc-placeholder')) { + return element + } + } + + throw new Error('Cannot find placeholder in a control list.') + } + + FormBuilder.prototype.findRootControlWrapper = function(control) { + var current = control + + while (current) { + if (current.hasAttribute('data-root-control-wrapper')) { + return current + } + + current = current.parentNode + } + + throw new Error('Cannot find root control wrapper.') + } + + FormBuilder.prototype.findInspectorContainer = function($element) { + var $containerRoot = $element.closest('[data-inspector-container]') + + return $containerRoot.find('.inspector-container') + } + + FormBuilder.prototype.elementIsControl = function(element) { + return element.tagName === 'LI' && element.hasAttribute('data-control-type') && $.oc.foundation.element.hasClass(element, 'control') + } + + FormBuilder.prototype.getClosestControl = function(element) { + var current = element + + while (current) { + if (this.elementIsControl(current)) { + return current + } + + current = current.parentNode + } + + return null + } + + // EVENT HANDLERS + // ============================ + + FormBuilder.prototype.onDragStart = function(ev) { + if (this.elementIsControl(ev.target)) { + this.startDragFromContainer(ev) + + return + } + } + + FormBuilder.prototype.onDragOver = function(ev) { + var targetLi = ev.target + + if (ev.target.tagName !== 'LI') { + targetLi = this.getClosestControl(ev.target) + } + + if (!targetLi || targetLi.tagName != 'LI') { + return + } + + var sourceIsContainer = this.sourceIsContainer(ev), + elementIsControl = this.elementIsControl(targetLi) + + if ((this.targetIsPlaceholder(ev) || elementIsControl) && sourceIsContainer) { + // Do not allow dropping controls to themselves or their + // children controls. + if (sourceIsContainer && elementIsControl && this.dropTargetIsChildOf(targetLi, ev)) { + return false + } + + // Dragging from container over a placeholder or another control. + // Allow the drop. + $.oc.foundation.event.stop(ev) + ev.dataTransfer.dropEffect = 'move' + return + } + } + + FormBuilder.prototype.onDragEnter = function(ev) { + var targetLi = ev.target + + if (ev.target.tagName !== 'LI') { + targetLi = this.getClosestControl(ev.target) + } + + if (!targetLi || targetLi.tagName != 'LI') { + return + } + + var sourceIsContainer = this.sourceIsContainer(ev) + + if (this.targetIsPlaceholder(ev) && sourceIsContainer) { + // Do not allow dropping controls to themselves or their + // children controls. + if (sourceIsContainer && this.dropTargetIsChildOf(ev.target, ev)) { + this.stopHighlightingTargets(ev.target, true) + return + } + + // Dragging from a container over a placeholder. + // Highlight the placeholder. + $.oc.foundation.element.addClass(ev.target, 'drag-over') + return + } + + var elementIsControl = this.elementIsControl(targetLi) + + if (elementIsControl && sourceIsContainer) { + // Do not allow dropping controls to themselves or their + // children controls. + if (sourceIsContainer && elementIsControl && this.dropTargetIsChildOf(targetLi, ev)) { + this.stopHighlightingTargets(targetLi, true) + return + } + + // Dragging from a container over another control. + // Highlight the other control. + $.oc.foundation.element.addClass(targetLi, 'drag-over') + + this.stopHighlightingTargets(targetLi, true) + + return + } + } + + FormBuilder.prototype.onDragLeave = function(ev) { + var targetLi = ev.target + + if (ev.target.tagName !== 'LI') { + targetLi = this.getClosestControl(ev.target) + } + + if (!targetLi || targetLi.tagName != 'LI') { + return + } + + if (this.targetIsPlaceholder(ev) && this.sourceIsContainer(ev)) { + // Dragging from a container over a placeholder. + // Stop highlighting the placeholder. + this.stopHighlightingTargets(ev.target) + + return + } + + if (this.elementIsControl(targetLi) && this.sourceIsContainer(ev)) { + // Dragging from a container over another control. + // Stop highlighting the other control. + var mousePosition = $.oc.foundation.event.pageCoordinates(ev) + + if (!this.elementContainsPoint(mousePosition, targetLi)) { + this.stopHighlightingTargets(targetLi) + } + } + } + + FormBuilder.prototype.onDragDrop = function(ev) { + var targetLi = ev.target + + if (ev.target.tagName !== 'LI') { + targetLi = this.getClosestControl(ev.target) + } + + if (!targetLi || targetLi.tagName != 'LI') { + return + } + + var elementIsControl = this.elementIsControl(targetLi), + sourceIsContainer = this.sourceIsContainer(ev) + + if ((elementIsControl || this.targetIsPlaceholder(ev)) && sourceIsContainer) { + this.stopHighlightingTargets(targetLi) + + if (this.dropTargetIsChildOf(targetLi, ev)) { + return + } + + // Dropped from a container to a placeholder or another control. + // Stop highlighting the placeholder, move the control. + this.dropFromContainerToPlaceholderOrControl(ev, targetLi) + return + } + } + + FormBuilder.prototype.onControlChange = function(ev) { + // Control has changed (with Inspector) - + // update the control markup with AJAX + + var li = ev.currentTarget, + properties = this.getControlProperties(li) + + this.setControlSpanFromProperties(li, properties) + this.updateControlBody(this.getControlId(li)) + + ev.stopPropagation() + return false + } + + FormBuilder.prototype.onControlLiveChange = function(ev) { + $(this.findForm(ev.currentTarget)).trigger('change') // Set modified state for the form + + var li = ev.currentTarget, + propertiesParsed = this.getControlProperties(li) + + this.setControlSpanFromProperties(li, propertiesParsed) + this.startUpdateControlBody(this.getControlId(li)) + + ev.stopPropagation() + return false + } + + FormBuilder.prototype.onAutocompleteItems = function(ev, data) { + if (data.propertyDefinition.fillFrom === 'model-fields') { + ev.preventDefault() + this.loadModelFields(ev.target, data.callback) + } + } + + FormBuilder.prototype.onDropdownOptions = function(ev, data) { + if (data.propertyDefinition.fillFrom === 'form-controls') { + this.getContainerFieldNames(ev.target, data.callback) + ev.preventDefault() + } + } + + FormBuilder.prototype.onRemoveControl = function(ev) { + this.removeControl($(ev.target).closest('li.control')) + + ev.preventDefault() + ev.stopPropagation() + + return false + } + + FormBuilder.prototype.onInspectorShowing = function(ev) { + if ($(ev.target).find('input[data-non-inspectable-control]').length > 0) { + ev.preventDefault() + return false + } + } + + FormBuilder.prototype.onPlaceholderClick = function(ev) { + this.displayControlPaletteForPlaceholder(ev.target) + ev.stopPropagation() + ev.preventDefault() + return false; + } + + $(document).ready(function(){ + // There is a single instance of the form builder. All operations + // are stateless, so instance properties or DOM references are not needed. + $.oc.builder.formbuilder.controller = new FormBuilder() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.tabs.js b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.tabs.js new file mode 100644 index 0000000..3123d25 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/assets/js/formbuilder.tabs.js @@ -0,0 +1,286 @@ +/* + * Manages tabs in the form builder area. + */ ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var TabManager = function() { + Base.call(this) + + this.init() + } + + TabManager.prototype = Object.create(BaseProto) + TabManager.prototype.constructor = TabManager + + // INTERNAL METHODS + // ============================ + + TabManager.prototype.init = function() { + this.registerHandlers() + } + + TabManager.prototype.registerHandlers = function() { + var $layoutBody = $('#layout-body') + + $layoutBody.on('click', 'li[data-builder-new-tab]', this.proxy(this.onNewTabClick)) + $layoutBody.on('click', 'div[data-builder-tab]', this.proxy(this.onTabClick)) + $layoutBody.on('click', 'div[data-builder-close-tab]', this.proxy(this.onTabCloseClick)) + $layoutBody.on('change livechange', 'ul.tabs > li div.inspector-trigger.tab-control', this.proxy(this.onTabChange)) + $layoutBody.on('hiding.oc.inspector', 'ul.tabs > li div.inspector-trigger.tab-control', this.proxy(this.onTabInspectorHiding)) + } + + TabManager.prototype.getTabList = function($tabControl) { + return $tabControl.find('> ul.tabs') + } + + TabManager.prototype.getPanelList = function($tabControl) { + return $tabControl.find('> ul.panels') + } + + TabManager.prototype.findTabControl = function($tab) { + return $tab.closest('div.tabs') + } + + TabManager.prototype.findTabPanel = function($tab) { + var $tabControl = this.findTabControl($tab), + tabIndex = $tab.index() + + return this.getPanelList($tabControl).find(' > li').eq(tabIndex) + } + + TabManager.prototype.findPanelTab = function($panel) { + var $tabControl = this.findTabControl($panel), + tabIndex = $panel.index() + + return this.getTabList($tabControl).find(' > li').eq(tabIndex) + } + + TabManager.prototype.findTabPanel = function($tab) { + var $tabControl = this.findTabControl($tab), + tabIndex = $tab.index() + + return this.getPanelList($tabControl).find(' > li').eq(tabIndex) + } + + TabManager.prototype.findTabForm = function(tab) { + return $(tab).closest('form') + } + + TabManager.prototype.getGlobalTabsProperties = function(tabsContainer) { + var properties = $(tabsContainer).find('.inspector-trigger.tab-control.global [data-inspector-values]').val() + + if (properties.length == 0) { + properties = '{}' + } + + return $.parseJSON(properties) + } + + /* + * Returns tab title an element belongs to + */ + TabManager.prototype.getElementTabTitle = function(element) { + var $panel = $(element).closest('li.tab-panel'), + $tab = this.findPanelTab($panel), + properties = $tab.find('[data-inspector-values]').val(), + propertiesParsed = $.parseJSON(properties) + + return propertiesParsed.title + } + + TabManager.prototype.tabHasControls = function($tab) { + return this.findTabPanel($tab).find('ul[data-control-list] li.control:not(.oc-placeholder)').length > 0 + } + + TabManager.prototype.tabNameExists = function($tabList, name, $ignoreTab) { + var tabs = $tabList.get(0).children + + for (var i=0, len = tabs.length; i li[data-builder-new-tab]') + + $('[data-tab-title]', $newTab).text(tabName) + + $newTab.insertBefore($newTabControl) + this.getPanelList($tabControl).append(panelTemplate) + + this.gotoTab($newTab) + } + + TabManager.prototype.gotoTab = function($tab) { + var tabIndex = $tab.index(), + $tabControl = this.findTabControl($tab), + $tabList = this.getTabList($tabControl), + $panelList = this.getPanelList($tabControl) + + $('> li', $tabList).removeClass('active') + $tab.addClass('active') + + $('> li', $panelList).removeClass('active') + $('> li', $panelList).eq(tabIndex).addClass('active') + } + + TabManager.prototype.findInspectorContainer = function($element) { + var $containerRoot = $element.closest('[data-inspector-container]') + + return $containerRoot.find('.inspector-container') + } + + TabManager.prototype.closeTabInspectors = function($tab, $tabPanel) { + if ($tab.find('.inspector-open').length === 0 && $tabPanel.find('.inspector-open').length === 0) { + return + } + + var $inspectorContainer = this.findInspectorContainer($tab) + + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)) + } + + TabManager.prototype.closeTabControlPalette = function($tab, $tabPanel) { + if ($tabPanel.find('.control-palette-open').length === 0) { + return + } + + var $inspectorContainer = this.findInspectorContainer($tab) + + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)) + } + + TabManager.prototype.closeTab = function($tab) { + var $tabControl = this.findTabControl($tab) + + if (this.tabHasControls($tab)) { + if (!confirm($tabControl.data('tabCloseConfirmation'))) { + return + } + + $tab.trigger('change') + } + + var $prevTab = $tab.prev(), + $nextTab = $tab.next(), + $tabPanel = this.findTabPanel($tab) + + this.closeTabInspectors($tab, $tabPanel) + this.closeTabControlPalette($tab, $tabPanel) + + $tab.remove() + $tabPanel.remove() + + if ($prevTab.length > 0) { + this.gotoTab($prevTab) + } + else { + if ($nextTab.length > 0 && !$nextTab.hasClass('new-tab')) { + this.gotoTab($nextTab) + } + else { + this.createNewTab($tabControl) + } + } + } + + TabManager.prototype.updateTabProperties = function($tab) { + var properties = $tab.find('[data-inspector-values]').val(), + propertiesParsed = $.parseJSON(properties), + $form = this.findTabForm($tab), + pluginCode = $form.find('input[name=plugin_code]').val() + + $tab.find('[data-tab-title]').attr('data-localization-key', propertiesParsed.title) + + $.oc.builder.dataRegistry.getLocalizationString($form, pluginCode, propertiesParsed.title, function(title){ + $tab.find('[data-tab-title]').text(title) + }) + } + + // EVENT HANDLERS + // ============================ + + TabManager.prototype.onNewTabClick = function(ev) { + this.createNewTab($(ev.currentTarget).closest('div.tabs')) + + ev.stopPropagation() + ev.preventDefault() + + return false + } + + TabManager.prototype.onTabClick = function(ev) { + this.gotoTab($(ev.currentTarget).closest('li')) + + ev.stopPropagation() + ev.preventDefault() + + return false + } + + TabManager.prototype.onTabCloseClick = function(ev) { + this.closeTab($(ev.currentTarget).closest('li')) + + ev.stopPropagation() + ev.preventDefault() + + return false + } + + TabManager.prototype.onTabChange = function(ev) { + this.updateTabProperties($(ev.currentTarget).closest('li')) + } + + TabManager.prototype.onTabInspectorHiding = function(ev, data) { + var $tab = $(ev.currentTarget).closest('li'), + $tabControl = this.findTabControl($tab), + $tabList = this.getTabList($tabControl) + + if (this.tabNameExists($tabList, data.values.title, $tab)) { + alert($tabControl.data('tabAlreadyExists')) + + ev.preventDefault() + } + } + + $(document).ready(function(){ + // There is a single instance of the tabs manager. + $.oc.builder.formbuilder.tabManager = new TabManager() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_body.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_body.php new file mode 100644 index 0000000..c6bd2b8 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_body.php @@ -0,0 +1,34 @@ +
    +
    +
    + makePartial('buildingarea') ?> +
    + + +
    +
    +
    + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_buildingarea.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_buildingarea.php new file mode 100644 index 0000000..a15e7a0 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_buildingarea.php @@ -0,0 +1,9 @@ +
    +
    +
    +
    + makePartial('controlcontainer', ['fieldsConfiguration'=>$model->controls]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlbody.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlbody.php new file mode 100644 index 0000000..1dfe6c9 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlbody.php @@ -0,0 +1,26 @@ +
    + getPropertyValue($properties, 'label'); + $comment = $this->getPropertyValue($properties, 'oc.comment'); + + // Note - the label and comment elements should not have whitespace in the markup. + ?> +
    + +
    getPropertyValue($properties, 'oc.commentPosition') == 'above'): ?>
    + + + + + +
    getPropertyValue($properties, 'oc.commentPosition') == 'below'): ?>
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlcontainer.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlcontainer.php new file mode 100644 index 0000000..eca3d0a --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlcontainer.php @@ -0,0 +1,26 @@ +
    + + makePartial('controllist', [ + 'controls' => isset($fieldsConfiguration['fields']) ? $fieldsConfiguration['fields'] : [], + 'listName' => '' + ]) ?> + + makePartial('tabs', [ + 'type' => 'primary', + 'controls' => $this->getTabsFields('tabs', $fieldsConfiguration), + 'listName' => 'tabs', + 'tabsTitle' => trans('rainlab.builder::lang.form.tabs_primary'), + 'configuration' => [], + 'tabNameTemplate' => trans('rainlab.builder::lang.form.tab_name_template'), + ]) ?> + + makePartial('tabs', [ + 'type' => 'secondary', + 'controls' => $this->getTabsFields('secondaryTabs', $fieldsConfiguration), + 'listName' => 'secondaryTabs', + 'tabsTitle' => trans('rainlab.builder::lang.form.tabs_secondary'), + 'configuration' => [], + 'tabNameTemplate' => trans('rainlab.builder::lang.form.tab_name_template'), + ]) ?> + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controllist.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controllist.php new file mode 100644 index 0000000..018e50e --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controllist.php @@ -0,0 +1,40 @@ +
      + $controlConfig): + $controlRenderingInfo = $this->getControlRenderingInfo($controlName, $controlConfig, $prevConfig); + + $prevSpan = $controlConfig['span'] = $controlRenderingInfo['span']; + $prevConfig = $controlConfig; + ?> +
    • + data-inspectable="true" + + data-unknown + + draggable="true" + data-control-type="" + data-inspector-title="" + data-inspector-description=""> + + renderControlWrapper($controlRenderingInfo['type'], $controlRenderingInfo['properties'], $controlConfig) ?> + +
    • + +
    • + + +
    • + +
    • +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlpalette.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlpalette.php new file mode 100644 index 0000000..05559ab --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlpalette.php @@ -0,0 +1,33 @@ +
    +
    + +
    + +
    + +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlwrapper.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlwrapper.php new file mode 100644 index 0000000..9256feb --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_controlwrapper.php @@ -0,0 +1,14 @@ + + +
    + renderControlBody($type, $properties) ?> +
    + +renderControlStaticBody($type, $properties, $controlConfiguration) ?> + + + + +
    ×
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tab.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tab.php new file mode 100644 index 0000000..140ed71 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tab.php @@ -0,0 +1,22 @@ +
  • +
    +
    + +
    +
    + +
    + + + + + +
    + +
    ×
    +
  • \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabpanel.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabpanel.php new file mode 100644 index 0000000..7c2d65c --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabpanel.php @@ -0,0 +1,6 @@ +
  • + makePartial('controllist', [ + 'controls' => $controls, + 'listName' => $listName + ]) ?> +
  • \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabs.php b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabs.php new file mode 100644 index 0000000..4ce12df --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/formbuilder/partials/_tabs.php @@ -0,0 +1,57 @@ +
    +
    +
      + + makePartial('tab', ['active'=>true, 'title'=>sprintf($tabNameTemplate, '1') ]) ?> + + $tabControls): + $tabIndex++; + ?> + makePartial('tab', ['active'=>$tabIndex == 1, 'title'=>$tabName]) ?> + + +
    • +
    + +
      + + makePartial('tabpanel', ['controls' => [], 'listName'=>$listName, 'active'=>true]); ?> + + + $tabControls): ?> + + makePartial('tabpanel', ['controls' => $tabControls, 'listName'=>$listName, 'active'=>$tabIndex == 1]) ?> + + +
    + +
    +
    + + + +
    + +
    + + + + +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/assets/js/menubuilder.js b/plugins/rainlab/builder/formwidgets/menueditor/assets/js/menubuilder.js new file mode 100644 index 0000000..7a76446 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/assets/js/menubuilder.js @@ -0,0 +1,230 @@ +/* + * Menu Builder widget class. + * + * There is only a single instance of the Menu Builder class and it handles + * as many menu builder user interfaces as needed. + * + */ ++function ($) { "use strict"; + + if ($.oc.builder.menubuilder === undefined) + $.oc.builder.menubuilder = {} + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + var MenuBulder = function() { + Base.call(this) + + this.init() + } + + MenuBulder.prototype = Object.create(BaseProto) + MenuBulder.prototype.constructor = MenuBulder + + // INTERNAL METHODS + // ============================ + + MenuBulder.prototype.init = function() { + this.registerHandlers() + } + + MenuBulder.prototype.registerHandlers = function() { + $(document).on('change', '.builder-menu-editor li.item', this.proxy(this.onItemChange)) + $(document).on('dragged.list.sortable', '.builder-menu-editor li.item', this.proxy(this.onItemDragged)) + $(document).on('livechange', '.builder-menu-editor li.item', this.proxy(this.onItemLiveChange)) + } + + MenuBulder.prototype.getParentList = function(element) { + return $(element).closest('ul') + } + + MenuBulder.prototype.findForm = function(item) { + return $(item).closest('form') + } + + MenuBulder.prototype.getElementListItem = function(element) { + return $(element).closest('li') + } + + MenuBulder.prototype.getMenuBuilderControlElement = function(element) { + return $(element).closest('[data-control=builder-menu-editor]') + } + + MenuBulder.prototype.getTemplateMarkup = function(element, templateAttribute) { + var $builderControl = this.getMenuBuilderControlElement(element) + + return $builderControl.find('script['+templateAttribute+']').html() + } + + MenuBulder.prototype.getItemProperties = function(item) { + return $.parseJSON($(item).find('> input[data-inspector-values]').val()) + } + + MenuBulder.prototype.itemCodeExistsInList = function($list, code) { + var valueInputs = $list.find('> li.item > input[data-inspector-values]') + + for (var i=valueInputs.length-1; i>=0; i--) { + var value = String(valueInputs[i].value) + + if (value.length === 0) { + continue + } + + var properties = $.parseJSON(value) + + if (properties['code'] == code) { + return true + } + } + + return false + } + + MenuBulder.prototype.replacePropertyValue = function($item, property, value) { + var input = $item.find(' > input[data-inspector-values]'), + properties = $.parseJSON(input.val()) + + properties[property] = value + input.val(JSON.stringify(properties)) + } + + MenuBulder.prototype.generateItemCode = function($parentList, baseCode) { + var counter = 1, + code = baseCode + + while (this.itemCodeExistsInList($parentList, code)) { + counter ++ + code = baseCode + counter + } + + return code + } + + MenuBulder.prototype.updateItemVisualProperties = function(item) { + var properties = this.getItemProperties(item), + $item = $(item), + $form = this.findForm(item), + pluginCode = $form.find('input[name=plugin_code]').val() + + $item.find('> .item-container > span.title').attr('data-localization-key', properties.label) + + $.oc.builder.dataRegistry.getLocalizationString($item, pluginCode, properties.label, function(label){ + $item.find('> .item-container > span.title').text(label) + }) + + $item.find('> .item-container > i').attr('class', properties.icon) + } + + MenuBulder.prototype.findInspectorContainer = function($element) { + var $containerRoot = $element.closest('[data-inspector-container]') + + return $containerRoot.find('.inspector-container') + } + + // BUILDER API METHODS + // ============================ + + MenuBulder.prototype.addMainMenuItem = function(ev) { + var newItemMarkup = this.getTemplateMarkup(ev.currentTarget, 'data-main-menu-template'), + $item = $(newItemMarkup), + $list = this.getParentList(ev.currentTarget), + newCode = this.generateItemCode($list, 'main-menu-item') + + this.replacePropertyValue($item, 'code', newCode) + + this.getElementListItem(ev.currentTarget).before($item) + $(this.findForm(ev.currentTarget)).trigger('change') + } + + MenuBulder.prototype.addSideMenuItem = function(ev) { + var newItemMarkup = this.getTemplateMarkup(ev.currentTarget, 'data-side-menu-template'), + $item = $(newItemMarkup), + $list = this.getParentList(ev.currentTarget), + newCode = this.generateItemCode($list, 'side-menu-item') + + this.replacePropertyValue($item, 'code', newCode) + + this.getElementListItem(ev.currentTarget).before($item) + $(this.findForm(ev.currentTarget)).trigger('change') + } + + MenuBulder.prototype.getJson = function(form) { + var mainMenuItems = form.querySelectorAll('ul.builder-main-menu > li.item'), + result = [] + + for (var i=0,lenOuter=mainMenuItems.length; i < lenOuter; i++) { + var mainMenuItem = mainMenuItems[i], + mainMenuItemConfig = this.getItemProperties(mainMenuItem) + + if (mainMenuItemConfig['sideMenu'] !== undefined) { + delete mainMenuItemConfig['sideMenu'] + } + + var sideMenuItems = mainMenuItem.querySelectorAll('ul.builder-submenu > li.item') + for (var j=0,lenInner=sideMenuItems.length; j < lenInner; j++) { + var sideMenuItem = sideMenuItems[j], + sideMenuItemConfig = this.getItemProperties(sideMenuItem) + + if (mainMenuItemConfig['sideMenu'] === undefined) { + mainMenuItemConfig['sideMenu'] = [] + } + + mainMenuItemConfig['sideMenu'].push(sideMenuItemConfig) + } + + result.push(mainMenuItemConfig) + } + + return JSON.stringify(result) + } + + MenuBulder.prototype.deleteMenuItem = function(ev) { + var item = this.getElementListItem(ev.currentTarget) + + if ($(item).hasClass('inspector-open')) { + var $inspectorContainer = this.findInspectorContainer($(item)) + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)) + } + + var subitems = item.get(0).querySelectorAll('li.inspector-open') + for (var i=subitems.length-1; i>=0; i--) { + var $inspectorContainer = this.findInspectorContainer($(subitems[i])) + $.oc.foundation.controlUtils.disposeControls($inspectorContainer.get(0)) + } + + $(this.findForm(ev.currentTarget)).trigger('change') + + $(item).remove() + } + + // EVENT HANDLERS + // ============================ + + MenuBulder.prototype.onItemLiveChange = function(ev) { + this.updateItemVisualProperties(ev.currentTarget) + + $(this.findForm(ev.currentTarget)).trigger('change') // Set modified state for the form + + ev.stopPropagation() + return false + } + + MenuBulder.prototype.onItemChange = function(ev) { + this.updateItemVisualProperties(ev.currentTarget) + + ev.stopPropagation() + return false + } + + MenuBulder.prototype.onItemDragged = function(ev) { + $(this.findForm(ev.target)).trigger('change') + } + + $(document).ready(function(){ + // There is a single instance of the form builder. All operations + // are stateless, so instance properties or DOM references are not needed. + $.oc.builder.menubuilder.controller = new MenuBulder() + }) + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/partials/_body.php b/plugins/rainlab/builder/formwidgets/menueditor/partials/_body.php new file mode 100644 index 0000000..f5bbcfb --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/partials/_body.php @@ -0,0 +1,28 @@ +
    +
    +
    + +
    +
    +
    +
    + makePartial('mainmenuitems', ['items' => $items]) ?> +
    +
    +
    + + + + +
    + +
    + + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitem.php b/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitem.php new file mode 100644 index 0000000..529d122 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitem.php @@ -0,0 +1,17 @@ +
  • +
    + + getItemArrayProperty($item, 'label'))) ?> + + × +
    + + + + + makePartial('submenuitems', ['items' => $this->getItemArrayProperty($item, 'sideMenu')]) ?> +
  • \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitems.php b/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitems.php new file mode 100644 index 0000000..5661380 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/partials/_mainmenuitems.php @@ -0,0 +1,15 @@ +
      + + makePartial('mainmenuitem', ['item' => $item]) ?> + + +
    • + + + + +
    • +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitem.php b/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitem.php new file mode 100644 index 0000000..673b4d4 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitem.php @@ -0,0 +1,15 @@ +
  • +
    + + getItemArrayProperty($item, 'label'))) ?> + × +
    + + + +
  • \ No newline at end of file diff --git a/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitems.php b/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitems.php new file mode 100644 index 0000000..24ee8b5 --- /dev/null +++ b/plugins/rainlab/builder/formwidgets/menueditor/partials/_submenuitems.php @@ -0,0 +1,17 @@ +
      + + + makePartial('submenuitem', ['item' => $item]) ?> + + + +
    • + + + + +
    • +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/cs.json b/plugins/rainlab/builder/lang/cs.json new file mode 100644 index 0000000..2e38a53 --- /dev/null +++ b/plugins/rainlab/builder/lang/cs.json @@ -0,0 +1,4 @@ +{ + "Builder": "Builder", + "Provides visual tools for building October plugins.": "Poskytuje vizuální nástroj pro tvorbu October pluginů." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/cs/lang.php b/plugins/rainlab/builder/lang/cs/lang.php new file mode 100644 index 0000000..2cc653d --- /dev/null +++ b/plugins/rainlab/builder/lang/cs/lang.php @@ -0,0 +1,621 @@ + [ + 'add' => 'Vytvořit plugin', + 'no_records' => 'Žádný plugin nenalezen', + 'no_description' => 'Tento plugin nemá žádný popisek', + 'no_name' => 'Bez jména', + 'search' => 'Vyhledávání...', + 'filter_description' => 'Zobrazit všechny pluginy, nebo pouze vaše.', + 'settings' => 'Nastavení', + 'entity_name' => 'Plugin', + 'field_name' => 'Název', + 'field_author' => 'Autor', + 'field_description' => 'Popis', + 'field_icon' => 'Ikona pluginu', + 'field_plugin_namespace' => 'Jmenný prostor pluginu', + 'field_author_namespace' => 'Jmenný prostor autora', + 'field_namespace_description' => 'Jmenný prostor může obsahovat pouze znaky, číslice a měl by začínat písmenem. Například Blog.', + 'field_author_namespace_description' => 'Zadaný jmenný prostor nebude možno přes Builder poté změnit. Příklad jmenného prostoru: JohnSmith.', + 'tab_general' => 'Základní parametry', + 'tab_description' => 'Popis', + 'field_homepage' => 'Domovská URL pluginu', + 'error_settings_not_editable' => 'Nastavení tohoto pluginu nelze přes Builder měnit, protože nemá soubor plugin.yaml.', + 'update_hint' => 'Překlad názvu a popisku můžete měnit v menu Lokalizace.', + 'manage_plugins' => 'Tvorba a úprava pluginů.', + ], + 'author_name' => [ + 'title' => 'Jméno autora', + 'description' => 'Výchozí jméno autora pro nově vytvořené pluginy. Toto jméno však můžete změnit při vytváření nového pluginu, nebo poté v editaci.', + ], + 'author_namespace' => [ + 'title' => 'Jmenný prostor autora', + 'description' => 'Pokud budete chtít plugin umístit na stránkách OctoberCMS, jmenný prostor by se měl být pro všechny vaše pluginy shodný. Více detailů najdete v dokumentaci publikace pluginů.', + ], + 'database' => [ + 'menu_label' => 'Databáze', + 'no_records' => 'Žádné tabulky nebyly nalezeny', + 'search' => 'Vyhledávání...', + 'confirmation_delete_multiple' => 'Opravdu chcete odstranit vybrané tabulky?', + 'field_name' => 'Název databázové tabulky', + 'tab_columns' => 'Sloupce', + 'column_name_name' => 'Sloupec', + 'column_name_required' => 'Zadejte prosím název sloupce', + 'column_name_type' => 'Typ', + 'column_type_required' => 'Vyberte prosím typ sloupce', + 'column_name_length' => 'Délka', + 'column_validation_length' => 'Délka by měla být zadaná číselně, nebo zadaná jako číslo a přesnost (10,2) pro desetinná čísla. Mezery nejsou povolené.', + 'column_validation_title' => 'V názvu sloupce mohou být pouze čísla, malá písmena a podtržítko.', + 'column_name_unsigned' => 'Bez znaménka', + 'column_name_nullable' => 'Nulový', + 'column_auto_increment' => 'AUTOINCR', + 'column_default' => 'Výchozí', + 'column_auto_primary_key' => 'PK', + 'tab_new_table' => 'Nová tabulka', + 'btn_add_column' => 'Přidat sloupec', + 'btn_delete_column' => 'Smazat sloupec', + 'confirm_delete' => 'Opravdu chcete smazat tuto tabulku?', + 'error_enum_not_supported' => 'Tabulka obsahuje sloupce s typem "enum" které Builder aktuálně nepodporuje.', + 'error_table_name_invalid_prefix' => 'Název tabulky by měl začínat prefixem pluginu: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Formát názvu tabulky není správný, měl by obsahovat pouze písmena, číslice a nebo podtržítka. Název by měl začínat písmenem a neměl by obsahovat mezery.', + 'error_table_duplicate_column' => 'Takový název sloupce již existuje: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => 'An auto-increment column cannot be a part of a compound primary key.', + 'error_table_mutliple_auto_increment' => 'Tabulka nemůže obsahovat více sloupců s auto-increment vlastností.', + 'error_table_auto_increment_non_integer' => 'Auto-increment sloupec by měl mít číselný typ.', + 'error_table_decimal_length' => 'Zápis délky pro typ :type by měl být ve formátu \'10,2\', bez mezer.', + 'error_table_length' => 'Zápis délky pro typ :type by měl být zadaný jako číslo.', + 'error_unsigned_type_not_int' => 'Chyba ve sloupci \':column\'. Přiznak \'bez znaménka\' můžete použít pouze pro číselné typy.', + 'error_integer_default_value' => 'Chybná výchozí hodnota pro číselný sloupec \':column\'. Povolené formáty jsou \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Chybná výchozí hodnota pro desetinný sloupec \':column\'. Povolené formáty jsou \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Chybná výchozí hodnota pro pravdivostní sloupec \':column\'. Povolené hodnoty jsou \'0\' and \'1\'.', + 'error_unsigned_negative_value' => 'Výchozí hodnota pro sloupec bez znaménka \':column\' nemůže být záporná.', + 'error_table_already_exists' => 'Tabulka \':name\' již v databázi existuje.', + ], + 'model' => [ + 'menu_label' => 'Modely', + 'entity_name' => 'Model', + 'no_records' => 'Žádný model nebyl nalezen', + 'search' => 'Vyhledávání...', + 'add' => 'Přidat...', + 'forms' => 'Formuláře', + 'lists' => 'Listování', + 'field_class_name' => 'Název třídy', + 'field_database_table' => 'Databazová tabulka', + 'error_class_name_exists' => 'Soubor modelu pro tuto třídu již existuje: :path', + 'add_form' => 'Přidat formulář', + 'add_list' => 'Přidat listování', + ], + 'form' => [ + 'saved' => 'Formulář byl úspěšně uložen.', + 'confirm_delete' => 'Opravdu chcete smazat tento formulář?', + 'tab_new_form' => 'Nový formulář', + 'property_label_title' => 'Popisek', + 'property_label_required' => 'Zadejte prosím popisek pole.', + 'property_span_title' => 'Zarovnání', + 'property_comment_title' => 'Komentář', + 'property_comment_above_title' => 'Komentář nad', + 'property_default_title' => 'Výchozí', + 'property_checked_default_title' => 'Ve výchozím stavu zaškrtnuto', + 'property_css_class_title' => 'CSS třída', + 'property_css_class_description' => 'Volitelná CSS třída která se přiřadí ke kontejneru pole.', + 'property_disabled_title' => 'Neaktivní', + 'property_hidden_title' => 'Skrytý', + 'property_required_title' => 'Povinný', + 'property_field_name_title' => 'Název pole', + 'property_placeholder_title' => 'Zástupný text', + 'property_default_from_title' => 'Default from', + 'property_stretch_title' => 'Stretch', + 'property_stretch_description' => 'Definuje jestli se toto pole zmenší tak, aby se vešlo to rodičovského prvku na výšku.', + 'property_context_title' => 'Kontext', + 'property_context_description' => 'Definuje jaký kontext bude zobrazen při zobrazení tohoto pole.', + 'property_context_create' => 'Vytvořit', + 'property_context_update' => 'Upravit', + 'property_context_preview' => 'Náhled', + 'property_dependson_title' => 'Závisí na', + 'property_trigger_action' => 'Akce', + 'property_trigger_show' => 'Zobrazit', + 'property_trigger_hide' => 'Schovat', + 'property_trigger_enable' => 'Aktivní', + 'property_trigger_disable' => 'Neaktivní', + 'property_trigger_empty' => 'Prázdný', + 'property_trigger_field' => 'Pole', + 'property_trigger_field_description' => 'Defines the other field name that will trigger the action.', + 'property_trigger_condition' => 'Podmínka', + 'property_trigger_condition_description' => 'Determines the condition the specified field should satisfy for the condition to be considered "true". Supported values: checked, unchecked, value[somevalue].', + 'property_trigger_condition_checked' => 'Zaškrtnuté', + 'property_trigger_condition_unchecked' => 'Nezaškrtnuté', + 'property_trigger_condition_somevalue' => 'value[enter-the-value-here]', + 'property_preset_title' => 'Preset', + 'property_preset_description' => 'Allows the field value to be initially set by the value of another field, converted using the input preset converter.', + 'property_preset_field' => 'Pole', + 'property_preset_field_description' => 'Defines the other field name to source the value from.', + 'property_preset_type' => 'Typ', + 'property_preset_type_description' => 'Specifies the conversion type', + 'property_attributes_title' => 'Atributy', + 'property_attributes_description' => 'Custom HTML attributes to add to the form field element.', + 'property_container_attributes_title' => 'Kontejnér atributů', + 'property_container_attributes_description' => 'Custom HTML attributes to add to the form field container element.', + 'property_group_advanced' => 'Pokročilé', + 'property_dependson_description' => 'A list of other field names this field depends on, when the other fields are modified, this field will update. One field per line.', + 'property_trigger_title' => 'Trigger', + 'property_trigger_description' => 'Allows to change elements attributes such as visibility or value, based on another elements\' state.', + 'property_default_from_description' => 'Takes the default value from the value of another field.', + 'property_field_name_required' => 'Název pole je povinný', + 'property_field_name_regex' => 'Název pole může obsahovat pouze písmena, číslice, podtržítka, pomlčky a hranaté závorky.', + 'property_attributes_size' => 'Velikost', + 'property_attributes_size_tiny' => 'Nejmenší', + 'property_attributes_size_small' => 'Malý', + 'property_attributes_size_large' => 'Normální', + 'property_attributes_size_huge' => 'Veliký', + 'property_attributes_size_giant' => 'Obrovský', + 'property_comment_position' => 'Zobrazit komentář', + 'property_comment_position_above' => 'Nad prvkem', + 'property_comment_position_below' => 'Pod prvkem', + 'property_hint_path' => 'Hint partial path', + 'property_hint_path_description' => 'Path to a partial file that contains the hint text. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_hint.htm', + 'property_hint_path_required' => 'Please enter the hint partial path', + 'property_partial_path' => 'Cesta k dílčímu souboru', + 'property_partial_path_description' => 'Path to a partial file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_partial.htm', + 'property_partial_path_required' => 'Prosím zadejte cestu k dílčímu souboru', + 'property_code_language' => 'Jazyk', + 'property_code_theme' => 'Téma', + 'property_theme_use_default' => 'Použít výchozí téma', + 'property_group_code_editor' => 'Editor kódu', + 'property_gutter' => 'Výplň', + 'property_gutter_show' => 'Viditelný', + 'property_gutter_hide' => 'Skrytý', + 'property_wordwrap' => 'Word wrap', + 'property_wordwrap_wrap' => 'Wrap', + 'property_wordwrap_nowrap' => 'Don\'t wrap', + 'property_fontsize' => 'Velikost písma', + 'property_codefolding' => 'Code folding', + 'property_codefolding_manual' => 'Manual', + 'property_codefolding_markbegin' => 'Mark begin', + 'property_codefolding_markbeginend' => 'Mark begin and end', + 'property_autoclosing' => 'Automatické zavírání', + 'property_enabled' => 'Aktivní', + 'property_disabled' => 'Neaktivní', + 'property_soft_tabs' => 'Soft tabs', + 'property_tab_size' => 'Velikost záložky', + 'property_readonly' => 'Pouze pro čtení', + 'property_use_default' => 'Use default settings', + 'property_options' => 'Volby', + 'property_prompt' => 'Prompt', + 'property_prompt_description' => 'Text to display for the create button.', + 'property_prompt_default' => 'Přidat nový prvek', + 'property_available_colors' => 'Dostupné barvy', + 'property_available_colors_description' => 'List of available colors in hex format (#FF0000). Leave empty for the default color set. Enter one value per line.', + 'property_datepicker_mode' => 'Mód', + 'property_datepicker_mode_date' => 'Datum', + 'property_datepicker_mode_datetime' => 'Datum a čas', + 'property_datepicker_mode_time' => 'Čas', + 'property_datepicker_min_date' => 'Min datum', + 'property_datepicker_max_date' => 'Max datum', + 'property_fileupload_mode' => 'Mód', + 'property_fileupload_mode_file' => 'Soubor', + 'property_fileupload_mode_image' => 'Obrázek', + 'property_group_fileupload' => 'Nahrávání obrázků', + 'property_fileupload_image_width' => 'Šířka obrázku', + 'property_fileupload_image_width_description' => 'Optional parameter - images will be resized to this width. Applies to Image mode only.', + 'property_fileupload_invalid_dimension' => 'Invalid dimension value - please enter a number.', + 'property_fileupload_image_height' => 'Výška obrázku', + 'property_fileupload_image_height_description' => 'Optional parameter - images will be resized to this height. Applies to Image mode only.', + 'property_fileupload_file_types' => 'Typy souborů', + 'property_fileupload_file_types_description' => 'Optional comma separated list of file extensions that are accepted by the uploader. Eg: zip,txt', + 'property_fileupload_mime_types' => 'MIME typy', + 'property_fileupload_mime_types_description' => 'Optional comma separated list of MIME types that are accepted by the uploader, either as file extensions or fully qualified names. Eg: bin,txt', + 'property_fileupload_use_caption' => 'Použít popisek', + 'property_fileupload_use_caption_description' => 'Allows a title and description to be set for the file.', + 'property_fileupload_thumb_options' => 'Volby náhledu', + 'property_fileupload_thumb_options_description' => 'Manages options for the automatically generated thumbnails. Applies only for the Image mode.', + 'property_fileupload_thumb_mode' => 'Mód', + 'property_fileupload_thumb_auto' => 'Auto', + 'property_fileupload_thumb_exact' => 'Přesně', + 'property_fileupload_thumb_portrait' => 'Portrét', + 'property_fileupload_thumb_landscape' => 'Krajina', + 'property_fileupload_thumb_crop' => 'Crop', + 'property_fileupload_thumb_extension' => 'Přípona souboru', + 'property_name_from' => 'Name column', + 'property_name_from_description' => 'Relation column name to use for displaying a name.', + 'property_description_from' => 'Description column', + 'property_description_from_description' => 'Relation column name to use for displaying a description.', + 'property_recordfinder_prompt' => 'Prompt', + 'property_recordfinder_prompt_description' => 'Text to display when there is no record selected. The %s character represents the search icon. Leave empty for the default prompt.', + 'property_recordfinder_list' => 'List configuration', + 'property_recordfinder_list_description' => 'A reference to a list column definition file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => 'Please provide a path to the list YAML file', + 'property_group_recordfinder' => 'Record finder', + 'property_mediafinder_mode' => 'Mód', + 'property_mediafinder_mode_file' => 'Soubor', + 'property_mediafinder_mode_image' => 'Obrázek', + 'property_group_relation' => 'Relace', + 'property_relation_prompt' => 'Prompt', + 'property_relation_prompt_description' => 'Text to display when there is no available selections.', + 'control_group_standard' => 'Standardní', + 'control_group_widgets' => 'Widgets', + 'click_to_add_control' => 'Přidat prvek', + 'loading' => 'Načítám...', + 'control_text' => 'Text', + 'control_text_description' => 'Single line text box', + 'control_password' => 'Heslo', + 'control_password_description' => 'Single line password text field', + 'control_checkbox' => 'Checkbox', + 'control_checkbox_description' => 'Single checkbox', + 'control_switch' => 'Přepínač', + 'control_switch_description' => 'Single switchbox, an alternative for checkbox', + 'control_textarea' => 'Víceřádkové textové pole', + 'control_textarea_description' => 'Multiline text box with controllable height', + 'control_dropdown' => 'Dropdown', + 'control_dropdown_description' => 'Dropdown list with static or dynamic options', + 'control_unknown' => 'Unknown control type: :type', + 'control_repeater' => 'Repeater', + 'control_repeater_description' => 'Outputs a repeating set of form controls', + 'control_number' => 'Číslo', + 'control_number_description' => 'Single line text box that takes numbers only', + 'control_hint' => 'Hint', + 'control_hint_description' => 'Outputs a partial contents in a box that can be hidden by the user', + 'control_partial' => 'Partial', + 'control_partial_description' => 'Outputs a partial contents', + 'control_section' => 'Sekce', + 'control_section_description' => 'Displays a form section with heading and subheading', + 'control_radio' => 'Radio list', + 'control_radio_description' => 'A list of radio options, where only one item can be selected at a time', + 'control_radio_option_1' => 'Volba 1', + 'control_radio_option_2' => 'Volba 2', + 'control_checkboxlist' => 'Checkbox list', + 'control_checkboxlist_description' => 'A list of checkboxes, where multiple items can be selected', + 'control_codeeditor' => 'Editor kódu', + 'control_codeeditor_description' => 'Plaintext editor for formatted code or markup', + 'control_colorpicker' => 'Výběr barvy', + 'control_colorpicker_description' => 'A field for selecting a hexadecimal color value', + 'control_datepicker' => 'Výběr data', + 'control_datepicker_description' => 'Text field used for selecting date and times', + 'control_richeditor' => 'Rich editor', + 'control_richeditor_description' => 'Visual editor for rich formatted text, also known as a WYSIWYG editor', + 'control_markdown' => 'Markdown editor', + 'control_markdown_description' => 'Basic editor for Markdown formatted text', + 'control_fileupload' => 'Nahrávání souborů', + 'control_fileupload_description' => 'File uploader for images or regular files', + 'control_recordfinder' => 'Record finder', + 'control_recordfinder_description' => 'Field with details of a related record with the record search feature', + 'control_mediafinder' => 'Media finder', + 'control_mediafinder_description' => 'Field for selecting an item from the Media Manager library', + 'control_relation' => 'Relace', + 'control_relation_description' => 'Displays either a dropdown or checkbox list for selecting a related record', + 'error_file_name_required' => 'Please enter the form file name.', + 'error_file_name_invalid' => 'The file name can contain only Latin letters, digits, underscores, dots and hashes.', + 'span_left' => 'Doleva', + 'span_right' => 'Doprava', + 'span_full' => 'Plná šířka', + 'span_auto' => 'Automaticky', + 'empty_tab' => 'Prázdná záložka', + 'confirm_close_tab' => 'The tab contains controls which will be deleted. Continue?', + 'tab' => 'Form tab', + 'tab_title' => 'Název', + 'controls' => 'Prvky formuláře', + 'property_tab_title_required' => 'Název záložky je povinný.', + 'tabs_primary' => 'Primární záložka', + 'tabs_secondary' => 'Vedlejší záložka', + 'tab_stretch' => 'Stretch', + 'tab_stretch_description' => 'Specifies if this tabs container stretches to fit the parent height.', + 'tab_css_class' => 'CSS třída', + 'tab_css_class_description' => 'Přiřadí CSS třídu kontejneru záložky.', + 'tab_name_template' => 'Záložka %s', + 'tab_already_exists' => 'Záložka s tímto názvem již existuje.', + ], + 'list' => [ + 'tab_new_list' => 'Nový list', + 'saved' => 'List byl úspěšně uložen.', + 'confirm_delete' => 'Opravdu chcete smazat tento list?', + 'tab_columns' => 'Sloupce', + 'btn_add_column' => 'Přidat sloupec', + 'btn_delete_column' => 'Smazat sloupec', + 'column_dbfield_label' => 'Field', + 'column_dbfield_required' => 'Please enter the model field', + 'column_name_label' => 'Popisek', + 'column_label_required' => 'Zadejte prosím popisek sloupce', + 'column_type_label' => 'Type', + 'column_type_required' => 'Zadejte prosím typ sloupce', + 'column_type_text' => 'Text', + 'column_type_number' => 'Číslo', + 'column_type_switch' => 'Switch', + 'column_type_datetime' => 'Datum a čas', + 'column_type_date' => 'Datum', + 'column_type_time' => 'Čas', + 'column_type_timesince' => 'Čas od', + 'column_type_timetense' => 'Čas do', + 'column_type_select' => 'Select', + 'column_type_partial' => 'Partial', + 'column_label_default' => 'Výchozí', + 'column_label_searchable' => 'Vyhledávání', + 'column_label_sortable' => 'Řazení', + 'column_label_invisible' => 'Neviditelný', + 'column_label_select' => 'Výběr', + 'column_label_relation' => 'Relace', + 'column_label_css_class' => 'CSS class', + 'column_label_width' => 'Šířka', + 'column_label_path' => 'Cesta', + 'column_label_format' => 'Formát', + 'column_label_value_from' => 'Hodnota od', + 'error_duplicate_column' => 'Duplicitní pole sloupce: \':column\'.', + ], + 'controller' => [ + 'menu_label' => 'Kontroléry', + 'no_records' => 'Žádné kontrolery nebyly nalezeny', + 'controller' => 'Kontrolér', + 'behaviors' => 'Chování', + 'new_controller' => 'Nový kontrolér', + 'error_controller_has_no_behaviors' => 'The controller doesn\'t have configurable behaviors.', + 'error_invalid_yaml_configuration' => 'Error loading behavior configuration file: :file', + 'behavior_form_controller' => 'Možnost vytvářet a měnit záznamy', + 'behavior_form_controller_description' => 'Přidá možnost vytvářet a měnit záznamy pomocí formulářů. Toto chování vytvoří tři pohledy pro tvorbu položky, úpravu a náhled.', + 'property_behavior_form_placeholder' => '--vyberte formulář--', + 'property_behavior_form_name' => 'Název', + 'property_behavior_form_name_description' => 'The name of the object being managed by this form', + 'property_behavior_form_name_required' => 'Please enter the form name', + 'property_behavior_form_file' => 'Form configuration', + 'property_behavior_form_file_description' => 'Reference to a form field definition file', + 'property_behavior_form_file_required' => 'Please enter a path to the form configuration file', + 'property_behavior_form_model_class' => 'Modelová třída', + 'property_behavior_form_model_class_description' => 'A model class name, the form data is loaded and saved against this model.', + 'property_behavior_form_model_class_required' => 'Please select a model class', + 'property_behavior_form_default_redirect' => 'Výchozí přesměrování', + 'property_behavior_form_default_redirect_description' => 'A page to redirect to by default when the form is saved or cancelled.', + 'property_behavior_form_create' => 'Create record page', + 'property_behavior_form_redirect' => 'Přesměrování', + 'property_behavior_form_redirect_description' => 'A page to redirect to when a record is created.', + 'property_behavior_form_redirect_close' => 'Close redirect', + 'property_behavior_form_redirect_close_description' => 'A page to redirect to when a record is created and the close post variable is sent with the request.', + 'property_behavior_form_flash_save' => 'Save flash message', + 'property_behavior_form_flash_save_description' => 'Flash message to display when record is saved.', + 'property_behavior_form_page_title' => 'Page title', + 'property_behavior_form_update' => 'Update record page', + 'property_behavior_form_update_redirect' => 'Přesměrování', + 'property_behavior_form_create_redirect_description' => 'A page to redirect to when a record is saved.', + 'property_behavior_form_flash_delete' => 'Delete flash message', + 'property_behavior_form_flash_delete_description' => 'Flash message to display when record is deleted.', + 'property_behavior_form_preview' => 'Preview record page', + 'behavior_list_controller' => 'Možnost listování záznamy', + 'behavior_list_controller_description' => 'Vytvoří tabulku s řazením a vyhledávání s možností definovat odkaz na detail jednotlivého záznamu. Chování automaticky vytvoří akci kontroléru "index".', + 'property_behavior_list_title' => 'List title', + 'property_behavior_list_title_required' => 'Please enter the list title', + 'property_behavior_list_placeholder' => '--select list--', + 'property_behavior_list_model_class' => 'Modelová třída', + 'property_behavior_list_model_class_description' => 'A model class name, the list data is loaded from this model.', + 'property_behavior_form_model_class_placeholder' => '--select model--', + 'property_behavior_list_model_class_required' => 'Please select a model class', + 'property_behavior_list_model_placeholder' => '--select model--', + 'property_behavior_list_file' => 'List configuration file', + 'property_behavior_list_file_description' => 'Reference to a list definition file', + 'property_behavior_list_file_required' => 'Please enter a path to the list configuration file', + 'property_behavior_list_record_url' => 'Record URL', + 'property_behavior_list_record_url_description' => 'Link each list record to another page. Eg: users/update:id. The :id part is replaced with the record identifier.', + 'property_behavior_list_no_records_message' => 'No records message', + 'property_behavior_list_no_records_message_description' => 'A message to display when no records are found', + 'property_behavior_list_recs_per_page' => 'Records per page', + 'property_behavior_list_recs_per_page_description' => 'Records to display per page, use 0 for no pages. Default: 0', + 'property_behavior_list_recs_per_page_regex' => 'Records per page should be an integer value', + 'property_behavior_list_show_setup' => 'Show setup button', + 'property_behavior_list_show_sorting' => 'Show sorting', + 'property_behavior_list_default_sort' => 'Default sorting', + 'property_behavior_form_ds_column' => 'Column', + 'property_behavior_form_ds_direction' => 'Direction', + 'property_behavior_form_ds_asc' => 'Ascending', + 'property_behavior_form_ds_desc' => 'Descending', + 'property_behavior_list_show_checkboxes' => 'Show checkboxes', + 'property_behavior_list_onclick' => 'On click handler', + 'property_behavior_list_onclick_description' => 'Custom JavaScript code to execute when clicking on a record.', + 'property_behavior_list_show_tree' => 'Show tree', + 'property_behavior_list_show_tree_description' => 'Displays a tree hierarchy for parent/child records.', + 'property_behavior_list_tree_expanded' => 'Tree expanded', + 'property_behavior_list_tree_expanded_description' => 'Determines if tree nodes should be expanded by default.', + 'property_behavior_list_toolbar' => 'Toolbar', + 'property_behavior_list_toolbar_buttons' => 'Buttons partial', + 'property_behavior_list_toolbar_buttons_description' => 'Reference to a controller partial file with the toolbar buttons. Eg: list_toolbar', + 'property_behavior_list_search' => 'Search', + 'property_behavior_list_search_prompt' => 'Search prompt', + 'property_behavior_list_filter' => 'Filter configuration', + 'error_controller_not_found' => 'Original controller file is not found.', + 'error_invalid_config_file_name' => 'The behavior :class configuration file name (:file) contains invalid characters and cannot be loaded.', + 'error_file_not_yaml' => 'The behavior :class configuration file (:file) is not a YAML file. Only YAML configuration files are supported.', + 'saved' => 'Kontrolér byl úspěšně uložen.', + 'controller_name' => 'Název kontroléru', + 'controller_name_description' => 'Název kontroléru definuje název třídy a URL kontroléru v administraci. Použijte prosím standardní pojmenování PHP tříd - první symbol je velkým písmenem a zbytek normálně, například: Categories, Posts, Products.', + 'base_model_class' => 'Rodičovská třída', + 'base_model_class_description' => 'Vyberte třídu modelu ze které bude tento kontrolér dědit. Chování kontroléru můžete nastavit později.', + 'base_model_class_placeholder' => '--vyberte model--', + 'controller_behaviors' => 'Chování', + 'controller_behaviors_description' => 'Vyberte chování, které má kontrolér implementovat. Builder automaticky vytvoří požadované soubory.', + 'controller_permissions' => 'Oprávnění', + 'controller_permissions_description' => 'Vyberte uživatelská práva potřebná pro tento kontrolér. Práva můžete nastavit v sekci Oprávnění v levém menu. Toto nastavení můžete později změnit v PHP skriptu.', + 'controller_permissions_no_permissions' => 'Plugin nemá vytvořena žádná oprávnění.', + 'menu_item' => 'Aktivní položka menu', + 'menu_item_description' => 'Vyberte položku menu, která bude aktivní pro tento kontrolér. Toto nastavení můžete kdykoli změnit v PHP skriptu.', + 'menu_item_placeholder' => '--vyberte položku menu--', + 'error_unknown_behavior' => 'Třída chování :class není registrovaná v knihovně všech chování.', + 'error_behavior_view_conflict' => 'The selected behaviors provide conflicting views (:view) and cannot be used together in a controller.', + 'error_behavior_config_conflict' => 'The selected behaviors provide conflicting configuration files (:file) and cannot be used together in a controller.', + 'error_behavior_view_file_not_found' => 'View template :view of the behavior :class cannot be found.', + 'error_behavior_config_file_not_found' => 'Configuration template :file of the behavior :class cannot be found.', + 'error_controller_exists' => 'Controller file already exists: :file.', + 'error_controller_name_invalid' => 'Invalid controller name format. The name can only contain digits and Latin letters. The first symbol should be a capital Latin letter.', + 'error_behavior_view_file_exists' => 'Controller view file already exists: :view.', + 'error_behavior_config_file_exists' => 'Behavior configuration file already exists: :file.', + 'error_save_file' => 'Error saving controller file: :file', + 'error_behavior_requires_base_model' => 'Behavior :behavior requires a base model class to be selected.', + 'error_model_doesnt_have_lists' => 'The selected model doesn\'t have any lists. Please create a list first.', + 'error_model_doesnt_have_forms' => 'The selected model doesn\'t have any forms. Please create a form first.', + ], + 'version' => [ + 'menu_label' => 'Verze', + 'no_records' => 'Žádné verze pluginu', + 'search' => 'Vyhledávání...', + 'tab' => 'Verze', + 'saved' => 'Verze byla úspěšně uložena.', + 'confirm_delete' => 'Opravdu chcete smazat vybranou verzi?', + 'tab_new_version' => 'Nová verze', + 'migration' => 'Migraci', + 'seeder' => 'Seeder', + 'custom' => 'Novou verzi', + 'apply_version' => 'Aplikovat tuto verzi', + 'applying' => 'Aplikování verze...', + 'rollback_version' => 'Vrátit na tuto verzi', + 'rolling_back' => 'Vracení zpět...', + 'applied' => 'Verze byla úspěšně aplikována.', + 'rolled_back' => 'Verze byla úspěšně vrácena zpět.', + 'hint_save_unapplied' => 'Byla uložena neaplikovaná verze. Neaplikované verze mohou být automaticky aplikovány po přihlášení do administrace jakýmkoli uživatelem a nebo pokud je databázová tabulka uložena v sekci Databáze.', + 'hint_rollback' => 'Vracení verze zpět rovněž vrátí zpět všechny novější verze. Neaplikované verze mohou být automaticky aplikovány po přihlášení do administrace jakýmkoli uživatelem a nebo pokud je databázová tabulka uložena v sekci Databáze.', + 'hint_apply' => 'Vracení verze zpět rovněž vrátí zpět všechny starší neaplikované verze.', + 'dont_show_again' => 'Znovu nezobrazovat', + 'save_unapplied_version' => 'Uložit neaplikovanou verzi', + ], + 'menu' => [ + 'menu_label' => 'Menu administrace', + 'tab' => 'Menu', + 'items' => 'Položky menu', + 'saved' => 'Menu byla úspěšně uložena.', + 'add_main_menu_item' => 'Přidat položku menu', + 'new_menu_item' => 'Položka menu', + 'add_side_menu_item' => 'Přidat pod-položku', + 'side_menu_item' => 'Side menu item', + 'property_label' => 'Popisek', + 'property_label_required' => 'Zadejte prosím popisek položky menu.', + 'property_url_required' => 'Zadejte prosím URL položky menu.', + 'property_url' => 'URL', + 'property_icon' => 'Ikona', + 'property_icon_required' => 'Vyberte prosím ikonu', + 'property_permissions' => 'Oprávnění', + 'property_order' => 'Pořadí', + 'property_order_invalid' => 'Zadejte prosím pořadí položky menu jako číslo.', + 'property_order_description' => 'Pořadí položek určuje jejich posloupnost v menu. Pokud není pořadí zadáno, automaticky se umístí položka nakonec. Výchozí hodnoty pořadí jsou násobky 100.', + 'property_attributes' => 'HTML attributy', + 'property_code' => 'Kód', + 'property_code_invalid' => 'Kód položky může obsahovat pouze písmena a číslice', + 'property_code_required' => 'Zadejte prosím kód položky menu.', + 'error_duplicate_main_menu_code' => 'Kód položky hlavního menu je duplicitní: \':code\'.', + 'error_duplicate_side_menu_code' => 'Kód položky postranního menu je duplicitní: \':code\'.', + ], + 'localization' => [ + 'menu_label' => 'Lokalizace', + 'language' => 'Zkratka jazyka', + 'strings' => 'Řetězce', + 'confirm_delete' => 'Opravdu chcete smazat soubor s překladem?', + 'tab_new_language' => 'Nový jazyk', + 'no_records' => 'Žádné jazyky nenalezeny', + 'saved' => 'Soubor s překladem byl úspěšně uložen.', + 'error_cant_load_file' => 'Nepodařilo se načíst požadovaný soubor protože neexistuje.', + 'error_bad_localization_file_contents' => 'Nepodařilo se načíst požadovaný soubor. Soubory s překladem mohou obsahovat pouze pole definující překlady řetězců.', + 'error_file_not_array' => 'Nepodařilo se načíst požadovaný soubor. Překladový soubor musí vrátit pole.', + 'save_error' => 'Chyba ukládání souboru \':name\'. Zkontrolujte prosím práva k zápisu.', + 'error_delete_file' => 'Chyba mazání překladového souboru.', + 'add_missing_strings' => 'Přidat chybějící řetězce', + 'copy' => 'Kopírovat', + 'add_missing_strings_label' => 'Vyberte jazyk ze kterého se zkopírují chybějící řetězce', + 'no_languages_to_copy_from' => 'Nejsou definovány žádné jazyky ze kterých bychom mohli zkopírovat řetězce.', + 'new_string_warning' => 'Nový řetězec nebo sekce', + 'structure_mismatch' => 'Struktura zdrojového jazykového souboru neodpovídá struktuře editovaného souboru. Některé nezávislé řetězce v editovaném souboru odpovídají sekcím ve zdrojovém souboru (nebo naopak) a nemohou být automaticky sloučeny.', + 'create_string' => 'Vytvořit nový řetězec', + 'string_key_label' => 'Klíč řetězce', + 'string_key_comment' => 'Zadejte klíč řetězce s použitím tečky jako oddělovače, například: plugin.search. Řetězec bude vytvořen ve výchozím jazykovém souboru pluginu.', + 'string_value' => 'Hodnota řetězce', + 'string_key_is_empty' => 'Musíte vyplnit klíč řetězce', + 'string_value_is_empty' => 'Musíte vyplnit hodnotu řetězce', + 'string_key_exists' => 'Takový klíč řetězce již existuje', + ], + 'permission' => [ + 'menu_label' => 'Oprávnění', + 'tab' => 'Oprávnění', + 'form_tab_permissions' => 'Oprávnění', + 'btn_add_permission' => 'Přidat oprávnění', + 'btn_delete_permission' => 'Smazat oprávnění', + 'column_permission_label' => 'Kód oprávnění', + 'column_permission_required' => 'Zadejte prosím kód oprávnění', + 'column_tab_label' => 'Název záložky', + 'column_tab_required' => 'Zadejte prosím název záložky oprávnění', + 'column_label_label' => 'Popisek', + 'column_label_required' => 'Zadejte prosím popisek oprávnění', + 'saved' => 'Oprávnění bylo úspěšně uloženo.', + 'error_duplicate_code' => 'Kód oprávnění je duplicitní: \':code\'.', + ], + 'yaml' => [ + 'save_error' => 'Chyba ukládání souboru \':name\'. Zkontrolujte prosím práva k zápisu.', + ], + 'common' => [ + 'error_file_exists' => 'Soubor již existuje: \':path\'.', + 'field_icon_description' => 'October používá ikony Font Autumn: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'Cílový adresář neexistuje: \':path\'.', + 'error_make_dir' => 'Chyba vytváření adresáře: \':name\'.', + 'error_dir_exists' => 'Adresář již existuje: \':path\'.', + 'template_not_found' => 'Soubor šablony nebyl nalezen: \':name\'.', + 'error_generating_file' => 'Chyba vytváření souboru: \':path\'.', + 'error_loading_template' => 'Chyba načtení souboru šablony: \':name\'.', + 'select_plugin_first' => 'Nejdříve vyberte plugin. Pro zobrazení všech pluginů klikněte na symbol > na vrchu levého menu.', + 'plugin_not_selected' => 'Není vybrán žádný plugin', + 'add' => 'Přidat', + ], + 'migration' => [ + 'entity_name' => 'Migrace', + 'error_version_invalid' => 'The version should be specified in format 1.0.1', + 'field_version' => 'Verze', + 'field_description' => 'Popis migrace', + 'field_code' => 'Kód migrace', + 'save_and_apply' => 'Uložit a aplikovat', + 'error_version_exists' => 'Tato verze migrace již existuje.', + 'error_script_filename_invalid' => 'The migration script file name can contain only Latin letters, digits and underscores. The name should start with a Latin letter and could not contain spaces.', + 'error_cannot_change_version_number' => 'Cannot change version number for an applied version.', + 'error_file_must_define_class' => 'Migration code should define a migration or seeder class. Leave the code field blank if you only want to update the version number.', + 'error_file_must_define_namespace' => 'Migration code should define a namespace. Leave the code field blank if you only want to update the version number.', + 'no_changes_to_save' => 'Žádné změny k uložení.', + 'error_namespace_mismatch' => 'Migrační kód by měl používat jmenný prostor pluginu: :namespace', + 'error_migration_file_exists' => 'Migrační soubor :file již existuje. Zadejte prosím jiný název třídy.', + 'error_cant_delete_applied' => 'This version has already been applied and cannot be deleted. Please rollback the version first.', + ], + 'components' => [ + 'list_title' => 'Record list', + 'list_description' => 'Displays a list of records for a selected model', + 'list_page_number' => 'Číslo stránky', + 'list_page_number_description' => 'This value is used to determine what page the user is on.', + 'list_records_per_page' => 'Records per page', + 'list_records_per_page_description' => 'Number of records to display on a single page. Leave empty to disable pagination.', + 'list_records_per_page_validation' => 'Invalid format of the records per page value. The value should be a number.', + 'list_no_records' => 'No records message', + 'list_no_records_description' => 'Message to display in the list in case if there are no records. Used in the default component\'s partial.', + 'list_no_records_default' => 'Žádné záznamy nebyly nalezeny', + 'list_sort_column' => 'Sort by column', + 'list_sort_column_description' => 'Model column the records should be ordered by', + 'list_sort_direction' => 'Směr', + 'list_display_column' => 'Display column', + 'list_display_column_description' => 'Column to display in the list. Used in the default component\'s partial.', + 'list_display_column_required' => 'Please select a display column.', + 'list_details_page' => 'Details page', + 'list_details_page_description' => 'Page to display record details.', + 'list_details_page_no' => '--no details page--', + 'list_sorting' => 'Řazení', + 'list_pagination' => 'Stránkování', + 'list_order_direction_asc' => 'Vzestupně', + 'list_order_direction_desc' => 'Sestupně', + 'list_model' => 'Modelová třída', + 'list_scope' => 'Scope', + 'list_scope_description' => 'Optional model scope to fetch the records', + 'list_scope_default' => '--select a scope, optional--', + 'list_details_page_link' => 'Link to the details page', + 'list_details_key_column' => 'Details key column', + 'list_details_key_column_description' => 'Model column to use as a record identifier in the details page links.', + 'list_details_url_parameter' => 'URL parameter name', + 'list_details_url_parameter_description' => 'Name of the details page URL parameter which takes the record identifier.', + 'details_title' => 'Record details', + 'details_description' => 'Displays record details for a selected model', + 'details_model' => 'Modelová třída', + 'details_identifier_value' => 'Identifier value', + 'details_identifier_value_description' => 'Identifier value to load the record from the database. Specify a fixed value or URL parameter name.', + 'details_identifier_value_required' => 'The identifier value is required', + 'details_key_column' => 'Key column', + 'details_key_column_description' => 'Model column to use as a record identifier for fetching the record from the database.', + 'details_key_column_required' => 'The key column name is required', + 'details_display_column' => 'Display column', + 'details_display_column_description' => 'Model column to display on the details page. Used in the default component\'s partial.', + 'details_display_column_required' => 'Please select a display column.', + 'details_not_found_message' => 'Not found message', + 'details_not_found_message_description' => 'Message to display if the record is not found. Used in the default component\'s partial.', + 'details_not_found_message_default' => 'Záznam nebyl nalezen', + ], +]; diff --git a/plugins/rainlab/builder/lang/en.json b/plugins/rainlab/builder/lang/en.json new file mode 100644 index 0000000..0827897 --- /dev/null +++ b/plugins/rainlab/builder/lang/en.json @@ -0,0 +1,6 @@ +{ + "Builder": "Builder", + "Provides visual tools for building October plugins.": "Provides visual tools for building October plugins.", + "Options Method": "Options Method", + "Request options from this method name defined on the model or as a static method (Class::method).": "Request options from this method name defined on the model or as a static method (Class::method)." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/en/lang.php b/plugins/rainlab/builder/lang/en/lang.php new file mode 100644 index 0000000..598aeaf --- /dev/null +++ b/plugins/rainlab/builder/lang/en/lang.php @@ -0,0 +1,824 @@ + [ + 'add' => 'Create Plugin', + 'no_records' => 'No plugins found', + 'no_name' => 'No Name', + 'search' => 'Search...', + 'filter_description' => 'Display all plugins or only your plugins.', + 'settings' => 'Settings', + 'entity_name' => 'Plugin', + 'field_name' => 'Name', + 'field_author' => 'Author', + 'field_description' => 'Description', + 'field_icon' => 'Plugin Icon', + 'field_plugin_namespace' => 'Plugin Namespace', + 'field_author_namespace' => 'Author Namespace', + 'field_namespace_description' => 'Namespace can contain only Latin letters and digits and should start with a Latin letter. Example plugin namespace: Blog', + 'field_author_namespace_description' => 'You cannot change the namespaces with Builder after you create the plugin. Example author namespace: JohnSmith', + 'tab_general' => 'General Parameters', + 'tab_description' => 'Description', + 'field_homepage' => 'Plugin Homepage (URL)', + 'no_description' => 'No description provided for this plugin', + 'error_settings_not_editable' => 'Settings of this plugin cannot be edited with Builder.', + 'update_hint' => 'You can edit localized plugin\'s name and description on the Localization tab.', + 'manage_plugins' => 'Create and edit plugins', + ], + 'author_name' => [ + 'title' => 'Author Name', + 'description' => 'Default author name to use for your new plugins. The author name is not fixed - you can change it in the plugins configuration at any time.', + ], + 'author_namespace' => [ + 'title' => 'Author Namespace', + 'description' => 'If you develop for the Marketplace, the namespace should match the author code and cannot be changed. Refer to the documentation for details.', + ], + 'config' => [ + 'use_table_comments_label' => 'Include Table Comments', + 'use_table_comments_comment' => 'Show comment field when defining table columns.', + ], + 'database' => [ + 'menu_label' => 'Database', + 'no_records' => 'No tables found', + 'search' => 'Search...', + 'confirmation_delete_multiple' => 'Delete the selected tables?', + 'field_name' => 'Table Name', + 'tab_columns' => 'Columns', + 'column_name_name' => 'Column', + 'column_name_required' => 'Please provide the column name', + 'column_name_type' => 'Type', + 'column_type_required' => 'Please select the column type', + 'column_name_length' => 'Length', + 'column_validation_length' => 'The Length value should be integer or specified as precision and scale (10,2) for decimal columns. Spaces are not allowed in the length column.', + 'column_validation_title' => 'Only digits, lower-case Latin letters and underscores are allowed in column names', + 'column_name_unsigned' => 'Unsigned', + 'column_name_nullable' => 'Nullable', + 'column_auto_increment' => 'AUTOINCR', + 'column_default' => 'Default', + 'column_comment' => 'Comment', + 'column_auto_primary_key' => 'PK', + 'tab_new_table' => 'New Table', + 'btn_add_column' => 'Add Column', + 'btn_delete_column' => 'Delete Column', + 'btn_add_id' => 'Add ID', + 'btn_add_timestamps' => 'Add Timestamps', + 'btn_add_soft_deleting' => 'Add Soft Deletes', + 'id_exists' => 'ID column already exists in the table.', + 'timestamps_exist' => 'created_at and deleted_at columns already exist in the table.', + 'soft_deleting_exist' => 'deleted_at column already exists in the table.', + 'confirm_delete' => 'Delete the table?', + 'error_enum_not_supported' => 'The table contains column(s) with type "enum" which is not currently supported by the Builder.', + 'error_table_name_invalid_prefix' => 'Table name should start with the plugin prefix: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Invalid table name. Table names should contain only Latin letters, digits and underscores. Names should start with a Latin letter and could not contain spaces.', + 'error_table_duplicate_column' => 'Duplicate column name: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => 'An auto-increment column cannot be a part of a compound primary key.', + 'error_table_mutliple_auto_increment' => 'The table cannot contain multiple auto-increment columns.', + 'error_table_auto_increment_non_integer' => 'Auto-increment columns should have integer type.', + 'error_table_decimal_length' => 'The Length parameter for :type type should be in format \'10,2\', without spaces.', + 'error_table_length' => 'The Length parameter for :type type should be specified as integer.', + 'error_unsigned_type_not_int' => 'Error in the \':column\' column. The Unsigned flag can be applied only to integer type columns.', + 'error_integer_default_value' => 'Invalid default value for the integer column \':column\'. The allowed formats are \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Invalid default value for the decimal or double column \':column\'. The allowed formats are \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Invalid default value for the boolean column \':column\'. The allowed values are \'0\' and \'1\', or \'true\' and \'false\'.', + 'error_unsigned_negative_value' => 'The default value for the unsigned column \':column\' can\'t be negative.', + 'error_table_already_exists' => 'The table \':name\' already exists in the database.', + 'error_table_name_too_long' => 'The table name should not be longer than 64 characters.', + 'error_column_name_too_long' => 'The column name \':column\' is too long. Column names should not be longer than 64 characters.', + ], + 'model' => [ + 'menu_label' => 'Models', + 'entity_name' => 'Model', + 'no_records' => 'No models found', + 'search' => 'Search...', + 'add' => 'Add...', + 'forms' => 'Forms', + 'lists' => 'Lists', + 'field_class_name' => 'Class Name', + 'field_database_table' => 'Database Table', + 'field_add_timestamps' => 'Add Timestamp Support', + 'field_add_timestamps_description' => 'The database table must have created_at and updated_at columns.', + 'field_add_soft_deleting' => 'Add Soft Deletes', + 'field_add_soft_deleting_description' => 'The database table must have deleted_at column.', + 'error_class_name_exists' => 'Model file already exists for the specified class name: :path', + 'error_timestamp_columns_must_exist' => 'The database table must have created_at and updated_at columns.', + 'error_deleted_at_column_must_exist' => 'The database table must have deleted_at column.', + 'add_form' => 'Add Form', + 'add_list' => 'Add List', + ], + 'form' => [ + 'saved' => 'Form saved', + 'confirm_delete' => 'Delete the form?', + 'tab_new_form' => 'New Form', + 'btn_add_database_fields' => 'Add Database Fields', + 'property_label_title' => 'Label', + 'property_label_required' => 'Please specify the control label.', + 'property_span_title' => 'Span', + 'property_comment_title' => 'Comment', + 'property_comment_above_title' => 'Comment Above', + 'property_default_title' => 'Default', + 'property_checked_default_title' => 'Checked by Default', + 'property_css_class_title' => 'CSS Class', + 'property_css_class_description' => 'Optional CSS class to assign to the field container.', + 'property_disabled_title' => 'Disabled', + 'property_read_only_title' => 'Read Only', + 'property_hidden_title' => 'Hidden', + 'property_required_title' => 'Required', + 'property_field_name_title' => 'Field Name', + 'property_placeholder_title' => 'Placeholder', + 'property_default_from_title' => 'Default From', + 'property_stretch_title' => 'Stretch', + 'property_stretch_description' => 'Specifies if this field stretches to fit the parent height.', + 'property_context_title' => 'Context', + 'property_context_description' => 'Specifies what form context should be used when displaying the field.', + 'property_context_create' => 'Create', + 'property_context_update' => 'Update', + 'property_context_preview' => 'Preview', + 'property_dependson_title' => 'Depends On', + 'property_trigger_action' => 'Action', + 'property_trigger_show' => 'Show', + 'property_trigger_hide' => 'Hide', + 'property_trigger_enable' => 'Enable', + 'property_trigger_disable' => 'Disable', + 'property_trigger_empty' => 'Empty', + 'property_trigger_field' => 'Field', + 'property_trigger_field_description' => 'Defines the other field name that will trigger the action.', + 'property_trigger_condition' => 'Condition', + 'property_trigger_condition_description' => 'Determines the condition the specified field should satisfy for the condition to be considered "true". Supported values: checked, unchecked, value[somevalue].', + 'property_trigger_condition_checked' => 'Checked', + 'property_trigger_condition_unchecked' => 'Unchecked', + 'property_trigger_condition_somevalue' => 'value[enter-the-value-here]', + 'property_preset_title' => 'Preset', + 'property_preset_description' => 'Allows the field value to be initially set by the value of another field, converted using the input preset converter.', + 'property_preset_field' => 'Field', + 'property_preset_field_description' => 'Defines the other field name to source the value from.', + 'property_preset_type' => 'Type', + 'property_preset_type_description' => 'Specifies the conversion type', + 'property_attributes_title' => 'Attributes', + 'property_attributes_description' => 'Custom HTML attributes to add to the form field element.', + 'property_container_attributes_title' => 'Container Attributes', + 'property_container_attributes_description' => 'Custom HTML attributes to add to the form field container element.', + 'property_group_advanced' => 'Advanced', + 'property_dependson_description' => 'A list of other field names this field depends on, when the other fields are modified, this field will update. One field per line.', + 'property_trigger_title' => 'Trigger', + 'property_trigger_description' => 'Allows to change elements attributes such as visibility or value, based on another elements\' state.', + 'property_default_from_description' => 'Takes the default value from the value of another field.', + 'property_field_name_required' => 'The field name is required', + 'property_field_name_regex' => 'The field name can contain only Latin letters, digits, underscores, dashes and square brackets.', + 'property_attributes_size' => 'Size', + 'property_attributes_size_tiny' => 'Tiny', + 'property_attributes_size_small' => 'Small', + 'property_attributes_size_large' => 'Large', + 'property_attributes_size_huge' => 'Huge', + 'property_attributes_size_giant' => 'Giant', + 'property_comment_position' => 'Comment Position', + 'property_comment_position_above' => 'Above', + 'property_comment_position_below' => 'Below', + 'property_hint_path' => 'Partial Path', + 'property_hint_path_description' => 'Path to a partial file that contains the hint text. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_hint.php', + 'property_hint_path_required' => 'Please enter the hint partial path', + 'property_partial_path' => 'Partial path', + 'property_partial_path_description' => 'Path to a partial file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_partial.php', + 'property_partial_path_required' => 'Please enter the partial path', + 'property_code_language' => 'Language', + 'property_code_theme' => 'Theme', + 'property_theme_use_default' => 'Use Default Theme', + 'property_group_code_editor' => 'Code Editor', + 'property_gutter' => 'Gutter', + 'property_gutter_show' => 'Visible', + 'property_gutter_hide' => 'Hidden', + 'property_wordwrap' => 'Word Wrap', + 'property_wordwrap_wrap' => 'Wrap', + 'property_wordwrap_nowrap' => 'Don\'t wrap', + 'property_fontsize' => 'Font Size', + 'property_codefolding' => 'Code Folding', + 'property_codefolding_manual' => 'Manual', + 'property_codefolding_markbegin' => 'Mark Begin', + 'property_codefolding_markbeginend' => 'Mark Begin and End', + 'property_autoclosing' => 'Auto Closing', + 'property_enabled' => 'Enabled', + 'property_disabled' => 'Disabled', + 'property_soft_tabs' => 'Soft tabs', + 'property_tab_size' => 'Tab size', + 'property_readonly' => 'Read only', + 'property_use_default' => 'Use default settings', + 'property_options' => 'Options', + 'property_options_method' => 'Options Method', + 'property_options_method_description' => 'Request options from this method name defined on the model or as a static method (Class::method).', + 'property_prompt' => 'Prompt', + 'property_prompt_description' => 'Text to display for the create button.', + 'property_prompt_default' => 'Add new item', + 'property_datepicker_mode' => 'Mode', + 'property_datepicker_mode_date' => 'Date', + 'property_datepicker_mode_datetime' => 'Date and Time', + 'property_datepicker_mode_time' => 'Time', + 'property_datepicker_min_date' => 'Min Date', + 'property_datepicker_min_date_description' => 'The minimum/earliest date that can be selected. This may be any string accepted by Carbon. Leave empty for no minimum date.', + 'property_datepicker_max_date' => 'Max Date', + 'property_datepicker_max_date_description' => 'The maximum/latest date that can be selected. This may be any string accepted by Carbon. Leave empty for no maximum date.', + 'property_datepicker_year_range' => 'Year Range', + 'property_datepicker_year_range_description' => 'Number of years either side (eg 10) or array of upper/lower range (eg [1900,2015]). Leave empty for the default value (10).', + 'property_datepicker_year_range_invalid_format' => 'Invalid year range format. Use number (eg "10") or array of upper/lower range (eg "[1900,2015]")', + 'property_datepicker_first_day' => 'First Day', + 'property_datepicker_first_day_description' => 'First day of the week (0 - 7) where 0 is Sunday.', + 'property_datepicker_first_day_regex' => 'First day must be a number', + 'property_datepicker_twelve_hour' => 'Twelve Hour', + 'property_datepicker_twelve_hour_description' => 'Display a 12-hour clock for selecting time.', + 'property_datepicker_show_week_number' => 'Show Week Number', + 'property_datepicker_show_week_number_description' => 'Show week numbers at head of row.', + 'property_datepicker_format' => 'Format', + 'property_datepicker_year_format_description' => 'Define a custom date format. The default format is "Y-m-d"', + 'property_fileupload_mode' => 'Mode', + 'property_fileupload_mode_file' => 'File', + 'property_fileupload_mode_image' => 'Image', + 'property_group_fileupload' => 'File Upload', + 'property_fileupload_image_width' => 'Image width', + 'property_fileupload_image_width_description' => 'Optional parameter - images will be resized to this width. Applies to Image mode only.', + 'property_fileupload_invalid_dimension' => 'Invalid dimension value - please enter a number.', + 'property_fileupload_image_height' => 'Image height', + 'property_fileupload_image_height_description' => 'Optional parameter - images will be resized to this height. Applies to Image mode only.', + 'property_fileupload_file_types' => 'File types', + 'property_fileupload_file_types_description' => 'Optional comma separated list of file extensions that are accepted by the uploader. Eg: zip,txt', + 'property_fileupload_mime_types' => 'MIME types', + 'property_fileupload_maxfilesize' => 'Max file size', + 'property_fileupload_maxfilesize_description' => 'File size in Mb that are accepted by the uploader, optional.', + 'property_fileupload_invalid_maxfilesize' => 'Invalid Max file size value', + 'property_fileupload_maxfiles' => 'Max files', + 'property_fileupload_invalid_maxfiles' => 'Invalid Max files value', + 'property_fileupload_maxfiles_description' => 'Maximum number of files allowed to be uploaded', + 'property_fileupload_mime_types_description' => 'Optional comma separated list of MIME types that are accepted by the uploader, either as file extensions or fully qualified names. Eg: bin,txt', + 'property_fileupload_use_caption' => 'Use caption', + 'property_fileupload_use_caption_description' => 'Allows a title and description to be set for the file.', + 'property_fileupload_thumb_options' => 'Thumbnail options', + 'property_fileupload_thumb_options_description' => 'Manages options for the automatically generated thumbnails. Applies only for the Image mode.', + 'property_fileupload_thumb_mode' => 'Mode', + 'property_fileupload_thumb_auto' => 'Auto', + 'property_fileupload_thumb_exact' => 'Exact', + 'property_fileupload_thumb_portrait' => 'Portrait', + 'property_fileupload_thumb_landscape' => 'Landscape', + 'property_fileupload_thumb_crop' => 'Crop', + 'property_fileupload_thumb_extension' => 'File extension', + 'property_name_from' => 'Name column', + 'property_name_from_description' => 'Relation column name to use for displaying a name.', + 'property_relation_select' => 'Select', + 'property_relation_select_description' => 'CONCAT multiple columns together for displaying a name', + 'property_relation_scope' => 'Scope', + 'property_relation_scope_description' => 'Specifies a query scope method that\'s defined in the related form model to always apply to the list query.', + 'property_description_from' => 'Description Column', + 'property_description_from_description' => 'Relation column name to use for displaying a description.', + 'property_recordfinder_title' => 'Prompt', + 'property_recordfinder_title_description' => 'Text to display in the title section of the popup.', + 'property_recordfinder_list' => 'List Configuration', + 'property_recordfinder_list_description' => 'A reference to a list column definition file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => 'Please provide a path to the list YAML file', + 'property_group_recordfinder' => 'Record Finder', + 'property_mediafinder_mode' => 'Mode', + 'property_mediafinder_mode_file' => 'File', + 'property_mediafinder_mode_image' => 'Image', + 'property_mediafinder_image_width_description' => 'If using image type, the preview image will be displayed to this width, optional.', + 'property_mediafinder_image_height_description' => 'If using image type, the preview image will be displayed to this height, optional.', + 'property_group_sensitive' => 'Sensitive', + 'property_group_taglist' => 'Tag List', + 'property_taglist_mode' => 'Mode', + 'property_taglist_mode_description' => 'Defines the format that this field\'s value is returned as', + 'property_taglist_mode_string' => 'String', + 'property_taglist_mode_array' => 'Array', + 'property_taglist_mode_relation' => 'Relation', + 'property_taglist_separator' => 'Separator', + 'property_taglist_separator_comma' => 'Commas', + 'property_taglist_separator_space' => 'Spaces', + 'property_taglist_options' => 'Predefined Tags', + 'property_taglist_custom_tags' => 'Custom Tags', + 'property_taglist_custom_tags_description' => 'Allow custom tags to be entered manually by the user.', + 'property_taglist_name_from' => 'Name From', + 'property_taglist_name_from_description' => 'Defines the relation model attribute displayed in the tag. Only used in "relation" mode.', + 'property_taglist_use_key' => 'Use Key', + 'property_taglist_use_key_description' => 'If checked, the tag list will use the key instead of the value for saving and reading data. Only used in "relation" mode.', + 'property_group_relation' => 'Relation', + 'property_relation_prompt' => 'Prompt', + 'property_relation_prompt_description' => 'Text to display when there is no available selections.', + 'property_empty_option' => 'Empty Option', + 'property_empty_option_description' => 'The empty option corresponds to the empty selection, but unlike the placeholder it can be reselected.', + 'property_show_search' => 'Show Search', + 'property_show_search_description' => 'Enables the search feature for this dropdown.', + 'property_title_from' => 'Title From', + 'property_title_from_description' => 'Specify a child field name to use the value of that field as the title for each repeater item.', + 'property_min_items' => 'Min Items', + 'property_min_items_description' => 'Minimum number of items that can be selected.', + 'property_min_items_integer' => 'Min items must be a positive integer.', + 'property_max_items' => 'Max Items', + 'property_max_items_description' => 'Maximum number of items that can be selected.', + 'property_max_items_integer' => 'Max items must be a positive integer.', + 'property_display_mode' => 'Display Mode', + 'property_display_mode_description' => 'Defines the display mode visually.', + 'control_group_standard' => 'Standard', + 'control_group_widgets' => 'Widgets', + 'control_group_ui' => 'UI', + 'click_to_add_control' => 'Add control', + 'loading' => 'Loading...', + 'control_text' => 'Text', + 'control_text_description' => 'Single line text box', + 'control_email' => 'Email', + 'control_email_description' => 'Single line text box that takes email addresses only', + 'control_password' => 'Password', + 'control_password_description' => 'Single line password text field', + 'control_checkbox' => 'Checkbox', + 'control_checkbox_description' => 'Single checkbox', + 'control_switch' => 'Switch', + 'control_switch_description' => 'Single light switch input, an alternative for checkbox', + 'control_textarea' => 'Text Area', + 'control_textarea_description' => 'Multiline text box with controllable height', + 'control_dropdown' => 'Dropdown', + 'control_dropdown_description' => 'Dropdown list with static or dynamic options', + 'control_balloon-selector' => 'Balloon Selector', + 'control_balloon-selector_description' => 'List where only one item can be selected at a time with static or dynamic options', + 'control_unknown' => 'Unknown control type: :type', + 'control_repeater' => 'Repeater', + 'control_repeater_description' => 'Outputs a repeating set of form controls', + 'property_repeater_show_reorder' => 'Show Reorder', + 'property_repeater_show_reorder_description' => 'Displays an interface for sorting items.', + 'property_repeater_show_duplicate' => 'Show Duplicate', + 'property_repeater_show_duplicate_description' => 'Displays an interface for cloning items.', + 'control_nestedform' => 'Nested Form', + 'control_nestedform_description' => 'Outputs a nested set of form controls', + 'property_nestedform_show_panel' => 'Show Panel', + 'property_nestedform_show_panel_description' => 'Places the form inside a panel container.', + 'property_nestedform_default_create' => 'Default Create', + 'property_nestedform_default_create_description' => 'If a related record is not found, attempt to create one.', + 'control_number' => 'Number', + 'control_number_description' => 'Single line text box that takes numbers only', + 'property_min' => 'Minimum', + 'property_min_description' => 'The client-side minimum value.', + 'property_min_number' => 'Minimum must be a number', + 'property_max' => 'Maximum', + 'property_max_description' => 'The client-side maximum value.', + 'property_max_number' => 'Maximum must be a number', + 'property_step' => 'Step', + 'property_step_description' => 'The client-side step increment.', + 'property_step_number' => 'Step must be a number', + 'control_hint' => 'Hint', + 'control_hint_description' => 'Outputs a partial contents in a box that can be hidden by the user', + 'control_partial' => 'Partial', + 'control_partial_description' => 'Outputs a partial contents', + 'control_section' => 'Section', + 'control_section_description' => 'Displays a form section with heading and subheading', + 'control_ruler' => 'Horizontal Rule', + 'control_ruler_description' => 'Displays a a horizontal rule to break up the contents', + 'control_radio' => 'Radio List', + 'control_radio_description' => 'A list of radio options, where only one item can be selected at a time', + 'control_radio_option_1' => 'Option 1', + 'control_radio_option_2' => 'Option 2', + 'control_checkboxlist' => 'Checkbox List', + 'control_checkboxlist_description' => 'A list of checkboxes, where multiple items can be selected', + 'property_quickselect' => 'Quick Select', + 'property_quickselect_description' => 'Show the quick selection buttons.', + 'property_inline_options' => 'Inline Options', + 'property_inline_options_description' => 'Display the options side-by-side instead of stacked, when less than 10 options.', + 'control_codeeditor' => 'Code Editor', + 'control_codeeditor_description' => 'Plaintext editor for formatted code or markup', + 'control_colorpicker' => 'Color Picker', + 'control_colorpicker_description' => 'A field for selecting a hexadecimal color value', + 'property_group_colorpicker' => 'Color Picker', + 'property_available_colors' => 'Available Colors', + 'property_available_colors_description' => 'List of available colors in hex format (#FF0000). Leave empty for the default color set. Enter one value per line.', + 'property_allow_empty' => 'Allow Empty', + 'property_allow_empty_description' => 'Allow empty input values.', + 'property_allow_custom' => 'Allow Custom', + 'property_allow_custom_description' => 'Allow selection of a custom color.', + 'property_show_alpha' => 'Show Alpha', + 'property_show_alpha_description' => 'Displays an opacity slider and sets an 8-digit hex code.', + 'property_show_input' => 'Show Input', + 'property_show_input_description' => 'Displays a text input next to the color picker and disables available colors.', + 'control_datatable' => 'Data Table', + 'control_datatable_description' => 'Renders an editable table of records, formatted as a grid', + 'property_group_datatable' => 'Data Table', + 'property_columns' => 'Columns', + 'property_columns_description' => 'Column configuration for the table', + 'property_datatable_type' => 'Type', + 'property_datatable_code' => 'Code', + 'property_datatable_code_regex' => 'A unique code is required for the column', + 'property_datatable_title' => 'Title', + 'property_datatable_width' => 'Width', + 'property_datatable_width_regex' => 'Width must be a number in pixels', + 'property_datatable_adding' => 'Allow Adding', + 'property_datatable_adding_description' => 'Allow records to be added.', + 'property_datatable_deleting' => 'Allow Adding', + 'property_datatable_deleting_description' => 'Allow records to be deleted.', + 'property_datatable_searching' => 'Searching', + 'property_datatable_searching_description' => 'Allow records to be searched.', + 'control_datepicker' => 'Date Picker', + 'control_datepicker_description' => 'Text field used for selecting date and times', + 'property_group_datepicker' => 'Date Picker', + 'control_richeditor' => 'Rich Editor', + 'control_richeditor_description' => 'Visual editor for rich formatted text, also known as a WYSIWYG editor', + 'control_pagefinder' => 'Page Finder', + 'control_pagefinder_description' => 'Renders a field for selecting a page link', + 'property_pagefinder_single_mode' => 'Single Mode', + 'property_pagefinder_single_mode_description' => 'Only allows items to be selected that resolve to a single URL.', + 'control_sensitive' => 'Sensitive', + 'control_sensitive_description' => 'Renders a revealable password field that can be used for sensitive information, such as API keys or secrets.', + 'allow_copy' => 'Allow Copy', + 'allow_copy_description' => 'Adds a copy action to the field, allowing the user to copy the password without revealing it.', + 'hidden_placeholder' => 'Hidden Placeholder', + 'hidden_placeholder_description' => 'Sets a placeholder string to emulate the unrevealed value. You can change this to a long or short string to emulate a different length.', + 'hide_on_tab_change' => 'Hide on Tab Change', + 'hide_on_tab_change_description' => 'Hides the field again if the user navigates to a different tab.', + 'property_group_rich_editor' => 'Rich Editor', + 'property_richeditor_toolbar_buttons' => 'Toolbar Buttons', + 'property_richeditor_toolbar_buttons_description' => 'Which buttons to show on the editor toolbar.', + 'control_markdown' => 'Markdown Editor', + 'control_markdown_description' => 'Basic editor for Markdown formatted text', + 'property_side_by_side' => 'Side By Side', + 'property_side_by_side_description' => 'Enables the side-by-side display mode by default', + 'control_taglist' => 'Tag List', + 'control_taglist_description' => 'Field for inputting a list of tags', + 'control_fileupload' => 'File Upload', + 'control_fileupload_description' => 'File uploader for images or regular files', + 'control_recordfinder' => 'Record Finder', + 'control_recordfinder_description' => 'Field with details of a related record with the record search feature', + 'control_mediafinder' => 'Media Finder', + 'control_mediafinder_description' => 'Field for selecting an item from the Media Manager library', + 'control_relation' => 'Relation', + 'control_relation_description' => 'Displays either a dropdown or checkbox list for selecting a related record', + 'control_widget_type' => 'Widget Type', + 'error_file_name_required' => 'Please enter the form file name.', + 'error_file_name_invalid' => 'The file name can contain only Latin letters, digits, underscores, dots and hashes.', + 'span_left' => 'Left', + 'span_right' => 'Right', + 'span_full' => 'Full', + 'span_auto' => 'Auto', + 'class_mode_tip' => 'Tip', + 'class_mode_info' => 'Info', + 'class_mode_warning' => 'Warning', + 'class_mode_danger' => 'Danger', + 'class_mode_success' => 'Success', + 'display_mode_builder' => 'Builder', + 'display_mode_accordion' => 'Accordion', + 'empty_tab' => 'Empty tab', + 'confirm_close_tab' => 'The tab contains controls which will be deleted. Continue?', + 'tab' => 'Form tab', + 'tab_title' => 'Title', + 'controls' => 'Controls', + 'property_tab_title_required' => 'The tab title is required.', + 'tabs_primary' => 'Primary tabs', + 'tabs_secondary' => 'Secondary tabs', + 'tab_stretch' => 'Stretch', + 'tab_stretch_description' => 'Specifies if this tabs container stretches to fit the parent height.', + 'tab_css_class' => 'CSS class', + 'tab_css_class_description' => 'Assigns a CSS class to the tabs container.', + 'tab_name_template' => 'Tab %s', + 'tab_already_exists' => 'Tab with the specified title already exists.', + ], + 'list' => [ + 'tab_new_list' => 'New List', + 'saved' => 'List saved', + 'confirm_delete' => 'Delete the list?', + 'tab_columns' => 'Columns', + 'btn_add_column' => 'Add Column', + 'btn_delete_column' => 'Delete Column', + 'column_dbfield_label' => 'Field', + 'column_dbfield_required' => 'Please enter the model field', + 'column_name_label' => 'Label', + 'column_label_required' => 'Please provide the column label', + 'column_type_label' => 'Type', + 'column_type_required' => 'Please provide the column type', + 'column_type_text' => 'Text', + 'column_type_number' => 'Number', + 'column_type_switch' => 'Switch', + 'column_type_datetime' => 'Datetime', + 'column_type_date' => 'Date', + 'column_type_time' => 'Time', + 'column_type_timesince' => 'Time Since', + 'column_type_timetense' => 'Time Tense', + 'column_type_select' => 'Select', + 'column_type_partial' => 'Partial', + 'column_label_default' => 'Default', + 'column_label_searchable' => 'Search', + 'column_label_sortable' => 'Sort', + 'column_label_invisible' => 'Invisible', + 'column_label_select' => 'Select', + 'column_label_relation' => 'Relation', + 'column_label_css_class' => 'CSS Class', + 'column_label_width' => 'Width', + 'column_label_path' => 'Path', + 'column_label_format' => 'Format', + 'column_label_value_from' => 'Value From', + 'error_duplicate_column' => 'Duplicate column field name: \':column\'.', + 'btn_add_database_columns' => 'Add Database Columns', + 'all_database_columns_exist' => 'All database columns are already defined in the list', + ], + 'controller' => [ + 'menu_label' => 'Controllers', + 'no_records' => 'No plugin controllers found', + 'controller' => 'Controller', + 'behaviors' => 'Behaviors', + 'new_controller' => 'New Controller', + 'error_controller_has_no_behaviors' => 'The controller doesn\'t have configurable behaviors.', + 'error_invalid_yaml_configuration' => 'Error loading behavior configuration file: :file', + 'behavior_form_controller' => 'Form Controller Behavior', + 'behavior_form_controller_description' => 'Adds form functionality to a back-end page. The behavior provides three pages called Create, Update and Preview.', + 'property_behavior_form_placeholder' => '--select form--', + 'property_behavior_form_name' => 'Name', + 'property_behavior_form_name_description' => 'The name of the object being managed by this form', + 'property_behavior_form_name_required' => 'Please enter the form name', + 'property_behavior_form_file' => 'Form Configuration', + 'property_behavior_form_file_description' => 'Reference to a form field definition file', + 'property_behavior_form_file_required' => 'Please enter a path to the form configuration file', + 'property_behavior_form_model_class' => 'Model Class', + 'property_behavior_form_model_class_description' => 'A model class name, the form data is loaded and saved against this model.', + 'property_behavior_form_model_class_required' => 'Please select a model class', + 'property_behavior_form_default_redirect' => 'Default Redirect', + 'property_behavior_form_default_redirect_description' => 'A page to redirect to by default when the form is saved or cancelled.', + 'property_behavior_form_create' => 'Create Record Page', + 'property_behavior_form_redirect' => 'Redirect', + 'property_behavior_form_redirect_description' => 'A page to redirect to when a record is created.', + 'property_behavior_form_redirect_close' => 'Close Redirect', + 'property_behavior_form_redirect_close_description' => 'A page to redirect to when a record is created and the close post variable is sent with the request.', + 'property_behavior_form_flash_save' => 'Save Flash Message', + 'property_behavior_form_flash_save_description' => 'Flash message to display when record is saved.', + 'property_behavior_form_page_title' => 'Page Title', + 'property_behavior_form_update' => 'Update Record Page', + 'property_behavior_form_update_redirect' => 'Redirect', + 'property_behavior_form_create_redirect_description' => 'A page to redirect to when a record is saved.', + 'property_behavior_form_flash_delete' => 'Delete Flash Message', + 'property_behavior_form_flash_delete_description' => 'Flash message to display when record is deleted.', + 'property_behavior_form_preview' => 'Preview Record Page', + 'behavior_list_controller' => 'List Controller Behavior', + 'behavior_list_controller_description' => 'Provides the sortable and searchable list with optional links on its records. The behavior automatically creates the controller action "index".', + 'property_behavior_list_title' => 'List Title', + 'property_behavior_list_title_required' => 'Please enter the list title', + 'property_behavior_list_placeholder' => '--select list--', + 'property_behavior_list_model_class' => 'Model Class', + 'property_behavior_list_model_class_description' => 'A model class name, the list data is loaded from this model.', + 'property_behavior_form_model_class_placeholder' => '--select model--', + 'property_behavior_list_model_class_required' => 'Please select a model class', + 'property_behavior_list_model_placeholder' => '--select model--', + 'property_behavior_list_file' => 'List Configuration File', + 'property_behavior_list_file_description' => 'Reference to a list definition file', + 'property_behavior_list_file_required' => 'Please enter a path to the list configuration file', + 'property_behavior_list_record_url' => 'Record URL', + 'property_behavior_list_record_url_description' => 'Link each list record to another page. Eg: users/update:id. The :id part is replaced with the record identifier.', + 'property_behavior_list_no_records_message' => 'No Records Message', + 'property_behavior_list_no_records_message_description' => 'A message to display when no records are found', + 'property_behavior_list_recs_per_page' => 'Records Per Page', + 'property_behavior_list_recs_per_page_description' => 'Records to display per page, use 0 for no pages. Default: 0', + 'property_behavior_list_recs_per_page_regex' => 'Records per page should be an integer value', + 'property_behavior_list_show_setup' => 'Show Setup Button', + 'property_behavior_list_structure' => 'Structure', + 'property_behavior_list_show_sorting' => 'Show Sorting', + 'property_behavior_list_show_reorder' => 'Show Reorder', + 'property_behavior_list_max_depth' => 'Max Depth', + 'property_behavior_list_max_depth_description' => 'Maximum depth - use 0 for unlimited depth. This field must be set to activate the structure.', + 'property_behavior_list_max_depth_regex' => 'Max depth should be an integer value', + 'property_behavior_list_drag_row' => 'Draw Row', + 'property_behavior_list_default_sort' => 'Default Sorting', + 'property_behavior_form_ds_column' => 'Column', + 'property_behavior_form_ds_direction' => 'Direction', + 'property_behavior_form_ds_asc' => 'Ascending', + 'property_behavior_form_ds_desc' => 'Descending', + 'property_behavior_list_show_checkboxes' => 'Show Checkboxes', + 'property_behavior_list_onclick' => 'On Click Handler', + 'property_behavior_list_onclick_description' => 'Custom JavaScript code to execute when clicking on a record.', + 'property_behavior_list_show_tree' => 'Show Tree', + 'property_behavior_list_show_tree_description' => 'Displays a tree hierarchy for parent/child records.', + 'property_behavior_list_tree_expanded' => 'Tree Expanded', + 'property_behavior_list_tree_expanded_description' => 'Determines if tree nodes should be expanded by default.', + 'property_behavior_list_toolbar' => 'Toolbar', + 'property_behavior_list_toolbar_buttons' => 'Buttons Partial', + 'property_behavior_list_toolbar_buttons_description' => 'Reference to a controller partial file with the toolbar buttons. Eg: list_toolbar', + 'property_behavior_list_search' => 'Search', + 'property_behavior_list_search_prompt' => 'Search Prompt', + 'property_behavior_list_filter' => 'Filter Configuration', + 'behavior_import_export_controller' => 'Import Export Controller Behavior', + 'behavior_import_export_controller_description' => 'Provides features for importing and exporting records. The behavior automatically creates the controller actions "import" and "export".', + 'property_group_import' => 'Import', + 'property_group_export' => 'Export', + 'property_behavior_import_title' => 'Import Title', + 'property_behavior_export_title' => 'Export Title', + 'property_behavior_import_title_required' => 'Please enter a title', + 'property_behavior_import_model_class' => 'Import Model Class', + 'property_behavior_import_model_class_description' => 'A model class name for importing, extending the Backend\Models\ImportModel class.', + 'property_behavior_import_model_class_placeholder' => '--select model--', + 'property_behavior_export_model_class' => 'Export Model Class', + 'property_behavior_export_model_class_description' => 'A model class name for export, extending the Backend\Models\ExportModel class.', + 'property_behavior_import_model_class_required' => 'Please select a model class', + 'property_behavior_import_redirect' => 'Redirect', + 'property_behavior_import_redirect_description' => 'A page to redirect to by default when the process is complete.', + 'error_controller_not_found' => 'Original controller file is not found.', + 'error_invalid_config_file_name' => 'The behavior :class configuration file name (:file) contains invalid characters and cannot be loaded.', + 'error_file_not_yaml' => 'The behavior :class configuration file (:file) is not a YAML file. Only YAML configuration files are supported.', + 'saved' => 'Controller saved', + 'controller_name' => 'Controller Name', + 'controller_name_description' => 'Controller name defines the class name and URL of the controller\'s back-end pages. Standard PHP variable naming conventions apply. The first symbol should be a capital Latin letter. Examples: Categories, Posts, Products.', + 'base_model_class' => 'Base Model Class', + 'base_model_class_description' => 'Select a model class to use as a base model in behaviors that require or support models. You can configure the behaviors later.', + 'base_model_class_placeholder' => '--select model--', + 'controller_behaviors' => 'Behaviors', + 'controller_behaviors_description' => 'Select behaviors the controller should implement. Builder will create view files required for the behaviors automatically.', + 'controller_permissions' => 'Permissions', + 'controller_permissions_description' => 'Select user permissions that can access the controller views. Permissions can be defined on the Permissions tab of the Builder. You can change this option in the controller PHP script later.', + 'controller_permissions_no_permissions' => 'The plugin doesn\'t define any permissions.', + 'menu_item' => 'Active Menu Item', + 'menu_item_description' => 'Select a menu item to make active for the controller pages. You can change this option in the controller PHP script later.', + 'menu_item_placeholder' => '--select menu item--', + 'error_unknown_behavior' => 'The behavior class :class is not registered in the behavior library.', + 'error_behavior_view_conflict' => 'The selected behaviors provide conflicting views (:view) and cannot be used together in a controller.', + 'error_behavior_config_conflict' => 'The selected behaviors provide conflicting configuration files (:file) and cannot be used together in a controller.', + 'error_behavior_view_file_not_found' => 'View template :view of the behavior :class cannot be found.', + 'error_behavior_config_file_not_found' => 'Configuration template :file of the behavior :class cannot be found.', + 'error_controller_exists' => 'Controller file already exists: :file.', + 'error_controller_name_invalid' => 'Invalid controller name format. The name can only contain digits and Latin letters. The first symbol should be a capital Latin letter.', + 'error_behavior_view_file_exists' => 'Controller view file already exists: :view.', + 'error_behavior_config_file_exists' => 'Behavior configuration file already exists: :file.', + 'error_save_file' => 'Error saving controller file: :file', + 'error_behavior_requires_base_model' => 'Behavior :behavior requires a base model class to be selected.', + 'error_model_doesnt_have_lists' => 'The selected model doesn\'t have any lists. Please create a list first.', + 'error_model_doesnt_have_forms' => 'The selected model doesn\'t have any forms. Please create a form first.', + ], + 'version' => [ + 'menu_label' => 'Versions', + 'no_records' => 'No plugin versions found', + 'search' => 'Search...', + 'tab' => 'Versions', + 'saved' => 'Version saved', + 'confirm_delete' => 'Delete the version?', + 'tab_new_version' => 'New version', + 'migration' => 'Migration', + 'seeder' => 'Seeder', + 'custom' => 'Increase the version number', + 'apply_version' => 'Apply version', + 'applying' => 'Applying...', + 'rollback_version' => 'Rollback version', + 'rolling_back' => 'Rolling back...', + 'applied' => 'Version applied', + 'rolled_back' => 'Version rolled back', + 'hint_save_unapplied' => 'You saved an unapplied version. Unapplied versions could be automatically applied when you or another user migrates the database or when a database table is saved in the Database section of the Builder.', + 'hint_rollback' => 'Rolling back a version will also roll back all versions newer than this version. Please note that unapplied versions could be automatically applied by the system when you or another user logs into the back-end or when a database table is saved in the Database section of the Builder.', + 'hint_apply' => 'Applying a version will also apply all older unapplied versions of the plugin.', + 'dont_show_again' => 'Don\'t show again', + 'save_unapplied_version' => 'Save unapplied version', + 'sort_ascending' => 'Sort ascending', + 'sort_descending' => 'Sort descending', + ], + 'menu' => [ + 'menu_label' => 'Backend Menu', + 'tab' => 'Menus', + 'items' => 'Menu items', + 'saved' => 'Menus saved', + 'add_main_menu_item' => 'Add main menu item', + 'new_menu_item' => 'Menu Item', + 'add_side_menu_item' => 'Add sub-item', + 'side_menu_item' => 'Side menu item', + 'property_label' => 'Label', + 'property_label_required' => 'Please enter the menu item labels.', + 'property_url_required' => 'Please enter the menu item URL', + 'property_url' => 'URL', + 'property_icon' => 'Icon', + 'property_icon_required' => 'Please select an icon', + 'property_permissions' => 'Permissions', + 'property_order' => 'Order', + 'property_order_invalid' => 'Please enter the menu item order as integer value.', + 'property_order_description' => 'Menu item order manages its position in the menu. If the order is not provided, the item will be placed to the end of the menu. The default order values have the increment of 100.', + 'property_attributes' => 'HTML Attributes', + 'property_code' => 'Code', + 'property_code_invalid' => 'The code should contain only Latin letter and digits', + 'property_code_required' => 'Please enter the menu item code.', + 'error_duplicate_main_menu_code' => 'Duplicate main menu item code: \':code\'.', + 'error_duplicate_side_menu_code' => 'Duplicate side menu item code: \':code\'.', + 'icon_svg' => 'Icon (SVG)', + 'icon_svg_description' => 'An SVG icon to be used in place of the standard icon. The SVG icon should be a rectangle and can support colors', + 'counter' => 'Counter', + 'counter_description' => 'A numeric value to output near the menu icon. The value should be a number or a callable returning a number', + 'counter_label' => 'Counter Label', + 'counter_label_description' => 'A string value to describe the numeric reference in counter', + 'counter_group' => 'Counter', + ], + 'localization' => [ + 'menu_label' => 'Localization', + 'language' => 'Language', + 'strings' => 'Strings', + 'confirm_delete' => 'Delete the language?', + 'tab_new_language' => 'New language', + 'no_records' => 'No languages found', + 'saved' => 'Language file saved', + 'error_cant_load_file' => 'Cannot load the requested language file - file not found.', + 'error_bad_localization_file_contents' => 'Cannot load the requested language file. Language files can only contain array definitions and strings.', + 'error_file_not_array' => 'Cannot load the requested language file. Language files should return an array.', + 'save_error' => 'Error saving file \':name\'. Please check write permissions.', + 'error_delete_file' => 'Error deleting localization file.', + 'add_missing_strings' => 'Add Missing Strings', + 'copy' => 'Copy', + 'add_missing_strings_label' => 'Select language to copy missing strings from', + 'no_languages_to_copy_from' => 'There are no other languages to copy strings from.', + 'new_string_warning' => 'New string or section', + 'structure_mismatch' => 'The structure of the source language file doesn\'t match the structure of the file being edited. Some individual strings in the edited file correspond to sections in the source file (or vice versa) and cannot be merged automatically.', + 'create_string' => 'Create Language String', + 'string_key_label' => 'String Key', + 'string_key_comment' => 'Enter the string key using period as a section separator. For example: plugin.search. The string will be created in the plugin\'s default language localization file.', + 'string_value' => 'String Value', + 'string_key_is_empty' => 'String key should not be empty', + 'string_key_is_a_string' => ':key is a string and cannot contain other strings.', + 'string_value_is_empty' => 'String value should not be empty', + 'string_key_exists' => 'The string key already exists', + ], + 'permission' => [ + 'menu_label' => 'Permissions', + 'tab' => 'Permissions', + 'form_tab_permissions' => 'Permissions', + 'btn_add_permission' => 'Add Permission', + 'btn_delete_permission' => 'Delete Permission', + 'column_permission_label' => 'Permission Code', + 'column_permission_required' => 'Please enter the permission code', + 'column_tab_label' => 'Tab Title', + 'column_tab_required' => 'Please enter the permission tab title', + 'column_label_label' => 'Label', + 'column_label_required' => 'Please enter the permission label', + 'saved' => 'Permissions saved', + 'error_duplicate_code' => 'Duplicate permission code: \':code\'.', + ], + 'yaml' => [ + 'save_error' => 'Error saving file \':name\'. Please check write permissions.', + ], + 'common' => [ + 'error_file_exists' => 'File already exists: \':path\'.', + 'field_icon_description' => 'October CMS uses Font Awesome icons: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'The destination directory doesn\'t exist: \':path\'.', + 'error_make_dir' => 'Error creating directory: \':name\'.', + 'error_dir_exists' => 'Directory already exists: \':path\'.', + 'template_not_found' => 'Template file is not found: \':name\'.', + 'error_generating_file' => 'Error generating file: \':path\'.', + 'error_loading_template' => 'Error loading template file: \':name\'.', + 'select_plugin_first' => 'Please select a plugin first. To see the plugin list click the > icon on the left sidebar.', + 'not_match' => 'The object you\'re trying to access doesn\'t belong to the plugin being edited. Please reload the page.', + 'plugin_not_selected' => 'Plugin is not selected', + 'add' => 'Add', + ], + 'migration' => [ + 'entity_name' => 'Migration', + 'error_version_invalid' => 'The version should be specified in format v1.0.1', + 'field_version' => 'Version', + 'field_description' => 'Description', + 'field_code' => 'Code', + 'save_and_apply' => 'Save & Apply', + 'error_version_exists' => 'The migration version already exists.', + 'error_script_filename_invalid' => 'The migration script file name can contain only Latin letters, digits and underscores. The name should start with a Latin letter and could not contain spaces.', + 'error_cannot_change_version_number' => 'Cannot change version number for an applied version.', + 'error_file_must_define_class' => 'Migration code should define a migration or seeder class. Leave the code field blank if you only want to update the version number.', + 'error_file_must_define_namespace' => 'Migration code should define a namespace. Leave the code field blank if you only want to update the version number.', + 'no_changes_to_save' => 'There are no changes to save.', + 'error_namespace_mismatch' => 'The migration code should use the plugin namespace: :namespace', + 'error_migration_file_exists' => 'Migration file :file already exists. Please use another class name.', + 'error_cant_delete_applied' => 'This version has already been applied and cannot be deleted. Please rollback the version first.', + ], + 'components' => [ + 'list_title' => 'Record list', + 'list_description' => 'Displays a list of records for a selected model', + 'list_page_number' => 'Page number', + 'list_page_number_description' => 'This value is used to determine what page the user is on.', + 'list_records_per_page' => 'Records per page', + 'list_records_per_page_description' => 'Number of records to display on a single page. Leave empty to disable pagination.', + 'list_records_per_page_validation' => 'Invalid format of the records per page value. The value should be a number.', + 'list_no_records' => 'No records message', + 'list_no_records_description' => 'Message to display in the list in case if there are no records. Used in the default component\'s partial.', + 'list_no_records_default' => 'No records found', + 'list_sort_column' => 'Sort by column', + 'list_sort_column_description' => 'Model column the records should be ordered by', + 'list_sort_direction' => 'Direction', + 'list_display_column' => 'Display column', + 'list_display_column_description' => 'Column to display in the list. Used in the default component\'s partial.', + 'list_display_column_required' => 'Please select a display column.', + 'list_details_page' => 'Details page', + 'list_details_page_description' => 'Page to display record details.', + 'list_details_page_no' => '--no details page--', + 'list_sorting' => 'Sorting', + 'list_pagination' => 'Pagination', + 'list_order_direction_asc' => 'Ascending', + 'list_order_direction_desc' => 'Descending', + 'list_model' => 'Model class', + 'list_scope' => 'Scope', + 'list_scope_description' => 'Optional model scope to fetch the records', + 'list_scope_default' => '--select a scope, optional--', + 'list_scope_value' => 'Scope value', + 'list_scope_value_description' => 'Optional value to pass to the model scope', + 'list_details_page_link' => 'Link to the details page', + 'list_details_key_column' => 'Details key column', + 'list_details_key_column_description' => 'Model column to use as a record identifier in the details page links.', + 'list_details_url_parameter' => 'URL parameter name', + 'list_details_url_parameter_description' => 'Name of the details page URL parameter which takes the record identifier.', + 'details_title' => 'Record details', + 'details_description' => 'Displays record details for a selected model', + 'details_model' => 'Model class', + 'details_identifier_value' => 'Identifier value', + 'details_identifier_value_description' => 'Identifier value to load the record from the database. Specify a fixed value or URL parameter name.', + 'details_identifier_value_required' => 'The identifier value is required', + 'details_key_column' => 'Key column', + 'details_key_column_description' => 'Model column to use as a record identifier for fetching the record from the database.', + 'details_key_column_required' => 'The key column name is required', + 'details_display_column' => 'Display column', + 'details_display_column_description' => 'Model column to display on the details page. Used in the default component\'s partial.', + 'details_display_column_required' => 'Please select a display column.', + 'details_not_found_message' => 'Not found message', + 'details_not_found_message_description' => 'Message to display if the record is not found. Used in the default component\'s partial.', + 'details_not_found_message_default' => 'Record not found', + ], + 'validation' => [ + 'reserved' => ':attribute cannot be a PHP reserved keyword', + ], +]; diff --git a/plugins/rainlab/builder/lang/es.json b/plugins/rainlab/builder/lang/es.json new file mode 100644 index 0000000..16fb070 --- /dev/null +++ b/plugins/rainlab/builder/lang/es.json @@ -0,0 +1,4 @@ +{ + "Builder": "Builder", + "Provides visual tools for building October plugins.": "Proporciona herramientas visuales para la construcción de plugins de October." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/es/lang.php b/plugins/rainlab/builder/lang/es/lang.php new file mode 100644 index 0000000..0e5adb6 --- /dev/null +++ b/plugins/rainlab/builder/lang/es/lang.php @@ -0,0 +1,642 @@ + [ + 'add' => 'Crear plugin', + 'no_records' => 'No se encuentran plugins', + 'no_name' => 'Sin nombre', + 'search' => 'Buscar...', + 'filter_description' => 'Mostrar todos los plugins o sólo tus plugins.', + 'settings' => 'Configuración', + 'entity_name' => 'Plugin', + 'field_name' => 'Nombre', + 'field_author' => 'Autor', + 'field_description' => 'Descripción', + 'field_icon' => 'Icono plugin', + 'field_plugin_namespace' => 'Espacio de nombres de plugin', + 'field_author_namespace' => 'Espacio de nombres de autor', + 'field_namespace_description' => 'Namespace can contain only Latin letters and digits and should start with a Latin letter. Example plugin namespace: Blog', + 'field_author_namespace_description' => 'You cannot change the namespaces with Builder after you create the plugin. Example author namespace: JohnSmith', + 'tab_general' => 'Parametros generales', + 'tab_description' => 'Descripción', + 'field_homepage' => 'Plugin Homepage (URL)', + 'no_description' => 'No hay descripción proporcionada para este plugin', + 'error_settings_not_editable' => 'Configuración de este plugin no se pueden editar con el Builder.', + 'update_hint' => 'Puedes editar el nombre de plugins y descripción localizada en la pestaña de localizaciones.', + 'manage_plugins' => 'Crear y editar plugins', + ], + 'author_name' => [ + 'title' => 'Nombre del autor', + 'description' => 'Por defecto el nombre del autor a utilizar para sus nuevos plugins. El nombre del autor no es fijo - se puede cambiar en la configuración de los plugins en cualquier momento.', + ], + 'author_namespace' => [ + 'title' => 'Espacio de nombres', + 'description' => 'Si desarrolla para el Marketplace, el espacio de nombres debe coincidir con el código de autor y no puede ser cambiado. Consulte la documentación para más detalles.', + ], + 'database' => [ + 'menu_label' => 'Base de datos', + 'no_records' => 'Tablas no encontradas', + 'search' => 'Buscar...', + 'confirmation_delete_multiple' => '¿Eliminar las tablas seleccionadas?', + 'field_name' => 'Nombre de la tabla', + 'tab_columns' => 'Columnas', + 'column_name_name' => 'Columna', + 'column_name_required' => 'Por favor ingrese el nombre de la columna', + 'column_name_type' => 'Tipo', + 'column_type_required' => 'Please select the column type', + 'column_name_length' => 'Length', + 'column_validation_length' => 'The Length value should be integer or specified as precision and scale (10,2) for decimal columns. Spaces are not allowed in the length column.', + 'column_validation_title' => 'Only digits, lower-case Latin letters and underscores are allowed in column names', + 'column_name_unsigned' => 'Unsigned', + 'column_name_nullable' => 'Nullable', + 'column_auto_increment' => 'AUTOINCR', + 'column_default' => 'Default', + 'column_auto_primary_key' => 'PK', + 'tab_new_table' => 'Nueva tabla', + 'btn_add_column' => 'Añadir columna', + 'btn_delete_column' => 'Borrar columna', + 'btn_add_id' => 'Añadir ID', + 'btn_add_timestamps' => 'Añadir timestamps', + 'btn_add_soft_deleting' => 'Añadir columna para soft delete', + 'confirm_delete' => '¿Borrar la tabla?', + 'error_enum_not_supported' => 'The table contains column(s) with type "enum" which is not currently supported by the Builder.', + 'error_table_name_invalid_prefix' => 'Table name should start with the plugin prefix: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Invalid table name. Table names should contain only Latin letters, digits and underscores. Names should start with a Latin letter and could not contain spaces.', + 'error_table_duplicate_column' => 'Nombre de columna duplicada: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => 'An auto-increment column cannot be a part of a compound primary key.', + 'error_table_mutliple_auto_increment' => 'La tabla no puede contener más de una columna auto-incrementable.', + 'error_table_auto_increment_non_integer' => 'Las columnas Auto-incrementables deben ser de tipo numerico.', + 'error_table_decimal_length' => 'The Length parameter for :type type should be in format \'10,2\', without spaces.', + 'error_table_length' => 'The Length parameter for :type type should be specified as integer.', + 'error_unsigned_type_not_int' => 'Error in the \':column\' column. The Unsigned flag can be applied only to integer type columns.', + 'error_integer_default_value' => 'Invalid default value for the integer column \':column\'. The allowed formats are \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Invalid default value for the decimal or double column \':column\'. The allowed formats are \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Invalid default value for the boolean column \':column\'. The allowed values are \'0\' and \'1\'.', + 'error_unsigned_negative_value' => 'The default value for the unsigned column \':column\' can\'t be negative.', + 'error_table_already_exists' => 'La tabla \':name\' ya existe en la base de datos.', + ], + 'model' => [ + 'menu_label' => 'Modelos', + 'entity_name' => 'Modelo', + 'no_records' => 'Modelos no encontrados', + 'search' => 'Buscar...', + 'add' => 'Añadir...', + 'forms' => 'Formularios', + 'lists' => 'Listas', + 'field_class_name' => 'Nombre Clase', + 'field_database_table' => 'Tabla de base de datos', + 'error_class_name_exists' => 'Ya existe el archivo modelo para el nombre de clase especificado: :path', + 'add_form' => 'Añadir formulario', + 'add_list' => 'Añadir lista', + ], + 'form' => [ + 'saved' => 'Formulario guardado', + 'confirm_delete' => '¿Borrar el formulario?', + 'tab_new_form' => 'Nuevo formulario', + 'property_label_title' => 'Etiqueta', + 'property_label_required' => 'Por favor especifique la etiqueta del control.', + 'property_span_title' => 'Span', + 'property_comment_title' => 'Commentario', + 'property_comment_above_title' => 'Comentario encima', + 'property_default_title' => 'Default', + 'property_checked_default_title' => 'Checked by default', + 'property_css_class_title' => 'CSS class', + 'property_css_class_description' => 'Optional CSS class to assign to the field container.', + 'property_disabled_title' => 'deshabilitado', + 'property_hidden_title' => 'Oculto', + 'property_required_title' => 'Requerido', + 'property_field_name_title' => 'Nombre del campo', + 'property_placeholder_title' => 'Placeholder', + 'property_default_from_title' => 'Default from', + 'property_stretch_title' => 'Stretch', + 'property_stretch_description' => 'Specifies if this field stretches to fit the parent height.', + 'property_context_title' => 'Context', + 'property_context_description' => 'Specifies what form context should be used when displaying the field.', + 'property_context_create' => 'Create', + 'property_context_update' => 'Update', + 'property_context_preview' => 'Preview', + 'property_dependson_title' => 'Depends on', + 'property_trigger_action' => 'Action', + 'property_trigger_show' => 'Show', + 'property_trigger_hide' => 'Hide', + 'property_trigger_enable' => 'Enable', + 'property_trigger_disable' => 'Disable', + 'property_trigger_empty' => 'Vacio', + 'property_trigger_field' => 'Campo', + 'property_trigger_field_description' => 'Defines the other field name that will trigger the action.', + 'property_trigger_condition' => 'Condition', + 'property_trigger_condition_description' => 'Determines the condition the specified field should satisfy for the condition to be considered "true". Supported values: checked, unchecked, value[somevalue].', + 'property_trigger_condition_checked' => 'Checked', + 'property_trigger_condition_unchecked' => 'Unchecked', + 'property_trigger_condition_somevalue' => 'value[enter-the-value-here]', + 'property_preset_title' => 'Preset', + 'property_preset_description' => 'Allows the field value to be initially set by the value of another field, converted using the input preset converter.', + 'property_preset_field' => 'Field', + 'property_preset_field_description' => 'Defines the other field name to source the value from.', + 'property_preset_type' => 'Type', + 'property_preset_type_description' => 'Specifies the conversion type', + 'property_attributes_title' => 'Atributos', + 'property_attributes_description' => 'Custom HTML attributes to add to the form field element.', + 'property_container_attributes_title' => 'Container attributes', + 'property_container_attributes_description' => 'Custom HTML attributes to add to the form field container element.', + 'property_group_advanced' => 'Avanzado', + 'property_dependson_description' => 'A list of other field names this field depends on, when the other fields are modified, this field will update. One field per line.', + 'property_trigger_title' => 'Trigger', + 'property_trigger_description' => 'Allows to change elements attributes such as visibility or value, based on another elements\' state.', + 'property_default_from_description' => 'Takes the default value from the value of another field.', + 'property_field_name_required' => 'El campo nombre es requerido', + 'property_field_name_regex' => 'The field name can contain only Latin letters, digits, underscores, dashes and square brackets.', + 'property_attributes_size' => 'Tamaño', + 'property_attributes_size_tiny' => 'Diminuto', + 'property_attributes_size_small' => 'Pequeño', + 'property_attributes_size_large' => 'Grande', + 'property_attributes_size_huge' => 'Enorme', + 'property_attributes_size_giant' => 'Gigante', + 'property_comment_position' => 'Posición del comentario', + 'property_comment_position_above' => 'Arriba', + 'property_comment_position_below' => 'Abajo', + 'property_hint_path' => 'Hint partial path', + 'property_hint_path_description' => 'Path to a partial file that contains the hint text. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_hint.htm', + 'property_hint_path_required' => 'Por favor ingresa la ruta hacia el archivo parcial de la pista', + 'property_partial_path' => 'Partial path', + 'property_partial_path_description' => 'Path to a partial file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/partials/_partial.htm', + 'property_partial_path_required' => 'Please enter the partial path', + 'property_code_language' => 'Idioma', + 'property_code_theme' => 'Tema', + 'property_theme_use_default' => 'Usar tema por defecto', + 'property_group_code_editor' => 'Code editor', + 'property_gutter' => 'Gutter', + 'property_gutter_show' => 'Visible', + 'property_gutter_hide' => 'Hidden', + 'property_wordwrap' => 'Word wrap', + 'property_wordwrap_wrap' => 'Wrap', + 'property_wordwrap_nowrap' => 'Don\'t wrap', + 'property_fontsize' => 'Font size', + 'property_codefolding' => 'Code folding', + 'property_codefolding_manual' => 'Manual', + 'property_codefolding_markbegin' => 'Mark begin', + 'property_codefolding_markbeginend' => 'Mark begin and end', + 'property_autoclosing' => 'Auto closing', + 'property_enabled' => 'Habilitado', + 'property_disabled' => 'Deshabilitado', + 'property_soft_tabs' => 'Soft tabs', + 'property_tab_size' => 'Tamaño de la pestaña', + 'property_readonly' => 'Solo lectura', + 'property_use_default' => 'Use default settings', + 'property_options' => 'Opciones', + 'property_prompt' => 'Prompt', + 'property_prompt_description' => 'Texto que se mostrará para el botón creado', + 'property_prompt_default' => 'Añadir nuevo item', + 'property_available_colors' => 'Colores disponibles', + 'property_available_colors_description' => 'List of available colors in hex format (#FF0000). Leave empty for the default color set. Enter one value per line.', + 'property_datepicker_mode' => 'Modo', + 'property_datepicker_mode_date' => 'Fecha', + 'property_datepicker_mode_datetime' => 'Fecha y hora', + 'property_datepicker_mode_time' => 'Hora', + 'property_datepicker_min_date' => 'Fecha mínima', + 'property_datepicker_max_date' => 'Fecha máxima', + 'property_fileupload_mode' => 'Mode', + 'property_fileupload_mode_file' => 'Archivo', + 'property_fileupload_mode_image' => 'Imágen', + 'property_group_fileupload' => 'File upload', + 'property_fileupload_image_width' => 'Ancho de la imagen (width)', + 'property_fileupload_image_width_description' => 'Optional parameter - images will be resized to this width. Applies to Image mode only.', + 'property_fileupload_invalid_dimension' => 'Invalid dimension value - please enter a number.', + 'property_fileupload_image_height' => 'Alto de la imagen (height)', + 'property_fileupload_image_height_description' => 'Optional parameter - images will be resized to this height. Applies to Image mode only.', + 'property_fileupload_file_types' => 'File types', + 'property_fileupload_file_types_description' => 'Optional comma separated list of file extensions that are accepted by the uploader. Eg: zip,txt', + 'property_fileupload_mime_types' => 'MIME types', + 'property_fileupload_mime_types_description' => 'Optional comma separated list of MIME types that are accepted by the uploader, either as file extensions or fully qualified names. Eg: bin,txt', + 'property_fileupload_use_caption' => 'Use caption', + 'property_fileupload_use_caption_description' => 'Allows a title and description to be set for the file.', + 'property_fileupload_thumb_options' => 'Thumbnail options', + 'property_fileupload_thumb_options_description' => 'Manages options for the automatically generated thumbnails. Applies only for the Image mode.', + 'property_fileupload_thumb_mode' => 'Mode', + 'property_fileupload_thumb_auto' => 'Auto', + 'property_fileupload_thumb_exact' => 'Exact', + 'property_fileupload_thumb_portrait' => 'Portrait', + 'property_fileupload_thumb_landscape' => 'Landscape', + 'property_fileupload_thumb_crop' => 'Crop', + 'property_fileupload_thumb_extension' => 'Extensión del archivo', + 'property_name_from' => 'Name column', + 'property_name_from_description' => 'Relation column name to use for displaying a name.', + 'property_relation_select' => 'Select', + 'property_relation_select_description' => 'CONCAT multiple columns together for displaying a name', + 'property_description_from' => 'Description column', + 'property_description_from_description' => 'Relation column name to use for displaying a description.', + 'property_recordfinder_prompt' => 'Prompt', + 'property_recordfinder_prompt_description' => 'Text to display when there is no record selected. The %s character represents the search icon. Leave empty for the default prompt.', + 'property_recordfinder_list' => 'List configuration', + 'property_recordfinder_list_description' => 'A reference to a list column definition file. Use the $ symbol to refer the plugins root directory, for example: $/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => 'Please provide a path to the list YAML file', + 'property_group_recordfinder' => 'Record finder', + 'property_mediafinder_mode' => 'Modo', + 'property_mediafinder_mode_file' => 'Archivo', + 'property_mediafinder_mode_image' => 'Imagen', + 'property_group_relation' => 'Relation', + 'property_relation_prompt' => 'Prompt', + 'property_relation_prompt_description' => 'Text to display when there is no available selections.', + 'control_group_standard' => 'Standard', + 'control_group_widgets' => 'Widgets', + 'click_to_add_control' => 'Añadir control', + 'loading' => 'Cargando...', + 'control_text' => 'Texto', + 'control_text_description' => 'Campo de texto de una linea', + 'control_password' => 'Contraseña', + 'control_password_description' => 'Campo de contraseña de una linea', + 'control_checkbox' => 'Checkbox', + 'control_checkbox_description' => 'Checkbox único', + 'control_switch' => 'Switch', + 'control_switch_description' => 'Control switch único, una alternativa al checkbox', + 'control_textarea' => 'Text area', + 'control_textarea_description' => 'Campo de texto multilinea con altura personalizable', + 'control_dropdown' => 'Dropdown', + 'control_dropdown_description' => 'Dropdown list with static or dynamic options', + 'control_unknown' => 'Unknown control type: :type', + 'control_repeater' => 'Repeater', + 'control_repeater_description' => 'Outputs a repeating set of form controls', + 'control_number' => 'Número', + 'control_number_description' => 'Campo de texto de una linea el cual sólo permite números', + 'control_hint' => 'Pista', + 'control_hint_description' => 'Despliega un contenido parcial en una caja que puede ser ocultado por el usuario', + 'control_partial' => 'Partial', + 'control_partial_description' => 'Incluye un contenido parcial', + 'control_section' => 'Section', + 'control_section_description' => 'Displays a form section with heading and subheading', + 'control_radio' => 'Lista de radios', + 'control_radio_description' => 'Una lista de radio buttons, donde solo un item puede ser seleccionado al mismo tiempo', + 'control_radio_option_1' => 'Opción 1', + 'control_radio_option_2' => 'Opción 2', + 'control_checkboxlist' => 'Lista de Checkbox', + 'control_checkboxlist_description' => 'Una lista de checkboxs, donde mas de un item puede ser seleccionado', + 'control_codeeditor' => 'Editor de código', + 'control_codeeditor_description' => 'Plaintext editor for formatted code or markup', + 'control_colorpicker' => 'Selector de color', + 'control_colorpicker_description' => 'Un campo para seleccionar un valor de color en hexadecimal', + 'control_datepicker' => 'Selector de fecha', + 'control_datepicker_description' => 'Un campo de texto para seleccionar fecha/hora', + 'control_richeditor' => 'Editor enriquecido', + 'control_richeditor_description' => 'Editor visual para texto enriquecido, también conocido como editor WYSIWYG', + 'control_markdown' => 'Editor Markdown', + 'control_markdown_description' => 'Editor básico para texto formateado en Markdown', + 'control_fileupload' => 'File upload', + 'control_fileupload_description' => 'File uploader for images or regular files', + 'control_recordfinder' => 'Record finder', + 'control_recordfinder_description' => 'Field with details of a related record with the record search feature', + 'control_mediafinder' => 'Media finder', + 'control_mediafinder_description' => 'Field for selecting an item from the Media Manager library', + 'control_relation' => 'Relation', + 'control_relation_description' => 'Despliega un Dropdown o una lista de Checkboxs para seleccionar el registro relacionado', + 'error_file_name_required' => 'Por favor ingresa el nombre del archivo del formulario.', + 'error_file_name_invalid' => 'El nombre de este archivo sólo puede contener letras, digitos, guiones (bajo y medio) y puntos.', + 'span_left' => 'Izquierda', + 'span_right' => 'Derecha', + 'span_full' => 'Completo', + 'span_auto' => 'Automático', + 'empty_tab' => 'Pestaña vacia', + 'confirm_close_tab' => 'La pestaña contiene controles que serán eliminados, ¿continuar?', + 'tab' => 'Pestaña de formulario', + 'tab_title' => 'Titulo', + 'controls' => 'Controles', + 'property_tab_title_required' => 'el campo titulo es requerido', + 'tabs_primary' => 'Pestañas primarias', + 'tabs_secondary' => 'Pestañas secundarias', + 'tab_stretch' => 'Stretch', + 'tab_stretch_description' => 'Especifica si el contenedor de esta pestaña se estira para ajustarse al alto del elemento padre.', + 'tab_css_class' => 'Clase CSS', + 'tab_css_class_description' => 'Asigna una clase CSS al contenedor de la pestaña.', + 'tab_name_template' => 'Pestaña %s', + 'tab_already_exists' => 'Ya existe una pestaña con el nombre especificado.', + ], + 'list' => [ + 'tab_new_list' => 'Nueva lista', + 'saved' => 'Lista guardada', + 'confirm_delete' => '¿Borrar la lista?', + 'tab_columns' => 'Columnas', + 'btn_add_column' => 'Añadir columna', + 'btn_delete_column' => 'Borrar columna', + 'column_dbfield_label' => 'Campo', + 'column_dbfield_required' => 'Por favor proporcione el campo del modelo', + 'column_name_label' => 'Etiqueta', + 'column_label_required' => 'Por favor proporcione la etiqueta de columna', + 'column_type_label' => 'Tipo', + 'column_type_required' => 'Por favor indique el tipo de columna', + 'column_type_text' => 'Texto', + 'column_type_number' => 'Numero', + 'column_type_switch' => 'Switch', + 'column_type_datetime' => 'Datetime', + 'column_type_date' => 'Fecha', + 'column_type_time' => 'Hora', + 'column_type_timesince' => 'Tine since', + 'column_type_timetense' => 'Tine tense', + 'column_type_select' => 'Select', + 'column_type_partial' => 'Partial', + 'column_label_default' => 'Default', + 'column_label_searchable' => 'Buscable', + 'column_label_sortable' => 'Ordenable', + 'column_label_invisible' => 'Invisible', + 'column_label_select' => 'Select', + 'column_label_relation' => 'Relation', + 'column_label_css_class' => 'CSS class', + 'column_label_width' => 'Width', + 'column_label_path' => 'Path', + 'column_label_format' => 'Format', + 'column_label_value_from' => 'Value from', + 'error_duplicate_column' => 'Nombre del campo duplicado: \':column\'.', + 'btn_add_database_columns' => 'Añadir columnas desde la base de datos', + ], + 'controller' => [ + 'menu_label' => 'Controladores', + 'no_records' => 'No se encuentran controladores del plugin', + 'controller' => 'Controlador', + 'behaviors' => 'Comportamientos', + 'new_controller' => 'Nuevo controlador', + 'error_controller_has_no_behaviors' => 'El controlador no tiene un behaviors configurado.', + 'error_invalid_yaml_configuration' => 'Error loading behavior configuration file: :file', + 'behavior_form_controller' => 'Form controller behavior', + 'behavior_form_controller_description' => 'Adds form functionality to a back-end page. The behavior provides three pages called Create, Update and Preview.', + 'property_behavior_form_placeholder' => '--select form--', + 'property_behavior_form_name' => 'Nombre', + 'property_behavior_form_name_description' => 'The name of the object being managed by this form', + 'property_behavior_form_name_required' => 'Por favor ingrese el nombre del formulario', + 'property_behavior_form_file' => 'Configuración del formulario', + 'property_behavior_form_file_description' => 'Reference to a form field definition file', + 'property_behavior_form_file_required' => 'Please enter a path to the form configuration file', + 'property_behavior_form_model_class' => 'Model class', + 'property_behavior_form_model_class_description' => 'A model class name, the form data is loaded and saved against this model.', + 'property_behavior_form_model_class_required' => 'Please select a model class', + 'property_behavior_form_default_redirect' => 'Default redirect', + 'property_behavior_form_default_redirect_description' => 'A page to redirect to by default when the form is saved or cancelled.', + 'property_behavior_form_create' => 'Create record page', + 'property_behavior_form_redirect' => 'Redirect', + 'property_behavior_form_redirect_description' => 'Una página a la que redirigir una vez el registro sea creado', + 'property_behavior_form_redirect_close' => 'Close redirect', + 'property_behavior_form_redirect_close_description' => 'A page to redirect to when a record is created and the close post variable is sent with the request.', + 'property_behavior_form_flash_save' => 'Save flash message', + 'property_behavior_form_flash_save_description' => 'Flash message to display when record is saved.', + 'property_behavior_form_page_title' => 'Page title', + 'property_behavior_form_update' => 'Update record page', + 'property_behavior_form_update_redirect' => 'Redirect', + 'property_behavior_form_create_redirect_description' => 'A page to redirect to when a record is saved.', + 'property_behavior_form_flash_delete' => 'Delete flash message', + 'property_behavior_form_flash_delete_description' => 'Flash message to display when record is deleted.', + 'property_behavior_form_preview' => 'Preview record page', + 'behavior_list_controller' => 'List controller behavior', + 'behavior_list_controller_description' => 'Provides the sortable and searchable list with optional links on its records. The behavior automatically creates the controller action "index".', + 'property_behavior_list_title' => 'List title', + 'property_behavior_list_title_required' => 'Please enter the list title', + 'property_behavior_list_placeholder' => '--select list--', + 'property_behavior_list_model_class' => 'Model class', + 'property_behavior_list_model_class_description' => 'A model class name, the list data is loaded from this model.', + 'property_behavior_form_model_class_placeholder' => '--select model--', + 'property_behavior_list_model_class_required' => 'Please select a model class', + 'property_behavior_list_model_placeholder' => '--select model--', + 'property_behavior_list_file' => 'List configuration file', + 'property_behavior_list_file_description' => 'Reference to a list definition file', + 'property_behavior_list_file_required' => 'Please enter a path to the list configuration file', + 'property_behavior_list_record_url' => 'Record URL', + 'property_behavior_list_record_url_description' => 'Link each list record to another page. Eg: users/update:id. The :id part is replaced with the record identifier.', + 'property_behavior_list_no_records_message' => 'No records message', + 'property_behavior_list_no_records_message_description' => 'A message to display when no records are found', + 'property_behavior_list_recs_per_page' => 'Records per page', + 'property_behavior_list_recs_per_page_description' => 'Records to display per page, use 0 for no pages. Default: 0', + 'property_behavior_list_recs_per_page_regex' => 'Records per page should be an integer value', + 'property_behavior_list_show_setup' => 'Show setup button', + 'property_behavior_list_show_sorting' => 'Show sorting', + 'property_behavior_list_default_sort' => 'Default sorting', + 'property_behavior_form_ds_column' => 'Columna', + 'property_behavior_form_ds_direction' => 'Direction', + 'property_behavior_form_ds_asc' => 'Ascending', + 'property_behavior_form_ds_desc' => 'Descending', + 'property_behavior_list_show_checkboxes' => 'Show checkboxes', + 'property_behavior_list_onclick' => 'On click handler', + 'property_behavior_list_onclick_description' => 'Custom JavaScript code to execute when clicking on a record.', + 'property_behavior_list_show_tree' => 'Show tree', + 'property_behavior_list_show_tree_description' => 'Displays a tree hierarchy for parent/child records.', + 'property_behavior_list_tree_expanded' => 'Tree expanded', + 'property_behavior_list_tree_expanded_description' => 'Determines if tree nodes should be expanded by default.', + 'property_behavior_list_toolbar' => 'Toolbar', + 'property_behavior_list_toolbar_buttons' => 'Buttons partial', + 'property_behavior_list_toolbar_buttons_description' => 'Reference to a controller partial file with the toolbar buttons. Eg: list_toolbar', + 'property_behavior_list_search' => 'Buscar', + 'property_behavior_list_search_prompt' => 'Search prompt', + 'property_behavior_list_filter' => 'Filter configuration', + 'behavior_reorder_controller' => 'Reorder controller behavior', + 'behavior_reorder_controller_description' => 'Provides features for sorting and reordering on its records. The behavior automatically creates the controller action "reorder".', + 'property_behavior_reorder_title' => 'Reorder title', + 'property_behavior_reorder_title_required' => 'Please enter the reorder title', + 'property_behavior_reorder_name_from' => 'Attribute name', + 'property_behavior_reorder_name_from_description' => 'Model\'s attribute that should be used as a label for each record.', + 'property_behavior_reorder_name_from_required' => 'Please enter the attribute name', + 'property_behavior_reorder_model_class' => 'Model class', + 'property_behavior_reorder_model_class_description' => 'A model class name, the reorder data is loaded from this model.', + 'property_behavior_reorder_model_class_placeholder' => '--select model--', + 'property_behavior_reorder_model_class_required' => 'Please select a model class', + 'property_behavior_reorder_model_placeholder' => '--select model--', + 'property_behavior_reorder_toolbar' => 'Toolbar', + 'property_behavior_reorder_toolbar_buttons' => 'Buttons partial', + 'property_behavior_reorder_toolbar_buttons_description' => 'Reference to a controller partial file with the toolbar buttons. Eg: reorder_toolbar', + 'error_controller_not_found' => 'Original controller file is not found.', + 'error_invalid_config_file_name' => 'The behavior :class configuration file name (:file) contains invalid characters and cannot be loaded.', + 'error_file_not_yaml' => 'The behavior :class configuration file (:file) is not a YAML file. Only YAML configuration files are supported.', + 'saved' => 'Controlador guardado', + 'controller_name' => 'Nombre del controlador', + 'controller_name_description' => 'El nombre del controlador define el nombre de la clase y la URL del controlador en el backend. Se aplican Las convenciones estándar de nombramiento de variables. El primer simbolo debe ser una letra Mayúscula: Ejemplo: Categories, Posts, Products', + 'base_model_class' => 'Clase de modelo base', + 'base_model_class_description' => 'Selecciona la clase del modelo para usarla como modelo base en los behaviors que se requieran o soporte el modelo. Puedes configurar los behaviors más adelante.', + 'base_model_class_placeholder' => '--selecciona el modelo--', + 'controller_behaviors' => 'Behaviors', + 'controller_behaviors_description' => 'Selecciona los behaviors que el controlador debe implementar. El Plugin Builder creará las vistas requeridas para los behaviors automáticamente.', + 'controller_permissions' => 'Permisos', + 'controller_permissions_description' => 'Select user permissions that can access the controller views. Permissions can be defined on the Permissions tab of the Builder. You can change this option in the controller PHP script later.', + 'controller_permissions_no_permissions' => 'El plugin no define ningún permiso.', + 'menu_item' => 'Item activo del menú', + 'menu_item_description' => 'Selecciona un item del menú para se active para las páginas de este controlador. Puedes cambiar esta opción en el script de PHP del controlador mas adelante.', + 'menu_item_placeholder' => '--selecciona el item del menú--', + 'error_unknown_behavior' => 'La clase :class del behavior no está registrado en la librería de behaviors.', + 'error_behavior_view_conflict' => 'El behaviors seleccionado provee vistas que ocasionan un conflicto (:view) y no pueden utilizarse juntas en un controlador.', + 'error_behavior_config_conflict' => 'El behaviors seleccionado provee archivos de configuración que ocasionan un conflicto (:file) y no pueden utilizarse juntas en un controlador.', + 'error_behavior_view_file_not_found' => 'View template :view of the behavior :class cannot be found.', + 'error_behavior_config_file_not_found' => 'Configuration template :file of the behavior :class cannot be found.', + 'error_controller_exists' => 'Controller file already exists: :file.', + 'error_controller_name_invalid' => 'Formato del nombre del controlador invalido. El nombre solo puede contener letras y digitos. El primer simbolo debe ser una mayúscula.', + 'error_behavior_view_file_exists' => 'El archivo de la vista del controlador ya existe: :view.', + 'error_behavior_config_file_exists' => 'El archivo de configuración de este Behavior ya existe: :file.', + 'error_save_file' => 'Error guardando el archivo del controlador: :file', + 'error_behavior_requires_base_model' => 'El Behavior :behavior require de un modelo base para ser seleccionado.', + 'error_model_doesnt_have_lists' => 'El modelo seleccionado no tiene ninguna lista. Favor de primero crear una lista.', + 'error_model_doesnt_have_forms' => 'El modelo seleccionado no tiene ningún formulario. Favor de primero crear un formulario.', + ], + 'version' => [ + 'menu_label' => 'Versiones', + 'no_records' => 'Versiones del plugin no encontradas', + 'search' => 'Buscar...', + 'tab' => 'Versiones', + 'saved' => 'Versión guardada', + 'confirm_delete' => '¿Borrar esta versión?', + 'tab_new_version' => 'Nueva versión', + 'migration' => 'Migración', + 'seeder' => 'Seeder', + 'custom' => 'Increase the verison number', + 'apply_version' => 'Apply version', + 'applying' => 'Applying...', + 'rollback_version' => 'Rollback versión', + 'rolling_back' => 'Rolling back...', + 'applied' => 'Version applied', + 'rolled_back' => 'Version rolled back', + 'hint_save_unapplied' => 'You saved an unapplied version. Unapplied versions could be automatically applied when you or another user logs into the back-end or when a database table is saved in the Database section of the Builder.', + 'hint_rollback' => 'Rolling back a version will also roll back all versions newer than this version. Please note that unapplied versions could be automatically applied by the system when you or another user logs into the back-end or when a database table is saved in the Database section of the Builder.', + 'hint_apply' => 'Applying a version will also apply all older unapplied versions of the plugin.', + 'dont_show_again' => 'Don\'t show again', + 'save_unapplied_version' => 'Save unapplied version', + ], + 'menu' => [ + 'menu_label' => 'Backend Menu', + 'tab' => 'Menus', + 'items' => 'Items del menú', + 'saved' => 'Menues guardados', + 'add_main_menu_item' => 'Añadir item al menú principal', + 'new_menu_item' => 'Menu Item', + 'add_side_menu_item' => 'Agregar sub-item', + 'side_menu_item' => 'Item de menú lateral', + 'property_label' => 'Etiqueta', + 'property_label_required' => 'Por favor ingrese la etiqueta para el item del menú.', + 'property_url_required' => 'Por favor ingrese la URL para el item del menú', + 'property_url' => 'URL', + 'property_icon' => 'Icono', + 'property_icon_required' => 'Por favor seleccione un ícono', + 'property_permissions' => 'Permisos', + 'property_order' => 'Orden', + 'property_order_invalid' => 'Por favor ingrese el orden el item del menú como un valor numérico.', + 'property_order_description' => 'Menu item order manages its position in the menu. If the order is not provided, the item will be placed to the end of the menu. The default order values have the increment of 100.', + 'property_attributes' => 'Atributos HTML', + 'property_code' => 'Código', + 'property_code_invalid' => 'El código sólo puede contener letras y números', + 'property_code_required' => 'Please enter the menu item code.', + 'error_duplicate_main_menu_code' => 'Duplicate main menu item code: \':code\'.', + 'error_duplicate_side_menu_code' => 'Duplicate side menu item code: \':code\'.', + ], + 'localization' => [ + 'menu_label' => 'Localización', + 'language' => 'Lenguaje', + 'strings' => 'Cadenas', + 'confirm_delete' => '¿Borrar el lenguaje?', + 'tab_new_language' => 'Nuevo lenguaje', + 'no_records' => 'Lenguajes no encontrados', + 'saved' => 'Archivo de lenguaje guardado', + 'error_cant_load_file' => 'Cannot load the requested language file - file not found.', + 'error_bad_localization_file_contents' => 'Cannot load the requested language file. Language files can only contain array definitions and strings.', + 'error_file_not_array' => 'Cannot load the requested language file. Language files should return an array.', + 'save_error' => 'Error saving file \':name\'. Please check write permissions.', + 'error_delete_file' => 'Error deleting localization file.', + 'add_missing_strings' => 'Añadir cadenas que faltan', + 'copy' => 'Copiar', + 'add_missing_strings_label' => 'Select language to copy missing strings from', + 'no_languages_to_copy_from' => 'There are no other languages to copy strings from.', + 'new_string_warning' => 'Nueva cadena o sección', + 'structure_mismatch' => 'The structure of the source language file doesn\'t match the structure of the file being edited. Some individual strings in the edited file correspond to sections in the source file (or vice versa) and cannot be merged automatically.', + 'create_string' => 'Create new string', + 'string_key_label' => 'String key', + 'string_key_comment' => 'Enter the string key using period as a section separator. For example: plugin.search. The string will be created in the plugin\'s default language localization file.', + 'string_value' => 'String value', + 'string_key_is_empty' => 'String key should not be empty', + 'string_value_is_empty' => 'String value should not be empty', + 'string_key_exists' => 'The string key already exists', + ], + 'permission' => [ + 'menu_label' => 'Permisos', + 'tab' => 'Permisos', + 'form_tab_permissions' => 'Permisos', + 'btn_add_permission' => 'Añadir permisos', + 'btn_delete_permission' => 'Borrar permisos', + 'column_permission_label' => 'Código del permiso', + 'column_permission_required' => 'Por favor ingrese el código del permiso', + 'column_tab_label' => 'Titulo pestaña', + 'column_tab_required' => 'Por favor introduzca el permiso de el título de la pestaña', + 'column_label_label' => 'Etiqueta', + 'column_label_required' => 'Por favor introduzca permiso de etiqueta', + 'saved' => 'Permisos guardados', + 'error_duplicate_code' => 'Código de permiso duplicado: \':code\'.', + ], + 'yaml' => [ + 'save_error' => 'Error al guardar el archivo \':name\'. Consulte permisos de escritura.', + ], + 'common' => [ + 'error_file_exists' => 'El archivo ya existe: \':path\'.', + 'field_icon_description' => 'October usa Font Autumn icons: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'El directorio de destino no existe: \':path\'.', + 'error_make_dir' => 'Error al crear directorio: \':name\'.', + 'error_dir_exists' => 'El directorio ya existe!: \':path\'.', + 'template_not_found' => 'Archivo de plantilla no encontrado: \':name\'.', + 'error_generating_file' => 'Error al generar el archivo: \':path\'.', + 'error_loading_template' => 'Error al cargar el archivo plantilla: \':name\'.', + 'select_plugin_first' => 'Seleccione primero un plugin. Para ver la lista de plugin, haga clic en el icono > en la barra lateral izquierda.', + 'plugin_not_selected' => 'Plugin no seleccionado', + 'add' => 'Añadir', + ], + 'migration' => [ + 'entity_name' => 'Migración', + 'error_version_invalid' => 'La versión debe ser especificada en el siguiente formato: 1.0.1', + 'field_version' => 'Versión', + 'field_description' => 'Descripción', + 'field_code' => 'Código', + 'save_and_apply' => 'Guardar y Aplicar', + 'error_version_exists' => 'La versión de la migración ya existe.', + 'error_script_filename_invalid' => 'El nombre del archivo de migración sólo puede contener letras, digitos y guiones bajos. El nombre de comenzar con el nombre debe comenzar con una letra y no puede contener espacios.', + 'error_cannot_change_version_number' => 'No se puede cambiar el número de la versión para una versión ya aplicada.', + 'error_file_must_define_class' => 'El código de la migración debe definir una clase de migración o de seeder. Dejar en blanco si solo quieres actualizar el número de la versión.', + 'error_file_must_define_namespace' => 'La migración debe definir un namespace. Deja el campo de código en blanco si solo quieres actualizar el número de la versión.', + 'no_changes_to_save' => 'No hay cambios que guardar.', + 'error_namespace_mismatch' => 'El código de la migración debe utilizar el namespace del plugin :namespace', + 'error_migration_file_exists' => 'El archivo de migración :file ya existe. Favor usa otro nombre para la clase.', + 'error_cant_delete_applied' => 'Esta versión ya ha sido aplicada y no puede ser eliminada. Favor haz un rollback a la versión primero.', + ], + 'components' => [ + 'list_title' => 'Lista de registros', + 'list_description' => 'Mostrar una lista de registros para el modelo seleccionado', + 'list_page_number' => 'Número de página', + 'list_page_number_description' => 'Este valor es usado para determinar en qué página se encuentra el usuario.', + 'list_records_per_page' => 'Registros por página', + 'list_records_per_page_description' => 'Número de registros a mostrar en una página. Dejar en blanco para desactivar paginación.', + 'list_records_per_page_validation' => 'Invalid format of the records per page value. The value should be a number.', + 'list_no_records' => 'No records message', + 'list_no_records_description' => 'Message to display in the list in case if there are no records. Used in the default component\'s partial.', + 'list_no_records_default' => 'No se han encontrado registros', + 'list_sort_column' => 'Ordenar por columna', + 'list_sort_column_description' => 'Model column the records should be ordered by', + 'list_sort_direction' => 'Dirección', + 'list_display_column' => 'Columna para mostrar', + 'list_display_column_description' => 'Column to display in the list. Used in the default component\'s partial.', + 'list_display_column_required' => 'Por favor, selecciona una columna para mostrar.', + 'list_details_page' => 'Página de detalles', + 'list_details_page_description' => 'Página para desplegar el registro de detalles.', + 'list_details_page_no' => '--No hay página de detalles--', + 'list_sorting' => 'Sorting', + 'list_pagination' => 'Paginación', + 'list_order_direction_asc' => 'Ascendente', + 'list_order_direction_desc' => 'Descendente', + 'list_model' => 'Model class', + 'list_scope' => 'Scope', + 'list_scope_description' => 'Optional model scope to fetch the records', + 'list_scope_default' => '--select a scope, optional--', + 'list_details_page_link' => 'Enlace a la página de detalles', + 'list_details_key_column' => 'Details key column', + 'list_details_key_column_description' => 'Model column to use as a record identifier in the details page links.', + 'list_details_url_parameter' => 'URL parameter name', + 'list_details_url_parameter_description' => 'Name of the details page URL parameter which takes the record identifier.', + 'details_title' => 'Detalles de un registro', + 'details_description' => 'Despliega el detalle de un registro para el modelo seleccionado', + 'details_model' => 'Clase del modelo', + 'details_identifier_value' => 'Identifier value', + 'details_identifier_value_description' => 'Identifier value to load the record from the database. Specify a fixed value or URL parameter name.', + 'details_identifier_value_required' => 'The identifier value is required', + 'details_key_column' => 'Key column', + 'details_key_column_description' => 'Model column to use as a record identifier for fetching the record from the database.', + 'details_key_column_required' => 'The key column name is required', + 'details_display_column' => 'Display column', + 'details_display_column_description' => 'Model column to display on the details page. Used in the default component\'s partial.', + 'details_display_column_required' => 'Por favor selecciona una columna para mostrar.', + 'details_not_found_message' => 'Mensaje registro no encontrado', + 'details_not_found_message_description' => 'Mensaje que se mostrará si el registro no fue encontrado. Usado en el partial del componente por defecto.', + 'details_not_found_message_default' => 'Registro no encontrado', + ], +]; diff --git a/plugins/rainlab/builder/lang/fa.json b/plugins/rainlab/builder/lang/fa.json new file mode 100644 index 0000000..8fafe6a --- /dev/null +++ b/plugins/rainlab/builder/lang/fa.json @@ -0,0 +1,4 @@ +{ + "Builder": "افزونه ساز", + "Provides visual tools for building October plugins.": "ساخت افزونه های اکتبر به صورت دیداری" +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/fa/lang.php b/plugins/rainlab/builder/lang/fa/lang.php new file mode 100644 index 0000000..3675efa --- /dev/null +++ b/plugins/rainlab/builder/lang/fa/lang.php @@ -0,0 +1,622 @@ + [ + 'add' => 'ایجاد افزونه', + 'no_records' => 'افزونه ای یافت نشد', + 'no_description' => 'توضیحی برای این افزونه وارد نشده است', + 'no_name' => 'بدون نام', + 'search' => 'جستجو...', + 'filter_description' => 'نمایش تمام افزونه ها و یا افزونه های نوشته شده توسط شما', + 'settings' => 'تنظیمات', + 'entity_name' => 'افزونه', + 'field_name' => 'نام', + 'field_author' => 'صاحب امتیاز', + 'field_description' => 'توضحات', + 'field_icon' => 'آیکن افزونه', + 'field_plugin_namespace' => 'نیم اسپیس افزونه', + 'field_author_namespace' => 'نیم اسپیس صاحب امتیاز', + 'field_namespace_description' => 'نیم اسپیس میتواند شامل حروف لاتین و اعداد باشد و باید با یک حرف لاتین آغاز شود مانند: Blog', + 'field_author_namespace_description' => 'امکان تغییر نیم پس از ایجاد آن توسط افزونه ساز وجود ندارد نمونه ای از نیم اسپیس صاحب امتیاز: OctoberFa', + 'tab_general' => 'پارامتر های عمومی', + 'tab_description' => 'توضیحات', + 'field_homepage' => 'آدرس وب افزونه', + 'error_settings_not_editable' => 'تنظیمات این افزونه توسط افزونه ساز قابل ویرایش نمی باشد.', + 'update_hint' => 'امکان ترجمه نام و توضیحات افزونه در بخش ترجمه وجود دارد', + ], + 'author_name' => [ + 'title' => 'نام صاحب امتیاز', + 'description' => 'نام صاحب امتیاز به هنگام ایجاد افزونه جدید، این نام را همیشه میتوان در تنظیمات افزونه ویرایش کرد.', + ], + 'author_namespace' => [ + 'title' => 'نیم اسپیس صاحب امتیاز', + 'description' => 'اگر شما در فروشگاه افزونه ها حساب کاربری دارید این گزینه باید با نیم اسپیس آن حساب یکی باشد.', + ], + 'database' => [ + 'menu_label' => 'پایگاه داده', + 'no_records' => 'جدولی یافت نشد', + 'search' => 'جستجو...', + 'confirmation_delete_multiple' => 'آیا از حذف جدول های انتخاب شده اطمینان دارید؟', + 'field_name' => 'نام جدول', + 'tab_columns' => 'ستون ها', + 'column_name_name' => 'ستون', + 'column_name_required' => 'لطفا نام ستون را وارد نمایید', + 'column_name_type' => 'نوع', + 'column_type_required' => 'لطفا نوع ستون را وارد نمایید', + 'column_name_length' => 'طول', + 'column_validation_length' => 'مقدار این گزینه باید یک عدد صحیح بوده و برای داده های اعشاری در بازه 2 تا 10 باشد.', + 'column_validation_title' => 'این گزینه باید فقط شامل اعداد، حروف لاتین و خط زیر باشد.', + 'column_name_unsigned' => 'بدون علامت', + 'column_name_nullable' => 'nullable', + 'column_auto_increment' => 'افزایشی خودکار', + 'column_default' => 'پیشفرض', + 'column_auto_primary_key' => 'PK', + 'tab_new_table' => 'جدول جدید', + 'btn_add_column' => 'افزودن ستون', + 'btn_delete_column' => 'حذف ستون', + 'confirm_delete' => 'آیا از حذف ان جدول اطمینان دارید؟', + 'error_enum_not_supported' => 'جدول حاوی ستون(ها) ای با نوع enum می باشد که در حال حاظر توسط افزونه ساز پشتیبانی نمیشود.', + 'error_table_name_invalid_prefix' => 'نام جدول باید با پیشوند افزونه \':prefix\' آغاز شود. ', + 'error_table_name_invalid_characters' => 'نام جدول صحیح نمی باشد. نام جدول میتواند حاوی حروف لاتین، اعداد و خط زیر باشد و باید با حرف لاتین شروع شود. همچنین نام جدول نمیتواند شامل فاصله باشد.', + 'error_table_duplicate_column' => 'نام ستون \':column\' تکراری می باشد.', + 'error_table_auto_increment_in_compound_pk' => 'ستون افزایشی خودکار نمیتواند بخشی از کلید اصلی مرکب باشد', + 'error_table_mutliple_auto_increment' => 'جدول فقط میتواند شامل یک ستون افزایشی باشد.', + 'error_table_auto_increment_non_integer' => 'نوع ستون افزایشی باید عدد صحیح (integer) باشد.', + 'error_table_decimal_length' => 'طول نوع :type باید در قالب \'10,2\' بدون فاصله باشد.', + 'error_table_length' => 'طول نوع :type باید یک عدد صحیح باشد.', + 'error_unsigned_type_not_int' => 'خطا در ایجاد ستون \':column\'، فقط ستون هایی از نوع عدد صحیح میتوانند نشانه بدون علامت داشته باشند.', + 'error_integer_default_value' => 'مقدار پیشفرض وارد شده برای ستون \':column\' باید یک مقدار صحیح باشد.', + 'error_decimal_default_value' => 'مقدار پیشفرض وارد شده برای ستون \':column\' باید یک عدد اعشاری باشد.', + 'error_boolean_default_value' => 'مقدار پیشفرض ستون \':column\' باید 0 ویا 1 باشد', + 'error_unsigned_negative_value' => 'مقدار پیشفرض ستون \':column\' باید یک عدد صحیح مثبت باشد.', + 'error_table_already_exists' => 'نام جدول \':name\' تکراری می باشد.', + ], + 'model' => [ + 'menu_label' => 'مدل ها', + 'entity_name' => 'مدل', + 'no_records' => 'مدلی یافت نشد', + 'search' => 'جستجو...', + 'add' => 'افزودن...', + 'forms' => 'فرم ها', + 'lists' => 'لیست ها', + 'field_class_name' => 'نام کلاس', + 'field_database_table' => 'نام پایگاه داده', + 'error_class_name_exists' => 'نام مدل برای کلاس وارد شده :path تکاریست', + 'add_form' => 'فرم جدید', + 'add_list' => 'لیست جدید', + ], + 'form' => [ + 'saved' => 'فرم با موفقیت ذخیره شد.', + 'confirm_delete' => 'آیا از حذف این فرم اطمینان دارید؟', + 'tab_new_form' => 'فرم جدید', + 'property_label_title' => 'برچسب', + 'property_label_required' => 'لطفا برچسب را وارد نمایید', + 'property_span_title' => 'موقعیت', + 'property_comment_title' => 'توضیح', + 'property_comment_above_title' => 'توضیح بالا', + 'property_default_title' => 'پیشفرض', + 'property_checked_default_title' => 'انتخاب شده (پیشفرض)', + 'property_css_class_title' => 'کلاس CSS', + 'property_css_class_description' => 'کلاس CSS اختیاری جهت والد فیلد', + 'property_disabled_title' => 'غیر فعال', + 'property_hidden_title' => 'مخفی', + 'property_required_title' => 'اجباری', + 'property_field_name_title' => 'نام فیلد', + 'property_placeholder_title' => 'پیش نوشته (Placeholder)', + 'property_default_from_title' => 'فرم پیشفرض', + 'property_stretch_title' => 'کامل', + 'property_stretch_description' => 'اگر میخواهید طول فیلد درون والد خود کامل باشد این گزینه را انتخاب نمایید.', + 'property_context_title' => 'بخش', + 'property_context_description' => 'مشخص کننده نمایش فیلد در حالت های فرم', + 'property_context_create' => 'ایجاد', + 'property_context_update' => 'به روز رسانی', + 'property_context_preview' => 'پیش نمایش', + 'property_dependson_title' => 'وابستگی', + 'property_trigger_action' => 'عمل', + 'property_trigger_show' => 'نمایش', + 'property_trigger_hide' => 'عدم نمایش', + 'property_trigger_enable' => 'فعال', + 'property_trigger_disable' => 'غیر فعال', + 'property_trigger_empty' => 'خالی', + 'property_trigger_field' => 'فیلد', + 'property_trigger_field_description' => 'نام فیلدی را که عمل را اجرا میکند وارد نمایید', + 'property_trigger_condition' => 'شرط', + 'property_trigger_condition_description' => 'شرطی که در صورت درستی عمل انجام میشود. مقادیر پشتیبانی شده این فیلد: checked، unchecked، value[somevalue].', + 'property_trigger_condition_checked' => 'Checked', + 'property_trigger_condition_unchecked' => 'Unchecked', + 'property_trigger_condition_somevalue' => 'value[enter-the-value-here]', + 'property_preset_title' => 'از پیش تایین شده', + 'property_preset_description' => 'این امکان را میدهد که نام فیلد توسط فیلد دیگری مقدار دهی شده و با مبدل از پیش تعریف شده ای تبدیل شود.', + 'property_preset_field' => 'فیلد', + 'property_preset_field_description' => 'فیلد منبعی که مقدار از آن گرفته میشود را وارد نمایید.', + 'property_preset_type' => 'نوع', + 'property_preset_type_description' => 'مشخص کردن نوع تبدیل', + 'property_attributes_title' => 'ویژگی ها', + 'property_attributes_description' => 'ویژگی های HTML ای را که میخواهید به فیلد بدهید را وارد نمایید', + 'property_container_attributes_title' => 'ویژگی های والد', + 'property_container_attributes_description' => 'ویژگی های HTML ای که والد فیلد باید داشته باشد را وارد نمایید.', + 'property_group_advanced' => 'پیشرفته', + 'property_dependson_description' => 'فیلد های دیگری را که این فیلد به آنها وابسته می باشد و به هنگام تغییر آن ها این فیلد نیز تغییر پیدا میکند را وارد نمایید. هر فیلد در یک خط تعریف می شود.', + 'property_trigger_title' => 'عکس العمل', + 'property_trigger_description' => 'به فیلد این اجاره را میدهد که با تغییر فیلد دیگری ویژگی های خود را مانند حالت نمایش و مقدار خود کنترل نماید', + 'property_default_from_description' => 'مقدار پیشفرض را از مقدار فیلد دیگری بگیر.', + 'property_field_name_required' => 'ورود نام فیلد اجباریست', + 'property_field_name_regex' => 'نام فیلد میتواند شامل حروف لاتین، اعداد، خط زیر، خط تیره و کروشه باشد.', + 'property_attributes_size' => 'اندازه', + 'property_attributes_size_tiny' => 'خیلی کوچک', + 'property_attributes_size_small' => 'کوچک', + 'property_attributes_size_large' => 'متوسط', + 'property_attributes_size_huge' => 'بزرک', + 'property_attributes_size_giant' => 'خیلی بزرگ', + 'property_comment_position' => 'محل قرارگیری توضیح', + 'property_comment_position_above' => 'قبل', + 'property_comment_position_below' => 'بعد', + 'property_hint_path' => 'آدرس فایل بخش راهنما', + 'property_hint_path_description' => 'آدرس فایل بخشی که شامل متن راهنما می باشد. علامت $ به پوشه افزونه ها اشاره میکند برای مثال: $/acme/blog/partials/_hint.htm', + 'property_hint_path_required' => 'لطفا مسیر بخش راهنما را وارد نمایید.', + 'property_partial_path' => 'مسیر بخش', + 'property_partial_path_description' => 'مسیر فال مربوط به بخش را وارد نمایید. علامت $ به پوشه پلاگین ها اشاره میکند برای مثال: $/acme/blog/partials/_hint.htm', + 'property_partial_path_required' => 'لطفا مسیر فایل بخش را وارد نمایید.', + 'property_code_language' => 'زبان', + 'property_code_theme' => 'قالب', + 'property_theme_use_default' => 'استفاده از قالب پیشفرض', + 'property_group_code_editor' => 'ویرایشگر کد', + 'property_gutter' => 'شیار', + 'property_gutter_show' => 'قابل مشاهده', + 'property_gutter_hide' => 'مخقی', + 'property_wordwrap' => 'Word wrap', + 'property_wordwrap_wrap' => 'Wrap', + 'property_wordwrap_nowrap' => 'Don\'t wrap', + 'property_fontsize' => 'اندازه فونت', + 'property_codefolding' => 'Code folding', + 'property_codefolding_manual' => 'دستی', + 'property_codefolding_markbegin' => 'علامت گذازی آغاز', + 'property_codefolding_markbeginend' => 'علامت گذاری آغاز و پایان', + 'property_autoclosing' => 'بستن خودکار', + 'property_enabled' => 'فعال', + 'property_disabled' => 'غیر فعال', + 'property_soft_tabs' => 'استفاده از فاصله به جای Tab', + 'property_tab_size' => 'اندازه Tab', + 'property_readonly' => 'فقط خواندنی', + 'property_use_default' => 'استفاده از تنظیمات پیشفرض', + 'property_options' => 'گزینه ها', + 'property_prompt' => 'متن', + 'property_prompt_description' => 'متن نمایش داده شده برای دکمه ایجاد', + 'property_prompt_default' => 'افزودن گزینه جدید', + 'property_available_colors' => 'رنگ های موجود', + 'property_available_colors_description' => 'لیست رنگ های موجود در قالب هگزادسیمال (#FF0000). برای استفاده از رنگ های پیشفرض این گزینه را خالی رها کنید. در هر خط یک رنگ وارد نمایید.', + 'property_datepicker_mode' => 'نحوه نمایش', + 'property_datepicker_mode_date' => 'تاریخ', + 'property_datepicker_mode_datetime' => 'تاریخ و ساعت', + 'property_datepicker_mode_time' => 'ساعت', + 'property_datepicker_min_date' => 'کمترین تاریخ', + 'property_datepicker_max_date' => 'بیشترین تاریخ', + 'property_fileupload_mode' => 'نحوه نمایش', + 'property_fileupload_mode_file' => 'فایل', + 'property_fileupload_mode_image' => 'تصویر', + 'property_group_fileupload' => 'ارسال فایل', + 'property_fileupload_image_width' => 'عرض تصویر', + 'property_fileupload_image_width_description' => 'گزینه اختیاری جهت تغییر اندازه عرض تصویر که فقط در حالت تصویر مورد استفاده قرار میگیرد.', + 'property_fileupload_invalid_dimension' => 'مقدار وارد شده صحیح نیست. لطفا یک عدد وارد نمایید', + 'property_fileupload_image_height' => 'طول تصویر', + 'property_fileupload_image_height_description' => 'گزینه اختیاری جهت تغییر اندازه طول تصویر که فقط در حالت تصویر مورد استفاده قرار میگیرد.', + 'property_fileupload_file_types' => 'نوع فایل ها', + 'property_fileupload_file_types_description' => 'گزینه اختیاری جهت تایین پسوند فایل های ارسالی که با کاما از هم جدا شنده اند برای مثال: zip,txt', + 'property_fileupload_mime_types' => 'MIME types', + 'property_fileupload_mime_types_description' => 'لیست اختیاری از MIME Type های مجاز جهت ارسال فایل که با کاما از هم جدا شده اند برای مثال: bin,txt', + 'property_fileupload_use_caption' => 'از عنوان استفاده شود', + 'property_fileupload_use_caption_description' => 'اجازه ورود عنوان و توضیحات برای فایل ارسالی.', + 'property_fileupload_thumb_options' => 'گزینه های تصویر بند انگشتی', + 'property_fileupload_thumb_options_description' => 'مدیریت گزینه های تولید خودکار تصویر بند انگشتی که فقط در حالت تصویر مورد استفاده قرار میگیرد.', + 'property_fileupload_thumb_mode' => 'حالت نمایش', + 'property_fileupload_thumb_auto' => 'خودکار', + 'property_fileupload_thumb_exact' => 'دقیقا', + 'property_fileupload_thumb_portrait' => 'Portrait', + 'property_fileupload_thumb_landscape' => 'Landscape', + 'property_fileupload_thumb_crop' => 'Crop', + 'property_fileupload_thumb_extension' => 'پسوند فایل', + 'property_name_from' => 'نام ستون', + 'property_name_from_description' => 'نام ستون ارتباطی جهت نمایش نام.', + 'property_description_from' => 'ستون توضیحات', + 'property_description_from_description' => 'نام ستون ارتباطی جهت نمایش توضیحات.', + 'property_recordfinder_prompt' => 'متن', + 'property_recordfinder_prompt_description' => 'متنی که به هنگام نبود هیچ رکوردی به نمایش در می آید. %s بیانگر آیکن جستجو می باشد. جهت استفاده از مقدار پیشفرض این گزینه را خالی رها کنید.', + 'property_recordfinder_list' => 'تنظیمات لیست', + 'property_recordfinder_list_description' => 'مرجعی برای تعریف ستون های لیست. علامت $ به پوشه افزونه ها اشاره میکند برای مثال: $/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => 'لطفا آدرس فایل YAML را وارد نمایید', + 'property_group_recordfinder' => 'انتخابگر رکورد', + 'property_mediafinder_mode' => 'حالت نمایش', + 'property_mediafinder_mode_file' => 'فایل', + 'property_mediafinder_mode_image' => 'تصویر', + 'property_group_relation' => 'ارتباط', + 'property_relation_select' => 'تحديد', + 'property_relation_select_description' => 'كونكات أعمدة متعددة معا لعرض اسم', + 'property_relation_prompt' => 'متن', + 'property_relation_prompt_description' => 'متنی که به هنگام موجود نبودن موردی جهت انتخاب نمایش داده میشود.', + 'control_group_standard' => 'استاندارد', + 'control_group_widgets' => 'ابزارک ها', + 'click_to_add_control' => 'افزودن کنترل', + 'loading' => 'درحال بارگذاری...', + 'control_text' => 'متن', + 'control_text_description' => 'ابزار ورود متن تک خطی', + 'control_password' => 'کلمه عبور', + 'control_password_description' => 'ابزار ورود کلمه عبور', + 'control_checkbox' => 'چک باکس', + 'control_checkbox_description' => 'یک چک باکس', + 'control_switch' => 'سویچ', + 'control_switch_description' => 'سوئیچ روشن و خاموش که میتواند جایگزین چک باکس شود.', + 'control_textarea' => 'متن چند خطی', + 'control_textarea_description' => 'ابزار ورود متن چند خطی', + 'control_dropdown' => 'لیست بازشونده', + 'control_dropdown_description' => 'لیست بازشونده با موارد مشخص و یا متغیر', + 'control_unknown' => 'نوع ابزار :type یافت نشد', + 'control_repeater' => 'تکرار کننده', + 'control_repeater_description' => 'تکرار کننده مجموعه ای از ابزار ها', + 'control_number' => 'عدد', + 'control_number_description' => 'ابزار تک خطی جهت ورود عدد.', + 'control_hint' => 'هشدار', + 'control_hint_description' => 'نشان دهنده یک بخش به عنوان هشدار و یا راهنمایی که میتواند توسط کاربر مخفی شود.', + 'control_partial' => 'بخش', + 'control_partial_description' => 'نشان دهنده محتوی یک بخش', + 'control_section' => 'قسمت', + 'control_section_description' => 'نمایش یک قسمت از فرم با عنوان و زیر عنوان', + 'control_radio' => 'لیست رادیویی', + 'control_radio_description' => 'لیستی از انتخاب گر های رادیویی که در هر لحظه فقط یک مورد میتواند انتخاب شود.', + 'control_radio_option_1' => 'گزینه 1', + 'control_radio_option_2' => 'گزینه 2', + 'control_checkboxlist' => 'لیست چک باکس', + 'control_checkboxlist_description' => 'لیستی از چک باکس', + 'control_codeeditor' => 'ادیتور کد', + 'control_codeeditor_description' => 'ادیتور کد که جهت ورود کد مورد استفاده قرار میگیرد', + 'control_colorpicker' => 'انتخابگر رنگ', + 'control_colorpicker_description' => 'فیلدی جهت انتخاب کد هگزادسیمال رنگ', + 'control_datepicker' => 'انتخابگر تاریخ', + 'control_datepicker_description' => 'ابزاری جهت انتخای تاریخ و زمان', + 'control_richeditor' => 'ویرایشگر متن', + 'control_richeditor_description' => 'ابزاری جهت ویرایش و فرمت بندی متن', + 'control_markdown' => 'ویرایشگر مارک داون', + 'control_markdown_description' => 'ویرایشگر ابتدایی جهت ورود و ویرایش متن در قالب مارک داون', + 'control_fileupload' => 'ارسال فایل', + 'control_fileupload_description' => 'ارسال کننده فایل جهت ارسال تصویر و یا فایل', + 'control_recordfinder' => 'انتخاب گر موارد', + 'control_recordfinder_description' => 'ابزاری جهت انتخاب موارد مرتبط در پایگاه داده با امکان جستجو', + 'control_mediafinder' => 'انتخابگر چند رسانه ای', + 'control_mediafinder_description' => 'ابزاری جهت انتخاب فایل های چند رسانه ای در ابزار مدیریت چند رسانه ای', + 'control_relation' => 'ارتباط', + 'control_relation_description' => 'نمایش لیست بازشونده و یا لیست چک باکس جهت انتخاب ارتباطات پایگاه داده', + 'error_file_name_required' => 'لطفا نام فایل فرم را وارد نمایید.', + 'error_file_name_invalid' => 'نام فایل میتواند شامل حروف لاتین، اعداد، خط زیر، خط تیره و یا نقطه باشد.', + 'span_left' => 'چپ', + 'span_right' => 'راست', + 'span_full' => 'کامل', + 'span_auto' => 'خودکار', + 'empty_tab' => 'بخش خالی', + 'confirm_close_tab' => 'بخش حاوی ابزار هایی می باشد که پاک خواهند شد. آیا میخواهید ادامه دهید؟', + 'tab' => 'بخش فرم', + 'tab_title' => 'عنوان', + 'controls' => 'ابزار ها', + 'property_tab_title_required' => 'وارد کردن عنوان بخش اجباریست', + 'tabs_primary' => 'بخش اصلی', + 'tabs_secondary' => 'بخش ثانویه', + 'tab_stretch' => 'کامل', + 'tab_stretch_description' => 'مشخص میکند که طول بخش برابر با طول والد خود باشد یا خیر', + 'tab_css_class' => 'کلاس CSS', + 'tab_css_class_description' => 'مقدار دهی خاصیت کلاس دربرگیرنده ابزار', + 'tab_name_template' => 'بخش %s', + 'tab_already_exists' => 'بخشی با نام مشخص شده وجود دارد', + ], + 'list' => [ + 'tab_new_list' => 'لیست جدید', + 'saved' => 'لیست با موفقیت ذخیره شد.', + 'confirm_delete' => 'آیا از حذف این لیست اطمینان دارید؟', + 'tab_columns' => 'ستون ها', + 'btn_add_column' => 'ستون جدید', + 'btn_delete_column' => 'حذف ستون', + 'column_dbfield_label' => 'فیلد', + 'column_dbfield_required' => 'لطفا فیلد مدل را وارد نمایید', + 'column_name_label' => 'عنوان', + 'column_label_required' => 'لطفا عنوان ستون را وارد نمایید.', + 'column_type_label' => 'نوع', + 'column_type_required' => 'لطفا نوع ستون را وارد نمایید.', + 'column_type_text' => 'متن', + 'column_type_number' => 'عدد', + 'column_type_switch' => 'سوییچ', + 'column_type_datetime' => 'تاریخ و زمان', + 'column_type_date' => 'تاریخ', + 'column_type_time' => 'زمان', + 'column_type_timesince' => 'تفاوت زمانی', + 'column_type_timetense' => 'تفاوت زمانی', + 'column_type_select' => 'انتخاب', + 'column_type_partial' => 'بخش', + 'column_label_default' => 'پیشفرض', + 'column_label_searchable' => 'قابلیت جستجو', + 'column_label_sortable' => 'قابلیت مرتب سازی', + 'column_label_invisible' => 'مخفی', + 'column_label_select' => 'انتخاب', + 'column_label_relation' => 'ارتباط پایگاه داده', + 'column_label_css_class' => 'کلاس CSS', + 'column_label_width' => 'عرض', + 'column_label_path' => 'مسیر', + 'column_label_format' => 'قالب', + 'column_label_value_from' => 'مقدار گرفته شده از', + 'error_duplicate_column' => 'نام \':column\' برای ستون تکراری میباشد.', + ], + 'controller' => [ + 'menu_label' => 'کنترلر ها', + 'no_records' => 'کنترلری در افزونه یافت نشد.', + 'controller' => 'کنترلر', + 'behaviors' => 'کنترل کننده رفتار از پیش تایین شده', + 'new_controller' => 'کنترلر جدید', + 'error_controller_has_no_behaviors' => 'کنترلر حاوی کنترل کننده رفتار از پیش تایین شده که حاوی تنظیمات باشد ندارد', + 'error_invalid_yaml_configuration' => 'خطایی در بارگذاری فایل :file مربوط به تنظیمات کنترل کننده رفتار از پیش تایین شده به وجود آمده است', + 'behavior_form_controller' => 'کنترل کننده رفتار از پیش تایین شده فرم', + 'behavior_form_controller_description' => 'افزودن امکان مدیریت فرم ها به صفحه مدیریت. این امکان سه صفحه ایجاد، به روزرسانی و پیش نمایش را اضافه میکند.', + 'property_behavior_form_placeholder' => '--انتخاب فرم--', + 'property_behavior_form_name' => 'نام', + 'property_behavior_form_name_description' => 'نام شی ای که توسط فرم مدیریت می شود', + 'property_behavior_form_name_required' => 'لطفا نام فرم را وارد نمایید.', + 'property_behavior_form_file' => 'تنظیمات فرم', + 'property_behavior_form_file_description' => 'به یک فایل تعریف فرم اشاره میکند.', + 'property_behavior_form_file_required' => 'لطفا آدرس فایل تنظیمات فرم را وارد نمایید.', + 'property_behavior_form_model_class' => 'کلاس مدل', + 'property_behavior_form_model_class_description' => 'نام کلاس مدل که داده ها از آن بارگذاری شده و ذخیره می شوند.', + 'property_behavior_form_model_class_required' => 'لطفا نام کلاس مدل را وارد نمایید', + 'property_behavior_form_default_redirect' => 'مسیر انتقال پیشفرض', + 'property_behavior_form_default_redirect_description' => 'آدرس صفحه ای جهت انتقال به هنگامی که فرم ذخیره میشود یا کاربر از ادامه کار انصراف می دهد.', + 'property_behavior_form_create' => 'صفحه ایجاد', + 'property_behavior_form_redirect' => 'انتقال', + 'property_behavior_form_redirect_description' => 'آدرس صفحه ای که به هنگام ایجاد مورد جدید به آن انتقال داده میشود', + 'property_behavior_form_redirect_close' => 'انتقال به هنگام خروج', + 'property_behavior_form_redirect_close_description' => 'آدرس صفحه ای که به هنگام کلیک دکمه انتقال و خروج به آن انتقال داده می شود.', + 'property_behavior_form_flash_save' => 'پیغام نمایش داده شده به هنگام ذخیره', + 'property_behavior_form_flash_save_description' => 'پیغامی که به هنگام ذخیره مورد نمایش داده می شود.', + 'property_behavior_form_page_title' => 'عنوان صفحه', + 'property_behavior_form_update' => 'صفحه ویرایش', + 'property_behavior_form_update_redirect' => 'انتقال', + 'property_behavior_form_create_redirect_description' => 'صفحه ای که به هنگام ذخیره مورد به آن انتقال داده می شود.', + 'property_behavior_form_flash_delete' => 'پیغام حذف', + 'property_behavior_form_flash_delete_description' => 'پیغامی که به هنگام حذف نمایش داده میشود.', + 'property_behavior_form_preview' => 'صفحه پیش نمایش', + 'behavior_list_controller' => 'کنترل کننده رفتار از پیش تایین شده لیست', + 'behavior_list_controller_description' => 'امکان نمایش لیستی با قابلیت جستجو و مرتب سازی را به کنترلر اظافه میکند. این امکان صفحه اصلی (index) را در کنترلر ایجاد میکند.', + 'property_behavior_list_title' => 'عنوان لیست', + 'property_behavior_list_title_required' => 'لطفا عنوان لیست را وارد نمایید', + 'property_behavior_list_placeholder' => '--انتخاب لیست--', + 'property_behavior_list_model_class' => 'کلاس مدل', + 'property_behavior_list_model_class_description' => 'نام کلاس مدلی که اطلاعات لیست از آن بارگذاری میشود.', + 'property_behavior_form_model_class_placeholder' => '--انتخاب مدل--', + 'property_behavior_list_model_class_required' => 'لطفا کلاس مدل را انتخاب نمایید', + 'property_behavior_list_model_placeholder' => '--انتخاب مدل--', + 'property_behavior_list_file' => 'فایل تنظیمات لیست', + 'property_behavior_list_file_description' => 'به یک فایل تعریف لیست اشاره میکند', + 'property_behavior_list_file_required' => 'لطفا مسیر فایل تنظیمات لیست را وارد نمایید.', + 'property_behavior_list_record_url' => 'آدرس مورد', + 'property_behavior_list_record_url_description' => 'آدرس صفحه هر مورد در لیست که در آن :id به مشخصه لیست اشاره میکند برای مثال: users/update:id', + 'property_behavior_list_no_records_message' => 'پیغام خالی بودن لیست', + 'property_behavior_list_no_records_message_description' => 'پیغامی که به هنگام خالی بودن لیست به نمایش در می آید.', + 'property_behavior_list_recs_per_page' => 'تعداد موارد در هر صفحه', + 'property_behavior_list_recs_per_page_description' => 'تعداد موارد در هر صفحه را مشخص میکند برای نمایش تمام موارد در یک صفحه این مقدار را 0 قرار دهید. مقدار پیشفرض: 0', + 'property_behavior_list_recs_per_page_regex' => 'مقدار تعداد موارد در هر صفحه باید یک عدد صحیح باشد.', + 'property_behavior_list_show_setup' => 'نمایش دکمه تنظیمات', + 'property_behavior_list_show_sorting' => 'نمایش مرتب سازی', + 'property_behavior_list_default_sort' => 'مرتب سازی پیشفرض', + 'property_behavior_form_ds_column' => 'ستون', + 'property_behavior_form_ds_direction' => 'جهت مرتب سازی', + 'property_behavior_form_ds_asc' => 'صعودی', + 'property_behavior_form_ds_desc' => 'نزولی', + 'property_behavior_list_show_checkboxes' => 'نمایش چک باکس', + 'property_behavior_list_onclick' => 'کنترل کننده کلیک', + 'property_behavior_list_onclick_description' => 'کد شخصی سازی شده جاوا اسکریپت به هنگام کلیک هر رکورد.', + 'property_behavior_list_show_tree' => 'نمایش درخت وار', + 'property_behavior_list_show_tree_description' => 'نمایش درخت وار موارد والد و فرزندی.', + 'property_behavior_list_tree_expanded' => 'درخت باز شده', + 'property_behavior_list_tree_expanded_description' => 'آیا تمامی موارد درخت به صورت پیشفرض باز باشند؟', + 'property_behavior_list_toolbar' => 'نوار ابزار', + 'property_behavior_list_toolbar_buttons' => 'بخش دکمه ها', + 'property_behavior_list_toolbar_buttons_description' => 'به یک فایل بخش موجود در کنترلر اشاره میکند. به عنوان مثال: list_toolbar', + 'property_behavior_list_search' => 'جستجو', + 'property_behavior_list_search_prompt' => 'متن جستجو', + 'property_behavior_list_filter' => 'تنظیمات فیلتر', + 'error_controller_not_found' => 'فایل کنترلر یافت نشد', + 'error_invalid_config_file_name' => 'تعریف فایل (:file) مربوط به کنترل کننده از پیش تایین شده :class صحیح نمی باشد.', + 'error_file_not_yaml' => 'فایل تنطیمات (:file) مربوط به کنترل کننده از پیش تایین شده باید از نوع YAML باشد.', + 'saved' => 'کنترلر با موفقیت ذخیره شدو', + 'controller_name' => 'نام کنترلر', + 'controller_name_description' => 'نام کنترلر مشخص کننده نام و آدرس آن در صفحه مدیریت می باشد و باید از استاندارد های نامگذاری متغیر در PHP پیروی کند. همچنین نام باید با حرف بزرگ لاتین شروع شود برای مثال: Categories', + 'base_model_class' => 'کلاس مدل', + 'base_model_class_description' => 'نام کلاس مدلی را که جهت استفاده توسط کنترل کننده های رفتار از پیش تایین شده مورد استفاده قرار میگیرد را وارد نمایید.', + 'base_model_class_placeholder' => '--انتخاب مدل--', + 'controller_behaviors' => 'کنترل کننده های رفتار از پیش تایین شده', + 'controller_behaviors_description' => 'لطفا کنترل کننده های رفتار از پیش تایین شده برای کنترلر را انتخاب نمایید. سازنده فایل های نمایش مربوط به هر یک از آنها را به صورت خودکار ایجاد میکند.', + 'controller_permissions' => 'مجوزهای دسترسی', + 'controller_permissions_description' => 'مجوز های دسترسی برای هر یک از بخش های کنترلر را وارد نمایید. مجوز ها در بخش مجوز ها قابل تعریف میباشند و شما میتوانید آنها را بعدا در کد PHP ویرایش نمایید.', + 'controller_permissions_no_permissions' => 'این افزونه دارای مجوزی نمی باشد.', + 'menu_item' => 'منوی فعال', + 'menu_item_description' => 'منو ای را که میخواهید به هنگام نمایش صفحه کنترلر فعال شود انتخاب نمایید. این گزینه بعدا در کد PHP کنترلر قابل تغییر می باشد.', + 'menu_item_placeholder' => '--انتخاب منو--', + 'error_unknown_behavior' => 'کنترل کننده رفتار از پیش تایین شده :class در شی مدیریت این موارد اضافه نشده است.', + 'error_behavior_view_conflict' => 'کنترل کننده های رفتار از پیش تایین شده انتخاب شده دارای موارد تداخلی (:view) می باشد و نمیتواند همزمان استفاده شود.', + 'error_behavior_config_conflict' => 'کنترل کننده های رفتار انتخاب شده حاوی فایل تنظیمات (:file) تداخلی بود و نمی توانند همزمان استفاده شوند.', + 'error_behavior_view_file_not_found' => 'قالب نمایشی :view در کنترل کننده رفتار از پیش تایین شده :class یافت نشد.', + 'error_behavior_config_file_not_found' => 'قالب فایل تنظیمات :file در کنترل کننده رفتار از پیش تایین شده :class یافت نشد.', + 'error_controller_exists' => 'فایل کنترلر :file قبلا ایجاد شده است.', + 'error_controller_name_invalid' => 'نام کنترلر صحیح نمی باشد. نام میتواند شامل حروف لاتین و اعداد بوده و باید با یک حرف بزرگ لاتین شروع شود.', + 'error_behavior_view_file_exists' => 'فایل نمایشی کنترلر :view قبلا ایجاد شده است.', + 'error_behavior_config_file_exists' => 'فایل تنظیمات :file مربوط به کنترل کننده رفتار از پیش تایین شده قبلا ایجاد شده است.', + 'error_save_file' => 'خطا به هنگام ذخیره فایل کنترلر: :file', + 'error_behavior_requires_base_model' => 'کنترل کننده رفتار از پیش تایین شده :behavior نیاز به انتخاب شدن کلاس مدل دارد.', + 'error_model_doesnt_have_lists' => 'مدل انتخاب شده حاوی لیستی نمی باشد. لطفا یک لیست برای آن ایجاد کنید.', + 'error_model_doesnt_have_forms' => 'مدل انتخاب شده حاوی فرمی نمی باشد. لطفا یک فرم برای آن ایجاد کنید.', + ], + 'version' => [ + 'menu_label' => 'ویرایش ها', + 'no_records' => 'ویرایشی برای افزونه یافت نشد', + 'search' => 'جستجو...', + 'tab' => 'ویرایش ها', + 'saved' => 'ویرایش با موفقیت ذخیره شد.', + 'confirm_delete' => 'آیا از حذف این ویرایش اطمینان دارید؟', + 'tab_new_version' => 'ویرایش جدید', + 'migration' => 'ساختار پایگاه داده', + 'seeder' => 'داده های پایگاه داده', + 'custom' => 'افزودن عدد ویرایش', + 'apply_version' => 'اعمال ویرایش', + 'applying' => 'درحال اعمال...', + 'rollback_version' => 'عقب گرد ویرایش', + 'rolling_back' => 'درحال عقبگرد...', + 'applied' => 'ویرایش با موفقیت اعمال شد.', + 'rolled_back' => 'ویرایش با موفقیت به عقب بازگشت.', + 'hint_save_unapplied' => 'شما یک نسخه اعمال نشده را ذخیره کردید. این نسخه ها به هنگام ورود شما و یا کاربر دیگری و یا ذخیره جدولی در پایگاه داده به صورت خودکار اعمال خواهند شد.', + 'hint_rollback' => 'ویرایش های قبلی و بازگشت داده شده به صورت خودکار در هنگام اعمال ویرایش جدید تر، ورود به بخش مدیریت و یا ایجاد و ذخیره جدولی در پایگاه داده به صورت خودکار اعمال خواهند شد.', + 'hint_apply' => 'اعمال یک ویرایش تمامی ویرایش های قبلی اعمال نشده را نیز اعمال خواهد کرد', + 'dont_show_again' => 'مجددا نمایش نده.', + 'save_unapplied_version' => 'ذخیره ویرایش اعمال نشده', + ], + 'menu' => [ + 'menu_label' => 'منوی مدیریت', + 'tab' => 'منو ها', + 'items' => 'موارد منو', + 'saved' => 'این منو با موفقیت ذخیره شد.', + 'add_main_menu_item' => 'افزودن منوی جدید در ریشه', + 'new_menu_item' => 'مورد منو', + 'add_side_menu_item' => 'افزودن زیر منو', + 'side_menu_item' => 'مورد منوی کناری', + 'property_label' => 'عنوان', + 'property_label_required' => 'لطفا عنوان منو را وارد نمایید', + 'property_url_required' => 'لطفا آدرس منو را وارد نمایید', + 'property_url' => 'آدرس', + 'property_icon' => 'آیکن', + 'property_icon_required' => 'لطفا آیکن منو را وارد نمایید', + 'property_permissions' => 'مجوز ها', + 'property_order' => 'ترتیب', + 'property_order_invalid' => 'مقدار وارد شده برای ترتیب منو باید عدد صحیح باشد.', + 'property_order_description' => 'ترتیب منو مشخص کننده ترتیب قرارگیری آن می باشد و اگر وارد نشود منو در انتهای لیست قرار میگیرد. مقدار این مورد بهتر است بیش از عدد 100 باشد.', + 'property_attributes' => 'خصوصیات HTML', + 'property_code' => 'کد', + 'property_code_invalid' => 'کد میتواند حاوی حروف لاتین و اعداد باشد.', + 'property_code_required' => 'لطفا کد مربوط به منو را وارد نمایید.', + 'error_duplicate_main_menu_code' => 'کد \':code\' وارد شده برای منوی اصلی تکراریست.', + 'error_duplicate_side_menu_code' => 'کد \':code\' وارد شده برای منوی کناری تکراریست.', + ], + 'localization' => [ + 'menu_label' => 'بومی سازی', + 'language' => 'زبان', + 'strings' => 'رشته های متنی', + 'confirm_delete' => 'آیا از حذف این زبان اطمینان دارید؟', + 'tab_new_language' => 'ربان جدید', + 'no_records' => 'زبانی یافت نشد.', + 'saved' => 'فایل زبان با موفقیت ذخیره شد.', + 'error_cant_load_file' => 'فایل زبان مورد درخواست یافت نشد.', + 'error_bad_localization_file_contents' => 'خطا در بارگذاری فایل زبان. فایل زبان میتواند شامل تعریف آرایه و رشته های متنی باشد.', + 'error_file_not_array' => 'خطا در بارگذاری فایل زبان. فایل زبان باید یک آرایه بازگرداند/', + 'save_error' => 'خطا در ذخیره فایل \':name\'. لطفا مجوز خای ذخیره فایل را بررسی نمایید.', + 'error_delete_file' => 'خطا در حذف فایل زبان.', + 'add_missing_strings' => 'افزودن رشته های جدید', + 'copy' => 'کپی', + 'add_missing_strings_label' => 'زبانی را که حاوی رشته جدید میباشد را انتخاب نمایید.', + 'no_languages_to_copy_from' => 'زبان دیگری جهت کپی رشته های جدید موجود نمی باشد.', + 'new_string_warning' => 'رشته یا بخش جدید', + 'structure_mismatch' => 'ساختار فایل منبع زبان با فایلی که در حال ویرایش می باشد مطابقت ندارد. برخی رشته ها در فایل در حال ویرایش فایل منبع را دچار مشکل میکند و فایل ها به صورت خودکار قابلیت تجمیع ندارند. ', + 'create_string' => 'ایجاد رشته متنی جدید', + 'string_key_label' => 'کلید رشته متنی', + 'string_key_comment' => 'کلید رشته متنی را که از نقطه به عنوان جدا کننده بخش ها استفاده میکند وارد نمایید. به عنوان مثال: plugin.search. رشته متنی در زبان پیشفرض بومی سازی افزونه ذخیره خواهد شد.', + 'string_value' => 'مقدار رشته متنی', + 'string_key_is_empty' => 'ورود کلید رشته متنی اجباریست.', + 'string_value_is_empty' => 'ورود مقدار رشته متنی اجباریست.', + 'string_key_exists' => 'کلید رشته وارد شده تکراری می باشد.', + ], + 'permission' => [ + 'menu_label' => 'مجوز های دسترسی', + 'tab' => 'مجوز های دسترسی', + 'form_tab_permissions' => 'مجوز های دسترسی', + 'btn_add_permission' => 'مجوز دسترسی جدید', + 'btn_delete_permission' => 'حذف مجوز دسترسی', + 'column_permission_label' => 'کد مجوز دسترسی', + 'column_permission_required' => 'لطفا کد مجوز دسترسی را وارد نمایید', + 'column_tab_label' => 'عنوان بخش', + 'column_tab_required' => 'لطفا عنوان بخش مجوز دسترسی را وارد نمایید', + 'column_label_label' => 'عنوان', + 'column_label_required' => 'لطفا عنوان مجوز دسترسی را وارد نمایید.', + 'saved' => 'مجوز های دسترسی با موفقیت ذخیره شدند.', + 'error_duplicate_code' => 'کد وارد شده \':code\' برای مجوز دسترسی تکراری می باشد.', + ], + 'yaml' => [ + 'save_error' => 'خطا در ذخیره فایل \':name\'. لطفا مجوزهای مربوط به نوشتن بر روی دیسک را بررسی نمایید.', + ], + 'common' => [ + 'error_file_exists' => 'فایل \':path\' قبلا ایجاد شده است.', + 'field_icon_description' => 'اکتبر از آیکن فونت خود به آدری http://daftspunk.github.io/Font-Autumn استفاده می نماید', + 'destination_dir_not_exists' => 'پوشه هدف به آدرس \':path\' یافت نشد.', + 'error_make_dir' => 'خطا در ایجاد پوشه به آدرس \':name\'', + 'error_dir_exists' => 'پوشه \':path\' قبلا ایجاد شده است.', + 'template_not_found' => 'فایل قالب \':name\' یافت نشد.', + 'error_generating_file' => 'خطا در تولید فایل \':path\'.', + 'error_loading_template' => 'خطا در بارگذاری قالب \':name\'.', + 'select_plugin_first' => 'لطفا افزونه ای را انتخاب نمایید. جهت انتخاب افزونه بر روی آیکون > در سمت راست منوی کناری کلیک نمایید.', + 'plugin_not_selected' => 'افزونه ای انتخاب نشده است', + 'add' => 'افرودن', + ], + 'migration' => [ + 'entity_name' => 'ساختار بانک اطلاعاتی', + 'error_version_invalid' => 'ویرایش باید در این قالب وارد شود: 1.0.1', + 'field_version' => 'ویرایش', + 'field_description' => 'توضیحات', + 'field_code' => 'کد', + 'save_and_apply' => 'ذخیره و اعمال', + 'error_version_exists' => 'فایل ساختار بانک اطلاعاتی قبلا تعریف شده است.', + 'error_script_filename_invalid' => 'نام فایل ساختار بانک اطلاعاتی فقط میتواند حاوی حروف لاتین، اعداد و خط زیر باشد و با یک حرف لاتین بزرگ شروع شود.', + 'error_cannot_change_version_number' => 'شماره ویرایش اعمال شده قابل تغییر نمی باشد.', + 'error_file_must_define_class' => 'کد ساختار بانک اطلاعاتی باید کلاس migration و یا seeder را تعریف کند. اگر فقط میخواهید فقط نسخه را افزایش دهید کد را خالی بگذارید.', + 'error_file_must_define_namespace' => 'ساختار بانک اطلاعاتی باشد شامل نیم اسپیس باشد. اگر فقط میخواهید شماره ویرایش را افزایش دهید کد را خالی بگذارید.', + 'no_changes_to_save' => 'تغییراتی جهت ذخیره وجود ندارد.', + 'error_namespace_mismatch' => 'کد ساختار بانک اطلاعاتی باید از نیم اسپیس افزونه :namespace استفاده نماید.', + 'error_migration_file_exists' => 'فایل ساختار بانک اطلاعات :file وجود دارد لطفا نام دیگری را وارد نمایید.', + 'error_cant_delete_applied' => 'این ویرایش اعمال شده است و شما نمیتوانید آن را حذف کنید لطفا جهت حذف آن ویرایش را به عقب باز گردانید.', + ], + 'components' => [ + 'list_title' => 'لیست موارد', + 'list_description' => 'نمایش لیستی از موارد مدل انتخاب شده', + 'list_page_number' => 'شماره صفحه', + 'list_page_number_description' => 'این مقدار جهت تعیین صفحه ای که کاربر در آن می باشد مورد استفاده قرار میگیرد.', + 'list_records_per_page' => 'تعداد موارد در هر صفحه', + 'list_records_per_page_description' => 'تعداد مواردی که در هر صفحه به نمایش در می آیند. جهت استفاده نکردن از خاصیت چند صفحه ای این مورد را خالی بگذارید.', + 'list_records_per_page_validation' => 'مقدار وارد شده در تعداد موارد در هر صفحه باید یک عدد صحیح باشد.', + 'list_no_records' => 'پیغام خالی بودن لیست', + 'list_no_records_description' => 'پیغامی که به هنگام نبودن موردی جهت نمایش به نمایش در می آید.', + 'list_no_records_default' => 'موردی یافت نشد', + 'list_sort_column' => 'مرتب سازی بر اساس', + 'list_sort_column_description' => 'ستونی که موارد بر اساس آن باید مرتب شوند', + 'list_sort_direction' => 'ترتیب مرتب سازی', + 'list_display_column' => 'نمایش ستون', + 'list_display_column_description' => 'ستونی که در لیست به نمایش در می آید.', + 'list_display_column_required' => 'لطفا ستونی را جهت نمایش انتخاب کنید.', + 'list_details_page' => 'صفحه جزییات', + 'list_details_page_description' => 'صفحه ای جهت نمایش جزییات موارد', + 'list_details_page_no' => '--صفحه جزییات موجود نیست--', + 'list_sorting' => 'مرتب سازی', + 'list_pagination' => 'صفحه بندی', + 'list_order_direction_asc' => 'صعودی', + 'list_order_direction_desc' => 'نزولی', + 'list_model' => 'کلاس مدل', + 'list_scope' => 'محدوده', + 'list_scope_description' => 'محدوده اختیاری کلاس مدل جهت واکشی موارد', + 'list_scope_default' => '--محدوده را انتخاب نمایید، اختیاری--', + 'list_details_page_link' => 'آدرس صفحه جزییات', + 'list_details_key_column' => 'ستون کلید جزییات', + 'list_details_key_column_description' => 'ستونی در مدل به عنوان کلید که مورد جهت نمایش جزییات از طریق آن در پایگاه داده یافت می شود.', + 'list_details_url_parameter' => 'پارامتر نام آدرس', + 'list_details_url_parameter_description' => 'نام پارامتر آدرس صفحه جزییات که کلید مورد را دریافت میکند.', + 'details_title' => 'جزییات مورد', + 'details_description' => 'نمایش جزییات مورد از مدل انتخاب شده', + 'details_model' => 'کلاس مدل', + 'details_identifier_value' => 'مقدار مشخصه', + 'details_identifier_value_description' => 'مقدار مشخصه جهت بارگذاری مورد از پایگاه داده. میتواند یک مقدار ثابت و یا پارامتر آدرس باشد.', + 'details_identifier_value_required' => 'وارد کردن مقدار مشخصه اجباریست', + 'details_key_column' => 'ستون کلید', + 'details_key_column_description' => 'ستونی در مدل که به عنوان مشخصه برای واکشی مورد در پایگاه داده مورد استفاده قرار میگیرد.', + 'details_key_column_required' => 'وارد کردن ستون کلید اجباریست', + 'details_display_column' => 'ستون نمایشی', + 'details_display_column_description' => 'ستونی در مدل که جهت نمایش در صفحه جزییات مورد استفاده قرار میگیرد.', + 'details_display_column_required' => 'وارد کردن ستون نمایشی اجباریست', + 'details_not_found_message' => 'موردی یافت نشد', + 'details_not_found_message_description' => 'پیغامی که به هنگام یافت نشدن مورد مورد استفاده قرار میگیرد.', + 'details_not_found_message_default' => 'موردی یافت نشد.', + ], +]; diff --git a/plugins/rainlab/builder/lang/nl.json b/plugins/rainlab/builder/lang/nl.json new file mode 100644 index 0000000..12b1912 --- /dev/null +++ b/plugins/rainlab/builder/lang/nl.json @@ -0,0 +1,4 @@ +{ + "Builder": "Builder", + "Provides visual tools for building October plugins.": "Stelt visuele hulpmiddelen beschikbaar om October plugins te maken." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/nl/lang.php b/plugins/rainlab/builder/lang/nl/lang.php new file mode 100644 index 0000000..a414140 --- /dev/null +++ b/plugins/rainlab/builder/lang/nl/lang.php @@ -0,0 +1,639 @@ + [ + 'add' => 'Maak plugin', + 'no_records' => 'Geen plugins aanwezig', + 'no_name' => 'Geen naam', + 'search' => 'Zoeken...', + 'filter_description' => 'Toon alle plugins of alleen eigen plugins.', + 'settings' => 'Instellingen', + 'entity_name' => 'Plugin', + 'field_name' => 'Naam', + 'field_author' => 'Auteur', + 'field_description' => 'Omschrijving', + 'field_icon' => 'Icoon', + 'field_plugin_namespace' => 'Plugin namespace', + 'field_author_namespace' => 'Auteur namespace', + 'field_namespace_description' => 'Een namespace kan alleen latijnse letters en cijfers bevatten en mag ook alleen met een letter beginnen. Bijvoorbeeld: Blog', + 'field_author_namespace_description' => 'Je kan de namespace van een Builder plugin niet meer wijzigen nadat je de plugin hebt gemaakt. Bijvoorbeeld: JohnSmith', + 'tab_general' => 'Algemeen', + 'tab_description' => 'Details', + 'field_homepage' => 'Plugin homepagina URL', + 'no_description' => 'Er is geen omschrijving opgegeven voor deze plugin.', + 'error_settings_not_editable' => 'De instellingen van deze plugin kunnen niet met Builder worden gewijzigd.', + 'update_hint' => 'Je kan de naam en omschrijving van de plugin vertalen in de \'Vertalen\' tab.', + ], + 'author_name' => [ + 'title' => 'Auteursnaam', + 'description' => 'Dit is de standaard auteursnaam die wordt gebruikt bij het maken van plugins. Deze naam staat niet vast, je kan hem altijd wijzigen in de instellingen van de plugin.', + ], + 'author_namespace' => [ + 'title' => 'Auteur namespace', + 'description' => 'Als je plugins maakt voor de Marketplace, dan moet de namespace gelijk zijn aan de auteur code en niet gewijzigd worden. Raadpleeg de documentatie voor aanvullende details.', + ], + 'database' => [ + 'menu_label' => 'Database', + 'no_records' => 'Geen database tabellen aanwezig', + 'search' => 'Zoeken...', + 'confirmation_delete_multiple' => 'Weet je zeker dat je de geselecteerde tabellen wilt verwijderen?', + 'field_name' => 'Tabelnaam', + 'tab_columns' => 'Kolommen', + 'column_name_name' => 'Kolom', + 'column_name_required' => 'Geef kolomnaam op', + 'column_name_type' => 'Type', + 'column_type_required' => 'Selecteer kolomtype', + 'column_name_length' => 'Lengte', + 'column_validation_length' => 'Lengte moet een getal zijn of een getal met dicimalen (10,2). Spaties zijn niet toegestaan.', + 'column_validation_title' => 'Alleen getallen, kleine letters en underscores zijn toegestaan in kolomnamen', + 'column_name_unsigned' => 'Unsigned', + 'column_name_nullable' => 'Nullable', + 'column_auto_increment' => 'AUTOINCR', + 'column_default' => 'Standaardwaarde', + 'column_auto_primary_key' => 'PK', + 'tab_new_table' => 'Nieuwe tabel', + 'btn_add_column' => 'Kolom toevoegen', + 'btn_delete_column' => 'Kolom verwijderen', + 'confirm_delete' => 'Do you really want to delete the table?', + 'error_enum_not_supported' => 'De tabel bevat kolom(men) van het type "enum", deze worden momenteel niet ondersteund door Builder.', + 'error_table_name_invalid_prefix' => 'De tabelnaam moet starten met de plugin prefix: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Ongeldige tabelnaam. Tabelnamen mogen alleen latijnse letters, cijfers en underscores bevatten. Tabelnamen moeten beginnen met een letter en mogen geen spaties bevatten.', + 'error_table_duplicate_column' => 'Kolomnaam bestaat reeds: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => 'Een `auto-increment` kolom kan geen deel uitmaken van een `compound primary key`.', + 'error_table_mutliple_auto_increment' => 'De tabel kan niet meerdere `auto-increment` kolommen bevatten.', + 'error_table_auto_increment_non_integer' => 'Auto-increment kolommen moeten van het type `integer` zijn.', + 'error_table_decimal_length' => 'De lengte voor type `:type` moet voldoen aan het formaat \'10,2\', zonder spaties.', + 'error_table_length' => 'De lengte voor type `:type` moet als een integer worden gespecificeerd.', + 'error_unsigned_type_not_int' => 'Fout gevonden in kolomdefinitie \':column\'. De `unsigned` vlag mag alleen op type `integer` kolommen worden toegepast.', + 'error_integer_default_value' => 'Ongeldige standaardwaarde voor kolom \':column\'. Toegestane waardes zijn \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Ongeldige standaardwaarde voor kolom \':column\'. Toegestane waardes zijn \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Ongeldige standaardwaarde voor kolom \':column\'. Toegestane waardes zijn \'0\' and \'1\'.', + 'error_unsigned_negative_value' => 'De standaardwaarde voor de kolom \':column\' mag niet negatief zijn.', + 'error_table_already_exists' => 'De tabel \':name\' bestaat reeds in de database.', + ], + 'model' => [ + 'menu_label' => 'Models', + 'entity_name' => 'Model', + 'no_records' => 'Geen models aanwezig', + 'search' => 'Zoeken...', + 'add' => 'Toevoegen...', + 'forms' => 'Formulieren', + 'lists' => 'Lijsten', + 'field_class_name' => 'Klasse naam', + 'field_database_table' => 'Database tabel', + 'error_class_name_exists' => 'Model bestand bestaat reeds voor de opgegeven klasse naam: :path', + 'add_form' => 'Fomulier toevoegen', + 'add_list' => 'Lijst toevoegen', + ], + 'form' => [ + 'saved' => 'Het formulier is succesvol opgeslagen.', + 'confirm_delete' => 'Weet je zeker dat je het formulier wilt verwijderen?', + 'tab_new_form' => 'Nieuw formulier', + 'property_label_title' => 'Label', + 'property_label_required' => 'Voor waarde voor label in.', + 'property_span_title' => 'Uitlijning', + 'property_comment_title' => 'Toelichting', + 'property_comment_above_title' => 'Toelichting (boven)', + 'property_default_title' => 'Standaard', + 'property_checked_default_title' => 'Standaard aangevinkt', + 'property_css_class_title' => 'CSS klassenaam', + 'property_css_class_description' => 'Optionele CSS klassenaam die wordt toegewezen aan het veld element.', + 'property_disabled_title' => 'Uitgeschakeld', + 'property_hidden_title' => 'Verborgen', + 'property_required_title' => 'Verplicht', + 'property_field_name_title' => 'Veldnaam', + 'property_placeholder_title' => 'Tijdelijke aanduiding', + 'property_default_from_title' => 'Waarde van', + 'property_stretch_title' => 'Uitrekken', + 'property_stretch_description' => 'Geeft aan of dit veld uitrekt naar de breedte van het bovenliggende element.', + 'property_context_title' => 'Context', + 'property_context_description' => 'Geeft aan welk formulier context gebruikt moet worden om het veld weer te geven.', + 'property_context_create' => 'Aanmaken', + 'property_context_update' => 'Wijzigen', + 'property_context_preview' => 'Voorvertoning', + 'property_dependson_title' => 'Afhankelijk van', + 'property_trigger_action' => 'Actie', + 'property_trigger_show' => 'Weergeven', + 'property_trigger_hide' => 'Verbergen', + 'property_trigger_enable' => 'Inschakelen', + 'property_trigger_disable' => 'Uitschakelen', + 'property_trigger_empty' => 'Leeg', + 'property_trigger_field' => 'Veld', + 'property_trigger_field_description' => 'Geeft het veld aan wat de actie veroorzaakt.', + 'property_trigger_condition' => 'Voorwaarde', + 'property_trigger_condition_description' => 'Bepaald de voorwaarde waaraan het betreffende veld aan moet voldoen. Ondersteunde waarden: checked, unchecked, value[waarde].', + 'property_trigger_condition_checked' => 'Aangevinkt: checked', + 'property_trigger_condition_unchecked' => 'Uitgevinkt: unchecked', + 'property_trigger_condition_somevalue' => 'Waarde: value[waarde]', + 'property_preset_title' => 'Voorinstelling', + 'property_preset_description' => 'Zorgt ervoor dat de veldwaarde initieel wordt gevuld met de waarde van een ander veld, al dan niet geconverteerd.', + 'property_preset_field' => 'Veld', + 'property_preset_field_description' => 'Het veld waarvan de waarde moet overgenomen worden.', + 'property_preset_type' => 'Type', + 'property_preset_type_description' => 'Conversie type', + 'property_attributes_title' => 'Attributen', + 'property_attributes_description' => 'Custom HTML attributen die aan het formulier veld moeten worden toegevoegd.', + 'property_container_attributes_title' => 'Container attributen', + 'property_container_attributes_description' => 'Custom HTML attributen die aan het formulier veld container moeten worden toegevoegd.', + 'property_group_advanced' => 'Geavanceerd', + 'property_dependson_description' => 'Een lijst van veldnamen waar dit veld van afhankelijk is. Als die velden een andere waarde krijgen, zal dit veld worden bijgewerkt. Een veld per regel.', + 'property_trigger_title' => 'Trigger', + 'property_trigger_description' => 'Zorgt ervoor dat veld eigenschappen veranderen, zoals bijvoorbeeld zichtbaarheid of waarde, gebaseerd op de staat van een ander veld.', + 'property_default_from_description' => 'Neemt de standaardwaarde over van een ander veld.', + 'property_field_name_required' => 'Veldnaam is verplicht', + 'property_field_name_regex' => 'Veldnaam kan alleen latijnse karakters bevatten of _ - [ ] .', + 'property_attributes_size' => 'Grootte', + 'property_attributes_size_tiny' => 'Kleiner', + 'property_attributes_size_small' => 'Klein', + 'property_attributes_size_large' => 'Groter', + 'property_attributes_size_huge' => 'Groot', + 'property_attributes_size_giant' => 'Grootst', + 'property_comment_position' => 'Toelichting positie', + 'property_comment_position_above' => 'Boven', + 'property_comment_position_below' => 'Beneden', + 'property_hint_path' => 'Pad naar hint-sjabloon', + 'property_hint_path_description' => 'Pad naar sjabloon bestand die de hint tekst bevat. Gebruik het $ symbool om het hoofdpad van de plugin aan te geven. Voorbeeld: $/acme/blog/partials/_partial.htm', + 'property_hint_path_required' => 'Geef het hint-sjabloon pad op', + 'property_partial_path' => 'Pad naar sjabloon', + 'property_partial_path_description' => 'Pad naar sjabloon bestand. Gebruik het $ symbool om het hoofdpad van de plugin aan te geven. Voorbeeld: $/acme/blog/partials/_partial.htm', + 'property_partial_path_required' => 'Geef het sjabloon pad op', + 'property_code_language' => 'Taal', + 'property_code_theme' => 'Thema', + 'property_theme_use_default' => 'Gebruik standaard thema', + 'property_group_code_editor' => 'Code editor', + 'property_gutter' => 'Goot', + 'property_gutter_show' => 'Weergeven', + 'property_gutter_hide' => 'Verbergen', + 'property_wordwrap' => 'Woordafbreking', + 'property_wordwrap_wrap' => 'Afbreken', + 'property_wordwrap_nowrap' => 'Niet afbreken', + 'property_fontsize' => 'Grootte lettertype', + 'property_codefolding' => 'Code inklappen', + 'property_codefolding_manual' => 'Handmatig', + 'property_codefolding_markbegin' => 'Begin markeren', + 'property_codefolding_markbeginend' => 'Begin en einde markeren', + 'property_autoclosing' => 'Automatisch sluiten', + 'property_enabled' => 'Ingeschakeld', + 'property_disabled' => 'Uitgeschakeld', + 'property_soft_tabs' => 'Zachte tabs', + 'property_tab_size' => 'Tab grootte', + 'property_readonly' => 'Alleen-lezen', + 'property_use_default' => 'Standaard instelling', + 'property_options' => 'Opties', + 'property_prompt' => 'Invoer', + 'property_prompt_description' => 'Tekst op de toevoegknop.', + 'property_prompt_default' => 'Nieuw item', + 'property_available_colors' => 'Beschikbare kleuren', + 'property_available_colors_description' => 'Lijst van beschikbare kleuren in HEX formaat (#FF0000). Laat leeg voor standaard kleuren set. Een waarde per regel.', + 'property_datepicker_mode' => 'Modus', + 'property_datepicker_mode_date' => 'Datum', + 'property_datepicker_mode_datetime' => 'Datum en tijd', + 'property_datepicker_mode_time' => 'Tijd', + 'property_datepicker_min_date' => 'Minimale datum', + 'property_datepicker_max_date' => 'Maximale datum', + 'property_fileupload_mode' => 'Modus', + 'property_fileupload_mode_file' => 'Bestand', + 'property_fileupload_mode_image' => 'Afbeelding', + 'property_group_fileupload' => 'Bestandsupload', + 'property_fileupload_image_width' => 'Breedte afbeelding', + 'property_fileupload_image_width_description' => 'Afbeeldingen zullen geschaald worden naar deze breedte (optioneel).', + 'property_fileupload_invalid_dimension' => 'Ongeldige waarde voor breedte/hoogte afbeelding, geef een getal in.', + 'property_fileupload_image_height' => 'Hoogte afbeelding', + 'property_fileupload_image_height_description' => 'Afbeeldingen zullen geschaald worden naar deze hoogte (optioneel).', + 'property_fileupload_file_types' => 'Bestandstypes', + 'property_fileupload_file_types_description' => 'Komma gescheiden lijst van toegestane bestandsextenties, bijvoorbeeld: zip,txt (optioneel).', + 'property_fileupload_mime_types' => 'MIME typen', + 'property_fileupload_mime_types_description' => 'Komma gescheiden lijst van toegestane MIME-typen; bestandsextenties of volledige namen, bijvoorbeeld: zip,txt', + 'property_fileupload_use_caption' => 'Gebruik annotatie', + 'property_fileupload_use_caption_description' => 'Staat toe dat er een titel en omschrijving kunnen worden opgegeven voor het bestand.', + 'property_fileupload_thumb_options' => 'Miniatuurweergave opties', + 'property_fileupload_thumb_options_description' => 'Beheer opties voor de automatisch gegenereerde miniatuurweergaven. Alleen van toepassing bij Afbeelding modus.', + 'property_fileupload_thumb_mode' => 'Modus', + 'property_fileupload_thumb_auto' => 'Automatisch', + 'property_fileupload_thumb_exact' => 'Exact', + 'property_fileupload_thumb_portrait' => 'Staand', + 'property_fileupload_thumb_landscape' => 'Liggend', + 'property_fileupload_thumb_crop' => 'Uitsnijden', + 'property_fileupload_thumb_extension' => 'Bestandsextentie', + 'property_name_from' => 'Kolomnaam', + 'property_name_from_description' => 'Gerelateerde kolomnaam die gebruikt moet worden voor het weergeven van een naam.', + 'property_description_from' => 'Omschrijving kolom', + 'property_description_from_description' => 'Gerelateerde kolomnaam die gebruikt moet worden voor het weergeven van een omschrijving.', + 'property_recordfinder_prompt' => 'Prompt', + 'property_recordfinder_prompt_description' => 'Text to display when there is no record selected. The %s character represents the search icon. Leave empty for the default prompt.', + 'property_recordfinder_list' => 'Lijst configuratie', + 'property_recordfinder_list_description' => 'Een referentie naar een lijstkolom definitie bestand. Gebruik het $ symbool om te refereren naar de plugin map, bijvoorbeeld: $/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => 'Geef een pad op naar het YAML bestand', + 'property_group_recordfinder' => 'Record zoeker', + 'property_mediafinder_mode' => 'Modus', + 'property_mediafinder_mode_file' => 'Bestand', + 'property_mediafinder_mode_image' => 'Afbeelding', + 'property_group_relation' => 'Relatie', + 'property_relation_select' => 'kiezen', + 'property_relation_select_description' => 'CONCAT meerdere kolommen samen voor het weergeven van een naam', + 'property_relation_prompt' => 'Prompt', + 'property_relation_prompt_description' => 'Tekst om weer te geven als er geen selecties beschikbaar zijn.', + 'property_max_items' => 'Maximum aantal', + 'property_max_items_description' => 'Maximum toegelaten aantal items in de herhaler.', + 'control_group_standard' => 'Standaard', + 'control_group_widgets' => 'Widgets', + 'click_to_add_control' => 'Element toevoegen', + 'loading' => 'Bezig met laden...', + 'control_text' => 'Tekst', + 'control_text_description' => 'Invoerveld voor één regel tekst.', + 'control_password' => 'Wachtwoord', + 'control_password_description' => 'Invoerveld voor een wachtwoord.', + 'control_checkbox' => 'Keuzevakje', + 'control_checkbox_description' => 'Enkelvoudig keuzevakje.', + 'control_switch' => 'Schakelaar', + 'control_switch_description' => 'Enkelvoudige schakelaar, een alternatief voor het keuzevakje.', + 'control_textarea' => 'Tekst', + 'control_textarea_description' => 'Tekstvak voor meerdere regels met instelbare hoogte.', + 'control_dropdown' => 'Selectieveld', + 'control_dropdown_description' => 'Een selectie lijst met vaste of dynamische opties.', + 'control_unknown' => 'Onbekend element type: :type', + 'control_repeater' => 'Herhaler', + 'control_repeater_description' => 'Toont een set van herhalende formulier elementen.', + 'control_number' => 'Nummer', + 'control_number_description' => 'Invoerveld voor een nummer.', + 'control_hint' => 'Tip', + 'control_hint_description' => 'Toont een tip in een vakje die verborgen kan worden door een gebruiker.', + 'control_partial' => 'Partial', + 'control_partial_description' => 'Toont inhoud van een zgn. partial.', + 'control_section' => 'Sectie', + 'control_section_description' => 'Toont een formuliersectie met een kop- en subkoptekst.', + 'control_radio' => 'Lijst van invoerrondjes', + 'control_radio_description' => 'Een lijst van invoerrondjes, er kan maar één invoerrondje geselecteerd worden.', + 'control_radio_option_1' => 'Optie 1', + 'control_radio_option_2' => 'Optie 2', + 'control_checkboxlist' => 'Lijst van keuzevakjes', + 'control_checkboxlist_description' => 'Een lijst van keuzevakjes, er kunnen meerdere keuzevakjes geselecteerd worden.', + 'control_codeeditor' => 'Code editor', + 'control_codeeditor_description' => 'Een editor voor het bewerken van geformatteerde code of opmaakcode.', + 'control_colorpicker' => 'Kleur kiezer', + 'control_colorpicker_description' => 'Een veld met de mogelijkheid voor het selecteren van een hexadecimale kleurcode.', + 'control_datepicker' => 'Datum kiezer', + 'control_datepicker_description' => 'Een veld met de mogelijkheid voor het selecteren van een datum en tijd.', + 'control_richeditor' => 'WYSIWYG editor', + 'control_richeditor_description' => 'Een editor voor het bewerken van uitgebreide opgemaakte tekst.', + 'control_markdown' => 'Markdown editor', + 'control_markdown_description' => 'Een editor voor het bewerken van tekst in het Markdown formaat.', + 'control_fileupload' => 'Bestand uploader', + 'control_fileupload_description' => 'Een bestandsuploader voor afbeeldingen of reguliere bestanden.', + 'control_recordfinder' => 'Record veld', + 'control_recordfinder_description' => 'Een zoekveld met details van een gerelateerd record.', + 'control_mediafinder' => 'Media veld', + 'control_mediafinder_description' => 'Een veld die een item uit de Media bibliotheek kan bevatten.', + 'control_relation' => 'Relatie', + 'control_relation_description' => 'Toont een selectieveld of een lijst van keuzevakjes om een gerelateerd record te selecteren.', + 'error_file_name_required' => 'Voer bestandsnaam in van het formulier.', + 'error_file_name_invalid' => 'Bestandsnaam kan alleen latijnse karakters, cijfers of een van de volgende tekens bevatten: _ - #', + 'span_left' => 'Links', + 'span_right' => 'Rechts', + 'span_full' => 'Volledige breedte', + 'span_auto' => 'Automatisch', + 'empty_tab' => 'Leeg tabblad', + 'confirm_close_tab' => 'Het tabblad bevat elementen die verwijderd zullen worden. Doorgaan?', + 'tab' => 'Formulier tabblad', + 'tab_title' => 'Titel', + 'controls' => 'Elementen', + 'property_tab_title_required' => 'De titel van het tabblad is verplicht.', + 'tabs_primary' => 'Primaire tabs', + 'tabs_secondary' => 'Secundaire tabs', + 'tab_stretch' => 'Uitrekken', + 'tab_stretch_description' => 'Met deze optie geef je aan dat de inhoud van het tabblad meerekt naar de hoogte van het bovenliggende element.', + 'tab_css_class' => 'CSS class', + 'tab_css_class_description' => 'Wijst een CSS class toe aan de inhoud van het tabblad.', + 'tab_name_template' => 'Tabblad %s', + 'tab_already_exists' => 'Tabblad met opgegeven titel bestaat reeds.', + ], + 'list' => [ + 'tab_new_list' => 'Nieuwe lijst', + 'saved' => 'De lijst is succesvol opgeslagen.', + 'confirm_delete' => 'Weet je zeker dat je de lijst wilt verwijderen?', + 'tab_columns' => 'Kolommen', + 'btn_add_column' => 'Kolom toevoegen', + 'btn_delete_column' => 'Kolom verwijderen', + 'column_dbfield_label' => 'Veld', + 'column_dbfield_required' => 'Geef Model veld op', + 'column_name_label' => 'Label', + 'column_label_required' => 'Geef kolom label op', + 'column_type_label' => 'Type', + 'column_type_required' => 'Geef kolomtype op', + 'column_type_text' => 'Tekst', + 'column_type_number' => 'Numeriek', + 'column_type_switch' => 'Schakelaar', + 'column_type_datetime' => 'Datum & Tijd', + 'column_type_date' => 'Datum', + 'column_type_time' => 'Tijd', + 'column_type_timesince' => 'Datum & tijd sinds', + 'column_type_timetense' => 'Datum & tijd afgekort', + 'column_type_select' => 'Keuze', + 'column_type_partial' => 'Partial', + 'column_label_default' => 'Standaard', + 'column_label_searchable' => 'Zoeken', + 'column_label_sortable' => 'Sorteerbaar', + 'column_label_invisible' => 'Onzichtbaar', + 'column_label_select' => 'Select', + 'column_label_relation' => 'Relatie', + 'column_label_css_class' => 'CSS class', + 'column_label_width' => 'Breedte', + 'column_label_path' => 'Pad', + 'column_label_format' => 'Formaat', + 'column_label_value_from' => 'Waarde van', + 'error_duplicate_column' => 'Kolom veldnaam bestaat reeds: \':column\'.', + ], + 'controller' => [ + 'menu_label' => 'Controllers', + 'no_records' => 'Geen controllers aanwezig', + 'controller' => 'Controller', + 'behaviors' => 'Behaviors', + 'new_controller' => 'Nieuwe controller', + 'error_controller_has_no_behaviors' => 'De controller heeft geen configureerbare behaviors.', + 'error_invalid_yaml_configuration' => 'Fout bij laden behavior configuratie bestand: :file', + 'behavior_form_controller' => 'Formulier controller behavior', + 'behavior_form_controller_description' => 'Voegt formulier functionaliteit toe aan een back-end pagina. Deze behavior bevat drie pagina\'s: Create (aanmaken), Update (wijzigen) en Preview (voorbeeldweergave).', + 'property_behavior_form_placeholder' => '-- Selecteer formulier --', + 'property_behavior_form_name' => 'Naam', + 'property_behavior_form_name_description' => 'De naam van het object wat beheerd wordt door dit formulier.', + 'property_behavior_form_name_required' => 'Geef de naam van het formulier op', + 'property_behavior_form_file' => 'Formulier configuratie', + 'property_behavior_form_file_description' => 'Referentie naar het formulieren veld definitie bestand.', + 'property_behavior_form_file_required' => 'Geef het pad op naar het configuratiebestand van het formulier', + 'property_behavior_form_model_class' => 'Model class', + 'property_behavior_form_model_class_description' => 'Klassenaam van een model, de data van het formulier wordt geladen en opgeslagen met dit model.', + 'property_behavior_form_model_class_required' => 'Selecteer een model class', + 'property_behavior_form_default_redirect' => 'Standaard redirect', + 'property_behavior_form_default_redirect_description' => 'De standaard pagina waarnaar verwezen wordt nadat het formulier is opgeslagen.', + 'property_behavior_form_create' => 'Maak record pagina', + 'property_behavior_form_redirect' => 'Redirect', + 'property_behavior_form_redirect_description' => 'Een pagina waarnaar verwezen wordt wanneer een record is aangemaakt.', + 'property_behavior_form_redirect_close' => 'Sluiten redirect', + 'property_behavior_form_redirect_close_description' => 'Een pagina waarnaar verwezen wordt wanneer er gekozen is voor \'Opslaan en sluiten\'.', + 'property_behavior_form_flash_save' => 'Bericht bij opslaan', + 'property_behavior_form_flash_save_description' => 'Bericht om weer te geven nadat een record is opgeslagen.', + 'property_behavior_form_page_title' => 'Paginatitel', + 'property_behavior_form_update' => 'Record bijwerken pagina', + 'property_behavior_form_update_redirect' => 'Redirect', + 'property_behavior_form_create_redirect_description' => 'Een pagina waarnaar verwezen wordt als een record wordt opgeslagen.', + 'property_behavior_form_flash_delete' => 'Delete flash message', + 'property_behavior_form_flash_delete_description' => 'Flash message to display when record is deleted.', + 'property_behavior_form_preview' => 'Voorbeeldweergave record pagina', + 'behavior_list_controller' => 'Lijst controller behavior', + 'behavior_list_controller_description' => 'Stelt een sorteerbare en doorzoekbare lijst beschikbaar. De \'behavior\' maakt de controller action "index" beschikbaar.', + 'property_behavior_list_title' => 'Titel lijst', + 'property_behavior_list_title_required' => 'Geeft de titel van de lijst op', + 'property_behavior_list_placeholder' => '-- Selecteer lijst --', + 'property_behavior_list_model_class' => 'Model class', + 'property_behavior_list_model_class_description' => 'Klassenaam van een model, de lijst wordt geladen m.b.v. dit model.', + 'property_behavior_form_model_class_placeholder' => '-- Selecteer model --', + 'property_behavior_list_model_class_required' => 'Selecteer een model class', + 'property_behavior_list_model_placeholder' => '-- Selecteer model --', + 'property_behavior_list_file' => 'Configuratiebestand lijst', + 'property_behavior_list_file_description' => 'Referentie naar een definitiebestand van een lijst.', + 'property_behavior_list_file_required' => 'Geeft het pad op naar het configuratiebestand van de lijst', + 'property_behavior_list_record_url' => 'Record URL', + 'property_behavior_list_record_url_description' => 'Koppel elk record van de lijst aan een andere pagina. Bijv. users/update:id. Het :id gedeelte wordt vervangen met het identificatie nummer van het record.', + 'property_behavior_list_no_records_message' => 'Bericht bij geen records', + 'property_behavior_list_no_records_message_description' => 'Het bericht wat moet worden weergegeven als er geen records gevonden zijn.', + 'property_behavior_list_recs_per_page' => 'Records per pagina', + 'property_behavior_list_recs_per_page_description' => 'Aantal records wat weergegeven moet worden per pagina. Geef 0 op om geen paginatie te gebruiken. Standaardwaarde: 0', + 'property_behavior_list_recs_per_page_regex' => 'Het aantal records per pagina moet een numerieke waarde zijn', + 'property_behavior_list_show_setup' => 'Toon setup knop', + 'property_behavior_list_show_sorting' => 'Toon sorteren', + 'property_behavior_list_default_sort' => 'Standaard sortering', + 'property_behavior_form_ds_column' => 'Kolom', + 'property_behavior_form_ds_direction' => 'Richting', + 'property_behavior_form_ds_asc' => 'Oplopend', + 'property_behavior_form_ds_desc' => 'Aflopend', + 'property_behavior_list_show_checkboxes' => 'Toon keuzevakjes', + 'property_behavior_list_onclick' => 'Klik handler', + 'property_behavior_list_onclick_description' => 'JavaScript code wat uitgevoerd moet worden als er op een record wordt geklikt.', + 'property_behavior_list_show_tree' => 'Toon hiërarchie', + 'property_behavior_list_show_tree_description' => 'Toont een hiërarchie boom voor ouder/kind-records.', + 'property_behavior_list_tree_expanded' => 'Uitgeklapte weergave', + 'property_behavior_list_tree_expanded_description' => 'Geeft aan of de hiërarchische boom standaard uitgeklapt moet worden weergegeven.', + 'property_behavior_list_toolbar' => 'Toolbar', + 'property_behavior_list_toolbar_buttons' => 'Knoppen partial bestand', + 'property_behavior_list_toolbar_buttons_description' => 'Referentie naar een partial bestand met de toolbar knoppen. Bijv. list_toolbar', + 'property_behavior_list_search' => 'Zoeken', + 'property_behavior_list_search_prompt' => 'Zoek prompt', + 'property_behavior_list_filter' => 'Filter configuratie', + 'behavior_reorder_controller' => 'Reorder controller behavior', + 'behavior_reorder_controller_description' => 'Stelt functies beschikbaar voor het sorteren en rangschikken van records. De behavior maakt automatisch de "reorder" controller actie aan.', + 'property_behavior_reorder_title' => 'Rangschik titel', + 'property_behavior_reorder_title_required' => 'De rangschik titel is verplicht.', + 'property_behavior_reorder_name_from' => 'Attribuut naam', + 'property_behavior_reorder_name_from_description' => 'Attribuut van het model wat als weergavenaam van het record moet worden gebruikt.', + 'property_behavior_reorder_name_from_required' => 'De attribuut naam is verplicht.', + 'property_behavior_reorder_model_class' => 'Model class', + 'property_behavior_reorder_model_class_description' => 'Model klassenaam, de rangschik data wordt geladen uit dit model.', + 'property_behavior_reorder_model_class_placeholder' => '-- Selecteer model --', + 'property_behavior_reorder_model_class_required' => 'Selecteer een model class', + 'property_behavior_reorder_model_placeholder' => '-- Selecteer model --', + 'property_behavior_reorder_toolbar' => 'Toolbar', + 'property_behavior_reorder_toolbar_buttons' => 'Knoppen partial bestand', + 'property_behavior_reorder_toolbar_buttons_description' => 'Referentie naar een partial bestand met de toolbar knoppen. Bijv. reorder_toolbar', + 'error_controller_not_found' => 'Het originele controller bestand kan niet gevonden worden.', + 'error_invalid_config_file_name' => 'De bestandsnaam van configuratiebestand :fil) (van behavior :class) bevat ongeldige karakters en kan daarom niet worden geladen.', + 'error_file_not_yaml' => 'Het configuratiebestad :file (van behavior :class) is geen YAML-bestand. Alleen YAML-bestanden worden ondersteund.', + 'saved' => 'De controller is succesvol opgeslagen.', + 'controller_name' => 'Naam controller', + 'controller_name_description' => 'De naam van de controller bepaald de uiteindelijk URL waarmee de controller beschikbaar is in de back-end. De standaard PHP conventies zijn van toepassing. Het eerste karakter moet een hoofdletter zijn. Voorbeelden van geldige namen: Categories, Posts of Products.', + 'base_model_class' => 'Basis model class', + 'base_model_class_description' => 'Selecteer een model class om te gebruiken als basis model in behaviors die models vereisen of ondersteunen. Je kan de behaviors later configureren.', + 'base_model_class_placeholder' => '-- Selecteer model --', + 'controller_behaviors' => 'Behaviors', + 'controller_behaviors_description' => 'Seleteer de behaviors die de controller moet implementeren. De view bestanden, die vereist zijn voor de behaviors, zullen automatisch worden aangemaakt.', + 'controller_permissions' => 'Toegangsrechten', + 'controller_permissions_description' => 'Selecteer de gebruikersrechten die toegang hebben tot de controller view. Toegangsrechten kunnen aangemaakt worden via het tabblad Toegangsrechten in het linkermenu. Je kunt deze optie ook later aanpassen in de PHP-code van de controller.', + 'controller_permissions_no_permissions' => 'De plugin heeft (nog) geen toegangsrechten gedefinieerd.', + 'menu_item' => 'Actief menu item', + 'menu_item_description' => 'Selecteer een menu item dat geactiveerd moet worden voor de pagina\'s van deze controller. Je kunt deze optie ook later aanpassen in de PHP-code van de controller.', + 'menu_item_placeholder' => '-- Selecteer menu item --', + 'error_unknown_behavior' => 'De behavior class :class is niet geregistreerd in de behavior bibliotheek.', + 'error_behavior_view_conflict' => 'De geselecteerde behaviors leveren conflicterende views (:view) en kunnen daarom niet samen worden gebruikt in een controller.', + 'error_behavior_config_conflict' => 'De geselecteerde behaviors leveren conflicterende configuratiebestanden op (:file) en kunnen daarom niet samen worden gebruikt in een controller.', + 'error_behavior_view_file_not_found' => 'De view template :view van behavior :class kan niet worden gevonden.', + 'error_behavior_config_file_not_found' => 'Het configuratiebestand template :file van behavior :class kan niet worden gevonden.', + 'error_controller_exists' => 'Het controller bestand :file bestaat reeds.', + 'error_controller_name_invalid' => 'Ongeldigde controllernaam. Voorbeelden van geldige namen: Posts, Categories of Products.', + 'error_behavior_view_file_exists' => 'De view :view bestaat reeds voor deze controller.', + 'error_behavior_config_file_exists' => 'Het behavior configuratiebestand: file bestaat reeds.', + 'error_save_file' => 'Fout bij opslaan van het controller bestand :file.', + 'error_behavior_requires_base_model' => 'Er moet een basis model class worden geselecteer voor behavior :behavior.', + 'error_model_doesnt_have_lists' => 'Het geselecteerde model heeft geen lijsten. Maak eerst een lijst.', + 'error_model_doesnt_have_forms' => 'Het geselecteerde model heeft geen formulieren. Maak eerst een formulier.', + ], + 'version' => [ + 'menu_label' => 'Versies', + 'no_records' => 'Geen plugin versies aanwezig', + 'search' => 'Zoeken...', + 'tab' => 'Versies', + 'saved' => 'De versie is succesvol opgeslagen.', + 'confirm_delete' => 'Weet je zeker dat je deze versie wilt verwijderen?', + 'tab_new_version' => 'Nieuwe versie', + 'migration' => 'Migratie', + 'seeder' => 'Seeder', + 'custom' => 'Versienummer ophogen', + 'apply_version' => 'Versie toepassen', + 'applying' => 'Bezig met toepassen...', + 'rollback_version' => 'Versie terugzetten', + 'rolling_back' => 'Bezig met terugzerren...', + 'applied' => 'De versie is succesvol toegepast.', + 'rolled_back' => 'De versie is succesvol teruggezet.', + 'hint_save_unapplied' => 'Je hebt een nog niet geactiveerde versie opgeslagen. Niet geactiveerde versies kunnen automatisch worden geactiveerd als jij of een andere gebruiker inlogd op de back-end. Of als een database tabel wordt opgeslagen binnen de Database sectie van de Builder plugin.', + 'hint_rollback' => 'Het terugzetten van een versie zal ook alle versies nieuwer dan deze versie terugzetten. Wees je ervan bewust dat niet geactiveerde versies automatisch geactiveerd kunnen worden, als jij of een andere gebruiker inlogd op de back-end. Of als een database tabel wordt opgeslagen binnen de Database sectie van de Builder plugin.', + 'hint_apply' => 'Het activeren van een versie zal ook oudere niet geactiveerde versies activeren.', + 'dont_show_again' => 'Laat niet meer zien', + 'save_unapplied_version' => 'Niet geactiveerde versie opslaan', + ], + 'menu' => [ + 'menu_label' => 'Backend menu', + 'tab' => 'Menu\'s', + 'items' => 'Menu items', + 'saved' => 'De menu\'s zijn succesvol opgeslagen.', + 'add_main_menu_item' => 'Hoofdmenu item toevoegen', + 'new_menu_item' => 'Menu item', + 'add_side_menu_item' => 'Sub-item toevoegen', + 'side_menu_item' => 'Linker menu item', + 'property_label' => 'Label', + 'property_label_required' => 'Voer label in van menu item.', + 'property_url_required' => 'Voer URL in van menu item.', + 'property_url' => 'URL', + 'property_icon' => 'Icoon', + 'property_icon_required' => 'Selecteer een icoon.', + 'property_permissions' => 'Toegangsrechten', + 'property_order' => 'Volgorde', + 'property_order_invalid' => 'Geef de volgorde aan met een getal.', + 'property_order_description' => 'De volgorde bepaalde de positie van het menu item. Als de volgorde niet is opgegeven zal het item aan het einde van het menu worden toegevoegd. De standaardwaarden van de volgordes worden elke keer opgehoogd met 100.', + 'property_attributes' => 'HTML attributen', + 'property_code' => 'Code', + 'property_code_invalid' => 'De code mag alleen bestaan uit letters en cijfers.', + 'property_code_required' => 'Geef menu item code op.', + 'error_duplicate_main_menu_code' => 'Dupliceer hoofdmenu item code: \':code\'.', + 'error_duplicate_side_menu_code' => 'Dupliceer linker menu item code: \':code\'.', + ], + 'localization' => [ + 'menu_label' => 'Vertalen', + 'language' => 'Taal', + 'strings' => 'Taallabels', + 'confirm_delete' => 'Weet je zeker dat je deze taal wilt verwijderen?', + 'tab_new_language' => 'Nieuwe taal', + 'no_records' => 'Geen talen aanwezig', + 'saved' => 'Het taalbestand is succesvol opgeslagen.', + 'error_cant_load_file' => 'Kan het taalbestand niet laden, bestand is niet gevonden.', + 'error_bad_localization_file_contents' => 'Kan het taalbestand niet laden. Taalbestanden kunnen alleen array-definities en teksten bevatten.', + 'error_file_not_array' => 'Kan het taalbestand niet laden. Taalbestanden moeten een array teruggeven.', + 'save_error' => 'Fout bij opslaan van bestand \':name\'. Controleer schrijfrechten.', + 'error_delete_file' => 'Fout bij verwijderen van taalbestand.', + 'add_missing_strings' => 'Toevoegen van ontbrekende taallabels.', + 'copy' => 'Kopiëren', + 'add_missing_strings_label' => 'Selecteer een taal waarvan de taallabels gekopiëerd moeten worden.', + 'no_languages_to_copy_from' => 'Er zijn geen andere talen waar de taallabels van gekopiëerd kunnen worden.', + 'new_string_warning' => 'Nieuwe taallabel of sectie', + 'structure_mismatch' => 'De structuur van het bron taalbestand komt niet overeen met het bestand wat nu wordt gewijzigd. Een aantal taallabels in het gewijzigde bestand corresponderen met secties in het bronbestand (of vice versa) en kunnen daarom niet automatisch worden samengevoegd.', + 'create_string' => 'Nieuw taallabel toevoegen', + 'string_key_label' => 'Taallabel ID', + 'string_key_comment' => 'Geef het taallabel ID op gescheiden met een punt, bijvoorbeeld: plugin.search. De taallabel zal worden aangemaakt in het standaard taalbestand van de plugin.', + 'string_value' => 'Taallabel waarde', + 'string_key_is_empty' => 'Het taallabel ID mag niet leeg zijn.', + 'string_value_is_empty' => 'Taallabel waarde mag niet leeg zijn.', + 'string_key_exists' => 'Het taallabel ID bestaat reeds. Geef een ander ID op.', + ], + 'permission' => [ + 'menu_label' => 'Toegangsrechten', + 'tab' => 'Toegangsrechten', + 'form_tab_permissions' => 'Toegangsrechten', + 'btn_add_permission' => 'Toegangsrechten toevoegen', + 'btn_delete_permission' => 'Toegangsrechten verwijderen', + 'column_permission_label' => 'Code', + 'column_permission_required' => 'Geef de code op.', + 'column_tab_label' => 'Tabblad titel', + 'column_tab_required' => 'Geef tabblad titel op.', + 'column_label_label' => 'Label', + 'column_label_required' => 'Geef een label op.', + 'saved' => 'Toegangsrechten zijn succesvol opgeslagen.', + 'error_duplicate_code' => 'Dupliceer code: \':code\'.', + ], + 'yaml' => [ + 'save_error' => 'Fout bij opslaan bestan \':name\'. Controleer schrijfrechten.', + ], + 'common' => [ + 'error_file_exists' => 'Het bestand bestaat reeds: \':path\'.', + 'field_icon_description' => 'OctoberCMS gebruikt Font Autumn iconen, zie: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'De doel directory bestaat niet: \':path\'.', + 'error_make_dir' => 'Fout bij aanmaken van directory: \':name\'.', + 'error_dir_exists' => 'Directory bestaat reeds: \':path\'.', + 'template_not_found' => 'Template-bestand kan niet worden gevonden: \':name\'.', + 'error_generating_file' => 'Fout bij genreren van bestand: \':path\'.', + 'error_loading_template' => 'Fout bij laden van template-bestand: \':name\'.', + 'select_plugin_first' => 'Selecteer eerst een plugin. Om een lijst van plugins te tonen, klik op het > icoon in de linker zijbalk.', + 'plugin_not_selected' => 'Plugin is niet geselecteerd.', + 'add' => 'Toevoegen', + ], + 'migration' => [ + 'entity_name' => 'Migratie', + 'error_version_invalid' => 'Het versienummer moet voldoen aan het formaat 1.0.1', + 'field_version' => 'Versie', + 'field_description' => 'Omschrijving', + 'field_code' => 'Code', + 'save_and_apply' => 'Opslaan & toepassen', + 'error_version_exists' => 'De migratie-versie bestaat reeds.', + 'error_script_filename_invalid' => 'De bestandsnaam van de migratie kan alleen letters, getallen en underscores bevatten. De naam moet beginnen met een letter en mag geen spaties bevatten.', + 'error_cannot_change_version_number' => 'Kan het versienummer niet aanpassen voor een reeds toegepaste versie.', + 'error_file_must_define_class' => 'De migratie code moet een migratie of een seeder class definieren. Laat het code veld leeg als je alleen het versienummer wilt bijwerken.', + 'error_file_must_define_namespace' => 'De migratie code moet een namespace definieren. Laat het code veld leeg als je alleen het versienummer wilt bijwerken.', + 'no_changes_to_save' => 'Er zijn geen wijzigingen om op te slaan.', + 'error_namespace_mismatch' => 'The migratie code moet de plugin namespace :namespace gebruiken.', + 'error_migration_file_exists' => 'Het migratie bestand :file bestaat reeds. Gebruik een andere klasse naam.', + 'error_cant_delete_applied' => 'Deze versie is reeds toegepast en kan daarom niet worden verwijderd. Ga eerst terug naar deze versie (rollback).', + ], + 'components' => [ + 'list_title' => 'Record lijst', + 'list_description' => 'Toont een lijst van records voor geselecteerde model.', + 'list_page_number' => 'Paginanummer', + 'list_page_number_description' => 'De waarde hiervan wordt gebruikt om te bepalen op welke pagina de gebruiker zit.', + 'list_records_per_page' => 'Records per pagina', + 'list_records_per_page_description' => 'Het aantal records wat per pagina moet worden weergegeven. Laat leeg om paginatie uit te schakelen.', + 'list_records_per_page_validation' => 'Ongeldige waarde. Het aantal records per pagina moet worden aangegeven met een nummer.', + 'list_no_records' => 'Bericht bij geen records', + 'list_no_records_description' => 'Bericht wat moet worden weergegeven als er geen records zijn.', + 'list_no_records_default' => 'Geen records gevonden', + 'list_sort_column' => 'Sorteer op kolom', + 'list_sort_column_description' => 'Kolom van model waarop de records gesorteerd moeten worden.', + 'list_sort_direction' => 'Sorteerrichting', + 'list_display_column' => 'Weergave kolom', + 'list_display_column_description' => 'Kolom die moet worden weergegeven in de lijst.', + 'list_display_column_required' => 'Selecteer een weergave kolom.', + 'list_details_page' => 'Detailpagina', + 'list_details_page_description' => 'Pagina waarop record details worden weergegeven.', + 'list_details_page_no' => '-- Geen detailpagina --', + 'list_sorting' => 'Sortering', + 'list_pagination' => 'Paginatie', + 'list_order_direction_asc' => 'Oplopend', + 'list_order_direction_desc' => 'Aflopend', + 'list_model' => 'Model class', + 'list_scope' => 'Scope', + 'list_scope_description' => 'Model scope waarin de records moeten worden opgevraagd (optioneel).', + 'list_scope_default' => '-- Selecteer een scope (optioneel) --', + 'list_details_page_link' => 'Link naar de detailpagina', + 'list_details_key_column' => 'Detail sleutelkolom', + 'list_details_key_column_description' => 'Model kolom die moet worden gebruikt als record ID in de detailpagina links.', + 'list_details_url_parameter' => 'URL parameter naam', + 'list_details_url_parameter_description' => 'Naam van de detailpagina URL parameter. De parameter bevat het record ID.', + 'details_title' => 'Record details', + 'details_description' => 'Toont record details voor een geselecteerd model.', + 'details_model' => 'Model class', + 'details_identifier_value' => 'ID-waarde', + 'details_identifier_value_description' => 'ID-waarde waarmee het record wordt opgevraagd uit de database. Geef een vaste waarde op of een parameter naam voor in de URL.', + 'details_identifier_value_required' => 'De ID-waarde mag niet leeg zijn.', + 'details_key_column' => 'Sleutelkolom', + 'details_key_column_description' => 'De kolom die gebruikt moet worden om het record (met ID-waarde) uit de database te kunnen opvragen.', + 'details_key_column_required' => 'De sleutelkolom mag niet leeg zijn.', + 'details_display_column' => 'Weergave kolom', + 'details_display_column_description' => 'De kolom uit het model die moet worden weergegeven op de detailpagina.', + 'details_display_column_required' => 'Selecteer de weergave kolom.', + 'details_not_found_message' => 'Bericht voor niet gevonden', + 'details_not_found_message_description' => 'Bericht wat moet worden weergegeven als het record niet is gevonden.', + 'details_not_found_message_default' => 'Record niet gevonden', + ], +]; diff --git a/plugins/rainlab/builder/lang/pl.json b/plugins/rainlab/builder/lang/pl.json new file mode 100644 index 0000000..3456ec1 --- /dev/null +++ b/plugins/rainlab/builder/lang/pl.json @@ -0,0 +1,3 @@ +{ + "Provides visual tools for building October plugins.": "Wizualne narzędzia do tworzenia wtyczek dla October CMS." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/pl/lang.php b/plugins/rainlab/builder/lang/pl/lang.php new file mode 100644 index 0000000..9931000 --- /dev/null +++ b/plugins/rainlab/builder/lang/pl/lang.php @@ -0,0 +1,688 @@ + [ + 'add' => 'Utwórz wtyczkę', + 'no_records' => 'Nie znaleziono wtyczek', + 'no_name' => 'Brak nazwy', + 'search' => 'Szukaj...', + 'filter_description' => 'Wyświetl wszystkie wtyczki lub tylko twoje wtyczki.', + 'settings' => 'Ustawienia', + 'entity_name' => 'Wtyczka', + 'field_name' => 'Nazwa', + 'field_author' => 'Autor', + 'field_description' => 'Opis', + 'field_icon' => 'Ikona wtyczki', + 'field_plugin_namespace' => 'Przestrzeń nazw wtyczki', + 'field_author_namespace' => 'Przestrzeń nazw autora', + 'field_namespace_description' => 'Przestrzeń nazw może zawierać tylko litery alfabetu łacińskiego oraz cyfry i powinna się zaczynać na literę z alfabetu łacińskiego. Przykładowa przestrzeń nazw: Blog', + 'field_author_namespace_description' => 'Nie możesz zmienić przestrzeni nazw za pomocą Buildera po utworzeniu wtyczki. Przykładowa przestrzeń nazw autora: JohnSmith', + 'tab_general' => 'Parametry ogólne', + 'tab_description' => 'Opis', + 'field_homepage' => 'Strona domowa wtyczki', + 'no_description' => 'Brak opisu dla tej wtyczki', + 'error_settings_not_editable' => 'Ustawienia tej wtyczki nie mogą być zmienione za pomocą Buildera.', + 'update_hint' => 'Możesz zmienić zlokalizowaną nazwę oraz opis wtyczki w zakładce Lokalizacja.', + 'manage_plugins' => 'Twórz i edytuj wtyczki', + ], + 'author_name' => [ + 'title' => 'Autor', + 'description' => 'Domyślna nazwa autora używana przy tworzeniu nowych wtyczek. Nazwa autora nie jest stała - możesz ją zmienić podczas konfiguracji wtyczek w dowolnym momencie.', + ], + 'author_namespace' => [ + 'title' => 'Przestrzeń nazw autora', + 'description' => 'Jeżeli tworzysz wtyczki dla Marketplace, przestrzeń nazw powinna być zgodna z kodem autora i nie można jej zmienić. Szczegółowe informacje na ten temat można znaleźć w dokumentacji.', + ], + 'database' => [ + 'menu_label' => 'Baza danych', + 'no_records' => 'Nie znaleziono tabel', + 'search' => 'Szukaj...', + 'confirmation_delete_multiple' => 'Usunąć wybrane tabele?', + 'field_name' => 'Nazwa tabeli', + 'tab_columns' => 'Kolumny', + 'column_name_name' => 'Kolumna', + 'column_name_required' => 'Podaj nazwę kolumny', + 'column_name_type' => 'Typ', + 'column_type_required' => 'Please typ kolumny', + 'column_name_length' => 'Długość', + 'column_validation_length' => 'Długość powinna być liczbą całkowitą lub być określona poprzez dokładność i skalę (10,2) dla kolumn typu Decimal. Spacje nie są dozwolone w tej kolumnie.', + 'column_validation_title' => 'Tylko cyfry, małe litery alfabetu łacińskiego oraz podkreślenia są dozwolone w nazwach kolumn', + 'column_default' => 'Wartość domyślna', + 'tab_new_table' => 'Nowa tabela', + 'btn_add_column' => 'Dodaj kolumnę', + 'btn_delete_column' => 'Usuń kolumnę', + 'btn_add_timestamps' => 'Dodaj znaczniki czasu (Timestamps)', + 'btn_add_soft_deleting' => 'Dodaj obsługę miękkiego usuwania (Soft deleting)', + 'timestamps_exist' => 'Kolumny created_at oraz deleted_at istnieją już w tabeli.', + 'soft_deleting_exist' => 'Kolumna deleted_at istnieje już w tabeli.', + 'confirm_delete' => 'Usunąć tabelę?', + 'error_enum_not_supported' => 'Tabela zawiera przynajmniej jedną kolumnę typu "enum", który nie jest aktualnie obsługiwany przez Buildera.', + 'error_table_name_invalid_prefix' => 'Nazwa tabeli powinna się zaczynać od prefiksu wtyczki: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Nieprawidłowa nazwa tabeli. Nazwy tabel powinny zawierać tylko małe litery alfabetu łacińskiego, cyfry oraz podkreślenia. Nazwy powinny zaczynać się od litery alfabetu łacińskiego i nie powinny zawierać spacji.', + 'error_table_duplicate_column' => 'Kolumna \':column\' już istnieje.', + 'error_table_auto_increment_in_compound_pk' => 'Kolumna Auto-increment nie może być częścią złożonego klucza głównego.', + 'error_table_mutliple_auto_increment' => 'Tabela nie może zawierać więcej niż jednej kolumny Auto-increment.', + 'error_table_auto_increment_non_integer' => 'Kolumna Auto-increment powinna być typu integer.', + 'error_table_decimal_length' => 'Długośc dla typu :type powinna być w formacie \'10,2\' i nie zawierać spacji.', + 'error_table_length' => 'Długość dla typu :type powinna być podana jako liczba całkowita.', + 'error_unsigned_type_not_int' => 'Błąd kolumny \':column\'. Flaga Unsigned może być ustawiona tylko dla kolumn typu integer.', + 'error_integer_default_value' => 'Niepoprawna wartość domyślna dla kolumny \':column\'. Dozwolone formaty to \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Niepoprawna wartość domyślna dla kolumny \':column\'. Dozwolone formaty to \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Niepoprawna wartość domyślna dla kolumny \':column\'. Dozwolone wartości to \'0\' oraz \'1\'.', + 'error_unsigned_negative_value' => 'Wartość domyślna dla kolumny \':column\' z flagą Unsigned nie może być ujemna.', + 'error_table_already_exists' => 'Tabela o nazwie \':name\' już istnieje w bazie danych', + 'error_table_name_too_long' => 'Nazwa tabeli nie powinna być dłuższa nić 64 znaki.', + 'error_column_name_too_long' => 'Nazwa kolumny \':column\' jest za długa. Nazwy kolumn nie powinny być dłuższe niż 64 znaki.', + ], + 'model' => [ + 'menu_label' => 'Modele', + 'entity_name' => 'Model', + 'no_records' => 'Nie znaleziono modeli', + 'search' => 'Szukaj...', + 'add' => 'Dodaj...', + 'forms' => 'Formularze', + 'lists' => 'Listy', + 'field_class_name' => 'Nazwa klasy', + 'field_database_table' => 'Tabela bazy danych', + 'field_add_timestamps' => 'Dodaj wsparcie dla znaczników czasu (Timestamps)', + 'field_add_timestamps_description' => 'Tabela musi posiadać kolumny created_at oraz updated_at.', + 'field_add_soft_deleting' => 'Dodaj wsparcie dla miękkiego usuwania (Soft deleting).', + 'field_add_soft_deleting_description' => 'Tabela musi posiadać kolumnę deleted_at.', + 'error_class_name_exists' => 'Istnieje już model o podanej nazwie klasy.: :path', + 'error_timestamp_columns_must_exist' => 'Tabela musi posiadać kolumny created_at oraz updated_at.', + 'error_deleted_at_column_must_exist' => 'Tabela musi posiadać kolumnę deleted_at.', + 'add_form' => 'Dodaj formularz', + 'add_list' => 'Dodaj listę', + ], + 'form' => [ + 'saved' => 'Zapisano formularz', + 'confirm_delete' => 'Usunąć formularz?', + 'tab_new_form' => 'Nowy formularz', + 'property_label_title' => 'Etykieta', + 'property_label_required' => 'Podaj tekst etykiety elementu.', + 'property_span_title' => 'Rozpiętość', + 'property_comment_title' => 'Komentarz', + 'property_comment_above_title' => 'Komentarz górny', + 'property_default_title' => 'Wartość domyślna', + 'property_checked_default_title' => 'Domyślnie zaznaczone', + 'property_css_class_title' => 'Klasa CSS', + 'property_css_class_description' => 'Opcjonalna klasa CSS dla kontenera.', + 'property_disabled_title' => 'Wyłączone', + 'property_read_only_title' => 'Tylko do odczytu', + 'property_hidden_title' => 'Ukryte', + 'property_required_title' => 'Wymagane', + 'property_field_name_title' => 'Nazwa pola', + 'property_placeholder_title' => 'Symbol zastępczy', + 'property_default_from_title' => 'Wartość domyślna z', + 'property_stretch_title' => 'Stretch', + 'property_stretch_description' => 'Specifies if this field stretches to fit the parent height.', + 'property_context_title' => 'Kontekst', + 'property_context_description' => 'Określa w jakim kontekście pole formularza będzie widoczne.', + 'property_context_create' => 'Stwórz', + 'property_context_update' => 'Edytuj', + 'property_context_preview' => 'Podgląd', + 'property_dependson_title' => 'Zależy od', + 'property_trigger_action' => 'Akcja', + 'property_trigger_show' => 'Pokaż', + 'property_trigger_hide' => 'Ukryj', + 'property_trigger_enable' => 'Włącz', + 'property_trigger_disable' => 'Wyłącz', + 'property_trigger_empty' => 'Puste', + 'property_trigger_field' => 'Pole', + 'property_trigger_field_description' => 'Określa nazwę drugiego pola które wyzwoli akcję.', + 'property_trigger_condition' => 'Warunek', + 'property_trigger_condition_description' => 'Określa warunek, który pole powinno spełniać, aby warunek był uznany za prawdziwy (true). Obsługiwane wartości to: checked, unchedked, value[wartość].', + 'property_trigger_condition_checked' => 'Zaznaczony', + 'property_trigger_condition_unchecked' => 'Odznaczony', + 'property_trigger_condition_somevalue' => 'value[wprowadź-wartość-tutaj]', + 'property_preset_title' => 'Preset', + 'property_preset_description' => 'Umożliwia ustawienie początkowej wartości pola na podstawie wartości innego pola przy użyciu wybranego konwertera.', + 'property_preset_field' => 'Pole', + 'property_preset_field_description' => 'Określa nazwę pola, z którego będzie pobierana wartość.', + 'property_preset_type' => 'Typ', + 'property_preset_type_description' => 'Określa typ konwertera', + 'property_attributes_title' => 'Atrybuty', + 'property_attributes_description' => 'Niestandardowe atrybuty HTML, które będą dodane do elementu formularza.', + 'property_container_attributes_title' => 'Atrybuty kontenera', + 'property_container_attributes_description' => 'Niestandardowe atrybuty HTML, które będą dodane do kontenera elementu.', + 'property_group_advanced' => 'Zaawansowane', + 'property_dependson_description' => 'Lista nazw pól, od których zależy to pole. Po ich zmianie to pole zostanie zaktualizowane. Jedno pole na linię.', + 'property_trigger_title' => 'Wyzwalacz', + 'property_trigger_description' => 'Pozwala zmieniać atrybuty elementów, takie jak widocznośc lub wartość, w zależności od stanu innych elementów.', + 'property_default_from_description' => 'Pobiera wartość domyślną z wartości innego pola.', + 'property_field_name_required' => 'Nazwa pola jest wymagana', + 'property_field_name_regex' => 'Nazwa pola może zawierać tylko litery alfabetu łacińskiego, cyfry, podkreślenia, myślniki oraz nawiasy prostokątne.', + 'property_attributes_size' => 'Rozmiar', + 'property_attributes_size_tiny' => 'Malutki', + 'property_attributes_size_small' => 'Mały', + 'property_attributes_size_large' => 'Duży', + 'property_attributes_size_huge' => 'Ogromny', + 'property_attributes_size_giant' => 'Gigantyczny', + 'property_comment_position' => 'Pozycja komentarza', + 'property_comment_position_above' => 'Powyżej', + 'property_comment_position_below' => 'Poniżej', + 'property_hint_path' => 'Ścieżka fragmentu podpowiedzi', + 'property_hint_path_description' => 'Ścieżka do pliku fragmentu zawierającego tekst podpowiedzi. Użyj symbolu $ aby odnieść się do katalogu głównego wtyczek, np.: $/acme/blog/parts/_partial.htm', + 'property_hint_path_required' => 'Podaj ścieżkę do pliku fragmentu z podpowiedzią', + 'property_partial_path' => 'Ścieżka fragmentu', + 'property_partial_path_description' => 'Ścieżka do pliku fragmentu. Użyj symbolu $ aby odnieść się do katalogu głównego wtyczek, np.: $/acme/blog/parts/_partial.htm', + 'property_partial_path_required' => 'Podaj ścieżkę do pliku fragmentu', + 'property_code_language' => 'Język', + 'property_code_theme' => 'Motyw', + 'property_theme_use_default' => 'Użyj domyślnego motywu', + 'property_group_code_editor' => 'Edytor kodu', + 'property_gutter' => 'Odstęp', + 'property_gutter_show' => 'Widoczny', + 'property_gutter_hide' => 'Ukryty', + 'property_wordwrap' => 'Zawijanie tekstu', + 'property_wordwrap_wrap' => 'Zawijaj', + 'property_wordwrap_nowrap' => 'Nie zawijaj', + 'property_fontsize' => 'Rozmiar czcionki', + 'property_codefolding' => 'Zwijanie kodu', + 'property_codefolding_manual' => 'Ręczne', + 'property_codefolding_markbegin' => 'Zaznacz początek', + 'property_codefolding_markbeginend' => 'Zaznacz początek i koniec', + 'property_autoclosing' => 'Auto zamykanie', + 'property_enabled' => 'Włączone', + 'property_disabled' => 'Wyłączone', + 'property_soft_tabs' => '"Miękkie" karty', + 'property_tab_size' => 'Rozmiar karty', + 'property_readonly' => 'Tylko do odczytu', + 'property_use_default' => 'Użyj domyślnych ustawień', + 'property_options' => 'Opcje', + 'property_prompt' => 'Zachęta', + 'property_prompt_description' => 'Tekst do wyświetlenia na przycisku utwórz.', + 'property_prompt_default' => 'Dodaj nowy element', + 'property_available_colors' => 'Dostępne kolory', + 'property_available_colors_description' => 'Lista dostępnych kolorów w formacie heksadecymalnym (#FF0000). Pozostaw puste dla domyślnego zestawu kolorów. Jedna wartość na wiersz.', + 'property_datepicker_mode' => 'Tryb', + 'property_datepicker_mode_date' => 'Data', + 'property_datepicker_mode_datetime' => 'Data i czas', + 'property_datepicker_mode_time' => 'Czas', + 'property_datepicker_min_date' => 'Najwcześniejsza data', + 'property_datepicker_max_date' => 'Najpóźniejsza data', + 'property_datepicker_year_range' => 'Zakres lat', + 'property_datepicker_year_range_description' => 'Liczba lat z obu stron zakresu (np. "10") lub dolny i górny zakres w tablicy (np. "[1900,2015]"). Pozostaw puste dla wartości domyślnej (10).', + 'property_datepicker_year_range_invalid_format' => 'Nieprawidłowy format zakresu lat. Użyj liczby (np. "10") lub dolnego i górnego zakresu w tablicy (np. "[1900,2015]")', + 'property_datepicker_format' => 'Format', + 'property_datepicker_year_format_description' => 'Zdefiniuj niestandardowy format daty. Domyślny format to "Y-m-d"', + 'property_fileupload_mode' => 'Tryb', + 'property_fileupload_mode_file' => 'Plik', + 'property_fileupload_mode_image' => 'Obraz', + 'property_group_fileupload' => 'Przesyłanie pliku', + 'property_fileupload_image_width' => 'Szerokość obrazu', + 'property_fileupload_image_width_description' => 'Opcjonalne. Obrazy będą przeskalowane do tej szerokości. Dotyczy tylko trybu Obraz.', + 'property_fileupload_invalid_dimension' => 'Niepoprawny rozmiar - wprowadź liczbę.', + 'property_fileupload_image_height' => 'Wysokość obrazu', + 'property_fileupload_image_height_description' => 'Opcjonalne. obrazy będą przeskalowane do tej wysokości. Dotyczy tylko trybu Obraz.', + 'property_fileupload_file_types' => 'Rozszerzenia plików', + 'property_fileupload_file_types_description' => 'Opcjonalne. Lista akceptowanych rozszerzeń plików rozdzielona przecinkami. Przykład: zip,txt', + 'property_fileupload_mime_types' => 'Typy MIME', + 'property_fileupload_mime_types_description' => 'Opcjonalne. Lista akceptowanych typów MIME rozdzielona przecinkami, podane jako rozszerzenia plików lub pełne nazwy. Przykład: bin,txt', + 'property_fileupload_use_caption' => 'Użyj podpisu', + 'property_fileupload_use_caption_description' => 'Umożliwia ustawienie tytułu i opisu pliku.', + 'property_fileupload_thumb_options' => 'Opcje miniatur', + 'property_fileupload_thumb_options_description' => 'Zarządza opcjami automatycznie generowanych miniatur. Dotyczy tylko trybu Obraz.', + 'property_fileupload_thumb_mode' => 'Tryb', + 'property_fileupload_thumb_auto' => 'Automatyczny', + 'property_fileupload_thumb_exact' => 'Dokładny', + 'property_fileupload_thumb_portrait' => 'Pionowy', + 'property_fileupload_thumb_landscape' => 'Poziomy', + 'property_fileupload_thumb_crop' => 'Przytnij', + 'property_fileupload_thumb_extension' => 'Rozszerzenie pliku', + 'property_name_from' => 'Kolumna nazwy', + 'property_name_from_description' => 'Nazwa kolumny w relacji używana do wyświetlania nazwy', + 'property_relation_select' => 'Wybierz', + 'property_relation_select_description' => 'Połącz (CONCAT) wiele kolumn do wyświetlania nazwy', + 'property_description_from' => 'Kolumna opisu', + 'property_description_from_description' => 'Nazwa kolumny w relacji używana do wyświetlania opisu', + 'property_recordfinder_prompt' => 'Zachęta', + 'property_recordfinder_prompt_description' => 'Tekst, który zostanie wyświetlony, gdy nie wybrano żadnego rekordu. Znak %s reprezentuje ikonę wyszukiwania. Pozostaw puste dla domyślnej zachęty.', + 'property_recordfinder_list' => 'Konfiguracja listy', + 'property_recordfinder_list_description' => 'Ścieżka do pliku konfiguracyjnego kolumny listy. Użyj symbolu $ aby odnieść się do katalogu głównego wtyczek, np.: $/acme/blog/parts/_partial.htm', + 'property_recordfinder_list_required' => 'Podaj ścieżkę do pliku YAML listy', + 'property_group_recordfinder' => 'Wyszukiwarka rekordów', + 'property_mediafinder_mode' => 'Tryb', + 'property_mediafinder_mode_file' => 'Plik', + 'property_mediafinder_mode_image' => 'Obraz', + 'property_mediafinder_image_width_description' => 'Opcjonalne. Jeżeli używasz trybu "Obraz", pogląd zostanie wyświetlony do tej szerokości.', + 'property_mediafinder_image_height_description' => 'Opcjonalne. Jeżeli używasz trybu "Obraz", pogląd zostanie wyświetlony do tej wysokości.', + 'property_group_taglist' => 'Lista tagów', + 'property_taglist_mode' => 'Tryb', + 'property_taglist_mode_description' => 'Określa format w jakim zwracana jest wartość pola', + 'property_taglist_mode_string' => 'Ciąg znaków', + 'property_taglist_mode_array' => 'Tablica', + 'property_taglist_mode_relation' => 'Relacja', + 'property_taglist_separator' => 'Separator', + 'property_taglist_separator_comma' => 'Przecinki', + 'property_taglist_separator_space' => 'Spacje', + 'property_taglist_options' => 'Predefiniowane tagi', + 'property_taglist_custom_tags' => 'Niestandardowe tagi', + 'property_taglist_custom_tags_description' => 'Zezwala na ręczne wprowadzanie niestandardowych tagów przez użytkownika.', + 'property_taglist_name_from' => 'Nazwa z', + 'property_taglist_name_from_description' => 'Określa atrybut modelu relacji wyświetlany w znaczniku. Używany tylko w trybie "Relacja".', + 'property_taglist_use_key' => 'Użyj klucza', + 'property_taglist_use_key_description' => 'Jeśli zaznaczone, lista tagów użyje klucza zamiast wartości do zapisu i odczytu danych. Używany tylko w trybie "Relacja".', + 'property_group_relation' => 'Relacja', + 'property_relation_prompt' => 'Zachęta', + 'property_relation_prompt_description' => 'Tekst wyświetlany, gdy nie ma dostępnych opcji.', + 'property_empty_option' => 'Pusta opcja', + 'property_empty_option_description' => 'Pusta opcja odpowiada pustemu wyborowi, jednakże w przeciwieństwie do niego może być ponownie wybrana.', + 'property_show_search' => 'Włącz wyszukiwanie', + 'property_show_search_description' => 'Włącza funkcję wyszukiwania dla tej listy.', + 'property_max_items' => 'Maksymalna liczba elementów', + 'property_max_items_description' => 'Maksymalna liczba elementów dozwolona w repeaterze.', + 'control_group_standard' => 'Standardowe kontrolki', + 'control_group_widgets' => 'Widżety', + 'click_to_add_control' => 'Dodaj kontrolkę', + 'loading' => 'Ładowanie...', + 'control_text' => 'Tekst', + 'control_text_description' => 'Jednowierszowe pole tekstowe', + 'control_password' => 'Hasło', + 'control_password_description' => 'Jednowierszowe pole hasła', + 'control_checkbox' => 'Pole wyboru', + 'control_checkbox_description' => 'Pojedyncze pole wyboru', + 'control_switch' => 'Przełącznik', + 'control_switch_description' => 'Pojedynczy przełącznik. Alternatywa dla pola wyboru', + 'control_textarea' => 'Pole tekstowe', + 'control_textarea_description' => 'Wielowierszowe pole tekstowe o kontrolowanej wysokości', + 'control_dropdown' => 'Lista rozwijana', + 'control_dropdown_description' => 'Lista rozwijana ze statycznymi lub dynamicznie ładowanymi opcjami', + 'control_balloon-selector' => 'Selektor typu Balloon', + 'control_balloon-selector_description' => 'Lista ze statycznymi lub dynamicznie ładowanymi opcjami, w której jednocześnie może być wybrany tylko jeden element', + 'control_unknown' => 'Nieznany rodzaj elementu: :type', + 'control_repeater' => 'Repeater', + 'control_repeater_description' => 'Wyświetla zestaw powtarzających się elementów formularza', + 'control_number' => 'Numer', + 'control_number_description' => 'Jednowierszowe pole tekstowe, przyjmujące jedynie liczby', + 'control_hint' => 'Podpowiedź', + 'control_hint_description' => 'Wyświetla zawartość fragmentu w elemencie, który może być ukryty przez użytkownika', + 'control_partial' => 'Fragment', + 'control_partial_description' => 'Wyświetla zawartość fragmentu', + 'control_section' => 'Sekcja', + 'control_section_description' => 'Wyświetla sekcję z tytułem i podtytułem', + 'control_radio' => 'Lista pól opcji', + 'control_radio_description' => 'Lista pól opcji, w której jednocześnie może być wybrany tylko jeden element', + 'control_radio_option_1' => 'Opcja 1', + 'control_radio_option_2' => 'Opcja 2', + 'control_checkboxlist' => 'Lista pól wyboru', + 'control_checkboxlist_description' => 'Lista pól wyboru, w której jednocześnie może być wybrane kilka elementów', + 'control_codeeditor' => 'Edytor kodu', + 'control_codeeditor_description' => 'Edytor tekstowy sformatowanego kodu lub znaczników', + 'control_colorpicker' => 'Próbnik koloru', + 'control_colorpicker_description' => 'Pole wyboru koloru w wartości heksadecymalnej', + 'control_datepicker' => 'Wybór daty', + 'control_datepicker_description' => 'Pole tekstowe używane do wyboru daty i godziny', + 'control_richeditor' => 'Edytor WYSYWIG', + 'control_richeditor_description' => 'Edytor wizualny tekstu sformatowanego', + 'control_markdown' => 'Edytor Markdown', + 'control_markdown_description' => 'Edytor tekstu sformatowanego za pomocą Markdown', + 'control_taglist' => 'Lista tagów', + 'control_taglist_description' => 'Pole do wprowadzania listy tagów', + 'control_fileupload' => 'Prześlij plik', + 'control_fileupload_description' => 'Pole do przesyłania obrazów lub zwykłych plików', + 'control_recordfinder' => 'Wyszukiwarka rekordów', + 'control_recordfinder_description' => 'Pole ze szcegółami powiązanego rekordu z funkcją wyszukiwania rekordów', + 'control_mediafinder' => 'Wyszukiwarka mediów', + 'control_mediafinder_description' => 'Pole wyboru elementu z biblioteki mediów', + 'control_relation' => 'Relacja', + 'control_relation_description' => 'Wyświetla listę rozwijaną lub listę pól wyboru w celu wybrania powiązanego rekordu', + 'error_file_name_required' => 'Wprowadź nazwę pliku formularza.', + 'error_file_name_invalid' => 'Nazwa pliku może zawierać jedynie litery alfabetu łacińskiego, cyfry, podkreślenia, kropki i krzyżyki.', + 'span_left' => 'Lewo', + 'span_right' => 'Prawo', + 'span_full' => 'Cała szerokość', + 'span_auto' => 'Auto', + 'empty_tab' => 'Pusta karta', + 'confirm_close_tab' => 'Zakładka zawiera elementy, które zostaną usunięte. Czy kontynuować?', + 'tab' => 'Karta formularza', + 'tab_title' => 'Tytuł', + 'controls' => 'Kontrolki', + 'property_tab_title_required' => 'Tytuł karty jest wymagany.', + 'tabs_primary' => 'Zakładki główne', + 'tabs_secondary' => 'Zakładki dodatkowe', + 'tab_stretch' => 'Rozciągnięcie', + 'tab_stretch_description' => 'Określa czy kontener kart rozciąga się, aby pasować do wysokości rodzica.', + 'tab_css_class' => 'Klasa CSS', + 'tab_css_class_description' => 'Przypisuje klasę CSS do kontenera kart.', + 'tab_name_template' => 'Karta %s', + 'tab_already_exists' => 'Tab with the specified title already exists.', + ], + 'list' => [ + 'tab_new_list' => 'Nowa lista', + 'saved' => 'Zapisano listę', + 'confirm_delete' => 'Usunąć listę?', + 'tab_columns' => 'Kolumny', + 'btn_add_column' => 'Dodaj kolumnę', + 'btn_delete_column' => 'Usuń kolumnę', + 'column_dbfield_label' => 'Pola', + 'column_dbfield_required' => 'Podaj pole modelu', + 'column_name_label' => 'Etykieta', + 'column_label_required' => 'Podaj etykietę kolumny', + 'column_type_label' => 'Type', + 'column_type_required' => 'Podaj typ kolumny', + 'column_type_text' => 'Tekst', + 'column_type_number' => 'Numer', + 'column_type_switch' => 'Przełącznik', + 'column_type_datetime' => 'Data/Czas', + 'column_type_date' => 'Data', + 'column_type_time' => 'Czas', + 'column_type_timesince' => 'Czas od', + 'column_type_timetense' => 'Określenie czasu', + 'column_type_select' => 'Lista elementów', + 'column_type_partial' => 'Fragment', + 'column_label_default' => 'Domyślny', + 'column_label_searchable' => 'Szukaj', + 'column_label_sortable' => 'Sortuj', + 'column_label_invisible' => 'Niewidoczny', + 'column_label_select' => 'Wybierz', + 'column_label_relation' => 'Relacja', + 'column_label_css_class' => 'Klasa CSS', + 'column_label_width' => 'Szerokość', + 'column_label_path' => 'Ścieżka', + 'column_label_format' => 'Format', + 'column_label_value_from' => 'Wartość z', + 'error_duplicate_column' => 'Kolumna \':column\' już istnieje.', + 'btn_add_database_columns' => 'Dodaj kolumny z bazy danych', + 'all_database_columns_exist' => 'Wszystkie kolumny są już zdefiniowane na liście', + ], + 'controller' => [ + 'menu_label' => 'Kontrolery', + 'no_records' => 'Nie znaleziono kontrolerów', + 'controller' => 'Kontroler', + 'behaviors' => 'Zachowania', + 'new_controller' => 'Nowy kontroler', + 'error_controller_has_no_behaviors' => 'Kontroler nie posiada konfigurowalnych zachowań.', + 'error_invalid_yaml_configuration' => 'Wystąpił błąd podczas ładowania pliku konfiguracji zachowania :file', + 'behavior_form_controller' => 'Zachowanie - Form Controller', + 'behavior_form_controller_description' => 'Dodaje funkcjonalność do strony backendu. Zapewnia strony Utwórz, Edytuj, Podgląd.', + 'property_behavior_form_placeholder' => '--wybierz formularz--', + 'property_behavior_form_name' => 'Nazwa', + 'property_behavior_form_name_description' => 'Nazwa obiektu zarządzanego przez ten formularz', + 'property_behavior_form_name_required' => 'Wprowadź nazwę formularza', + 'property_behavior_form_file' => 'Konfiguracja formularza', + 'property_behavior_form_file_description' => 'Odwołanie do pliku konfiguracyjnego formularza', + 'property_behavior_form_file_required' => 'Wprowadź ścieżkę do pliku konfiguracyjnego formularza', + 'property_behavior_form_model_class' => 'Klasa modelu', + 'property_behavior_form_model_class_description' => 'Nazwa klasy modelu. Dane formularza są ładowane i zapisywane używając tego modelu.', + 'property_behavior_form_model_class_required' => 'Wybierz klasę modelu', + 'property_behavior_form_default_redirect' => 'Domyślne przekierowanie', + 'property_behavior_form_default_redirect_description' => 'Strona, na którą zostanie przekierowany użytkownik po zapisaniu lub anulowaniu formularza.', + 'property_behavior_form_create' => 'Stwórz stronę rekordu', + 'property_behavior_form_redirect' => 'Przekierowanie', + 'property_behavior_form_redirect_description' => 'Strona, na którą zostanie przekierowany użytkownik po utworzeniu rekordu.', + 'property_behavior_form_redirect_close' => 'Przekierowanie po zamknięciu', + 'property_behavior_form_redirect_close_description' => 'Strona na którą zostanie przekierowany użytkownik po utworzeniu rekordu, gdy zmienna "close" w tablicy POST jest obecna.', + 'property_behavior_form_flash_save' => 'Komunikat Flash - Zapisano', + 'property_behavior_form_flash_save_description' => 'Komunikat do wyświetlenia po zapisaniu rekordu.', + 'property_behavior_form_page_title' => 'Tytuł strony', + 'property_behavior_form_update' => 'Aktualizuj stronę rekordu', + 'property_behavior_form_update_redirect' => 'Przekierowanie', + 'property_behavior_form_create_redirect_description' => 'Strona, na którą zostanie przekierowany użytkownik po zapisaniu rekordu.', + 'property_behavior_form_flash_delete' => 'Komunikat Flash - Usunięto', + 'property_behavior_form_flash_delete_description' => 'Komunikat do wyświetlenia po usunięciu rekordu.', + 'property_behavior_form_preview' => 'Strona podglądu rekordu', + 'behavior_list_controller' => 'Zachowanie - List Controller', + 'behavior_list_controller_description' => 'Dodaje funkcjonalność do strony backendu. Zapewnia listę z możliwością sortowania i przeszukiwania i opcjonalnymi linkami do rekordów. Automatycznie tworzy akcję kontrolera "index".', + 'property_behavior_list_title' => 'Tytuł listy', + 'property_behavior_list_title_required' => 'Wprowadź tytuł listy', + 'property_behavior_list_placeholder' => '--wybierz listę--', + 'property_behavior_list_model_class' => 'Klasa modelu', + 'property_behavior_list_model_class_description' => 'Nazwa klasy modelu, dane są ładowane z tego modelu.', + 'property_behavior_form_model_class_placeholder' => '--wybierz model--', + 'property_behavior_list_model_class_required' => 'Wybierz klasę modelu', + 'property_behavior_list_model_placeholder' => '--wybierz model--', + 'property_behavior_list_file' => 'Plik konfiguracyjny listy', + 'property_behavior_list_file_description' => 'Odwołanie do pliku konfiguracyjnego listy', + 'property_behavior_list_file_required' => 'Wprowadź ścieżkę do pliku konfiguracyjnego listy', + 'property_behavior_list_record_url' => 'Adres URL rekordu', + 'property_behavior_list_record_url_description' => 'Połącz każdy rekord z unikatową stroną. Np.: "users/update/:id". Parametr ":id" będzie zastąpiony identyfikatorem rekordu.', + 'property_behavior_list_no_records_message' => 'Komunikat o braku rekordów', + 'property_behavior_list_no_records_message_description' => 'Komunikat, który zostanie wyświetlony w przypadku braku rekordów.', + 'property_behavior_list_recs_per_page' => 'Rekordy na stronę', + 'property_behavior_list_recs_per_page_description' => 'Liczba rekordów do wyświetlenia na stronie. Wpisz 0 aby wyłączyć paginację. Wartość domyślna: 0', + 'property_behavior_list_recs_per_page_regex' => 'Liczba rekordów na stronę powinna być liczbą całkowitą', + 'property_behavior_list_show_setup' => 'Pokaż przycisk konfiguracji', + 'property_behavior_list_show_sorting' => 'Pokaż sortowanie', + 'property_behavior_list_default_sort' => 'Domyślne sortowanie', + 'property_behavior_form_ds_column' => 'Kolumna', + 'property_behavior_form_ds_direction' => 'Kierunek', + 'property_behavior_form_ds_asc' => 'Rosnąco', + 'property_behavior_form_ds_desc' => 'Malejąco', + 'property_behavior_list_show_checkboxes' => 'Pokaż pola wyboru', + 'property_behavior_list_onclick' => 'Kod JS po kliknięciu', + 'property_behavior_list_onclick_description' => 'Dodatkowy kod JavaScript do wykonania po kliknięciu rekordu.', + 'property_behavior_list_show_tree' => 'Pokaż drzewo', + 'property_behavior_list_show_tree_description' => 'Wyświetla hierarchię dla rekordów nadrzędnych / podrzędnych.', + 'property_behavior_list_tree_expanded' => 'Rozwinięte drzewo', + 'property_behavior_list_tree_expanded_description' => 'Określa czy gałęzie drzewa powinny być domyślnie rozwinięte.', + 'property_behavior_list_toolbar' => 'Pasek narzędzi', + 'property_behavior_list_toolbar_buttons' => 'Fragment z przyciskami', + 'property_behavior_list_toolbar_buttons_description' => 'Odwołanie do pliku fragmentu z przyciskami paska narzędzi. Np.: list_toolbar', + 'property_behavior_list_search' => 'Szukaj', + 'property_behavior_list_search_prompt' => 'Zachęta wyszukiwarki', + 'property_behavior_list_filter' => 'Konfiguracja filtrów', + 'behavior_reorder_controller' => 'Zachowanie - Reorder Controller', + 'behavior_reorder_controller_description' => 'Dodaje funkcjonalność do strony backendu. Zapewnia możliwość sortowania i zmiany kolejności rekordów. Automatycznie tworzy akcję kontrolera "reorder".', + 'property_behavior_reorder_title' => 'Tekst zmiany kolejności', + 'property_behavior_reorder_title_required' => 'Wprowadź tekst zmiany kolejności', + 'property_behavior_reorder_name_from' => 'Nazwa atrybutu', + 'property_behavior_reorder_name_from_description' => 'Atrybut modelu, który zostanie użyty jako etykieta rekordu.', + 'property_behavior_reorder_name_from_required' => 'Wprowadź nazwę atrybutu', + 'property_behavior_reorder_model_class' => 'Klasa modelu', + 'property_behavior_reorder_model_class_description' => 'Nazwa klasy modelu. Dane są ładowane z tego modelu.', + 'property_behavior_reorder_model_class_placeholder' => '--wybierz model--', + 'property_behavior_reorder_model_class_required' => 'Wybierz klasę modelu', + 'property_behavior_reorder_model_placeholder' => '--wybierz klasę modelu--', + 'property_behavior_reorder_toolbar' => 'Pasek narzędzi', + 'property_behavior_reorder_toolbar_buttons' => 'Fragment z przyciskami', + 'property_behavior_reorder_toolbar_buttons_description' => 'Odwołanie do pliku fragmentu z przyciskami paska narzędzi. Np.: reorder_toolbar', + 'error_controller_not_found' => 'Plik kontrolera nie został znaleziony.', + 'error_invalid_config_file_name' => 'Plik konfiguracyjny zachowania :class (:file) zawiera nieprawidłowe znaki i nie może zostać załadowany.', + 'error_file_not_yaml' => 'Plik konfiguracyjny zachowania :class (:file) nie jest plikiem YAML. Obsługiwane są tylko pliki konfiguracyjne YAML.', + 'saved' => 'Zapisano kontroler', + 'controller_name' => 'Nazwa kontrolera', + 'controller_name_description' => 'Nazwa kontrolera określa nazwę klasy i adres URL stron w backendzie. Obowiązują standardowe konwencje nazewnictwa zmiennych w PHP. Pierwszym znakiem powinna być duża litera alfabetu łacińskiego. Przykłady: Categories, Posts, Products.', + 'base_model_class' => 'Bazowa klasa modelu', + 'base_model_class_description' => 'Wybierz klasę modelu, która ma być używana jako model bazowy w zachowaniach które go wymagają lub wspierają. Zachowania możesz skonfigurować później.', + 'base_model_class_placeholder' => '--wybierz model--', + 'controller_behaviors' => 'Zachowania', + 'controller_behaviors_description' => 'Wybierz zachowania, które chcesz zaimplementować w kontrolerze. Builder automatycznie utworzy pliki widoków wymaganych przez zachowania.', + 'controller_permissions' => 'Uprawnienia', + 'controller_permissions_description' => 'Wybierz uprawnienia, które pozwolą na dostęp do widoków kontrolera. Uprawnienia można zdefiniować w zakładce "Uprawnienia". Możesz zmienić tę opcję później edytując skrypt PHP kontrolera.', + 'controller_permissions_no_permissions' => 'Wtyczka nie definiuje żadnych uprawnień.', + 'menu_item' => 'Aktywny element menu', + 'menu_item_description' => 'Wybierz element menu dla stron tego kontrolera. Możesz zmienić tę opcję później edytując skrypt PHP kontrolera.', + 'menu_item_placeholder' => '--wybierz element menu--', + 'error_unknown_behavior' => 'Klasa zachowania :class nie jest zarejestrowana w bibliotece zachowań.', + 'error_behavior_view_conflict' => 'Wybrane zachowania implementują sprzeczne ze sobą widoki (:view) i nie mogą być używane razem w jednym kontrolerze.', + 'error_behavior_config_conflict' => 'Wybrane zachowania implementują sprzeczne pliki konfiguracyjne (:file) i nie mogą być używane razem w jednym kontrolerze.', + 'error_behavior_view_file_not_found' => 'Nie można znaleźć szablonu widoku zachowania :class w lokalizacji :file.', + 'error_behavior_config_file_not_found' => 'Nie można znaleźć szablonu konfiguracji zachowania :class w lokalizacji :file.', + 'error_controller_exists' => 'Plik kontrolera :file już istnieje.', + 'error_controller_name_invalid' => 'Nieprawidłowy format nazwy kontrolera. Nazwa powinna zawierać tylko cyfry i litery alfabetu łacińskiego. Pierwszym symbolem powinna być duża litera alfabetu łacińskiego.', + 'error_behavior_view_file_exists' => 'Widok kontrolera :view już istnieje.', + 'error_behavior_config_file_exists' => 'Plik konfiguracyjny zachowania :file już istnieje.', + 'error_save_file' => 'Błąd podczas zapisu pliku kontrolera: :file', + 'error_behavior_requires_base_model' => 'Zachowanie :behavior wymaga wybranej bazowej klasy modelu.', + 'error_model_doesnt_have_lists' => 'Wybrany model nie posiada żadnych list. Najpierw utwórz listę.', + 'error_model_doesnt_have_forms' => 'Wybrany model nie posiada żadnych formularzy. Najpierw utwórz formularz.', + ], + 'version' => [ + 'menu_label' => 'Wersje', + 'no_records' => 'Nie znaleziono żadnej wersji wtyczki', + 'search' => 'Szukaj...', + 'tab' => 'Wersje', + 'saved' => 'Zapisano wersję', + 'confirm_delete' => 'Usunąć wersję?', + 'tab_new_version' => 'Nowa wersja', + 'migration' => 'Migracja', + 'seeder' => 'Seeder', + 'custom' => 'Zwiększ numer wersji', + 'apply_version' => 'Zastosuj wersję', + 'applying' => 'Zastosowywanie...', + 'rollback_version' => 'Wycofaj wersję', + 'rolling_back' => 'Wycofywanie...', + 'applied' => 'Zastosowano wersję', + 'rolled_back' => 'Wycofano wersję', + 'hint_save_unapplied' => 'Zapisałeś niezastosowaną wersję. Należy pamiętać, że niezastosowane wersje mogą być automatycznie zastosowane przez system, gdy Ty lub inny użytkownik loguje się do backendu lub gdy tabela bazy danych jest zapisywana w sekcji Baza danych Buildera.', + 'hint_rollback' => 'Wycofanie wersji spowoduje wycofanie wszystkich nowszych wersji niż ta. Należy pamiętać, że niezastosowane wersje mogą być automatycznie zastosowane przez system, gdy Ty lub inny użytkownik loguje się do backendu lub gdy tabela bazy danych jest zapisywana w sekcji Baza danych Buildera.', + 'hint_apply' => 'Zastosowanie wersji spowoduje zastosowanie wszystkich poprzednich niezastosowanych wersji', + 'dont_show_again' => 'Nie pokazuj ponownie', + 'save_unapplied_version' => 'Zapisz niezastosowaną wersję', + ], + 'menu' => [ + 'menu_label' => 'Menu Back-End', + 'tab' => 'Menu', + 'items' => 'Elementy menu', + 'saved' => 'Zapisano menu', + 'add_main_menu_item' => 'Dodaj element menu głównego', + 'new_menu_item' => 'Element menu', + 'add_side_menu_item' => 'Dodaj element podrzędny', + 'side_menu_item' => 'Element menu bocznego', + 'property_label' => 'Etykieta', + 'property_label_required' => 'Podaj etykietę elementu menu.', + 'property_url_required' => 'Podaj adres URL elementu menu', + 'property_url' => 'URL', + 'property_icon' => 'Ikona', + 'property_icon_required' => 'Wybierz ikonę', + 'property_permissions' => 'Uprawnienia', + 'property_order' => 'Kolejność', + 'property_order_invalid' => 'Podaj kolejność elementu menu jako liczbę całkowitą.', + 'property_order_description' => 'Kolejność elementu menu ustala jego pozycje w menu. Jeśli kolejność nie jest podana, element zostanie ustawiony na końcu menu. Domyślne wartości kolejności są wielokrotnością liczby 100.', + 'property_attributes' => 'Atrybuty HTML', + 'property_code' => 'Kod', + 'property_code_invalid' => 'Kod powinien zawierać tylko litery alfabetu łacińskiego i cyfry.', + 'property_code_required' => 'Podaj kod elementu menu.', + 'error_duplicate_main_menu_code' => 'Kod \':code\' istnieje już w menu głównym.', + 'error_duplicate_side_menu_code' => 'Kod \':code\' istnieje już w menu bocznym.', + ], + 'localization' => [ + 'menu_label' => 'Lokalizacja', + 'language' => 'Język', + 'strings' => 'Ciągi znaków', + 'confirm_delete' => 'Usunąć język?', + 'tab_new_language' => 'Nowy język', + 'no_records' => 'Nie znaleziono języków', + 'saved' => 'Zapisano plik języka', + 'error_cant_load_file' => 'Nie można załadować pliku tekstowego - nie znaleziono pliku.', + 'error_bad_localization_file_contents' => 'Nie można załadować pliku tekstowego. Pliki językowe powinny zawierać jedynie definicje tablicy i ciągi znaków.', + 'error_file_not_array' => 'Nie można załadować pliku tekstowego. Pliki językowe powinny zwracać tablicę.', + 'save_error' => 'Błąd podczas zapisywania pliku \':name\'. Sprawdź uprawnienia do zapisu.', + 'error_delete_file' => 'Nie można usunąć pliku językowego.', + 'add_missing_strings' => 'Dodaj brakujące ciągi znaków', + 'copy' => 'Kopiuj', + 'add_missing_strings_label' => 'Wybierz język, z którego chcesz skopiować brakujące ciągi znaków', + 'no_languages_to_copy_from' => 'Nie ma języków, z których można skopiować ciągi znaków.', + 'new_string_warning' => 'Nowy ciąg znaków lub sekcja', + 'structure_mismatch' => 'Struktura pliku języka źródłowego nie zgadza się ze strukturą edytowanego pliku. Niektóre pojedyncze ciągi znaków w edytowanym pliku odpowiadają sekcjom w pliku źródłowym (lub odwrotnie) ie nie można ich scalić automatycznie.', + 'create_string' => 'Utwórz nowy ciąg znaków', + 'string_key_label' => 'Klucz', + 'string_key_comment' => 'Wprowadź klucz używając kropki jako separatora sekcji. Na przykład plugin.search. Ciąg znaków zostanie utworzony w domyślnym pliku językowym wtyczki.', + 'string_value' => 'Wartość', + 'string_key_is_empty' => 'Klucz nie powinien być pusty', + 'string_key_is_a_string' => ':key jest ciągiem znaków i nie może zawierać innych ciągów znaków.', + 'string_value_is_empty' => 'Wartość nie powinna być pusta', + 'string_key_exists' => 'Klucz już istnieje', + ], + 'permission' => [ + 'menu_label' => 'Uprawnienia', + 'tab' => 'Uprawnienia', + 'form_tab_permissions' => 'Uprawnienia', + 'btn_add_permission' => 'Dodaj uprawnienie', + 'btn_delete_permission' => 'Usuń uprawnienie', + 'column_permission_label' => 'Kod uprawnienia', + 'column_permission_required' => 'Podaj kod uprawnienia', + 'column_tab_label' => 'Tytuł karty', + 'column_tab_required' => 'Podaj nazwę karty uprawnienia', + 'column_label_label' => 'Etykieta', + 'column_label_required' => 'Podaj etykietę uprawnienia', + 'saved' => 'Zapisano uprawnienie', + 'error_duplicate_code' => 'Kod uprawnienia \':code\' już istnieje.', + ], + 'yaml' => [ + 'save_error' => 'Błąd podczas zapisywania pliku \':name\'. Sprawdź uprawnienia do zapisu.', + ], + 'common' => [ + 'error_file_exists' => 'Plik już istnieje: \':path\'.', + 'field_icon_description' => 'October używa ikon Font Autumn: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'Folder docelowy nie istnieje: \':path\'.', + 'error_make_dir' => 'Błąd podczas tworzenia folderu: \':name\'.', + 'error_dir_exists' => 'Folder już istnieje: \':path\'.', + 'template_not_found' => 'Nie znaleziono pliku szablonu: \':name\'.', + 'error_generating_file' => 'Błąd podczas tworzenia pliku: \':path\'.', + 'error_loading_template' => 'Błąd podczas ładowania pliku szablonu: \':name\'.', + 'select_plugin_first' => 'Najpierw wybierz wtyczkę. Aby zobaczyć listę wtyczek kliknij ikonę > w lewym pasku bocznym.', + 'plugin_not_selected' => 'Nie wybrano wtyczki', + 'add' => 'Dodaj', + ], + 'migration' => [ + 'entity_name' => 'Migracja', + 'error_version_invalid' => 'Wersja powinna być w formacie 1.0.1', + 'field_version' => 'Wersja', + 'field_description' => 'Opis', + 'field_code' => 'Kod', + 'save_and_apply' => 'Zapisz & Zastosuj', + 'error_version_exists' => 'Wersja już istnieje.', + 'error_script_filename_invalid' => 'Nazwa pliku migracji może zawierać tylko litery z alfabetu łacińskiego, cyfry i podkreślenia. Nazwa powinna zaczynać się od litery z alfabetu łacińskiego i nie może zawierać spacji.', + 'error_cannot_change_version_number' => 'Nie można zmienić numeru wersji dla zastosowanej wersji', + 'error_file_must_define_class' => 'Kod migracji powinien definiować klasę migracji lub seedera. Pozostaw pole puste jeśli chcesz tylko zaktualizować numer wersji.', + 'error_file_must_define_namespace' => 'Kod migracji powinien definiować przestrzeń nazw. Pozostaw pole puste, jeśli chcesz tylko zaktualizować numer wersji.', + 'no_changes_to_save' => 'Brak zmian do zapisania.', + 'error_namespace_mismatch' => 'Kod migracji powinien używać przestrzeni nazw wtyczki: :namespace', + 'error_migration_file_exists' => 'Plik migracji :file już istnieje. Użyj innej nazwy klasy.', + 'error_cant_delete_applied' => 'Ta wersja została już zastosowana i nie może być usunięta. Wycofaj ją w celu usunięcia.', + ], + 'components' => [ + 'list_title' => 'Lista rekordów', + 'list_description' => 'Wyświetla listę rekordów dla wybranego modelu', + 'list_page_number' => 'Numer strony', + 'list_page_number_description' => 'Wartość jest używana aby określić, na której stronie znajduje się użytkownik.', + 'list_records_per_page' => 'Rekordy na stronę', + 'list_records_per_page_description' => 'Liczba rekordów do wyświetlenia na stronie. Zostaw puste aby wyłączyć paginację.', + 'list_records_per_page_validation' => 'Nieprawidłowy format liczby rekordów na stronę. Wartość powinna być liczbą.', + 'list_no_records' => 'Komunikat o braku rekordów', + 'list_no_records_description' => 'Komunikat, który zostanie wyświetlony w przypadku braku rekordów. Używany w domyślnym fragmencie komponentu.', + 'list_no_records_default' => 'Nie znaleziono rekordów', + 'list_sort_column' => 'Sortuj wg kolumny', + 'list_sort_column_description' => 'Kolumna modelu wg której będą sortowane rekordy', + 'list_sort_direction' => 'Kierunek', + 'list_display_column' => 'Kolumna do wyświetlenia', + 'list_display_column_description' => 'Kolumna do wyświetlenia na liście. Używana w domyślnym fragmencie komponentu.', + 'list_display_column_required' => 'Wybierz kolumnę do wyświetlenia.', + 'list_details_page' => 'Strona szczegółów', + 'list_details_page_description' => 'Strona do wyświetlania szczegółów rekordu.', + 'list_details_page_no' => '--brak strony szczegółów--', + 'list_sorting' => 'Sortowanie', + 'list_pagination' => 'Paginacja', + 'list_order_direction_asc' => 'Rosnąco', + 'list_order_direction_desc' => 'Malejąco', + 'list_model' => 'Klasa modelu', + 'list_scope' => 'Zakres', + 'list_scope_description' => 'Zakres modelu do pobierania rekordów. Opcjonalne', + 'list_scope_default' => '--wybierz zakres, opcjonalne--', + 'list_scope_value' => 'Wartość zakresu', + 'list_scope_value_description' => 'Opcjonalna wartość do przekazania do zakresu modelu.', + 'list_details_page_link' => 'Link do strony szczegółów', + 'list_details_key_column' => 'Kolumna klucza szczegółów', + 'list_details_key_column_description' => 'Kolumna modelu używana jako identyfikator rekordu w linkach na stronie szczegółów.', + 'list_details_url_parameter' => 'Nazwa parametru URL', + 'list_details_url_parameter_description' => 'Nazwa parametru adresu URL strony ze szczegółami, który służy jako identyfikator rekordu.', + 'details_title' => 'Szczegóły rekordu', + 'details_description' => 'Wyświetla szczegóły rekordu dla wybranego modelu', + 'details_model' => 'Klasa modelu', + 'details_identifier_value' => 'Identyfikator', + 'details_identifier_value_description' => 'Wartość identyfikatora, na podstawie której będzie ładowany rekord z bazy danych. Podaj wartość lub nazwę parametru adresu URL.', + 'details_identifier_value_required' => 'Identyfikator jest wymagany', + 'details_key_column' => 'Kolumna klucza', + 'details_key_column_description' => 'Kolumna używana jako identyfikator do pobierania rekordu z bazy danych.', + 'details_key_column_required' => 'Nazwa kolumny klucza jest wymagana', + 'details_display_column' => 'Kolumna do wyświetlenia', + 'details_display_column_description' => 'Kolumna modelu do wyświetlenia na stronie szczegółów. Używana w domyślnym fragmencie komponentu.', + 'details_display_column_required' => 'Wybierz kolumnę do wyświetlenia.', + 'details_not_found_message' => 'Komunikat o braku rekordu', + 'details_not_found_message_description' => 'Komunikat, który zostanie wyświetlony w przypadku nieznalezienia rekordu. Używany w domyślnym fragmencie komponentu.', + 'details_not_found_message_default' => 'Nie znaleziono rekordu', + ], + 'validation' => [ + 'reserved' => ':attribute nie może być zarezerwowanym słowem kluczowym PHP', + ], +]; diff --git a/plugins/rainlab/builder/lang/pt-br.json b/plugins/rainlab/builder/lang/pt-br.json new file mode 100644 index 0000000..cd304ef --- /dev/null +++ b/plugins/rainlab/builder/lang/pt-br.json @@ -0,0 +1,4 @@ +{ + "Builder": "Builder", + "Provides visual tools for building October plugins.": "Provê ferramentas visuais para construir plugins para October CMS." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/pt-br/lang.php b/plugins/rainlab/builder/lang/pt-br/lang.php new file mode 100644 index 0000000..4e248b8 --- /dev/null +++ b/plugins/rainlab/builder/lang/pt-br/lang.php @@ -0,0 +1,669 @@ + [ + 'add' => 'Criar plugin', + 'no_records' => 'Nenhum plugins encontrado', + 'no_name' => 'Sem nome', + 'search' => 'Buscar...', + 'filter_description' => 'Exibe todos os plugins ou apenas os seus plugins.', + 'settings' => 'Configurações', + 'entity_name' => 'Plugin', + 'field_name' => 'Nome', + 'field_author' => 'Autor', + 'field_description' => 'Descrição', + 'field_icon' => 'Ícone do Plugin', + 'field_plugin_namespace' => 'Namespace Plugin', + 'field_author_namespace' => 'Namespace Autor', + 'field_namespace_description' => 'Namespace pode conter apenas letras latinas, numeros e deve começar com uma letra latina. Exemplo de namespace plugin: Blog', + 'field_author_namespace_description' => 'Você não pode alterar o namespace com Builder depois que você criar o plugin. Exemplo de namespace autor: JohnSmith', + 'tab_general' => 'Parâmetros gerais', + 'tab_description' => 'Descrição', + 'field_homepage' => 'URL da homepage do Plugin', + 'no_description' => 'Nenhuma descrição informada para este plugin', + 'error_settings_not_editable' => 'Configurações deste plugin não podem ser editadas com Builder.', + 'update_hint' => 'Você pode editar nome e descrição dos plugins localizados na aba localização.', + 'manage_plugins' => 'Criar e editar plugins', + ], + 'author_name' => [ + 'title' => 'Nome do autor', + 'description' => 'Nome padrão de autor para usar para seus novos plugins. O nome do autor não é fixo - você pode alterá-lo nas configurações do plugin a qualquer hora.', + ], + 'author_namespace' => [ + 'title' => 'Namespace Autor', + 'description' => 'Se você desenvolve para a Marketplace, o namespace deve combinar com o código de autor e não pode ser mudado. Veja a documentação para mais detalhes.', + ], + 'database' => [ + 'menu_label' => 'Banco de Dados', + 'no_records' => 'Nenhuma tabela encontrada', + 'search' => 'Buscar...', + 'confirmation_delete_multiple' => 'Deletar as tabelas selecionadas?', + 'field_name' => 'Nome da tabela', + 'tab_columns' => 'Colunas', + 'column_name_name' => 'Coluna', + 'column_name_required' => 'Por favor, informe um nome da tabela', + 'column_name_type' => 'Tipo', + 'column_type_required' => 'Por favor, selecione o tipo da coluna', + 'column_name_length' => 'Tamanho', + 'column_validation_length' => 'O valor do tamanho deve ser integer ou especificado como precisão e escala (10,2) para colunas decimais. Espaços não são permitidos na coluna tamanho.', + 'column_validation_title' => 'Apenas números, letras latinas minúsculas e underlines são permitidos nos nomes das colunas', + 'column_name_unsigned' => 'Somente positivos', + 'column_name_nullable' => 'Nulo', + 'column_auto_increment' => 'AUTOINCR', + 'column_default' => 'Padrão', + 'column_auto_primary_key' => 'Primary Key', + 'tab_new_table' => 'Nova Tabela', + 'btn_add_column' => 'Acrescentar coluna', + 'btn_delete_column' => 'Deletar coluna', + 'btn_add_timestamps' => 'Acrescentar timestamps', + 'btn_add_soft_deleting' => 'Acrescentar suporte a soft deleting', + 'timestamps_exist' => 'Colunas created_at e deleted_at já existem na tabela.', + 'soft_deleting_exist' => 'Coluna deleted_at já existe na tabela.', + 'confirm_delete' => 'Deletar a tabela?', + 'error_enum_not_supported' => 'A tabela contém coluna(s) com tipo "enum" que não é atualmente suportado pelo Builder.', + 'error_table_name_invalid_prefix' => 'Nome da tabela deve começar com o prefixo do plugin: \':prefix\'.', + 'error_table_name_invalid_characters' => 'Nome de tabela inválido. Nomes de tabelas devem conter apenas letras latinas, números e underlines. Nomes devem começar com uma letra latina e não podem conter espaços.', + 'error_table_duplicate_column' => 'Nome de coluna duplicado: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => 'Uma coluna auto-increment não pode ser parte de uma chave primária composta.', + 'error_table_mutliple_auto_increment' => 'A tabela não pode conter multiplas colunas auto-increment.', + 'error_table_auto_increment_non_integer' => 'Colunas auto-increment devem ser do tipo integer.', + 'error_table_decimal_length' => 'O parâmetro de tamanho para o tipo :type deve ser no formato \'10,2\', sem espaços.', + 'error_table_length' => 'O parâmetro de tamanho para o tipo :type deve ser especificado como integer.', + 'error_unsigned_type_not_int' => 'Erro na coluna \':column\'. A opção Somente Positivos só pode ser aplicada a colunas do tipo integer.', + 'error_integer_default_value' => 'Valor padrão inválido para a coluna integer \':column\'. Os formatos permitidos são \'10\', \'-10\'.', + 'error_decimal_default_value' => 'Valor padrão inválido para a coluna decimal ou double \':column\'. Os formatos permitodos são \'1.00\', \'-1.00\'.', + 'error_boolean_default_value' => 'Valor padrão inválido para a coluna booleana \':column\'. Os valores permitidos são \'0\' e \'1\'.', + 'error_unsigned_negative_value' => 'O valor padrão para a coluna somente positivos \':column\' não pode ser negativo.', + 'error_table_already_exists' => 'A tabela \':name\' já existe no banco de dados.', + 'error_table_name_too_long' => 'O nome da tabela não deve ser maior que 64 caracteres.', + 'error_column_name_too_long' => 'O nome da coluna \':column\' é muito longo. Nomes de colunas não podem ser maiores que 64 caracteres.', + ], + 'model' => [ + 'menu_label' => 'Models', + 'entity_name' => 'Model', + 'no_records' => 'Nenhum model encontrado', + 'search' => 'Buscar...', + 'add' => 'Acrescentar...', + 'forms' => 'Formulários', + 'lists' => 'Listas', + 'field_class_name' => 'Nome da classe', + 'field_database_table' => 'Nome do Banco de dados', + 'field_add_timestamps' => 'Acrescentar suporte a timestamp', + 'field_add_timestamps_description' => 'A tabela do banco de dados precisa conter as colunas created_at e updated_at.', + 'field_add_soft_deleting' => 'Acrescentar suporte a soft deleting', + 'field_add_soft_deleting_description' => 'A tabela do banco de dados precisa conter a coluna deleted_at.', + 'error_class_name_exists' => 'Arquivo Model já existe para o nome da classe especificada: :path', + 'error_timestamp_columns_must_exist' => 'A tabela do banco de dados precisa conter as colunas created_at e updated_at.', + 'error_deleted_at_column_must_exist' => 'A tabela do banco de dados precisa conter a coluna deleted_at.', + 'add_form' => 'Acrescentar formulário', + 'add_list' => 'Acrescentar lista', + ], + 'form' => [ + 'saved' => 'Formulário Salvo', + 'confirm_delete' => 'Deletar o formulário?', + 'tab_new_form' => 'Novo formulário', + 'property_label_title' => 'Título', + 'property_label_required' => 'Por favor, especifique o título do control.', + 'property_span_title' => 'Span', + 'property_comment_title' => 'Comentário', + 'property_comment_above_title' => 'Comentário a cima', + 'property_default_title' => 'Padrão', + 'property_checked_default_title' => 'Marcado por padrão', + 'property_css_class_title' => 'Classe CSS', + 'property_css_class_description' => 'Classe CSS opcional para assinar o campo container.', + 'property_disabled_title' => 'Desabilitado', + 'property_hidden_title' => 'Oculto', + 'property_required_title' => 'Obrigatório', + 'property_field_name_title' => 'Nome do campo', + 'property_placeholder_title' => 'Placeholder', + 'property_default_from_title' => 'Padrão de', + 'property_stretch_title' => 'Estender', + 'property_stretch_description' => 'Especifica se este campo se estende para se ajustar à altura dos pais.', + 'property_context_title' => 'Contexto', + 'property_context_description' => 'Especifica que conceito de formulário deve ser usado quando exibir o campo.', + 'property_context_create' => 'Criar', + 'property_context_update' => 'Atualizar', + 'property_context_preview' => 'Preview', + 'property_dependson_title' => 'Depende de', + 'property_trigger_action' => 'Ação', + 'property_trigger_show' => 'Exibir', + 'property_trigger_hide' => 'Ocultar', + 'property_trigger_enable' => 'Habilitar', + 'property_trigger_disable' => 'Desabilitar', + 'property_trigger_empty' => 'Vazio', + 'property_trigger_field' => 'Campo', + 'property_trigger_field_description' => 'Define o outro nome de campo que dispara a ação.', + 'property_trigger_condition' => 'Condição', + 'property_trigger_condition_description' => 'Determina a condição que especifica o campo deve satisfazer a condição a ser considerada "true". Valores suportados: marcado, não marcado, valor[algumvalor].', + 'property_trigger_condition_checked' => 'Marcado', + 'property_trigger_condition_unchecked' => 'Não marcado', + 'property_trigger_condition_somevalue' => 'valor[digite-o-valor-aqui]', + 'property_preset_title' => 'Pré-configurado', + 'property_preset_description' => 'Permite o valor do campo a ser inicalmente configurado pelo valor de outro campo, convertido usando a entrada de conversor pré-configurada.', + 'property_preset_field' => 'Campo', + 'property_preset_field_description' => 'Define o nome do campo pelo valor de fonte de outro campo.', + 'property_preset_type' => 'Tipo', + 'property_preset_type_description' => 'Especifica o tipo de conversão', + 'property_attributes_title' => 'Atributos', + 'property_attributes_description' => 'Atributos HTML customizados para acrescentar ao elemento do campo do formulário.', + 'property_container_attributes_title' => 'Atributos do container', + 'property_container_attributes_description' => 'Atributos HTML customizados para adicionar ao elemento container do campo do formulário.', + 'property_group_advanced' => 'Avançado', + 'property_dependson_description' => 'Uma lista de outros nomes de campos dos quais este campo depende, quando os outros campos são modificados, este campo será atualizado. Um campo por linha.', + 'property_trigger_title' => 'Gatilho (Trigger)', + 'property_trigger_description' => 'Permite mudar os atributos do elemento assim como visibilidade ou valor, baseado no estado de outros elementos.', + 'property_default_from_description' => 'Pega o valor padrão do valor de outro campo.', + 'property_field_name_required' => 'O nome do campo é obrigatório', + 'property_field_name_regex' => 'O nome do campo apenas pode conter letras latinas, números, underlines, traços e colchetes.', + 'property_attributes_size' => 'Tamanho', + 'property_attributes_size_tiny' => 'Minúsculo', + 'property_attributes_size_small' => 'Pequeno', + 'property_attributes_size_large' => 'Largo', + 'property_attributes_size_huge' => 'Grande', + 'property_attributes_size_giant' => 'Gigante', + 'property_comment_position' => 'Posição do Comentário', + 'property_comment_position_above' => 'A cima', + 'property_comment_position_below' => 'A baixo', + 'property_hint_path' => 'Caminho sugerido do partial', + 'property_hint_path_description' => 'Caminho para um arquivo partial que contenha o texto sugerido. Use o simbolo $ para referenciar à raiz do diretório, por exemplo: $/acme/blog/partials/_sugestao.htm', + 'property_hint_path_required' => 'Por favor, digite a sugestão de caminho para o partial', + 'property_partial_path' => 'Caminho para o partial', + 'property_partial_path_description' => 'Caminho para um arquivo partial. Use o simbolo $ para referenciar ao diretório raiz dos plugins, por exemplo: $/acme/blog/partials/_partial.htm', + 'property_partial_path_required' => 'Por favor, digite o caminho do partial', + 'property_code_language' => 'Linguagem', + 'property_code_theme' => 'Tema', + 'property_theme_use_default' => 'Use o tema padrão', + 'property_group_code_editor' => 'Editor de código', + 'property_gutter' => 'Gutter', + 'property_gutter_show' => 'Visível', + 'property_gutter_hide' => 'Oculto', + 'property_wordwrap' => 'Abreviar palavra', + 'property_wordwrap_wrap' => 'Abreviar', + 'property_wordwrap_nowrap' => 'Não abreviar', + 'property_fontsize' => 'Tamanho da fonte', + 'property_codefolding' => 'Duplicar código', + 'property_codefolding_manual' => 'Manual', + 'property_codefolding_markbegin' => 'Marcar início', + 'property_codefolding_markbeginend' => 'Marcar início e fim', + 'property_autoclosing' => 'Auto encerrar', + 'property_enabled' => 'Habilitado', + 'property_disabled' => 'Desabilitado', + 'property_soft_tabs' => 'Tabelas suaves', + 'property_tab_size' => 'Tamanho da tabela', + 'property_readonly' => 'Somente leitura', + 'property_use_default' => 'Usar configurações padrão', + 'property_options' => 'Opções', + 'property_prompt' => 'Pronto', + 'property_prompt_description' => 'Texto a exibir para o botão criar.', + 'property_prompt_default' => 'Acrescentar novo item', + 'property_available_colors' => 'Cores disponíveis', + 'property_available_colors_description' => 'Lista de cores disponíveis no frmato hexadecimal (#FF0000). Deixe em branco para a configuração padrão de cores. Digite um valor por linha.', + 'property_datepicker_mode' => 'Modo', + 'property_datepicker_mode_date' => 'Data', + 'property_datepicker_mode_datetime' => 'Data e hora', + 'property_datepicker_mode_time' => 'Hora', + 'property_datepicker_min_date' => 'Data mínima', + 'property_datepicker_max_date' => 'Data máxima', + 'property_datepicker_year_range' => 'Alcançe de Anos', + 'property_datepicker_year_range_description' => 'Número de anos que cada lado (ex. 10) ou array de alcançe superior/inferior (ex. [1900,2015]). Deixe em branco para o valor padrão (10).', + 'property_datepicker_year_range_invalid_format' => 'Formato de alcançe de anos inválido. Use Números (ex. "10") ou array de alcançe superior/inferior (ex. "[1900,2015]")', + 'property_fileupload_mode' => 'Modo', + 'property_fileupload_mode_file' => 'Arquivo', + 'property_fileupload_mode_image' => 'Imagem', + 'property_group_fileupload' => 'Upload de arquivo', + 'property_fileupload_image_width' => 'Largura da imagem', + 'property_fileupload_image_width_description' => 'Parâmetro opcional - imagens serão redimensionadas para esta largura. Aplica-se apenas ao modo Imagem.', + 'property_fileupload_invalid_dimension' => 'Valor de dimensão inválido - por favor, digite um número.', + 'property_fileupload_image_height' => 'Altura da imagem', + 'property_fileupload_image_height_description' => 'Parâmetro opcional - imagens serão redimensionadas para esta altura. Aplica-se apenas ao modo Imagem.', + 'property_fileupload_file_types' => 'Tipos de arquivos', + 'property_fileupload_file_types_description' => 'Lista opcional separada por vírgula das extensões de arquivos que são aceitas pelo uploaded. Ex: zip, txt', + 'property_fileupload_mime_types' => 'Tipos MIME', + 'property_fileupload_mime_types_description' => 'Lista opcional separada por vírgula dos tipos MIME que são aceitas pelo uploader, assim como extensões ou nomes totalmente qualificados. Ex: bin, txt', + 'property_fileupload_use_caption' => 'Usar legenda', + 'property_fileupload_use_caption_description' => 'Permite um título e descrição a serem configurados para o arquivo.', + 'property_fileupload_thumb_options' => 'Opções de miniatua', + 'property_fileupload_thumb_options_description' => 'Gerencia opções para as miniaturas geradas automaticamente. Aplica-se apenas ao modo imagem.', + 'property_fileupload_thumb_mode' => 'Modo', + 'property_fileupload_thumb_auto' => 'Automático', + 'property_fileupload_thumb_exact' => 'Exato', + 'property_fileupload_thumb_portrait' => 'Retrato', + 'property_fileupload_thumb_landscape' => 'Paisagem', + 'property_fileupload_thumb_crop' => 'Cropar', + 'property_fileupload_thumb_extension' => 'Extensão de arquivo', + 'property_name_from' => 'Nome da coluna', + 'property_name_from_description' => 'Relação de nome de coluna a usar para exibir um nome.', + 'property_relation_select' => 'Selecionar', + 'property_relation_select_description' => 'CONCATENA multiplas colunas para exibir um nome', + 'property_description_from' => 'Coluna Descrição', + 'property_description_from_description' => 'Relação de nome de coluna a usar para exibir uma descrição.', + 'property_recordfinder_prompt' => 'Pronto', + 'property_recordfinder_prompt_description' => 'Texto para exibir quando não há gravação selecionada. O caractere %s representa o ícone buscar. Deixe em branco para o padrão pronto.', + 'property_recordfinder_list' => 'Configuração de lista', + 'property_recordfinder_list_description' => 'Uma referencia a um arquivo de definições de listas de colunas. Use o simbolo $ para referenciar a raiz de diretório do plugin, por exemplo: $/acme/blog/lists/_lista.yaml', + 'property_recordfinder_list_required' => 'Por favor, forneça um caminho para a lista de arquivos YAML', + 'property_group_recordfinder' => 'Gravar buscador', + 'property_mediafinder_mode' => 'Modo', + 'property_mediafinder_mode_file' => 'Arquivo', + 'property_mediafinder_mode_image' => 'Imagem', + 'property_mediafinder_image_width_description' => 'Se estiver usando o tipo imagem, o preview de imagem será exibido nesta largura, opcional.', + 'property_mediafinder_image_height_description' => 'Se estiver usando o modo imagem, o preview de imagem será exibido nesta altura, opcional.', + 'property_group_relation' => 'Relação', + 'property_relation_prompt' => 'Pronto', + 'property_relation_prompt_description' => 'Texto a exibir quando não há seleções disponíveis.', + 'property_empty_option' => 'Opção Vazia', + 'property_empty_option_description' => 'A opção Vazia corresponde a seleção vazia, mas ao contrário do marcador, ele pode ser selecionado novamente.', + 'property_max_items' => 'Máximo de itens', + 'property_max_items_description' => 'Número máximo de itens a permitir com o repetidor.', + 'control_group_standard' => 'Comum', + 'control_group_widgets' => 'Widgets', + 'click_to_add_control' => 'Acrescentar control', + 'loading' => 'Carregando...', + 'control_text' => 'Texto', + 'control_text_description' => 'Caixa de texto único', + 'control_password' => 'Senha', + 'control_password_description' => 'Campo de texto para senha de linha única', + 'control_checkbox' => 'Checkbox', + 'control_checkbox_description' => 'Único checkbox', + 'control_switch' => 'Switch', + 'control_switch_description' => 'Switchbox único, uma alternativa ao checkbox', + 'control_textarea' => 'Text area', + 'control_textarea_description' => 'Multiplas caixas de texto com altura controlável', + 'control_dropdown' => 'Dropdown', + 'control_dropdown_description' => 'Lista dropdown com opções estáticas ou dinâmicas', + 'control_balloon-selector' => 'Balloon Selector', + 'control_balloon-selector_description' => 'Lista onde somente um item pode ser selecionado por vez, com opções estáticas ou dinâmicas', + 'control_unknown' => 'Tipo de control desconhecido: :type', + 'control_repeater' => 'Repetidor', + 'control_repeater_description' => 'Exibe um conjunto de repetições de form controls', + 'control_number' => 'Número', + 'control_number_description' => 'Text box de linha única que aceita apenas números', + 'control_hint' => 'Aviso', + 'control_hint_description' => 'Exibe o conteúdo de uma partial em uma caixa que pode ser ocultada pelo usuário', + 'control_partial' => 'Partial', + 'control_partial_description' => 'Exibe o conteúdo de um partial', + 'control_section' => 'Seção', + 'control_section_description' => 'Exibe uma seção de formulário com cabeçalho e sub-cabeçalho', + 'control_radio' => 'Lista Radio', + 'control_radio_description' => 'Uma lista de opções radio, onde apenas um item pode ser selecionado por vez', + 'control_radio_option_1' => 'Opção 1', + 'control_radio_option_2' => 'Opção 2', + 'control_checkboxlist' => 'Lista Checkbox', + 'control_checkboxlist_description' => 'Uma lista de checkboxes, onde múltiplos itens podem ser selecionados', + 'control_codeeditor' => 'Editor de códigos', + 'control_codeeditor_description' => 'Editor modo texto para código formatado ou de marcação', + 'control_colorpicker' => 'Seletor de cor', + 'control_colorpicker_description' => 'Um campo para selecionar um valor haxadecimal de cor', + 'control_datepicker' => 'Seletor Data', + 'control_datepicker_description' => 'Campo texto usado para selecionar data e hora', + 'control_richeditor' => 'Editor Rico', + 'control_richeditor_description' => 'Editor visual para formatação rica de texto, também conhecido como um editor WYSIWYG', + 'control_markdown' => 'Editor Markdown', + 'control_markdown_description' => 'Editor básico para formatação de texto Markdown', + 'control_fileupload' => 'Upload de arquivo', + 'control_fileupload_description' => 'Uploader de arquivos para imagens ou arquivos comuns', + 'control_recordfinder' => 'Buscador de registro', + 'control_recordfinder_description' => 'Campo com detalhes de um registro relacionado com o recurso de pesquisa de registro', + 'control_mediafinder' => 'Buscador de mídia', + 'control_mediafinder_description' => 'Campo para selecionar um item de uma biblioteca do gerenciador de mídia', + 'control_relation' => 'Relação', + 'control_relation_description' => 'Exibe tanto um dropdown ou uma lista checkbox para selecionar um registro relacionado', + 'error_file_name_required' => 'Por favor, digite o nome de arquivo do formulário.', + 'error_file_name_invalid' => 'O nome de arquivo pode conter apenas letras latinas, números, underlines, pontos e barras.', + 'span_left' => 'Esquerda', + 'span_right' => 'Direita', + 'span_full' => 'Completo', + 'span_auto' => 'Automático', + 'empty_tab' => 'Aba vazia', + 'confirm_close_tab' => 'A aba contém controles que serão deletados. Continuar?', + 'tab' => 'Aba formulário', + 'tab_title' => 'Título', + 'controls' => 'Controles', + 'property_tab_title_required' => 'A aba título é obrigatória.', + 'tabs_primary' => 'Abas primárias', + 'tabs_secondary' => 'Abas secundárias', + 'tab_stretch' => 'Extender', + 'tab_stretch_description' => 'Especifica se este contêiner de guias se estende para se ajustar à altura pai.', + 'tab_css_class' => 'Classe CSS', + 'tab_css_class_description' => 'Assinala uma classe CSS ao contêiner de abas.', + 'tab_name_template' => 'Aba %s', + 'tab_already_exists' => 'Aba com o título especificado já existe.', + ], + 'list' => [ + 'tab_new_list' => 'Nova lista', + 'saved' => 'Lista salva', + 'confirm_delete' => 'Deletar a lista?', + 'tab_columns' => 'Colunas', + 'btn_add_column' => 'Acrescentar coluna', + 'btn_delete_column' => 'Deletar coluna', + 'column_dbfield_label' => 'Campo', + 'column_dbfield_required' => 'Por favor, digite um campo model', + 'column_name_label' => 'Rótulo', + 'column_label_required' => 'Por favor, informe o rótulo da coluna', + 'column_type_label' => 'Tipo', + 'column_type_required' => 'Por favor, informe o tipo da coluna', + 'column_type_text' => 'Texto', + 'column_type_number' => 'Número', + 'column_type_switch' => 'Switch', + 'column_type_datetime' => 'Data e Hora', + 'column_type_date' => 'Data', + 'column_type_time' => 'Hora', + 'column_type_timesince' => 'Tempo Passado', + 'column_type_timetense' => 'Tempo por Extenso', + 'column_type_select' => 'Seleção', + 'column_type_partial' => 'Partial', + 'column_label_default' => 'Padrão', + 'column_label_searchable' => 'Buscar', + 'column_label_sortable' => 'Classificável', + 'column_label_invisible' => 'Invisível', + 'column_label_select' => 'Seleção', + 'column_label_relation' => 'Relação', + 'column_label_css_class' => 'Classe CSS', + 'column_label_width' => 'Largura', + 'column_label_path' => 'Caminho', + 'column_label_format' => 'Formato', + 'column_label_value_from' => 'Valor de', + 'error_duplicate_column' => 'Campo nome de coluna duplicado: \':column\'.', + 'btn_add_database_columns' => 'Acrescentar colunas ao banco de dados', + 'all_database_columns_exist' => 'Todas as colunas do banco de dados já estão definidas na lista', + ], + 'controller' => [ + 'menu_label' => 'Controllers', + 'no_records' => 'Nenhum controller de plugin encontrado', + 'controller' => 'Controller', + 'behaviors' => 'Comportamentos', + 'new_controller' => 'Novo controller', + 'error_controller_has_no_behaviors' => 'O controller não possui nenhum comportamento configurável.', + 'error_invalid_yaml_configuration' => 'Erro ao carregar arquivo de configuração de comportamento: :file', + 'behavior_form_controller' => 'Comportamento de controller de formulários', + 'behavior_form_controller_description' => 'Acrescentar funcionalidade de formulários a uma página back-end. O comportamento provê três páginas chamadas Create, Update e Preview.', + 'property_behavior_form_placeholder' => '--selecionar formulário--', + 'property_behavior_form_name' => 'Nome', + 'property_behavior_form_name_description' => 'O nome do objeto gerenciado por este formulário', + 'property_behavior_form_name_required' => 'Por favor, digite o nome do formulário', + 'property_behavior_form_file' => 'Configuração de formulário', + 'property_behavior_form_file_description' => 'Refere-se a um arquivo de definição de campos de formulário', + 'property_behavior_form_file_required' => 'Por favor, digite um caminho para o arquivo de configuração do formulário', + 'property_behavior_form_model_class' => 'Classe Model', + 'property_behavior_form_model_class_description' => 'Um nome de classe model, os dados do formulário são carregados e salvos neste model.', + 'property_behavior_form_model_class_required' => 'Por favor, selecione uma classe de model', + 'property_behavior_form_default_redirect' => 'Redirecionamento padrão', + 'property_behavior_form_default_redirect_description' => 'Uma página para redirecionar por padrão quando o formulário for salvo ou cancelado.', + 'property_behavior_form_create' => 'Criar registrar página', + 'property_behavior_form_redirect' => 'Redirecionar', + 'property_behavior_form_redirect_description' => 'Uma página para redirecionar quando um registro é criado.', + 'property_behavior_form_redirect_close' => 'Encerrar redirecionamento', + 'property_behavior_form_redirect_close_description' => 'Uma página para redirecionar quando um registro é criado e a variável encerrar é enviada com o requerimento.', + 'property_behavior_form_flash_save' => 'Salvar menságem rápida', + 'property_behavior_form_flash_save_description' => 'Menságem rápida para exibir quando o registro é salvo.', + 'property_behavior_form_page_title' => 'Título da página', + 'property_behavior_form_update' => 'Atualizar registro da página', + 'property_behavior_form_update_redirect' => 'Redirecionar', + 'property_behavior_form_create_redirect_description' => 'Uma página para redirecionar quando um registro é salvo.', + 'property_behavior_form_flash_delete' => 'Deletar mensagem rápida', + 'property_behavior_form_flash_delete_description' => 'Mensagem rápida para exibir quando o registro é deletado.', + 'property_behavior_form_preview' => 'Preview registro da página', + 'behavior_list_controller' => 'Controller de Lista Comportamento', + 'behavior_list_controller_description' => 'Provê a lista classificável e buscável com links opcionais em seus registros. O comportamento cria automaticamente a ação controller "index".', + 'property_behavior_list_title' => 'Título da lista', + 'property_behavior_list_title_required' => 'Por favor, digite o título da lista', + 'property_behavior_list_placeholder' => '--Selecione a lista--', + 'property_behavior_list_model_class' => 'Classe Model', + 'property_behavior_list_model_class_description' => 'Um nome de classe model, os dados da lista são carregados deste model.', + 'property_behavior_form_model_class_placeholder' => '--selecione o model--', + 'property_behavior_list_model_class_required' => 'Por favor, selecione uma classe model', + 'property_behavior_list_model_placeholder' => '--selecione o model--', + 'property_behavior_list_file' => 'Arquivo de configuração de lista', + 'property_behavior_list_file_description' => 'Refere-se a um arquivo de definição de lista', + 'property_behavior_list_file_required' => 'Por favor, digite um caminho para o arquivo de configuração de lista', + 'property_behavior_list_record_url' => 'Gravar URL', + 'property_behavior_list_record_url_description' => 'Link cada lista de registro para outra página. Ex: users/update:id. A :id parte é substituída com o identificador de registro.', + 'property_behavior_list_no_records_message' => 'Nenhum registro de mensagem', + 'property_behavior_list_no_records_message_description' => 'Uma mensagem a exibir quando nenhum registro for encontrado', + 'property_behavior_list_recs_per_page' => 'Registros por página', + 'property_behavior_list_recs_per_page_description' => 'Registros a exibir por página, use 0 para nenhuma página. O padrão é: 0', + 'property_behavior_list_recs_per_page_regex' => 'Registros por página devem ser um valor integer', + 'property_behavior_list_show_setup' => 'Exibe botão configurações', + 'property_behavior_list_show_sorting' => 'Exibir classificação', + 'property_behavior_list_default_sort' => 'Classificação padrão', + 'property_behavior_form_ds_column' => 'Coluna', + 'property_behavior_form_ds_direction' => 'Direção', + 'property_behavior_form_ds_asc' => 'Ascendente', + 'property_behavior_form_ds_desc' => 'Descendente', + 'property_behavior_list_show_checkboxes' => 'Exibir checkboxes', + 'property_behavior_list_onclick' => 'Manusear On click', + 'property_behavior_list_onclick_description' => 'Customiza código JavaScript para executar quando clicar em um registro.', + 'property_behavior_list_show_tree' => 'Exibir árvore', + 'property_behavior_list_show_tree_description' => 'Exibe uma árvore de hierarquia para registros pai/filho.', + 'property_behavior_list_tree_expanded' => 'Expandir árvore', + 'property_behavior_list_tree_expanded_description' => 'Determina se os nós da árvore devem ser expandidos por padrão.', + 'property_behavior_list_toolbar' => 'Barra de ferramentas', + 'property_behavior_list_toolbar_buttons' => 'Partial Botões', + 'property_behavior_list_toolbar_buttons_description' => 'Refere-se a um arquivo controller partial com os botões da barra de ferramentas. Ex: lista_BarradeFerramentas', + 'property_behavior_list_search' => 'Buscar', + 'property_behavior_list_search_prompt' => 'Buscar pronto', + 'property_behavior_list_filter' => 'Configurar filtros', + 'behavior_reorder_controller' => 'Reordenar comportamento do controller', + 'behavior_reorder_controller_description' => 'Fornece recursos para classificar e reordenar seus registros. O comportamento cria automaticamente a ação "reorder" do controller.', + 'property_behavior_reorder_title' => 'Título do reordenador', + 'property_behavior_reorder_title_required' => 'Por favor, digite o título do reordenador', + 'property_behavior_reorder_name_from' => 'Nome do atributo', + 'property_behavior_reorder_name_from_description' => 'Atributo do model que pode ser usado como um rótulo para cada registro.', + 'property_behavior_reorder_name_from_required' => 'Por favor, digite o nome do atributo', + 'property_behavior_reorder_model_class' => 'Classe Model', + 'property_behavior_reorder_model_class_description' => 'Um nome de classe model, o dado registrado é carregado deste model.', + 'property_behavior_reorder_model_class_placeholder' => '--selecionar model--', + 'property_behavior_reorder_model_class_required' => 'Por favor, selecione uma classe model', + 'property_behavior_reorder_model_placeholder' => '--selecione o model--', + 'property_behavior_reorder_toolbar' => 'Bara de ferramentas', + 'property_behavior_reorder_toolbar_buttons' => 'Botões da Partial', + 'property_behavior_reorder_toolbar_buttons_description' => 'Refere-se ao arquivo partial controller com os botões da barra de ferramentas. Ex: reordenar_BarradeFerramentas', + 'error_controller_not_found' => 'Arquivo original do controller não foi encontrado.', + 'error_invalid_config_file_name' => 'O nome do arquivo (:file) de configuração do comportamento :class contém caracteres inválidos e não pode ser carregado.', + 'error_file_not_yaml' => 'O nome do arquivo (:file) de configuração do comportamento :class não é um arquivo YAML. Somente arquivos de configuração YAML são suportados.', + 'saved' => 'Controller salvo', + 'controller_name' => 'Nome do Controller', + 'controller_name_description' => 'O nome do controller define o nome da classe e URL das páginas back-end do controller. Convenções de nomenclatura de variáveis PHP padrão se aplicam. O primeiro símbolo deve ser uma letra latina maiúscula. Exemplos: Categorias, Postagens, Produtos.', + 'base_model_class' => 'Classe model base', + 'base_model_class_description' => 'Selecione uma classe model para usar como um model base no comportamento que necessita ou suporta models. Você pode configurar o comportamento depois.', + 'base_model_class_placeholder' => '--Selecione o model--', + 'controller_behaviors' => 'Comportamentos', + 'controller_behaviors_description' => 'Selecione os comportamentos que o controller deve implementar. O Builder criará arquivos de view necessários para os comportamentos automaticamente.', + 'controller_permissions' => 'Permissões', + 'controller_permissions_description' => 'Selecione permissões de usuário que podem acessar as views dos controllers. As permissões podem ser definidas na guia Permissões do Builder. Você pode alterar essa opção no script PHP do controller mais tarde.', + 'controller_permissions_no_permissions' => 'O plugin não definiu nenhuma permissão.', + 'menu_item' => 'Item de menu ativo', + 'menu_item_description' => 'Selecione um item de menu para tornar ativo para as páginas do controller. Você pode alterar essa opção no script PHP do controlador mais tarde.', + 'menu_item_placeholder' => '--selecione o item de menu--', + 'error_unknown_behavior' => 'A classe de comportamento :class não está registrada na biblioteca de comportamento.', + 'error_behavior_view_conflict' => 'Os comportamentos selecionados fornecem visualizações conflitantes (:view) e não podem ser usadas junto a um controller.', + 'error_behavior_config_conflict' => 'Os comportamentos selecionados fornecem arquivos de configuração conflitantes (:file) e não podem ser usadas junto a um controller.', + 'error_behavior_view_file_not_found' => 'A view do template :view do comportamento :class não pode ser localizado.', + 'error_behavior_config_file_not_found' => 'O template de configuração :file do comportamento :class não pode ser localizado.', + 'error_controller_exists' => 'O arquivo do controlador :file já existe.', + 'error_controller_name_invalid' => 'Formato de nome de controller inválido. O nome deve conter apenas letras latinas e números. O primeiro simbolo deve ser uma letra latina maiúscula.', + 'error_behavior_view_file_exists' => 'O arquivo de configuração do controller :view já existe.', + 'error_behavior_config_file_exists' => 'O arquivo de configuração do comportamento :file já existe.', + 'error_save_file' => 'Erro ao salvar o arquivo de controller :file', + 'error_behavior_requires_base_model' => 'O comportamento :behavior necessita que seja selecionado uma classe model base.', + 'error_model_doesnt_have_lists' => 'O model selecionado não possui nenhuma lista. Por favor, crie primeiro uma lista.', + 'error_model_doesnt_have_forms' => 'O model selecionado não possui nenhum formulário. Por favor, crie primeiro um formulário.', + ], + 'version' => [ + 'menu_label' => 'Versões', + 'no_records' => 'Nenhuma versão de plugin encontrada', + 'search' => 'Buscar...', + 'tab' => 'Versões', + 'saved' => 'Versão salva', + 'confirm_delete' => 'Deletar a versão?', + 'tab_new_version' => 'Nova versão', + 'migration' => 'Migração', + 'seeder' => 'Semeador', + 'custom' => 'Aumente o número da versão', + 'apply_version' => 'Aplicar versão', + 'applying' => 'Aplicando...', + 'rollback_version' => 'Reverter versão', + 'rolling_back' => 'Revertendo...', + 'applied' => 'Versão aplicada', + 'rolled_back' => 'Versão revertida', + 'hint_save_unapplied' => 'Você salvou uma versão não aplicada. As versões não aplicadas podem ser aplicadas automaticamente quando você ou outro usuário faz o login no back-end ou quando uma tabela de banco de dados é salva na seção Banco de Dados do Builder.', + 'hint_rollback' => 'Reverter uma versão também reverterá todas as versões mais recentes que esta. Observe que versões não aplicadas podem ser aplicadas automaticamente pelo sistema quando você ou outro usuário faz o login no back-end ou quando uma tabela do banco de dados é salva na seção Banco de Dados do Builder.', + 'hint_apply' => 'A aplicação de uma versão também aplicará todas as versões antigas não aplicadas do plug-in.', + 'dont_show_again' => 'Não mostrar novamente', + 'save_unapplied_version' => 'Salvar versão não aplicada', + ], + 'menu' => [ + 'menu_label' => 'Menu Backend', + 'tab' => 'Menus', + 'items' => 'Itens de Menu', + 'saved' => 'Menus salvos', + 'add_main_menu_item' => 'Acrescentar item de menu principal', + 'new_menu_item' => 'Item de menu', + 'add_side_menu_item' => 'Acrescentar sub-item', + 'side_menu_item' => 'Item de menu lateral', + 'property_label' => 'Rótulo', + 'property_label_required' => 'Por favor, digite os rótulos dos itens de menu.', + 'property_url_required' => 'Por favor, digite a URL do item de menu', + 'property_url' => 'URL', + 'property_icon' => 'Ícone', + 'property_icon_required' => 'Por favor, selecione um ícone', + 'property_permissions' => 'Permissões', + 'property_order' => 'Ordenar', + 'property_order_invalid' => 'Por favor, insira a ordem do item de menu como valor integer.', + 'property_order_description' => 'A ordem do item de menu gerencia sua posição no menu. Se a ordem não for fornecida, o item será colocado no final do menu. Os valores de ordem padrão têm o incremento de 100.', + 'property_attributes' => 'Atributos HTML', + 'property_code' => 'Código', + 'property_code_invalid' => 'O código deve conter apenas letra latina e números', + 'property_code_required' => 'Por favor, insira o código do item de menu.', + 'error_duplicate_main_menu_code' => 'Código de item de menu \':code\' duplicado.', + 'error_duplicate_side_menu_code' => 'Código de item de menu lateral \':code\' duplicado.', + ], + 'localization' => [ + 'menu_label' => 'Idiomas', + 'language' => 'Idioma', + 'strings' => 'Strings', + 'confirm_delete' => 'Deletar o idioma?', + 'tab_new_language' => 'Novo idioma', + 'no_records' => 'Nenhum idioma encontrado', + 'saved' => 'Arquivo de idioma salvo', + 'error_cant_load_file' => 'Não é possível carregar o arquivo de idioma solicitado - arquivo não encontrado.', + 'error_bad_localization_file_contents' => 'Não é possível carregar o arquivo de idioma solicitado. Os arquivos de idiomas podem conter apenas definições e strings de array.', + 'error_file_not_array' => 'Não é possível carregar o arquivo de idioma solicitado. Arquivos de idioma devem retornar um array.', + 'save_error' => 'Erro ao salvar o arquivo \':name\'. Por favor, verifique as permissões de escrita.', + 'error_delete_file' => 'Erro ao deletar arquivo de idioma.', + 'add_missing_strings' => 'Acrescentar strings faltantes', + 'copy' => 'Copiar', + 'add_missing_strings_label' => 'Selecione o idioma para copiar as strings faltantes', + 'no_languages_to_copy_from' => 'Não há outro idioma para copiar strings.', + 'new_string_warning' => 'Nova string ou seção', + 'structure_mismatch' => 'A estrutura do arquivo fonte do idioma não combina com a estrutura do arquivo que está sendo editado. Algumas strings individuais do arquivo editado correspondente a seções no arquivo fonte (ou vice versa) e não pode ser mesclado automaticamente.', + 'create_string' => 'Criar nova string', + 'string_key_label' => 'Chave de String', + 'string_key_comment' => 'Insira a chave de string usando o ponto como um separador de seção. For example: plugin.buscar. A string será criada no arquivo de localização do idioma padrão do plugin.', + 'string_value' => 'Valor da string', + 'string_key_is_empty' => 'Chave de string não pode ser vazio', + 'string_key_is_a_string' => ':key é uma string e não pode conter outras strings.', + 'string_value_is_empty' => 'Valor da string não pode ser vazio', + 'string_key_exists' => 'A chave de string já existe', + ], + 'permission' => [ + 'menu_label' => 'Permissões', + 'tab' => 'Permissões', + 'form_tab_permissions' => 'Permissões', + 'btn_add_permission' => 'Acrescentar permissão', + 'btn_delete_permission' => 'Deletar permissão', + 'column_permission_label' => 'Código de permissão', + 'column_permission_required' => 'Por favor, digite o código de permissão', + 'column_tab_label' => 'Título da aba', + 'column_tab_required' => 'Por favor, digite o título da aba permissão', + 'column_label_label' => 'Rótulo', + 'column_label_required' => 'Por favor, digite o rótulo da permissão', + 'saved' => 'Permissões salvas', + 'error_duplicate_code' => 'Código de permissão \':code\' duplicado.', + ], + 'yaml' => [ + 'save_error' => 'Erro ao salvar o arquivo \':name\'. Por favor, verifique permissões de escrita.', + ], + 'common' => [ + 'error_file_exists' => 'Arquivo \':path\' já existe.', + 'field_icon_description' => 'October usa ícones Font Autumn: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => 'O diretório de destino \':path\' não existe.', + 'error_make_dir' => 'Erro ao criar diretório: \':name\'.', + 'error_dir_exists' => 'Diretório \':path\' já existe.', + 'template_not_found' => 'Arquivo de template \':name\' não encontrado.', + 'error_generating_file' => 'Erro ao gerar arquivo: \':path\'.', + 'error_loading_template' => 'Erro ao carregar arquivo de template: \':name\'.', + 'select_plugin_first' => 'Por favor, selecione um plugin primeiro. Para ver a lista de plugins, clique no ícone > na barra lateral esquerda.', + 'plugin_not_selected' => 'Plugin não selecionado', + 'add' => 'Acrescentar', + ], + 'migration' => [ + 'entity_name' => 'Migration', + 'error_version_invalid' => 'A versão deve ser especificada no formato 1.0.1', + 'field_version' => 'Versão', + 'field_description' => 'Descrição', + 'field_code' => 'Código', + 'save_and_apply' => 'Salvar e aplicar', + 'error_version_exists' => 'A versão da migration já existe.', + 'error_script_filename_invalid' => 'O nome do arquivo de script do migration pode conter apenas letras latinas, números e underlines. O nome deve começar com uma letra latina e não pode conter espaços.', + 'error_cannot_change_version_number' => 'Não é possível alterar o número da versão para uma versão aplicada.', + 'error_file_must_define_class' => 'O código de migração deve definir uma migration ou classe de semeador. Deixe o campo de código em branco se você quiser apenas atualizar o número da versão.', + 'error_file_must_define_namespace' => 'O código do migration deve definir um namespace. Deixe o campo de código em branco se você quiser apenas atualizar o número da versão.', + 'no_changes_to_save' => 'Não há mudanças para salvar.', + 'error_namespace_mismatch' => 'O código do migration deve usar o namespace :namespace do plugin', + 'error_migration_file_exists' => 'O arquivo migration :file já existe. Por favor, use outro nome de classe.', + 'error_cant_delete_applied' => 'Esta versão já foi aplicada e não pode ser excluída. Por favor, reverter a versão primeiro.', + ], + 'components' => [ + 'list_title' => 'Lista de registros', + 'list_description' => 'Exibe uma lista de registros para um model selecionado', + 'list_page_number' => 'Número da página', + 'list_page_number_description' => 'Este valor é usado para determinar em qual página o usuário está.', + 'list_records_per_page' => 'Registros por página', + 'list_records_per_page_description' => 'Número de registros para exibir em uma única página. Deixe em branco para desabilitar a ação.', + 'list_records_per_page_validation' => 'Formato inválido dos registros por valor de página. O valor deve ser um número.', + 'list_no_records' => 'Nenhuma mensagem de registro', + 'list_no_records_description' => 'Mensagem a exibir na lista, em caso de não haver registros. Usado na partial padrão do componente.', + 'list_no_records_default' => 'Nenhum registro encontrado', + 'list_sort_column' => 'Classificar por coluna', + 'list_sort_column_description' => 'A coluna modela como os registros devem ser classificados', + 'list_sort_direction' => 'Direção', + 'list_display_column' => 'Exibir coluna', + 'list_display_column_description' => 'Coluna para exibir na lista. Usado no partial padrão do componente.', + 'list_display_column_required' => 'Por favor, selecione uma coluna exibida.', + 'list_details_page' => 'Página detalhes', + 'list_details_page_description' => 'Página para exibir detalhes dos registros.', + 'list_details_page_no' => '--nenhuma página de detalhes--', + 'list_sorting' => 'Classificação', + 'list_pagination' => 'Paginação', + 'list_order_direction_asc' => 'Ascendente', + 'list_order_direction_desc' => 'Descendente', + 'list_model' => 'Classe Model', + 'list_scope' => 'Escopo', + 'list_scope_description' => 'Escopo do modelo opcional para buscar os registros', + 'list_scope_default' => '--selecione um escopo, opcional--', + 'list_scope_value' => 'Valor do escopo', + 'list_scope_value_description' => 'Valor opcional para passar para o escopo do modelo', + 'list_details_page_link' => 'Link para a página de detalhes', + 'list_details_key_column' => 'Coluna chave de detalhes', + 'list_details_key_column_description' => 'Coluna Modelo para usar como um identificador de registro nos links da página de detalhes.', + 'list_details_url_parameter' => 'Nome do parâmetro de URL ', + 'list_details_url_parameter_description' => 'Nome do parâmetro de URL da página de detalhes que leva o identificador de registro.', + 'details_title' => 'Detalhes de registros', + 'details_description' => 'Exibe detalhes de registros para um modelo selecionado', + 'details_model' => 'Classe Model', + 'details_identifier_value' => 'Valor do identificador', + 'details_identifier_value_description' => 'Valor identificador para carregar os registros do banco de dados. Especifique um valor fixo ou nome de parâmetro URL.', + 'details_identifier_value_required' => 'O valor do identificador é obrigatório', + 'details_key_column' => 'Coluna chave', + 'details_key_column_description' => 'Coluna model para usar como um identificador de registro para buscar os registros do banco de dados.', + 'details_key_column_required' => 'O nome de coluna chave é obrigatório', + 'details_display_column' => 'Exibir coluna', + 'details_display_column_description' => 'Coluna model para exibir na página de detalhes. Usado no partial padrão do componente.', + 'details_display_column_required' => 'Por favor, selecione uma coluna de exibição.', + 'details_not_found_message' => 'Mensagem não encontrada', + 'details_not_found_message_description' => 'Mensagem para exibir se o registro não for encontrado. Usado no partial padrão do componente.', + 'details_not_found_message_default' => 'Registro não encontrado', + ], + 'validation' => [ + 'reserved' => ':attribute não pode ser uma palavra reservada do PHP', + ], +]; diff --git a/plugins/rainlab/builder/lang/zh-cn.json b/plugins/rainlab/builder/lang/zh-cn.json new file mode 100644 index 0000000..3420ab0 --- /dev/null +++ b/plugins/rainlab/builder/lang/zh-cn.json @@ -0,0 +1,4 @@ +{ + "Builder": "构造器", + "Provides visual tools for building October plugins.": "提供用于构建October插件的可视化工具." +} \ No newline at end of file diff --git a/plugins/rainlab/builder/lang/zh-cn/lang.php b/plugins/rainlab/builder/lang/zh-cn/lang.php new file mode 100644 index 0000000..b0cd3f0 --- /dev/null +++ b/plugins/rainlab/builder/lang/zh-cn/lang.php @@ -0,0 +1,735 @@ + [ + 'add' => '创建插件', + 'no_records' => '找不到插件', + 'no_name' => '没有名称', + 'search' => '搜索...', + 'filter_description' => '显示所有插件或只显示您的插件.', + 'settings' => '设置', + 'entity_name' => '插件', + 'field_name' => '名称', + 'field_author' => '作者', + 'field_description' => '描述', + 'field_icon' => '插件图标', + 'field_plugin_namespace' => '插件命名空间', + 'field_author_namespace' => '作者命名空间', + 'field_namespace_description' => '命名空间只能包含拉丁字母和数字,并且应该以拉丁字母开头。示例插件命名空间:Blog', + 'field_author_namespace_description' => '创建插件后,不能使用Builder更改命名空间。示例作者名称空间:JohnSmith', + 'tab_general' => '常规参数', + 'tab_description' => '描述', + 'field_homepage' => '插件主页', + 'no_description' => '常规参数没有为此插件提供说明', + 'error_settings_not_editable' => '无法使用生成器编辑此插件的设置.', + 'update_hint' => '您可以在“本地化”选项卡上编辑本地化插件的名称和说明.', + 'manage_plugins' => '创建和编辑插件', + ], + 'author_name' => [ + 'title' => '作者名称', + 'description' => '用于新插件的默认作者名称。作者名不是固定的-你可以在插件配置中随时更改它.', + ], + 'author_namespace' => [ + 'title' => '作者命名空间', + 'description' => '如果您是为市场开发的,命名空间应该与作者代码匹配,并且不能更改。有关详细信息,请参阅文档.', + ], + 'database' => [ + 'menu_label' => '数据库', + 'no_records' => '未找到数据库表', + 'search' => '搜索...', + 'confirmation_delete_multiple' => '删除选中的数据库表?', + 'field_name' => '数据库表名', + 'tab_columns' => '列', + 'column_name_name' => '列', + 'column_name_required' => '请提供列名', + 'column_name_type' => '类型', + 'column_type_required' => '请选择列类型', + 'column_name_length' => '长度', + 'column_validation_length' => '对于十进制列,长度值应为整数或指定为精度和小数位数(10,2)。长度列中不允许有空格.', + 'column_validation_title' => '列名中只允许数字、小写拉丁字母和下划线', + 'column_name_unsigned' => '符号', + 'column_name_nullable' => '可为NULL', + 'column_auto_increment' => '自增', + 'column_default' => '默认', + 'column_comment' => '注释', + 'column_auto_primary_key' => '键', + 'tab_new_table' => '新增表', + 'btn_add_column' => '新增列', + 'btn_delete_column' => '删除列', + 'btn_add_id' => '添加ID', + 'btn_add_timestamps' => '添加时间戳', + 'btn_add_soft_deleting' => '添加软删除支持', + 'id_exists' => '表中已存在ID列.', + 'timestamps_exist' => '表中已存在created_at列和deleted_at列.', + 'soft_deleting_exist' => '表中已存在deleted_at列.', + 'confirm_delete' => '删除表?', + 'error_enum_not_supported' => '表包含生成器当前不支持的类型为“enum”的列.', + 'error_table_name_invalid_prefix' => '表名应以插件前缀开头: \':prefix\'.', + 'error_table_name_invalid_characters' => '表名无效。表名只能包含拉丁字母、数字和下划线。名称应以拉丁字母开头,不能包含空格.', + 'error_table_duplicate_column' => '重复列名: \':column\'.', + 'error_table_auto_increment_in_compound_pk' => '自动递增列不能是复合主键的一部分.', + 'error_table_mutliple_auto_increment' => '表不能包含多个自动递增列.', + 'error_table_auto_increment_non_integer' => '自动递增列应具有整数类型.', + 'error_table_decimal_length' => ':type 的Length参数的格式应为“10,2”,不带空格.', + 'error_table_length' => ':type 长度参数应指定为整数.', + 'error_unsigned_type_not_int' => '\':column\' 列出错。无符号标志只能应用于整数类型列.', + 'error_integer_default_value' => '整数列 \':column\' 的默认值无效。允许的格式为“10”、“-10”.', + 'error_decimal_default_value' => '十进制或双精度列的默认值无效 \':column\'. 允许的格式为\'1.00\',\'1.00\'.', + 'error_boolean_default_value' => '布尔列的默认值无效 \':column\'. 允许的值为“0”和“1”,或“true”和“false”.', + 'error_unsigned_negative_value' => '无符号列 \':column\' 的默认值不能为负.', + 'error_table_already_exists' => '数据库中已存在表 \':name\'.', + 'error_table_name_too_long' => '表名的长度不应超过64个字符.', + 'error_column_name_too_long' => '列名 \':column\' 太长。列名的长度不应超过64个字符.', + ], + 'model' => [ + 'menu_label' => '模型', + 'entity_name' => '模型', + 'no_records' => '未找到模型', + 'search' => '搜索...', + 'add' => '添加...', + 'forms' => '表单', + 'lists' => '列表', + 'field_class_name' => '类名', + 'field_database_table' => '数据库表', + 'field_add_timestamps' => '添加时间戳支持', + 'field_add_timestamps_description' => '数据库表必须存在 created_at 和 updated_at 字段.', + 'field_add_soft_deleting' => '添加软删除支持', + 'field_add_soft_deleting_description' => '数据库表必须存在 deleted_at 字段.', + 'error_class_name_exists' => '指定类名的模型文件已存在: :path', + 'error_timestamp_columns_must_exist' => '数据库表必须存在 created_at 和 updated_at 字段.', + 'error_deleted_at_column_must_exist' => '数据库表必须存在 deleted_at 字段.', + 'add_form' => '添加表单', + 'add_list' => '添加列表', + ], + 'form' => [ + 'saved' => '表单已保存', + 'confirm_delete' => '删除表单?', + 'tab_new_form' => '新表单', + 'btn_add_database_fields' => '添加数据库字段', + 'property_label_title' => '标签', + 'property_label_required' => '请指定控制标签.', + 'property_span_title' => '跨度', + 'property_comment_title' => '注释', + 'property_comment_above_title' => '在注释上', + 'property_default_title' => '默认', + 'property_checked_default_title' => '默认选中', + 'property_css_class_title' => 'CSS 类名', + 'property_css_class_description' => '分配给字段容器的可选CSS类.', + 'property_disabled_title' => '禁止', + 'property_read_only_title' => '仅读', + 'property_hidden_title' => '隐藏', + 'property_required_title' => '必填', + 'property_field_name_title' => '字段名', + 'property_placeholder_title' => '占位符', + 'property_default_from_title' => '默认来源', + 'property_stretch_title' => '伸展', + 'property_stretch_description' => '指定此字段是否拉伸以适合父级高度.', + 'property_context_title' => '上下文', + 'property_context_description' => '指定显示字段时应使用的窗体上下文.', + 'property_context_create' => '创建', + 'property_context_update' => '更新', + 'property_context_preview' => '预览', + 'property_dependson_title' => '依赖', + 'property_trigger_action' => '行为', + 'property_trigger_show' => '显示', + 'property_trigger_hide' => '隐藏', + 'property_trigger_enable' => '开启', + 'property_trigger_disable' => '禁止', + 'property_trigger_empty' => '空', + 'property_trigger_field' => '字段', + 'property_trigger_field_description' => '定义将触发操作的其他字段名.', + 'property_trigger_condition' => '条件', + 'property_trigger_condition_description' => '确定指定字段应满足的条件,以便将条件视为“true”。支持的值:选中、未选中、值[somevalue].', + 'property_trigger_condition_checked' => '已选中', + 'property_trigger_condition_unchecked' => '未选中', + 'property_trigger_condition_somevalue' => '值[在此处输入值]', + 'property_preset_title' => '预设', + 'property_preset_description' => '允许字段值最初由另一个字段的值设置,使用输入预置转换器进行转换.', + 'property_preset_field' => '字段', + 'property_preset_field_description' => '定义要从中获取值的其他字段名.', + 'property_preset_type' => '类型', + 'property_preset_type_description' => '指定转换类型', + 'property_attributes_title' => '属性', + 'property_attributes_description' => '要添加到表单字段元素的自定义HTML属性.', + 'property_container_attributes_title' => '容器属性', + 'property_container_attributes_description' => '要添加到表单域容器元素的自定义HTML属性.', + 'property_group_advanced' => '高级', + 'property_dependson_description' => '此字段所依赖的其他字段名的列表,当其他字段被修改时,此字段将更新。每行一个字段.', + 'property_trigger_title' => '触发', + 'property_trigger_description' => '允许根据其他元素的状态更改元素属性,如可见性或值.', + 'property_default_from_description' => '从另一个字段的值中获取默认值.', + 'property_field_name_required' => '字段名是必需的', + 'property_field_name_regex' => '字段名只能包含拉丁字母、数字、下划线、短划线和方括号.', + 'property_attributes_size' => '大小', + 'property_attributes_size_tiny' => '微小的', + 'property_attributes_size_small' => '小', + 'property_attributes_size_large' => '大', + 'property_attributes_size_huge' => '巨大的', + 'property_attributes_size_giant' => '特大的', + 'property_comment_position' => '注释位置', + 'property_comment_position_above' => '在上面', + 'property_comment_position_below' => '在下面', + 'property_hint_path' => '提示部分路径', + 'property_hint_path_description' => '包含提示文本的部分文件的路径。使用$符号来引用插件根目录,例如:$/acme/blog/partials/_hint.htm', + 'property_hint_path_required' => '请输入提示部分路径', + 'property_partial_path' => '部分路径', + 'property_partial_path_description' => '部分文件的路径。使用$符号来引用插件根目录,例如:$/acme/blog/partials/_partial.htm', + 'property_partial_path_required' => '请输入部分路径', + 'property_code_language' => '语言', + 'property_code_theme' => '主题', + 'property_theme_use_default' => '使用默认主题', + 'property_group_code_editor' => '代码编辑器', + 'property_gutter' => 'Gutter', + 'property_gutter_show' => '可见', + 'property_gutter_hide' => '隐藏', + 'property_wordwrap' => '自动换行', + 'property_wordwrap_wrap' => '换行', + 'property_wordwrap_nowrap' => '不换行', + 'property_fontsize' => '字体大小', + 'property_codefolding' => '代码目录', + 'property_codefolding_manual' => '指南', + 'property_codefolding_markbegin' => '标记开始', + 'property_codefolding_markbeginend' => '标记开始和结束', + 'property_autoclosing' => '自动关闭', + 'property_enabled' => '开启', + 'property_disabled' => '禁止', + 'property_soft_tabs' => '软标签', + 'property_tab_size' => '标签大小', + 'property_readonly' => '仅读', + 'property_use_default' => '使用默认设置', + 'property_options' => '选项', + 'property_prompt' => '提示', + 'property_prompt_description' => '“创建”按钮显示的文本.', + 'property_prompt_default' => '添加新项', + 'property_available_colors' => '可用颜色', + 'property_available_colors_description' => '十六进制格式的可用颜色列表(#FF0000)。将默认颜色集保留为空。每行输入一个值.', + 'property_datepicker_mode' => '模式', + 'property_datepicker_mode_date' => '日期', + 'property_datepicker_mode_datetime' => '日期时间', + 'property_datepicker_mode_time' => '时间', + 'property_datepicker_min_date' => '最小日期', + 'property_datepicker_min_date_description' => '可以选择的最小/最早日期。默认值为空(2000-01-01).', + 'property_datepicker_max_date' => '最大日期', + 'property_datepicker_max_date_description' => '可选择的最大/最晚日期。将默认值留空(2020-12-31).', + 'property_datepicker_date_invalid_format' => '无效的日期格式。使用格式YYYY-MM-DD.', + 'property_datepicker_year_range' => '年份范围', + 'property_datepicker_year_range_description' => '两边的年数(如10)或上下范围的数组(如[19002015])。将默认值留空(10).', + 'property_datepicker_year_range_invalid_format' => '年份范围格式无效。使用数字(如“10”)或上限/下限数组(如“[19002015]”)', + 'property_datepicker_format' => '格式', + 'property_datepicker_year_format_description' => '定义自定义日期格式。默认格式为“Y-m-d”', + 'property_fileupload_mode' => '模式', + 'property_fileupload_mode_file' => '文件', + 'property_fileupload_mode_image' => '图片', + 'property_group_fileupload' => '上传文件', + 'property_fileupload_image_width' => '图片宽度', + 'property_fileupload_image_width_description' => '可选参数-图像大小将调整为此宽度。仅适用于图像模式.', + 'property_fileupload_invalid_dimension' => '维度值无效-请输入一个数字.', + 'property_fileupload_image_height' => '图片高度', + 'property_fileupload_image_height_description' => '可选参数-图像大小将调整到此高度。仅适用于图像模式.', + 'property_fileupload_file_types' => '文件类型', + 'property_fileupload_file_types_description' => '上传可接受的文件扩展名的可选逗号分隔列表。如: zip,txt', + 'property_fileupload_mime_types' => 'MIME 类型', + 'property_fileupload_maxfilesize' => '最大文件大小', + 'property_fileupload_maxfilesize_description' => '上传者接受的文件大小(以 Mb 为单位),可选。', + 'property_fileupload_invalid_maxfilesize' => '最大文件大小值无效', + 'property_fileupload_maxfiles' => '最大文件数量', + 'property_fileupload_invalid_maxfiles' => '最大文件数量值无效', + 'property_fileupload_maxfiles_description' => '允许上传的最大文件数量', + 'property_fileupload_mime_types_description' => '上传接受的可选逗号分隔的MIME类型列表,可以是文件扩展名,也可以是完全限定名. 如: bin,txt', + 'property_fileupload_use_caption' => '使用说明', + 'property_fileupload_use_caption_description' => '允许为文件设置标题和说明.', + 'property_fileupload_thumb_options' => '缩略图选项', + 'property_fileupload_thumb_options_description' => '管理自动生成的缩略图的选项。仅适用于图像模式.', + 'property_fileupload_thumb_mode' => '模式', + 'property_fileupload_thumb_auto' => '自动', + 'property_fileupload_thumb_exact' => '扩展', + 'property_fileupload_thumb_portrait' => '纵向', + 'property_fileupload_thumb_landscape' => '横向', + 'property_fileupload_thumb_crop' => '裁切', + 'property_fileupload_thumb_extension' => '文件扩展名', + 'property_name_from' => '列名', + 'property_name_from_description' => '用于显示名称的关系列名.', + 'property_relation_select' => '选择', + 'property_relation_select_description' => '将多个列合并在一起以显示名称', + 'property_relation_scope' => '范围', + 'property_relation_scope_description' => '指定在相关表单模型中定义的查询范围方法,以始终应用于列表查询', + 'property_description_from' => '列描述', + 'property_description_from_description' => '用于显示说明的关系列名称.', + 'property_recordfinder_prompt' => '提示', + 'property_recordfinder_prompt_description' => '未选择记录时要显示的文本。%s字符表示搜索图标。为默认提示保留为空.', + 'property_recordfinder_list' => '列表配置', + 'property_recordfinder_list_description' => '对列表列定义文件的引用。使用$符号来引用插件根目录,例如:$/acme/blog/lists/_list.yaml', + 'property_recordfinder_list_required' => '请提供列表YAML文件的路径', + 'property_group_recordfinder' => '查找记录', + 'property_mediafinder_mode' => '模式', + 'property_mediafinder_mode_file' => '文件', + 'property_mediafinder_mode_image' => '图片', + 'property_mediafinder_image_width_description' => '如果使用图像类型,预览图像将按此宽度显示(可选).', + 'property_mediafinder_image_height_description' => '如果使用图像类型,预览图像将显示到此高度(可选).', + 'property_group_taglist' => '标签列表', + 'property_taglist_mode' => '模式', + 'property_taglist_mode_description' => '定义此字段的值作为返回格式', + 'property_taglist_mode_string' => '字符串', + 'property_taglist_mode_array' => '数组', + 'property_taglist_mode_relation' => '关联', + 'property_taglist_separator' => '分割符', + 'property_taglist_separator_comma' => '分割符号', + 'property_taglist_separator_space' => '空格', + 'property_taglist_options' => '预定义的标记', + 'property_taglist_custom_tags' => '自定义标签', + 'property_taglist_custom_tags_description' => '允许用户手动输入自定义标记.', + 'property_taglist_name_from' => '来源名称', + 'property_taglist_name_from_description' => '定义标记中显示的关系模型属性。仅用于“关联”模式.', + 'property_taglist_use_key' => '使用密钥', + 'property_taglist_use_key_description' => '如果选中,标记列表将使用键而不是值来保存和读取数据。仅用于“关联”模式.', + 'property_group_relation' => '关联', + 'property_relation_prompt' => '提示', + 'property_relation_prompt_description' => '没有可用选择时要显示的文本.', + 'property_empty_option' => '空选项', + 'property_empty_option_description' => 'empty选项对应于空选择,但与占位符不同,它可以重新选择.', + 'property_show_search' => '显示搜索', + 'property_show_search_description' => '启用此下拉列表的搜索功能.', + 'property_title_from' => '标题来自', + 'property_title_from_description' => '指定一个子字段名称以使用该字段的值作为每个转发器项目的标题。', + 'property_min_items' => '最小项', + 'property_min_items_description' => '转发器内允许的最小项目数。', + 'property_min_items_integer' => '最小项目必须是正整数。', + 'property_max_items' => '最大项', + 'property_max_items_description' => '转发器中允许的最大项目数。', + 'property_max_items_integer' => '最大项目必须是正整数。', + 'property_display_mode' => '风格', + 'property_display_mode_description' => '定义应用到这个转发器的行为。', + 'property_switch_label_on' => 'ON标题', + 'property_switch_label_on_description' => '为“ON”开关状态设置自定义标题', + 'property_switch_label_off' => 'OFF标题', + 'property_switch_label_off_description' => '为“OFF”开关状态设置自定义标题', + 'control_group_standard' => '标准', + 'control_group_widgets' => '控件', + 'click_to_add_control' => '添加控件', + 'loading' => '加载中...', + 'control_text' => '文本', + 'control_text_description' => '单行文本框', + 'control_password' => '密码', + 'control_password_description' => '单行密码文本字段', + 'control_checkbox' => '复选框', + 'control_checkbox_description' => '单选框', + 'control_switch' => '开关', + 'control_switch_description' => '单选开关盒,复选框的替代品', + 'control_textarea' => '文本域', + 'control_textarea_description' => '高度可控的多行文本框', + 'control_dropdown' => '下拉框', + 'control_dropdown_description' => '带有静态或动态选项的下拉列表', + 'control_balloon-selector' => '气球选择器', + 'control_balloon-selector_description' => '一次只能使用静态或动态选项选择一个项目的列表', + 'control_unknown' => '未知控件类型: :type', + 'control_repeater' => '循环组件', + 'control_repeater_description' => '输出一组重复的窗体控件', + 'control_number' => '数值', + 'control_number_description' => '只接受数字的单行文本框', + 'control_hint' => '提示', + 'control_hint_description' => '输出可由用户隐藏的框中的部分内容', + 'control_partial' => '部分', + 'control_partial_description' => '输出部分内容', + 'control_section' => '切片', + 'control_section_description' => '显示带有标题和副标题的窗体节', + 'control_radio' => '单选框列表', + 'control_radio_description' => '单选选项列表,一次只能选择一个项目', + 'control_radio_option_1' => '选项 1', + 'control_radio_option_2' => '选项 2', + 'control_checkboxlist' => '复选框列表', + 'control_checkboxlist_description' => '复选框列表,可在其中选择多个项目', + 'control_codeeditor' => '代码编辑器', + 'control_codeeditor_description' => '用于格式化代码或标记的纯文本编辑器', + 'control_colorpicker' => '颜色选择器', + 'control_colorpicker_description' => '用于选择十六进制颜色值的字段', + 'control_datepicker' => '日期选择器', + 'control_datepicker_description' => '用于选择日期和时间的文本字段', + 'control_richeditor' => '富文本编辑器', + 'control_richeditor_description' => '富格式文本的可视化编辑器,也称为所见即所得编辑器', + 'property_group_rich_editor' => '富文本编辑器', + 'property_richeditor_toolbar_buttons' => '工具栏按钮', + 'property_richeditor_toolbar_buttons_description' => '在编辑器工具栏上显示哪些按钮', + 'control_markdown' => 'Markdown 编辑器', + 'control_markdown_description' => '标记格式文本的基本编辑器', + 'control_taglist' => '标签列表', + 'control_taglist_description' => '用于输入标签列表的字段', + 'control_fileupload' => '文件上传', + 'control_fileupload_description' => '图像或常规文件的文件上传', + 'control_recordfinder' => '记录查找', + 'control_recordfinder_description' => '具有记录搜索功能的相关记录的详细信息的字段', + 'control_mediafinder' => '媒体查找器', + 'control_mediafinder_description' => '从媒体管理器库中选择项目的字段', + 'control_relation' => '关联', + 'control_relation_description' => '显示用于选择相关记录的下拉列表或复选框列表', + 'control_widget_type' => '小部件类型', + 'error_file_name_required' => '请输入表单文件名.', + 'error_file_name_invalid' => '文件名只能包含拉丁字母、数字、下划线、点和哈希.', + 'span_left' => '左边', + 'span_right' => '右边', + 'span_full' => '铺满', + 'span_auto' => '自动', + 'style_default' => '默认', + 'style_collapsed' => '折叠', + 'style_accordion' => '手风琴', + 'empty_tab' => '空标签', + 'confirm_close_tab' => '选项卡包含将被删除的控件。继续?', + 'tab' => '表单选项卡', + 'tab_title' => '标题', + 'controls' => '控件', + 'property_tab_title_required' => '选项卡标题不能为空.', + 'tabs_primary' => '主选项卡', + 'tabs_secondary' => '辅助选项卡', + 'tab_stretch' => '标准', + 'tab_stretch_description' => '指定此选项卡容器是否拉伸以适合父级高度.', + 'tab_css_class' => 'CSS 类', + 'tab_css_class_description' => '将CSS类分配给tabs容器.', + 'tab_name_template' => '选项卡 %s', + 'tab_already_exists' => '具有指定标题的选项卡已存在.', + ], + 'list' => [ + 'tab_new_list' => '新列表', + 'saved' => '列表已保存', + 'confirm_delete' => '删除列表?', + 'tab_columns' => '列', + 'btn_add_column' => '添加列', + 'btn_delete_column' => '删除列', + 'column_dbfield_label' => '字段', + 'column_dbfield_required' => '请输入模型字段', + 'column_name_label' => '标签', + 'column_label_required' => '请输入列标签', + 'column_type_label' => '类型', + 'column_type_required' => '请输入列类型', + 'column_type_text' => '文本', + 'column_type_number' => '数值', + 'column_type_switch' => '开关', + 'column_type_datetime' => '日期时间', + 'column_type_date' => '日期', + 'column_type_time' => '时间', + 'column_type_timesince' => '开始时间', + 'column_type_timetense' => '结束时间', + 'column_type_select' => '选择', + 'column_type_partial' => '部分', + 'column_label_default' => '默认', + 'column_label_searchable' => '搜索', + 'column_label_sortable' => '排序', + 'column_label_invisible' => '隐形的', + 'column_label_select' => '选择', + 'column_label_relation' => '关系', + 'column_label_css_class' => 'CSS 类', + 'column_label_width' => '宽度', + 'column_label_path' => '路径', + 'column_label_format' => '格式', + 'column_label_value_from' => '来源值', + 'error_duplicate_column' => '重复的列字段名: \':column\'.', + 'btn_add_database_columns' => '添加数据库列', + 'all_database_columns_exist' => '列表中已定义所有数据库列', + ], + 'controller' => [ + 'menu_label' => '控制器', + 'no_records' => '找不到插件控制器', + 'controller' => '控制器', + 'behaviors' => '行为', + 'new_controller' => '新控制器', + 'error_controller_has_no_behaviors' => '控制器没有可配置的行为.', + 'error_invalid_yaml_configuration' => '加载行为配置文件时出错: :file', + 'behavior_form_controller' => '窗体控制器行为', + 'behavior_form_controller_description' => '向后端页面添加表单功能。该行为提供了三个页面,分别称为Create、Update和Preview.', + 'property_behavior_form_placeholder' => '--选择表单--', + 'property_behavior_form_name' => '名称', + 'property_behavior_form_name_description' => '此窗体管理的对象的名称', + 'property_behavior_form_name_required' => '请输入表单名称', + 'property_behavior_form_file' => '表单配置', + 'property_behavior_form_file_description' => '对表单域定义文件的引用', + 'property_behavior_form_file_required' => '请输入表单配置文件的路径', + 'property_behavior_form_model_class' => '模型类', + 'property_behavior_form_model_class_description' => '一个模型类名,表单数据将根据此模型加载和保存.', + 'property_behavior_form_model_class_required' => '请选择一个模型类', + 'property_behavior_form_default_redirect' => '默认重定向', + 'property_behavior_form_default_redirect_description' => '保存或取消表单时默认重定向到的页面.', + 'property_behavior_form_create' => '创建记录页', + 'property_behavior_form_redirect' => '重定向', + 'property_behavior_form_redirect_description' => '创建记录时要重定向到的页.', + 'property_behavior_form_redirect_close' => '关闭重定向', + 'property_behavior_form_redirect_close_description' => '创建记录并随请求一起发送close post变量时要重定向到的页面.', + 'property_behavior_form_flash_save' => '保存闪存消息', + 'property_behavior_form_flash_save_description' => '保存记录时要显示的闪存消息.', + 'property_behavior_form_page_title' => '页面标题', + 'property_behavior_form_update' => '更新记录页', + 'property_behavior_form_update_redirect' => '重定向', + 'property_behavior_form_create_redirect_description' => '保存记录时要重定向到的页.', + 'property_behavior_form_flash_delete' => '删除闪存消息', + 'property_behavior_form_flash_delete_description' => '删除记录时显示的闪烁消息.', + 'property_behavior_form_preview' => '预览记录页', + 'behavior_list_controller' => '列表控制器行为', + 'behavior_list_controller_description' => '提供可排序和可搜索的列表,其记录上有可选链接。行为自动创建控制器操作“index”.', + 'property_behavior_list_title' => '列表标题', + 'property_behavior_list_title_required' => '请输入列表标题', + 'property_behavior_list_placeholder' => '--选择列表--', + 'property_behavior_list_model_class' => '模型类', + 'property_behavior_list_model_class_description' => '模型类名,列表数据从此模型加载.', + 'property_behavior_form_model_class_placeholder' => '--选择模型--', + 'property_behavior_list_model_class_required' => '请输入一个模型名称', + 'property_behavior_list_model_placeholder' => '--选择模型--', + 'property_behavior_list_file' => '列表配置文件', + 'property_behavior_list_file_description' => '对列表定义文件的引用', + 'property_behavior_list_file_required' => '请输入列表配置文件的路径', + 'property_behavior_list_record_url' => '记录 URL', + 'property_behavior_list_record_url_description' => '将每个列表记录链接到另一页。例:用户/更新:id:id部分替换为记录标识符.', + 'property_behavior_list_no_records_message' => '无记录消息', + 'property_behavior_list_no_records_message_description' => '找不到记录时要显示的消息', + 'property_behavior_list_recs_per_page' => '每一页记录', + 'property_behavior_list_recs_per_page_description' => '每页要显示的记录,使用0表示没有页。默认值:0', + 'property_behavior_list_recs_per_page_regex' => '每页记录数应为整数值', + 'property_behavior_list_show_setup' => '显示设置按钮', + 'property_behavior_list_show_sorting' => '显示排序', + 'property_behavior_list_default_sort' => '默认排序', + 'property_behavior_form_ds_column' => '列', + 'property_behavior_form_ds_direction' => '方向', + 'property_behavior_form_ds_asc' => '升序', + 'property_behavior_form_ds_desc' => '降序', + 'property_behavior_list_show_checkboxes' => '显示复选框', + 'property_behavior_list_onclick' => '点击处理程序', + 'property_behavior_list_onclick_description' => '单击记录时要执行的自定义JavaScript代码.', + 'property_behavior_list_show_tree' => '显示树', + 'property_behavior_list_show_tree_description' => '显示父/子记录的树层次结构.', + 'property_behavior_list_tree_expanded' => '树已展开', + 'property_behavior_list_tree_expanded_description' => '确定默认情况下是否应展开树节点.', + 'property_behavior_list_toolbar' => '工具栏', + 'property_behavior_list_toolbar_buttons' => '按钮部分', + 'property_behavior_list_toolbar_buttons_description' => '使用工具栏按钮引用控制器部分文件。例如:列表工具栏', + 'property_behavior_list_search' => '搜索', + 'property_behavior_list_search_prompt' => '搜索提示', + 'property_behavior_list_filter' => '筛选配置', + 'behavior_reorder_controller' => '重新排序控制器行为', + 'behavior_reorder_controller_description' => '提供对其记录进行排序和重新排序的功能。该行为会自动创建控制器操作“重新排序”.', + 'property_behavior_reorder_title' => '重新排序标题', + 'property_behavior_reorder_title_required' => '请输入重新排序标题', + 'property_behavior_reorder_name_from' => '属性名称', + 'property_behavior_reorder_name_from_description' => '应用作每个记录的标签的模型属性.', + 'property_behavior_reorder_name_from_required' => '请输入属性名称', + 'property_behavior_reorder_model_class' => '模型类', + 'property_behavior_reorder_model_class_description' => '模型类名,重新排序数据从此模型加载.', + 'property_behavior_reorder_model_class_placeholder' => '--选择模型--', + 'property_behavior_reorder_model_class_required' => '请选择模型类', + 'property_behavior_reorder_model_placeholder' => '--选择模型--', + 'property_behavior_reorder_toolbar' => '工具栏', + 'property_behavior_reorder_toolbar_buttons' => '按钮部分', + 'property_behavior_reorder_toolbar_buttons_description' => '使用工具栏按钮引用控制器部分文件。例如:重新排序工具栏', + 'error_controller_not_found' => '找不到原始控制器文件.', + 'error_invalid_config_file_name' => '行为 :class 配置文件名 (:file) 包含无效字符,造成不能假装.', + 'error_file_not_yaml' => '行为 :class 配置文件 (:file) 不是一个YAML文件. 仅支持YAML配置文件.', + 'saved' => '控制器已保存', + 'controller_name' => '控制器名称', + 'controller_name_description' => '控制器名称定义控制器后端页的类名和URL。应用标准的PHP变量命名约定。第一个符号应该是大写拉丁字母。示例:Build、News、Case.', + 'base_model_class' => '基本模型类', + 'base_model_class_description' => '选择要在需要或支持模型的行为中用作基础模型的模型类。您可以稍后配置这些行为.', + 'base_model_class_placeholder' => '--选择模型--', + 'controller_behaviors' => '行为', + 'controller_behaviors_description' => '选择控制器应该实现的行为。生成器将自动创建行为所需的视图文件.', + 'controller_permissions' => '权限', + 'controller_permissions_description' => '选择可以访问控制器视图的用户权限。权限可以在生成器的“权限”选项卡上定义。稍后可以在控制器PHP脚本中更改此选项.', + 'controller_permissions_no_permissions' => '插件没有定义任何权限.', + 'menu_item' => '活动菜单项', + 'menu_item_description' => '选择要激活控制器页面的菜单项。稍后可以在控制器PHP脚本中更改此选项.', + 'menu_item_placeholder' => '--选择菜单项--', + 'error_unknown_behavior' => '行为类 :class 没有在行为类库注册.', + 'error_behavior_view_conflict' => '所选行为提供了冲突的视图 (:view) 不能在控制器中一起使用.', + 'error_behavior_config_conflict' => '所选行为提供冲突的配置文件 (:file) 无法在控制器中一起使用.', + 'error_behavior_view_file_not_found' => '行为 :class 的视图模板 :view 没有找到.', + 'error_behavior_config_file_not_found' => '行为 :class 的配置文件 :file 没有找到.', + 'error_controller_exists' => '控制器文件已存在: :file.', + 'error_controller_name_invalid' => '无效格式的控制名称. 名称只能包含数字和拉丁字母。第一个符号应该是大写拉丁字母.', + 'error_behavior_view_file_exists' => '控制器视图文件已存在: :view.', + 'error_behavior_config_file_exists' => '行为配置文件已存在: :file.', + 'error_save_file' => '保存控制器文件错误: :file', + 'error_behavior_requires_base_model' => '行为 :behavior 必须选择一个基础模型类.', + 'error_model_doesnt_have_lists' => '所选模型没有任何列表。请先创建列表.', + 'error_model_doesnt_have_forms' => '所选模型没有任何窗体。请先创建表单.', + ], + 'version' => [ + 'menu_label' => '版本', + 'no_records' => '找不到插件版本', + 'search' => '搜索...', + 'tab' => '版本', + 'saved' => '已保存版本', + 'confirm_delete' => '删除版本?', + 'tab_new_version' => '新奔奔', + 'migration' => '迁移', + 'seeder' => '填充', + 'custom' => '增加版本号', + 'apply_version' => '应用版本', + 'applying' => '应用中...', + 'rollback_version' => '回滚版本', + 'rolling_back' => '回滚中...', + 'applied' => '应用的版本', + 'rolled_back' => '版本已回滚', + 'hint_save_unapplied' => '您保存了一个未应用的版本。当您或其他用户登录到后端或数据库表保存在生成器的数据库部分时,可以自动应用未应用的版本.', + 'hint_rollback' => '回滚某个版本也将回滚比此版本更新的所有版本。请注意,当您或其他用户登录到后端或数据库表保存在生成器的数据库部分时,系统会自动应用未应用的版本.', + 'hint_apply' => '应用一个版本也会应用插件的所有旧版本.', + 'dont_show_again' => '不再显示', + 'save_unapplied_version' => '保存未应用的版本', + 'sort_ascending' => '升序排序', + 'sort_descending' => '降序排序', + ], + 'menu' => [ + 'menu_label' => '后台菜单', + 'tab' => '菜单', + 'items' => '菜单选项', + 'saved' => '已保存菜单', + 'add_main_menu_item' => '添加主菜单项', + 'new_menu_item' => '菜单选项', + 'add_side_menu_item' => '添加子项', + 'side_menu_item' => '侧边栏菜单项', + 'property_label' => '标签', + 'property_label_required' => '请输入菜单项标签.', + 'property_url_required' => '请输入菜单项URL', + 'property_url' => 'URL', + 'property_icon' => '图标', + 'property_icon_required' => '请选择一个图标', + 'property_permissions' => '权限', + 'property_order' => '顺序', + 'property_order_invalid' => '请以整数值形式输入菜单项顺序。', + 'property_order_description' => '菜单项顺序管理其在菜单中的位置。如果没有提供订单,该项目将被放在菜单的末尾。默认订单值的增量为100.', + 'property_attributes' => 'HTML属性', + 'property_code' => '代码', + 'property_code_invalid' => '代码只能包含拉丁字母和数字', + 'property_code_required' => '请输入菜单项代码.', + 'error_duplicate_main_menu_code' => '主菜单项代码重复: \':code\'.', + 'error_duplicate_side_menu_code' => '重复的侧边栏菜单项代码: \':code\'.', + 'icon_svg' => 'SVG图标', + 'icon_svg_description' => '用于代替标准图标的SVG图标,SVG图标应该是一个矩形并且可以支持颜色', + 'counter' => '通知内容', + 'counter_description' => '要在菜单图标附近输出的数值。 该值应该是一个数字或一个返回数字的可调用对象', + 'counter_label' => '通知描述', + 'counter_label_description' => '一个字符串值,用于描述计数器中的数字引用', + 'counter_group' => '通知气泡', + ], + 'localization' => [ + 'menu_label' => '本地化', + 'language' => '语言', + 'strings' => '字符串', + 'confirm_delete' => '删除语言?', + 'tab_new_language' => '新语言', + 'no_records' => '未找到语言', + 'saved' => '语言文件已保存', + 'error_cant_load_file' => '无法加载请求的语言文件-找不到文件.', + 'error_bad_localization_file_contents' => '无法加载请求的语言文件。语言文件只能包含数组定义和字符串.', + 'error_file_not_array' => '无法加载请求的语言文件。语言文件应该返回一个数组.', + 'save_error' => '保存文件错误 \':name\'. 请检查写入权限.', + 'error_delete_file' => '删除本地化文件时出错.', + 'add_missing_strings' => '添加缺少的字符串', + 'copy' => '复制', + 'add_missing_strings_label' => '选择要从中复制缺少字符串的语言', + 'no_languages_to_copy_from' => '没有其他语言可用于复制字符串.', + 'new_string_warning' => '新字符串或节', + 'structure_mismatch' => '源语言文件的结构与正在编辑的文件的结构不匹配。编辑文件中的某些单独字符串与源文件中的节相对应(反之亦然),因此无法自动合并.', + 'create_string' => '创建新的字符串', + 'string_key_label' => '字符串键', + 'string_key_comment' => '使用句点作为节分隔符输入字符串键。例如:plugin.search. 字符串将在插件的默认语言本地化文件中创建.', + 'string_value' => '字符串值', + 'string_key_is_empty' => '字符串键不应为空', + 'string_key_is_a_string' => ':key 是字符串,不能包含其他字符串.', + 'string_value_is_empty' => '字符串值不应为空', + 'string_key_exists' => '字符串键已存在', + ], + 'permission' => [ + 'menu_label' => '权限', + 'tab' => '权限', + 'form_tab_permissions' => '权限', + 'btn_add_permission' => '添加权限', + 'btn_delete_permission' => '删除权限', + 'column_permission_label' => '权限代码', + 'column_permission_required' => '请输入权限代码', + 'column_tab_label' => '选项卡标题', + 'column_tab_required' => '请输入权限选项卡标题', + 'column_label_label' => '标签', + 'column_label_required' => '请输入选项卡标签', + 'saved' => '权限已保存', + 'error_duplicate_code' => '权限代码重复: \':code\'.', + ], + 'yaml' => [ + 'save_error' => '保存文件错误 \':name\'. 请检查写入权限.', + ], + 'common' => [ + 'error_file_exists' => '文件已存在: \':path\'.', + 'field_icon_description' => 'October 使用字体图标: http://octobercms.com/docs/ui/icon', + 'destination_dir_not_exists' => '目标目录不存在: \':path\'.', + 'error_make_dir' => '创建目录错误: \':name\'.', + 'error_dir_exists' => '目录已存在: \':path\'.', + 'template_not_found' => '模板文件未找到: \':name\'.', + 'error_generating_file' => '生成文件错误: \':path\'.', + 'error_loading_template' => '加载模板文件错误: \':name\'.', + 'select_plugin_first' => '请先选择一个插件。要查看插件列表,请单击左侧边栏上的>图标。.', + 'plugin_not_selected' => '未选择插件', + 'add' => '添加', + ], + 'migration' => [ + 'entity_name' => '迁移', + 'error_version_invalid' => '版本格式应参照:1.0.1', + 'field_version' => '版本', + 'field_description' => '描述', + 'field_code' => '代码', + 'save_and_apply' => '保存 & 应用', + 'error_version_exists' => '迁移版本已存在.', + 'error_script_filename_invalid' => '迁移脚本文件名只能包含拉丁字母、数字和下划线。名称应以拉丁字母开头,不能包含空格.', + 'error_cannot_change_version_number' => '无法更改应用版本的版本号.', + 'error_file_must_define_class' => '迁移代码应该定义迁移或种子类。如果只想更新版本号,请将“代码”字段留空.', + 'error_file_must_define_namespace' => '应该定义一个代码迁移。如果只想更新版本号,请将“代码”字段留空.', + 'no_changes_to_save' => '没有要保存的更改.', + 'error_namespace_mismatch' => '迁移代码应该使用插件名称空间: :namespace', + 'error_migration_file_exists' => '迁移文件 :file 已存在. 请使用其他的文件名称.', + 'error_cant_delete_applied' => '此版本已应用,无法删除。请先回滚版本.', + ], + 'components' => [ + 'list_title' => '记录列表', + 'list_description' => '显示选定模型的记录列表', + 'list_page_number' => '页数', + 'list_page_number_description' => '此值用于确定用户所在的页面.', + 'list_records_per_page' => '每页记录', + 'list_records_per_page_description' => '要在单个页面上显示的记录数。保留为空可禁用分页.', + 'list_records_per_page_validation' => '每页记录值的格式无效。值应为数字.', + 'list_no_records' => '无记录消息', + 'list_no_records_description' => '如果没有记录,则在列表中显示的消息。在默认组件的.', + 'list_no_records_default' => '找不到记录', + 'list_sort_column' => '列排序', + 'list_sort_column_description' => '记录排序依据的模型列', + 'list_sort_direction' => '方向', + 'list_display_column' => '显示列', + 'list_display_column_description' => '要在列表中显示的列。在默认组件的.', + 'list_display_column_required' => '请选择显示列.', + 'list_details_page' => '详情页', + 'list_details_page_description' => '显示记录详细信息的页面.', + 'list_details_page_no' => '--无详细信息页--', + 'list_sorting' => '排序', + 'list_pagination' => '分页', + 'list_order_direction_asc' => '升序', + 'list_order_direction_desc' => '倒叙', + 'list_model' => '模型类', + 'list_scope' => '范围', + 'list_scope_description' => '获取记录的可选模型范围', + 'list_scope_default' => '--选择范围,可选--', + 'list_scope_value' => '范围值', + 'list_scope_value_description' => '传递给模型范围的可选值', + 'list_details_page_link' => '链接到详细信息页', + 'list_details_key_column' => '详细信息键列', + 'list_details_key_column_description' => '要用作详细信息页链接中的记录标识符的模型列.', + 'list_details_url_parameter' => 'URL 参数名称', + 'list_details_url_parameter_description' => '采用记录标识符的详细信息页URL参数的名称.', + 'details_title' => '记录详情', + 'details_description' => '显示选定模型的记录详细信息', + 'details_model' => '模型类', + 'details_identifier_value' => '标识符值', + 'details_identifier_value_description' => '从数据库加载记录的标识符值。指定固定值或URL参数名称.', + 'details_identifier_value_required' => '标识符值是必需的', + 'details_key_column' => '键列', + 'details_key_column_description' => '要用作从数据库提取记录的记录标识符的模型列.', + 'details_key_column_required' => '键列名是必需的', + 'details_display_column' => '显示列', + 'details_display_column_description' => '要在详细信息页面上显示的模型列。在默认组件的.', + 'details_display_column_required' => '请选择显示列.', + 'details_not_found_message' => '未找到消息', + 'details_not_found_message_description' => '未找到记录时要显示的消息。在默认组件的.', + 'details_not_found_message_default' => '记录未找到', + ], + 'validation' => [ + 'reserved' => ':attribute 不能是PHP保留关键字', + ], +]; diff --git a/plugins/rainlab/builder/models/BaseModel.php b/plugins/rainlab/builder/models/BaseModel.php new file mode 100644 index 0000000..c2f042e --- /dev/null +++ b/plugins/rainlab/builder/models/BaseModel.php @@ -0,0 +1,163 @@ +updatedData = []; + + foreach ($attributes as $key => $value) { + if (!in_array($key, static::$fillable)) { + continue; + } + + $methodName = 'set'.ucfirst($key); + if (method_exists($this, $methodName)) { + $this->$methodName($value); + } + else { + if (is_scalar($value) && strpos($value, ' ') !== false) { + $value = trim($value); + } + + $this->$key = $value; + } + + $this->updatedData[$key] = $value; + } + } + + /** + * validate + */ + public function validate() + { + $existingData = []; + foreach (static::$fillable as $field) { + $existingData[$field] = $this->$field; + } + + $validation = Validator::make( + array_merge($existingData, $this->updatedData), + $this->validationRules, + $this->validationMessages + ); + + if ($validation->fails()) { + throw new ValidationException($validation); + } + + if (!$this->isNewModel()) { + $this->validateBeforeCreate(); + } + } + + /** + * isNewModel + */ + public function isNewModel() + { + return $this->exists === false; + } + + /** + * Sets a string code of a plugin the model is associated with + * @param string $code Specifies the plugin code + */ + public function setPluginCode($code) + { + $this->pluginCodeObj = new PluginCode($code); + } + + /** + * Sets a code object of a plugin the model is associated with + * @param PluginCode $obj Specifies the plugin code object + */ + public function setPluginCodeObj($obj) + { + $this->pluginCodeObj = $obj; + } + + /** + * validateBeforeCreate + */ + protected function validateBeforeCreate() + { + } + + /** + * getModelPluginName + */ + public function getModelPluginName() + { + $pluginCodeObj = $this->getPluginCodeObj(); + $pluginCode = $pluginCodeObj->toCode(); + + $vector = PluginVector::createFromPluginCode($pluginCode); + if ($vector) { + return $vector->getPluginName(); + } + + return null; + } + + /** + * getPluginCodeObj + */ + public function getPluginCodeObj() + { + if (!$this->pluginCodeObj) { + throw new SystemException(sprintf('The active plugin is not set in the %s object.', get_class($this))); + } + + return $this->pluginCodeObj; + } +} diff --git a/plugins/rainlab/builder/models/CodeFileModel.php b/plugins/rainlab/builder/models/CodeFileModel.php new file mode 100644 index 0000000..c8ef7d9 --- /dev/null +++ b/plugins/rainlab/builder/models/CodeFileModel.php @@ -0,0 +1,282 @@ +allowedExtensions = self::getEditableExtensions(); + } + + /** + * load a single template by its file name. + * + * @param string $fileName + * @return mixed|static + */ + public function load($fileName) + { + $filePath = $this->getFilePath($fileName); + + if (!File::isFile($filePath)) { + return null; + } + + if (($content = @File::get($filePath)) === false) { + return null; + } + + $this->fileName = $fileName; + $this->originalFileName = $fileName; + $this->mtime = File::lastModified($filePath); + $this->content = $content; + $this->exists = true; + + return $this; + } + + /** + * Sets the object attributes. + * @param array $attributes A list of attributes to set. + */ + public function fill(array $attributes) + { + foreach ($attributes as $key => $value) { + if (!in_array($key, static::$fillable)) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.invalid_property', + ['name' => $key] + )); + } + + $this->$key = $value; + } + } + + /** + * Saves the object to the disk. + */ + public function save() + { + $this->validateFileName(); + + $fullPath = $this->getFilePath(); + + if (File::isFile($fullPath) && $this->originalFileName !== $this->fileName) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.file_already_exists', + ['name' => $this->fileName] + )); + } + + $dirPath = base_path($this->dirName); + if (!file_exists($dirPath) || !is_dir($dirPath)) { + if (!File::makeDirectory($dirPath, 0777, true, true)) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.error_creating_directory', + ['name' => $dirPath] + )); + } + } + + if (($pos = strpos($this->fileName, '/')) !== false) { + $dirPath = dirname($fullPath); + + if (!is_dir($dirPath) && !File::makeDirectory($dirPath, 0777, true, true)) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.error_creating_directory', + ['name' => $dirPath] + )); + } + } + + $newFullPath = $fullPath; + if (@File::put($fullPath, $this->content) === false) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.error_saving', + ['name' => $this->fileName] + )); + } + + if (strlen($this->originalFileName) && $this->originalFileName !== $this->fileName) { + $fullPath = $this->getFilePath($this->originalFileName); + + if (File::isFile($fullPath)) { + @unlink($fullPath); + } + } + + clearstatcache(); + + $this->mtime = @File::lastModified($newFullPath); + $this->originalFileName = $this->fileName; + $this->exists = true; + } + + /** + * delete + */ + public function delete() + { + $fileName = Request::input('fileName'); + $fullPath = $this->getFilePath($fileName); + + $this->validateFileName($fileName); + + if (File::exists($fullPath)) { + if (!@File::delete($fullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_file', + ['name' => $fileName] + )); + } + } + } + + /** + * validateFileName validates the supplied filename, extension and path. + * @param string $fileName + */ + protected function validateFileName($fileName = null) + { + if ($fileName === null) { + $fileName = $this->fileName; + } + + $fileName = trim($fileName); + + if (!strlen($fileName)) { + throw new ValidationException(['fileName' => + Lang::get('cms::lang.cms_object.file_name_required', [ + 'allowed' => implode(', ', $this->allowedExtensions), + 'invalid' => pathinfo($fileName, PATHINFO_EXTENSION) + ]) + ]); + } + + if (!FileHelper::validateExtension($fileName, $this->allowedExtensions, false)) { + throw new ValidationException(['fileName' => + Lang::get('cms::lang.cms_object.invalid_file_extension', [ + 'allowed' => implode(', ', $this->allowedExtensions), + 'invalid' => pathinfo($fileName, PATHINFO_EXTENSION) + ]) + ]); + } + + if (!FileHelper::validatePath($fileName, null)) { + throw new ValidationException(['fileName' => + Lang::get('cms::lang.cms_object.invalid_file', [ + 'name' => $fileName + ]) + ]); + } + } + + /** + * getFileName returns the file name. + * @return string + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * getFilePath returns the absolute file path. + * @param string $fileName Specifies the file name to return the path to. + * @return string + */ + public function getFilePath($fileName = null) + { + if ($fileName === null) { + $fileName = $this->fileName; + } + + $pluginPath = $this->getPluginCodeObj()->toFilesystemPath(); + + return base_path($this->dirName.'/'.$pluginPath.'/'.$fileName); + } + + /** + * getEditableExtensions returns a list of editable asset extensions. + * The list can be overridden with the cms.editableAssetTypes configuration option. + * @return array + */ + public static function getEditableExtensions() + { + $defaultTypes = ['js', 'jsx', 'css', 'sass', 'scss', 'less', 'php', 'htm', 'html', 'yaml', 'md', 'txt']; + + $configTypes = Config::get('rainlab.builder::editable_code_types'); + if (!$configTypes) { + return $defaultTypes; + } + + return $configTypes; + } +} diff --git a/plugins/rainlab/builder/models/ControllerModel.php b/plugins/rainlab/builder/models/ControllerModel.php new file mode 100644 index 0000000..cf42958 --- /dev/null +++ b/plugins/rainlab/builder/models/ControllerModel.php @@ -0,0 +1,572 @@ + ['regex:/^[A-Z]+[a-zA-Z0-9_]+$/'] + ]; + + /** + * load + */ + public function load($controller) + { + if (!$this->validateFileName($controller)) { + throw new SystemException("Invalid controller file name: {$controller}"); + } + + $this->controller = $this->trimExtension($controller); + $this->loadControllerBehaviors(); + $this->exists = true; + } + + /** + * save + */ + public function save() + { + if (!$this->controllerName) { + $this->controllerName = $this->controller; + } + + if ($this->isNewModel()) { + $this->generateController(); + } + else { + $this->saveController(); + } + } + + /** + * fill + */ + public function fill(array $attributes) + { + parent::fill($attributes); + + if (is_array($this->behaviors)) { + // Convert [1,2,3] to [1=>[], 2=>[], 3=>[]] + if (Arr::isList($this->behaviors)) { + $this->behaviors = array_combine($this->behaviors, array_fill(0, count($this->behaviors), [])); + } + + foreach ($this->behaviors as $class => &$configuration) { + if (is_scalar($configuration)) { + $configuration = json_decode($configuration, true); + } + } + } + } + + /** + * listPluginControllers + */ + public static function listPluginControllers($pluginCodeObj) + { + $controllersDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/controllers'; + + $controllersDirectoryPath = File::symbolizePath($controllersDirectoryPath); + + if (!File::isDirectory($controllersDirectoryPath)) { + return []; + } + + $result = []; + foreach (new DirectoryIterator($controllersDirectoryPath) as $fileInfo) { + if ($fileInfo->isDir()) { + continue; + } + + if ($fileInfo->getExtension() !== 'php') { + continue; + } + + $result[] = $fileInfo->getBasename('.php'); + } + + return $result; + } + + /** + * getBaseModelClassNameOptions + */ + public function getBaseModelClassNameOptions() + { + $models = ModelModel::listPluginModels($this->getPluginCodeObj()); + + $result = []; + foreach ($models as $model) { + $result[$model->className] = $model->className; + } + + return $result; + } + + /** + * getBehaviorsOptions + */ + public function getBehaviorsOptions() + { + $library = ControllerBehaviorLibrary::instance(); + $behaviors = $library->listBehaviors(); + + $result = []; + foreach ($behaviors as $behaviorClass => $behaviorInfo) { + $result[$behaviorClass] = [ + $behaviorInfo['name'], + $behaviorInfo['description'] + ]; + } + + // Support for this is added via import tool + unset($result[\Backend\Behaviors\ImportExportController::class]); + + return $result; + } + + /** + * getPermissionsOptions + */ + public function getPermissionsOptions() + { + $model = new PermissionsModel(); + + $model->loadPlugin($this->getPluginCodeObj()->toCode()); + + $result = []; + + foreach ($model->permissions as $permissionInfo) { + if (!isset($permissionInfo['label']) || !isset($permissionInfo['permission'])) { + continue; + } + + $result[$permissionInfo['permission']] = Lang::get($permissionInfo['label']); + } + + return $result; + } + + /** + * getMenuItemOptions + */ + public function getMenuItemOptions() + { + $model = new MenusModel(); + + $model->loadPlugin($this->getPluginCodeObj()->toCode()); + + $result = []; + + foreach ($model->menus as $itemInfo) { + if (!isset($itemInfo['label']) || !isset($itemInfo['code'])) { + continue; + } + + $itemCode = $itemInfo['code']; + $result[$itemCode] = Lang::get($itemInfo['label']); + + if (!isset($itemInfo['sideMenu'])) { + continue; + } + + foreach ($itemInfo['sideMenu'] as $itemInfo) { + if (!isset($itemInfo['label']) || !isset($itemInfo['code'])) { + continue; + } + + $subItemCode = $itemInfo['code']; + + $result[$itemCode.'||'.$subItemCode] = str_repeat(' ', 4).Lang::get($itemInfo['label']); + } + } + + return $result; + } + + /** + * getControllerFilePath + */ + public function getControllerFilePath($controllerFilesDirectory = false) + { + $pluginCodeObj = $this->getPluginCodeObj(); + $controllersDirectoryPath = File::symbolizePath($pluginCodeObj->toPluginDirectoryPath().'/controllers'); + + if (!$controllerFilesDirectory) { + return $controllersDirectoryPath.'/'.$this->controller.'.php'; + } + + return $controllersDirectoryPath.'/'.strtolower($this->controller); + } + + /** + * getPluginRegistryData + */ + public static function getPluginRegistryData($pluginCode, $subtype) + { + $pluginCodeObj = new PluginCode($pluginCode); + $urlBase = $pluginCodeObj->toUrl().'/'; + + $controllers = self::listPluginControllers($pluginCodeObj); + $result = []; + + foreach ($controllers as $controller) { + $controllerPath = strtolower(basename($controller)); + + $url = $urlBase.$controllerPath; + + $result[$url] = $url; + } + + return $result; + } + + /** + * saveController + */ + protected function saveController() + { + $this->validate(); + + $controllerPath = $this->getControllerFilePath(); + if (!File::isFile($controllerPath)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_controller_not_found')); + } + + if (!is_array($this->behaviors)) { + throw new SystemException('The behaviors data should be an array.'); + } + + $fileContents = File::get($controllerPath); + + $parser = new ControllerFileParser($fileContents); + + $behaviors = $parser->listBehaviors(); + if (!$behaviors) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_controller_has_no_behaviors')); + } + + $library = ControllerBehaviorLibrary::instance(); + foreach ($behaviors as $behaviorClass) { + $behaviorInfo = $library->getBehaviorInfo($behaviorClass); + + if (!$behaviorInfo) { + continue; + } + + $propertyName = $behaviorInfo['configPropertyName']; + $propertyValue = $parser->getStringPropertyValue($propertyName); + if (!strlen($propertyValue)) { + continue; + } + + if (array_key_exists($behaviorClass, $this->behaviors)) { + $this->saveBehaviorConfiguration($propertyValue, $this->behaviors[$behaviorClass], $behaviorClass); + } + } + } + + /** + * generateController + */ + protected function generateController() + { + $this->validationMessages = [ + 'controller.regex' => Lang::get('rainlab.builder::lang.controller.error_controller_name_invalid') + ]; + + $this->validationRules['controller'][] = 'required'; + + $this->validate(); + + $generator = new ControllerGenerator($this); + $generator->generate(); + } + + /** + * loadControllerBehaviors + */ + protected function loadControllerBehaviors() + { + $filePath = $this->getControllerFilePath(); + if (!File::isFile($filePath)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_controller_not_found')); + } + + $fileContents = File::get($filePath); + + $parser = new ControllerFileParser($fileContents); + + $behaviors = $parser->listBehaviors(); + if (!$behaviors) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_controller_has_no_behaviors')); + } + + $library = ControllerBehaviorLibrary::instance(); + $this->behaviors = []; + foreach ($behaviors as $behaviorClass) { + $behaviorInfo = $library->getBehaviorInfo($behaviorClass); + if (!$behaviorInfo) { + continue; + } + + $propertyName = $behaviorInfo['configPropertyName']; + $propertyValue = $parser->getStringPropertyValue($propertyName); + if (!strlen($propertyValue)) { + continue; + } + + $configuration = $this->loadBehaviorConfiguration($propertyValue, $behaviorClass); + if ($configuration === false) { + continue; + } + + $this->behaviors[$behaviorClass] = $configuration; + } + } + + /** + * loadBehaviorConfiguration + */ + protected function loadBehaviorConfiguration($fileName, $behaviorClass) + { + if (!preg_match('/^[a-z0-9\.\-_]+$/i', $fileName)) { + return false; + } + + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + if (strlen($extension) && $extension != 'yaml') { + return false; + } + + $controllerPath = $this->getControllerFilePath(true); + $filePath = $controllerPath.'/'.$fileName; + + if (!File::isFile($filePath)) { + return false; + } + + try { + $configuration = Yaml::parse(File::get($filePath)); + if ($behaviorClass === \Backend\Behaviors\ImportExportController::class) { + $this->processImportExportBehaviorConfig($configuration, true); + } + return $configuration; + } + catch (Exception $ex) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_invalid_yaml_configuration', ['file'=>$fileName])); + } + } + + /** + * saveBehaviorConfiguration + */ + protected function saveBehaviorConfiguration($fileName, $configuration, $behaviorClass) + { + if (!preg_match('/^[a-z0-9\.\-_]+$/i', $fileName)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_invalid_config_file_name', ['file'=>$fileName, 'class'=>$behaviorClass])); + } + + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + if (strlen($extension) && $extension != 'yaml') { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_file_not_yaml', ['file'=>$fileName, 'class'=>$behaviorClass])); + } + + $controllerPath = $this->getControllerFilePath(true); + $filePath = $controllerPath.'/'.$fileName; + + $fileDirectory = dirname($filePath); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory])); + } + } + + if ($behaviorClass === \Backend\Behaviors\ImportExportController::class) { + $this->processImportExportBehaviorConfig($configuration); + } + elseif ($behaviorClass === \Backend\Behaviors\ListController::class) { + $this->processListBehaviorConfig($configuration); + } + + if ($configuration !== null) { + $yamlData = Yaml::render($configuration); + } + else { + $yamlData = ''; + } + + if (@File::put($filePath, $yamlData) === false) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.yaml.save_error', ['name'=>$filePath])); + } + + @File::chmod($filePath); + } + + /** + * trimExtension + */ + protected function trimExtension($fileName) + { + if (substr($fileName, -4) == '.php') { + return substr($fileName, 0, -4); + } + + return $fileName; + } + + /** + * validateFileName + */ + protected function validateFileName($fileName) + { + if (!preg_match('/^[a-z0-9\.\-_]+$/i', $fileName)) { + return false; + } + + $extension = pathinfo($fileName, PATHINFO_EXTENSION); + if (strlen($extension) && $extension != 'php') { + return false; + } + + return true; + } + + /** + * processListBehaviorConfig converts booleans + */ + protected function processListBehaviorConfig(array &$configuration, $isLoad = false) + { + if (!isset($configuration['structure'])) { + return; + } + + $booleanFields = [ + 'showTree', + 'treeExpanded', + 'showReorder', + 'showSorting', + 'dragRow', + ]; + + foreach ($booleanFields as $booleanField) { + if (!array_key_exists($booleanField, $configuration['structure'])) { + continue; + } + + $value = $configuration['structure'][$booleanField]; + if ($value == '1' || $value == 'true') { + $value = true; + } + else { + $value = false; + } + + + $configuration['structure'][$booleanField] = $value; + } + + $numericFields = [ + 'maxDepth' + ]; + + foreach ($numericFields as $numericField) { + if (!array_key_exists($numericField, $configuration['structure'])) { + continue; + } + + $configuration['structure'][$numericField] = +$configuration['structure'][$numericField]; + } + } + + /** + * processImportExportBehaviorConfig converts import. and export. keys to and from their config + */ + protected function processImportExportBehaviorConfig(array &$configuration, $isLoad = false) + { + if ($isLoad) { + foreach ($configuration as $key => $value) { + if (!is_array($value) || !in_array($key, ['import', 'export'])) { + continue; + } + + foreach ($value as $k => $v) { + $configuration[$key.'.'.$k] = $v; + } + + unset($configuration[$key]); + } + } + else { + foreach ($configuration as $key => $value) { + if (starts_with($key, ['import.', 'export.'])) { + array_set($configuration, $key, array_pull($configuration, $key)); + } + } + } + } +} diff --git a/plugins/rainlab/builder/models/DatabaseTableModel.php b/plugins/rainlab/builder/models/DatabaseTableModel.php new file mode 100644 index 0000000..b7d243d --- /dev/null +++ b/plugins/rainlab/builder/models/DatabaseTableModel.php @@ -0,0 +1,477 @@ + ['required', 'regex:/^[a-z]+[a-z0-9_]+$/', 'tablePrefix', 'uniqueTableName', 'max:64'] + ]; + + /** + * @var \Doctrine\DBAL\Schema\Table Table details loaded from the database. + */ + protected $tableInfo; + + /** + * @var \Doctrine\DBAL\Schema\AbstractSchemaManager Contains the database schema + */ + protected static $schemaManager = null; + + /** + * @var \Doctrine\DBAL\Schema\Schema Contains the database schema + */ + protected static $schema = null; + + /** + * listPluginTables + */ + public static function listPluginTables($pluginCode) + { + $pluginCodeObj = new PluginCode($pluginCode); + $prefix = $pluginCodeObj->toDatabasePrefix(true); + + $tables = self::getSchemaManager()->listTableNames(); + + $foundTables = array_filter($tables, function ($item) use ($prefix) { + return Str::startsWith($item, $prefix); + }); + + $unprefixedTables = array_map(function($table) { + return substr($table, mb_strlen(Db::getTablePrefix())); + }, $foundTables); + + return $unprefixedTables; + } + + /** + * tableExists + */ + public static function tableExists($name) + { + return self::getSchema()->hasTable(Db::getTablePrefix() . $name); + } + + /** + * Loads the table from the database. + * @param string $name Specifies the table name. + */ + public function load($name) + { + if (!self::tableExists($name)) { + throw new SystemException(sprintf('The table with name %s doesn\'t exist', $name)); + } + + $schema = self::getSchemaManager()->createSchema(); + + $this->name = $name; + $this->tableInfo = $schema->getTable($this->getFullTableName()); + $this->loadColumnsFromTableInfo(); + $this->exists = true; + } + + /** + * getFullTableName + */ + protected function getFullTableName() + { + return Db::getTablePrefix() . $this->name; + } + + /** + * validate + */ + public function validate() + { + $pluginDbPrefix = $this->getPluginCodeObj()->toDatabasePrefix(); + + if (!strlen($pluginDbPrefix)) { + throw new SystemException('Error saving the table model - the plugin database prefix is not set for the object.'); + } + + $prefix = $pluginDbPrefix.'_'; + + $this->validationMessages = [ + 'name.table_prefix' => Lang::get('rainlab.builder::lang.database.error_table_name_invalid_prefix', [ + 'prefix' => $prefix + ]), + 'name.regex' => Lang::get('rainlab.builder::lang.database.error_table_name_invalid_characters'), + 'name.unique_table_name' => Lang::get('rainlab.builder::lang.database.error_table_already_exists', ['name'=>$this->name]), + 'name.max' => Lang::get('rainlab.builder::lang.database.error_table_name_too_long') + ]; + + Validator::extend('tablePrefix', function ($attribute, $value, $parameters) use ($prefix) { + $value = trim($value); + + if (!Str::startsWith($value, $prefix)) { + return false; + } + + return true; + }); + + Validator::extend('uniqueTableName', function ($attribute, $value, $parameters) { + $value = trim($value); + + $schema = $this->getSchema(); + if ($this->isNewModel()) { + return !$schema->hasTable($value); + } + + if ($value != $this->tableInfo->getName()) { + return !$schema->hasTable($value); + } + + return true; + }); + + $this->validateColumns(); + + return parent::validate(); + } + + /** + * generateCreateOrUpdateMigration + */ + public function generateCreateOrUpdateMigration() + { + $schemaCreator = new DatabaseTableSchemaCreator(); + $existingSchema = $this->tableInfo; + $newTableName = $this->name; + $tableName = $existingSchema ? $existingSchema->getName() : $this->name; + + $newSchema = $schemaCreator->createTableSchema($tableName, $this->columns); + + $codeGenerator = new TableMigrationCodeGenerator(); + $migrationCode = $codeGenerator->createOrUpdateTable($newSchema, $existingSchema, $newTableName); + if ($migrationCode === false) { + return $migrationCode; + } + + $description = $existingSchema ? 'Updated table %s' : 'Created table %s'; + return $this->createMigrationObject($migrationCode, sprintf($description, $tableName)); + } + + /** + * generateDropMigration + */ + public function generateDropMigration() + { + $existingSchema = $this->tableInfo; + $codeGenerator = new TableMigrationCodeGenerator(); + $migrationCode = $codeGenerator->dropTable($existingSchema); + + return $this->createMigrationObject($migrationCode, sprintf('Drop table %s', $this->name)); + } + + /** + * getSchema + */ + public static function getSchema() + { + if (!self::$schema) { + self::$schema = self::getSchemaManager()->createSchema(); + } + + return self::$schema; + } + + /** + * validateColumns + */ + protected function validateColumns() + { + $this->validateColumnNameLengths(); + $this->validateDuplicateColumns(); + $this->validateDuplicatePrimaryKeys(); + $this->validateAutoIncrementColumns(); + $this->validateColumnsLengthParameter(); + $this->validateUnsignedColumns(); + $this->validateDefaultValues(); + } + + /** + * validateColumnNameLengths + */ + protected function validateColumnNameLengths() + { + foreach ($this->columns as $column) { + $name = trim($column['name']); + + if (Str::length($name) > 64) { + throw new ValidationException([ + 'columns' => Lang::get( + 'rainlab.builder::lang.database.error_column_name_too_long', + ['column' => $name] + ) + ]); + } + } + } + + /** + * validateDuplicateColumns + */ + protected function validateDuplicateColumns() + { + foreach ($this->columns as $outerIndex => $outerColumn) { + foreach ($this->columns as $innerIndex => $innerColumn) { + if ($innerIndex != $outerIndex && $innerColumn['name'] == $outerColumn['name']) { + throw new ValidationException([ + 'columns' => Lang::get( + 'rainlab.builder::lang.database.error_table_duplicate_column', + ['column' => $outerColumn['name']] + ) + ]); + } + } + } + } + + /** + * validateDuplicatePrimaryKeys + */ + protected function validateDuplicatePrimaryKeys() + { + $keysFound = 0; + $autoIncrementsFound = 0; + foreach ($this->columns as $column) { + if ($column['primary_key']) { + $keysFound++; + } + + if ($column['auto_increment']) { + $autoIncrementsFound++; + } + } + + if ($keysFound > 1 && $autoIncrementsFound) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_table_auto_increment_in_compound_pk') + ]); + } + } + + /** + * validateAutoIncrementColumns + */ + protected function validateAutoIncrementColumns() + { + $autoIncrement = null; + foreach ($this->columns as $column) { + if (!$column['auto_increment']) { + continue; + } + + if ($autoIncrement) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_table_mutliple_auto_increment') + ]); + } + + $autoIncrement = $column; + } + + if (!$autoIncrement) { + return; + } + + if (!in_array($autoIncrement['type'], MigrationColumnType::getIntegerTypes())) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_table_auto_increment_non_integer') + ]); + } + } + + /** + * validateUnsignedColumns + */ + protected function validateUnsignedColumns() + { + foreach ($this->columns as $column) { + if (!$column['unsigned']) { + continue; + } + + if (!in_array($column['type'], MigrationColumnType::getIntegerTypes())) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_unsigned_type_not_int', ['column'=>$column['name']]) + ]); + } + } + } + + /** + * validateColumnsLengthParameter + */ + protected function validateColumnsLengthParameter() + { + foreach ($this->columns as $column) { + try { + MigrationColumnType::validateLength($column['type'], $column['length']); + } + catch (Exception $ex) { + throw new ValidationException([ + 'columns' => $ex->getMessage() + ]); + } + } + } + + /** + * validateDefaultValues + */ + protected function validateDefaultValues() + { + foreach ($this->columns as $column) { + if (!strlen($column['default'])) { + continue; + } + + $default = trim($column['default']); + + if (in_array($column['type'], MigrationColumnType::getIntegerTypes())) { + if (!preg_match('/^\-?[0-9]+$/', $default)) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_integer_default_value', ['column'=>$column['name']]) + ]); + } + + if ($column['unsigned'] && $default < 0) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_unsigned_negative_value', ['column'=>$column['name']]) + ]); + } + + continue; + } + + if (in_array($column['type'], MigrationColumnType::getDecimalTypes())) { + if (!preg_match('/^\-?([0-9]+\.[0-9]+|[0-9]+)$/', $default)) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_decimal_default_value', ['column'=>$column['name']]) + ]); + } + + continue; + } + + if ($column['type'] == MigrationColumnType::TYPE_BOOLEAN) { + if (!preg_match('/^0|1|true|false$/i', $default)) { + throw new ValidationException([ + 'columns' => Lang::get('rainlab.builder::lang.database.error_boolean_default_value', ['column'=>$column['name']]) + ]); + } + } + } + } + + /** + * getSchemaManager + */ + protected static function getSchemaManager() + { + if (!self::$schemaManager) { + self::$schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); + + Type::addType('enumdbtype', \RainLab\Builder\Classes\EnumDbType::class); + + // Fixes the problem with enum column type not supported + // by Doctrine (https://github.com/laravel/framework/issues/1346) + $platform = self::$schemaManager->getDatabasePlatform(); + $platform->registerDoctrineTypeMapping('enum', 'enumdbtype'); + $platform->registerDoctrineTypeMapping('json', 'text'); + } + + return self::$schemaManager; + } + + /** + * loadColumnsFromTableInfo + */ + protected function loadColumnsFromTableInfo() + { + $this->columns = []; + $columns = $this->tableInfo->getColumns(); + + $primaryKey = $this->tableInfo->getPrimaryKey(); + $primaryKeyColumns =[]; + if ($primaryKey) { + $primaryKeyColumns = $primaryKey->getColumns(); + } + + foreach ($columns as $column) { + $columnName = $column->getName(); + $typeName = $column->getType()->getName(); + + if ($typeName == EnumDbType::TYPENAME) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_enum_not_supported')); + } + + $item = [ + 'name' => $columnName, + 'type' => MigrationColumnType::toMigrationMethodName($typeName, $columnName), + 'length' => MigrationColumnType::doctrineLengthToMigrationLength($column), + 'unsigned' => $column->getUnsigned(), + 'allow_null' => !$column->getNotnull(), + 'auto_increment' => $column->getAutoincrement(), + 'primary_key' => in_array($columnName, $primaryKeyColumns), + 'default' => $column->getDefault(), + 'comment' => $column->getComment(), + 'id' => $columnName, + ]; + + $this->columns[] = $item; + } + } + + /** + * createMigrationObject + */ + protected function createMigrationObject($code, $description) + { + $migration = new MigrationModel(); + $migration->setPluginCodeObj($this->getPluginCodeObj()); + + $migration->code = $code; + $migration->version = $migration->getNextVersion(); + $migration->description = $description; + + return $migration; + } +} diff --git a/plugins/rainlab/builder/models/ImportsModel.php b/plugins/rainlab/builder/models/ImportsModel.php new file mode 100644 index 0000000..564488a --- /dev/null +++ b/plugins/rainlab/builder/models/ImportsModel.php @@ -0,0 +1,270 @@ +blueprints)) { + foreach ($this->blueprints as &$configuration) { + if (is_scalar($configuration)) { + $configuration = json_decode($configuration, true); + } + } + } + } + + /** + * setBlueprintContext + */ + public function setBlueprintContext($blueprint, $config) + { + $this->activeBlueprint = $blueprint; + $this->activeConfig = $config; + } + + /** + * getBlueprintObject + */ + public function getBlueprintObject() + { + return $this->activeBlueprint; + } + + /** + * getBlueprintConfig + */ + public function getBlueprintConfig($name = null, $default = null) + { + if ($name === null) { + return $this->activeConfig; + } + + return array_key_exists($name, $this->activeConfig) + ? $this->activeConfig[$name] + : $default; + } + + /** + * loadPlugin + */ + public function loadPlugin($pluginCode) + { + $this->pluginName = $pluginCode; + } + + /** + * getPluginName + */ + public function getPluginName() + { + return Lang::get($this->pluginName); + } + + /** + * import runs the import + */ + public function import() + { + if (!$this->blueprints || !is_array($this->blueprints)) { + throw new ApplicationException(__("There are no blueprints to import, please select a blueprint and try again.")); + } + + $generator = new BlueprintGenerator($this); + $generator->generate(); + } + + /** + * inspect a blueprint before import + */ + public function inspect($blueprint): array + { + if (!$this->blueprints || !is_array($this->blueprints)) { + return []; + } + + $generator = new BlueprintGenerator($this); + + return $generator->inspect($blueprint); + } + + /** + * getBlueprintUuidOptions + */ + public function getBlueprintUuidOptions() + { + $result = []; + + foreach (EntryBlueprint::listInProject() as $blueprint) { + if (!isset($this->blueprints[$blueprint->uuid])) { + $result[$blueprint->uuid] = $blueprint->handle; + } + } + + foreach (GlobalBlueprint::listInProject() as $blueprint) { + if (!isset($this->blueprints[$blueprint->uuid])) { + $result[$blueprint->uuid] = $blueprint->handle; + } + } + + asort($result); + + return $result; + } + + /** + * getPluginFilePath + */ + public function getPluginFilePath($path) + { + $pluginDir = $this->getPluginCodeObj()->toPluginDirectoryPath(); + + return File::symbolizePath("{$pluginDir}/{$path}"); + } + + /** + * getPluginVersionInformation + */ + public function getPluginVersionInformation() + { + $versionObj = new PluginVersion; + + return $versionObj->getPluginVersionInformation($this->getPluginCodeObj()); + } + + /** + * getBlueprintFieldset + */ + public function getBlueprintFieldset($blueprint = null) + { + $blueprint = $blueprint ?: $this->getBlueprintObject(); + + $uuid = $blueprint->uuid ?? '???'; + + $fieldset = BlueprintIndexer::instance()->findContentFieldset($uuid); + if (!$fieldset) { + throw new ApplicationException("Unable to find content fieldset definition with UUID of '{$uuid}'."); + } + + return $fieldset; + } + + /** + * useListController + */ + public function useListController(): bool + { + if ( + $this->activeBlueprint instanceof SingleBlueprint || + $this->activeBlueprint instanceof GlobalBlueprint + ) { + return false; + } + + return true; + } + + /** + * useSettingModel + */ + public function useSettingModel(): bool + { + if ($this->activeBlueprint instanceof GlobalBlueprint) { + return true; + } + + return false; + } + + /** + * wantsDatabaseMigration + */ + public function wantsDatabaseMigration(): bool + { + if ($this->useSettingModel()) { + return false; + } + + return true; + } + + /** + * useMultisite + */ + public function useMultisite() + { + return $this->activeBlueprint->useMultisite(); + } + + /** + * useStructure + */ + public function useStructure() + { + if ($this->activeBlueprint instanceof StructureBlueprint) { + return true; + } + + return false; + } +} diff --git a/plugins/rainlab/builder/models/LocalizationModel.php b/plugins/rainlab/builder/models/LocalizationModel.php new file mode 100644 index 0000000..8d37458 --- /dev/null +++ b/plugins/rainlab/builder/models/LocalizationModel.php @@ -0,0 +1,439 @@ + ['required', 'regex:/^[a-z0-9\.\-]+$/i'] + ]; + + protected $originalStringArray = []; + + public function load($language) + { + $this->language = $language; + + $this->originalLanguage = $language; + + $filePath = $this->getFilePath(); + + if (!File::isFile($filePath)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_cant_load_file')); + } + + if (!$this->validateFileContents($filePath)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_bad_localization_file_contents')); + } + + $strings = include($filePath); + if (!is_array($strings)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_file_not_array')); + } + + $this->originalStringArray = $strings; + + if (count($strings) > 0) { + $dumper = new YamlDumper(); + $this->strings = $dumper->dump($strings, 20, 0, false, true); + } + else { + $this->strings = ''; + } + + $this->exists = true; + } + + public static function initModel($pluginCode, $language) + { + $model = new self(); + $model->setPluginCode($pluginCode); + $model->language = $language; + + return $model; + } + + public function save() + { + $data = $this->modelToLanguageFile(); + $this->validate(); + + $filePath = File::symbolizePath($this->getFilePath()); + $isNew = $this->isNewModel(); + + if (File::isFile($filePath)) { + if ($isNew || $this->originalLanguage != $this->language) { + throw new ValidationException(['fileName' => Lang::get('rainlab.builder::lang.common.error_file_exists', ['path'=>$this->language.'/'.basename($filePath)])]); + } + } + + $fileDirectory = dirname($filePath); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory])); + } + } + + if (@File::put($filePath, $data) === false) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.save_error', ['name'=>$filePath])); + } + + @File::chmod($filePath); + + if (!$this->isNewModel() && strlen($this->originalLanguage) > 0 && $this->originalLanguage != $this->language) { + $this->originalFilePath = $this->getFilePath($this->originalLanguage); + @File::delete($this->originalFilePath); + } + + $this->originalLanguage = $this->language; + $this->exists = true; + } + + public function deleteModel() + { + if ($this->isNewModel()) { + throw new ApplicationException('Cannot delete language file which is not saved yet.'); + } + + $filePath = File::symbolizePath($this->getFilePath()); + if (File::isFile($filePath)) { + if (!@unlink($filePath)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_delete_file')); + } + } + } + + public function initContent() + { + $templatePath = '$/rainlab/builder/models/localizationmodel/templates/lang.php'; + $templatePath = File::symbolizePath($templatePath); + + $strings = include($templatePath); + $dumper = new YamlDumper(); + $this->strings = $dumper->dump($strings, 20, 0, false, true); + } + + public static function listPluginLanguages($pluginCodeObj) + { + $languagesDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/lang'; + + $languagesDirectoryPath = File::symbolizePath($languagesDirectoryPath); + + if (!File::isDirectory($languagesDirectoryPath)) { + return []; + } + + $result = []; + foreach (new DirectoryIterator($languagesDirectoryPath) as $fileInfo) { + if (!$fileInfo->isDir() || $fileInfo->isDot()) { + continue; + } + + $langFilePath = $fileInfo->getPathname().'/lang.php'; + + if (File::isFile($langFilePath)) { + $result[] = $fileInfo->getFilename(); + } + } + + return $result; + } + + public function copyStringsFrom($destinationText, $sourceLanguageCode) + { + $sourceLanguageModel = new self(); + $sourceLanguageModel->setPluginCodeObj($this->getPluginCodeObj()); + $sourceLanguageModel->load($sourceLanguageCode); + + $srcArray = $sourceLanguageModel->getOriginalStringsArray(); + + $languageMixer = new LanguageMixer(); + + return $languageMixer->addStringsFromAnotherLanguage($destinationText, $srcArray); + } + + public function getOriginalStringsArray() + { + return $this->originalStringArray; + } + + public function createStringAndSave($stringKey, $stringValue) + { + $stringKey = trim($stringKey, '.'); + + if (!strlen($stringKey)) { + throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_empty')]); + } + + if (!strlen($stringValue)) { + throw new ValidationException(['value' => Lang::get('rainlab.builder::lang.localization.string_value_is_empty')]); + } + + $originalStringArray = $this->getOriginalStringsArray(); + $languagePrefix = strtolower($this->getPluginCodeObj()->toCode()).'::lang.'; + + $existingStrings = self::convertToStringsArray($originalStringArray, $languagePrefix); + if (array_key_exists($languagePrefix.$stringKey, $existingStrings)) { + throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]); + } + + $existingSections = self::convertToSectionsArray($originalStringArray); + if (array_key_exists($stringKey.'.', $existingSections)) { + throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]); + } + + $sectionArray = []; + self::createStringSections($sectionArray, $stringKey, $stringValue) ; + + $this->checkKeyWritable($stringKey, $existingStrings, $languagePrefix); + $newStrings = LanguageMixer::arrayMergeRecursive($originalStringArray, $sectionArray); + + $dumper = new YamlDumper(); + $this->strings = $dumper->dump($newStrings, 20, 0, false, true); + + $this->save(); + + return $languagePrefix.$stringKey; + } + + public static function getDefaultLanguage() + { + $language = Config::get('app.locale'); + + if (!$language) { + throw new ApplicationException('The default language is not defined in the application configuration (app.locale).'); + } + + return $language; + } + + public static function getPluginRegistryData($pluginCode, $subtype) + { + $defaultLanguage = self::getDefaultLanguage(); + + $model = new self(); + $model->setPluginCode($pluginCode); + $model->language = $defaultLanguage; + + $filePath = $model->getFilePath(); + if (!File::isFile($filePath)) { + return []; + } + + $model->load($defaultLanguage); + + $array = $model->getOriginalStringsArray(); + $languagePrefix = strtolower($model->getPluginCodeObj()->toCode()).'::lang.'; + + if ($subtype !== 'sections') { + return self::convertToStringsArray($array, $languagePrefix); + } + + return self::convertToSectionsArray($array); + } + + public static function languageFileExists($pluginCode, $language) + { + $model = new self(); + $model->setPluginCode($pluginCode); + $model->language = $language; + + $filePath = $model->getFilePath(); + return File::isFile($filePath); + } + + protected static function createStringSections(&$arr, $path, $value) + { + $keys = explode('.', $path); + + while ($key = array_shift($keys)) { + $arr = &$arr[$key]; + } + + $arr = $value; + } + + protected static function convertToStringsArray($stringsArray, $prefix, $currentKey = '') + { + $result = []; + + foreach ($stringsArray as $key => $value) { + $newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key; + + if (is_scalar($value)) { + $result[$prefix.$newKey] = $value; + } + else { + $result = array_merge($result, self::convertToStringsArray($value, $prefix, $newKey)); + } + } + + return $result; + } + + protected static function convertToSectionsArray($stringsArray, $currentKey = '') + { + $result = []; + + foreach ($stringsArray as $key => $value) { + $newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key; + + if (is_scalar($value)) { + $result[$currentKey.'.'] = $currentKey.'.'; + } + else { + $result = array_merge($result, self::convertToSectionsArray($value, $newKey)); + } + } + + return $result; + } + + protected function validateLanguage($language) + { + return preg_match('/^[a-z0-9\.\-]+$/i', $language); + } + + protected function getFilePath($language = null) + { + if ($language === null) { + $language = $this->language; + } + + $language = trim($language); + + if (!strlen($language)) { + throw new SystemException('The form model language is not set.'); + } + + if (!$this->validateLanguage($language)) { + throw new SystemException('Invalid language file name: '.$language); + } + + $path = $this->getPluginCodeObj()->toPluginDirectoryPath().'/lang/'.$language.'/lang.php'; + return File::symbolizePath($path); + } + + protected function modelToLanguageFile() + { + $this->strings = trim($this->strings); + + if (!strlen($this->strings)) { + return "getSanitizedPHPStrings(Yaml::parse($this->strings)); + + $phpData = var_export($data, true); + $phpData = preg_replace('/^(\s+)\),/m', '$1],', $phpData); + $phpData = preg_replace('/^(\s+)array\s+\(/m', '$1[', $phpData); + $phpData = preg_replace_callback('/^(\s+)/m', function ($matches) { + return str_repeat($matches[1], 2); // Increase indentation + }, $phpData); + $phpData = preg_replace('/\n\s+\[/m', '[', $phpData); + $phpData = preg_replace('/^array\s\(/', '[', $phpData); + $phpData = preg_replace('/^\)\Z/m', ']', $phpData); + + return "getMessage())); + } + } + + protected function validateFileContents($path) + { + $fileContents = File::get($path); + + $stream = new PhpSourceStream($fileContents); + + $invalidTokens = [ + T_CLASS, + T_FUNCTION, + T_INCLUDE, + T_INCLUDE_ONCE, + T_REQUIRE, + T_REQUIRE_ONCE, + T_EVAL, + T_ECHO, + T_GOTO, + T_HALT_COMPILER, + T_STRING // Unescaped strings - function names, etc. + ]; + + while ($stream->forward()) { + $tokenCode = $stream->getCurrentCode(); + + if (in_array($tokenCode, $invalidTokens)) { + return false; + } + } + + return true; + } + + protected function getSanitizedPHPStrings($strings) + { + array_walk_recursive($strings, function (&$item, $key) { + if (!is_scalar($item)) { + return; + } + + // In YAML single quotes are escaped with two single quotes + // http://yaml.org/spec/current.html#id2534365 + $item = str_replace("''", "'", $item); + }); + + return $strings; + } + + protected function checkKeyWritable($stringKey, $existingStrings, $languagePrefix) + { + $sectionList = explode('.', $stringKey); + + $lastElement = array_pop($sectionList); + while (strlen($lastElement)) { + if (count($sectionList) > 0) { + $fullKey = implode('.', $sectionList).'.'.$lastElement; + } + else { + $fullKey = $lastElement; + } + + if (array_key_exists($languagePrefix.$fullKey, $existingStrings)) { + throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_a_string', ['key'=>$fullKey])]); + } + + $lastElement = array_pop($sectionList); + } + } +} diff --git a/plugins/rainlab/builder/models/MenusModel.php b/plugins/rainlab/builder/models/MenusModel.php new file mode 100644 index 0000000..1a661a6 --- /dev/null +++ b/plugins/rainlab/builder/models/MenusModel.php @@ -0,0 +1,242 @@ +menus as $mainMenuItem) { + $mainMenuItem = $this->trimMenuProperties($mainMenuItem); + + if (!isset($mainMenuItem['code'])) { + throw new ApplicationException('Cannot save menus - the main menu item code should not be empty.'); + } + + if (isset($mainMenuItem['sideMenu'])) { + $sideMenuItems = []; + + foreach ($mainMenuItem['sideMenu'] as $sideMenuItem) { + $sideMenuItem = $this->trimMenuProperties($sideMenuItem); + + if (!isset($sideMenuItem['code'])) { + throw new ApplicationException('Cannot save menus - the side menu item code should not be empty.'); + } + + $code = $sideMenuItem['code']; + unset($sideMenuItem['code']); + + $sideMenuItems[$code] = $sideMenuItem; + } + + $mainMenuItem['sideMenu'] = $sideMenuItems; + } + + $code = $mainMenuItem['code']; + unset($mainMenuItem['code']); + + $fileMenus[$code] = $mainMenuItem; + } + + return $fileMenus; + } + + /** + * validate + */ + public function validate() + { + parent::validate(); + + $this->validateDuplicateMenus(); + } + + /** + * fill + */ + public function fill(array $attributes) + { + if (!is_array($attributes['menus'])) { + $attributes['menus'] = json_decode($attributes['menus'], true); + + if ($attributes['menus'] === null) { + throw new SystemException('Cannot decode menus JSON string.'); + } + } + + return parent::fill($attributes); + } + + /** + * setPluginCodeObj + */ + public function setPluginCodeObj($pluginCodeObj) + { + $this->pluginCodeObj = $pluginCodeObj; + } + + /** + * yamlArrayToModel loads the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $fileMenus = $array; + $menus = []; + $index = 0; + + foreach ($fileMenus as $code => $mainMenuItem) { + $mainMenuItem['code'] = $code; + + if (isset($mainMenuItem['sideMenu'])) { + $sideMenuItems = []; + + foreach ($mainMenuItem['sideMenu'] as $code => $sideMenuItem) { + $sideMenuItem['code'] = $code; + $sideMenuItems[] = $sideMenuItem; + } + + $mainMenuItem['sideMenu'] = $sideMenuItems; + } + + $menus[] = $mainMenuItem; + } + + $this->menus = $menus; + } + + /** + * trimMenuProperties + */ + protected function trimMenuProperties($menu) + { + array_walk($menu, function ($value, $key) { + if (!is_scalar($value)) { + return $value; + } + + return trim($value); + }); + + return $menu; + } + + /** + * getFilePath returns a file path to save the model to. + * @return string Returns a path. + */ + protected function getFilePath() + { + if ($this->pluginCodeObj === null) { + throw new SystemException('Error saving plugin menus model - the plugin code object is not set.'); + } + + return $this->pluginCodeObj->toPluginFilePath(); + } + + /** + * validateDuplicateMenus + */ + protected function validateDuplicateMenus() + { + foreach ($this->menus as $outerIndex => $mainMenuItem) { + $mainMenuItem = $this->trimMenuProperties($mainMenuItem); + + if (!isset($mainMenuItem['code'])) { + continue; + } + + if ($this->codeExistsInList($outerIndex, $mainMenuItem['code'], $this->menus)) { + throw new ValidationException([ + 'permissions' => Lang::get( + 'rainlab.builder::lang.menu.error_duplicate_main_menu_code', + ['code' => $mainMenuItem['code']] + ) + ]); + } + + if (isset($mainMenuItem['sideMenu'])) { + foreach ($mainMenuItem['sideMenu'] as $innerIndex => $sideMenuItem) { + $sideMenuItem = $this->trimMenuProperties($sideMenuItem); + + if (!isset($sideMenuItem['code'])) { + continue; + } + + if ($this->codeExistsInList($innerIndex, $sideMenuItem['code'], $mainMenuItem['sideMenu'])) { + throw new ValidationException([ + 'permissions' => Lang::get( + 'rainlab.builder::lang.menu.error_duplicate_side_menu_code', + ['code' => $sideMenuItem['code']] + ) + ]); + } + } + } + } + } + + /** + * codeExistsInList + */ + protected function codeExistsInList($codeIndex, $code, $list) + { + foreach ($list as $index => $item) { + if (!isset($item['code'])) { + continue; + } + + if ($index == $codeIndex) { + continue; + } + + if ($code == $item['code']) { + return true; + } + } + + return false; + } +} diff --git a/plugins/rainlab/builder/models/MigrationModel.php b/plugins/rainlab/builder/models/MigrationModel.php new file mode 100644 index 0000000..38e7aee --- /dev/null +++ b/plugins/rainlab/builder/models/MigrationModel.php @@ -0,0 +1,614 @@ + ['required', 'regex:/^[0-9]+\.[0-9]+\.[0-9]+$/', 'uniqueVersion'], + 'description' => ['required'], + 'scriptFileName' => ['regex:/^[a-z]+[a-z0-9_]+$/'] + ]; + + /** + * validate + */ + public function validate() + { + $isNewModel = $this->isNewModel(); + + $this->validationMessages = [ + 'version.regex' => Lang::get('rainlab.builder::lang.migration.error_version_invalid'), + 'version.unique_version' => Lang::get('rainlab.builder::lang.migration.error_version_exists'), + 'scriptFileName.regex' => Lang::get('rainlab.builder::lang.migration.error_script_filename_invalid') + ]; + + $versionInformation = $this->getPluginVersionInformation(); + + Validator::extend('uniqueVersion', function ($attribute, $value, $parameters) use ($versionInformation, $isNewModel) { + if ($isNewModel || $this->version != $this->originalVersion) { + return !array_key_exists($value, $versionInformation); + } + return true; + }); + + if (!$isNewModel && $this->version != $this->originalVersion && $this->isApplied()) { + throw new ValidationException([ + 'version' => Lang::get('rainlab.builder::lang.migration.error_cannot_change_version_number') + ]); + } + + return parent::validate(); + } + + /** + * getNextVersion + */ + public function getNextVersion() + { + $versionInformation = $this->getPluginVersionInformation(); + + if (!count($versionInformation)) { + return '1.0.0'; + } + + $versions = array_keys($versionInformation); + $latestVersion = end($versions); + + $versionNumbers = []; + if (!preg_match('/^([0-9]+)\.([0-9]+)\.([0-9]+)$/', $latestVersion, $versionNumbers)) { + throw new SystemException(sprintf('Cannot parse the latest plugin version number: %s.', $latestVersion)); + } + + return $versionNumbers[1].'.'.$versionNumbers[2].'.'.($versionNumbers[3]+1); + } + + /** + * save the migration and applies all outstanding migrations for the plugin. + */ + public function save($executeOnSave = true) + { + $this->validate(); + + if (!strlen($this->scriptFileName) || !$this->isNewModel()) { + $this->assignFileName(); + } + + $originalFileContents = $this->saveScriptFile(); + + try { + $originalVersionData = $this->insertOrUpdateVersion(); + } + catch (Exception $ex) { + // Remove the script file, but don't rollback + // the version.yaml. + $this->rollbackSaving(null, $originalFileContents); + + throw $ex; + } + + try { + if ($executeOnSave) { + VersionManager::instance()->updatePlugin($this->getPluginCodeObj()->toCode(), $this->version); + } + } + catch (Throwable $e) { + // Remove the script file, and rollback + // the version.yaml. + $this->rollbackSaving($originalVersionData, $originalFileContents); + + throw $e; + } + + $this->originalVersion = $this->version; + $this->exists = true; + } + + /** + * load + */ + public function load($versionNumber) + { + $versionNumber = trim($versionNumber); + + if (!strlen($versionNumber)) { + throw new ApplicationException('Cannot load the the version model - the version number should not be empty.'); + } + + $pluginVersions = $this->getPluginVersionInformation(); + if (!array_key_exists($versionNumber, $pluginVersions)) { + throw new ApplicationException('The requested version does not exist in the version information file.'); + } + + $this->version = $versionNumber; + $this->originalVersion = $this->version; + $this->exists = true; + + $versionInformation = $pluginVersions[$versionNumber]; + if (!is_array($versionInformation)) { + $this->description = $versionInformation; + } + else { + $cnt = count($versionInformation); + + if ($cnt > 2) { + throw new ApplicationException('The requested version cannot be edited with Builder as it refers to multiple PHP scripts.'); + } + + if ($cnt > 0) { + $this->description = $versionInformation[0]; + } + + if ($cnt > 1) { + $this->scriptFileName = pathinfo(trim($versionInformation[1]), PATHINFO_FILENAME); + $this->code = $this->loadScriptFile(); + } + } + + $this->originalScriptFileName = $this->scriptFileName; + } + + /** + * initVersion + */ + public function initVersion($versionType) + { + $versionTypes = ['migration', 'seeder', 'custom']; + + if (!in_array($versionType, $versionTypes)) { + throw new SystemException('Unknown version type.'); + } + + $this->version = $this->getNextVersion(); + + if ($versionType == 'custom') { + $this->scriptFileName = null; + return; + } + + $templateFiles = [ + 'migration' => 'migration.php.tpl', + 'seeder' => 'seeder.php.tpl' + ]; + + $templatePath = '$/rainlab/builder/models/migrationmodel/templates/'.$templateFiles[$versionType]; + $templatePath = File::symbolizePath($templatePath); + + $fileContents = File::get($templatePath); + $scriptFileName = $versionType.str_replace('.', '-', $this->version); + + $pluginCodeObj = $this->getPluginCodeObj(); + $this->code = TextParser::parse($fileContents, [ + 'className' => Str::studly($scriptFileName), + 'namespace' => $pluginCodeObj->toUpdatesNamespace(), + 'tableNamePrefix' => $pluginCodeObj->toDatabasePrefix() + ]); + + $this->scriptFileName = $scriptFileName; + } + + /** + * makeScriptFileNameUnique + */ + public function makeScriptFileNameUnique() + { + $updatesPath = $this->getPluginUpdatesPath(); + $baseFileName = $fileName = $this->scriptFileName; + + $counter = 2; + while (File::isFile($updatesPath.'/'.$fileName.'.php')) { + $fileName = $baseFileName.'_'.$counter; + $counter++; + } + + return $this->scriptFileName = $fileName; + } + + /** + * deleteModel + */ + public function deleteModel() + { + if ($this->isApplied()) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.migration.error_cant_delete_applied')); + } + + $this->deleteVersion(); + $this->removeScriptFile(); + } + + /** + * isApplied + */ + public function isApplied() + { + if ($this->isNewModel()) { + return false; + } + + $versionManager = VersionManager::instance(); + $unappliedVersions = $versionManager->listNewVersions($this->pluginCodeObj->toCode()); + + return !array_key_exists($this->originalVersion, $unappliedVersions); + } + + /** + * apply + */ + public function apply() + { + if ($this->isApplied()) { + return; + } + + $versionManager = VersionManager::instance(); + $versionManager->updatePlugin($this->pluginCodeObj->toCode(), $this->version); + } + + /** + * rollback + */ + public function rollback() + { + if (!$this->isApplied()) { + return; + } + + $versionManager = VersionManager::instance(); + $versionManager->removePlugin($this->pluginCodeObj->toCode(), $this->version); + } + + /** + * assignFileName + */ + protected function assignFileName() + { + $code = trim($this->code); + + if (!strlen($code)) { + $this->scriptFileName = null; + return; + } + + /* + * The file name is based on the migration class name. + */ + $parser = new MigrationFileParser(); + $migrationInfo = $parser->extractMigrationInfoFromSource($code); + + if (!$migrationInfo || !array_key_exists('class', $migrationInfo)) { + throw new ValidationException([ + 'code' => Lang::get('rainlab.builder::lang.migration.error_file_must_define_class') + ]); + } + + if (!array_key_exists('namespace', $migrationInfo)) { + throw new ValidationException([ + 'code' => Lang::get('rainlab.builder::lang.migration.error_file_must_define_namespace') + ]); + } + + $pluginCodeObj = $this->getPluginCodeObj(); + $pluginNamespace = $pluginCodeObj->toUpdatesNamespace(); + + if ($migrationInfo['namespace'] != $pluginNamespace) { + throw new ValidationException([ + 'code' => Lang::get('rainlab.builder::lang.migration.error_namespace_mismatch', ['namespace'=>$pluginNamespace]) + ]); + } + + $this->scriptFileName = $this->makeScriptFileName($migrationInfo['class']); + + /* + * Validate that a file with the generated name does not exist yet. + */ + if ($this->scriptFileName != $this->originalScriptFileName) { + $fileName = $this->scriptFileName.'.php'; + $filePath = $this->getPluginUpdatesPath($fileName); + + if (File::isFile($filePath)) { + throw new ValidationException([ + 'code' => Lang::get('rainlab.builder::lang.migration.error_migration_file_exists', ['file'=>$fileName]) + ]); + } + } + } + + /** + * saveScriptFile + */ + protected function saveScriptFile() + { + $originalFileContents = $this->getOriginalFileContents(); + + if (strlen($this->scriptFileName)) { + $scriptFilePath = $this->getPluginUpdatesPath($this->scriptFileName.'.php'); + + if (!File::put($scriptFilePath, $this->code)) { + throw new SystemException(sprintf('Error saving file %s', $scriptFilePath)); + } + + @File::chmod($scriptFilePath); + } + + if (strlen($this->originalScriptFileName) && $this->scriptFileName != $this->originalScriptFileName) { + $originalScriptFilePath = $this->getPluginUpdatesPath($this->originalScriptFileName.'.php'); + if (File::isFile($originalScriptFilePath)) { + @unlink($originalScriptFilePath); + } + } + + return $originalFileContents; + } + + /** + * getOriginalFileContents + */ + protected function getOriginalFileContents() + { + if (!strlen($this->originalScriptFileName)) { + return null; + } + + $scriptFilePath = $this->getPluginUpdatesPath($this->originalScriptFileName.'.php'); + if (File::isFile($scriptFilePath)) { + return File::get($scriptFilePath); + } + } + + /** + * loadScriptFile + */ + protected function loadScriptFile() + { + $scriptFilePath = $this->getPluginUpdatesPath($this->scriptFileName.'.php'); + + if (!File::isFile($scriptFilePath)) { + throw new ApplicationException(sprintf('Version file %s is not found.', $scriptFilePath)); + } + + return File::get($scriptFilePath); + } + + /** + * removeScriptFile + */ + protected function removeScriptFile() + { + $scriptFilePath = $this->getPluginUpdatesPath($this->scriptFileName.'.php'); + + // Using unlink instead of File::remove() is safer here. + @unlink($scriptFilePath); + } + + /** + * rollbackScriptFile + */ + protected function rollbackScriptFile($fileContents) + { + $scriptFilePath = $this->getPluginUpdatesPath($this->originalScriptFileName.'.php'); + + @File::put($scriptFilePath, $fileContents); + + if ($this->scriptFileName != $this->originalScriptFileName) { + $scriptFilePath = $this->getPluginUpdatesPath($this->scriptFileName.'.php'); + @unlink($scriptFilePath); + } + } + + /** + * rollbackSaving + */ + protected function rollbackSaving($originalVersionData, $originalScriptFileContents) + { + if ($originalVersionData) { + $this->rollbackVersionFile($originalVersionData); + } + + if ($this->isNewModel()) { + $this->removeScriptFile(); + } + else { + $this->rollbackScriptFile($originalScriptFileContents); + } + } + + /** + * insertOrUpdateVersion + */ + protected function insertOrUpdateVersion() + { + $versionFilePath = $this->getPluginUpdatesPath('version.yaml'); + + $versionInformation = $this->getPluginVersionInformation(); + if (!$versionInformation) { + $versionInformation = []; + } + + $originalFileContents = File::get($versionFilePath); + if (!$originalFileContents) { + throw new SystemException(sprintf('Error loading file %s', $versionFilePath)); + } + + $versionInformation[$this->version] = [ + $this->description + ]; + + if (strlen($this->scriptFileName)) { + $versionInformation[$this->version][] = $this->scriptFileName.'.php'; + } + + if (!$this->isNewModel() && $this->version != $this->originalVersion) { + if (array_key_exists($this->originalVersion, $versionInformation)) { + unset($versionInformation[$this->originalVersion]); + } + } + + // Add "v" to the version information + $versionInformation = $this->normalizeVersions((array) $versionInformation); + + $yamlData = Yaml::render($versionInformation); + + if (!File::put($versionFilePath, $yamlData)) { + throw new SystemException(sprintf('Error saving file %s', $versionFilePath)); + } + + @File::chmod($versionFilePath); + + return $originalFileContents; + } + + /** + * deleteVersion + */ + protected function deleteVersion() + { + $versionInformation = $this->getPluginVersionInformation(); + if (!$versionInformation) { + $versionInformation = []; + } + + if (array_key_exists($this->version, $versionInformation)) { + unset($versionInformation[$this->version]); + } + + $versionFilePath = $this->getPluginUpdatesPath('version.yaml'); + $yamlData = Yaml::render($versionInformation); + + if (!File::put($versionFilePath, $yamlData)) { + throw new SystemException(sprintf('Error saving file %s', $versionFilePath)); + } + + @File::chmod($versionFilePath); + } + + /** + * rollbackVersionFile + */ + protected function rollbackVersionFile($fileData) + { + $versionFilePath = $this->getPluginUpdatesPath('version.yaml'); + File::put($versionFilePath, $fileData); + } + + /** + * getPluginUpdatesPath + */ + protected function getPluginUpdatesPath($fileName = null) + { + $pluginCodeObj = $this->getPluginCodeObj(); + + $filePath = '$/'.$pluginCodeObj->toFilesystemPath().'/updates'; + $filePath = File::symbolizePath($filePath); + + if ($fileName !== null) { + return $filePath .= '/'.$fileName; + } + + return $filePath; + } + + /** + * getPluginVersionInformation + */ + protected function getPluginVersionInformation() + { + $versionObj = new PluginVersion; + return $versionObj->getPluginVersionInformation($this->getPluginCodeObj()); + } + + /** + * makeScriptFileName will ensure the last digit in the script contains an underscore, + * for consistency with other areas. + * + * eg: Some123Script3 → Some123Script_3 + */ + protected function makeScriptFileName($value): string + { + $value = Str::snake($value); + + $value = preg_replace_callback('/[0-9]+$/u', function ($match) { + $numericSuffix = $match[0]; + + return '_' . $numericSuffix; + }, $value); + + return $value; + } + + /** + * normalizeVersions checks some versions start with v and others not + */ + protected function normalizeVersions(array $versions): array + { + $result = []; + foreach ($versions as $key => $value) { + $version = rtrim(ltrim((string) $key, 'v'), '.'); + $result['v'.$version] = $value; + } + return $result; + } +} diff --git a/plugins/rainlab/builder/models/ModelFilterModel.php b/plugins/rainlab/builder/models/ModelFilterModel.php new file mode 100644 index 0000000..a641d6c --- /dev/null +++ b/plugins/rainlab/builder/models/ModelFilterModel.php @@ -0,0 +1,213 @@ + ['required', 'regex:/^[a-z0-9\.\-_]+$/i'] + ]; + + /** + * loadForm + */ + public function loadForm($path) + { + $this->fileName = $path; + + return parent::load($this->getFilePath()); + } + + /** + * fill + */ + public function fill(array $attributes) + { + if (!is_array($attributes['scopes'])) { + $attributes['scopes'] = json_decode($attributes['scopes'], true); + + if ($attributes['scopes'] === null) { + throw new SystemException('Cannot decode scopes JSON string.'); + } + } + + return parent::fill($attributes); + } + + /** + * validateFileIsModelType + */ + public static function validateFileIsModelType($fileContentsArray) + { + $modelRootNodes = [ + 'scopes' + ]; + + foreach ($modelRootNodes as $node) { + if (array_key_exists($node, $fileContentsArray)) { + return true; + } + } + + return false; + } + + /** + * validate + */ + public function validate() + { + parent::validate(); + + $this->validateDuplicateScopes(); + + if (!$this->scopes) { + throw new ValidationException(['scopes' => 'Please create at least one scope.']); + } + } + + /** + * initDefaults + */ + public function initDefaults() + { + $this->fileName = 'scopes.yaml'; + } + + /** + * validateDuplicateScopes + */ + protected function validateDuplicateScopes() + { + foreach ($this->scopes as $outerIndex => $outerScope) { + foreach ($this->scopes as $innerIndex => $innerScope) { + if ($innerIndex != $outerIndex && $innerScope['field'] == $outerScope['field']) { + throw new ValidationException([ + 'scopes' => Lang::get( + 'rainlab.builder::lang.list.error_duplicate_scope', + ['scope' => $outerScope['field']] + ) + ]); + } + } + } + } + + /** + * modelToYamlArray converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + protected function modelToYamlArray() + { + $fileScopes = []; + + foreach ($this->scopes as $scope) { + if (!isset($scope['field'])) { + throw new ApplicationException('Cannot save the list - the scope field name should not be empty.'); + } + + $scopeName = $scope['field']; + unset($scope['field']); + + if (array_key_exists('id', $scope)) { + unset($scope['id']); + } + + $scope = $this->preprocessScopeDataBeforeSave($scope); + + $fileScopes[$scopeName] = $scope; + } + + return [ + 'scopes'=>$fileScopes + ]; + } + + /** + * yamlArrayToModel loads the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $fileScopes = $array['scopes']; + $scopes = []; + $index = 0; + + foreach ($fileScopes as $scopeName => $scope) { + if (!is_array($scope)) { + // Handle the case when a scope is defined as + // scope: Title + $scope = [ + 'label' => $scope + ]; + } + + $scope['id'] = $index; + $scope['field'] = $scopeName; + + $scopes[] = $scope; + + $index++; + } + + $this->scopes = $scopes; + } + + /** + * preprocessScopeDataBeforeSave + */ + protected function preprocessScopeDataBeforeSave($scope) + { + // Filter empty values + $scope = array_filter($scope, function ($value) { + return !is_array($value) && strlen($value) > 0; + }); + + // Cast booleans + $booleanFields = []; + + foreach ($booleanFields as $booleanField) { + if (!array_key_exists($booleanField, $scope)) { + continue; + } + + $value = $scope[$booleanField]; + if ($value == '1' || $value == 'true') { + $value = true; + } + else { + $value = false; + } + + + $scope[$booleanField] = $value; + } + + return $scope; + } +} diff --git a/plugins/rainlab/builder/models/ModelFormModel.php b/plugins/rainlab/builder/models/ModelFormModel.php new file mode 100644 index 0000000..d792f39 --- /dev/null +++ b/plugins/rainlab/builder/models/ModelFormModel.php @@ -0,0 +1,124 @@ + ['required', 'regex:/^[a-z0-9\.\-_]+$/i'] + ]; + + /** + * loadForm + */ + public function loadForm($path) + { + $this->fileName = $path; + + return parent::load($this->getFilePath()); + } + + /** + * fill + */ + public function fill(array $attributes) + { + if (!is_array($attributes['controls'])) { + $attributes['controls'] = json_decode($attributes['controls'], true); + + if ($attributes['controls'] === null) { + throw new SystemException('Cannot decode controls JSON string.'); + } + } + + return parent::fill($attributes); + } + + /** + * validateFileIsModelType + */ + public static function validateFileIsModelType($fileContentsArray) + { + $modelRootNodes = [ + 'fields', + 'tabs', + 'secondaryTabs' + ]; + + foreach ($modelRootNodes as $node) { + if (array_key_exists($node, $fileContentsArray)) { + return true; + } + } + + return false; + } + + /** + * validate + */ + public function validate() + { + parent::validate(); + + if (!$this->controls) { + throw new ValidationException(['controls' => 'Please create at least one field.']); + } + } + + /** + * initDefaults + */ + public function initDefaults() + { + $this->fileName = 'fields.yaml'; + } + + /** + * modelToYamlArray converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + protected function modelToYamlArray() + { + return array_merge((array) $this->originals, $this->controls); + } + + /** + * yamlArrayToModel loads the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $this->originals = array_except($array, 'fields'); + + $this->controls = $array; + } +} diff --git a/plugins/rainlab/builder/models/ModelListModel.php b/plugins/rainlab/builder/models/ModelListModel.php new file mode 100644 index 0000000..c119702 --- /dev/null +++ b/plugins/rainlab/builder/models/ModelListModel.php @@ -0,0 +1,217 @@ + ['required', 'regex:/^[a-z0-9\.\-_]+$/i'] + ]; + + /** + * loadForm + */ + public function loadForm($path) + { + $this->fileName = $path; + + return parent::load($this->getFilePath()); + } + + /** + * fill + */ + public function fill(array $attributes) + { + if (!is_array($attributes['columns'])) { + $attributes['columns'] = json_decode($attributes['columns'], true); + + if ($attributes['columns'] === null) { + throw new SystemException('Cannot decode columns JSON string.'); + } + } + + return parent::fill($attributes); + } + + /** + * validateFileIsModelType + */ + public static function validateFileIsModelType($fileContentsArray) + { + $modelRootNodes = [ + 'columns' + ]; + + foreach ($modelRootNodes as $node) { + if (array_key_exists($node, $fileContentsArray)) { + return true; + } + } + + return false; + } + + /** + * validate + */ + public function validate() + { + parent::validate(); + + $this->validateDuplicateColumns(); + + if (!$this->columns) { + throw new ValidationException(['columns' => 'Please create at least one column.']); + } + } + + /** + * initDefaults + */ + public function initDefaults() + { + $this->fileName = 'columns.yaml'; + } + + /** + * validateDuplicateColumns + */ + protected function validateDuplicateColumns() + { + foreach ($this->columns as $outerIndex => $outerColumn) { + foreach ($this->columns as $innerIndex => $innerColumn) { + if ($innerIndex != $outerIndex && $innerColumn['field'] == $outerColumn['field']) { + throw new ValidationException([ + 'columns' => Lang::get( + 'rainlab.builder::lang.list.error_duplicate_column', + ['column' => $outerColumn['field']] + ) + ]); + } + } + } + } + + /** + * Converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + protected function modelToYamlArray() + { + $fileColumns = []; + + foreach ($this->columns as $column) { + if (!isset($column['field'])) { + throw new ApplicationException('Cannot save the list - the column field name should not be empty.'); + } + + $columnName = $column['field']; + unset($column['field']); + + if (array_key_exists('id', $column)) { + unset($column['id']); + } + + $column = $this->preprocessColumnDataBeforeSave($column); + + $fileColumns[$columnName] = $column; + } + + return [ + 'columns'=>$fileColumns + ]; + } + + /** + * Load the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $fileColumns = $array['columns']; + $columns = []; + $index = 0; + + foreach ($fileColumns as $columnName => $column) { + if (!is_array($column)) { + // Handle the case when a column is defined as + // column: Title + $column = [ + 'label' => $column + ]; + } + + $column['id'] = $index; + $column['field'] = $columnName; + + $columns[] = $column; + + $index++; + } + + $this->columns = $columns; + } + + /** + * preprocessColumnDataBeforeSave + */ + protected function preprocessColumnDataBeforeSave($column) + { + // Filter empty values, if not array + $column = array_filter($column, function ($value) { + return !is_array($value) && strlen($value) > 0; + }); + + // Cast booleans + $booleanFields = [ + 'searchable', + 'invisible', + 'sortable' + ]; + + foreach ($booleanFields as $booleanField) { + if (!array_key_exists($booleanField, $column)) { + continue; + } + + $value = $column[$booleanField]; + if ($value == '1' || $value == 'true') { + $value = true; + } + else { + $value = false; + } + + + $column[$booleanField] = $value; + } + + return $column; + } +} diff --git a/plugins/rainlab/builder/models/ModelModel.php b/plugins/rainlab/builder/models/ModelModel.php new file mode 100644 index 0000000..34bc47a --- /dev/null +++ b/plugins/rainlab/builder/models/ModelModel.php @@ -0,0 +1,500 @@ + ['required', 'regex:' . self::UNQUALIFIED_CLASS_NAME_PATTERN, 'uniqModelName'], + 'databaseTable' => ['required'], + 'addTimestamps' => ['timestampColumnsMustExist'], + 'addSoftDeleting' => ['deletedAtColumnMustExist'] + ]; + + /** + * listPluginModels + */ + public static function listPluginModels($pluginCodeObj) + { + $modelsDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/models'; + $pluginNamespace = $pluginCodeObj->toPluginNamespace(); + + $modelsDirectoryPath = File::symbolizePath($modelsDirectoryPath); + if (!File::isDirectory($modelsDirectoryPath)) { + return []; + } + + $parser = new ModelFileParser(); + $result = []; + foreach (new DirectoryIterator($modelsDirectoryPath) as $fileInfo) { + if (!$fileInfo->isFile()) { + continue; + } + + if ($fileInfo->getExtension() != 'php') { + continue; + } + + $filePath = $fileInfo->getPathname(); + $contents = File::get($filePath); + + $modelInfo = $parser->extractModelInfoFromSource($contents); + if (!$modelInfo) { + continue; + } + + if (!Str::startsWith($modelInfo['namespace'], $pluginNamespace.'\\')) { + continue; + } + + $model = new ModelModel(); + $model->className = $modelInfo['class']; + $model->databaseTable = isset($modelInfo['table']) ? $modelInfo['table'] : null; + + $result[] = $model; + } + + return $result; + } + + /** + * save + */ + public function save() + { + $this->validate(); + + $modelFilePath = $this->getFilePath(); + $namespace = $this->getPluginCodeObj()->toPluginNamespace().'\\Models'; + $templateFile = $this->baseClassName === \System\Models\SettingModel::class + ? 'settingmodel.php.tpl' + : 'model.php.tpl'; + + $structure = [ + $modelFilePath => $templateFile + ]; + + $variables = [ + 'namespace' => $namespace, + 'classname' => $this->className, + 'baseclass' => $this->baseClassName, + 'baseclassname' => class_basename($this->baseClassName), + 'table' => $this->databaseTable + ]; + + $generator = new FilesystemGenerator('$', $structure, '$/rainlab/builder/models/modelmodel/templates'); + $generator->setVariables($variables); + + // Trait contents + if ($this->addSoftDeleting) { + $this->traits[] = \October\Rain\Database\Traits\SoftDelete::class; + } + + usort($this->traits, function($a, $b) { return strlen($a) > strlen($b); }); + + $traitContents = []; + foreach ($this->traits as $trait) { + $traitContents[] = " use \\{$trait};"; + } + $generator->setVariable('traitContents', implode(PHP_EOL, $traitContents)); + + // Dynamic contents + $dynamicContents = []; + + if ($this->addSoftDeleting) { + $dynamicContents[] = $generator->getTemplateContents('soft-delete.php.tpl'); + } + + if (!$this->addTimestamps) { + $dynamicContents[] = $generator->getTemplateContents('no-timestamps.php.tpl'); + } + + $dynamicContents = array_merge($dynamicContents, (array) $this->injectedRawContents); + + $generator->setVariable('dynamicContents', implode('', $dynamicContents)); + + // Validation contents + $validationDefinitions = (array) $this->validationDefinitions; + foreach ($validationDefinitions as $type => &$definitions) { + foreach ($definitions as $field => &$rule) { + // Cannot process anything other than string at this time + if (!is_string($rule)) { + unset($definitions[$field]); + } + } + } + + $validationTemplate = File::get(__DIR__.'/modelmodel/templates/validation-definitions.php.tpl'); + + $validationContents = Twig::parse($validationTemplate, ['validation' => $validationDefinitions]); + + $generator->setVariable('validationContents', $validationContents); + + // Relation contents + $relationContents = []; + + $relationTemplate = File::get(__DIR__.'/modelmodel/templates/relation-definitions.php.tpl'); + + foreach ((array) $this->relationDefinitions as $relationType => $definitions) { + if (!$definitions) { + continue; + } + + $relationVars = [ + 'relationType' => $relationType, + 'relations' => [], + ]; + + foreach ($definitions as $relationName => $definition) { + $definition = (array) $definition; + $modelClass = array_shift($definition); + + $props = $definition; + foreach ($props as &$prop) { + $prop = var_export($prop, true); + } + + $relationVars['relations'][$relationName] = [ + 'class' => $modelClass, + 'props' => $props + ]; + } + + $relationContents[] = Twig::parse($relationTemplate, $relationVars); + } + + $generator->setVariable('relationContents', implode(PHP_EOL, $relationContents)); + + // Multisite contents + $multisiteTemplate = File::get(__DIR__.'/modelmodel/templates/multisite-definitions.php.tpl'); + + $multisiteContents = Twig::parse($multisiteTemplate, ['multisite' => $this->multisiteDefinition]); + + $generator->setVariable('multisiteContents', $multisiteContents); + + $generator->generate(); + } + + /** + * validate + */ + public function validate() + { + $path = File::symbolizePath('$/'.$this->getFilePath()); + + $this->validationMessages = [ + 'className.uniq_model_name' => Lang::get('rainlab.builder::lang.model.error_class_name_exists', ['path'=>$path]), + 'addTimestamps.timestamp_columns_must_exist' => Lang::get('rainlab.builder::lang.model.error_timestamp_columns_must_exist'), + 'addSoftDeleting.deleted_at_column_must_exist' => Lang::get('rainlab.builder::lang.model.error_deleted_at_column_must_exist') + ]; + + Validator::extend('uniqModelName', function ($attribute, $value, $parameters) use ($path) { + $value = trim($value); + + if (!$this->isNewModel()) { + // Editing models is not supported at the moment, + // so no validation is required. + return true; + } + + return !File::isFile($path); + }); + + $columns = $this->isNewModel() ? Schema::getColumnListing($this->databaseTable) : []; + Validator::extend('timestampColumnsMustExist', function ($attribute, $value, $parameters) use ($columns) { + return $this->validateColumnsExist($value, $columns, ['created_at', 'updated_at']); + }); + + Validator::extend('deletedAtColumnMustExist', function ($attribute, $value, $parameters) use ($columns) { + return $this->validateColumnsExist($value, $columns, ['deleted_at']); + }); + + if ($this->skipDbValidation) { + unset( + $this->validationRules['addTimestamps'], + $this->validationRules['addSoftDeleting'] + ); + } + + parent::validate(); + } + + /** + * addRawContentToModel + */ + public function addRawContentToModel($content) + { + $this->injectedRawContents[] = $content; + } + + /** + * getDatabaseTableOptions + */ + public function getDatabaseTableOptions() + { + $pluginCode = $this->getPluginCodeObj()->toCode(); + + $tables = DatabaseTableModel::listPluginTables($pluginCode); + return array_combine($tables, $tables); + } + + /** + * getTableNameFromModelClass + */ + private static function getTableNameFromModelClass($pluginCodeObj, $modelClassName) + { + if (!self::validateModelClassName($modelClassName)) { + throw new SystemException('Invalid model class name: '.$modelClassName); + } + + $modelsDirectoryPath = File::symbolizePath($pluginCodeObj->toPluginDirectoryPath().'/models'); + if (!File::isDirectory($modelsDirectoryPath)) { + return ''; + } + + $modelFilePath = $modelsDirectoryPath.'/'.$modelClassName.'.php'; + if (!File::isFile($modelFilePath)) { + return ''; + } + + $parser = new ModelFileParser(); + $modelInfo = $parser->extractModelInfoFromSource(File::get($modelFilePath)); + if (!$modelInfo || !isset($modelInfo['table'])) { + return ''; + } + + return $modelInfo['table']; + } + + /** + * getModelFields + */ + public static function getModelFields($pluginCodeObj, $modelClassName) + { + $tableName = self::getTableNameFromModelClass($pluginCodeObj, $modelClassName); + + // Currently we return only table columns, + // but eventually we might want to return relations as well. + + return Schema::getColumnListing($tableName); + } + + /** + * getModelColumnsAndTypes + */ + public static function getModelColumnsAndTypes($pluginCodeObj, $modelClassName) + { + $tableName = self::getTableNameFromModelClass($pluginCodeObj, $modelClassName); + + if (!DatabaseTableModel::tableExists($tableName)) { + throw new ApplicationException('Database table not found: '.$tableName); + } + + $schema = DatabaseTableModel::getSchema(); + $tableInfo = $schema->getTable($tableName); + + $columns = $tableInfo->getColumns(); + $result = []; + foreach ($columns as $column) { + $columnName = $column->getName(); + $typeName = $column->getType()->getName(); + + if ($typeName == EnumDbType::TYPENAME) { + continue; + } + + $item = [ + 'name' => $columnName, + 'type' => MigrationColumnType::toMigrationMethodName($typeName, $columnName) + ]; + + $result[] = $item; + } + + return $result; + } + + /** + * getPluginRegistryData + */ + public static function getPluginRegistryData($pluginCode, $subtype) + { + $pluginCodeObj = new PluginCode($pluginCode); + + $models = self::listPluginModels($pluginCodeObj); + $result = []; + foreach ($models as $model) { + $fullClassName = $pluginCodeObj->toPluginNamespace().'\\Models\\'.$model->className; + + $result[$fullClassName] = $model->className; + } + + return $result; + } + + /** + * getPluginRegistryDataColumns + */ + public static function getPluginRegistryDataColumns($pluginCode, $modelClassName) + { + $classParts = explode('\\', $modelClassName); + if (!$classParts) { + return []; + } + + $modelClassName = array_pop($classParts); + + if (!self::validateModelClassName($modelClassName)) { + return []; + } + + $pluginCodeObj = new PluginCode($pluginCode); + $columnNames = self::getModelFields($pluginCodeObj, $modelClassName); + + $result = []; + foreach ($columnNames as $columnName) { + $result[$columnName] = $columnName; + } + + return $result; + } + + /** + * validateModelClassName + */ + public static function validateModelClassName($modelClassName) + { + return class_exists($modelClassName) || !!preg_match(self::UNQUALIFIED_CLASS_NAME_PATTERN, $modelClassName); + } + + /** + * getModelFilePath + */ + public function getModelFilePath() + { + return File::symbolizePath($this->getPluginCodeObj()->toPluginDirectoryPath().'/models/'.$this->className.'.php'); + } + + /** + * getFilePath + */ + protected function getFilePath() + { + return $this->getPluginCodeObj()->toFilesystemPath().'/models/'.$this->className.'.php'; + } + + /** + * validateColumnsExist + */ + protected function validateColumnsExist($value, $columns, $columnsToCheck) + { + if (!strlen(trim($this->databaseTable))) { + return true; + } + + if (!$this->isNewModel()) { + // Editing models is not supported at the moment, + // so no validation is required. + return true; + } + + if (!$value) { + return true; + } + + return count(array_intersect($columnsToCheck, $columns)) == count($columnsToCheck); + } +} diff --git a/plugins/rainlab/builder/models/ModelYamlModel.php b/plugins/rainlab/builder/models/ModelYamlModel.php new file mode 100644 index 0000000..c554b68 --- /dev/null +++ b/plugins/rainlab/builder/models/ModelYamlModel.php @@ -0,0 +1,250 @@ +fileName)) { + $this->fileName = $this->addExtension($this->fileName); + } + } + + /** + * setModelClassName + */ + public function setModelClassName($className) + { + if (!preg_match('/^[a-zA-Z]+[0-9a-z\_]*$/', $className)) { + throw new SystemException('Invalid class name: '.$className); + } + + $this->modelClassName = $className; + } + + /** + * validate + */ + public function validate() + { + $this->validationMessages = [ + 'fileName.required' => Lang::get('rainlab.builder::lang.form.error_file_name_required'), + 'fileName.regex' => Lang::get('rainlab.builder::lang.form.error_file_name_invalid') + ]; + + return parent::validate(); + } + + /** + * getDisplayName returns a string suitable for displaying in the Builder UI tabs. + */ + public function getDisplayName($nameFallback) + { + $fileName = $this->fileName; + + if (substr($fileName, -5) == '.yaml') { + $fileName = substr($fileName, 0, -5); + } + + if (!strlen($fileName)) { + $fileName = $nameFallback; + } + + return $this->getModelClassName().'/'.$fileName; + } + + /** + * listModelFiles + */ + public static function listModelFiles($pluginCodeObj, $modelClassName) + { + if (!self::validateModelClassName($modelClassName)) { + throw new SystemException('Invalid model class name: '.$modelClassName); + } + + $modelDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/models/'.strtolower($modelClassName); + + $modelDirectoryPath = File::symbolizePath($modelDirectoryPath); + + if (!File::isDirectory($modelDirectoryPath)) { + return []; + } + + $result = []; + foreach (new DirectoryIterator($modelDirectoryPath) as $fileInfo) { + if (!$fileInfo->isFile() || $fileInfo->getExtension() != 'yaml') { + continue; + } + + try { + $fileContents = Yaml::parseFile($fileInfo->getPathname()); + } + catch (Exception $ex) { + continue; + } + + if (!is_array($fileContents)) { + $fileContents = []; + } + + if (!static::validateFileIsModelType($fileContents)) { + continue; + } + + $result[] = $fileInfo->getBasename(); + } + + return $result; + } + + /** + * getPluginRegistryData + */ + public static function getPluginRegistryData($pluginCode, $modelClassName) + { + $pluginCodeObj = new PluginCode($pluginCode); + + $classParts = explode('\\', $modelClassName); + if (!$classParts) { + return []; + } + + $modelClassName = array_pop($classParts); + + if (!self::validateModelClassName($modelClassName)) { + return []; + } + + $models = self::listModelFiles($pluginCodeObj, $modelClassName); + $modelDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/models/'.strtolower($modelClassName).'/'; + + $result = []; + foreach ($models as $fileName) { + $fullFilePath = $modelDirectoryPath.$fileName; + + $result[$fullFilePath] = $fileName; + } + + return $result; + } + + /** + * getPluginRegistryDataAllRecords + */ + public static function getPluginRegistryDataAllRecords($pluginCode) + { + $pluginCodeObj = new PluginCode($pluginCode); + $pluginDirectoryPath = $pluginCodeObj->toPluginDirectoryPath(); + + $models = ModelModel::listPluginModels($pluginCodeObj); + $result = []; + foreach ($models as $model) { + $modelRecords = self::listModelFiles($pluginCodeObj, $model->className); + $modelDirectoryPath = $pluginDirectoryPath.'/models/'.strtolower($model->className).'/'; + + foreach ($modelRecords as $fileName) { + $label = $model->className.'/'.$fileName; + $key = $modelDirectoryPath.$fileName; + + $result[$key] = $label; + } + } + + return $result; + } + + /** + * validateFileIsModelType + */ + public static function validateFileIsModelType($fileContentsArray) + { + return false; + } + + /** + * validateModelClassName + */ + protected static function validateModelClassName($modelClassName) + { + return preg_match('/^[A-Z]+[a-zA-Z0-9_]+$/i', $modelClassName); + } + + /** + * getModelClassName + */ + protected function getModelClassName() + { + if ($this->modelClassName === null) { + throw new SystemException('The model class name is not set.'); + } + + return $this->modelClassName; + } + + /** + * getYamlFilePath + */ + public function getYamlFilePath() + { + $fileName = $this->addExtension(trim($this->fileName)); + + return File::symbolizePath($this->getPluginCodeObj()->toPluginDirectoryPath().'/models/'.strtolower($this->getModelClassName()).'/'.$fileName); + } + + /** + * getFilePath returns a file path to save the model to. + * @return string Returns a path. + */ + protected function getFilePath() + { + $fileName = trim($this->fileName); + if (!strlen($fileName)) { + throw new SystemException('The form model file name is not set.'); + } + + $fileName = $this->addExtension($fileName); + + return $this->getPluginCodeObj()->toPluginDirectoryPath().'/models/'.strtolower($this->getModelClassName()).'/'.$fileName; + } + + /** + * addExtension + */ + protected function addExtension($fileName) + { + if (substr($fileName, -5) !== '.yaml') { + $fileName .= '.yaml'; + } + + return $fileName; + } +} diff --git a/plugins/rainlab/builder/models/PermissionsModel.php b/plugins/rainlab/builder/models/PermissionsModel.php new file mode 100644 index 0000000..6a6f551 --- /dev/null +++ b/plugins/rainlab/builder/models/PermissionsModel.php @@ -0,0 +1,234 @@ +pluginCodeObj = $pluginCodeObj; + } + + /** + * modelToYamlArray converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + protected function modelToYamlArray() + { + $filePermissions = []; + + foreach ($this->permissions as $permission) { + if (array_key_exists('id', $permission)) { + unset($permission['id']); + } + + $permission = $this->trimPermissionProperties($permission); + + if ($this->isEmptyRow($permission)) { + continue; + } + + if (!isset($permission['permission'])) { + throw new ApplicationException('Cannot save permissions - the permission code should not be empty.'); + } + + $code = $permission['permission']; + unset($permission['permission']); + + $filePermissions[$code] = $permission; + } + + return $filePermissions; + } + + /** + * validate + */ + public function validate() + { + parent::validate(); + + $this->validateDuplicatePermissions(); + $this->validateRequiredProperties(); + } + + /** + * getPluginRegistryData + */ + public static function getPluginRegistryData($pluginCode) + { + $model = new PermissionsModel(); + + $model->loadPlugin($pluginCode); + + $result = []; + + foreach ($model->permissions as $permissionInfo) { + if (!isset($permissionInfo['permission']) || !isset($permissionInfo['label'])) { + continue; + } + + $key = $permissionInfo['permission']; + $result[$key] = $key.' - '.Lang::get($permissionInfo['label']); + } + + return $result; + } + + /** + * validateDuplicatePermissions + */ + protected function validateDuplicatePermissions() + { + foreach ($this->permissions as $outerIndex => $outerPermission) { + if (!isset($outerPermission['permission'])) { + continue; + } + + foreach ($this->permissions as $innerIndex => $innerPermission) { + if (!isset($innerPermission['permission'])) { + continue; + } + + $outerCode = trim($outerPermission['permission']); + $innerCode = trim($innerPermission['permission']); + + if ($innerIndex != $outerIndex && $outerCode == $innerCode && strlen($outerCode)) { + throw new ValidationException([ + 'permissions' => Lang::get( + 'rainlab.builder::lang.permission.error_duplicate_code', + ['code' => $outerCode] + ) + ]); + } + } + } + } + + /** + * validateRequiredProperties + */ + protected function validateRequiredProperties() + { + foreach ($this->permissions as $permission) { + if (array_key_exists('id', $permission)) { + unset($permission['id']); + } + + $permission = $this->trimPermissionProperties($permission); + + if ($this->isEmptyRow($permission)) { + continue; + } + + if (!strlen($permission['permission'])) { + throw new ValidationException([ + 'permissions' => Lang::get('rainlab.builder::lang.permission.column_permission_required') + ]); + } + + if (!strlen($permission['label'])) { + throw new ValidationException([ + 'permissions' => Lang::get('rainlab.builder::lang.permission.column_label_required') + ]); + } + + if (!strlen($permission['tab'])) { + throw new ValidationException([ + 'permissions' => Lang::get('rainlab.builder::lang.permission.column_tab_required') + ]); + } + } + } + + /** + * trimPermissionProperties + */ + protected function trimPermissionProperties($permission) + { + array_walk($permission, function ($value, $key) { + return trim($value); + }); + + return $permission; + } + + /** + * isEmptyRow + */ + protected function isEmptyRow($permission) + { + return !isset($permission['tab']) || !isset($permission['permission']) || !isset($permission['label']); + } + + /** + * yamlArrayToModel loads the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $filePermissions = $array; + $permissions = []; + $index = 0; + + foreach ($filePermissions as $code => $permission) { + $permission['permission'] = $code; + + $permissions[] = $permission; + } + + $this->permissions = $permissions; + } + + /** + * getFilePath returns a file path to save the model to. + * @return string Returns a path. + */ + protected function getFilePath() + { + if ($this->pluginCodeObj === null) { + throw new SystemException('Error saving plugin permission model - the plugin code object is not set.'); + } + + return $this->pluginCodeObj->toPluginFilePath(); + } +} diff --git a/plugins/rainlab/builder/models/PluginBaseModel.php b/plugins/rainlab/builder/models/PluginBaseModel.php new file mode 100644 index 0000000..a9d9e82 --- /dev/null +++ b/plugins/rainlab/builder/models/PluginBaseModel.php @@ -0,0 +1,259 @@ + 'required', + 'author' => ['required'], + 'namespace' => ['required', 'regex:/^[a-z]+[a-z0-9]+$/i', 'reserved'], + 'author_namespace' => ['required', 'regex:/^[a-z]+[a-z0-9]+$/i', 'reserved'], + 'homepage' => 'url' + ]; + + /** + * getIconOptions + */ + public function getIconOptions() + { + return IconList::getList(); + } + + public function initDefaults() + { + $settings = PluginSettings::instance(); + $this->author = $settings->author_name; + $this->author_namespace = $settings->author_namespace; + } + + public function getPluginCode() + { + return $this->author_namespace.'.'.$this->namespace; + } + + public static function listAllPluginCodes() + { + $plugins = PluginManager::instance()->getPlugins(); + + return array_keys($plugins); + } + + protected function initPropertiesFromPluginCodeObject($pluginCodeObj) + { + $this->author_namespace = $pluginCodeObj->getAuthorCode(); + $this->namespace = $pluginCodeObj->getPluginCode(); + } + + /** + * Converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + protected function modelToYamlArray() + { + return [ + 'name' => $this->name, + 'description' => $this->description, + 'author' => $this->author, + 'icon' => $this->icon, + 'homepage' => $this->homepage + ]; + } + + /** + * Load the model's data from an array. + * @param array $array An array to load the model fields from. + */ + protected function yamlArrayToModel($array) + { + $this->name = $this->getArrayKeySafe($array, 'name'); + $this->description = $this->getArrayKeySafe($array, 'description'); + $this->author = $this->getArrayKeySafe($array, 'author'); + $this->icon = $this->getArrayKeySafe($array, 'icon'); + $this->homepage = $this->getArrayKeySafe($array, 'homepage'); + } + + protected function beforeCreate() + { + $this->localizedName = $this->name; + $this->localizedDescription = $this->description; + + $pluginCode = strtolower($this->author_namespace.'.'.$this->namespace); + + $this->name = $pluginCode.'::lang.plugin.name'; + $this->description = $pluginCode.'::lang.plugin.description'; + } + + protected function afterCreate() + { + try { + $this->initPluginStructure(); + $this->forcePluginRegistration(); + $this->initBuilderSettings(); + } + catch (Exception $ex) { + $this->rollbackPluginCreation(); + throw $ex; + } + } + + protected function initPluginStructure() + { + $basePath = $this->getPluginPath(); + + $defaultLanguage = LocalizationModel::getDefaultLanguage(); + + $structure = [ + $basePath.'/Plugin.php' => 'plugin.php.tpl', + $basePath.'/updates/version.yaml' => 'version.yaml.tpl', + $basePath.'/classes', + $basePath.'/lang/'.$defaultLanguage.'/lang.php' => 'lang.php.tpl' + ]; + + $variables = [ + 'authorNamespace' => $this->author_namespace, + 'pluginNamespace' => $this->namespace, + 'pluginNameSanitized' => $this->sanitizePHPString($this->localizedName), + 'pluginDescriptionSanitized' => $this->sanitizePHPString($this->localizedDescription), + ]; + + $generator = new FilesystemGenerator('$', $structure, '$/rainlab/builder/models/pluginbasemodel/templates'); + $generator->setVariables($variables); + $generator->generate(); + } + + protected function forcePluginRegistration() + { + PluginManager::instance()->loadPlugins(); + UpdateManager::instance()->update(); + } + + protected function rollbackPluginCreation() + { + $basePath = '$/'.$this->getPluginPath(); + $basePath = File::symbolizePath($basePath); + + if (basename($basePath) == strtolower($this->namespace)) { + File::deleteDirectory($basePath); + } + } + + protected function sanitizePHPString($str) + { + return str_replace("'", "\'", $str); + } + + /** + * Returns a file path to save the model to. + * @return string Returns a path. + */ + protected function getFilePath() + { + return $this->getPluginPathObj()->toPluginFilePath(); + } + + protected function getPluginPath() + { + return $this->getPluginPathObj()->toFilesystemPath(); + } + + protected function getPluginPathObj() + { + return new PluginCode($this->getPluginCode()); + } + + protected function initBuilderSettings() + { + // Initialize Builder configuration - author name and namespace + // if it was not set yet. + + $settings = PluginSettings::instance(); + if (strlen($settings->author_name) || strlen($settings->author_namespace)) { + return; + } + + $settings->author_name = $this->author; + $settings->author_namespace = $this->author_namespace; + + $settings->save(); + } +} diff --git a/plugins/rainlab/builder/models/PluginYamlModel.php b/plugins/rainlab/builder/models/PluginYamlModel.php new file mode 100644 index 0000000..5239348 --- /dev/null +++ b/plugins/rainlab/builder/models/PluginYamlModel.php @@ -0,0 +1,85 @@ +initPropertiesFromPluginCodeObject($pluginCodeObj); + + $result = parent::load($filePath); + + $this->loadCommonProperties(); + + return $result; + } + + /** + * getPluginName + */ + public function getPluginName() + { + return Lang::get($this->pluginName); + } + + /** + * loadCommonProperties + */ + protected function loadCommonProperties() + { + if (!array_key_exists('plugin', $this->originalFileData)) { + return; + } + + $pluginData = $this->originalFileData['plugin']; + + if (array_key_exists('name', $pluginData)) { + $this->pluginName = $pluginData['name']; + } + } + + /** + * initPropertiesFromPluginCodeObject + */ + protected function initPropertiesFromPluginCodeObject($pluginCodeObj) + { + } + + /** + * pluginSettingsFileExists + */ + protected static function pluginSettingsFileExists($pluginCodeObj) + { + $filePath = File::symbolizePath($pluginCodeObj->toPluginFilePath()); + if (File::isFile($filePath)) { + return $filePath; + } + + return false; + } +} diff --git a/plugins/rainlab/builder/models/Settings.php b/plugins/rainlab/builder/models/Settings.php new file mode 100644 index 0000000..14590fc --- /dev/null +++ b/plugins/rainlab/builder/models/Settings.php @@ -0,0 +1,38 @@ + 'required', + 'author_namespace' => ['required', 'regex:/^[a-z]+[a-z0-9]+$/i', 'reserved'] + ]; +} diff --git a/plugins/rainlab/builder/models/YamlModel.php b/plugins/rainlab/builder/models/YamlModel.php new file mode 100644 index 0000000..85c0384 --- /dev/null +++ b/plugins/rainlab/builder/models/YamlModel.php @@ -0,0 +1,252 @@ +validate(); + + if ($this->isNewModel()) { + $this->beforeCreate(); + } + + $data = $this->modelToYamlArray(); + + if ($this->yamlSection) { + $fileData = $this->originalFileData; + + // Save the section data only if the section is not empty. + if ($data) { + $originalData = $this->preserveOriginal === true ? ($fileData[$this->yamlSection] ?? []) : []; + $fileData[$this->yamlSection] = $this->arrayMergeMany($originalData, $data); + } + else { + if (array_key_exists($this->yamlSection, $fileData)) { + unset($fileData[$this->yamlSection]); + } + } + + $data = $fileData; + } + + $dumper = new YamlDumper(); + + if ($data !== null) { + $yamlData = $dumper->dump($data, 20, 0, false, true); + } + else { + $yamlData = ''; + } + + $filePath = File::symbolizePath($this->getFilePath()); + $isNew = $this->isNewModel(); + + if (File::isFile($filePath)) { + if ($isNew || $this->originalFilePath != $filePath) { + throw new ValidationException(['fileName' => Lang::get('rainlab.builder::lang.common.error_file_exists', ['path'=>basename($filePath)])]); + } + } + + $fileDirectory = dirname($filePath); + if (!File::isDirectory($fileDirectory)) { + if (!File::makeDirectory($fileDirectory, 0777, true, true)) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory])); + } + } + + if (@File::put($filePath, $yamlData) === false) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.yaml.save_error', ['name'=>$filePath])); + } + + @File::chmod($filePath); + + if ($this->isNewModel()) { + $this->afterCreate(); + } + + if ($this->yamlSection) { + $this->originalFileData = $data; + } + + if (strlen($this->originalFilePath) > 0 && $this->originalFilePath != $filePath) { + @File::delete($this->originalFilePath); + } + + $this->originalFilePath = $filePath; + } + + /** + * load + */ + protected function load($filePath) + { + $filePath = File::symbolizePath($filePath); + + if (!File::isFile($filePath)) { + throw new ApplicationException('Cannot load the model - the original file is not found: '.basename($filePath)); + } + + try { + $data = Yaml::parse(File::get($filePath)); + } + catch (Exception $ex) { + throw new ApplicationException(sprintf('Cannot parse the YAML file %s: %s', basename($filePath), $ex->getMessage())); + } + + $this->originalFilePath = $filePath; + + if ($this->yamlSection) { + $this->originalFileData = $data; + if (!is_array($this->originalFileData)) { + $this->originalFileData = []; + } + + if (array_key_exists($this->yamlSection, $data)) { + $data = $data[$this->yamlSection]; + } + else { + $data = []; + } + } + + $this->yamlArrayToModel($data); + } + + /** + * deleteModel + */ + public function deleteModel() + { + if (!File::isFile($this->originalFilePath)) { + throw new ApplicationException('Cannot load the model - the original file is not found: '.$filePath); + } + + if (strtolower(substr($this->originalFilePath, -5)) !== '.yaml') { + throw new ApplicationException('Cannot delete the model - the original file should be a YAML document'); + } + + File::delete($this->originalFilePath); + } + + /** + * initDefaults + */ + public function initDefaults() + { + } + + /** + * isNewModel + */ + public function isNewModel() + { + return !strlen($this->originalFilePath); + } + + /** + * beforeCreate + */ + protected function beforeCreate() + { + } + + /** + * afterCreate + */ + protected function afterCreate() + { + } + + /** + * getArrayKeySafe + */ + protected function getArrayKeySafe($array, $key, $default = null) + { + return array_key_exists($key, $array) ? $array[$key] : $default; + } + + /** + * arrayMergeMany is a deep array merge function used to preserve existing + * YAML properties and splicing in new ones from builder. + */ + protected function arrayMergeMany($arr1, $arr2) + { + $arrMerge = array_merge($arr1, $arr2); + + foreach ($arrMerge as $key => $val) { + if (!is_array($val) || !$this->isArrayAssociative($val)) { + continue; + } + + if (isset($arr1[$key]) && isset($arr2[$key])) { + $arrMerge[$key] = $this->arrayMergeMany($arr1[$key], $arr2[$key]); + } + } + + return $arrMerge; + } + + /** + * isArrayAssociative retruns true if the array is associative + */ + protected function isArrayAssociative($arr) + { + return is_array($arr) && array_keys($arr) !== range(0, count($arr) - 1); + } + + /** + * Converts the model's data to an array before it's saved to a YAML file. + * @return array + */ + abstract protected function modelToYamlArray(); + + /** + * Load the model's data from an array. + * @param array $array An array to load the model fields from. + */ + abstract protected function yamlArrayToModel($array); + + /** + * Returns a file path to save the model to. + * @return string Returns a path. + */ + abstract protected function getFilePath(); +} diff --git a/plugins/rainlab/builder/models/codefilemodel/fields.yaml b/plugins/rainlab/builder/models/codefilemodel/fields.yaml new file mode 100644 index 0000000..614b9d1 --- /dev/null +++ b/plugins/rainlab/builder/models/codefilemodel/fields.yaml @@ -0,0 +1,21 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + fileName: + label: cms::lang.editor.filename + attributes: + default-focus: 1 + + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexcodeoperations/partials/_toolbar.php + cssClass: collapse-visible + +secondaryTabs: + stretch: true + fields: + content: + stretch: true + type: codeeditor diff --git a/plugins/rainlab/builder/models/controllermodel/fields.yaml b/plugins/rainlab/builder/models/controllermodel/fields.yaml new file mode 100644 index 0000000..1d0bf30 --- /dev/null +++ b/plugins/rainlab/builder/models/controllermodel/fields.yaml @@ -0,0 +1,23 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + controller: + label: rainlab.builder::lang.controller.controller + attributes: + readonly: true + + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexcontrolleroperations/partials/_toolbar.php + cssClass: collapse-visible + +secondaryTabs: + stretch: true + fields: + formBuilder: + type: RainLab\Builder\FormWidgets\ControllerBuilder + stretch: true + cssClass: layout + tab: rainlab.builder::lang.controller.behaviors diff --git a/plugins/rainlab/builder/models/controllermodel/fields_new_controller.yaml b/plugins/rainlab/builder/models/controllermodel/fields_new_controller.yaml new file mode 100644 index 0000000..ba06469 --- /dev/null +++ b/plugins/rainlab/builder/models/controllermodel/fields_new_controller.yaml @@ -0,0 +1,37 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + controller: + label: rainlab.builder::lang.controller.controller_name + commentAbove: rainlab.builder::lang.controller.controller_name_description + required: true + attributes: + default-focus: 1 + + baseModelClassName: + label: rainlab.builder::lang.controller.base_model_class + commentAbove: rainlab.builder::lang.controller.base_model_class_description + placeholder: rainlab.builder::lang.controller.base_model_class_placeholder + span: left + type: dropdown + + menuItem: + label: rainlab.builder::lang.controller.menu_item + commentAbove: rainlab.builder::lang.controller.menu_item_description + placeholder: rainlab.builder::lang.controller.menu_item_placeholder + span: right + type: dropdown + +tabs: + fields: + behaviors: + commentAbove: rainlab.builder::lang.controller.controller_behaviors_description + tab: rainlab.builder::lang.controller.controller_behaviors + type: checkboxlist + permissions: + commentAbove: rainlab.builder::lang.controller.controller_permissions_description + tab: rainlab.builder::lang.controller.controller_permissions + type: checkboxlist + placeholder: rainlab.builder::lang.controller.controller_permissions_no_permissions diff --git a/plugins/rainlab/builder/models/databasetablemodel/fields.yaml b/plugins/rainlab/builder/models/databasetablemodel/fields.yaml new file mode 100644 index 0000000..8631b05 --- /dev/null +++ b/plugins/rainlab/builder/models/databasetablemodel/fields.yaml @@ -0,0 +1,91 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + name: + label: rainlab.builder::lang.database.field_name + attributes: + default-focus: 1 + spellcheck: 'false' + + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexdatabasetableoperations/partials/_toolbar.php + cssClass: collapse-visible + +tabs: + stretch: true + cssClass: master-area + fields: + columns: + stretch: true + cssClass: frameless + tab: rainlab.builder::lang.database.tab_columns + type: datatable + btnAddRowLabel: rainlab.builder::lang.database.btn_add_column + btnDeleteRowLabel: rainlab.builder::lang.database.btn_delete_column + height: 100 + dynamicHeight: true + columns: + name: + title: rainlab.builder::lang.database.column_name_name + validation: + required: + message: rainlab.builder::lang.database.column_name_required + regex: + pattern: ^[0-9_a-z]+$ + message: rainlab.builder::lang.database.column_validation_title + type: + title: rainlab.builder::lang.database.column_name_type + type: dropdown + options: + integer: Integer + smallInteger: Small Integer + bigInteger: Big Integer + date: Date + time: Time + dateTime: Date and Time + timestamp: Timestamp + string: String + text: Text + binary: Binary + boolean: Boolean + decimal: Decimal + double: Double + validation: + required: + message: rainlab.builder::lang.database.column_type_required + length: + title: rainlab.builder::lang.database.column_name_length + validation: + regex: + pattern: (^[0-9]+$)|(^[0-9]+,[0-9]+$) + message: rainlab.builder::lang.database.column_validation_length + width: 8% + + default: + title: rainlab.builder::lang.database.column_default + + unsigned: + title: rainlab.builder::lang.database.column_name_unsigned + type: checkbox + width: 8% + + allow_null: + title: rainlab.builder::lang.database.column_name_nullable + type: checkbox + width: 8% + + auto_increment: + title: rainlab.builder::lang.database.column_auto_increment + type: checkbox + width: 8% + + primary_key: + title: rainlab.builder::lang.database.column_auto_primary_key + type: checkbox + width: 8% + + comment: + title: rainlab.builder::lang.database.column_comment diff --git a/plugins/rainlab/builder/models/databasetablemodel/templates/full-migration-code.php.tpl b/plugins/rainlab/builder/models/databasetablemodel/templates/full-migration-code.php.tpl new file mode 100644 index 0000000..61009c1 --- /dev/null +++ b/plugins/rainlab/builder/models/databasetablemodel/templates/full-migration-code.php.tpl @@ -0,0 +1,9 @@ + [ + 'name' => 'Plugin name', + 'description' => 'Plugin description.' + ] +]; diff --git a/plugins/rainlab/builder/models/menusmodel/fields.yaml b/plugins/rainlab/builder/models/menusmodel/fields.yaml new file mode 100644 index 0000000..756a532 --- /dev/null +++ b/plugins/rainlab/builder/models/menusmodel/fields.yaml @@ -0,0 +1,17 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexmenusoperations/partials/_toolbar.php + cssClass: collapse-visible + +secondaryTabs: + stretch: true + fields: + menus: + stretch: true + tab: rainlab.builder::lang.menu.items + type: RainLab\Builder\FormWidgets\MenuEditor \ No newline at end of file diff --git a/plugins/rainlab/builder/models/migrationmodel/fields.yaml b/plugins/rainlab/builder/models/migrationmodel/fields.yaml new file mode 100644 index 0000000..cff6f49 --- /dev/null +++ b/plugins/rainlab/builder/models/migrationmodel/fields.yaml @@ -0,0 +1,9 @@ +fields: + version: + label: rainlab.builder::lang.migration.field_version + description: + label: rainlab.builder::lang.migration.field_description + code: + label: rainlab.builder::lang.migration.field_code + type: codeeditor + language: php diff --git a/plugins/rainlab/builder/models/migrationmodel/management-fields.yaml b/plugins/rainlab/builder/models/migrationmodel/management-fields.yaml new file mode 100644 index 0000000..8c52588 --- /dev/null +++ b/plugins/rainlab/builder/models/migrationmodel/management-fields.yaml @@ -0,0 +1,31 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + version: + span: left + label: rainlab.builder::lang.migration.field_version + attributes: + default-focus: 1 + spellcheck: 'false' + cssClass: size-quarter + + description: + span: right + label: rainlab.builder::lang.migration.field_description + cssClass: size-three-quarter + + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexversionsoperations/partials/_toolbar.php + cssClass: collapse-visible + +secondaryTabs: + stretch: true + fields: + code: + tab: rainlab.builder::lang.migration.field_code + stretch: true + type: codeeditor + language: php diff --git a/plugins/rainlab/builder/models/migrationmodel/templates/migration.php.tpl b/plugins/rainlab/builder/models/migrationmodel/templates/migration.php.tpl new file mode 100644 index 0000000..c53eb39 --- /dev/null +++ b/plugins/rainlab/builder/models/migrationmodel/templates/migration.php.tpl @@ -0,0 +1,19 @@ + [ + \{{ relation.class }}::class, +{% for prop, value in relation.props %} + '{{ prop }}' => {{ value|raw }}{% if not loop.last %}, +{% endif %} +{% endfor %} + + ]{% if not loop.last %}, +{% endif %} +{% endfor %} + + ]; \ No newline at end of file diff --git a/plugins/rainlab/builder/models/modelmodel/templates/settingmodel.php.tpl b/plugins/rainlab/builder/models/modelmodel/templates/settingmodel.php.tpl new file mode 100644 index 0000000..81f97cf --- /dev/null +++ b/plugins/rainlab/builder/models/modelmodel/templates/settingmodel.php.tpl @@ -0,0 +1,23 @@ + '{{ value }}'{% if not loop.last %}, +{% endif %} +{% endfor %} + + ]; +{% endif %}{% if validation.attributeNames %} + + /** + * @var array attributeNames for validation. + */ + public $attributeNames = [ +{% for attr, value in validation.attributeNames %} + '{{ attr }}' => '{{ value }}'{% if not loop.last %}, +{% endif %} +{% endfor %} + + ]; +{% endif %}{% if validation.customMessages %} + + /** + * @var array customMessages for validation. + */ + public $customMessages = [ +{% for msg, value in validation.customMessages %} + '{{ msg }}' => '{{ value }}'{% if not loop.last %}, +{% endif %} +{% endfor %} + + ]; +{% endif %}{% else %} + + /** + * @var array rules for validation. + */ + public $rules = [ + ]; +{% endif %} \ No newline at end of file diff --git a/plugins/rainlab/builder/models/permissionsmodel/fields.yaml b/plugins/rainlab/builder/models/permissionsmodel/fields.yaml new file mode 100644 index 0000000..597d121 --- /dev/null +++ b/plugins/rainlab/builder/models/permissionsmodel/fields.yaml @@ -0,0 +1,31 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + toolbar: + type: partial + path: $/rainlab/builder/behaviors/indexpermissionsoperations/partials/_toolbar.php + cssClass: collapse-visible + +tabs: + cssClass: master-area + stretch: true + fields: + permissions: + stretch: true + cssClass: frameless + tab: rainlab.builder::lang.permission.form_tab_permissions + type: datatable + btnAddRowLabel: rainlab.builder::lang.permission.btn_add_permission + btnDeleteRowLabel: rainlab.builder::lang.permission.btn_delete_permission + columns: + permission: + title: rainlab.builder::lang.permission.column_permission_label + type: string + tab: + title: rainlab.builder::lang.permission.column_tab_label + type: builderLocalization + label: + title: rainlab.builder::lang.permission.column_label_label + type: builderLocalization diff --git a/plugins/rainlab/builder/models/pluginbasemodel/fields.yaml b/plugins/rainlab/builder/models/pluginbasemodel/fields.yaml new file mode 100644 index 0000000..48ef103 --- /dev/null +++ b/plugins/rainlab/builder/models/pluginbasemodel/fields.yaml @@ -0,0 +1,57 @@ +tabs: + fields: + content: + type: hint + path: $/rainlab/builder/behaviors/indexpluginoperations/partials/_plugin-update-hint.php + tab: rainlab.builder::lang.plugin.tab_general + context: [update] + + name: + span: left + label: rainlab.builder::lang.plugin.field_name + required: true + tab: rainlab.builder::lang.plugin.tab_general + + author: + span: right + label: rainlab.builder::lang.plugin.field_author + tab: rainlab.builder::lang.plugin.tab_general + required: true + + namespace: + context: [create] + span: left + label: rainlab.builder::lang.plugin.field_plugin_namespace + commentAbove: rainlab.builder::lang.plugin.field_namespace_description + tab: rainlab.builder::lang.plugin.tab_general + required: true + preset: + field: name + type: namespace + + author_namespace: + context: [create] + span: right + label: rainlab.builder::lang.plugin.field_author_namespace + commentAbove: rainlab.builder::lang.plugin.field_author_namespace_description + tab: rainlab.builder::lang.plugin.tab_general + required: true + preset: + field: author + type: namespace + + icon: + type: dropdown + label: rainlab.builder::lang.plugin.field_icon + commentAbove: rainlab.builder::lang.common.field_icon_description + tab: rainlab.builder::lang.plugin.tab_general + + description: + label: rainlab.builder::lang.plugin.field_description + type: textarea + size: tiny + tab: rainlab.builder::lang.plugin.tab_description + + homepage: + label: rainlab.builder::lang.plugin.field_homepage + tab: rainlab.builder::lang.plugin.tab_description \ No newline at end of file diff --git a/plugins/rainlab/builder/models/pluginbasemodel/templates/lang.php.tpl b/plugins/rainlab/builder/models/pluginbasemodel/templates/lang.php.tpl new file mode 100644 index 0000000..54894bc --- /dev/null +++ b/plugins/rainlab/builder/models/pluginbasemodel/templates/lang.php.tpl @@ -0,0 +1,6 @@ + [ + 'name' => '{pluginNameSanitized}', + 'description' => '{pluginDescriptionSanitized}' + ] +]; \ No newline at end of file diff --git a/plugins/rainlab/builder/models/pluginbasemodel/templates/plugin.php.tpl b/plugins/rainlab/builder/models/pluginbasemodel/templates/plugin.php.tpl new file mode 100644 index 0000000..9f41fa4 --- /dev/null +++ b/plugins/rainlab/builder/models/pluginbasemodel/templates/plugin.php.tpl @@ -0,0 +1,37 @@ + + + The coding standard for October CMS Plugins. + + + + + + + + + + + */tests/* + + + + + + + . + */assets/* + diff --git a/plugins/rainlab/builder/phpunit.xml b/plugins/rainlab/builder/phpunit.xml new file mode 100644 index 0000000..db31479 --- /dev/null +++ b/plugins/rainlab/builder/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./tests/unit + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/rainlab-builder.mix.js b/plugins/rainlab/builder/rainlab-builder.mix.js new file mode 100644 index 0000000..e58f4fa --- /dev/null +++ b/plugins/rainlab/builder/rainlab-builder.mix.js @@ -0,0 +1,36 @@ +/* + |-------------------------------------------------------------------------- + | Mix Asset Management + |-------------------------------------------------------------------------- + | + | Mix provides a clean, fluent API for defining some Webpack build steps + | for your theme assets. By default, we are compiling the CSS + | file for the application as well as bundling up all the JS files. + | + */ + +module.exports = (mix) => { + mix.less('plugins/rainlab/builder/assets/less/builder.less', 'plugins/rainlab/builder/assets/css/'); + + mix.combine([ + 'plugins/rainlab/builder/assets/js/builder.dataregistry.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.base.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.plugin.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.databasetable.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.model.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.modelform.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.modellist.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.permission.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.menus.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.imports.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.code.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.version.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.localization.js', + 'plugins/rainlab/builder/assets/js/builder.index.entity.controller.js', + 'plugins/rainlab/builder/assets/js/builder.index.js', + 'plugins/rainlab/builder/assets/js/builder.localizationinput.js', + 'plugins/rainlab/builder/assets/js/builder.inspector.editor.localization.js', + 'plugins/rainlab/builder/assets/js/builder.table.processor.localization.js', + 'plugins/rainlab/builder/assets/js/builder.codelist.js' + ], 'plugins/rainlab/builder/assets/js/build-min.js'); +} diff --git a/plugins/rainlab/builder/rules/Reserved.php b/plugins/rainlab/builder/rules/Reserved.php new file mode 100644 index 0000000..8b4eb10 --- /dev/null +++ b/plugins/rainlab/builder/rules/Reserved.php @@ -0,0 +1,140 @@ +passes($attribute, $value); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return !in_array(strtolower($value), $this->reserved); + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return Lang::get('rainlab.builder::lang.validation.reserved'); + } +} diff --git a/plugins/rainlab/builder/tests/TestCase.php b/plugins/rainlab/builder/tests/TestCase.php new file mode 100644 index 0000000..c441f6c --- /dev/null +++ b/plugins/rainlab/builder/tests/TestCase.php @@ -0,0 +1,2 @@ +cleanUp(); + } + + public function tearDown() : void + { + $this->cleanUp(); + } + + public function testGenerate() + { + $generatedDir = $this->getFixturesDir('temporary/generated'); + $this->assertFileNotExists($generatedDir); + + File::makeDirectory($generatedDir, 0777, true, true); + $this->assertFileExists($generatedDir); + + $structure = [ + 'author', + 'author/plugin', + 'author/plugin/plugin.php' => 'plugin.php.tpl', + 'author/plugin/classes' + ]; + + $templatesDir = $this->getFixturesDir('templates'); + $generator = new FilesystemGenerator($generatedDir, $structure, $templatesDir); + + $variables = [ + 'authorNamespace' => 'Author', + 'pluginNamespace' => 'Plugin' + ]; + $generator->setVariables($variables); + $generator->setVariable('className', 'TestClass'); + + $generator->generate(); + + $this->assertFileExists($generatedDir.'/author/plugin/plugin.php'); + $this->assertFileExists($generatedDir.'/author/plugin/classes'); + + $content = file_get_contents($generatedDir.'/author/plugin/plugin.php'); + $this->assertContains('Author\Plugin', $content); + $this->assertContains('TestClass', $content); + } + + /** + * @expectedException October\Rain\Exception\SystemException + * @expectedExceptionMessage exists + */ + public function testDestNotExistsException() + { + $dir = $this->getFixturesDir('temporary/null'); + $generator = new FilesystemGenerator($dir, []); + $generator->generate(); + } + + /** + * @expectedException October\Rain\Exception\ApplicationException + * @expectedExceptionMessage exists + */ + public function testDirExistsException() + { + $generatedDir = $this->getFixturesDir('temporary/generated'); + $this->assertFileNotExists($generatedDir); + + File::makeDirectory($generatedDir.'/plugin', 0777, true, true); + $this->assertFileExists($generatedDir.'/plugin'); + + $structure = [ + 'plugin' + ]; + + $generator = new FilesystemGenerator($generatedDir, $structure); + $generator->generate(); + } + + /** + * @expectedException October\Rain\Exception\ApplicationException + * @expectedExceptionMessage exists + */ + public function testFileExistsException() + { + $generatedDir = $this->getFixturesDir('temporary/generated'); + $this->assertFileNotExists($generatedDir); + + File::makeDirectory($generatedDir, 0777, true, true); + $this->assertFileExists($generatedDir); + + File::put($generatedDir.'/plugin.php', 'contents'); + $this->assertFileExists($generatedDir.'/plugin.php'); + + $structure = [ + 'plugin.php' => 'plugin.php.tpl' + ]; + + $generator = new FilesystemGenerator($generatedDir, $structure); + $generator->generate(); + } + + /** + * @expectedException October\Rain\Exception\SystemException + * @expectedExceptionMessage found + */ + public function testTemplateNotFound() + { + $generatedDir = $this->getFixturesDir('temporary/generated'); + $this->assertFileNotExists($generatedDir); + + File::makeDirectory($generatedDir, 0777, true, true); + $this->assertFileExists($generatedDir); + + $structure = [ + 'plugin.php' => 'null.tpl' + ]; + + $generator = new FilesystemGenerator($generatedDir, $structure); + $generator->generate(); + } + + protected function getFixturesDir($subdir) + { + $result = __DIR__.'/../../fixtures/filesystemgenerator'; + + if (strlen($subdir)) { + $result .= '/'.$subdir; + } + + return $result; + } + + protected function cleanUp() + { + $generatedDir = $this->getFixturesDir('temporary/generated'); + File::deleteDirectory($generatedDir); + } +} diff --git a/plugins/rainlab/builder/tests/unit/classes/ModelModelTest.php b/plugins/rainlab/builder/tests/unit/classes/ModelModelTest.php new file mode 100644 index 0000000..8556a2f --- /dev/null +++ b/plugins/rainlab/builder/tests/unit/classes/ModelModelTest.php @@ -0,0 +1,68 @@ +assertTrue(ModelModel::validateModelClassName($unQualifiedClassName)); + + $qualifiedClassName = 'RainLab\Builder\Models\Settings'; + $this->assertTrue(ModelModel::validateModelClassName($qualifiedClassName)); + + $fullyQualifiedClassName = '\RainLab\Builder\Models\Settings'; + $this->assertTrue(ModelModel::validateModelClassName($fullyQualifiedClassName)); + + $qualifiedClassNameStartingWithLowerCase = 'rainLab\Builder\Models\Settings'; + $this->assertTrue(ModelModel::validateModelClassName($qualifiedClassNameStartingWithLowerCase)); + } + + public function testInvalidateModelClassName() + { + $unQualifiedClassName = 'myClassName'; // starts with lower case + $this->assertFalse(ModelModel::validateModelClassName($unQualifiedClassName)); + + $qualifiedClassName = 'MyNameSpace\MyPlugin\Models\MyClassName'; // namespace\class doesn't exist + $this->assertFalse(ModelModel::validateModelClassName($qualifiedClassName)); + + $fullyQualifiedClassName = '\MyNameSpace\MyPlugin\Models\MyClassName'; // namespace\class doesn't exist + $this->assertFalse(ModelModel::validateModelClassName($fullyQualifiedClassName)); + } + + public function testGetModelFields() + { + // Invalid Class Name + try { + ModelModel::getModelFields(null, 'myClassName'); + } catch (SystemException $e) { + $this->assertEquals($e->getMessage(), 'Invalid model class name: myClassName'); + return; + } + + // Directory Not Found + $pluginCodeObj = PluginCode::createFromNamespace('MyNameSpace\MyPlugin\Models\MyClassName'); + $this->assertEquals([], ModelModel::getModelFields($pluginCodeObj, 'MyClassName')); + + // Directory Found, but Class Not Found + $pluginCodeObj = PluginCode::createFromNamespace('RainLab\Builder\Models\MyClassName'); + $this->assertEquals([], ModelModel::getModelFields($pluginCodeObj, 'MyClassName')); + + // Model without Table Name + $pluginCodeObj = PluginCode::createFromNamespace('RainLab\Builder\Models\Settings'); + $this->assertEquals([], ModelModel::getModelFields($pluginCodeObj, 'Settings')); + + // Model with Table Name + copy(__DIR__."/../../fixtures/MyMock.php", __DIR__."/../../../models/MyMock.php"); + $pluginCodeObj = PluginCode::createFromNamespace('RainLab\Builder\Models\MyMock'); + $this->assertEquals([], ModelModel::getModelFields($pluginCodeObj, 'MyMock')); + } +} diff --git a/plugins/rainlab/builder/tests/unit/phpunit.xml b/plugins/rainlab/builder/tests/unit/phpunit.xml new file mode 100644 index 0000000..43f7f24 --- /dev/null +++ b/plugins/rainlab/builder/tests/unit/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./ + + + + + + + + \ No newline at end of file diff --git a/plugins/rainlab/builder/updates/version.yaml b/plugins/rainlab/builder/updates/version.yaml new file mode 100644 index 0000000..36298cc --- /dev/null +++ b/plugins/rainlab/builder/updates/version.yaml @@ -0,0 +1,48 @@ +v1.0.1: Initialize plugin. +v1.0.2: Fixes the problem with selecting a plugin. Minor localization corrections. Configuration files in the list and form behaviors are now autocomplete. +v1.0.3: Improved handling of the enum data type. +v1.0.4: Added user permissions to work with the Builder. +v1.0.5: Fixed permissions registration. +v1.0.6: Fixed front-end record ordering in the Record List component. +v1.0.7: Builder settings are now protected with user permissions. The database table column list is scrollable now. Minor code cleanup. +v1.0.8: Added the Reorder Controller behavior. +v1.0.9: Minor API and UI updates. +v1.0.10: Minor styling update. +v1.0.11: Fixed a bug where clicking placeholder in a repeater would open Inspector. Fixed a problem with saving forms with repeaters in tabs. Minor style fix. +v1.0.12: Added support for the Trigger property to the Media Finder widget configuration. Names of form fields and list columns definition files can now contain underscores. +v1.0.13: Minor styling fix on the database editor. +v1.0.14: Added support for published_at timestamp field +v1.0.15: Fixed a bug where saving a localization string in Inspector could cause a JavaScript error. Added support for Timestamps and Soft Deleting for new models. +v1.0.16: Fixed a bug when saving a form with the Repeater widget in a tab could create invalid fields in the form's outside area. Added a check that prevents creating localization strings inside other existing strings. +v1.0.17: Added support Trigger attribute support for RecordFinder and Repeater form widgets. +v1.0.18: Fixes a bug where '::class' notations in a model class definition could prevent the model from appearing in the Builder model list. Added emptyOption property support to the dropdown form control. +v1.0.19: Added a feature allowing to add all database columns to a list definition. Added max length validation for database table and column names. +v1.0.20: Fixes a bug where form the builder could trigger the "current.hasAttribute is not a function" error. +v1.0.21: Back-end navigation sort order updated. +v1.0.22: Added scopeValue property to the RecordList component. +v1.0.23: Added support for balloon-selector field type, added Brazilian Portuguese translation, fixed some bugs +v1.0.24: Added support for tag list field type, added read only toggle for fields. Prevent plugins from using reserved PHP keywords for class names and namespaces +v1.0.25: Allow editing of migration code in the "Migration" popup when saving changes in the database editor. +v1.0.26: Allow special default values for columns and added new "Add ID column" button to database editor. +v1.0.27: Added ability to use 'scope' in a form relation field, added ability to change the sort order of versions and added additional properties for repeater widget in form builder. Added Polish translation. +v1.0.28: Fixes support for PHP 8 +v1.0.29: Disable touch device detection +v1.0.30: Minor styling improvements +v1.0.31: Added support for more rich editor and file upload properties +v1.0.32: Minor styling improvements +v1.1.0: Adds feature for adding database fields to a form definition. +v1.1.1: Adds DBAL timestamp column type. Adds database prefix support. Fixes various bugs. +v1.1.2: Compatibility with October CMS v2.2 +v1.1.3: Adds comment support to database tables. +v1.1.4: Fixes duplication bug saving backend menu permissions. +v1.2.0: Improve support with October v3.0 +v1.2.2: Compatibility updates. +v1.2.3: Fixes issue when removing items from permissions and menus. +v1.2.5: Fixes validator conflict with other plugins. +v1.2.6: Compatibility with October v3.1 +v2.0.1: Adds Tailor blueprint importer and code editor. +v2.0.2: Fixes visual bug when tab fields overflow. +v2.0.3: Fixes missing import in CMS components. +v2.0.4: Fixes bad method name in controller model. +v2.0.5: Fixes bug adding data table controls. +v2.0.6: Fixes importing Tailor multisite globals. diff --git a/plugins/rainlab/builder/widgets/CodeList.php b/plugins/rainlab/builder/widgets/CodeList.php new file mode 100644 index 0000000..91bbc73 --- /dev/null +++ b/plugins/rainlab/builder/widgets/CodeList.php @@ -0,0 +1,821 @@ +alias = $alias; + $this->selectionInputName = 'file'; + $this->assetExtensions = FileDefinitions::get('assetExtensions'); + + parent::__construct($controller, []); + + if (!Request::ajax()) { + $this->resetSelection(); + } + + $this->bindToController(); + } + + /** + * Renders the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', [ + 'items' => $this->getData(), + 'pluginCode' => $this->getPluginCode() + ]); + } + + /** + * onOpenDirectory + */ + public function onOpenDirectory() + { + $path = Input::get('path'); + if (!$this->validatePath($path)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + $this->putSession('currentPath', $path); + + return [ + '#'.$this->getId('code-list') => $this->makePartial('items', ['items' => $this->getData()]) + ]; + } + + /** + * refreshActivePlugin + */ + public function refreshActivePlugin() + { + $this->plugin = null; + + return [ + '#'.$this->getId('body') => $this->makePartial('widget-contents', [ + 'items' => $this->getData(), + 'pluginCode' => $this->getPluginCode() + ]) + ]; + } + + /** + * onRefresh + */ + public function onRefresh() + { + return [ + '#'.$this->getId('code-list') => $this->makePartial('items', ['items' => $this->getData()]) + ]; + } + + /** + * onUpdate + */ + public function onUpdate() + { + $this->extendSelection(); + + return $this->onRefresh(); + } + + /** + * onDeleteFiles + */ + public function onDeleteFiles() + { + $fileList = Request::input('file'); + $error = null; + $deleted = []; + + try { + $assetsPath = $this->getAssetsPath(); + + foreach ($fileList as $path => $selected) { + if ($selected) { + if (!$this->validatePath($path)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + $fullPath = $assetsPath.'/'.$path; + if (File::exists($fullPath)) { + if (!File::isDirectory($fullPath)) { + if (!@File::delete($fullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_file', + ['name' => $path] + )); + } + } + else { + $empty = File::isDirectoryEmpty($fullPath); + if ($empty === false) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_dir_not_empty', + ['name' => $path] + )); + } + + if (!@rmdir($fullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_dir', + ['name' => $path] + )); + } + } + + $deleted[] = $path; + $this->removeSelection($path); + } + } + } + } + catch (Exception $ex) { + $error = $ex->getMessage(); + } + + return [ + 'deleted' => $deleted, + 'error' => $error, + 'plugin_code' => Request::input('plugin_code') + ]; + } + + /** + * onLoadRenamePopup + */ + public function onLoadRenamePopup() + { + $path = Input::get('renamePath'); + if (!$this->validatePath($path)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + $this->vars['originalPath'] = $path; + $this->vars['name'] = basename($path); + + return $this->makePartial('rename_form'); + } + + /** + * onApplyName + */ + public function onApplyName() + { + $newName = trim(Input::get('name')); + if (!strlen($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty')); + } + + if (!$this->validatePath($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + if (!$this->validateName($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name')); + } + + $originalPath = Input::get('originalPath'); + if (!$this->validatePath($originalPath)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + $originalFullPath = $this->getFullPath($originalPath); + if (!file_exists($originalFullPath)) { + throw new ApplicationException(Lang::get('cms::lang.asset.original_not_found')); + } + + if (!is_dir($originalFullPath) && !$this->validateFileType($newName)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.type_not_allowed', + ['allowed_types' => implode(', ', $this->assetExtensions)] + )); + } + + $newFullPath = $this->getFullPath(dirname($originalPath).'/'.$newName); + if (file_exists($newFullPath) && $newFullPath !== $originalFullPath) { + throw new ApplicationException(Lang::get('cms::lang.asset.already_exists')); + } + + if (!@rename($originalFullPath, $newFullPath)) { + throw new ApplicationException(Lang::get('cms::lang.asset.error_renaming')); + } + + return [ + '#'.$this->getId('code-list') => $this->makePartial('items', ['items' => $this->getData()]) + ]; + } + + /** + * onLoadNewDirPopup + */ + public function onLoadNewDirPopup() + { + return $this->makePartial('new_dir_form'); + } + + /** + * onNewDirectory + */ + public function onNewDirectory() + { + $newName = trim(Input::get('name')); + if (!strlen($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.name_cant_be_empty')); + } + + if (!$this->validatePath($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + if (!$this->validateName($newName)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_name')); + } + + $newFullPath = $this->getCurrentPath().'/'.$newName; + if (file_exists($newFullPath)) { + throw new ApplicationException(Lang::get('cms::lang.asset.already_exists')); + } + + if (!File::makeDirectory($newFullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.error_creating_directory', + ['name' => $newName] + )); + } + + return [ + '#'.$this->getId('code-list') => $this->makePartial('items', ['items' => $this->getData()]) + ]; + } + + /** + * onLoadMovePopup + */ + public function onLoadMovePopup() + { + $fileList = Request::input('file'); + $directories = []; + + $selectedList = array_filter($fileList, function ($value) { + return $value == 1; + }); + + $this->listDestinationDirectories($directories, $selectedList); + + $this->vars['directories'] = $directories; + $this->vars['selectedList'] = base64_encode(json_encode(array_keys($selectedList))); + + return $this->makePartial('move_form'); + } + + /** + * onMove + */ + public function onMove() + { + $selectedList = Input::get('selectedList'); + if (!strlen($selectedList)) { + throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found')); + } + + $destinationDir = Input::get('dest'); + if (!strlen($destinationDir)) { + throw new ApplicationException(Lang::get('cms::lang.asset.select_destination_dir')); + } + + $destinationFullPath = $this->getFullPath($destinationDir); + if (!file_exists($destinationFullPath) || !is_dir($destinationFullPath)) { + throw new ApplicationException(Lang::get('cms::lang.asset.destination_not_found')); + } + + $list = @json_decode(@base64_decode($selectedList)); + if ($list === false) { + throw new ApplicationException(Lang::get('cms::lang.asset.selected_files_not_found')); + } + + foreach ($list as $path) { + if (!$this->validatePath($path)) { + throw new ApplicationException(Lang::get('cms::lang.asset.invalid_path')); + } + + $basename = basename($path); + $originalFullPath = $this->getFullPath($path); + $newFullPath = realpath(rtrim($destinationFullPath, '/')) . '/' . $basename; + $safeDir = $this->getAssetsPath(); + + if ($originalFullPath == $newFullPath) { + continue; + } + + if (!starts_with($newFullPath, $safeDir)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_moving_file', + ['file' => $basename] + )); + } + + if (is_file($originalFullPath)) { + if (!@File::move($originalFullPath, $newFullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_moving_file', + ['file' => $basename] + )); + } + } + elseif (is_dir($originalFullPath)) { + if (!@File::copyDirectory($originalFullPath, $newFullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_moving_directory', + ['dir' => $basename] + )); + } + + if (strpos($originalFullPath, '../') !== false) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_directory', + ['dir' => $basename] + )); + } + + if (strpos($originalFullPath, $safeDir) !== 0) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_directory', + ['dir' => $basename] + )); + } + + if (!@File::deleteDirectory($originalFullPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.error_deleting_directory', + ['dir' => $basename] + )); + } + } + } + + return [ + '#'.$this->getId('code-list') => $this->makePartial('items', ['items' => $this->getData()]) + ]; + } + + /** + * onSearch + */ + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + + $this->extendSelection(); + + return $this->onRefresh(); + } + + /** + * getData + */ + protected function getData() + { + $assetsPath = $this->getAssetsPath(); + + if (!file_exists($assetsPath) || !is_dir($assetsPath)) { + if (!File::makeDirectory($assetsPath)) { + throw new ApplicationException(Lang::get( + 'cms::lang.cms_object.error_creating_directory', + ['name' => $assetsPath] + )); + } + } + + $searchTerm = Str::lower($this->getSearchTerm()); + + if (!strlen($searchTerm)) { + $currentPath = $this->getCurrentPath(); + return $this->getDirectoryContents( + new DirectoryIterator($currentPath) + ); + } + + return $this->findFiles(); + } + + /** + * getAssetsPath + */ + protected function getAssetsPath() + { + return base_path('plugins/'.$this->getActivePluginObj()?->toFilesystemPath()); + } + + /** + * getPluginFileUrl + */ + protected function getPluginFileUrl($path) + { + return Url::to('plugins/'.$this->getActivePluginObj()?->toFilesystemPath().$path); + } + + /** + * getCurrentRelativePath + */ + public function getCurrentRelativePath() + { + $path = $this->getSession('currentPath', '/'); + + if (!$this->validatePath($path)) { + return null; + } + + if ($path == '.') { + return null; + } + + return ltrim($path, '/'); + } + + /** + * getCurrentPath + */ + protected function getCurrentPath() + { + $assetsPath = $this->getAssetsPath(); + + $path = $assetsPath.'/'.$this->getCurrentRelativePath(); + if (!is_dir($path)) { + return $assetsPath; + } + + return $path; + } + + /** + * getRelativePath + */ + protected function getRelativePath($path) + { + $prefix = $this->getAssetsPath(); + + if (substr($path, 0, strlen($prefix)) == $prefix) { + $path = substr($path, strlen($prefix)); + } + + return $path; + } + + /** + * getFullPath + */ + protected function getFullPath($path) + { + return $this->getAssetsPath().'/'.ltrim($path, '/'); + } + + /** + * validatePath + */ + protected function validatePath($path) + { + if (!preg_match('/^[0-9a-z\.\s_\-\/]+$/i', $path)) { + return false; + } + + if (strpos($path, '..') !== false || strpos($path, './') !== false) { + return false; + } + + return true; + } + + /** + * validateName + */ + protected function validateName($name) + { + if (!preg_match('/^[0-9a-z\.\s_\-]+$/i', $name)) { + return false; + } + + if (strpos($name, '..') !== false) { + return false; + } + + return true; + } + + /** + * getDirectoryContents + */ + protected function getDirectoryContents($dir) + { + $editableAssetTypes = CodeFileModel::getEditableExtensions(); + + $result = []; + $files = []; + + foreach ($dir as $node) { + if (substr($node->getFileName(), 0, 1) == '.') { + continue; + } + + if ($node->isDir() && !$node->isDot()) { + $result[$node->getFilename()] = (object)[ + 'type' => 'directory', + 'path' => File::normalizePath($this->getRelativePath($node->getPathname())), + 'name' => $node->getFilename(), + 'editable' => false + ]; + } + elseif ($node->isFile()) { + $files[] = (object)[ + 'type' => 'file', + 'path' => File::normalizePath($this->getRelativePath($node->getPathname())), + 'name' => $node->getFilename(), + 'editable' => in_array(strtolower($node->getExtension()), $editableAssetTypes) + ]; + } + } + + foreach ($files as $file) { + $result[] = $file; + } + + return $result; + } + + /** + * listDestinationDirectories + */ + protected function listDestinationDirectories(&$result, $excludeList, $startDir = null, $level = 0) + { + if ($startDir === null) { + $startDir = $this->getAssetsPath(); + + $result['/'] = 'assets'; + $level = 1; + } + + $dirs = new DirectoryIterator($startDir); + foreach ($dirs as $node) { + if (substr($node->getFileName(), 0, 1) == '.') { + continue; + } + + if ($node->isDir() && !$node->isDot()) { + $fullPath = $node->getPathname(); + $relativePath = $this->getRelativePath($fullPath); + if (array_key_exists($relativePath, $excludeList)) { + continue; + } + + $result[$relativePath] = str_repeat(' ', $level * 4).$node->getFilename(); + + $this->listDestinationDirectories($result, $excludeList, $fullPath, $level+1); + } + } + } + + /** + * getSearchTerm + */ + protected function getSearchTerm() + { + return $this->searchTerm !== false ? $this->searchTerm : $this->getSession('search'); + } + + /** + * isSearchMode + */ + protected function isSearchMode() + { + return strlen($this->getSearchTerm()); + } + + /** + * getUpPath + */ + protected function getUpPath() + { + $path = $this->getCurrentRelativePath(); + if (!strlen(rtrim(ltrim($path, '/'), '/'))) { + return null; + } + + return dirname($path); + } + + /** + * getPluginCode + */ + protected function getPluginCode() + { + return $this->getActivePluginObj()?->toCode(); + } + + /** + * getActivePluginObj + */ + protected function getActivePluginObj() + { + if ($this->plugin !== null) { + return $this->plugin; + } + + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + if (!$activePluginVector) { + return null; + } + + return $this->plugin = $activePluginVector->pluginCodeObj; + } + + /** + * validateFileType checks for valid asset file extension + * @param string + * @return bool + */ + protected function validateFileType($name) + { + $extension = strtolower(File::extension($name)); + + if (!in_array($extension, $this->assetExtensions)) { + return false; + } + + return true; + } + + public function onUpload() + { + $fileName = null; + + try { + $uploadedFile = Input::file('file_data'); + + if (!is_object($uploadedFile)) { + return; + } + + $fileName = $uploadedFile->getClientOriginalName(); + + // Check valid upload + if (!$uploadedFile->isValid()) { + throw new ApplicationException(Lang::get('cms::lang.asset.file_not_valid')); + } + + // Check file size + $maxSize = UploadedFile::getMaxFilesize(); + if ($uploadedFile->getSize() > $maxSize) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.too_large', + ['max_size' => File::sizeToString($maxSize)] + )); + } + + // Check for valid file extensions + if (!$this->validateFileType($fileName)) { + throw new ApplicationException(Lang::get( + 'cms::lang.asset.type_not_allowed', + ['allowed_types' => implode(', ', $this->assetExtensions)] + )); + } + + // Accept the uploaded file + $uploadedFile = $uploadedFile->move($this->getCurrentPath(), $uploadedFile->getClientOriginalName()); + + File::chmod($uploadedFile->getRealPath()); + + $response = Response::make('success'); + } + catch (Exception $ex) { + $message = $fileName !== null + ? Lang::get('cms::lang.asset.error_uploading_file', ['name' => $fileName, 'error' => $ex->getMessage()]) + : $ex->getMessage(); + + $response = Response::make($message); + } + + // Override the controller response + $this->controller->setResponse($response); + } + + /** + * setSearchTerm + */ + protected function setSearchTerm($term) + { + $this->searchTerm = trim($term); + $this->putSession('search', $this->searchTerm); + } + + /** + * findFiles + */ + protected function findFiles() + { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($this->getAssetsPath(), RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST, + RecursiveIteratorIterator::CATCH_GET_CHILD + ); + + $editableAssetTypes = CodeFileModel::getEditableExtensions(); + $searchTerm = Str::lower($this->getSearchTerm()); + $words = explode(' ', $searchTerm); + + $result = []; + foreach ($iterator as $item) { + if (!$item->isDir()) { + if (substr($item->getFileName(), 0, 1) == '.') { + continue; + } + + $path = $this->getRelativePath($item->getPathname()); + + if ($this->pathMatchesSearch($words, $path)) { + $result[] = (object)[ + 'type' => 'file', + 'path' => File::normalizePath($path), + 'name' => $item->getFilename(), + 'editable' => in_array(strtolower($item->getExtension()), $editableAssetTypes) + ]; + } + } + } + + return $result; + } + + /** + * pathMatchesSearch + */ + protected function pathMatchesSearch(&$words, $path) + { + foreach ($words as $word) { + $word = trim($word); + if (!strlen($word)) { + continue; + } + + if (!Str::contains(Str::lower($path), $word)) { + return false; + } + } + + return true; + } +} diff --git a/plugins/rainlab/builder/widgets/ControllerList.php b/plugins/rainlab/builder/widgets/ControllerList.php new file mode 100644 index 0000000..515303e --- /dev/null +++ b/plugins/rainlab/builder/widgets/ControllerList.php @@ -0,0 +1,124 @@ +alias = $alias; + + parent::__construct($controller, []); + $this->bindToController(); + } + + /** + * render the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + /** + * updateList + */ + public function updateList() + { + return [ + '#'.$this->getId('plugin-controller-list') => $this->makePartial('items', $this->getRenderData()) + ]; + } + + /** + * refreshActivePlugin + */ + public function refreshActivePlugin() + { + return [ + '#'.$this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData()) + ]; + } + + /** + * onUpdate + */ + public function onUpdate() + { + return $this->updateList(); + } + + /** + * onSearch + */ + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /** + * getControllerList + */ + protected function getControllerList($pluginCode) + { + $result = ControllerModel::listPluginControllers($pluginCode); + + return $result; + } + + /** + * getRenderData + */ + protected function getRenderData() + { + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + if (!$activePluginVector) { + return [ + 'pluginVector' => null, + 'items' => [] + ]; + } + + $items = $this->getControllerList($activePluginVector->pluginCodeObj); + + $searchTerm = Str::lower($this->getSearchTerm()); + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($items as $controller) { + if ($this->textMatchesSearch($words, $controller)) { + $result[] = $controller; + } + } + + $items = $result; + } + + return [ + 'pluginVector' => $activePluginVector, + 'items' => $items + ]; + } +} diff --git a/plugins/rainlab/builder/widgets/DatabaseTableList.php b/plugins/rainlab/builder/widgets/DatabaseTableList.php new file mode 100644 index 0000000..a0fa092 --- /dev/null +++ b/plugins/rainlab/builder/widgets/DatabaseTableList.php @@ -0,0 +1,118 @@ +alias = $alias; + + parent::__construct($controller, []); + $this->bindToController(); + } + + /** + * Renders the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + public function updateList() + { + return ['#'.$this->getId('database-table-list') => $this->makePartial('items', $this->getRenderData())]; + } + + public function refreshActivePlugin() + { + return ['#'.$this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData())]; + } + + /* + * Event handlers + */ + + public function onUpdate() + { + return $this->updateList(); + } + + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /* + * Methods for the internal use + */ + + protected function getData($pluginVector) + { + if (!$pluginVector) { + return []; + } + + $pluginCode = $pluginVector->pluginCodeObj->toCode(); + + if (!$pluginCode) { + return []; + } + + $tables = $this->getTableList($pluginCode); + $searchTerm = Str::lower($this->getSearchTerm()); + + // Apply the search + // + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($tables as $table) { + if ($this->textMatchesSearch($words, $table)) { + $result[] = $table; + } + } + + $tables = $result; + } + + return $tables; + } + + protected function getTableList($pluginCode) + { + $result = DatabaseTableModel::listPluginTables($pluginCode); + + return $result; + } + + protected function getRenderData() + { + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + + return [ + 'pluginVector'=>$activePluginVector, + 'items'=>$this->getData($activePluginVector) + ]; + } +} diff --git a/plugins/rainlab/builder/widgets/DefaultBehaviorDesignTimeProvider.php b/plugins/rainlab/builder/widgets/DefaultBehaviorDesignTimeProvider.php new file mode 100644 index 0000000..f8fe86f --- /dev/null +++ b/plugins/rainlab/builder/widgets/DefaultBehaviorDesignTimeProvider.php @@ -0,0 +1,218 @@ + 'form-controller', + \Backend\Behaviors\ListController::class => 'list-controller', + \Backend\Behaviors\ImportExportController::class => 'import-export-controller' + ]; + + /** + * Renders behavior body. + * @param string $class Specifies the behavior class to render. + * @param array $properties Behavior property values. + * @param \RainLab\Builder\FormWidgets\ControllerBuilder $controllerBuilder ControllerBuilder widget instance. + * @return string Returns HTML markup string. + */ + public function renderBehaviorBody($class, $properties, $controllerBuilder) + { + if (!array_key_exists($class, $this->defaultBehaviorClasses)) { + return $this->renderUnknownBehavior($class, $properties); + } + + $partial = $this->defaultBehaviorClasses[$class]; + + return $this->makePartial('behavior-'.$partial, [ + 'properties'=>$properties, + 'controllerBuilder' => $controllerBuilder + ]); + } + + /** + * Returns default behavior configuration as an array. + * @param string $class Specifies the behavior class name. + * @param string $controllerModel Controller model. + * @param mixed $controllerGenerator Controller generator object. + * @return array Returns the behavior configuration array. + */ + public function getDefaultConfiguration($class, $controllerModel, $controllerGenerator) + { + if (!array_key_exists($class, $this->defaultBehaviorClasses)) { + throw new SystemException('Unknown behavior class: '.$class); + } + + switch ($class) { + case \Backend\Behaviors\FormController::class: + return $this->getFormControllerDefaultConfiguration($controllerModel, $controllerGenerator); + case \Backend\Behaviors\ListController::class: + return $this->getListControllerDefaultConfiguration($controllerModel, $controllerGenerator); + case \Backend\Behaviors\ImportExportController::class: + return $this->getImportExportControllerDefaultConfiguration($controllerModel, $controllerGenerator); + } + } + + /** + * renderUnknownControl + */ + protected function renderUnknownControl($class, $properties) + { + return $this->makePartial('behavior-unknown', [ + 'properties'=>$properties, + 'class'=>$class + ]); + } + + /** + * getFormControllerDefaultConfiguration + */ + protected function getFormControllerDefaultConfiguration($controllerModel, $controllerGenerator) + { + if (!$controllerModel->baseModelClassName) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_behavior_requires_base_model', [ + 'behavior' => 'Form Controller' + ])); + } + + $pluginCodeObj = $controllerModel->getPluginCodeObj(); + + $forms = ModelFormModel::listModelFiles($pluginCodeObj, $controllerModel->baseModelClassName); + if (!$forms) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_model_doesnt_have_forms')); + } + + $controllerUrl = $this->getControllerUrl($pluginCodeObj, $controllerModel->controller); + + $result = [ + 'name' => $controllerModel->controllerName, + 'form' => $this->getModelFilePath($pluginCodeObj, $controllerModel->baseModelClassName, $forms[0]), + 'modelClass' => $this->getFullModelClass($pluginCodeObj, $controllerModel->baseModelClassName), + 'defaultRedirect' => $controllerUrl, + 'create' => [ + 'redirect' => $controllerUrl.'/update/:id', + 'redirectClose' => $controllerUrl + ], + 'update' => [ + 'redirect' => $controllerUrl, + 'redirectClose' => $controllerUrl + ] + ]; + + return $result; + } + + /** + * getListControllerDefaultConfiguration + */ + protected function getListControllerDefaultConfiguration($controllerModel, $controllerGenerator) + { + if (!$controllerModel->baseModelClassName) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_behavior_requires_base_model', [ + 'behavior' => 'List Controller' + ])); + } + + $pluginCodeObj = $controllerModel->getPluginCodeObj(); + + $lists = ModelListModel::listModelFiles($pluginCodeObj, $controllerModel->baseModelClassName); + if (!$lists) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_model_doesnt_have_lists')); + } + + $result = [ + 'list' => $this->getModelFilePath($pluginCodeObj, $controllerModel->baseModelClassName, $lists[0]), + 'modelClass' => $this->getFullModelClass($pluginCodeObj, $controllerModel->baseModelClassName), + 'title' => $controllerModel->controller, + 'noRecordsMessage' => 'backend::lang.list.no_records', + 'showSetup' => true, + 'showCheckboxes' => true, + 'recordsPerPage' => 20, + 'toolbar' => [ + 'buttons' => 'list_toolbar', + 'search' => [ + 'prompt' => 'backend::lang.list.search_prompt' + ] + ] + ]; + + if (array_key_exists(\Backend\Behaviors\FormController::class, $controllerModel->behaviors)) { + $updateUrl = $this->getControllerUrl($pluginCodeObj, $controllerModel->controller).'/update/:id'; + $createUrl = $this->getControllerUrl($pluginCodeObj, $controllerModel->controller).'/create'; + + $result['recordUrl'] = $updateUrl; + + $controllerGenerator->setTemplateVariable('hasFormBehavior', true); + $controllerGenerator->setTemplateVariable('createUrl', $createUrl); + } + + if (in_array(\Backend\Behaviors\ImportExportController::class, $controllerModel->behaviors)) { + $importUrl = $this->getControllerUrl($pluginCodeObj, $controllerModel->controller).'/import'; + $exportUrl = $this->getControllerUrl($pluginCodeObj, $controllerModel->controller).'/export'; + $controllerGenerator->setTemplateVariable('hasImportExportBehavior', true); + $controllerGenerator->setTemplateVariable('importUrl', $importUrl); + $controllerGenerator->setTemplateVariable('exportUrl', $exportUrl); + } + + return $result; + } + + /** + * getImportExportControllerDefaultConfiguration + */ + protected function getImportExportControllerDefaultConfiguration($controllerModel, $controllerGenerator) + { + if (!$controllerModel->baseModelClassName) { + throw new ApplicationException(Lang::get('rainlab.builder::lang.controller.error_behavior_requires_base_model', [ + 'behavior' => 'Import Export Controller' + ])); + } + + $pluginCodeObj = $controllerModel->getPluginCodeObj(); + + $result = [ + 'import.title' => $controllerModel->controller, + 'import.modelClass' => $this->getFullModelClass($pluginCodeObj, $controllerModel->baseModelClassName), + 'export.title' => $controllerModel->controller, + 'export.modelClass' => $this->getFullModelClass($pluginCodeObj, $controllerModel->baseModelClassName), + ]; + + return $result; + } + + /** + * getFullModelClass + */ + protected function getFullModelClass($pluginCodeObj, $modelClassName) + { + return $pluginCodeObj->toPluginNamespace().'\\Models\\'.$modelClassName; + } + + /** + * getModelFilePath + */ + protected function getModelFilePath($pluginCodeObj, $modelClassName, $file) + { + return '$/' . $pluginCodeObj->toFilesystemPath() . '/models/' . strtolower($modelClassName) . '/' . $file; + } + + /** + * getControllerUrl + */ + protected function getControllerUrl($pluginCodeObj, $controller) + { + return $pluginCodeObj->toUrl().'/'.strtolower($controller); + } +} diff --git a/plugins/rainlab/builder/widgets/DefaultBlueprintDesignTimeProvider.php b/plugins/rainlab/builder/widgets/DefaultBlueprintDesignTimeProvider.php new file mode 100644 index 0000000..af66833 --- /dev/null +++ b/plugins/rainlab/builder/widgets/DefaultBlueprintDesignTimeProvider.php @@ -0,0 +1,123 @@ + 'entry', + \Tailor\Classes\Blueprint\StreamBlueprint::class => 'entry', + \Tailor\Classes\Blueprint\SingleBlueprint::class => 'entry', + \Tailor\Classes\Blueprint\StructureBlueprint::class => 'entry', + \Tailor\Classes\Blueprint\GlobalBlueprint::class => 'global', + ]; + + /** + * renderBlueprintBody + * @param string $class Specifies the blueprint class to render. + * @param array $properties Blueprint property values. + * @param object $blueprintObj + * @return string Returns HTML markup string. + */ + public function renderBlueprintBody($class, $properties, $blueprintObj) + { + if (!array_key_exists($class, $this->defaultBlueprintClasses)) { + return $this->renderUnknownBlueprint($class, $properties); + } + + $partial = $this->defaultBlueprintClasses[$class]; + + return $this->makePartial('blueprint-'.$partial, [ + 'properties' => $properties, + 'blueprintObj' => $blueprintObj + ]); + } + + /** + * getDefaultConfiguration returns default blueprint configuration as an array. + * @param string $class Specifies the blueprint class name. + * @param string $blueprintObj + * @param mixed $importsModel + * @return array + */ + public function getDefaultConfiguration($class, $blueprintObj, $importsModel) + { + if (!array_key_exists($class, $this->defaultBlueprintClasses)) { + throw new SystemException('Unknown blueprint class: '.$class); + } + + switch ($class) { + case \Tailor\Classes\Blueprint\EntryBlueprint::class: + case \Tailor\Classes\Blueprint\StreamBlueprint::class: + case \Tailor\Classes\Blueprint\SingleBlueprint::class: + case \Tailor\Classes\Blueprint\StructureBlueprint::class: + return $this->getEntryBlueprintDefaultConfiguration($blueprintObj, $importsModel); + case \Tailor\Classes\Blueprint\GlobalBlueprint::class: + return $this->getGlobalBlueprintDefaultConfiguration($blueprintObj, $importsModel); + } + } + + /** + * renderUnknownBlueprint + */ + protected function renderUnknownBlueprint($class, $properties) + { + return $this->makePartial('blueprint-unknown', [ + 'properties' => $properties, + 'class' => $class + ]); + } + + /** + * getEntryBlueprintDefaultConfiguration + */ + protected function getEntryBlueprintDefaultConfiguration($blueprintObj, $importsModel) + { + $handleBase = class_basename($blueprintObj->handle); + $dbPrefix = $importsModel->getPluginCodeObj()->toDatabasePrefix().'_'; + $permissionPrefix = $importsModel->getPluginCodeObj()->toPermissionPrefix().'.manage_'; + + $result = [ + 'name' => $blueprintObj->name, + 'controllerClass' => Str::plural($handleBase), + 'modelClass' => Str::singular($handleBase), + 'tableName' => $dbPrefix . Str::snake($handleBase), + 'permissionCode' => $permissionPrefix . Str::snake($handleBase), + 'menuCode' => Str::snake($handleBase), + ]; + + return $result; + } + + /** + * getGlobalBlueprintDefaultConfiguration + */ + protected function getGlobalBlueprintDefaultConfiguration($blueprintObj, $importsModel) + { + $handleBase = class_basename($blueprintObj->handle); + $dbPrefix = $importsModel->getPluginCodeObj()->toDatabasePrefix().'_'; + $permissionPrefix = $importsModel->getPluginCodeObj()->toPermissionPrefix().'.manage_'; + + $result = [ + 'name' => $blueprintObj->name, + 'controllerClass' => Str::plural($handleBase), + 'modelClass' => Str::singular($handleBase), + 'tableName' => $dbPrefix . Str::snake($handleBase), + 'permissionCode' => $permissionPrefix . Str::snake($handleBase), + 'menuCode' => Str::snake($handleBase), + ]; + + return $result; + } +} diff --git a/plugins/rainlab/builder/widgets/DefaultControlDesignTimeProvider.php b/plugins/rainlab/builder/widgets/DefaultControlDesignTimeProvider.php new file mode 100644 index 0000000..70cc7a8 --- /dev/null +++ b/plugins/rainlab/builder/widgets/DefaultControlDesignTimeProvider.php @@ -0,0 +1,135 @@ +defaultControlsTypes)) { + return $this->renderUnknownControl($type, $properties); + } + + return $this->makePartial('control-'.$type, [ + 'properties' => $properties, + 'formBuilder' => $formBuilder + ]); + } + + /** + * renderControlStaticBody renders control static body. + * The control static body is never updated with AJAX during the form editing. + * @param string $type Specifies the control type to render. + * @param array $properties Control property values preprocessed for the Inspector. + * @param array $controlConfiguration Raw control property values. + * @param \RainLab\Builder\FormWidgets\FormBuilder $formBuilder FormBuilder widget instance. + * @return string Returns HTML markup string. + */ + public function renderControlStaticBody($type, $properties, $controlConfiguration, $formBuilder) + { + if (!in_array($type, $this->defaultControlsTypes)) { + return null; + } + + $partialName = 'control-'.$type.'-static'; + $partialPath = $this->getViewPath('_'.$partialName.'.php'); + + if (!File::exists($partialPath)) { + return null; + } + + return $this->makePartial($partialName, [ + 'properties' => $properties, + 'controlConfiguration' => $controlConfiguration, + 'formBuilder' => $formBuilder + ]); + } + + /** + * controlHasLabels determines whether a control supports default labels and comments. + * @param string $type Specifies the control type. + * @return boolean + */ + public function controlHasLabels($type) + { + if (in_array($type, ['checkbox', 'switch', 'hint', 'partial', 'section', 'ruler'])) { + return false; + } + + return true; + } + + /** + * getPropertyValue + */ + protected function getPropertyValue($properties, $property) + { + if (array_key_exists($property, $properties)) { + return $properties[$property]; + } + + return null; + } + + /** + * renderUnknownControl + */ + protected function renderUnknownControl($type, $properties) + { + return $this->makePartial('control-unknowncontrol', [ + 'properties'=>$properties, + 'type'=>$type + ]); + } +} diff --git a/plugins/rainlab/builder/widgets/LanguageList.php b/plugins/rainlab/builder/widgets/LanguageList.php new file mode 100644 index 0000000..539de94 --- /dev/null +++ b/plugins/rainlab/builder/widgets/LanguageList.php @@ -0,0 +1,104 @@ +alias = $alias; + + parent::__construct($controller, []); + $this->bindToController(); + } + + /** + * Renders the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + public function updateList() + { + return ['#'.$this->getId('plugin-language-list') => $this->makePartial('items', $this->getRenderData())]; + } + + public function refreshActivePlugin() + { + return ['#'.$this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData())]; + } + + /* + * Event handlers + */ + + public function onUpdate() + { + return $this->updateList(); + } + + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /* + * Methods for the internal use + */ + + protected function getLanguageList($pluginCode) + { + $result = LocalizationModel::listPluginLanguages($pluginCode); + + return $result; + } + + protected function getRenderData() + { + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + if (!$activePluginVector) { + return [ + 'pluginVector'=>null, + 'items' => [] + ]; + } + + $items = $this->getLanguageList($activePluginVector->pluginCodeObj); + + $searchTerm = Str::lower($this->getSearchTerm()); + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($items as $language) { + if ($this->textMatchesSearch($words, $language)) { + $result[] = $language; + } + } + + $items = $result; + } + + return [ + 'pluginVector'=>$activePluginVector, + 'items'=>$items + ]; + } +} diff --git a/plugins/rainlab/builder/widgets/ModelList.php b/plugins/rainlab/builder/widgets/ModelList.php new file mode 100644 index 0000000..cf44707 --- /dev/null +++ b/plugins/rainlab/builder/widgets/ModelList.php @@ -0,0 +1,129 @@ +alias = $alias; + + parent::__construct($controller, []); + $this->bindToController(); + } + + /** + * Renders the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + public function updateList() + { + return ['#'.$this->getId('plugin-model-list') => $this->makePartial('items', $this->getRenderData())]; + } + + public function refreshActivePlugin() + { + return ['#'.$this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData())]; + } + + /* + * Event handlers + */ + + public function onUpdate() + { + return $this->updateList(); + } + + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /* + * Methods for the internal use + */ + + protected function getData($pluginVector) + { + if (!$pluginVector) { + return []; + } + + $pluginCode = $pluginVector->pluginCodeObj; + + if (!$pluginCode) { + return []; + } + + $models = $this->getModelList($pluginCode); + $searchTerm = Str::lower($this->getSearchTerm()); + + // Apply the search + // + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($models as $modelInfo) { + if ($this->textMatchesSearch($words, $modelInfo['model']->className)) { + $result[] = $modelInfo; + } + } + + $models = $result; + } + + return $models; + } + + protected function getModelList($pluginCode) + { + $models = ModelModel::listPluginModels($pluginCode); + $result = []; + + foreach ($models as $model) { + $result[] = [ + 'model' => $model, + 'forms' => ModelFormModel::listModelFiles($pluginCode, $model->className), + 'lists' => ModelListModel::listModelFiles($pluginCode, $model->className) + ]; + } + + return $result; + } + + protected function getRenderData() + { + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + + return [ + 'pluginVector'=>$activePluginVector, + 'items'=>$this->getData($activePluginVector) + ]; + } +} diff --git a/plugins/rainlab/builder/widgets/PluginList.php b/plugins/rainlab/builder/widgets/PluginList.php new file mode 100644 index 0000000..205c151 --- /dev/null +++ b/plugins/rainlab/builder/widgets/PluginList.php @@ -0,0 +1,233 @@ +alias = $alias; + + parent::__construct($controller, []); + $this->bindToController(); + } + + /** + * render the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + /** + * setActivePlugin + */ + public function setActivePlugin($pluginCode) + { + $pluginCodeObj = new PluginCode($pluginCode); + + $this->putSession('activePlugin', $pluginCodeObj->toCode()); + } + + /** + * getActivePluginVector + */ + public function getActivePluginVector() + { + $pluginCode = $this->getActivePluginCode(); + + try { + if (strlen($pluginCode)) { + $pluginCodeObj = new PluginCode($pluginCode); + $path = $pluginCodeObj->toPluginInformationFilePath(); + if (!File::isFile(File::symbolizePath($path))) { + return null; + } + + $plugins = PluginManager::instance()->getPlugins(); + foreach ($plugins as $code => $plugin) { + if ($code == $pluginCode) { + return new PluginVector($plugin, $pluginCodeObj); + } + } + } + } + catch (Exception $ex) { + return null; + } + + return null; + } + + /** + * updateList + */ + public function updateList() + { + return ['#'.$this->getId('plugin-list') => $this->makePartial('items', $this->getRenderData())]; + } + + /** + * onUpdate + */ + public function onUpdate() + { + return $this->updateList(); + } + + /** + * onSearch + */ + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /** + * onToggleFilter + */ + public function onToggleFilter() + { + $mode = $this->getFilterMode(); + $this->setFilterMode($mode == 'my' ? 'all' : 'my'); + + $result = $this->updateList(); + $result['#'.$this->getId('toolbar-buttons')] = $this->makePartial('toolbar-buttons'); + + return $result; + } + + /** + * getData + */ + protected function getData() + { + $plugins = $this->getPluginList(); + $searchTerm = Str::lower($this->getSearchTerm()); + + // Apply the search + // + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($plugins as $code => $plugin) { + if ($this->textMatchesSearch($words, $plugin['full-text'])) { + $result[$code] = $plugin; + } + } + + $plugins = $result; + } + + // Apply the my plugins / all plugins filter + // + $mode = $this->getFilterMode(); + if ($mode == 'my') { + $namespace = PluginSettings::instance()->author_namespace; + + $result = []; + foreach ($plugins as $code => $plugin) { + if (strcasecmp($plugin['namespace'], $namespace) === 0) { + $result[$code] = $plugin; + } + } + + $plugins = $result; + } + + return $plugins; + } + + /** + * getPluginList + */ + protected function getPluginList() + { + $plugins = PluginManager::instance()->getPlugins(); + + $result = []; + foreach ($plugins as $code => $plugin) { + $pluginInfo = $plugin->pluginDetails(); + + $itemInfo = [ + 'name' => isset($pluginInfo['name']) ? $pluginInfo['name'] : 'rainlab.builder::lang.plugin.no_name', + 'description' => isset($pluginInfo['description']) ? $pluginInfo['description'] : 'rainlab.builder::lang.plugin.no_description', + 'icon' => isset($pluginInfo['icon']) ? $pluginInfo['icon'] : null + ]; + + list($namespace) = explode('\\', get_class($plugin)); + $itemInfo['namespace'] = trim($namespace); + $itemInfo['full-text'] = trans($itemInfo['name']).' '.trans($itemInfo['description']); + + $result[$code] = $itemInfo; + } + + uasort($result, function($a, $b) { + return strcmp(trans($a['name']), trans($b['name'])); + }); + + return $result; + } + + /** + * setFilterMode + */ + protected function setFilterMode($mode) + { + $this->putSession('filter', $mode); + } + + /** + * getFilterMode + */ + protected function getFilterMode() + { + return $this->getSession('filter', 'my'); + } + + /** + * getActivePluginCode + */ + protected function getActivePluginCode() + { + return $this->getSession('activePlugin'); + } + + /** + * getRenderData + */ + protected function getRenderData() + { + return [ + 'items'=>$this->getData() + ]; + } +} diff --git a/plugins/rainlab/builder/widgets/VersionList.php b/plugins/rainlab/builder/widgets/VersionList.php new file mode 100644 index 0000000..08eab99 --- /dev/null +++ b/plugins/rainlab/builder/widgets/VersionList.php @@ -0,0 +1,155 @@ +alias = $alias; + + parent::__construct($controller, []); + + $this->config->sort = $this->getSession('sort', 'asc'); + + $this->bindToController(); + } + + /** + * render the widget. + * @return string + */ + public function render() + { + return $this->makePartial('body', $this->getRenderData()); + } + + /** + * updateList + */ + public function updateList() + { + return ['#'.$this->getId('plugin-version-list') => $this->makePartial('items', $this->getRenderData())]; + } + + /** + * refreshActivePlugin + */ + public function refreshActivePlugin() + { + return ['#'.$this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData())]; + } + + /** + * onUpdate + */ + public function onUpdate() + { + return $this->updateList(); + } + + /** + * onSearch + */ + public function onSearch() + { + $this->setSearchTerm(Input::get('search')); + return $this->updateList(); + } + + /** + * onSort + */ + public function onSort() + { + $this->config->sort = Input::input('sort'); + + $this->putSession('sort', $this->config->sort); + + return ['#' . $this->getId('body') => $this->makePartial('widget-contents', $this->getRenderData())]; + } + + /** + * getRenderData + */ + protected function getRenderData() + { + $activePluginVector = $this->controller->getBuilderActivePluginVector(); + if (!$activePluginVector) { + return [ + 'pluginVector'=>null, + 'items' => [], + 'unappliedVersions' => [] + ]; + } + + $versionObj = new PluginVersion(); + $items = $versionObj->getPluginVersionInformation($activePluginVector->pluginCodeObj); + + $searchTerm = Str::lower($this->getSearchTerm()); + if (strlen($searchTerm)) { + $words = explode(' ', $searchTerm); + $result = []; + + foreach ($items as $version => $versionInfo) { + $description = $this->getVersionDescription($versionInfo); + + if ( + $this->textMatchesSearch($words, $version) || + (strlen($description) && $this->textMatchesSearch($words, $description)) + ) { + $result[$version] = $versionInfo; + } + } + + $items = $result; + } + + if ($this->getConfig('sort', 'asc') === 'desc') { + $items = array_reverse($items, false); + } + + $versionManager = VersionManager::instance(); + $unappliedVersions = $versionManager->listNewVersions($activePluginVector->pluginCodeObj->toCode()); + return [ + 'pluginVector'=>$activePluginVector, + 'items'=>$items, + 'unappliedVersions'=>$unappliedVersions + ]; + } + + /** + * getVersionDescription + */ + protected function getVersionDescription($versionInfo) + { + if (is_array($versionInfo)) { + if (array_key_exists(0, $versionInfo)) { + return $versionInfo[0]; + } + } + + if (is_scalar($versionInfo)) { + return $versionInfo; + } + + return null; + } +} diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_body.php b/plugins/rainlab/builder/widgets/codelist/partials/_body.php new file mode 100644 index 0000000..7a3f9c2 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginCode'=>$pluginCode, 'items'=>$items]) ?> +
    diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_files.php b/plugins/rainlab/builder/widgets/codelist/partials/_files.php new file mode 100644 index 0000000..b0e04f1 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_files.php @@ -0,0 +1,7 @@ +
    +
    +
    + makePartial('items', ['items' => $items]) ?> +
    +
    +
    diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_items.php b/plugins/rainlab/builder/widgets/codelist/partials/_items.php new file mode 100644 index 0000000..8f74444 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_items.php @@ -0,0 +1,54 @@ +isSearchMode(); +?> +getUpPath()) !== null && !$searchMode): ?> +

    + getCurrentRelativePath() ?> +

    + +
    + + + +

    noRecordsMessage)) ?>

    + +
    diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_move_form.php b/plugins/rainlab/builder/widgets/codelist/partials/_move_form.php new file mode 100644 index 0000000..b774168 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_move_form.php @@ -0,0 +1,40 @@ +$this->getEventHandler('onMove'), + 'data-request-success'=>"\$(this).trigger('close.oc.popup')", + 'data-stripe-load-indicator'=>1, + 'id'=>'asset-move-popup-form' +]) ?> + + + + diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_new_dir_form.php b/plugins/rainlab/builder/widgets/codelist/partials/_new_dir_form.php new file mode 100644 index 0000000..14ebf9c --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_new_dir_form.php @@ -0,0 +1,45 @@ +$this->getEventHandler('onNewDirectory'), + 'data-request-success'=>"\$(this).trigger('close.oc.popup')", + 'data-stripe-load-indicator'=>1, + 'id'=>'asset-new-dir-popup-form' +]) ?> + + + + + + + + diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_rename_form.php b/plugins/rainlab/builder/widgets/codelist/partials/_rename_form.php new file mode 100644 index 0000000..2801d29 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_rename_form.php @@ -0,0 +1,46 @@ +getEventHandler('onApplyName'), [ + 'success' => "\$el.trigger('close.oc.popup');", + 'data-stripe-load-indicator' => 1, + 'id' => 'asset-rename-popup-form' +]) ?> + + + + + + + + diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/codelist/partials/_toolbar.php new file mode 100644 index 0000000..b68e459 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_toolbar.php @@ -0,0 +1,74 @@ +
    +
    +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    diff --git a/plugins/rainlab/builder/widgets/codelist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/codelist/partials/_widget-contents.php new file mode 100644 index 0000000..bf54926 --- /dev/null +++ b/plugins/rainlab/builder/widgets/codelist/partials/_widget-contents.php @@ -0,0 +1,27 @@ + +
    +
    + +
    +
    + + + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('files', ['items' => $items]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/controllerlist/partials/_body.php b/plugins/rainlab/builder/widgets/controllerlist/partials/_body.php new file mode 100644 index 0000000..46306d3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/controllerlist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginVector'=>$pluginVector, 'items'=>$items]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/controllerlist/partials/_controller-list.php b/plugins/rainlab/builder/widgets/controllerlist/partials/_controller-list.php new file mode 100644 index 0000000..c99bfa9 --- /dev/null +++ b/plugins/rainlab/builder/widgets/controllerlist/partials/_controller-list.php @@ -0,0 +1,13 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/controllerlist/partials/_items.php b/plugins/rainlab/builder/widgets/controllerlist/partials/_items.php new file mode 100644 index 0000000..c7e04ff --- /dev/null +++ b/plugins/rainlab/builder/widgets/controllerlist/partials/_items.php @@ -0,0 +1,20 @@ + +
      + pluginCodeObj->toCode(); + foreach ($items as $controller): + $dataId = 'controller-'.e($pluginCode).'-'.$controller; + ?> +
    • + data-id=""> + + + +
    • + +
    + +

    noRecordsMessage)) ?>

    + diff --git a/plugins/rainlab/builder/widgets/controllerlist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/controllerlist/partials/_toolbar.php new file mode 100644 index 0000000..5ce9f57 --- /dev/null +++ b/plugins/rainlab/builder/widgets/controllerlist/partials/_toolbar.php @@ -0,0 +1,25 @@ +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/controllerlist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/controllerlist/partials/_widget-contents.php new file mode 100644 index 0000000..14affab --- /dev/null +++ b/plugins/rainlab/builder/widgets/controllerlist/partials/_widget-contents.php @@ -0,0 +1,28 @@ +
    +
    + + getPluginName())) ?> + + + +
    +
    + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('controller-list', ['items'=>$items, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/databasetablelist/partials/_body.php b/plugins/rainlab/builder/widgets/databasetablelist/partials/_body.php new file mode 100644 index 0000000..46306d3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/databasetablelist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginVector'=>$pluginVector, 'items'=>$items]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/databasetablelist/partials/_items.php b/plugins/rainlab/builder/widgets/databasetablelist/partials/_items.php new file mode 100644 index 0000000..90c821f --- /dev/null +++ b/plugins/rainlab/builder/widgets/databasetablelist/partials/_items.php @@ -0,0 +1,18 @@ + + + +

    noRecordsMessage)) ?>

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/databasetablelist/partials/_table-list.php b/plugins/rainlab/builder/widgets/databasetablelist/partials/_table-list.php new file mode 100644 index 0000000..0f711cd --- /dev/null +++ b/plugins/rainlab/builder/widgets/databasetablelist/partials/_table-list.php @@ -0,0 +1,13 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/databasetablelist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/databasetablelist/partials/_toolbar.php new file mode 100644 index 0000000..b268611 --- /dev/null +++ b/plugins/rainlab/builder/widgets/databasetablelist/partials/_toolbar.php @@ -0,0 +1,24 @@ +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/databasetablelist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/databasetablelist/partials/_widget-contents.php new file mode 100644 index 0000000..e529225 --- /dev/null +++ b/plugins/rainlab/builder/widgets/databasetablelist/partials/_widget-contents.php @@ -0,0 +1,28 @@ +
    +
    + + getPluginName())) ?> + + + +
    +
    + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('table-list', ['items'=>$items]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-form-controller.php b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-form-controller.php new file mode 100644 index 0000000..c0b7bd4 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-form-controller.php @@ -0,0 +1,23 @@ +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-import-export-controller.php b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-import-export-controller.php new file mode 100644 index 0000000..bee9d63 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-import-export-controller.php @@ -0,0 +1,19 @@ +
    +
    + + + + + + + + + + + + + + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-list-controller.php b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-list-controller.php new file mode 100644 index 0000000..e2b1bd2 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-list-controller.php @@ -0,0 +1,41 @@ +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-unknown.php b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-unknown.php new file mode 100644 index 0000000..1a0e7af --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultbehaviordesigntimeprovider/partials/_behavior-unknown.php @@ -0,0 +1 @@ +Unknown behavior \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-entry.php b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-entry.php new file mode 100644 index 0000000..f900cf3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-entry.php @@ -0,0 +1,51 @@ + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Namename) ?>
    Handlehandle) ?>
    Controllercontrollers/
    Models + + models/ + +
    Migrations + + updates/ + +
    Error
    +
    +
    diff --git a/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-global.php b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-global.php new file mode 100644 index 0000000..f900cf3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-global.php @@ -0,0 +1,51 @@ + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Namename) ?>
    Handlehandle) ?>
    Controllercontrollers/
    Models + + models/ + +
    Migrations + + updates/ + +
    Error
    +
    +
    diff --git a/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-unknown.php b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-unknown.php new file mode 100644 index 0000000..abddc94 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultblueprintdesigntimeprovider/partials/_blueprint-unknown.php @@ -0,0 +1 @@ +Unknown blueprint \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-balloon-selector.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-balloon-selector.php new file mode 100644 index 0000000..f834507 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-balloon-selector.php @@ -0,0 +1,16 @@ +
    +
      + +
    • + +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkbox.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkbox.php new file mode 100644 index 0000000..0b68995 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkbox.php @@ -0,0 +1,9 @@ +getPropertyValue($properties, 'label'); + $comment = $this->getPropertyValue($properties, 'oc.comment'); +?> + +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkboxlist.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkboxlist.php new file mode 100644 index 0000000..ef4c056 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-checkboxlist.php @@ -0,0 +1,16 @@ +
    +
      + +
    • + +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-codeeditor.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-codeeditor.php new file mode 100644 index 0000000..297f2e9 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-codeeditor.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-colorpicker.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-colorpicker.php new file mode 100644 index 0000000..5ed57b6 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-colorpicker.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datatable.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datatable.php new file mode 100644 index 0000000..aba343d --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datatable.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datepicker.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datepicker.php new file mode 100644 index 0000000..4cf196f --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-datepicker.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-dropdown.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-dropdown.php new file mode 100644 index 0000000..9b0fa20 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-dropdown.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-email.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-email.php new file mode 100644 index 0000000..557f21c --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-email.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-fileupload.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-fileupload.php new file mode 100644 index 0000000..4988ad8 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-fileupload.php @@ -0,0 +1,6 @@ +
    + + getPropertyValue($properties, 'mode') != 'image'): ?> + + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-hint.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-hint.php new file mode 100644 index 0000000..fcd7858 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-hint.php @@ -0,0 +1,7 @@ +
    + + getPropertyValue($properties, 'path'); + echo strlen($path) ? ' - '.$path : null; + ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-markdown.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-markdown.php new file mode 100644 index 0000000..404c61c --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-markdown.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-mediafinder.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-mediafinder.php new file mode 100644 index 0000000..14d7ec8 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-mediafinder.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform-static.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform-static.php new file mode 100644 index 0000000..5f2e08a --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform-static.php @@ -0,0 +1,11 @@ +
    + + + renderControlList($controls) ?> +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform.php new file mode 100644 index 0000000..2b947a8 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-nestedform.php @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-number.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-number.php new file mode 100644 index 0000000..7a9ae7f --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-number.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-pagefinder.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-pagefinder.php new file mode 100644 index 0000000..d6930b6 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-pagefinder.php @@ -0,0 +1,3 @@ +
    + +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-partial.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-partial.php new file mode 100644 index 0000000..f43bf7e --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-partial.php @@ -0,0 +1,7 @@ +
    + + getPropertyValue($properties, 'path'); + echo strlen($path) ? ' - '.$path : null; + ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-password.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-password.php new file mode 100644 index 0000000..d684156 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-password.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-radio.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-radio.php new file mode 100644 index 0000000..6685f86 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-radio.php @@ -0,0 +1,16 @@ +
    +
      + +
    • + +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-recordfinder.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-recordfinder.php new file mode 100644 index 0000000..85f0d48 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-recordfinder.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-relation.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-relation.php new file mode 100644 index 0000000..4bbfc9d --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-relation.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater-static.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater-static.php new file mode 100644 index 0000000..5f2e08a --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater-static.php @@ -0,0 +1,11 @@ +
    + + + renderControlList($controls) ?> +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater.php new file mode 100644 index 0000000..d6c577d --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-repeater.php @@ -0,0 +1,9 @@ +
    + getPropertyValue($properties, 'prompt'); + if (!strlen($prompt)) { + $prompt = 'rainlab.builder::lang.form.property_prompt_default'; + } + ?> +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-richeditor.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-richeditor.php new file mode 100644 index 0000000..e56619b --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-richeditor.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-ruler.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-ruler.php new file mode 100644 index 0000000..0fd8f18 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-ruler.php @@ -0,0 +1 @@ +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-section.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-section.php new file mode 100644 index 0000000..a9d75b3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-section.php @@ -0,0 +1,9 @@ +getPropertyValue($properties, 'label'); + $comment =$this->getPropertyValue($properties, 'oc.comment'); +?> + +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-sensitive.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-sensitive.php new file mode 100644 index 0000000..72fa918 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-sensitive.php @@ -0,0 +1,3 @@ +
    + +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-switch.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-switch.php new file mode 100644 index 0000000..2d2adc4 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-switch.php @@ -0,0 +1,9 @@ +getPropertyValue($properties, 'label'); + $comment = $this->getPropertyValue($properties, 'oc.comment'); +?> + +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-taglist.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-taglist.php new file mode 100644 index 0000000..c2a2281 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-taglist.php @@ -0,0 +1,3 @@ +
    + +
    diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-text.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-text.php new file mode 100644 index 0000000..308339e --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-text.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-textarea.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-textarea.php new file mode 100644 index 0000000..c9fd1f0 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-textarea.php @@ -0,0 +1,3 @@ +
    + +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-unknowncontrol.php b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-unknowncontrol.php new file mode 100644 index 0000000..fd2af48 --- /dev/null +++ b/plugins/rainlab/builder/widgets/defaultcontroldesigntimeprovider/partials/_control-unknowncontrol.php @@ -0,0 +1,4 @@ +
    + $type])) ?> +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/languagelist/partials/_body.php b/plugins/rainlab/builder/widgets/languagelist/partials/_body.php new file mode 100644 index 0000000..46306d3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/languagelist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginVector'=>$pluginVector, 'items'=>$items]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/languagelist/partials/_items.php b/plugins/rainlab/builder/widgets/languagelist/partials/_items.php new file mode 100644 index 0000000..cb2e0b4 --- /dev/null +++ b/plugins/rainlab/builder/widgets/languagelist/partials/_items.php @@ -0,0 +1,20 @@ + +
      + pluginCodeObj->toCode(); + foreach ($items as $language): + $dataId = 'localization-'.e($pluginCode).'-'.$language; + ?> +
    • + data-id=""> + + + +
    • + +
    + +

    noRecordsMessage)) ?>

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/languagelist/partials/_language-list.php b/plugins/rainlab/builder/widgets/languagelist/partials/_language-list.php new file mode 100644 index 0000000..ef3df47 --- /dev/null +++ b/plugins/rainlab/builder/widgets/languagelist/partials/_language-list.php @@ -0,0 +1,13 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/languagelist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/languagelist/partials/_toolbar.php new file mode 100644 index 0000000..d8b409b --- /dev/null +++ b/plugins/rainlab/builder/widgets/languagelist/partials/_toolbar.php @@ -0,0 +1,24 @@ +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/languagelist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/languagelist/partials/_widget-contents.php new file mode 100644 index 0000000..2a9b2f9 --- /dev/null +++ b/plugins/rainlab/builder/widgets/languagelist/partials/_widget-contents.php @@ -0,0 +1,28 @@ +
    +
    + + getPluginName())) ?> + + + +
    +
    + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('language-list', ['items'=>$items, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/modellist/partials/_body.php b/plugins/rainlab/builder/widgets/modellist/partials/_body.php new file mode 100644 index 0000000..46306d3 --- /dev/null +++ b/plugins/rainlab/builder/widgets/modellist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginVector'=>$pluginVector, 'items'=>$items]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/modellist/partials/_items.php b/plugins/rainlab/builder/widgets/modellist/partials/_items.php new file mode 100644 index 0000000..b2196c4 --- /dev/null +++ b/plugins/rainlab/builder/widgets/modellist/partials/_items.php @@ -0,0 +1,94 @@ + +
      + className; + + $modelGroup = $model->className; + $formsGroup = $modelGroup.'-forms'; + $listsGroup = $modelGroup.'-lists'; + $modelGroupStatus = $this->getCollapseStatus($modelGroup); + $formsGroupStatus = $this->getCollapseStatus($formsGroup); + $listsGroupStatus = $this->getCollapseStatus($listsGroup); + ?> +
    • +

      className) ?>

      +
        +
      • +

        +
        + +
        + +
          + className.'-'.$modelForm; + ?> +
        • + +
        • + +
        +
      • +
      • +

        +
        + +
        + +
          + className.'-'.$modelList; + ?> +
        • + +
        • + +
        +
      • +
      + +
    • + +
    + +

    noRecordsMessage)) ?>

    + diff --git a/plugins/rainlab/builder/widgets/modellist/partials/_model-list.php b/plugins/rainlab/builder/widgets/modellist/partials/_model-list.php new file mode 100644 index 0000000..d1ab5d9 --- /dev/null +++ b/plugins/rainlab/builder/widgets/modellist/partials/_model-list.php @@ -0,0 +1,14 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items]) ?> +
    +
    +
    +
    diff --git a/plugins/rainlab/builder/widgets/modellist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/modellist/partials/_toolbar.php new file mode 100644 index 0000000..346057f --- /dev/null +++ b/plugins/rainlab/builder/widgets/modellist/partials/_toolbar.php @@ -0,0 +1,24 @@ +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/modellist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/modellist/partials/_widget-contents.php new file mode 100644 index 0000000..f87cd74 --- /dev/null +++ b/plugins/rainlab/builder/widgets/modellist/partials/_widget-contents.php @@ -0,0 +1,28 @@ +
    +
    + + getPluginName())) ?> + + + +
    +
    + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('model-list', ['items'=>$items]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/pluginlist/partials/_body.php b/plugins/rainlab/builder/widgets/pluginlist/partials/_body.php new file mode 100644 index 0000000..0040fb2 --- /dev/null +++ b/plugins/rainlab/builder/widgets/pluginlist/partials/_body.php @@ -0,0 +1,8 @@ +makePartial('toolbar') ?> +
    +
    +
    + makePartial('plugin-list', ['items'=>$items]) ?> +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/pluginlist/partials/_items.php b/plugins/rainlab/builder/widgets/pluginlist/partials/_items.php new file mode 100644 index 0000000..b304288 --- /dev/null +++ b/plugins/rainlab/builder/widgets/pluginlist/partials/_items.php @@ -0,0 +1,35 @@ + + getActivePluginCode(); ?> + + +

    noRecordsMessage)) ?>

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/pluginlist/partials/_plugin-list.php b/plugins/rainlab/builder/widgets/pluginlist/partials/_plugin-list.php new file mode 100644 index 0000000..eab0976 --- /dev/null +++ b/plugins/rainlab/builder/widgets/pluginlist/partials/_plugin-list.php @@ -0,0 +1,13 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar-buttons.php b/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar-buttons.php new file mode 100644 index 0000000..d2226d8 --- /dev/null +++ b/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar-buttons.php @@ -0,0 +1,14 @@ + + +getFilterMode(); ?> + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar.php new file mode 100644 index 0000000..69d6269 --- /dev/null +++ b/plugins/rainlab/builder/widgets/pluginlist/partials/_toolbar.php @@ -0,0 +1,22 @@ +
    +
    +
    +
    + makePartial("toolbar-buttons") ?> +
    +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_body.php b/plugins/rainlab/builder/widgets/versionlist/partials/_body.php new file mode 100644 index 0000000..b39cdd0 --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_body.php @@ -0,0 +1,3 @@ +
    + makePartial('widget-contents', ['pluginVector'=>$pluginVector, 'items'=>$items, 'unappliedVersions'=>$unappliedVersions]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_items.php b/plugins/rainlab/builder/widgets/versionlist/partials/_items.php new file mode 100644 index 0000000..940a482 --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_items.php @@ -0,0 +1,35 @@ + +
      + pluginCodeObj->toCode(); + foreach ($items as $versionNumber=>$versionInfo): + $dataId = 'version-'.e($pluginCode).'-'.$versionNumber; + $description = $this->getVersionDescription($versionInfo); + $applied = !array_key_exists($versionNumber, $unappliedVersions); + ?> +
    • + data-id=""> + + + + + + + + + + + + + + +
    • + +
    + +

    noRecordsMessage)) ?>

    + \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_sort.php b/plugins/rainlab/builder/widgets/versionlist/partials/_sort.php new file mode 100644 index 0000000..d84feb0 --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_sort.php @@ -0,0 +1,11 @@ +getConfig('sort', 'asc') === 'asc'): ?> + + + + diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_toolbar.php b/plugins/rainlab/builder/widgets/versionlist/partials/_toolbar.php new file mode 100644 index 0000000..f019738 --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_toolbar.php @@ -0,0 +1,35 @@ +
    +
    +
    +
    + + makePartial('sort'); ?> +
    +
    +
    + +
    +
    +
    diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_version-list.php b/plugins/rainlab/builder/widgets/versionlist/partials/_version-list.php new file mode 100644 index 0000000..6d480c9 --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_version-list.php @@ -0,0 +1,13 @@ +
    +
    +
    +
    + makePartial('items', ['items'=>$items, 'unappliedVersions'=>$unappliedVersions, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    +
    \ No newline at end of file diff --git a/plugins/rainlab/builder/widgets/versionlist/partials/_widget-contents.php b/plugins/rainlab/builder/widgets/versionlist/partials/_widget-contents.php new file mode 100644 index 0000000..a2813ef --- /dev/null +++ b/plugins/rainlab/builder/widgets/versionlist/partials/_widget-contents.php @@ -0,0 +1,28 @@ +
    +
    + + getPluginName())) ?> + + + +
    +
    + + + makePartial('toolbar') ?> +
    +
    +
    + makePartial('version-list', ['items'=>$items, 'unappliedVersions'=>$unappliedVersions, 'pluginVector'=>$pluginVector]) ?> +
    +
    +
    + +
    +
    +
    +

    +
    +
    +
    + diff --git a/plugins/rainlab/translate/.gitignore b/plugins/rainlab/translate/.gitignore new file mode 100644 index 0000000..4b53aba --- /dev/null +++ b/plugins/rainlab/translate/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.lock +.DS_Store +.phpunit.result.cache diff --git a/plugins/rainlab/translate/LICENCE.md b/plugins/rainlab/translate/LICENCE.md new file mode 100644 index 0000000..e49b459 --- /dev/null +++ b/plugins/rainlab/translate/LICENCE.md @@ -0,0 +1,21 @@ +# MIT license + +Copyright (c) 2014-2022 Responsiv Pty Ltd, October CMS + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/rainlab/translate/Plugin.php b/plugins/rainlab/translate/Plugin.php new file mode 100644 index 0000000..ec57883 --- /dev/null +++ b/plugins/rainlab/translate/Plugin.php @@ -0,0 +1,381 @@ + 'rainlab.translate::lang.plugin.name', + 'description' => 'rainlab.translate::lang.plugin.description', + 'author' => 'Alexey Bobkov, Samuel Georges', + 'icon' => 'icon-language', + 'homepage' => 'https://github.com/rainlab/translate-plugin' + ]; + } + + /** + * register the plugin + */ + public function register() + { + // Load localized version of mail templates (akin to localized CMS content files) + Event::listen('mailer.beforeAddContent', function ($mailer, $message, $view, $data, $raw, $plain) { + return EventRegistry::instance()->findLocalizedMailViewContent($mailer, $message, $view, $data, $raw, $plain); + }, 1); + + // Defer event with low priority to let others contribute before this registers. + Event::listen('backend.form.extendFieldsBefore', function($widget) { + EventRegistry::instance()->registerFormFieldReplacements($widget); + }, -1); + + // Handle translated page URLs + Page::extend(function($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + $model->translatable = array_merge($model->translatable, ['title', 'description', 'meta_title', 'meta_description']); + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePageUrl::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatablePageUrl::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePage::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatablePage::class); + } + }); + + // Extension logic for October CMS v1.0 + if (!class_exists('System')) { + $this->extendLegacyPlatform(); + } + // Extension logic for October CMS v2.0 + else { + Event::listen('cms.theme.createThemeDataModel', function($attributes) { + return new \RainLab\Translate\Models\MLThemeData($attributes); + }); + + Event::listen('cms.template.getTemplateToolbarSettingsButtons', function($extension, $dataHolder) { + if ($dataHolder->templateType === 'page') { + EventRegistry::instance()->extendEditorPageToolbar($dataHolder); + } + }); + } + + // Register console commands + $this->registerConsoleCommand('translate.scan', \Rainlab\Translate\Console\ScanCommand::class); + + // Register asset bundles + $this->registerAssetBundles(); + } + + /** + * extendLegacyPlatform will add the legacy features expected in v1.0 + */ + protected function extendLegacyPlatform() + { + // Adds translation support to file models + File::extend(function ($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + $model->translatable = array_merge($model->translatable, ['title', 'description']); + if (!$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Purgeable::class)) { + $model->extendClassWith(\October\Rain\Database\Behaviors\Purgeable::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatableModel::class); + } + }); + + // Adds translation support to theme settings + ThemeData::extend(static function ($model) { + if (!$model->propertyExists('translatable')) { + $model->addDynamicProperty('translatable', []); + } + + if (!$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Purgeable::class)) { + $model->extendClassWith(\October\Rain\Database\Behaviors\Purgeable::class); + } + if (!$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class)) { + $model->extendClassWith(\RainLab\Translate\Behaviors\TranslatableModel::class); + } + + $model->bindEvent('model.afterFetch', static function() use ($model) { + foreach ($model->getFormFields() as $id => $field) { + if (!empty($field['translatable'])) { + $model->translatable[] = $id; + } + } + }); + }); + } + + /** + * boot the plugin + */ + public function boot() + { + // Set the page context for translation caching with high priority. + Event::listen('cms.page.init', function($controller, $page) { + EventRegistry::instance()->setMessageContext($page); + }, 100); + + // Populate MenuItem properties with localized values if available + Event::listen('pages.menu.referencesGenerated', function (&$items) { + $locale = App::getLocale(); + $iterator = function ($menuItems) use (&$iterator, $locale) { + $result = []; + foreach ($menuItems as $item) { + $localeFields = array_get($item->viewBag, "locale.$locale", []); + foreach ($localeFields as $fieldName => $fieldValue) { + if ($fieldValue) { + $item->$fieldName = $fieldValue; + } + } + if ($item->items) { + $item->items = $iterator($item->items); + } + $result[] = $item; + } + return $result; + }; + $items = $iterator($items); + }); + + // Import messages defined by the theme + Event::listen('cms.theme.setActiveTheme', function($code) { + EventRegistry::instance()->importMessagesFromTheme($code); + }); + + // Adds language suffixes to content files. + Event::listen('cms.page.beforeRenderContent', function($controller, $fileName) { + return EventRegistry::instance() + ->findTranslatedContentFile($controller, $fileName) + ; + }); + + // Prune localized content files from template list + Event::listen('pages.content.templateList', function($widget, $templates) { + return EventRegistry::instance() + ->pruneTranslatedContentTemplates($templates) + ; + }); + + // Look at session for locale using middleware + \Cms\Classes\CmsController::extend(function($controller) { + $controller->middleware(\RainLab\Translate\Classes\LocaleMiddleware::class); + }); + + // Append current locale to static page's cache keys + $modifyKey = function (&$key) { + $key = $key . '-' . Lang::getLocale(); + }; + Event::listen('pages.router.getCacheKey', $modifyKey); + Event::listen('pages.page.getMenuCacheKey', $modifyKey); + Event::listen('pages.snippet.getMapCacheKey', $modifyKey); + Event::listen('pages.snippet.getPartialMapCacheKey', $modifyKey); + + if (class_exists('\RainLab\Pages\Classes\SnippetManager')) { + $handler = function ($controller, $template, $type) { + if (!$template->methodExists('getDirtyLocales')) { + return; + } + + // Get the locales that have changed + $dirtyLocales = $template->getDirtyLocales(); + + if (!empty($dirtyLocales)) { + $currentLocale = Lang::getLocale(); + + foreach ($dirtyLocales as $locale) { + if (!$template->isTranslateDirty(null, $locale)) { + continue; + } + + // Clear the RainLab.Pages caches for each dirty locale + App::setLocale($locale); + \RainLab\Pages\Plugin::clearCache(); + } + + // Restore the original locale for this request + App::setLocale($currentLocale); + } + }; + + Event::listen('cms.template.save', $handler); + Event::listen('pages.object.save', $handler); + } + } + + /** + * registerComponents + */ + public function registerComponents() + { + return [ + \RainLab\Translate\Components\LocalePicker::class => 'localePicker', + \RainLab\Translate\Components\AlternateHrefLangElements::class => 'alternateHrefLangElements' + ]; + } + + /** + * registerPermissions + */ + public function registerPermissions() + { + return [ + 'rainlab.translate.manage_locales' => [ + 'tab' => 'rainlab.translate::lang.plugin.tab', + 'label' => 'rainlab.translate::lang.plugin.manage_locales' + ], + 'rainlab.translate.manage_messages' => [ + 'tab' => 'rainlab.translate::lang.plugin.tab', + 'label' => 'rainlab.translate::lang.plugin.manage_messages' + ] + ]; + } + + /** + * registerSettings + */ + public function registerSettings() + { + return [ + 'locales' => [ + 'label' => 'rainlab.translate::lang.locale.title', + 'description' => 'rainlab.translate::lang.plugin.description', + 'icon' => 'icon-language', + 'url' => Backend::url('rainlab/translate/locales'), + 'order' => 550, + 'category' => 'rainlab.translate::lang.plugin.name', + 'permissions' => ['rainlab.translate.manage_locales'], + 'keywords' => 'translate', + ], + 'messages' => [ + 'label' => 'rainlab.translate::lang.messages.title', + 'description' => 'rainlab.translate::lang.messages.description', + 'icon' => 'icon-list-alt', + 'url' => Backend::url('rainlab/translate/messages'), + 'order' => 551, + 'category' => 'rainlab.translate::lang.plugin.name', + 'permissions' => ['rainlab.translate.manage_messages'], + 'keywords' => 'translate', + ] + ]; + } + + /** + * registerMarkupTags for Twig + * @return array + */ + public function registerMarkupTags() + { + return [ + 'filters' => [ + '_' => [$this, 'translateString'], + '__' => [$this, 'translatePlural'], + 'transRaw' => [$this, 'translateRawString'], + 'transRawPlural' => [$this, 'translateRawPlural'], + 'localeUrl' => [$this, 'localeUrl'], + ] + ]; + } + + /** + * registerFormWidgets for multi-lingual + */ + public function registerFormWidgets() + { + $mediaFinderClass = class_exists('System') + ? \RainLab\Translate\FormWidgets\MLMediaFinderv2::class + : \RainLab\Translate\FormWidgets\MLMediaFinder::class; + + return [ + \RainLab\Translate\FormWidgets\MLText::class => 'mltext', + \RainLab\Translate\FormWidgets\MLTextarea::class => 'mltextarea', + \RainLab\Translate\FormWidgets\MLRichEditor::class => 'mlricheditor', + \RainLab\Translate\FormWidgets\MLMarkdownEditor::class => 'mlmarkdowneditor', + \RainLab\Translate\FormWidgets\MLRepeater::class => 'mlrepeater', + \RainLab\Translate\FormWidgets\MLNestedForm::class => 'mlnestedform', + $mediaFinderClass => 'mlmediafinder', + ]; + } + + /** + * registerAssetBundles for compilation + */ + protected function registerAssetBundles() + { + CombineAssets::registerCallback(function ($combiner) { + $combiner->registerBundle('$/rainlab/translate/assets/less/messages.less'); + $combiner->registerBundle('$/rainlab/translate/assets/less/multilingual.less'); + }); + } + + /** + * localeUrl builds a localized URL + */ + public function localeUrl($url, $locale) + { + $translator = Translator::instance(); + + $parts = parse_url($url); + + $path = array_get($parts, 'path'); + + return http_build_url($parts, [ + 'path' => '/' . $translator->getPathInLocale($path, $locale) + ]); + } + + /** + * translateString + */ + public function translateString($string, $params = [], $locale = null) + { + return Message::trans($string, $params, $locale); + } + + /** + * translatePlural + */ + public function translatePlural($string, $count = 0, $params = [], $locale = null) + { + return Lang::choice(Message::trans($string, $params, $locale), $count, $params); + } + + /** + * translateRawString + */ + public function translateRawString($string, $params = [], $locale = null) + { + return Message::transRaw($string, $params, $locale); + } + + /** + * translateRawPlural + */ + public function translateRawPlural($string, $count = 0, $params = [], $locale = null) + { + return Lang::choice(Message::transRaw($string, $params, $locale), $count, $params); + } +} diff --git a/plugins/rainlab/translate/README.md b/plugins/rainlab/translate/README.md new file mode 100644 index 0000000..6522717 --- /dev/null +++ b/plugins/rainlab/translate/README.md @@ -0,0 +1,394 @@ +# Translation plugin + +Enables multi-lingual sites. + +## Selecting a language + +Different languages can be set up in the back-end area, with a single default language selected. This activates the use of the language on the front-end and in the back-end UI. + +A visitor can select a language by prefixing the language code to the URL, this is then stored in the user's session as their chosen language. For example: + +* `http://website/ru/` will display the site in Russian +* `http://website/fr/` will display the site in French +* `http://website/` will display the site in the default language or the user's chosen language. + +## Language Picker Component + +A visitor can select their chosen language using the `LocalePicker` component. This component will display a simple dropdown that changes the page language depending on the selection. + + title = "Home" + url = "/" + + [localePicker] + == + +

    {{ 'Please select your language:'|_ }}

    + {% component 'localePicker' %} + +If translated, the text above will appear as whatever language is selected by the user. The dropdown is very basic and is intended to be restyled. A simpler example might be: + + [...] + == + +

    + Switch language to: + English, + Russian +

    + +## Message translation + +Message or string translation is the conversion of adhoc strings used throughout the site. A message can be translated with parameters. + + {{ 'site.name'|_ }} + + {{ 'Welcome to our website!'|_ }} + + {{ 'Hello :name!'|_({ name: 'Friend' }) }} + +A message can also be translated for a choice usage. + + {{ 'There are no apples|There are :number applies!'|__(2, { number: 'two' }) }} + +Or you set a locale manually by passing a second argument. + + {{ 'this is always english'|_({}, 'en') }} + +Themes can provide default values for these messages by defining a `translate` key in the `theme.yaml` file, located in the theme directory. + + name: My Theme + # [...] + + translate: + en: + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + title.video: 'Screencast Video' + +You may also define the translations in a separate file, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang.yaml** inside the theme. + + name: My Theme + # [...] + + translate: config/lang.yaml + +This is an example of **config/lang.yaml** file with two languages: + + en: + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + hr: + site.name: 'Moje web stranice' + nav.home: 'Početna' + nav.video: 'Video' + title.home: 'Dobrodošli' + +You may also define the translations in a separate file per locale, where the path is relative to the theme. The following definition will source the default messages from the file **config/lang-en.yaml** inside the theme for the english locale and from the file **config/lang-fr.yaml** for the french locale. + + name: My Theme + # [...] + + translate: + en: config/lang-en.yaml + fr: config/lang-fr.yaml + +This is an example for the **config/lang-en.yaml** file: + + site.name: 'My Website' + nav.home: 'Home' + nav.video: 'Video' + title.home: 'Welcome Home' + +In order to make these default values reflected to your frontend site, go to **Settings -> Translate messages** in the backend and hit **Scan for messages**. They will also be loaded automatically when the theme is activated. + +The same operation can be performed with the `translate:scan` artisan command. It may be worth including it in a deployment script to automatically fetch updated messages: + + php artisan translate:scan + +Add the `--purge` option to clear old messages first: + + php artisan translate:scan --purge + +## Content & mail template translation + +This plugin activates a feature in the CMS that allows content & mail template files to use language suffixes, for example: + +* **welcome.htm** will contain the content or mail template in the default language. +* **welcome-ru.htm** will contain the content or mail template in Russian. +* **welcome-fr.htm** will contain the content or mail template in French. + +## Model translation + +Models can have their attributes translated by using the `RainLab.Translate.Behaviors.TranslatableModel` behavior and specifying which attributes to translate in the class. + + class User + { + public $implement = ['RainLab.Translate.Behaviors.TranslatableModel']; + + public $translatable = ['name']; + } + +The attribute will then contain the default language value and other language code values can be created by using the `translateContext()` method. + + $user = User::first(); + + // Outputs the name in the default language + echo $user->name; + + $user->translateContext('fr'); + + // Outputs the name in French + echo $user->name; + +You may use the same process for setting values. + + $user = User::first(); + + // Sets the name in the default language + $user->name = 'English'; + + $user->translateContext('fr'); + + // Sets the name in French + $user->name = 'Anglais'; + +The `lang()` method is a shorthand version of `translateContext()` and is also chainable. + + // Outputs the name in French + echo $user->lang('fr')->name; + +This can be useful inside a Twig template. + + {{ user.lang('fr').name }} + +There are ways to get and set attributes without changing the context. + + // Gets a single translated attribute for a language + $user->getAttributeTranslated('name', 'fr'); + + // Sets a single translated attribute for a language + $user->setAttributeTranslated('name', 'Jean-Claude', 'fr'); + +## Theme data translation + +It is also possible to translate theme customisation options. Just mark your form fields with `translatable` property and the plugin will take care about everything else: + + tabs: + fields: + website_name: + tab: Info + label: Website Name + type: text + default: Your website name + translatable: true + +## Fallback attribute values + +By default, untranslated attributes will fall back to the default locale. This behavior can be disabled by calling the `noFallbackLocale` method. + + $user = User::first(); + + $user->noFallbackLocale()->lang('fr'); + + // Returns NULL if there is no French translation + $user->name; + +## Indexed attributes + +Translatable model attributes can also be declared as an index by passing the `$transatable` attribute value as an array. The first value is the attribute name, the other values represent options, in this case setting the option `index` to `true`. + + public $translatable = [ + 'name', + ['slug', 'index' => true] + ]; + +Once an attribute is indexed, you may use the `transWhere` method to apply a basic query to the model. + + Post::transWhere('slug', 'hello-world')->first(); + +The `transWhere` method accepts a third argument to explicitly pass a locale value, otherwise it will be detected from the environment. + + Post::transWhere('slug', 'hello-world', 'en')->first(); + +## URL translation + +Pages in the CMS support translating the URL property. Assuming you have 3 languages set up: + +- en: English +- fr: French +- ru: Russian + +There is a page with the following content: + + url = "/contact" + + [viewBag] + localeUrl[ru] = "/контакт" + == +

    Page content

    + +The word "Contact" in French is the same so a translated URL is not given, or needed. If the page has no URL override specified, then the default URL will be used. Pages will not be duplicated for a given language. + +- /fr/contact - Page in French +- /en/contact - Page in English +- /ru/контакт - Page in Russian +- /ru/contact - 404 + +## URL parameter translation + +It's possible to translate URL parameters by listening to the `translate.localePicker.translateParams` event, which is fired when switching languages. + + Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } + }); + +In YourModel, one possible implementation might look like this: + + public static function translateParams($params, $oldLocale, $newLocale) { + $newParams = $params; + foreach ($params as $paramName => $paramValue) { + $records = self::transWhere($paramName, $paramValue, $oldLocale)->first(); + if ($records) { + $records->translateContext($newLocale); + $newParams[$paramName] = $records->$paramName; + } + } + return $newParams; + } + +## Query string translation + +It's possible to translate query string parameters by listening to the `translate.localePicker.translateQuery` event, which is fired when switching languages. + + Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { + if ($page->baseFileName == 'your-page-filename') { + return YourModel::translateParams($params, $oldLocale, $newLocale); + } + }); + +For a possible implementation of the `YourModel::translateParams` method look at the example under `URL parameter translation` from above. + +## Extend theme scan + + Event::listen('rainlab.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { + ... + }); + +## Settings model translation + +It's possible to translate your settings model like any other model. To retrieve translated values use: + + Settings::instance()->getAttributeTranslated('your_attribute_name'); + +## Conditionally extending plugins + +#### Models + +It is possible to conditionally extend a plugin's models to support translation by placing an `@` symbol before the behavior definition. This is a soft implement will only use `TranslatableModel` if the Translate plugin is installed, otherwise it will not cause any errors. + + /** + * Blog Post Model + */ + class Post extends Model + { + + [...] + + /** + * Softly implement the TranslatableModel behavior. + */ + public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel']; + + /** + * @var array Attributes that support translation, if available. + */ + public $translatable = ['title']; + + [...] + + } + +The back-end forms will automatically detect the presence of translatable fields and replace their controls for multilingual equivalents. + +#### Messages + +Since the Twig filter will not be available all the time, we can pipe them to the native Laravel translation methods instead. This ensures translated messages will always work on the front end. + + /** + * Register new Twig variables + * @return array + */ + public function registerMarkupTags() + { + // Check the translate plugin is installed + if (!class_exists('RainLab\Translate\Behaviors\TranslatableModel')) + return; + + return [ + 'filters' => [ + '_' => ['Lang', 'get'], + '__' => ['Lang', 'choice'], + ] + ]; + } + +# User Interface + +#### Switching locales + +Users can switch between locales by clicking on the locale indicator on the right hand side of the Multi-language input. By holding the CMD / CTRL key all Multi-language Input fields will switch to the selected locale. + +## Integration without jQuery and October Framework files + +It is possible to use the front-end language switcher without using jQuery or the OctoberCMS AJAX Framework by making the AJAX API request yourself manually. The following is an example of how to do that. + + document.querySelector('#languageSelect').addEventListener('change', function () { + const details = { + _session_key: document.querySelector('input[name="_session_key"]').value, + _token: document.querySelector('input[name="_token"]').value, + locale: this.value + } + + let formBody = [] + + for (var property in details) { + let encodedKey = encodeURIComponent(property) + let encodedValue = encodeURIComponent(details[property]) + formBody.push(encodedKey + '=' + encodedValue) + } + + formBody = formBody.join('&') + + fetch(location.href + '/', { + method: 'POST', + body: formBody, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-OCTOBER-REQUEST-HANDLER': 'onSwitchLocale', + 'X-OCTOBER-REQUEST-PARTIALS': '', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(res => res.json()) + .then(res => window.location.replace(res.X_OCTOBER_REDIRECT)) + .catch(err => console.log(err)) + }) + +The HTML: + + {{ form_open() }} + + {{ form_close() }} diff --git a/plugins/rainlab/translate/assets/css/messages.css b/plugins/rainlab/translate/assets/css/messages.css new file mode 100644 index 0000000..13b13fa --- /dev/null +++ b/plugins/rainlab/translate/assets/css/messages.css @@ -0,0 +1,17 @@ +#messagesContainer{padding-top:20px} +.translate-messages{position:relative;margin-top:-20px} +.translate-messages .dropdown-button-placeholder{display:block;width:1px;height:20px} +.translate-messages .dropdown-to, +.translate-messages .dropdown-from{position:absolute;top:57px} +.translate-messages .dropdown-to{left:50%} +.translate-messages .dropdown-from{left:0} +.translate-messages .no-other-languages{text-align:center;padding:5px} +.translate-messages .header-language, +.translate-messages .header-swap-languages, +.translate-messages .header-hide-translated{line-height:27px} +.translate-messages .header-language{float:left;cursor:pointer} +.translate-messages .header-language span.is-default{text-transform:none} +.translate-messages .header-swap-languages{margin-right:10px;float:right;font-size:16px;cursor:pointer} +.translate-messages .header-hide-translated{float:right;text-align:right} +.translate-messages .header-hide-translated label{font-weight:normal;margin-bottom:0;margin-right:10px;font-size:12px;white-space:normal} +.translate-messages .header-hide-translated label:before{top:5px;left:0} \ No newline at end of file diff --git a/plugins/rainlab/translate/assets/css/multilingual-v1.css b/plugins/rainlab/translate/assets/css/multilingual-v1.css new file mode 100644 index 0000000..aa2ed8b --- /dev/null +++ b/plugins/rainlab/translate/assets/css/multilingual-v1.css @@ -0,0 +1,13 @@ +/* + * Legacy styles for October v1.0 + */ + +/* Fancy layout */ + +.fancy-layout .form-tabless-fields .field-multilingual .ml-btn { + color: rgba(255,255,255,0.8); +} + +.fancy-layout .form-tabless-fields .field-multilingual .ml-btn:hover { + color: #fff; +} diff --git a/plugins/rainlab/translate/assets/css/multilingual.css b/plugins/rainlab/translate/assets/css/multilingual.css new file mode 100644 index 0000000..278a925 --- /dev/null +++ b/plugins/rainlab/translate/assets/css/multilingual.css @@ -0,0 +1,23 @@ +.field-multilingual{position:relative} +.field-multilingual .ml-btn{background:transparent;position:absolute;color:#7b7b7b;text-transform:uppercase;font-size:11px;letter-spacing:1px;width:44px;padding-right:0;-webkit-box-shadow:none;box-shadow:none;text-shadow:none;z-index:2} +.field-multilingual .ml-btn:hover{color:#555} +.field-multilingual.field-multilingual-text .form-control{padding-right:44px} +.field-multilingual.field-multilingual-text .ml-btn{right:4px;top:50%;margin-top:-22px;height:44px} +.field-multilingual.field-multilingual-textarea textarea{padding-right:30px} +.field-multilingual.field-multilingual-textarea .ml-btn{right:25px;top:5px;width:24px;text-align:right;padding-left:4px} +.field-multilingual.field-multilingual-textarea .ml-dropdown-menu{top:39px;right:1px} +.field-multilingual.field-multilingual-richeditor .ml-btn{top:43px;right:25px;text-align:right} +.field-multilingual.field-multilingual-richeditor .ml-dropdown-menu{top:74px;right:12px} +.field-multilingual.field-multilingual-markdowneditor .ml-btn{top:43px;right:25px;text-align:right} +.field-multilingual.field-multilingual-markdowneditor .ml-dropdown-menu{top:74px;right:12px} +.field-multilingual.field-multilingual-repeater .ml-btn{top:-27px;right:11px;text-align:right} +.field-multilingual.field-multilingual-repeater .ml-dropdown-menu{top:0;right:0} +.field-multilingual.field-multilingual-repeater.is-empty{padding-top:5px} +.field-multilingual.field-multilingual-repeater.is-empty .ml-btn{top:-10px;right:5px;text-align:right} +.field-multilingual.field-multilingual-repeater.is-empty .ml-dropdown-menu{top:15px;right:-7px} +.field-multilingual.field-multilingual-nestedform .ml-btn{top:-3px;right:11px;text-align:right} +.field-multilingual.field-multilingual-nestedform .ml-dropdown-menu{top:24px;right:-2px} +.field-multilingual.field-multilingual-nestedform.is-paneled .ml-btn{top:7px;right:15px} +.field-multilingual.field-multilingual-nestedform.is-paneled .ml-dropdown-menu{top:34px;right:2px} +.fancy-layout .field-multilingual-text input.form-control{padding-right:44px} +.help-block.before-field + .field-multilingual.field-multilingual-textarea .ml-btn{top:-41px} diff --git a/plugins/rainlab/translate/assets/js/locales.js b/plugins/rainlab/translate/assets/js/locales.js new file mode 100644 index 0000000..10e652a --- /dev/null +++ b/plugins/rainlab/translate/assets/js/locales.js @@ -0,0 +1,28 @@ +/* + * Scripts for the Locales controller. + */ ++function ($) { "use strict"; + + var TranslateLocales = function() { + + this.clickRecord = function(recordId) { + var newPopup = $('') + + newPopup.popup({ + handler: 'onUpdateForm', + extraData: { + 'record_id': recordId, + } + }) + } + + this.createRecord = function() { + var newPopup = $('') + newPopup.popup({ handler: 'onCreateForm' }) + } + + } + + $.translateLocales = new TranslateLocales; + +}(window.jQuery); \ No newline at end of file diff --git a/plugins/rainlab/translate/assets/js/messages.js b/plugins/rainlab/translate/assets/js/messages.js new file mode 100644 index 0000000..84fd27e --- /dev/null +++ b/plugins/rainlab/translate/assets/js/messages.js @@ -0,0 +1,173 @@ +/* + * Scripts for the Messages controller. + */ ++function ($) { "use strict"; + + var TranslateMessages = function() { + var self = this + + this.$form = null + + /* + * Table toolbar + */ + this.tableToolbar = null + + /* + * Input with the "from" locale value + */ + this.fromInput = null + + /* + * Template for the "from" header (title) + */ + this.fromHeader = null + + /* + * Input with the "to" locale value + */ + this.toInput = null + + /* + * Template for the "to" header (title) + */ + this.toHeader = null + + /* + * Template for the "found" header (title) + */ + this.foundHeader = null + + /* + * The table widget element + */ + this.tableElement = null + + /* + * Hide translated strings (show only from the empty data set) + */ + this.hideTranslated = false + + /* + * Data sets, complete and untranslated (empty) + */ + this.emptyDataSet = null + this.dataSet = null + + $(document).on('change', '#hideTranslated', function(){ + self.toggleTranslated($(this).is(':checked')) + self.refreshTable() + }); + + $(document).on('keyup', '.control-table input.string-input', function(ev) { + self.onApplyValue(ev) + }); + + this.toggleTranslated = function(isHide) { + this.hideTranslated = isHide + this.setTitleContents() + } + + this.setToolbarContents = function(tableToolbar) { + if (tableToolbar) this.tableToolbar = $(tableToolbar) + if (!this.tableElement) return + + var $toolbar = $('.toolbar', this.tableElement) + if ($toolbar.hasClass('message-buttons-added')) { + return + } + + if (!this.tableToolbar.length) { + return; + } + + $toolbar.addClass('message-buttons-added') + $toolbar.prepend(Mustache.render(this.tableToolbar.html())) + } + + this.setTitleContents = function(fromEl, toEl, foundEl) { + if (fromEl) this.fromHeader = $(fromEl) + if (toEl) this.toHeader = $(toEl) + if (foundEl) this.foundHeader = $(foundEl) + if (!this.tableElement) return + + if (!this.toHeader.length) { + return; + } + + var $headers = $('table.headers th', this.tableElement) + $headers.eq(0).html(this.fromHeader.html()) + $headers.eq(1).html(Mustache.render(this.toHeader.html(), { hideTranslated: this.hideTranslated } )) + $headers.eq(2).html(this.foundHeader.html()) + } + + this.setTableElement = function(el) { + this.tableElement = $(el) + this.$form = $('#messagesForm') + this.fromInput = this.$form.find('input[name=locale_from]') + this.toInput = this.$form.find('input[name=locale_to]') + + this.tableElement.one('oc.tableUpdateData', $.proxy(this.updateTableData, this)) + } + + this.onApplyValue = function(ev) { + if (ev.keyCode == 13) { + var $table = $(ev.currentTarget).closest('[data-control=table]') + + if (!$table.length) { + return + } + + var tableObj = $table.data('oc.table') + if (tableObj) { + tableObj.setCellValue($(ev.currentTarget).closest('td').get(0), ev.currentTarget.value) + tableObj.commitEditedRow() + } + } + } + + this.updateTableData = function(event, records) { + if (this.hideTranslated && !records.length) { + self.toggleTranslated($(this).is(':checked')) + self.refreshTable() + } + } + + this.toggleDropdown = function(el) { + setTimeout(function(){ $(el).dropdown('toggle') }, 1) + return false + } + + this.setLanguage = function(type, code) { + if (type == 'to') + this.toInput.val(code) + else if (type == 'from') + this.fromInput.val(code) + + this.refreshGrid() + return false + } + + this.swapLanguages = function() { + var from = this.fromInput.val(), + to = this.toInput.val() + + this.toggleTranslated(false) + this.fromInput.val(to) + this.toInput.val(from) + this.refreshGrid() + } + + this.refreshGrid = function() { + this.$form.request('onRefresh') + } + + this.refreshTable = function() { + this.tableElement.table('updateDataTable') + } + + } + + $.translateMessages = new TranslateMessages; + +}(window.jQuery); diff --git a/plugins/rainlab/translate/assets/js/multilingual.js b/plugins/rainlab/translate/assets/js/multilingual.js new file mode 100644 index 0000000..2fd6bc3 --- /dev/null +++ b/plugins/rainlab/translate/assets/js/multilingual.js @@ -0,0 +1,145 @@ +/* + * Multi lingual control plugin + * + * Data attributes: + * - data-control="multilingual" - enables the plugin on an element + * - data-default-locale="en" - default locale code + * - data-placeholder-field="#placeholderField" - an element that contains the placeholder value + * + * JavaScript API: + * $('a#someElement').multiLingual({ option: 'value' }) + * + * Dependences: + * - Nil + */ + ++function ($) { "use strict"; + + // MULTILINGUAL CLASS DEFINITION + // ============================ + + var MultiLingual = function(element, options) { + var self = this + this.options = options + this.$el = $(element) + + this.$activeField = null + this.$activeButton = $('[data-active-locale]', this.$el) + this.$dropdown = $('ul.ml-dropdown-menu', this.$el) + this.$placeholder = $(this.options.placeholderField) + + /* + * Init locale + */ + this.activeLocale = this.options.defaultLocale + this.$activeField = this.getLocaleElement(this.activeLocale) + this.$activeButton.text(this.activeLocale) + + this.$dropdown.on('click', '[data-switch-locale]', this.$activeButton, function(event){ + var currentLocale = event.data.text(); + var selectedLocale = $(this).data('switch-locale') + + // only call setLocale() if locale has changed + if (selectedLocale != currentLocale) { + self.setLocale(selectedLocale) + } + + /* + * If Ctrl/Cmd key is pressed, find other instances and switch + */ + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + $('[data-switch-locale="'+selectedLocale+'"]').click() + } + }) + + this.$placeholder.on('input', function(){ + self.$activeField.val(this.value) + }) + + /* + * Handle oc.inputPreset.beforeUpdate event + */ + $('[data-input-preset]', this.$el).on('oc.inputPreset.beforeUpdate', function(event, src) { + var sourceLocale = src.siblings('.ml-btn[data-active-locale]').text() + var targetLocale = $(this).data('locale-value') + var targetActiveLocale = $(this).siblings('.ml-btn[data-active-locale]').text() + + if (sourceLocale && targetLocale && targetActiveLocale) { + if (targetActiveLocale !== sourceLocale) + self.setLocale(sourceLocale) + $(this).data('update', sourceLocale === targetLocale) + } + }) + } + + MultiLingual.DEFAULTS = { + defaultLocale: 'en', + defaultField: null, + placeholderField: null + } + + MultiLingual.prototype.getLocaleElement = function(locale) { + var el = this.$el.find('[data-locale-value="'+locale+'"]') + return el.length ? el : null + } + + MultiLingual.prototype.getLocaleValue = function(locale) { + var value = this.getLocaleElement(locale) + return value ? value.val() : null + } + + MultiLingual.prototype.setLocaleValue = function(value, locale) { + if (locale) { + this.getLocaleElement(locale).val(value) + } + else { + this.$activeField.val(value) + } + } + + MultiLingual.prototype.setLocale = function(locale) { + this.activeLocale = locale + this.$activeField = this.getLocaleElement(locale) + this.$activeButton.text(locale) + + this.$placeholder.val(this.getLocaleValue(locale)) + this.$el.trigger('setLocale.oc.multilingual', [locale, this.getLocaleValue(locale)]) + } + + // MULTILINGUAL PLUGIN DEFINITION + // ============================ + + var old = $.fn.multiLingual + + $.fn.multiLingual = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.multilingual') + var options = $.extend({}, MultiLingual.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.multilingual', (data = new MultiLingual(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.multiLingual.Constructor = MultiLingual + + // MULTILINGUAL NO CONFLICT + // ================= + + $.fn.multiLingual.noConflict = function () { + $.fn.multiLingual = old + return this + } + + // MULTILINGUAL DATA-API + // =============== + $(document).render(function () { + $('[data-control="multilingual"]').multiLingual() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/assets/less/messages.less b/plugins/rainlab/translate/assets/less/messages.less new file mode 100644 index 0000000..7a76a04 --- /dev/null +++ b/plugins/rainlab/translate/assets/less/messages.less @@ -0,0 +1,72 @@ +#messagesContainer { + padding-top: 20px; +} + +.translate-messages { + position: relative; + margin-top: -20px; + + .dropdown-button-placeholder { + display:block; + width: 1px; + height: 20px + } + + .dropdown-to, .dropdown-from { + position: absolute; + top: 57px; + } + + .dropdown-to { + left: 50%; + } + + .dropdown-from { + left: 0; + } + + .no-other-languages { + text-align: center; + padding: 5px; + } + + .header-language, + .header-swap-languages, + .header-hide-translated { + line-height: 27px; + } + + .header-language { + float: left; + cursor: pointer; + + span.is-default { + text-transform: none; + } + } + + .header-swap-languages { + margin-right: 10px; + float: right; + font-size: 16px; + cursor: pointer; + } + + .header-hide-translated { + float: right; + text-align: right; + + label { + font-weight: normal; + margin-bottom: 0; + margin-right: 10px; + font-size: 12px; + white-space: normal; + + &:before { + top: 5px; + left: 0; + } + } + } +} diff --git a/plugins/rainlab/translate/assets/less/multilingual.less b/plugins/rainlab/translate/assets/less/multilingual.less new file mode 100644 index 0000000..8024ea5 --- /dev/null +++ b/plugins/rainlab/translate/assets/less/multilingual.less @@ -0,0 +1,148 @@ +@import "../../../../../modules/backend/assets/less/core/boot.less"; + +@multilingual-btn-color: #555555; + +.field-multilingual { + position: relative; + + .ml-btn { + background: transparent; + position: absolute; + color: lighten(@multilingual-btn-color, 15%); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; + width: 44px; + padding-right: 0; + .box-shadow(none); + text-shadow: none; + z-index: 2; + + &:hover { + color: @multilingual-btn-color; + } + } + + &.field-multilingual-text { + .form-control { + padding-right: 44px; + } + .ml-btn { + right: 4px; + top: 50%; + margin-top: -22px; + height: 44px; + } + } + + &.field-multilingual-textarea { + textarea { + // increase padding on the right so that the textarea content does not overlap with the language button + padding-right: 30px; + } + + .ml-btn { + right: 25px; + top: 5px; + width: 24px; + text-align: right; + padding-left: 4px; + } + + .ml-dropdown-menu { + top: 39px; + right: 1px; + } + } + + &.field-multilingual-richeditor { + .ml-btn { + top: 43px; + right: 25px; + text-align: right; + } + + .ml-dropdown-menu { + top: 74px; + right: 12px; + } + } + + &.field-multilingual-markdowneditor { + .ml-btn { + top: 43px; + right: 25px; + text-align: right; + } + + .ml-dropdown-menu { + top: 74px; + right: 12px; + } + } + + &.field-multilingual-repeater { + .ml-btn { + top: -27px; + right: 11px; + text-align: right; + } + + .ml-dropdown-menu { + top: 0; + right: 0; + } + + &.is-empty { + padding-top: 5px; + + .ml-btn { + top: -10px; + right: 5px; + text-align: right; + } + + .ml-dropdown-menu { + top: 15px; + right: -7px; + } + } + } + + &.field-multilingual-nestedform { + .ml-btn { + top: -3px; + right: 11px; + text-align: right; + } + + .ml-dropdown-menu { + top: 24px; + right: -2px; + } + + &.is-paneled { + .ml-btn { + top: 7px; + right: 15px; + } + + .ml-dropdown-menu { + top: 34px; + right: 2px; + } + } + } +} + +.fancy-layout { + .field-multilingual-text input.form-control { + padding-right: 44px; + } +} + +.help-block.before-field + .field-multilingual.field-multilingual-textarea { + .ml-btn { + top: -41px; + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php b/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php new file mode 100644 index 0000000..f327f2a --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatableCmsObject.php @@ -0,0 +1,230 @@ +model->bindEvent('cmsObject.fillViewBagArray', function() { + $this->mergeViewBagAttributes(); + }); + + $this->model->bindEvent('cmsObject.getTwigCacheKey', function($key) { + return $this->overrideTwigCacheKey($key); + }); + + // delete all translation files associated with the default language static page + $this->model->bindEvent('model.afterDelete', function() use ($model) { + foreach (Locale::listEnabled() as $locale => $label) { + if ($locale == $this->translatableDefault) { + continue; + } + if ($obj = $this->getCmsObjectForLocale($locale)) { + $obj->delete(); + } + } + }); + } + + /** + * Merge the viewBag array for the base and translated objects. + * @return void + */ + protected function mergeViewBagAttributes() + { + $locale = $this->translatableContext; + + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + if (isset($this->translatableViewBag[$locale])) { + $this->model->viewBag = array_merge( + $this->model->viewBag, + $this->translatableViewBag[$locale] + ); + } + } + + /** + * Translated CMS objects need their own unique cache key in twig. + * @return string|null + */ + protected function overrideTwigCacheKey($key) + { + if (!$locale = $this->translatableContext) { + return null; + } + + return $key . '-' . $locale; + } + + /** + * {@inheritDoc} + */ + public function syncTranslatableAttributes() + { + parent::syncTranslatableAttributes(); + + if ($this->model->isDirty('fileName')) { + $this->syncTranslatableFileNames(); + } + } + + /** + * If the parent model file name is changed, this should + * be reflected in the translated models also. + */ + protected function syncTranslatableFileNames() + { + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if ($locale == $this->translatableDefault) { + continue; + } + + if ($obj = $this->getCmsObjectForLocale($locale)) { + $obj->fileName = $this->model->fileName; + $obj->forceSave(); + } + } + } + + /** + * Saves the translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + /* + * Model doesn't exist yet, defer this logic in memory + */ + if (!$this->model->exists) { + $this->model->bindEventOnce('model.afterCreate', function() use ($locale) { + $this->storeTranslatableData($locale); + }); + + return; + } + + $data = $this->translatableAttributes[$locale]; + + if (!$obj = $this->getCmsObjectForLocale($locale)) { + $model = $this->createModel(); + $obj = $model::forLocale($locale, $this->model); + $obj->fileName = $this->model->fileName; + } + + if (!$this->isEmptyDataSet($data)) { + $obj->fill($data); + $obj->forceSave(); + } + } + + /** + * Returns true if all attributes are empty (false when converted to booleans). + * @param array $data + * @return bool + */ + protected function isEmptyDataSet($data) + { + return !array_get($data, 'markup') && + !count(array_filter(array_get($data, 'viewBag', []))) && + !count(array_filter(array_get($data, 'placeholders', []))); + } + + /** + * Loads the translation data from the join table. + * @param string $locale + * @return array + */ + protected function loadTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!$this->model->exists) { + return $this->translatableAttributes[$locale] = []; + } + + $obj = $this->getCmsObjectForLocale($locale); + + $result = $obj ? $obj->getAttributes() : []; + + $this->translatableViewBag[$locale] = $obj ? $obj->viewBag : []; + + return $this->translatableOriginals[$locale] = $this->translatableAttributes[$locale] = $result; + } + + protected function getCmsObjectForLocale($locale) + { + if ($locale == $this->translatableDefault) { + return $this->model; + } + + $model = $this->createModel(); + return $model::findLocale($locale, $this->model); + } + + /** + * Internal method, prepare the form model object + * @return Model + */ + protected function createModel() + { + $class = $this->getTranslatableModelClass(); + $model = new $class; + return $model; + } + + /** + * Returns a collection of fields that will be hashed. + * @return array + */ + public function getTranslatableModelClass() + { + if (property_exists($this->model, 'translatableModel')) { + return $this->model->translatableModel; + } + + return 'RainLab\Translate\Classes\MLCmsObject'; + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatableModel.php b/plugins/rainlab/translate/behaviors/TranslatableModel.php new file mode 100644 index 0000000..21c759f --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatableModel.php @@ -0,0 +1,357 @@ +morphMany['translations'] = [ + 'RainLab\Translate\Models\Attribute', + 'name' => 'model' + ]; + + // October v2.0 + if (class_exists('System')) { + $this->extendFileModels('attachOne'); + $this->extendFileModels('attachMany'); + } + + // Clean up indexes when this model is deleted + $model->bindEvent('model.afterDelete', function() use ($model) { + Db::table('rainlab_translate_attributes') + ->where('model_id', $model->getKey()) + ->where('model_type', get_class($model)) + ->delete(); + + Db::table('rainlab_translate_indexes') + ->where('model_id', $model->getKey()) + ->where('model_type', get_class($model)) + ->delete(); + }); + } + + /** + * extendFileModels will swap the standard File model with MLFile instead + */ + protected function extendFileModels(string $relationGroup) + { + foreach ($this->model->$relationGroup as $relationName => $relationObj) { + $relationClass = is_array($relationObj) ? $relationObj[0] : $relationObj; + if ($relationClass === \System\Models\File::class) { + if (is_array($relationObj)) { + $this->model->$relationGroup[$relationName][0] = \RainLab\Translate\Models\MLFile::class; + } + else { + $this->model->$relationGroup[$relationName] = \RainLab\Translate\Models\MLFile::class; + } + } + } + } + + /** + * scopeTransWhere applies a translatable index to a basic query. This scope will join the + * index table and can be executed neither more than once, nor with scopeTransOrder. + * @param Builder $query + * @param string $index + * @param string $value + * @param string $locale + * @return Builder + */ + public function scopeTransWhere($query, $index, $value, $locale = null, $operator = '=') + { + return $this->transWhereInternal($query, $index, $value, [ + 'locale' => $locale, + 'operator' => $operator + ]); + } + + /** + * scopeTransWhereNoFallback is identical to scopeTransWhere except it will not + * use a fallback query when there are no indexes found. + * @see scopeTransWhere + */ + public function scopeTransWhereNoFallback($query, $index, $value, $locale = null, $operator = '=') + { + // Ignore translatable indexes in default locale context + if(($locale ?: $this->translatableContext) === $this->translatableDefault) { + return $query->where($index, $operator, $value); + } + + return $this->transWhereInternal($query, $index, $value, [ + 'locale' => $locale, + 'operator' => $operator, + 'noFallback' => true + ]); + } + + /** + * transWhereInternal + * @link https://github.com/rainlab/translate-plugin/pull/623 + */ + protected function transWhereInternal($query, $index, $value, $options = []) + { + extract(array_merge([ + 'locale' => null, + 'operator' => '=', + 'noFallback' => false + ], $options)); + + if (!$locale) { + $locale = $this->translatableContext; + } + + // Separate query into two separate queries for improved performance + $translateIndexes = Db::table('rainlab_translate_indexes') + ->where('rainlab_translate_indexes.model_type', '=', $this->getClass()) + ->where('rainlab_translate_indexes.locale', '=', $locale) + ->where('rainlab_translate_indexes.item', $index) + ->where('rainlab_translate_indexes.value', $operator, $value) + ->pluck('model_id') + ; + + if ($translateIndexes->count() || $noFallback) { + $query->whereIn($this->model->getQualifiedKeyName(), $translateIndexes); + } + else { + $query->where($index, $operator, $value); + } + + return $query; + } + + /** + * Applies a sort operation with a translatable index to a basic query. This scope will join the index table. + * @param Builder $query + * @param string $index + * @param string $direction + * @param string $locale + * @return Builder + */ + public function scopeTransOrderBy($query, $index, $direction = 'asc', $locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + $indexTableAlias = 'rainlab_translate_indexes_' . $index . '_' . $locale; + + $query->select( + $this->model->getTable().'.*', + Db::raw('COALESCE(' . $indexTableAlias . '.value, '. $this->model->getTable() .'.'.$index.') AS translate_sorting_key') + ); + + $query->orderBy('translate_sorting_key', $direction); + + $this->joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias); + + return $query; + } + + /** + * Joins the translatable indexes table to a query. + * @param Builder $query + * @param string $locale + * @param string $indexTableAlias + * @return Builder + */ + protected function joinTranslateIndexesTable($query, $locale, $index, $indexTableAlias) + { + $joinTableWithAlias = 'rainlab_translate_indexes as ' . $indexTableAlias; + // check if table with same name and alias is already joined + if (collect($query->getQuery()->joins)->contains('table', $joinTableWithAlias)) { + return $query; + } + + $query->leftJoin($joinTableWithAlias, function($join) use ($locale, $index, $indexTableAlias) { + $join + ->on(Db::raw(DbDongle::cast($this->model->getQualifiedKeyName(), 'TEXT')), '=', $indexTableAlias . '.model_id') + ->where($indexTableAlias . '.model_type', '=', $this->getClass()) + ->where($indexTableAlias . '.item', '=', $index) + ->where($indexTableAlias . '.locale', '=', $locale); + }); + + return $query; + } + + /** + * Saves the translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + /* + * Model doesn't exist yet, defer this logic in memory + */ + if (!$this->model->exists) { + $this->model->bindEventOnce('model.afterCreate', function() use ($locale) { + $this->storeTranslatableData($locale); + }); + + return; + } + + /** + * @event model.translate.resolveComputedFields + * Resolve computed fields before saving + * + * Example usage: + * + * Override Model's __construct method + * + * public function __construct(array $attributes = []) + * { + * parent::__construct($attributes); + * + * $this->bindEvent('model.translate.resolveComputedFields', function ($locale) { + * return [ + * 'content_html' => + * self::formatHtml($this->asExtension('TranslatableModel') + * ->getAttributeTranslated('content', $locale)) + * ]; + * }); + * } + * + */ + $computedFields = $this->model->fireEvent('model.translate.resolveComputedFields', [$locale], true); + if (is_array($computedFields)) { + $this->translatableAttributes[$locale] = array_merge($this->translatableAttributes[$locale], $computedFields); + } + + $this->storeTranslatableBasicData($locale); + $this->storeTranslatableIndexData($locale); + } + + /** + * Saves the basic translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableBasicData($locale = null) + { + $data = json_encode($this->translatableAttributes[$locale], JSON_UNESCAPED_UNICODE); + + $obj = Db::table('rainlab_translate_attributes') + ->where('locale', $locale) + ->where('model_id', $this->model->getKey()) + ->where('model_type', $this->getClass()); + + if ($obj->count() > 0) { + $obj->update(['attribute_data' => $data]); + } + else { + Db::table('rainlab_translate_attributes')->insert([ + 'locale' => $locale, + 'model_id' => $this->model->getKey(), + 'model_type' => $this->getClass(), + 'attribute_data' => $data + ]); + } + } + + /** + * Saves the indexed translation data in the join table. + * @param string $locale + * @return void + */ + protected function storeTranslatableIndexData($locale = null) + { + $optionedAttributes = $this->getTranslatableAttributesWithOptions(); + if (!count($optionedAttributes)) { + return; + } + + $data = $this->translatableAttributes[$locale]; + + foreach ($optionedAttributes as $attribute => $options) { + if (!array_get($options, 'index', false)) { + continue; + } + + $value = array_get($data, $attribute); + + $obj = Db::table('rainlab_translate_indexes') + ->where('locale', $locale) + ->where('model_id', $this->model->getKey()) + ->where('model_type', $this->getClass()) + ->where('item', $attribute); + + $recordExists = $obj->count() > 0; + + if (!strlen($value)) { + if ($recordExists) { + $obj->delete(); + } + continue; + } + + if ($recordExists) { + $obj->update(['value' => $value]); + } + else { + Db::table('rainlab_translate_indexes')->insert([ + 'locale' => $locale, + 'model_id' => $this->model->getKey(), + 'model_type' => $this->getClass(), + 'item' => $attribute, + 'value' => $value + ]); + } + } + } + + /** + * Loads the translation data from the join table. + * @param string $locale + * @return array + */ + protected function loadTranslatableData($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!$this->model->exists) { + return $this->translatableAttributes[$locale] = []; + } + + $obj = $this->model->translations->first(function ($value, $key) use ($locale) { + return $value->attributes['locale'] === $locale; + }); + + $result = $obj ? json_decode($obj->attribute_data, true) : []; + + return $this->translatableOriginals[$locale] = $this->translatableAttributes[$locale] = $result; + } + + /** + * Returns the class name of the model. Takes any + * custom morphMap aliases into account. + * + * @return string + */ + protected function getClass() + { + return $this->model->getMorphClass(); + } +} diff --git a/plugins/rainlab/translate/behaviors/TranslatablePage.php b/plugins/rainlab/translate/behaviors/TranslatablePage.php new file mode 100644 index 0000000..4797e29 --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatablePage.php @@ -0,0 +1,140 @@ +model->bindEvent('model.afterFetch', function() { + $this->translatableOriginals = $this->getModelAttributes(); + + if (!App::runningInBackend()) { + $this->rewriteTranslatablePageAttributes(); + } + }); + } + + public function isTranslatable($key) + { + if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) { + return false; + } + + return in_array($key, $this->model->translatable); + } + + public function getTranslatableAttributes() + { + $attributes = []; + + foreach ($this->model->translatable as $attr) { + $attributes[] = 'settings['.$attr.']'; + } + + return $attributes; + } + + public function getModelAttributes() + { + $attributes = []; + + foreach ($this->model->translatable as $attr) { + $attributes[$attr] = $this->model[$attr]; + } + + return $attributes; + } + + public function initTranslatableContext() + { + parent::initTranslatableContext(); + $this->translatableOriginals = $this->getModelAttributes(); + } + + public function rewriteTranslatablePageAttributes($locale = null) + { + $locale = $locale ?: $this->translatableContext; + + foreach ($this->model->translatable as $attr) { + $localeAttr = $this->translatableOriginals[$attr]; + + if ($locale != $this->translatableDefault) { + $translated = $this->getAttributeTranslated($attr, $locale); + $localeAttr = ($translated ?: $this->translatableUseFallback) ? $localeAttr : null; + } + + $this->model[$attr] = $localeAttr; + } + } + + public function getAttributeTranslated($key, $locale = null) + { + $locale = $locale ?: $this->translatableContext; + + if (strpbrk($key, '[]') !== false) { + // Retrieve attr name within brackets (i.e. settings[title] yields title) + $key = preg_split("/[\[\]]/", $key)[1]; + } + + $default = ($locale == $this->translatableDefault || $this->translatableUseFallback) + ? array_get($this->translatableOriginals, $key) + : ''; + + $localeAttr = sprintf('viewBag.locale%s.%s', ucfirst($key), $locale); + return array_get($this->model->attributes, $localeAttr, $default); + } + + public function setAttributeTranslated($key, $value, $locale = null) + { + $locale = $locale ?: $this->translatableContext; + + if ($locale == $this->translatableDefault) { + return; + } + + if (strpbrk($key, '[]') !== false) { + // Retrieve attr name within brackets (i.e. settings[title] yields title) + $key = preg_split("/[\[\]]/", $key)[1]; + } + + if ($value == array_get($this->translatableOriginals, $key)) { + return; + } + + $this->saveTranslation($key, $value, $locale); + $this->model->bindEventOnce('model.beforeSave', function() use ($key, $value, $locale) { + $this->saveTranslation($key, $value, $locale); + }); + } + + public function saveTranslation($key, $value, $locale) + { + $localeAttr = sprintf('viewBag.locale%s.%s', ucfirst($key), $locale); + if (!$value) { + array_forget($this->model->attributes, $localeAttr); + } + else { + array_set($this->model->attributes, $localeAttr, $value); + } + } + + // Not needed but parent abstract model requires those + protected function storeTranslatableData($locale = null) {} + protected function loadTranslatableData($locale = null) {} +} diff --git a/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php b/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php new file mode 100644 index 0000000..bbc7147 --- /dev/null +++ b/plugins/rainlab/translate/behaviors/TranslatablePageUrl.php @@ -0,0 +1,173 @@ +model = $model; + + $this->initTranslatableContext(); + + $this->model->bindEvent('model.afterFetch', function() { + $this->translatableDefaultUrl = $this->getModelUrl(); + + if (!App::runningInBackend()) { + $this->rewriteTranslatablePageUrl(); + } + }); + } + + protected function setModelUrl($value) + { + if ($this->model instanceof \RainLab\Pages\Classes\Page) { + array_set($this->model->attributes, 'viewBag.url', $value); + } + else { + $this->model->url = $value; + } + } + + protected function getModelUrl() + { + if ($this->model instanceof \RainLab\Pages\Classes\Page) { + return array_get($this->model->attributes, 'viewBag.url'); + } + else { + return $this->model->url; + } + } + + /** + * Initializes this class, sets the default language code to use. + * @return void + */ + public function initTranslatableContext() + { + $translate = Translator::instance(); + $this->translatableContext = $translate->getLocale(); + $this->translatableDefault = $translate->getDefaultLocale(); + } + + /** + * Checks if a translated URL exists and rewrites it, this method + * should only be called from the context of front-end. + * @return void + */ + public function rewriteTranslatablePageUrl($locale = null) + { + $locale = $locale ?: $this->translatableContext; + $localeUrl = $this->translatableDefaultUrl; + + if ($locale != $this->translatableDefault) { + $localeUrl = $this->getSettingsUrlAttributeTranslated($locale) ?: $localeUrl; + } + + $this->setModelUrl($localeUrl); + } + + /** + * Determines if a locale has a translated URL. + * @return bool + */ + public function hasTranslatablePageUrl($locale = null) + { + $locale = $locale ?: $this->translatableContext; + + return strlen($this->getSettingsUrlAttributeTranslated($locale)) > 0; + } + + /** + * Mutator detected by MLControl + * @return string + */ + public function getSettingsUrlAttributeTranslated($locale) + { + $defaults = ($locale == $this->translatableDefault) ? $this->translatableDefaultUrl : null; + + return array_get($this->model->attributes, 'viewBag.localeUrl.'.$locale, $defaults); + } + + /** + * Mutator detected by MLControl + * @return void + */ + public function setSettingsUrlAttributeTranslated($value, $locale) + { + if ($locale == $this->translatableDefault) { + return; + } + + if ($value == $this->translatableDefaultUrl) { + return; + } + + /* + * The CMS controller will purge attributes just before saving, this + * will ensure the attributes are injected after this logic. + */ + $this->model->bindEventOnce('model.beforeSave', function() use ($value, $locale) { + if (!$value) { + array_forget($this->model->attributes, 'viewBag.localeUrl.'.$locale); + } + else { + array_set($this->model->attributes, 'viewBag.localeUrl.'.$locale, $value); + } + }); + } + + /** + * Mutator detected by MLControl, proxy for Static Pages plugin. + * @return string + */ + public function getViewBagUrlAttributeTranslated($locale) + { + return $this->getSettingsUrlAttributeTranslated($locale); + } + + /** + * Mutator detected by MLControl, proxy for Static Pages plugin. + * @return void + */ + public function setViewBagUrlAttributeTranslated($value, $locale) + { + $this->setSettingsUrlAttributeTranslated($value, $locale); + } +} diff --git a/plugins/rainlab/translate/classes/EventRegistry.php b/plugins/rainlab/translate/classes/EventRegistry.php new file mode 100644 index 0000000..262bf91 --- /dev/null +++ b/plugins/rainlab/translate/classes/EventRegistry.php @@ -0,0 +1,408 @@ +code ?? null; + + $properties = []; + foreach ($locales as $locale => $label) { + if ($locale == $defaultLocale) { + continue; + } + + $properties[] = [ + 'property' => 'localeUrl.'.$locale, + 'title' => 'cms::lang.editor.url', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeTitle.'.$locale, + 'title' => 'cms::lang.editor.title', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeDescription.'.$locale, + 'title' => 'cms::lang.editor.description', + 'tab' => $label, + 'type' => 'text', + ]; + + $properties[] = [ + 'property' => 'localeMeta_title.'.$locale, + 'title' => 'cms::lang.editor.meta_title', + 'tab' => $label, + 'type' => 'string', + ]; + + $properties[] = [ + 'property' => 'localeMeta_description.'.$locale, + 'title' => 'cms::lang.editor.meta_description', + 'tab' => $label, + 'type' => 'text', + ]; + } + + $dataHolder->buttons[] = [ + 'button' => 'rainlab.translate::lang.plugin.name', + 'icon' => 'octo-icon-globe', + 'popupTitle' => 'Translate Page Properties', + 'properties' => $properties + ]; + } + + // + // Utility + // + + /** + * registerFormFieldReplacements + */ + public function registerFormFieldReplacements($widget) + { + // Replace with ML Controls for translatable attributes + $this->registerModelTranslation($widget); + + // Handle URL translations + $this->registerPageUrlTranslation($widget); + + // Handle RainLab.Pages MenuItem translations + if (PluginManager::instance()->exists('RainLab.Pages')) { + $this->registerMenuItemTranslation($widget); + } + } + + /** + * registerMenuItemTranslation for RainLab.Pages MenuItem data + * @param Backend\Widgets\Form $widget + */ + public function registerMenuItemTranslation($widget) + { + if ($widget->model instanceof \RainLab\Pages\Classes\MenuItem) { + $defaultLocale = LocaleModel::getDefault(); + $availableLocales = LocaleModel::listAvailable(); + $fieldsToTranslate = ['title', 'url']; + + // Replace specified fields with multilingual versions + foreach ($fieldsToTranslate as $fieldName) { + $widget->fields[$fieldName]['type'] = 'mltext'; + + foreach ($availableLocales as $code => $locale) { + if (!$defaultLocale || $defaultLocale->code === $code) { + continue; + } + + // Add data locker fields for the different locales under the `viewBag[locale]` property + $widget->fields["viewBag[locale][$code][$fieldName]"] = [ + 'cssClass' => 'hidden', + 'attributes' => [ + 'data-locale' => $code, + 'data-field-name' => $fieldName, + ], + ]; + } + } + } + } + + // + // Translate URLs + // + + /** + * registerPageUrlTranslation + */ + public function registerPageUrlTranslation($widget) + { + if (!$model = $widget->model) { + return; + } + + if ( + $model instanceof Page && + isset($widget->fields['settings[url]']) + ) { + $widget->fields['settings[url]']['type'] = 'mltext'; + } + elseif ( + $model instanceof \RainLab\Pages\Classes\Page && + isset($widget->fields['viewBag[url]']) + ) { + $widget->fields['viewBag[url]']['type'] = 'mltext'; + } + } + + // + // Translatable behavior + // + + /** + * registerModelTranslation automatically replaces form fields for multi-lingual equivalents + */ + public function registerModelTranslation($widget) + { + if ($widget->isNested) { + return; + } + + if (!$model = $widget->model) { + return; + } + + if (!method_exists($model, 'isClassExtendedWith')) { + return; + } + + if ( + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableModel::class) && + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatablePage::class) && + !$model->isClassExtendedWith(\RainLab\Translate\Behaviors\TranslatableCmsObject::class) + ) { + return; + } + + if (!$model->hasTranslatableAttributes()) { + return; + } + + if (!empty($widget->fields)) { + $widget->fields = $this->processFormMLFields($widget->fields, $model); + } + + if (!empty($widget->tabs['fields'])) { + $widget->tabs['fields'] = $this->processFormMLFields($widget->tabs['fields'], $model); + } + + if (!empty($widget->secondaryTabs['fields'])) { + $widget->secondaryTabs['fields'] = $this->processFormMLFields($widget->secondaryTabs['fields'], $model); + } + } + + /** + * processFormMLFields function to replace standard fields with multi-lingual equivalents + * @param array $fields + * @param Model $model + * @return array + */ + protected function processFormMLFields($fields, $model) + { + $typesMap = [ + 'text' => 'mltext', + 'textarea' => 'mltextarea', + 'richeditor' => 'mlricheditor', + 'markdown' => 'mlmarkdowneditor', + 'repeater' => 'mlrepeater', + 'nestedform' => 'mlnestedform', + 'mediafinder' => 'mlmediafinder', + ]; + + $translatable = array_flip($model->getTranslatableAttributes()); + + /* + * Special: A custom field "markup_html" is used for Content templates. + */ + if ($model instanceof Content && array_key_exists('markup', $translatable)) { + $translatable['markup_html'] = true; + } + + foreach ($fields as $name => $config) { + if (!array_key_exists($name, $translatable)) { + continue; + } + + $type = array_get($config, 'type', 'text'); + + if (array_key_exists($type, $typesMap)) { + $fields[$name]['type'] = $typesMap[$type]; + } + } + + return $fields; + } + + // + // Theme + // + + /** + * importMessagesFromTheme + */ + public function importMessagesFromTheme($themeCode) + { + try { + (new ThemeScanner)->scanThemeConfigForMessages($themeCode); + } + catch (Exception $ex) {} + } + + // + // CMS objects + // + + /** + * setMessageContext for translation caching. + */ + public function setMessageContext($page) + { + if (!$page) { + return; + } + + $translator = Translator::instance(); + + Message::setContext($translator->getLocale(), $page->url); + } + + /** + * findTranslatedContentFile adds language suffixes to content files. + * @return string|null + */ + public function findTranslatedContentFile($controller, $fileName) + { + if (!strlen(File::extension($fileName))) { + $fileName .= '.htm'; + } + + /* + * Splice the active locale in to the filename + * - content.htm -> content.en.htm + */ + $locale = Translator::instance()->getLocale(); + $fileName = substr_replace($fileName, '.'.$locale, strrpos($fileName, '.'), 0); + if (($content = Content::loadCached($controller->getTheme(), $fileName)) !== null) { + return $content; + } + } + + // + // Static pages + // + + /** + * pruneTranslatedContentTemplates removes localized content files from templates collection + * @param \October\Rain\Database\Collection $templates + * @return \October\Rain\Database\Collection + */ + public function pruneTranslatedContentTemplates($templates) + { + $locales = LocaleModel::listAvailable(); + + $extensions = array_map(function($ext) { + return '.'.$ext; + }, array_keys($locales)); + + return $templates->filter(function($template) use ($extensions) { + return !Str::endsWith($template->getBaseFileName(), $extensions); + }); + } + + /** + * findLocalizedMailViewContent adds language suffixes to mail view files. + * @param \October\Rain\Mail\Mailer $mailer + * @param \Illuminate\Mail\Message $message + * @param string $view + * @param array $data + * @param string $raw + * @param string $plain + * @return bool|void Will return false if the translation process successfully replaced the original message with a translated version to prevent the original version from being processed. + */ + public function findLocalizedMailViewContent($mailer, $message, $view, $data, $raw, $plain) + { + // Raw content cannot be localized at this level + if (!empty($raw)) { + return; + } + + // Get the locale to use for this template + $locale = !empty($data['_current_locale']) ? $data['_current_locale'] : App::getLocale(); + + $factory = $mailer->getViewFactory(); + + if (!empty($view)) { + $view = $this->getLocalizedView($factory, $view, $locale); + } + + if (!empty($plain)) { + $plain = $this->getLocalizedView($factory, $plain, $locale); + } + + $code = $view ?: $plain; + if (empty($code)) { + return null; + } + + $plainOnly = empty($view); + + if (MailManager::instance()->addContentToMailer($message, $code, $data, $plainOnly)) { + // the caller who fired the event is expecting a FALSE response to halt the event + return false; + } + } + + /** + * getLocalizedView searches mail view files based on locale + * @param \October\Rain\Mail\Mailer $mailer + * @param \Illuminate\Mail\Message $message + * @param string $code + * @param string $locale + * @return string|null + */ + public function getLocalizedView($factory, $code, $locale) + { + $locale = strtolower($locale); + + $searchPaths[] = $locale; + + if (str_contains($locale, '-')) { + list($lang) = explode('-', $locale); + $searchPaths[] = $lang; + } + + foreach ($searchPaths as $path) { + $localizedView = sprintf('%s-%s', $code, $path); + + if ($factory->exists($localizedView)) { + return $localizedView; + } + } + return null; + } +} diff --git a/plugins/rainlab/translate/classes/LocaleMiddleware.php b/plugins/rainlab/translate/classes/LocaleMiddleware.php new file mode 100644 index 0000000..20d4f03 --- /dev/null +++ b/plugins/rainlab/translate/classes/LocaleMiddleware.php @@ -0,0 +1,31 @@ +isConfigured(); + + if (!$translator->loadLocaleFromRequest()) { + if (Config::get('rainlab.translate::prefixDefaultLocale')) { + $translator->loadLocaleFromSession(); + } else { + $translator->setLocale($translator->getDefaultLocale()); + } + } + + return $next($request); + } +} diff --git a/plugins/rainlab/translate/classes/MLCmsObject.php b/plugins/rainlab/translate/classes/MLCmsObject.php new file mode 100644 index 0000000..2dce2a0 --- /dev/null +++ b/plugins/rainlab/translate/classes/MLCmsObject.php @@ -0,0 +1,56 @@ +theme); + } + + public static function findLocale($locale, $page) + { + return static::forLocale($locale, $page)->find($page->fileName); + } + + /** + * Returns the directory name corresponding to the object type. + * For pages the directory name is "pages", for layouts - "layouts", etc. + * @return string + */ + public function getObjectTypeDirName() + { + $dirName = static::$parent->getObjectTypeDirName(); + + if (strlen(static::$locale)) { + $dirName .= '-' . static::$locale; + } + + return $dirName; + } +} diff --git a/plugins/rainlab/translate/classes/MLContent.php b/plugins/rainlab/translate/classes/MLContent.php new file mode 100644 index 0000000..0402d8d --- /dev/null +++ b/plugins/rainlab/translate/classes/MLContent.php @@ -0,0 +1,70 @@ +exists) { + return null; + } + + $fileName = $page->getOriginal('fileName') ?: $page->fileName; + + $fileName = static::addLocaleToFileName($fileName, $locale); + + return static::forLocale($locale, $page)->find($fileName); + } + + /** + * Returns the directory name corresponding to the object type. + * Content does not use localized sub directories, but as file suffix instead. + * @return string + */ + public function getObjectTypeDirName() + { + return static::$parent->getObjectTypeDirName(); + } + + /** + * Splice in the locale when setting the file name. + * @param mixed $value + */ + public function setFileNameAttribute($value) + { + $value = static::addLocaleToFileName($value, static::$locale); + + parent::setFileNameAttribute($value); + } + + /** + * Splice the active locale in to the filename + * - content.htm -> content.en.htm + */ + protected static function addLocaleToFileName($fileName, $locale) + { + /* + * Check locale not already present + */ + $parts = explode('.', $fileName); + array_shift($parts); + + foreach ($parts as $part) { + if (trim($part) === $locale) { + return $fileName; + } + } + + return substr_replace($fileName, '.'.$locale, strrpos($fileName, '.'), 0); + } +} diff --git a/plugins/rainlab/translate/classes/MLStaticPage.php b/plugins/rainlab/translate/classes/MLStaticPage.php new file mode 100644 index 0000000..0fba770 --- /dev/null +++ b/plugins/rainlab/translate/classes/MLStaticPage.php @@ -0,0 +1,118 @@ +getPlaceholdersAttribute(); + } + + /** + * Parses the page placeholder {% put %} tags and extracts the placeholder values. + * @return array Returns an associative array of the placeholder names and values. + */ + public function getPlaceholdersAttribute() + { + if (!strlen($this->code)) { + return []; + } + + if ($placeholders = array_get($this->attributes, 'placeholders')) { + return $placeholders; + } + + $bodyNode = $this->getTwigNodeTree($this->code)->getNode('body')->getNode(0); + if ($bodyNode instanceof \Cms\Twig\PutNode) { + $bodyNode = [$bodyNode]; + } + + $result = []; + foreach ($bodyNode as $node) { + if (!$node instanceof \Cms\Twig\PutNode) { + continue; + } + + // October CMS v2.2 and above + if (class_exists('System') && version_compare(\System::VERSION, '2.1') === 1) { + $names = $node->getNode('names'); + $values = $node->getNode('values'); + $isCapture = $node->getAttribute('capture'); + if ($isCapture) { + $name = $names->getNode(0); + $result[$name->getAttribute('name')] = trim($values->getAttribute('data')); + } + } + // Legacy PutNode support + else { + $values = $node->getNode('body'); + $result[$node->getAttribute('name')] = trim($values->getAttribute('data')); + } + } + + $this->attributes['placeholders'] = $result; + + return $result; + } + + /** + * Takes an array of placeholder data (key: code, value: content) and renders + * it as a single string of Twig markup against the "code" attribute. + * @param array $value + * @return void + */ + public function setPlaceholdersAttribute($value) + { + if (!is_array($value)) { + return; + } + + $placeholders = $value; + $result = ''; + + foreach ($placeholders as $code => $content) { + if (!strlen($content)) { + continue; + } + + $result .= '{% put '.$code.' %}'.PHP_EOL; + $result .= $content.PHP_EOL; + $result .= '{% endput %}'.PHP_EOL; + $result .= PHP_EOL; + } + + $this->attributes['code'] = trim($result); + $this->attributes['placeholders'] = $placeholders; + } + + /** + * Disables safe mode check for static pages. + * + * This allows developers to use placeholders in layouts even if safe mode is enabled. + * + * @return void + */ + protected function checkSafeMode() + { + } +} diff --git a/plugins/rainlab/translate/classes/ThemeScanner.php b/plugins/rainlab/translate/classes/ThemeScanner.php new file mode 100644 index 0000000..3ad0929 --- /dev/null +++ b/plugins/rainlab/translate/classes/ThemeScanner.php @@ -0,0 +1,229 @@ +scanForMessages(); + + /** + * @event rainlab.translate.themeScanner.afterScan + * Fires after theme scanning. + * + * Example usage: + * + * Event::listen('rainlab.translate.themeScanner.afterScan', function (ThemeScanner $scanner) { + * // added an extra scan. Add generation files... + * }); + * + */ + Event::fire('rainlab.translate.themeScanner.afterScan', [$obj]); + } + + /** + * Scans theme templates and config for messages. + * @return void + */ + public function scanForMessages() + { + // Set all messages initially as being not found. The scanner later + // sets the entries it finds as found. + Message::query()->update(['found' => false]); + + $this->scanThemeConfigForMessages(); + $this->scanThemeTemplatesForMessages(); + $this->scanMailTemplatesForMessages(); + } + + /** + * Scans the theme configuration for defined messages + * @return void + */ + public function scanThemeConfigForMessages($themeCode = null) + { + if (!$themeCode) { + $theme = Theme::getActiveTheme(); + + if (!$theme) { + return; + } + } + else { + if (!Theme::exists($themeCode)) { + return; + } + + $theme = Theme::load($themeCode); + } + + // October v2.0 + if (class_exists('System') && $theme->hasParentTheme()) { + $parentTheme = $theme->getParentTheme(); + + try { + if (!$this->scanThemeConfigForMessagesInternal($theme)) { + $this->scanThemeConfigForMessagesInternal($parentTheme); + } + } + catch (Exception $ex) { + $this->scanThemeConfigForMessagesInternal($parentTheme); + } + } + else { + $this->scanThemeConfigForMessagesInternal($theme); + } + } + + /** + * scanThemeConfigForMessagesInternal + */ + protected function scanThemeConfigForMessagesInternal(Theme $theme) + { + $config = $theme->getConfigArray('translate'); + + if (!count($config)) { + return false; + } + + $translator = Translator::instance(); + $keys = []; + + foreach ($config as $locale => $messages) { + if (is_string($messages)) { + // $message is a yaml filename, load the yaml file + $messages = $theme->getConfigArray('translate.'.$locale); + } + $keys = array_merge($keys, array_keys($messages)); + } + + Message::importMessages($keys); + + foreach ($config as $locale => $messages) { + if (is_string($messages)) { + // $message is a yaml filename, load the yaml file + $messages = $theme->getConfigArray('translate.'.$locale); + } + Message::importMessageCodes($messages, $locale); + } + } + + /** + * Scans the theme templates for message references. + * @return void + */ + public function scanThemeTemplatesForMessages() + { + $messages = []; + + foreach (Layout::all() as $layout) { + $messages = array_merge($messages, $this->parseContent($layout->markup)); + } + + foreach (Page::all() as $page) { + $messages = array_merge($messages, $this->parseContent($page->markup)); + } + + foreach (Partial::all() as $partial) { + $messages = array_merge($messages, $this->parseContent($partial->markup)); + } + + Message::importMessages($messages); + } + + /** + * Scans the mail templates for message references. + * @return void + */ + public function scanMailTemplatesForMessages() + { + $messages = []; + + foreach (MailTemplate::allTemplates() as $mailTemplate) { + $messages = array_merge($messages, $this->parseContent($mailTemplate->subject)); + $messages = array_merge($messages, $this->parseContent($mailTemplate->content_html)); + } + + Message::importMessages($messages); + } + + /** + * Parse the known language tag types in to messages. + * @param string $content + * @return array + */ + protected function parseContent($content) + { + $messages = []; + $messages = array_merge($messages, $this->processStandardTags($content)); + + return $messages; + } + + /** + * Process standard language filter tag (_|) + * @param string $content + * @return array + */ + protected function processStandardTags($content) + { + $messages = []; + + /* + * Regex used: + * + * {{'AJAX framework'|_}} + * {{\s*'([^'])+'\s*[|]\s*_\s*}} + * + * {{'AJAX framework'|_(variables)}} + * {{\s*'([^'])+'\s*[|]\s*_\s*\([^\)]+\)\s*}} + */ + + $quoteChar = preg_quote("'"); + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*(?:[|].+)?}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*\([^\)]+\)\s*}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + $quoteChar = preg_quote('"'); + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*(?:[|].+)?}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + preg_match_all('#{{\s*'.$quoteChar.'([^'.$quoteChar.']+)'.$quoteChar.'\s*[|]\s*_\s*\([^\)]+\)\s*}}#', $content, $match); + if (isset($match[1])) { + $messages = array_merge($messages, $match[1]); + } + + return $messages; + } +} diff --git a/plugins/rainlab/translate/classes/TranslatableBehavior.php b/plugins/rainlab/translate/classes/TranslatableBehavior.php new file mode 100644 index 0000000..3ae9e65 --- /dev/null +++ b/plugins/rainlab/translate/classes/TranslatableBehavior.php @@ -0,0 +1,494 @@ +model = $model; + + $this->initTranslatableContext(); + + $this->model->bindEvent('model.beforeGetAttribute', function ($key) use ($model) { + if ($this->isTranslatable($key)) { + $value = $this->getAttributeTranslated($key); + if ($model->hasGetMutator($key)) { + $method = 'get' . Str::studly($key) . 'Attribute'; + $value = $model->{$method}($value); + } + return $value; + } + }); + + $this->model->bindEvent('model.beforeSetAttribute', function ($key, $value) use ($model) { + if ($this->isTranslatable($key)) { + $value = $this->setAttributeTranslated($key, $value); + if ($model->hasSetMutator($key)) { + $method = 'set' . Str::studly($key) . 'Attribute'; + $value = $model->{$method}($value); + } + return $value; + } + }); + + $this->model->bindEvent('model.saveInternal', function() { + $this->syncTranslatableAttributes(); + }); + } + + /** + * Initializes this class, sets the default language code to use. + * @return void + */ + public function initTranslatableContext() + { + $translate = Translator::instance(); + $this->translatableContext = $translate->getLocale(); + $this->translatableDefault = $translate->getDefaultLocale(); + } + + /** + * Checks if an attribute should be translated or not. + * @param string $key + * @return boolean + */ + public function isTranslatable($key) + { + if ($key === 'translatable' || $this->translatableDefault == $this->translatableContext) { + return false; + } + + return in_array($key, $this->model->getTranslatableAttributes()); + } + + /** + * Disables translation fallback locale. + * @return self + */ + public function noFallbackLocale() + { + $this->translatableUseFallback = false; + + return $this->model; + } + + /** + * Enables translation fallback locale. + * @return self + */ + public function withFallbackLocale() + { + $this->translatableUseFallback = true; + + return $this->model; + } + + /** + * Returns a translated attribute value. + * + * The base value must come from 'attributes' on the model otherwise the process + * can possibly loop back to this event, then method triggered by __get() magic. + * + * @param string $key + * @param string $locale + * @return string + */ + public function getAttributeTranslated($key, $locale = null) + { + if ($locale == null) { + $locale = $this->translatableContext; + } + + /* + * Result should not return NULL to successfully hook beforeGetAttribute event + */ + $result = ''; + + /* + * Default locale + */ + if ($locale == $this->translatableDefault) { + $result = $this->getAttributeFromData($this->model->attributes, $key); + } + /* + * Other locale + */ + else { + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + if ($this->hasTranslation($key, $locale)) { + $result = $this->getAttributeFromData($this->translatableAttributes[$locale], $key); + } + elseif ($this->translatableUseFallback) { + $result = $this->getAttributeFromData($this->model->attributes, $key); + } + } + + /* + * Handle jsonable attributes, default locale may return the value as a string + */ + if ( + is_string($result) && + method_exists($this->model, 'isJsonable') && + $this->model->isJsonable($key) + ) { + $result = json_decode($result, true); + } + + return $result; + } + + /** + * Returns all translated attribute values. + * @param string $locale + * @return array + */ + public function getTranslateAttributes($locale) + { + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + return array_get($this->translatableAttributes, $locale, []); + } + + /** + * Returns whether the attribute is translatable (has a translation) for the given locale. + * @param string $key + * @param string $locale + * @return bool + */ + public function hasTranslation($key, $locale) + { + /* + * If the default locale is passed, the attributes are retreived from the model, + * otherwise fetch the attributes from the $translatableAttributes property + */ + if ($locale == $this->translatableDefault) { + $translatableAttributes = $this->model->attributes; + } + else { + /* + * Ensure that the translatableData has been loaded + * @see https://github.com/rainlab/translate-plugin/issues/302 + */ + if (!isset($this->translatableAttributes[$locale])) { + $this->loadTranslatableData($locale); + } + + $translatableAttributes = $this->translatableAttributes[$locale]; + } + + return !!$this->getAttributeFromData($translatableAttributes, $key); + } + + /** + * Sets a translated attribute value. + * @param string $key Attribute + * @param string $value Value to translate + * @return string Translated value + */ + public function setAttributeTranslated($key, $value, $locale = null) + { + if ($locale == null) { + $locale = $this->translatableContext; + } + + if ($locale == $this->translatableDefault) { + return $this->setAttributeFromData($this->model->attributes, $key, $value); + } + + if (!array_key_exists($locale, $this->translatableAttributes)) { + $this->loadTranslatableData($locale); + } + + return $this->setAttributeFromData($this->translatableAttributes[$locale], $key, $value); + } + + /** + * Restores the default language values on the model and + * stores the translated values in the attributes table. + * @return void + */ + public function syncTranslatableAttributes() + { + /* + * Spin through the known locales, store the translations if necessary + */ + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if (!$this->isTranslateDirty(null, $locale)) { + continue; + } + + $this->storeTranslatableData($locale); + } + + /* + * Saving the default locale, no need to restore anything + */ + if ($this->translatableContext == $this->translatableDefault) { + return; + } + + /* + * Restore translatable values to models originals + */ + $original = $this->model->getOriginal(); + $attributes = $this->model->getAttributes(); + $translatable = $this->model->getTranslatableAttributes(); + $originalValues = array_intersect_key($original, array_flip($translatable)); + $this->model->attributes = array_merge($attributes, $originalValues); + } + + /** + * Changes the active language for this model + * @param string $context + * @return void + */ + public function translateContext($context = null) + { + if ($context === null) { + return $this->translatableContext; + } + + $this->translatableContext = $context; + } + + /** + * Shorthand for translateContext method, and chainable. + * @param string $context + * @return self + */ + public function lang($context = null) + { + $this->translateContext($context); + + return $this->model; + } + + /** + * Checks if this model has transatable attributes. + * @return true + */ + public function hasTranslatableAttributes() + { + return is_array($this->model->translatable) && + count($this->model->translatable) > 0; + } + + /** + * Returns a collection of fields that will be hashed. + * @return array + */ + public function getTranslatableAttributes() + { + $translatable = []; + + if (!is_array($this->model->translatable)) { + return []; + } + + foreach ($this->model->translatable as $attribute) { + $translatable[] = is_array($attribute) ? array_shift($attribute) : $attribute; + } + + return $translatable; + } + + /** + * Returns the defined options for a translatable attribute. + * @return array + */ + public function getTranslatableAttributesWithOptions() + { + $attributes = []; + + foreach ($this->model->translatable as $options) { + if (!is_array($options)) { + continue; + } + + $attributeName = array_shift($options); + + $attributes[$attributeName] = $options; + } + + return $attributes; + } + + /** + * Determine if the model or a given translated attribute has been modified. + * @param string|null $attribute + * @return bool + */ + public function isTranslateDirty($attribute = null, $locale = null) + { + $dirty = $this->getTranslateDirty($locale); + + if (is_null($attribute)) { + return count($dirty) > 0; + } + else { + return array_key_exists($attribute, $dirty); + } + } + + /** + * Get the locales that have changed, if any + * + * @return array + */ + public function getDirtyLocales() + { + $dirtyLocales = []; + $knownLocales = array_keys($this->translatableAttributes); + foreach ($knownLocales as $locale) { + if ($this->isTranslateDirty(null, $locale)) { + $dirtyLocales[] = $locale; + } + } + + return $dirtyLocales; + } + + /** + * Get the original values of the translated attributes. + * @param string|null $locale If `null`, the method will get the original data for all locales. + * @return array|null Returns locale data as an array, or `null` if an invalid locale is specified. + */ + public function getTranslatableOriginals($locale = null) + { + if (!$locale) { + return $this->translatableOriginals; + } else { + return $this->translatableOriginals[$locale] ?? null; + } + } + + /** + * Get the translated attributes that have been changed since last sync. + * @return array + */ + public function getTranslateDirty($locale = null) + { + if (!$locale) { + $locale = $this->translatableContext; + } + + if (!array_key_exists($locale, $this->translatableAttributes)) { + return []; + } + + if (!array_key_exists($locale, $this->translatableOriginals)) { + return $this->translatableAttributes[$locale]; // All dirty + } + + $dirty = []; + + foreach ($this->translatableAttributes[$locale] as $key => $value) { + + if (!array_key_exists($key, $this->translatableOriginals[$locale])) { + $dirty[$key] = $value; + } + elseif ($value != $this->translatableOriginals[$locale][$key]) { + $dirty[$key] = $value; + } + } + + return $dirty; + } + + /** + * Extracts a attribute from a model/array with nesting support. + * @param mixed $data + * @param string $attribute + * @return mixed + */ + protected function getAttributeFromData($data, $attribute) + { + $keyArray = HtmlHelper::nameToArray($attribute); + + return array_get($data, implode('.', $keyArray)); + } + + /** + * Sets an attribute from a model/array with nesting support. + * @param mixed $data + * @param string $attribute + * @return mixed + */ + protected function setAttributeFromData(&$data, $attribute, $value) + { + $keyArray = HtmlHelper::nameToArray($attribute); + + array_set($data, implode('.', $keyArray), $value); + + return $value; + } + + /** + * Saves the translation data for the model. + * @param string $locale + * @return void + */ + abstract protected function storeTranslatableData($locale = null); + + /** + * Loads the translation data from the model. + * @param string $locale + * @return array + */ + abstract protected function loadTranslatableData($locale = null); +} diff --git a/plugins/rainlab/translate/classes/Translator.php b/plugins/rainlab/translate/classes/Translator.php new file mode 100644 index 0000000..de9dd70 --- /dev/null +++ b/plugins/rainlab/translate/classes/Translator.php @@ -0,0 +1,259 @@ +defaultLocale = $this->isConfigured() ? array_get(Locale::getDefault(), 'code', 'en') : 'en'; + $this->activeLocale = $this->defaultLocale; + } + + /** + * Changes the locale in the application and optionally stores it in the session. + * @param string $locale Locale to use + * @param boolean $remember Set to false to not store in the session. + * @return boolean Returns true if the locale exists and is set. + */ + public function setLocale($locale, $remember = true) + { + if (!Locale::isValid($locale)) { + return false; + } + + App::setLocale($locale); + + $this->activeLocale = $locale; + + if ($remember) { + $this->setSessionLocale($locale); + } + + return true; + } + + /** + * Returns the active locale set by this instance. + * @param boolean $fromSession Look in the session. + * @return string + */ + public function getLocale($fromSession = false) + { + if ($fromSession && ($locale = $this->getSessionLocale())) { + return $locale; + } + + return $this->activeLocale; + } + + /** + * Returns the default locale as set by the application. + * @return string + */ + public function getDefaultLocale() + { + return $this->defaultLocale; + } + + /** + * Check if this plugin is installed and the database is available, + * stores the result in the session for efficiency. + * @return boolean + */ + public function isConfigured() + { + if ($this->isConfigured !== null) { + return $this->isConfigured; + } + + if (Session::has(self::SESSION_CONFIGURED)) { + $result = true; + } + elseif (App::hasDatabase() && Schema::hasTable('rainlab_translate_locales')) { + Session::put(self::SESSION_CONFIGURED, true); + $result = true; + } + else { + $result = false; + } + + return $this->isConfigured = $result; + } + + // + // Request handling + // + + /** + * handleLocaleRoute will check if the route contains a translated locale prefix (/en/) + * and return that locale to be registered with the router. + * @return string + */ + public function handleLocaleRoute() + { + if (Config::get('rainlab.translate::disableLocalePrefixRoutes', false)) { + return ''; + } + + if (App::runningInBackend()) { + return ''; + } + + if (!$this->isConfigured()) { + return ''; + } + + if (!$this->loadLocaleFromRequest()) { + return ''; + } + + $locale = $this->getLocale(); + if (!$locale) { + return ''; + } + + return $locale; + } + + /** + * Sets the locale based on the first URI segment. + * @return bool + */ + public function loadLocaleFromRequest() + { + $locale = Request::segment(1); + + if (!Locale::isValid($locale)) { + return false; + } + + $this->setLocale($locale); + return true; + } + + /** + * Returns the current path prefixed with language code. + * + * @param string $locale optional language code, default to the system default language + * @return string + */ + public function getCurrentPathInLocale($locale = null) + { + return $this->getPathInLocale(Request::path(), $locale); + } + + /** + * Returns the path prefixed with language code. + * + * @param string $path Path to rewrite, already translate, with or without locale prefixed + * @param string $locale optional language code, default to the system default language + * @param boolean $prefixDefaultLocale should we prefix the path when the locale = default locale + * @return string + */ + public function getPathInLocale($path, $locale = null, $prefixDefaultLocale = null) + { + $prefixDefaultLocale = (is_null($prefixDefaultLocale)) + ? Config::get('rainlab.translate::prefixDefaultLocale') + : $prefixDefaultLocale; + + $segments = explode('/', $path); + + $segments = array_values(array_filter($segments, function ($v) { + return $v != ''; + })); + + if (is_null($locale) || !Locale::isValid($locale)) { + $locale = $this->defaultLocale; + } + + if (count($segments) == 0 || Locale::isValid($segments[0])) { + $segments[0] = $locale; + } else { + array_unshift($segments, $locale); + } + + // If we don't want te default locale to be prefixed + // and the first segment equals the default locale + if ( + !$prefixDefaultLocale && + isset($segments[0]) && + $segments[0] == $this->defaultLocale + ) { + // Remove the default locale + array_shift($segments); + }; + + return htmlspecialchars(implode('/', $segments), ENT_QUOTES, 'UTF-8'); + } + + // + // Session handling + // + + /** + * Looks at the session storage to find a locale. + * @return bool + */ + public function loadLocaleFromSession() + { + $locale = $this->getSessionLocale(); + + if (!$locale) { + return false; + } + + $this->setLocale($locale); + return true; + } + + protected function getSessionLocale() + { + if (!Session::has(self::SESSION_LOCALE)) { + return null; + } + + return Session::get(self::SESSION_LOCALE); + } + + protected function setSessionLocale($locale) + { + Session::put(self::SESSION_LOCALE, $locale); + } +} diff --git a/plugins/rainlab/translate/components/AlternateHrefLangElements.php b/plugins/rainlab/translate/components/AlternateHrefLangElements.php new file mode 100644 index 0000000..687fd49 --- /dev/null +++ b/plugins/rainlab/translate/components/AlternateHrefLangElements.php @@ -0,0 +1,82 @@ + 'rainlab.translate::lang.alternate_hreflang.component_name', + 'description' => 'rainlab.translate::lang.alternate_hreflang.component_description' + ]; + } + + /** + * locales + */ + public function locales() + { + // Available locales + $locales = collect(LocaleModel::listEnabled()); + + // Transform it to contain the new urls + $locales->transform(function ($item, $key) { + return $this->retrieveLocalizedUrl($key); + }); + + return $locales->toArray(); + } + + /** + * retrieveLocalizedUrl + */ + protected function retrieveLocalizedUrl($locale) + { + $translator = Translator::instance(); + $page = $this->getPage(); + + /* + * Static Page + */ + if (isset($page->apiBag['staticPage'])) { + $staticPage = $page->apiBag['staticPage']; + $staticPage->rewriteTranslatablePageUrl($locale); + $localeUrl = array_get($staticPage->attributes, 'viewBag.url'); + } + /* + * CMS Page + */ + else { + $page->rewriteTranslatablePageUrl($locale); + $params = $this->getRouter()->getParameters(); + + $translatedParams = Event::fire('translate.localePicker.translateParams', [ + $page, + $params, + $this->oldLocale, + $locale + ], true); + + if ($translatedParams) { + $params = $translatedParams; + } + + $router = new RainRouter; + $localeUrl = $router->urlFromPattern($page->url, $params); + } + + return $translator->getPathInLocale($localeUrl, $locale); + } + +} diff --git a/plugins/rainlab/translate/components/LocalePicker.php b/plugins/rainlab/translate/components/LocalePicker.php new file mode 100644 index 0000000..c93b55b --- /dev/null +++ b/plugins/rainlab/translate/components/LocalePicker.php @@ -0,0 +1,226 @@ + 'rainlab.translate::lang.locale_picker.component_name', + 'description' => 'rainlab.translate::lang.locale_picker.component_description', + ]; + } + + public function defineProperties() + { + return [ + 'forceUrl' => [ + 'title' => 'Force URL schema', + 'description' => 'Always prefix the URL with a language code.', + 'default' => 0, + 'type' => 'checkbox' + ], + ]; + } + + public function init() + { + $this->translator = Translator::instance(); + } + + public function onRun() + { + if ($redirect = $this->redirectForceUrl()) { + return $redirect; + } + + $this->page['locales'] = $this->locales = LocaleModel::listEnabled(); + $this->page['activeLocale'] = $this->activeLocale = $this->translator->getLocale(); + $this->page['activeLocaleName'] = $this->activeLocaleName = array_get($this->locales, $this->activeLocale); + } + + public function onSwitchLocale() + { + if (!$locale = post('locale')) { + return; + } + + // Remember the current locale before switching to the requested one + $this->oldLocale = $this->translator->getLocale(); + + $this->translator->setLocale($locale); + + $pageUrl = $this->withPreservedQueryString($this->makeLocaleUrlFromPage($locale), $locale); + if ($this->property('forceUrl')) { + return Redirect::to($this->translator->getPathInLocale($pageUrl, $locale)); + } + + return Redirect::to($pageUrl); + } + + protected function redirectForceUrl() + { + if ( + Request::ajax() || + !$this->property('forceUrl') || + $this->translator->loadLocaleFromRequest() + ) { + return; + } + + $prefixDefaultLocale = Config::get('rainlab.translate::prefixDefaultLocale'); + $locale = $this->translator->getLocale(false) + ?: $this->translator->getDefaultLocale(); + + if ($prefixDefaultLocale) { + return Redirect::to( + $this->withPreservedQueryString( + $this->translator->getCurrentPathInLocale($locale), + $locale + ) + ); + } elseif ( $locale == $this->translator->getDefaultLocale()) { + return; + } else { + $this->translator->setLocale($this->translator->getDefaultLocale()); + return; + } + + } + + /** + * Returns the URL from a page object, including current parameter values. + * @return string + */ + protected function makeLocaleUrlFromPage($locale) + { + $page = $this->getPage(); + + /* + * Static Page + */ + if (isset($page->apiBag['staticPage'])) { + $staticPage = $page->apiBag['staticPage']; + + $staticPage->rewriteTranslatablePageUrl($locale); + + $localeUrl = array_get($staticPage->attributes, 'viewBag.url'); + } + /* + * CMS Page + */ + else { + $page->rewriteTranslatablePageUrl($locale); + + $router = new RainRouter; + + $params = $this->getRouter()->getParameters(); + + /** + * @event translate.localePicker.translateParams + * Enables manipulating the URL parameters + * + * You will have access to the page object, the old and new locale and the URL parameters. + * + * Example usage: + * + * Event::listen('translate.localePicker.translateParams', function($page, $params, $oldLocale, $newLocale) { + * if ($page->baseFileName == 'your-page-filename') { + * return YourModel::translateParams($params, $oldLocale, $newLocale); + * } + * }); + * + */ + $translatedParams = Event::fire('translate.localePicker.translateParams', [ + $page, + $params, + $this->oldLocale, + $locale + ], true); + + if ($translatedParams) { + $params = $translatedParams; + } + + $localeUrl = $router->urlFromPattern($page->url, $params); + } + + return $localeUrl; + } + + /** + * Makes sure to add any existing query string to the redirect url. + * + * @param $pageUrl + * @param $locale + * + * @return string + */ + protected function withPreservedQueryString($pageUrl, $locale) + { + $page = $this->getPage(); + $query = request()->query(); + + /** + * @event translate.localePicker.translateQuery + * Enables manipulating the URL query parameters + * + * You will have access to the page object, the old and new locale and the URL query parameters. + * + * Example usage: + * + * Event::listen('translate.localePicker.translateQuery', function($page, $params, $oldLocale, $newLocale) { + * if ($page->baseFileName == 'your-page-filename') { + * return YourModel::translateParams($params, $oldLocale, $newLocale); + * } + * }); + * + */ + $translatedQuery = Event::fire('translate.localePicker.translateQuery', [ + $page, + $query, + $this->oldLocale, + $locale + ], true); + + $query = http_build_query($translatedQuery ?: $query); + + return $query ? $pageUrl . '?' . $query : $pageUrl; + } +} diff --git a/plugins/rainlab/translate/components/alternatehreflangelements/default.htm b/plugins/rainlab/translate/components/alternatehreflangelements/default.htm new file mode 100644 index 0000000..e7b5fd8 --- /dev/null +++ b/plugins/rainlab/translate/components/alternatehreflangelements/default.htm @@ -0,0 +1,3 @@ +{% for locale, alternateUrl in __SELF__.locales %} + +{% endfor %} diff --git a/plugins/rainlab/translate/components/localepicker/default.htm b/plugins/rainlab/translate/components/localepicker/default.htm new file mode 100644 index 0000000..90c3444 --- /dev/null +++ b/plugins/rainlab/translate/components/localepicker/default.htm @@ -0,0 +1,7 @@ +{{ form_open() }} + +{{ form_close() }} \ No newline at end of file diff --git a/plugins/rainlab/translate/composer.json b/plugins/rainlab/translate/composer.json new file mode 100644 index 0000000..af34178 --- /dev/null +++ b/plugins/rainlab/translate/composer.json @@ -0,0 +1,25 @@ +{ + "name": "rainlab/translate-plugin", + "type": "october-plugin", + "description": "Translate plugin for October CMS", + "homepage": "https://octobercms.com/plugin/rainlab-translate", + "keywords": ["october", "octobercms", "translate"], + "license": "MIT", + "authors": [ + { + "name": "Alexey Bobkov", + "email": "aleksey.bobkov@gmail.com", + "role": "Co-founder" + }, + { + "name": "Samuel Georges", + "email": "daftspunky@gmail.com", + "role": "Co-founder" + } + ], + "require": { + "php": ">=5.5.9", + "composer/installers": "~1.0" + }, + "minimum-stability": "dev" +} diff --git a/plugins/rainlab/translate/config/config.php b/plugins/rainlab/translate/config/config.php new file mode 100644 index 0000000..d9c60fc --- /dev/null +++ b/plugins/rainlab/translate/config/config.php @@ -0,0 +1,49 @@ + env('TRANSLATE_FORCE_LOCALE', null), + + /* + |-------------------------------------------------------------------------- + | Prefix the Default Locale + |-------------------------------------------------------------------------- + | + | Specifies if the default locale be prefixed by the plugin. + | + */ + 'prefixDefaultLocale' => env('TRANSLATE_PREFIX_LOCALE', true), + + /* + |-------------------------------------------------------------------------- + | Cache Timeout in Minutes + |-------------------------------------------------------------------------- + | + | By default all translations are cached for 24 hours (1440 min). + | This setting allows to change that period with given amount of minutes. + | + | For example, 43200 for 30 days or 525600 for one year. + | + */ + 'cacheTimeout' => env('TRANSLATE_CACHE_TIMEOUT', 1440), + + /* + |-------------------------------------------------------------------------- + | Disable Locale Prefix Routes + |-------------------------------------------------------------------------- + | + | Disables the automatically generated locale prefixed routes + | (i.e. /en/original-route) when enabled. + | + */ + 'disableLocalePrefixRoutes' => env('TRANSLATE_DISABLE_PREFIX_ROUTES', false), + +]; diff --git a/plugins/rainlab/translate/console/ScanCommand.php b/plugins/rainlab/translate/console/ScanCommand.php new file mode 100644 index 0000000..4d38604 --- /dev/null +++ b/plugins/rainlab/translate/console/ScanCommand.php @@ -0,0 +1,40 @@ +option('purge')) { + $this->output->writeln('Purging messages...'); + Message::truncate(); + } + + ThemeScanner::scan(); + $this->output->success('Messages scanned successfully.'); + $this->output->note('You may need to run cache:clear for updated messages to take effect.'); + } + + protected function getArguments() + { + return []; + } + + protected function getOptions() + { + return [ + ['purge', 'null', InputOption::VALUE_NONE, 'First purge existing messages.', null], + ]; + } +} diff --git a/plugins/rainlab/translate/controllers/Locales.php b/plugins/rainlab/translate/controllers/Locales.php new file mode 100644 index 0000000..7f3be5d --- /dev/null +++ b/plugins/rainlab/translate/controllers/Locales.php @@ -0,0 +1,92 @@ +addJs('/plugins/rainlab/translate/assets/js/locales.js'); + } + + /** + * {@inheritDoc} + */ + public function listInjectRowClass($record, $definition = null) + { + if (!$record->is_enabled) { + return 'safe disabled'; + } + } + + public function onCreateForm() + { + $this->asExtension('FormController')->create(); + + return $this->makePartial('create_form'); + } + + public function onCreate() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->create_onSave(); + + return $this->listRefresh(); + } + + public function onUpdateForm() + { + $this->asExtension('FormController')->update(post('record_id')); + $this->vars['recordId'] = post('record_id'); + + return $this->makePartial('update_form'); + } + + public function onUpdate() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->update_onSave(post('record_id')); + + return $this->listRefresh(); + } + + public function onDelete() + { + LocaleModel::clearCache(); + $this->asExtension('FormController')->update_onDelete(post('record_id')); + + return $this->listRefresh(); + } + + public function onReorder() + { + LocaleModel::clearCache(); + $this->asExtension('ReorderController')->onReorder(); + } +} diff --git a/plugins/rainlab/translate/controllers/Messages.php b/plugins/rainlab/translate/controllers/Messages.php new file mode 100644 index 0000000..4f84286 --- /dev/null +++ b/plugins/rainlab/translate/controllers/Messages.php @@ -0,0 +1,237 @@ +addJs('/plugins/rainlab/translate/assets/js/messages.js'); + $this->addCss('/plugins/rainlab/translate/assets/css/messages.css'); + + $this->importColumns = MessageExport::getColumns(); + $this->exportColumns = MessageExport::getColumns(); + } + + public function index() + { + $this->pageTitle = 'rainlab.translate::lang.messages.title'; + $this->prepareTable(); + } + + public function onRefresh() + { + $this->prepareTable(); + return ['#messagesContainer' => $this->makePartial('messages')]; + } + + public function onClearCache() + { + CacheHelper::clear(); + + Flash::success(Lang::get('rainlab.translate::lang.messages.clear_cache_success')); + } + + public function onLoadScanMessagesForm() + { + return $this->makePartial('scan_messages_form'); + } + + public function onScanMessages() + { + if (post('purge_messages', false)) { + Message::truncate(); + } + + ThemeScanner::scan(); + + if (post('purge_deleted_messages', false)) { + Message::where('found', 0)->delete(); + } + + Flash::success(Lang::get('rainlab.translate::lang.messages.scan_messages_success')); + + return $this->onRefresh(); + } + + public function prepareTable() + { + $fromCode = post('locale_from', null); + $toCode = post('locale_to', Locale::getDefault()->code); + $this->hideTranslated = post('hide_translated', false); + + /* + * Page vars + */ + $this->vars['hideTranslated'] = $this->hideTranslated; + $this->vars['defaultLocale'] = Locale::getDefault(); + $this->vars['locales'] = Locale::all(); + $this->vars['selectedFrom'] = $selectedFrom = Locale::findByCode($fromCode); + $this->vars['selectedTo'] = $selectedTo = Locale::findByCode($toCode); + + /* + * Make table config, make default column read only + */ + $config = $this->makeConfig('config_table.yaml'); + + if (!$selectedFrom) { + $config->columns['from']['readOnly'] = true; + } + if (!$selectedTo) { + $config->columns['to']['readOnly'] = true; + } + + /* + * Make table widget + */ + $widget = $this->makeWidget(\Backend\Widgets\Table::class, $config); + $widget->bindToController(); + + /* + * Populate data + */ + $dataSource = $widget->getDataSource(); + + $dataSource->bindEvent('data.getRecords', function($offset, $count) use ($selectedFrom, $selectedTo) { + $messages = $this->listMessagesForDatasource([ + 'offset' => $offset, + 'count' => $count + ]); + + return $this->processTableData($messages, $selectedFrom, $selectedTo); + }); + + $dataSource->bindEvent('data.searchRecords', function($search, $offset, $count) use ($selectedFrom, $selectedTo) { + $messages = $this->listMessagesForDatasource([ + 'search' => $search, + 'offset' => $offset, + 'count' => $count + ]); + + return $this->processTableData($messages, $selectedFrom, $selectedTo); + }); + + $dataSource->bindEvent('data.getCount', function() { + return Message::count(); + }); + + $dataSource->bindEvent('data.updateRecord', function($key, $data) { + $message = Message::find($key); + $this->updateTableData($message, $data); + CacheHelper::clear(); + }); + + $dataSource->bindEvent('data.deleteRecord', function($key) { + if ($message = Message::find($key)) { + $message->delete(); + } + }); + + $this->vars['table'] = $widget; + } + + protected function isHideTranslated() + { + return post('hide_translated', false); + } + + protected function listMessagesForDatasource($options = []) + { + extract(array_merge([ + 'search' => null, + 'offset' => null, + 'count' => null, + ], $options)); + + $query = Message::orderBy('message_data','asc'); + + if ($search) { + $query = $query->searchWhere($search, ['message_data']); + } + + if ($count) { + $query = $query->limit($count)->offset($offset); + } + + return $query->get(); + } + + protected function processTableData($messages, $from, $to) + { + $fromCode = $from ? $from->code : null; + $toCode = $to ? $to->code : null; + + $data = []; + foreach ($messages as $message) { + $toContent = $message->forLocale($toCode); + if ($this->hideTranslated && $toContent) { + continue; + } + + $data[] = [ + 'id' => $message->id, + 'code' => $message->code, + 'from' => $message->forLocale($fromCode), + 'to' => $toContent, + 'found' => $message->found ? '' : Lang::get('rainlab.translate::lang.messages.not_found'), + ]; + } + + return $data; + } + + protected function updateTableData($message, $data) + { + if (!$message) { + return; + } + + $fromCode = post('locale_from'); + $toCode = post('locale_to'); + + // @todo This should be unified to a single save() + if ($fromCode) { + $fromValue = array_get($data, 'from'); + if ($fromValue != $message->forLocale($fromCode)) { + $message->toLocale($fromCode, $fromValue); + } + } + + if ($toCode) { + $toValue = array_get($data, 'to'); + if ($toValue != $message->forLocale($toCode)) { + $message->toLocale($toCode, $toValue); + } + } + } +} diff --git a/plugins/rainlab/translate/controllers/locales/_create_form.htm b/plugins/rainlab/translate/controllers/locales/_create_form.htm new file mode 100644 index 0000000..e33ba27 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_create_form.htm @@ -0,0 +1,55 @@ + 'createForm']) ?> + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/locales/_hint.htm b/plugins/rainlab/translate/controllers/locales/_hint.htm new file mode 100644 index 0000000..45d85a4 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_hint.htm @@ -0,0 +1,4 @@ + +

    + +

    \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm b/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm new file mode 100644 index 0000000..126253b --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_list_toolbar.htm @@ -0,0 +1,15 @@ +
    diff --git a/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm b/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm new file mode 100644 index 0000000..30a2fd6 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_reorder_toolbar.htm @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/_update_form.htm b/plugins/rainlab/translate/controllers/locales/_update_form.htm new file mode 100644 index 0000000..c820921 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/_update_form.htm @@ -0,0 +1,67 @@ + 'updateForm']) ?> + + + + + + fatalError): ?> + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/locales/config_form.yaml b/plugins/rainlab/translate/controllers/locales/config_form.yaml new file mode 100644 index 0000000..781bcfb --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_form.yaml @@ -0,0 +1,7 @@ +# =================================== +# Form Behavior Config +# =================================== + +form: ~/plugins/rainlab/translate/models/locale/fields.yaml +modelClass: RainLab\Translate\Models\Locale +defaultRedirect: rainlab/translate/locales diff --git a/plugins/rainlab/translate/controllers/locales/config_list.yaml b/plugins/rainlab/translate/controllers/locales/config_list.yaml new file mode 100644 index 0000000..6447ec8 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_list.yaml @@ -0,0 +1,52 @@ +# =================================== +# List Behavior Config +# =================================== + +# Model List Column configuration +list: ~/plugins/rainlab/translate/models/locale/columns.yaml + +# Model Class name +modelClass: RainLab\Translate\Models\Locale + +# List Title +title: rainlab.translate::lang.locale.title + +# Link URL for each record +# recordUrl: rainlab/translate/locale/update/:id + +recordOnClick: $.translateLocales.clickRecord(:id) + +# Message to display if the list is empty +noRecordsMessage: backend::lang.list.no_records + +# Records to display per page +recordsPerPage: 20 + +# Displays the list column set up button +showSetup: true + +# Displays the sorting link on each column +showSorting: true + +# Default sorting column +defaultSort: + column: sort_order + direction: asc + +# Display checkboxes next to each record +# showCheckboxes: true + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: list_toolbar + + # Search widget configuration + search: + prompt: backend::lang.list.search_prompt + +# Reordering +structure: + showTree: false + showReorder: true + maxDepth: 1 diff --git a/plugins/rainlab/translate/controllers/locales/config_reorder.yaml b/plugins/rainlab/translate/controllers/locales/config_reorder.yaml new file mode 100644 index 0000000..797a21f --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/config_reorder.yaml @@ -0,0 +1,17 @@ +# =================================== +# Reorder Behavior Config +# =================================== + +# Reorder Title +title: rainlab.translate::lang.locale.reorder_title + +# Attribute name +nameFrom: name + +# Model Class name +modelClass: RainLab\Translate\Models\Locale + +# Toolbar widget configuration +toolbar: + # Partial for toolbar buttons + buttons: reorder_toolbar \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/locales/index.htm b/plugins/rainlab/translate/controllers/locales/index.htm new file mode 100644 index 0000000..5bdfd58 --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/index.htm @@ -0,0 +1,12 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +
    + makeHintPartial('translation_locales_hint', 'hint') ?> +
    + +listRender() ?> diff --git a/plugins/rainlab/translate/controllers/locales/reorder.htm b/plugins/rainlab/translate/controllers/locales/reorder.htm new file mode 100644 index 0000000..f820c6a --- /dev/null +++ b/plugins/rainlab/translate/controllers/locales/reorder.htm @@ -0,0 +1,9 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + +reorderRender() ?> diff --git a/plugins/rainlab/translate/controllers/messages/_hint.htm b/plugins/rainlab/translate/controllers/messages/_hint.htm new file mode 100644 index 0000000..18a041b --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_hint.htm @@ -0,0 +1,6 @@ + +

    + + + +

    \ No newline at end of file diff --git a/plugins/rainlab/translate/controllers/messages/_messages.htm b/plugins/rainlab/translate/controllers/messages/_messages.htm new file mode 100644 index 0000000..87ed504 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_messages.htm @@ -0,0 +1,10 @@ +
    +
    + makePartial('table_headers') ?> +
    +
    + makePartial('table_toolbar') ?> +
    + + render() ?> +
    diff --git a/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm b/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm new file mode 100644 index 0000000..101f82a --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_scan_messages_form.htm @@ -0,0 +1,80 @@ +
    + 'scanMessagesForm']) ?> + + + + + + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/_table_headers.htm b/plugins/rainlab/translate/controllers/messages/_table_headers.htm new file mode 100644 index 0000000..8a5c03c --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_table_headers.htm @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + diff --git a/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm b/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm new file mode 100644 index 0000000..eb24d78 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/_table_toolbar.htm @@ -0,0 +1,24 @@ + diff --git a/plugins/rainlab/translate/controllers/messages/config_import_export.yaml b/plugins/rainlab/translate/controllers/messages/config_import_export.yaml new file mode 100644 index 0000000..3aaf39f --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/config_import_export.yaml @@ -0,0 +1,9 @@ +import: + title: 'rainlab.translate::lang.messages.import_messages_link' + modelClass: RainLab\Translate\Models\MessageImport + redirect: rainlab/translate/messages + +export: + title: 'rainlab.translate::lang.messages.export_messages_link' + modelClass: RainLab\Translate\Models\MessageExport + redirect: rainlab/translate/messages diff --git a/plugins/rainlab/translate/controllers/messages/config_table.yaml b/plugins/rainlab/translate/controllers/messages/config_table.yaml new file mode 100644 index 0000000..331f33e --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/config_table.yaml @@ -0,0 +1,17 @@ +# =================================== +# Grid Widget Configuration +# =================================== + +dataSource: server +keyFrom: id +recordsPerPage: 500 +adding: false +searching: true + +columns: + from: + title: From + to: + title: To + found: + title: Scan errors diff --git a/plugins/rainlab/translate/controllers/messages/export.htm b/plugins/rainlab/translate/controllers/messages/export.htm new file mode 100644 index 0000000..589b191 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/export.htm @@ -0,0 +1,26 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + exportRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/import.htm b/plugins/rainlab/translate/controllers/messages/import.htm new file mode 100644 index 0000000..9538215 --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/import.htm @@ -0,0 +1,26 @@ + +
      +
    • +
    • +
    • pageTitle)) ?>
    • +
    + + + 'layout']) ?> + +
    + importRender() ?> +
    + +
    + +
    + + diff --git a/plugins/rainlab/translate/controllers/messages/index.htm b/plugins/rainlab/translate/controllers/messages/index.htm new file mode 100644 index 0000000..3f3777a --- /dev/null +++ b/plugins/rainlab/translate/controllers/messages/index.htm @@ -0,0 +1,34 @@ + +
      +
    • +
    • pageTitle)) ?>
    • +
    + + +
    + makeHintPartial('translation_messages_hint', 'hint') ?> +
    + + 'messagesForm', 'class'=>'layout-item stretch layout-column', 'onsubmit'=>'return false']) ?> + +
    + makePartial('messages') ?> +
    + + + + + + + + + + diff --git a/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php b/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php new file mode 100644 index 0000000..ec74673 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMarkdownEditor.php @@ -0,0 +1,107 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['markdowneditor'] = $parentContent; + return $this->makePartial('mlmarkdowneditor'); + } + + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmarkdowneditor.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/markdowneditor/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/markdowneditor/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLMediaFinder.php b/plugins/rainlab/translate/formwidgets/MLMediaFinder.php new file mode 100644 index 0000000..5e21269 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMediaFinder.php @@ -0,0 +1,104 @@ +initLocale(); + } + + /** + * @inheritDoc + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['mediafinder'] = $parentContent; + return $this->makePartial('mlmediafinder'); + } + + /** + * Prepares the form widget view data + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + // make root path of media files accessible + $this->vars['mediaPath'] = $this->mediaPath = MediaLibrary::url('/'); + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * @inheritDoc + */ + public function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmediafinder.js'); + $this->addCss('css/mlmediafinder.css'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/mediafinder/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/mediafinder/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php b/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php new file mode 100644 index 0000000..250463b --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLMediaFinderv2.php @@ -0,0 +1,108 @@ +initLocale(); + } + + /** + * @inheritDoc + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['mediafinder'] = $parentContent; + return $this->makePartial('mlmediafinder'); + } + + /** + * prepareVars prepares the form widget view data + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + // make root path of media files accessible + $this->vars['mediaPath'] = $this->mediaPath = MediaLibrary::url('/'); + } + + /** + * @inheritDoc + */ + public function getSaveValue($value) + { + if ($this->isAvailable) { + return $this->getLocaleSaveValue($value); + } + + return parent::getSaveValue($value); + } + + /** + * @inheritDoc + */ + public function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlmediafinder.js'); + $this->addCss('../../mlmediafinder/assets/css/mlmediafinder.css'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/media/formwidgets/mediafinder/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/media/formwidgets/mediafinder/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLNestedForm.php b/plugins/rainlab/translate/formwidgets/MLNestedForm.php new file mode 100644 index 0000000..f04896b --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLNestedForm.php @@ -0,0 +1,187 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['nestedform'] = $parentContent; + + return $this->makePartial('mlnestedform'); + } + + /** + * prepareVars for viewing + */ + public function prepareVars() + { + parent::prepareVars(); + + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + $this->rewritePostValues(); + + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlnestedform.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/nestedform/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/nestedform/assets'; + } + + /** + * onSwitchItemLocale handler + */ + public function onSwitchItemLocale() + { + if (!$locale = post('_nestedform_locale')) { + throw new ApplicationException('Unable to find a nested form locale for: '.$locale); + } + + // Store previous value + $previousLocale = post('_nestedform_previous_locale'); + $previousValue = $this->getPrimarySaveDataAsArray(); + + // Update widget to show form for switched locale + $lockerData = $this->getLocaleSaveDataAsArray($locale) ?: []; + $this->formWidget->setFormValues($lockerData); + + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + return [ + '#'.$this->getId('mlNestedForm') => $parentContent, + 'updateValue' => json_encode($previousValue), + 'updateLocale' => $previousLocale, + ]; + } + + /** + * getPrimarySaveDataAsArray gets the active values from the selected locale. + */ + protected function getPrimarySaveDataAsArray(): array + { + return post($this->formField->getName()) ?: []; + } + + /** + * getLocaleSaveDataAsArray returns the stored locale data as an array. + */ + protected function getLocaleSaveDataAsArray($locale): ?array + { + $saveData = array_get($this->getLocaleSaveData(), $locale, []); + + if (!is_array($saveData)) { + $saveData = json_decode($saveData, true); + } + + return $saveData; + } + + /** + * rewritePostValues since the locker does always contain the latest values, + * this method will take the save data from the nested form and merge it in to + * the locker based on which ever locale is selected using an item map + */ + protected function rewritePostValues() + { + // Get the selected locale at postback + $data = post('RLTranslateNestedFormLocale'); + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $locale = array_get($data, $fieldName); + + if (!$locale) { + return; + } + + // Splice the save data in to the locker data for selected locale + $data = $this->getPrimarySaveDataAsArray(); + $fieldName = 'RLTranslate.'.$locale.'.'.implode('.', HtmlHelper::nameToArray($this->fieldName)); + + $requestData = Request::all(); + array_set($requestData, $fieldName, json_encode($data)); + $this->mergeWithPost($requestData); + } + + /** + * mergeWithPost will apply postback values globally + */ + protected function mergeWithPost(array $values) + { + Request::merge($values); + $_POST = array_merge($_POST, $values); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLRepeater.php b/plugins/rainlab/translate/formwidgets/MLRepeater.php new file mode 100644 index 0000000..237d145 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLRepeater.php @@ -0,0 +1,239 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['repeater'] = $parentContent; + return $this->makePartial('mlrepeater'); + } + + /** + * prepareVars + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + $this->rewritePostValues(); + + return $this->getLocaleSaveValue(is_array($value) ? array_values($value) : $value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlrepeater.js'); + } + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/repeater/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/repeater/assets'; + } + + /** + * onAddItem + */ + public function onAddItem() + { + $this->actAsParent(); + return parent::onAddItem(); + } + + /** + * onDuplicateItem + */ + public function onDuplicateItem() + { + $this->actAsParent(); + return parent::onDuplicateItem(); + } + + /** + * onSwitchItemLocale + */ + public function onSwitchItemLocale() + { + if (!$locale = post('_repeater_locale')) { + throw new ApplicationException('Unable to find a repeater locale for: '.$locale); + } + + // Store previous value + $previousLocale = post('_repeater_previous_locale'); + $previousValue = $this->getPrimarySaveDataAsArray(); + + // Update widget to show form for switched locale + $lockerData = $this->getLocaleSaveDataAsArray($locale) ?: []; + $this->reprocessLocaleItems($lockerData); + + foreach ($this->formWidgets as $key => $widget) { + $value = array_shift($lockerData); + if (!$value) { + unset($this->formWidgets[$key]); + } + else { + $widget->setFormValues($value); + } + } + + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + return [ + '#'.$this->getId('mlRepeater') => $parentContent, + 'updateValue' => json_encode($previousValue), + 'updateLocale' => $previousLocale, + ]; + } + + /** + * reprocessLocaleItems ensures that the current locale data is processed by + * the repeater instead of the original non-translated data + * @return void + */ + protected function reprocessLocaleItems($data) + { + $this->formWidgets = []; + + $this->formField->value = $data; + + $key = implode('.', HtmlHelper::nameToArray($this->formField->getName())); + + $requestData = Request::all(); + + array_set($requestData, $key, $data); + + $this->mergeWithPost($requestData); + + $this->processItems(); + } + + /** + * getPrimarySaveDataAsArray gets the active values from the selected locale. + * @return array + */ + protected function getPrimarySaveDataAsArray() + { + $data = post($this->formField->getName()) ?: []; + + return $this->processSaveValue($data); + } + + /** + * getLocaleSaveDataAsArray returns the stored locale data as an array. + * @return array + */ + protected function getLocaleSaveDataAsArray($locale) + { + $saveData = array_get($this->getLocaleSaveData(), $locale, []); + + if (!is_array($saveData)) { + $saveData = json_decode($saveData, true); + } + + return $saveData; + } + + /** + * rewritePostValues since the locker does always contain the latest values, this method + * will take the save data from the repeater and merge it in to the + * locker based on which ever locale is selected using an item map + * @return void + */ + protected function rewritePostValues() + { + // Get the selected locale at postback + $data = post('RLTranslateRepeaterLocale'); + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $locale = array_get($data, $fieldName); + + if (!$locale) { + return; + } + + // Splice the save data in to the locker data for selected locale + $data = $this->getPrimarySaveDataAsArray(); + $fieldName = 'RLTranslate.'.$locale.'.'.implode('.', HtmlHelper::nameToArray($this->fieldName)); + + $requestData = Request::all(); + array_set($requestData, $fieldName, json_encode($data)); + $this->mergeWithPost($requestData); + } + + /** + * mergeWithPost will apply postback values globally + */ + protected function mergeWithPost(array $values) + { + Request::merge($values); + $_POST = array_merge($_POST, $values); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLRichEditor.php b/plugins/rainlab/translate/formwidgets/MLRichEditor.php new file mode 100644 index 0000000..92f4bd2 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLRichEditor.php @@ -0,0 +1,119 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->actAsParent(); + $parentContent = parent::render(); + $this->actAsParent(false); + + if (!$this->isAvailable) { + return $parentContent; + } + + $this->vars['richeditor'] = $parentContent; + return $this->makePartial('mlricheditor'); + } + + /** + * prepareVars + */ + public function prepareVars() + { + parent::prepareVars(); + $this->prepareLocaleVars(); + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->actAsParent(); + parent::loadAssets(); + $this->actAsParent(false); + + if (Locale::isAvailable()) { + $this->loadLocaleAssets(); + $this->addJs('js/mlricheditor.js'); + } + } + + /** + * {@inheritDoc} + */ + public function onLoadPageLinksForm() + { + $this->actAsParent(); + return parent::onLoadPageLinksForm(); + } + + /** + * {@inheritDoc} + */ + protected function getParentViewPath() + { + return base_path().'/modules/backend/formwidgets/richeditor/partials'; + } + + /** + * {@inheritDoc} + */ + protected function getParentAssetPath() + { + return '/modules/backend/formwidgets/richeditor/assets'; + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLText.php b/plugins/rainlab/translate/formwidgets/MLText.php new file mode 100644 index 0000000..c0a74bc --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLText.php @@ -0,0 +1,59 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareLocaleVars(); + + if ($this->isAvailable) { + return $this->makePartial('mltext'); + } + else { + return $this->renderFallbackField(); + } + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->loadLocaleAssets(); + } +} diff --git a/plugins/rainlab/translate/formwidgets/MLTextarea.php b/plugins/rainlab/translate/formwidgets/MLTextarea.php new file mode 100644 index 0000000..12b0837 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/MLTextarea.php @@ -0,0 +1,64 @@ +initLocale(); + } + + /** + * {@inheritDoc} + */ + public function render() + { + $this->prepareLocaleVars(); + + if ($this->isAvailable) { + return $this->makePartial('mltextarea'); + } + else { + return $this->renderFallbackField(); + } + } + + /** + * getSaveValue returns an array of translated values for this field + * @return array + */ + public function getSaveValue($value) + { + return $this->getLocaleSaveValue($value); + } + + /** + * {@inheritDoc} + */ + protected function loadAssets() + { + $this->loadLocaleAssets(); + } +} diff --git a/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js new file mode 100644 index 0000000..e6b0f56 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/assets/js/mlmarkdowneditor.js @@ -0,0 +1,107 @@ +/* + * MLMarkdownEditor plugin + * + * Data attributes: + * - data-control="mlmarkdowneditor" - enables the plugin on an element + * - data-textarea-element="textarea#id" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMarkdownEditor({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMARKDOWNEDITOR CLASS DEFINITION + // ============================ + + var MLMarkdownEditor = function(element, options) { + this.options = options + this.$el = $(element) + this.$textarea = $(options.textareaElement) + this.$markdownEditor = $('[data-control=markdowneditor]:first', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLMarkdownEditor.prototype = Object.create(BaseProto) + MLMarkdownEditor.prototype.constructor = MLMarkdownEditor + + MLMarkdownEditor.DEFAULTS = { + textareaElement: null, + placeholderField: null, + defaultLocale: 'en' + } + + MLMarkdownEditor.prototype.init = function() { + this.$el.multiLingual() + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.on('changeContent.oc.markdowneditor', this.proxy(this.onChangeContent)) + + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLMarkdownEditor.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.off('changeContent.oc.markdowneditor', this.proxy(this.onChangeContent)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlMarkdownEditor') + + this.$textarea = null + this.$markdownEditor = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLMarkdownEditor.prototype.onSetLocale = function(e, locale, localeValue) { + if (typeof localeValue === 'string' && this.$markdownEditor.data('oc.markdownEditor')) { + this.$markdownEditor.markdownEditor('setContent', localeValue); + } + } + + MLMarkdownEditor.prototype.onChangeContent = function(ev, markdowneditor, value) { + this.$el.multiLingual('setLocaleValue', value) + } + + var old = $.fn.mlMarkdownEditor + + $.fn.mlMarkdownEditor = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMarkdownEditor') + var options = $.extend({}, MLMarkdownEditor.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMarkdownEditor', (data = new MLMarkdownEditor(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMarkdownEditor.Constructor = MLMarkdownEditor; + + $.fn.mlMarkdownEditor.noConflict = function () { + $.fn.mlMarkdownEditor = old + return this + } + + $(document).render(function (){ + $('[data-control="mlmarkdowneditor"]').mlMarkdownEditor() + }) + + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm new file mode 100644 index 0000000..03179f9 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmarkdowneditor/partials/_mlmarkdowneditor.htm @@ -0,0 +1,23 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css new file mode 100644 index 0000000..9bd687b --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/css/mlmediafinder.css @@ -0,0 +1,8 @@ +.field-multilingual-mediafinder > .ml-btn { + top: -28px; + right: 4px; +} + +.field-multilingual-mediafinder > .ml-dropdown-menu { + top: 1px; +} diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js new file mode 100644 index 0000000..1b51234 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/assets/js/mlmediafinder.js @@ -0,0 +1,136 @@ +/* + * MLMediaFinder plugin + * + * Data attributes: + * - data-control="mlmediafinder" - enables the plugin on an element + * - data-option="value" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMediaFinder({ option: 'value' }) + * + * Dependences: + * - mediafinder (mediafinder.js) + */ + ++function($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMEDIAFINDER CLASS DEFINITION + // ============================ + + var MLMediaFinder = function(element, options) { + this.options = options + this.$el = $(element) + this.$mediafinder = $('[data-control=mediafinder]', this.$el) + this.$findValue = $('[data-find-value]', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + MLMediaFinder.prototype = Object.create(BaseProto) + MLMediaFinder.prototype.constructor = MLMediaFinder + + MLMediaFinder.DEFAULTS = { + placeholderField: null, + defaultLocale: 'en', + mediaPath: '/', + } + + MLMediaFinder.prototype.init = function() { + + this.$el.multiLingual() + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + // Listen for change event from mediafinder + this.$findValue.on('change', this.proxy(this.setValue)) + + // Stop here for preview mode + if (this.options.isPreview) + return + } + + // Simplify setPath + MLMediaFinder.prototype.setValue = function(e) { + this.setPath($(e.target).val()) + } + + MLMediaFinder.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$findValue.off('change', this.proxy(this.setValue)) + + this.$el.removeData('oc.mlMediaFinder') + + this.$findValue = null + this.$mediafinder = null; + this.$el = null + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null + + BaseProto.dispose.call(this) + } + + + MLMediaFinder.prototype.onSetLocale = function(e, locale, localeValue) { + this.setPath(localeValue) + } + + MLMediaFinder.prototype.setPath = function(localeValue) { + if (typeof localeValue === 'string') { + this.$findValue = localeValue; + + var path = localeValue ? this.options.mediaPath + localeValue : '' + + $('[data-find-image]', this.$mediafinder).attr('src', path) + $('[data-find-file-name]', this.$mediafinder).text(localeValue.substring(1)) + + // if value is present display image/file, else display open icon for media manager + this.$mediafinder.toggleClass('is-populated', !!localeValue) + + this.$el.multiLingual('setLocaleValue', localeValue); + } + } + + // MLMEDIAFINDER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlMediaFinder + + $.fn.mlMediaFinder = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMediaFinder') + var options = $.extend({}, MLMediaFinder.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMediaFinder', (data = new MLMediaFinder(this, options))) + if (typeof option === 'string') result = data[option].apply(data, args) + if (typeof result !== 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMediaFinder.Constructor = MLMediaFinder + + // MLMEDIAFINDER NO CONFLICT + // ================= + + $.fn.mlMediaFinder.noConflict = function () { + $.fn.mlMediaFinder = old + return this + } + + // MLMEDIAFINDER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlmediafinder"]').mlMediaFinder() + }) + + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm b/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm new file mode 100644 index 0000000..8c0d86e --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinder/partials/_mlmediafinder.htm @@ -0,0 +1,22 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js new file mode 100644 index 0000000..4e050e5 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/assets/js/mlmediafinder.js @@ -0,0 +1,182 @@ +/* + * MLMediaFinder plugin + * + * Data attributes: + * - data-control="mlmediafinder" - enables the plugin on an element + * - data-option="value" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlMediaFinder({ option: 'value' }) + * + * Dependences: + * - mediafinder (mediafinder.js) + */ + ++function($) { "use strict"; + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLMEDIAFINDER CLASS DEFINITION + // ============================ + + var MLMediaFinder = function(element, options) { + this.options = options + this.$el = $(element) + this.$mediafinder = $('[data-control=mediafinder]', this.$el) + this.$dataLocker = $('[data-data-locker]', this.$el) + this.isMulti = this.$mediafinder.hasClass('is-multi') + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + this.init() + } + + MLMediaFinder.prototype = Object.create(BaseProto) + MLMediaFinder.prototype.constructor = MLMediaFinder + + MLMediaFinder.DEFAULTS = { + placeholderField: null, + defaultLocale: 'en', + mediaPath: '/', + } + + MLMediaFinder.prototype.init = function() { + + this.$el.multiLingual() + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + + // Listen for change event from mediafinder + this.$dataLocker.on('change', this.proxy(this.setValue)) + + // Stop here for preview mode + if (this.options.isPreview) { + return; + } + } + + MLMediaFinder.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + this.$el.off('dispose-control', this.proxy(this.dispose)); + this.$dataLocker.off('change', this.proxy(this.setValue)); + + this.$el.removeData('oc.mlMediaFinder'); + + this.$dataLocker = null; + this.$mediafinder = null; + this.$el = null; + + // In some cases options could contain callbacks, + // so it's better to clean them up too. + this.options = null; + + BaseProto.dispose.call(this) + } + + MLMediaFinder.prototype.setValue = function(e) { + var mediafinder = this.$mediafinder.data('oc.mediaFinder'), + value = mediafinder.getValue(); + + if (value) { + if (this.isMulti) { + value = JSON.stringify(value); + } + else { + value = value[0]; + } + } + + this.setPath(value); + } + + MLMediaFinder.prototype.onSetLocale = function(e, locale, localeValue) { + this.setPath(localeValue) + } + + MLMediaFinder.prototype.setPath = function(localeValue) { + if (typeof localeValue === 'string') { + var self = this, + isMulti = this.isMulti, + mediaFinder = this.$mediafinder.data('oc.mediaFinder'), + items = [], + localeValueArr = []; + + try { + localeValueArr = JSON.parse(localeValue); + if (!$.isArray(localeValueArr)) { + localeValueArr = [localeValueArr]; + } + } + catch(e) { + isMulti = false; + } + + mediaFinder.$filesContainer.empty(); + + if (isMulti) { + $.each(localeValueArr, function(k, v) { + if (v) { + items.push({ + path: v, + publicUrl: self.options.mediaPath + v, + thumbUrl: self.options.mediaPath + v, + title: v.substring(1) + }); + } + }); + } + else { + if (localeValue) { + items = [{ + path: localeValue, + publicUrl: this.options.mediaPath + localeValue, + thumbUrl: this.options.mediaPath + localeValue, + title: localeValue.substring(1) + }]; + } + } + + mediaFinder.addItems(items); + mediaFinder.evalIsPopulated(); + mediaFinder.evalIsMaxReached(); + + this.$el.multiLingual('setLocaleValue', localeValue); + } + } + + // MLMEDIAFINDER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlMediaFinder + + $.fn.mlMediaFinder = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlMediaFinder') + var options = $.extend({}, MLMediaFinder.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlMediaFinder', (data = new MLMediaFinder(this, options))) + if (typeof option === 'string') result = data[option].apply(data, args) + if (typeof result !== 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlMediaFinder.Constructor = MLMediaFinder + + // MLMEDIAFINDER NO CONFLICT + // ================= + + $.fn.mlMediaFinder.noConflict = function () { + $.fn.mlMediaFinder = old + return this + } + + // MLMEDIAFINDER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlmediafinder"]').mlMediaFinder() + }); +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm new file mode 100644 index 0000000..8c0d86e --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlmediafinderv2/partials/_mlmediafinder.htm @@ -0,0 +1,22 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js b/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js new file mode 100644 index 0000000..ace10ad --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlnestedform/assets/js/mlnestedform.js @@ -0,0 +1,126 @@ +/* + * MLNestedForm plugin + * + * Data attributes: + * - data-control="mlnestedform" - enables the plugin on an element + * + * JavaScript API: + * $('a#someElement').mlNestedForm({ option: 'value' }) + * + */ ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLREPEATER CLASS DEFINITION + // ============================ + + var MLNestedForm = function(element, options) { + this.options = options; + this.$el = $(element); + this.$selector = $('[data-locale-dropdown]', this.$el); + this.$locale = $('[data-nestedform-active-locale]', this.$el); + this.locale = options.defaultLocale; + + $.oc.foundation.controlUtils.markDisposable(element); + Base.call(this); + + // Init + this.init(); + } + + MLNestedForm.prototype = Object.create(BaseProto); + MLNestedForm.prototype.constructor = MLNestedForm; + + MLNestedForm.DEFAULTS = { + switchHandler: null, + defaultLocale: 'en' + } + + MLNestedForm.prototype.init = function() { + this.$el.multiLingual(); + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + + this.$el.one('dispose-control', this.proxy(this.dispose)); + } + + MLNestedForm.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)); + + this.$el.off('dispose-control', this.proxy(this.dispose)); + + this.$el.removeData('oc.mlNestedForm'); + + this.$selector = null; + this.$locale = null; + this.locale = null; + this.$el = null; + + this.options = null; + + BaseProto.dispose.call(this); + } + + MLNestedForm.prototype.onSetLocale = function(e, locale, localeValue) { + var self = this, + previousLocale = this.locale; + + this.$el + .addClass('loading-indicator-container size-form-field') + .loadIndicator(); + + this.locale = locale; + this.$locale.val(locale); + + this.$el.request(this.options.switchHandler, { + data: { + _nestedform_previous_locale: previousLocale, + _nestedform_locale: locale + }, + success: function(data) { + self.$el.multiLingual('setLocaleValue', data.updateValue, data.updateLocale); + self.$el.loadIndicator('hide'); + this.success(data); + } + }); + } + + // MLREPEATER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlNestedForm; + + $.fn.mlNestedForm = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result; + this.each(function () { + var $this = $(this); + var data = $this.data('oc.mlNestedForm'); + var options = $.extend({}, MLNestedForm.DEFAULTS, $this.data(), typeof option == 'object' && option); + if (!data) $this.data('oc.mlNestedForm', (data = new MLNestedForm(this, options))); + if (typeof option == 'string') result = data[option].apply(data, args); + if (typeof result != 'undefined') return false; + }) + + return result ? result : this; + } + + $.fn.mlNestedForm.Constructor = MLNestedForm; + + // MLREPEATER NO CONFLICT + // ================= + + $.fn.mlNestedForm.noConflict = function () { + $.fn.mlNestedForm = old + return this + } + + // MLREPEATER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlnestedform"]').mlNestedForm(); + }); + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm b/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm new file mode 100644 index 0000000..d46d6f9 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlnestedform/partials/_mlnestedform.htm @@ -0,0 +1,31 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js b/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js new file mode 100644 index 0000000..d412b0d --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlrepeater/assets/js/mlrepeater.js @@ -0,0 +1,139 @@ +/* + * MLRepeater plugin + * + * Data attributes: + * - data-control="mlrepeater" - enables the plugin on an element + * + * JavaScript API: + * $('a#someElement').mlRepeater({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLREPEATER CLASS DEFINITION + // ============================ + + var MLRepeater = function(element, options) { + this.options = options + this.$el = $(element) + this.$selector = $('[data-locale-dropdown]', this.$el) + this.$locale = $('[data-repeater-active-locale]', this.$el) + this.locale = options.defaultLocale + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLRepeater.prototype = Object.create(BaseProto) + MLRepeater.prototype.constructor = MLRepeater + + MLRepeater.DEFAULTS = { + switchHandler: null, + defaultLocale: 'en' + } + + MLRepeater.prototype.init = function() { + this.$el.multiLingual() + + this.checkEmptyItems() + + $(document).on('render', this.proxy(this.checkEmptyItems)) + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLRepeater.prototype.dispose = function() { + + $(document).off('render', this.proxy(this.checkEmptyItems)) + + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlRepeater') + + this.$selector = null + this.$locale = null + this.locale = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLRepeater.prototype.checkEmptyItems = function() { + var isEmpty = !$('ul.field-repeater-items > li', this.$el).length + this.$el.toggleClass('is-empty', isEmpty) + } + + MLRepeater.prototype.onSetLocale = function(e, locale, localeValue) { + var self = this, + previousLocale = this.locale + + this.$el + .addClass('loading-indicator-container size-form-field') + .loadIndicator() + + this.locale = locale + this.$locale.val(locale) + + this.$el.request(this.options.switchHandler, { + data: { + _repeater_previous_locale: previousLocale, + _repeater_locale: locale + }, + success: function(data) { + self.$el.multiLingual('setLocaleValue', data.updateValue, data.updateLocale) + self.$el.loadIndicator('hide') + this.success(data) + } + }) + } + + // MLREPEATER PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlRepeater + + $.fn.mlRepeater = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlRepeater') + var options = $.extend({}, MLRepeater.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlRepeater', (data = new MLRepeater(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlRepeater.Constructor = MLRepeater + + // MLREPEATER NO CONFLICT + // ================= + + $.fn.mlRepeater.noConflict = function () { + $.fn.mlRepeater = old + return this + } + + // MLREPEATER DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlrepeater"]').mlRepeater() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm b/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm new file mode 100644 index 0000000..1d6480e --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlrepeater/partials/_mlrepeater.htm @@ -0,0 +1,31 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js b/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js new file mode 100644 index 0000000..807848a --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlricheditor/assets/js/mlricheditor.js @@ -0,0 +1,135 @@ +/* + * MLRichEditor plugin + * + * Data attributes: + * - data-control="mlricheditor" - enables the plugin on an element + * - data-textarea-element="textarea#id" - an option with a value + * + * JavaScript API: + * $('a#someElement').mlRichEditor({ option: 'value' }) + * + */ + ++function ($) { "use strict"; + + var Base = $.oc.foundation.base, + BaseProto = Base.prototype + + // MLRICHEDITOR CLASS DEFINITION + // ============================ + + var MLRichEditor = function(element, options) { + this.options = options + this.$el = $(element) + this.$textarea = $(options.textareaElement) + this.$richeditor = $('[data-control=richeditor]:first', this.$el) + + $.oc.foundation.controlUtils.markDisposable(element) + Base.call(this) + + // Init + this.init() + } + + MLRichEditor.prototype = Object.create(BaseProto) + MLRichEditor.prototype.constructor = MLRichEditor + + MLRichEditor.DEFAULTS = { + textareaElement: null, + placeholderField: null, + defaultLocale: 'en' + } + + MLRichEditor.prototype.init = function() { + this.$el.multiLingual() + + this.$el.on('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.on('syncContent.oc.richeditor', this.proxy(this.onSyncContent)) + + this.updateLayout() + + $(window).on('resize', this.proxy(this.updateLayout)) + $(window).on('oc.updateUi', this.proxy(this.updateLayout)) + this.$el.one('dispose-control', this.proxy(this.dispose)) + } + + MLRichEditor.prototype.dispose = function() { + this.$el.off('setLocale.oc.multilingual', this.proxy(this.onSetLocale)) + this.$textarea.off('syncContent.oc.richeditor', this.proxy(this.onSyncContent)) + $(window).off('resize', this.proxy(this.updateLayout)) + $(window).off('oc.updateUi', this.proxy(this.updateLayout)) + + this.$el.off('dispose-control', this.proxy(this.dispose)) + + this.$el.removeData('oc.mlRichEditor') + + this.$textarea = null + this.$richeditor = null + this.$el = null + + this.options = null + + BaseProto.dispose.call(this) + } + + MLRichEditor.prototype.onSetLocale = function(e, locale, localeValue) { + if (typeof localeValue === 'string' && this.$richeditor.data('oc.richEditor')) { + this.$richeditor.richEditor('setContent', localeValue); + } + } + + MLRichEditor.prototype.onSyncContent = function(ev, richeditor, value) { + this.$el.multiLingual('setLocaleValue', value.html) + } + + MLRichEditor.prototype.updateLayout = function() { + var $toolbar = $('.fr-toolbar', this.$el), + $btn = $('.ml-btn[data-active-locale]:first', this.$el), + $dropdown = $('.ml-dropdown-menu[data-locale-dropdown]:first', this.$el) + + if (!$toolbar.length) { + return + } + + var height = $toolbar.outerHeight(true) + $btn.css('top', height + 6) + $dropdown.css('top', height + 40) + } + + // MLRICHEDITOR PLUGIN DEFINITION + // ============================ + + var old = $.fn.mlRichEditor + + $.fn.mlRichEditor = function (option) { + var args = Array.prototype.slice.call(arguments, 1), result + this.each(function () { + var $this = $(this) + var data = $this.data('oc.mlRichEditor') + var options = $.extend({}, MLRichEditor.DEFAULTS, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('oc.mlRichEditor', (data = new MLRichEditor(this, options))) + if (typeof option == 'string') result = data[option].apply(data, args) + if (typeof result != 'undefined') return false + }) + + return result ? result : this + } + + $.fn.mlRichEditor.Constructor = MLRichEditor + + // MLRICHEDITOR NO CONFLICT + // ================= + + $.fn.mlRichEditor.noConflict = function () { + $.fn.mlRichEditor = old + return this + } + + // MLRICHEDITOR DATA-API + // =============== + + $(document).render(function () { + $('[data-control="mlricheditor"]').mlRichEditor() + }) + +}(window.jQuery); diff --git a/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm b/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm new file mode 100644 index 0000000..6f3e19c --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mlricheditor/partials/_mlricheditor.htm @@ -0,0 +1,23 @@ + + diff --git a/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm b/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm new file mode 100644 index 0000000..b8a5735 --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mltext/partials/_mltext.htm @@ -0,0 +1,36 @@ + +previewMode): ?> + value ? e($field->value) : ' ' ?> + + + diff --git a/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm b/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm new file mode 100644 index 0000000..1a710bd --- /dev/null +++ b/plugins/rainlab/translate/formwidgets/mltextarea/partials/_mltextarea.htm @@ -0,0 +1,32 @@ + +previewMode): ?> +
    value)) ?>
    + + + diff --git a/plugins/rainlab/translate/lang/ar/ar.php b/plugins/rainlab/translate/lang/ar/ar.php new file mode 100644 index 0000000..aee91f4 --- /dev/null +++ b/plugins/rainlab/translate/lang/ar/ar.php @@ -0,0 +1,59 @@ + [ + 'name' => 'ترجمة', + 'description' => 'تفعيل تعدد اللغات للموقع.', + 'tab' => 'ترجمة', + 'manage_locales' => 'إدارة اللغات', + 'manage_messages' => 'إدارة النصوص', + ], + 'locale_picker' => [ + 'component_name' => 'محدد اللغات', + 'component_description' => 'عرض قائمة لتحديد لغة الواجهة.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'عناصر بدائل hrefLang', + 'component_description' => 'إضافة بدائل اللغة للصفحة كعنصر hreflang ' + ], + 'locale' => [ + 'title' => 'إدارة اللغات', + 'update_title' => 'تحديث اللغة', + 'create_title' => 'إنشاء لغة', + 'select_label' => 'تحديد اللغة', + 'default_suffix' => 'افتراضي', + 'unset_default' => '":locale" بالفعل افتراضي ولايمكن نزع الافتراضية عنها.', + 'delete_default' => '":locale" هو افتراضي فلا يمكن حذفه.', + 'disabled_default' => '":locale" معطل فلايمكن تعيينه كافتراضي.', + 'name' => 'اسم', + 'code' => 'رمز', + 'is_default' => 'افتراضي', + 'is_default_help' => 'اللغة الافتراضية تمثل المحتوى قبل ترجمته.', + 'is_enabled' => 'مفعل', + 'is_enabled_help' => 'اللغات المعطلة ستكون غير متاحة في الواجهة.', + 'not_available_help' => 'لا توجد لغات أخرى لإعدادها.', + 'hint_locales' => 'إنشاء لغة جديدة هنا لترجمة محتويات الواجهة. اللغة الافتراضية تمثل المحتوى قبل ترجمته.', + 'reorder_title' => 'إعادة ترتيب لغات', + 'sort_order' => 'اتجاه الترتيب', + ], + 'messages' => [ + 'title' => 'ترجمة النصوص', + 'description' => 'تحديث النصوص', + 'clear_cache_link' => 'مسح المخبآت', + 'clear_cache_loading' => 'يتم مسح مخبآت التطبيق...', + 'clear_cache_success' => 'تم مسح مخبآت التطبيق بنجاح!', + 'clear_cache_hint' => 'ربما يجب عليك الضغط على مسح المخبآت لترى التغييرات في الواجهة.', + 'scan_messages_link' => 'استكشاف النصوص', + 'scan_messages_begin_scan' => 'بدء الاستكشاف', + 'scan_messages_loading' => 'يتم استكشاف نصوص جديدة...', + 'scan_messages_success' => 'تم استكشاف ملفات القالب بنجاح!', + 'scan_messages_hint' => 'عند الضغط على استكشاف النصوص سيتم استكشاف ملفات القالب النشط لإيجاد نصوص جديدة لترجمتها.', + 'scan_messages_process' => 'هذه العملية ستقوم باستكشاف ملفات القالب النشط لإيجاد أي نصوص جديدة يمكن ترجمتها.', + 'scan_messages_process_limitations' => 'بعض النصوص قد لا يتم التقاطها وستظهر بعد أول استعمال لها.', + 'scan_messages_purge_label' => 'إفراغ جميع الرسائل أولا', + 'scan_messages_purge_help' => 'عند تحديد هذه الخاصية سيتم حذف جميع النصوص قبل علية الاستكشاف.', + 'scan_messages_purge_confirm' => 'هل أنت متأكد من حذف جميع النصوص? لن يمكنك التراجع بعد الآن!', + 'hint_translate' => 'هنا يمكن ترجمة النصوص المستعملة في الواجهة، سيتم الحفظ آليا.', + 'hide_translated' => 'إخفاء النصوص المترجمة', + 'export_messages_link' => 'تصدير النصوص', + 'import_messages_link' => 'استيراد النصوص', + ], +]; diff --git a/plugins/rainlab/translate/lang/bg/lang.php b/plugins/rainlab/translate/lang/bg/lang.php new file mode 100644 index 0000000..99d362c --- /dev/null +++ b/plugins/rainlab/translate/lang/bg/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'Превод', + 'description' => 'Активира функцията многоезични уебсайтове.', + 'tab' => 'Преводи', + 'manage_locales' => 'Настройка на локация', + 'manage_messages' => 'Настройка на съобщения' + ], + 'locale_picker' => [ + 'component_name' => 'Избор на местоположение', + 'component_description' => 'Показва меню за избор на езика на сайта.', + ], + 'locale' => [ + 'title' => 'Настройка на езици', + 'update_title' => 'Актуализация на език', + 'create_title' => 'Създай език', + 'select_label' => 'Избери език', + 'default_suffix' => 'по подразбиране', + 'unset_default' => '":locale" вече е по подразбиране и не може да бъде изключен по подразбиране.', + 'disabled_default' => '":locale" е забранен и не може да бъде зададен по подразбиране.', + 'name' => 'Име', + 'code' => 'Код', + 'is_default' => 'По подразбиране', + 'is_default_help' => 'Основният език представлява съдържанието на сайта преди превод.', + 'is_enabled' => 'Включен', + 'is_enabled_help' => 'Изключените езици няма да са налични за преглед в сайта.', + 'not_available_help' => 'Не съществуват други езици за настройка.', + 'hint_locales' => 'Създайте нови езици за превод на съдържанието на сайта. Основният език представлява съдържанието, преди той да бъде преведен.', + ], + 'messages' => [ + 'title' => 'Преведени Съобщения', + 'description' => 'Актуализарай Съобщения', + 'clear_cache_link' => 'Изтрий кеш-паметта', + 'clear_cache_loading' => 'Изчистване кеша на приложението...', + 'clear_cache_success' => 'Кеш кеш-паметта успешно изтрита!', + 'clear_cache_hint' => 'Може да е необходимо да кликнете на бутона Изтрий кеш-паметта за да видите промените на вашата страница.', + 'scan_messages_link' => 'Сканирай за съобщения', + 'scan_messages_loading' => 'Сканиране за нови съобщения...', + 'scan_messages_success' => 'Темата е сканирана успешно!', + 'scan_messages_hint' => 'Ако кликнете на Сканирай за съобщения това ще провери темата за нови съобщения (изречения) за превод.', + 'hint_translate' => 'Тук може да превеждате съобщенията (изреченията) на самият сайт, полетата ще се запазят автоматично.', + 'hide_translated' => 'Скрий преведените', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/cs/lang.php b/plugins/rainlab/translate/lang/cs/lang.php new file mode 100644 index 0000000..8bc9f5d --- /dev/null +++ b/plugins/rainlab/translate/lang/cs/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'Překlady', + 'description' => 'Aktivuje vícejazyčné stránky a překlady.', + 'tab' => 'Překlad', + 'manage_locales' => 'Správa jazyků', + 'manage_messages' => 'Správa překladů' + ], + 'locale_picker' => [ + 'component_name' => 'Výběr jazyka', + 'component_description' => 'Zobrazí možnost výberu jazyka ve stránkách.', + ], + 'locale' => [ + 'title' => 'Správa jazyků', + 'update_title' => 'Upravit jazyk', + 'create_title' => 'Přidat jazyk', + 'select_label' => 'Výběr jazyka', + 'default_suffix' => 'výchozí', + 'unset_default' => '":locale" je již výchozí a nemůže být odnastavena. Zkuste nastavit jiný jazyk jako výchozí.', + 'disabled_default' => '":locale" je neaktivní, takže nemůže být nastavený jako výchozí.', + 'name' => 'Název', + 'code' => 'Kód', + 'is_default' => 'Výchozí', + 'is_default_help' => 'Výchozí jazyk je jazyk webových stránek před překladem.', + 'is_enabled' => 'Aktivní', + 'is_enabled_help' => 'Neaktivní jazyky nepůjdou vybrat na webových stránkách.', + 'not_available_help' => 'Nemáte nastavené žádné jiné jazyky.', + 'hint_locales' => 'Zde můžete přidat nový jazyk pro překlad webových stránek. Výchozí jazyk reprezentuje obsah stránek ještě před překladem.', + ], + 'messages' => [ + 'title' => 'Překlad textů', + 'description' => 'Upravit text', + 'clear_cache_link' => 'Vymazat cache', + 'clear_cache_loading' => 'Mazání aplikační cache...', + 'clear_cache_success' => 'Aplikační cache úspěšně vymazána!', + 'clear_cache_hint' => 'Možná bude potřeba kliknout na Vymazat cache, aby se změny projevily na webobých stránkách.', + 'scan_messages_link' => 'Najít texty k překladu', + 'scan_messages_loading' => 'Hledání textů k překladu...', + 'scan_messages_success' => 'Prohledávání šablon pro získání textů k překladu úspěšně dokončeno!', + 'scan_messages_hint' => 'Kliknutím na Najít texty k překladu zkontroluje soubory aktivních témat a najde texty k překladu.', + 'hint_translate' => 'Zde můžete přeložit texty použité na webových stránkách. Pole budou automaticky uložena.', + 'hide_translated' => 'Schovat přeložené', + ], +]; diff --git a/plugins/rainlab/translate/lang/de/lang.php b/plugins/rainlab/translate/lang/de/lang.php new file mode 100644 index 0000000..bd6e366 --- /dev/null +++ b/plugins/rainlab/translate/lang/de/lang.php @@ -0,0 +1,47 @@ + [ + 'name' => 'Translate', + 'description' => 'Ermöglicht mehrsprachige Seiten.', + 'manage_locales' => 'Sprachen verwalten', + 'manage_messages' => 'Übersetzungen verwalten', + ], + 'locale_picker' => [ + 'component_name' => 'Sprachauswahl', + 'component_description' => 'Zeigt ein Dropdown-Menü zur Auswahl der Sprache im Frontend.', + ], + 'locale' => [ + 'title' => 'Sprachen verwalten', + 'update_title' => 'Sprache bearbeiten', + 'create_title' => 'Sprache erstellen', + 'select_label' => 'Sprache auswählen', + 'default_suffix' => 'Standard', + 'unset_default' => '":locale" ist bereits die Standardsprache und kann nicht abgewählt werden.', + 'disabled_default' => '":locale" ist deaktiviert und kann deshalb nicht als Standardsprache festgelegt werden.', + 'name' => 'Name', + 'code' => 'Code', + 'is_default' => 'Standard', + 'is_default_help' => 'Die Übersetzung der Standardsprache wird verwendet, um Inhalte anzuzeigen, die in der Sprache des Nutzers nicht vorhanden sind.', + 'is_enabled' => 'Aktiv', + 'is_enabled_help' => 'Deaktivierte Sprachen sind im Frontend nicht verfügbar.', + 'not_available_help' => 'Es gibt keine anderen Sprachen.', + 'hint_locales' => 'Hier können neue Sprachen angelegt werden, in die Inhalte im Frontend übersetzt werden können. Die Standardsprache dient als Ausgangssprache für Übersetzungen.', + 'reorder_title' => 'Sprachen sortieren', + 'sort_order' => 'Sortierung', + ], + 'messages' => [ + 'title' => 'Übersetzungen verwalten', + 'description' => 'Inhalte verwalten und übersetzen', + 'clear_cache_link' => 'Cache leeren', + 'clear_cache_loading' => 'Leere Application-Cache...', + 'clear_cache_success' => 'Application-Cache erfolgreich geleert!', + 'clear_cache_hint' => 'Möglicherweise muss der Cache geleert werden (Button Cache leeren), bevor Änderungen im Frontend sichtbar werden.', + 'scan_messages_link' => 'Nach Inhalten suchen', + 'scan_messages_loading' => 'Suche nach neuen Inhalte...', + 'scan_messages_success' => 'Suche nach neuen Inhalte erfolgreich abgeschlossen!', + 'scan_messages_hint' => 'Ein Klick auf Nach Inhalten suchen sucht nach neuen Inhalten, die übersetzt werden können.', + 'hint_translate' => 'Hier können Inhalte aus dem Frontend übersetzt werden. Die Felder werden automatisch gespeichert.', + 'hide_translated' => 'Bereits übersetzte Inhalte ausblenden', + 'export_messages_link' => 'Übersetzungen exportieren', + 'import_messages_link' => 'Übersetzungen importieren', + ], +]; diff --git a/plugins/rainlab/translate/lang/en/lang.php b/plugins/rainlab/translate/lang/en/lang.php new file mode 100644 index 0000000..8fe2ce3 --- /dev/null +++ b/plugins/rainlab/translate/lang/en/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Translate', + 'description' => 'Enables multi-lingual websites.', + 'tab' => 'Translation', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages', + ], + 'locale_picker' => [ + 'component_name' => 'Locale Picker', + 'component_description' => 'Shows a dropdown to select a front-end language.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternate hrefLang elements', + 'component_description' => 'Injects the language alternatives for page as hreflang elements' + ], + 'locale' => [ + 'title' => 'Manage Languages', + 'update_title' => 'Update Language', + 'create_title' => 'Create Language', + 'select_label' => 'Select Language', + 'default_suffix' => 'default', + 'unset_default' => '":locale" is already default and cannot be unset as default.', + 'delete_default' => '":locale" is the default and cannot be deleted.', + 'disabled_default' => '":locale" is disabled and cannot be set as default.', + 'name' => 'Name', + 'code' => 'Code', + 'is_default' => 'Default', + 'is_default_help' => 'The default language represents the content before translation.', + 'is_enabled' => 'Enabled', + 'is_enabled_help' => 'Disabled languages will not be available in the front-end.', + 'not_available_help' => 'There are no other languages set up.', + 'hint_locales' => 'Create new languages here for translating front-end content. The default language represents the content before it has been translated.', + 'reorder_title' => 'Reorder Languages', + 'sort_order' => 'Sort Order', + ], + 'messages' => [ + 'title' => 'Translate Messages', + 'description' => 'Update messages', + 'clear_cache_link' => 'Clear Cache', + 'clear_cache_loading' => 'Clearing application cache...', + 'clear_cache_success' => 'Cleared the application cache successfully!', + 'clear_cache_hint' => 'You may need to click Clear cache to see the changes on the front-end.', + 'scan_messages_link' => 'Scan for Messages', + 'scan_messages_begin_scan' => 'Begin Scan', + 'scan_messages_loading' => 'Scanning for new messages...', + 'scan_messages_success' => 'Scanned theme template files successfully!', + 'scan_messages_hint' => 'Clicking Scan for messages will check the active theme files for any new messages to translate.', + 'scan_messages_process' => 'This process will attempt to scan the active theme for messages that can be translated.', + 'scan_messages_process_limitations' => 'Some messages may not be captured and will only appear after the first time they are used.', + 'scan_messages_purge_label' => 'Purge all messages first', + 'scan_messages_purge_help' => 'If checked, this will delete all messages, including their translations, before performing the scan.', + 'scan_messages_purge_confirm' => 'Are you sure you want to delete all messages? This cannot be undone!', + 'scan_messages_purge_deleted_label' => 'Purge missing messages after scan', + 'scan_messages_purge_deleted_help' => 'If checked, after the scan is done, any messages the scanner did not find, including their translations, will be deleted. This cannot be undone!', + 'hint_translate' => 'Here you can translate messages used on the front-end, the fields will save automatically.', + 'hide_translated' => 'Hide translated', + 'export_messages_link' => 'Export Messages', + 'import_messages_link' => 'Import Messages', + 'not_found' => 'Not found', + 'found_help' => 'Whether any errors occurred during scanning.', + 'found_title' => 'Scan errors', + ], +]; diff --git a/plugins/rainlab/translate/lang/es/lang.php b/plugins/rainlab/translate/lang/es/lang.php new file mode 100644 index 0000000..f7a11c6 --- /dev/null +++ b/plugins/rainlab/translate/lang/es/lang.php @@ -0,0 +1,51 @@ + [ + 'name' => 'Multilenguaje', + 'description' => 'Permite sitios web multilingües', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages' + ], + 'locale_picker' => [ + 'component_name' => 'Selección de idioma', + 'component_description' => 'Muestra una lista desplegable para seleccionar un idioma para el usuario', + ], + 'locale' => [ + 'title' => 'Administrar idiomas', + 'update_title' => 'Actualizar idioma', + 'create_title' => 'Crear idioma', + 'select_label' => 'Seleccionar idioma', + 'default_suffix' => 'Defecto', + 'unset_default' => '": locale" ya está predeterminado y no puede ser nulo por defecto.', + 'disabled_default' => '":locale" esta desactivado y no puede ser idioma por defecto', + 'name' => 'Nombre', + 'code' => 'Código', + 'is_default' => 'Por defecto', + 'is_default_help' => 'El idioma por defecto con el que se representa el contenido antes de la traducción.', + 'is_enabled' => 'Habilitado', + 'is_enabled_help' => 'Los idiomas desactivados no estarán disponibles en el front-end', + 'not_available_help' => 'No hay otros idiomas establecidos.', + 'hint_locales' => 'Crear nuevos idiomas aquí para traducir el contenido de front-end. El idioma por defecto representa el contenido antes de que haya sido traducido.', + ], + 'messages' => [ + 'title' => 'Traducir mensajes', + 'description' => 'Editar mensajes', + 'clear_cache_link' => 'Limpiar cache', + 'clear_cache_loading' => 'Borrado de la memoria caché de aplicaciones ...', + 'clear_cache_success' => 'Se ha borrado la memoria cache dela aplicación con éxito', + 'clear_cache_hint' => 'Es posible que tenga que hacer clic en Borrar caché para ver los cambios en el front-end.', + 'scan_messages_link' => 'Escanear mensajes', + 'scan_messages_loading' => 'Escaneando nuevos mensajes...', + 'scan_messages_success' => 'Escaneado de los archivos del tema completado!', + 'scan_messages_hint' => 'Al hacer click en Escanear comprobaremos los mensajes de los archivos de los temas activos para localizar nuevos mensajes a traducir.', + 'hint_translate' => 'Aquí usted puede traducir los mensajes utilizados en el front-end, los campos se guardará automáticamente.', + 'hide_translated' => 'Ocultar traducción', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/fa/lang.php b/plugins/rainlab/translate/lang/fa/lang.php new file mode 100644 index 0000000..ed608e0 --- /dev/null +++ b/plugins/rainlab/translate/lang/fa/lang.php @@ -0,0 +1,46 @@ + [ + 'name' => 'مترجم', + 'description' => 'فعال سازی وب سایت چند زبانه', + 'tab' => 'ترجمه', + 'manage_locales' => 'مدیریت مناطق', + 'manage_messages' => 'مدیریت پیغام ها' + ], + 'locale_picker' => [ + 'component_name' => 'انتخابگر منطقه', + 'component_description' => 'نمایش انتخابگر کشویی جهت انتخاب زبان.', + ], + 'locale' => [ + 'title' => 'مدیریت زبان ها', + 'update_title' => 'به روز رسانی زبان', + 'create_title' => 'ایجاد زبان', + 'select_label' => 'انتخاب زبان', + 'default_suffix' => 'پیشفرض', + 'unset_default' => '":locale" در حال حاظر پیشفرض می باشد و نمیتوانید آن را خارج کنید.', + 'disabled_default' => '":locale" غیر فعال می باشد و نمیتوانید آن را پیشفرض قرار دهید.', + 'name' => 'نام', + 'code' => 'کد یکتا', + 'is_default' => 'پیشفرض', + 'is_default_help' => 'زبان پیشفرضی که داده ها قبل از ترجمه به آن زبان وارد می شوند', + 'is_enabled' => 'فعال', + 'is_enabled_help' => 'زبان های غیر فعال در دسترس نخواهند بود.', + 'not_available_help' => 'زبان دیگری جهت نصب وجود ندارد.', + 'hint_locales' => 'زبان جدیدی را جهت ترجمه محتوی ایجاد نمایید.', + ], + 'messages' => [ + 'title' => 'ترجمه پیغام ها', + 'description' => 'به روز رسانی پیغام ها', + 'clear_cache_link' => 'پاکسازی حافظه کش', + 'clear_cache_loading' => 'در حال پاکسازی حافظه کش برنامه...', + 'clear_cache_success' => 'عملیات پاکسازی حافظه کش به اتمام رسید.', + 'clear_cache_hint' => 'جهت نمایش تغیرات در سایت نیاز است که شما بر رور پاکسازی حافظه کش کلیک نمایید.', + 'scan_messages_link' => 'جستجوی پیغام ها', + 'scan_messages_loading' => 'جستجوی پیغام های جدید...', + 'scan_messages_success' => 'جستجوی پیغام های جدید به اتمام رسید.', + 'scan_messages_hint' => 'جهت جستجو و نمایش پیغامهای جدیدی که باید ترجمه شوند بر روی جستجوی پیغام های جدید کلیک نمایید.', + 'hint_translate' => 'در این قسمت شما میتوانید پیغام ها را ترجمه نمایید. گزینه ها به صورت خودکار ذخیره میشوند.', + 'hide_translated' => 'مخفی سازی ترجمه شده ها', + ], +]; diff --git a/plugins/rainlab/translate/lang/fr/lang.php b/plugins/rainlab/translate/lang/fr/lang.php new file mode 100644 index 0000000..3d12f86 --- /dev/null +++ b/plugins/rainlab/translate/lang/fr/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Traductions', + 'description' => 'Permet de créer des sites Internet multilingues', + 'tab' => 'Traduction', + 'manage_locales' => 'Manage locales', + 'manage_messages' => 'Manage messages' + ], + 'locale_picker' => [ + 'component_name' => 'Sélection de la langue', + 'component_description' => 'Affiche un menu déroulant pour sélectionner la langue sur le site.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Éléments hrefLang alternatifs', + 'component_description' => "Injecte les alternatives linguistiques pour la page en tant qu'éléments hreflang" + ], + 'locale' => [ + 'title' => 'Gestion des langues', + 'update_title' => 'Mettre à jour la langue', + 'create_title' => 'Ajouter une langue', + 'select_label' => 'Sélectionner une langue', + 'default_suffix' => 'défaut', + 'unset_default' => '":locale" est déjà la langue par défaut et ne peut être désactivée', + 'delete_default' => '":locale" est la valeur par défaut et ne peut pas être supprimé.', + 'disabled_default' => '":locale" est désactivé et ne peut être utilisé comme paramètre par défaut.', + 'name' => 'Nom', + 'code' => 'Code', + 'is_default' => 'Défaut', + 'is_default_help' => 'La langue par défaut représente le contenu avant la traduction.', + 'is_enabled' => 'Activé', + 'is_enabled_help' => 'Les langues désactivées ne seront plus disponibles sur le site.', + 'not_available_help' => 'Aucune autre langue n\'est définie.', + 'hint_locales' => 'Vous pouvez ajouter de nouvelles langues et traduire les messages du site. La langue par défaut est celle utilisée pour les contenus avant toute traduction.', + 'reorder_title' => 'Réorganiser les langues', + 'sort_order' => 'Ordre de tri', + ], + 'messages' => [ + 'title' => 'Traduction des Messages', + 'description' => 'Mettre à jour Messages', + 'clear_cache_link' => 'Supprimer le cache', + 'clear_cache_loading' => 'Suppression du cache de l\'application...', + 'clear_cache_success' => 'Le cache de l\'application a été supprimé !', + 'clear_cache_hint' => 'Vous devez cliquer sur Supprimer le cache pour voir les modifications sur le site.', + 'scan_messages_link' => 'Rechercher des messages à traduire', + 'scan_messages_begin_scan' => 'Commencer la recherche', + 'scan_messages_loading' => 'Recherche de nouveaux messages...', + 'scan_messages_success' => 'Recherche dans les fichiers du thème effectuée !', + 'scan_messages_hint' => 'Cliquez sur Rechercher des messages à traduire pour parcourir les fichiers du thème actif à la recherche de messages à traduire.', + 'scan_messages_process' => 'Ce processus tentera d\'analyser le thème actif pour les messages qui peuvent être traduits.', + 'scan_messages_process_limitations' => 'Certains messages peuvent ne pas être capturés et n\'apparaîtront qu\'après leur première utilisation.', + 'scan_messages_purge_label' => 'Purger tous les messages en premier', + 'scan_messages_purge_help' => 'Si cette case est cochée, tous les messages seront supprimés avant d\'effectuer l\'analyse.', + 'scan_messages_purge_confirm' => 'Êtes-vous sûr de vouloir supprimer tous les messages? Cela ne peut pas être annulé!', + 'hint_translate' => 'Vous pouvez traduire les messages affichés sur le site, les champs s\'enregistrent automatiquement.', + 'hide_translated' => 'Masquer les traductions', + 'export_messages_link' => 'Exporter les messages', + 'import_messages_link' => 'Importer les messages', + ], +]; diff --git a/plugins/rainlab/translate/lang/gr/lang.php b/plugins/rainlab/translate/lang/gr/lang.php new file mode 100644 index 0000000..b49eac4 --- /dev/null +++ b/plugins/rainlab/translate/lang/gr/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Μετέφρασε', + 'description' => 'Ενεργοποίηση πολύγλωσσων ιστότοπων.', + 'tab' => 'Μετάφραση', + 'manage_locales' => 'Διαχείριση τοπικών ρυθμίσεων', + 'manage_messages' => 'Διαχείριση μηνυμάτων', + ], + 'locale_picker' => [ + 'component_name' => 'Επιλογή τοπικών ρυθμίσεων', + 'component_description' => 'Εμφάνιση αναπτυσσόμενου μενού για επιλογή γλώσσας front-end.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Εναλλακτικά στοιχεία hrefLang', + 'component_description' => 'Εισαγωγή εναλλακτικών γλωσσών για τη σελίδα ως στοιχεία hreflang' + ], + 'locale' => [ + 'title' => 'Διαχείριση γλώσσας', + 'update_title' => 'Ενημέρωση γλώσσας', + 'create_title' => 'Δημιουργία γλώσσας', + 'select_label' => 'Επιλογή γλώσσας', + 'default_suffix' => 'Προεπιλογή', + 'unset_default' => '":locale" είναι ήδη προεπιλογή και δεν μπορεί να οριστεί ως προεπιλογή.', + 'delete_default' => '":locale" είναι ήδη προεπιλογή και δεν μπορεί να διαγραφεί.', + 'disabled_default' => '":locale" είναι απενεργοποιημένο και δεν μπορεί να οριστεί ως προεπιλογή.', + 'name' => 'Όνομα', + 'code' => 'Κωδικός', + 'is_default' => 'Προεπιλογή', + 'is_default_help' => 'Η προεπιλεγμένη γλώσσα αντιπροσωπεύει το περιεχόμενο πριν από τη μετάφραση.', + 'is_enabled' => 'Ενεργοποίηση', + 'is_enabled_help' => 'Οι απενεργοποιημένες γλώσσες δεν θα είναι διαθέσιμες στο front-end.', + 'not_available_help' => 'Δεν έχουν ρυθμιστεί άλλες γλώσσες.', + 'hint_locales' => 'Δημιουργήστε νέες γλώσσες εδώ για τη μετάφραση περιεχομένου front-end. Η προεπιλεγμένη γλώσσα αντιπροσωπεύει το περιεχόμενο προτού μεταφραστεί.', + 'reorder_title' => 'Αναδιάταξη γλωσσών', + 'sort_order' => 'Σειρά ταξινόμησης', + ], + 'messages' => [ + 'title' => 'Μετάφραση μηνυμάτων', + 'description' => 'Ενημέρωση μηνυμάτων', + 'clear_cache_link' => 'Εκκαθάριση προσωρινής μνήμης', + 'clear_cache_loading' => 'Εκκαθάριση προσωρινής μνήμης εφαρμογής ...', + 'clear_cache_success' => 'Επιτυχής εκκαθάριση της προσωρινής μνήμης της εφαρμογής!', + 'clear_cache_hint' => 'Ίσως χρειαστεί να κάνετε κλικ στην επιλογή Clear cashe για να δείτε τις αλλαγές στο front-end.', + 'scan_messages_link' => 'Σάρωση για μηνύματα', + 'scan_messages_begin_scan' => 'Έναρξη σάρωσης', + 'scan_messages_loading' => 'Σάρωση για νέα μηνύματα ...', + 'scan_messages_success' => 'Επιτυχής σάρωση θεματικών πρότυπων αρχείων!', + 'scan_messages_hint' => 'Κάνοντας κλικ στην επιλογή Scan για messages θα ελεγθούν τα ενεργά αρχεία θεμάτων για τυχόν νέα μηνύματα που χρειάζονται μετάφραση.', + 'scan_messages_process' => 'Αυτή η διαδικασία θα προσπαθήσει να σαρώσει το ενεργό θέμα για μηνύματα που μπορούν να μεταφραστούν.', + 'scan_messages_process_limitations' => 'Ορισμένα μηνύματα ενδέχεται να μην καταγράφονται και θα εμφανίζονται μόνο μετά την πρώτη φορά που χρησιμοποιούνται.', + 'scan_messages_purge_label' => 'Εκκαθάριση πρώτα όλων των μηνυμάτων', + 'scan_messages_purge_help' => 'Επιλέγοντας εδώ θα διαγραφούν όλα τα μηνύματα, συμπεριλαμβανομένων και των μεταφράσεών τους, πριν από τη σάρωση.', + 'scan_messages_purge_confirm' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα μηνύματα; Αυτό δεν μπορεί να αναιρεθεί!', + 'scan_messages_purge_deleted_label' => 'Εκκαθάριση απολεσθέντων μηνυμάτων μετά τη σάρωση', + 'scan_messages_purge_deleted_help' => 'Επιλέγοντας εδώ, αφού ολοκληρωθεί η σάρωση, τυχόν μηνύματα που δεν βρήκε ο σαρωτής, συμπεριλαμβανομένων και των μεταφράσεών τους, θα διαγραφούν. Αυτό δεν μπορεί να αναιρεθεί!', + 'hint_translate' => 'Εδώ μπορείτε να μεταφράσετε μηνύματα που χρησιμοποιούνται στο front-end, τα πεδία θα αποθηκεύονται αυτόματα.', + 'hide_translated' => 'Απόκρυψη μετάφρασης', + 'export_messages_link' => 'Εξαγωγή μηνυμάτων', + 'import_messages_link' => 'Εισαγωγή μηνυμάτων', + 'not_found' => 'Δεν βρέθηκε', + 'found_help' => 'Εάν τυχόν σφάλματα παρουσιάστηκαν κατά τη σάρωση.', + 'found_title' => 'Σάρωση σφαλμάτων', + ], +]; \ No newline at end of file diff --git a/plugins/rainlab/translate/lang/hu/lang.php b/plugins/rainlab/translate/lang/hu/lang.php new file mode 100644 index 0000000..364613f --- /dev/null +++ b/plugins/rainlab/translate/lang/hu/lang.php @@ -0,0 +1,66 @@ + [ + 'name' => 'Fordítás', + 'description' => 'Többnyelvű weboldal létrehozását teszi lehetővé.', + 'tab' => 'Fordítás', + 'manage_locales' => 'Nyelvek kezelése', + 'manage_messages' => 'Szövegek fordítása' + ], + 'locale_picker' => [ + 'component_name' => 'Nyelvi választó', + 'component_description' => 'Legördülő menüt jelenít meg a nyelv kiválasztásához.' + ], + 'alternate_hreflang' => [ + 'component_name' => 'Nyelvi oldalak', + 'component_description' => 'A hreflang HTML meta sorok generálása a keresők számára.' + ], + 'locale' => [ + 'title' => 'Nyelvek', + 'update_title' => 'Nyelv frissítése', + 'create_title' => 'Nyelv hozzáadása', + 'select_label' => 'Nyelv választása', + 'default_suffix' => 'alapértelmezett', + 'unset_default' => 'Már a(z) ":locale" nyelv az alapértelmezett, így nem használható alapértelmezettként.', + 'delete_default' => 'A(z) ":locale" nyelv az alapértelmezett, így nem törölhető.', + 'disabled_default' => 'A(z) ":locale" nyelv letiltott, így nem állítható be alapértelmezettként.', + 'name' => 'Név', + 'code' => 'Kód', + 'is_default' => 'Alapértelmezett', + 'is_default_help' => 'Az alapértelmezett nyelv a fordítás előtti tartalmat képviseli.', + 'is_enabled' => 'Engedélyezve', + 'is_enabled_help' => 'A letiltott nyelvek nem lesznek elérhetőek a látogatói oldalon.', + 'not_available_help' => 'Nincsenek más beállított nyelvek.', + 'hint_locales' => 'Itt hozhat létre új nyelveket a látogatói oldal tartalmának lefordításához. Az alapértelmezett nyelv képviseli a fordítás előtti tartalmat.', + 'reorder_title' => 'Rendezés', + 'sort_order' => 'Sorrend' + ], + 'messages' => [ + 'title' => 'Szövegek', + 'description' => 'Nyelvi változatok menedzselése.', + 'clear_cache_link' => 'Gyorsítótár kiürítése', + 'clear_cache_loading' => 'A weboldal gyorsítótár kiürítése...', + 'clear_cache_success' => 'Sikerült a weboldal gyorsítótár kiürítése!', + 'clear_cache_hint' => 'Kattintson a Gyorsítótár kiürítése gombra, hogy biztosan láthatóvá váljanak a beírt módosítások a látogatói oldalon is.', + 'scan_messages_link' => 'Szövegek keresése', + 'scan_messages_begin_scan' => 'Keresés indítása', + 'scan_messages_loading' => 'Új szövegek keresése...', + 'scan_messages_success' => 'Sikerült a szövegek beolvasása!', + 'scan_messages_hint' => 'A Szövegek keresése gombra kattintva pedig beolvashatja a lefordítandó szövegeket.', + 'scan_messages_process' => 'A folyamat megkisérli beolvasni az aktív témában lévő lefordítandó szövegeket.', + 'scan_messages_process_limitations' => 'Néhány szöveg nem biztos, hogy azonnal meg fog jelenni a listában.', + 'scan_messages_purge_label' => 'Szövegek törlése a művelet előtt', + 'scan_messages_purge_help' => 'Amennyiben bejelöli, úgy minden szöveg törlésre kerül a beolvasást megelőzően.', + 'scan_messages_purge_confirm' => 'Biztos, hogy töröljük az összes szöveget?', + 'scan_messages_purge_deleted_label' => 'Törölje a hiányzó üzeneteket a vizsgálat után', + 'scan_messages_purge_deleted_help' => 'Ha be van jelölve, akkor a keresés befejezése után minden olyan üzenet törlődik, amelyet a keresés nem talált. A művelet nem visszavonható!', + 'hint_translate' => 'Itt fordíthatja le a látogatók által elérhető oldalon megjelenő szövegeket. A beírt változtatások automatikusan mentésre kerülnek.', + 'hide_translated' => 'Lefordítottak elrejtése', + 'export_messages_link' => 'Szövegek exportálása', + 'import_messages_link' => 'Szövegek importálása', + 'not_found' => 'Nem található', + 'found_help' => 'Történt-e hiba a keresés során.', + 'found_title' => 'Hibák', + ] +]; diff --git a/plugins/rainlab/translate/lang/it/lang.php b/plugins/rainlab/translate/lang/it/lang.php new file mode 100644 index 0000000..d1213b4 --- /dev/null +++ b/plugins/rainlab/translate/lang/it/lang.php @@ -0,0 +1,53 @@ + [ + 'name' => 'Traduci', + 'description' => 'Abilita siti multi-lingua.', + 'tab' => 'Traduzioni', + 'manage_locales' => 'Gestisci lingue', + 'manage_messages' => 'Gestisci messaggi', + ], + 'locale_picker' => [ + 'component_name' => 'Selezione lingua', + 'component_description' => 'Mostra un elenco a discesa per selezionare una delle lingue del sito web.', + ], + 'locale' => [ + 'title' => 'Gestisci lingue', + 'update_title' => 'Aggiorna lingua', + 'create_title' => 'Crea lingua', + 'select_label' => 'Scegli lingua', + 'default_suffix' => 'default', + 'unset_default' => '":locale" è già impostato come default e non si può modificare.', + 'delete_default' => '":locale" è la lingua di default e non può essere cancellata.', + 'disabled_default' => '":locale" è disabilitata e non può essere impostata come default.', + 'name' => 'Nome', + 'code' => 'Codice', + 'is_default' => 'Default', + 'is_default_help' => 'La lingua di default rappresenta il contenuto prima che sia tradotto.', + 'is_enabled' => 'Abilitata', + 'is_enabled_help' => 'Le lingue disabilitate non saranno visualizzate nel sito web.', + 'not_available_help' => 'Non ci sono altre lingue da impostare.', + 'hint_locales' => 'Crea qui le nuove lingue per tradurre i contenuti del sito web. La lingua di default rappresenta il contenuto prima che sia tradotto', + 'reorder_title' => 'Riordina lingue', + 'sort_order' => 'Criterio di ordinamento', + ], + 'messages' => [ + 'title' => 'Traduci messaggi', + 'description' => 'Aggiorna messaggi', + 'clear_cache_link' => 'Pulisci cache', + 'clear_cache_loading' => 'Pulizia della cache dell\'applicazione in corso...', + 'clear_cache_success' => 'La cache è stata pulita con successo!', + 'clear_cache_hint' => 'Potrebbe essere necessario cliccare il bottobe Pulisci cache per vedere i cambiamenti nel sito web.', + 'scan_messages_link' => 'Scansiona per nuovi messaggi', + 'scan_messages_begin_scan' => 'Inizio scansione', + 'scan_messages_loading' => 'Scansione in corso per nuovi messaggi...', + 'scan_messages_success' => 'File del template scansionati con successo!', + 'scan_messages_hint' => 'Cliccando Scansiona per nuovi messaggi avvierai la ricerca di nuovi messaggi da tradurre nel template attivo.', + 'scan_messages_process' => 'Questo processo cercherà di scansionare il tema attivo per trovare messaggi che possono essere tradotti.', + 'scan_messages_process_limitations' => 'Qualche messaggio potrebbe non essere individuato e apparirà solo dopo la prima volta che verrà usato.', + 'scan_messages_purge_label' => 'Rimuovi prima tutti i messaggi', + 'scan_messages_purge_help' => 'Se selezionato, prima di eseguire la scansione verranno eliminati tutti i messaggi già presenti.', + 'scan_messages_purge_confirm' => 'Sei sicuro di voler cancellare tutti i messaggi? Questa operazione non può essere annullata!', + 'hint_translate' => 'Qui puoi tradurre i messaggi usati nel sito web, i campi verranno salvati automaticamente.', + 'hide_translated' => 'Nascondi i messaggi tradotti', + ], +]; diff --git a/plugins/rainlab/translate/lang/nl/lang.php b/plugins/rainlab/translate/lang/nl/lang.php new file mode 100644 index 0000000..26823d8 --- /dev/null +++ b/plugins/rainlab/translate/lang/nl/lang.php @@ -0,0 +1,57 @@ + [ + 'name' => 'Vertaal', + 'description' => 'Stelt meerdere talen in voor een website.', + 'tab' => 'Vertalingen', + 'manage_locales' => 'Beheer talen', + 'manage_messages' => 'Beheer vertaalde berichten' + ], + 'locale_picker' => [ + 'component_name' => 'Taalkeuze menu', + 'component_description' => 'Geeft een taal keuzemenu weer om de taal te wijzigen voor de website.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatieve hrefLang elementen', + 'component_description' => 'Toont hreflang elementen voor de alt. talen' + ], + 'locale' => [ + 'title' => 'Beheer talen', + 'update_title' => 'Wijzig taal', + 'create_title' => 'Taal toevoegen', + 'select_label' => 'Selecteer taal', + 'default_suffix' => 'standaard', + 'unset_default' => '":locale" is de standaard taal en kan niet worden uitgeschakeld', + 'delete_default' => '":locale" is de standaard taal en kan niet worden verwijderd.', + 'disabled_default' => '":locale" is uitgeschakeld en kan niet als standaard taal worden ingesteld.', + 'name' => 'Naam', + 'code' => 'Code', + 'is_default' => 'Standaard', + 'is_default_help' => 'De standaard taal voor de inhoud.', + 'is_enabled' => 'Geactiveerd', + 'is_enabled_help' => 'Uitgeschakelde talen zijn niet beschikbaar op de website.', + 'not_available_help' => 'Er zijn geen andere talen beschikbaar.', + 'hint_locales' => 'Voeg hier nieuwe talen toe voor het vertalen van de website inhoud. De standaard taal geeft de inhoud weer voordat het is vertaald.', + 'reorder_title' => 'Talen rangschikken', + 'sort_order' => 'Volgorde', + ], + 'messages' => [ + 'title' => 'Vertaal berichten', + 'description' => 'Wijzig berichten', + 'clear_cache_link' => 'Leeg cache', + 'clear_cache_loading' => 'Applicatie cache legen...', + 'clear_cache_success' => 'De applicatie cache is succesvol geleegd.', + 'clear_cache_hint' => 'Het kan zijn dat het nodig is om op cache legen" te klikken om wijzigingen op de website te zien.', + 'scan_messages_link' => 'Zoek naar nieuwe berichten', + 'scan_messages_begin_scan' => 'Begin Scan', + 'scan_messages_loading' => 'Zoeken naar nieuwe berichten...', + 'scan_messages_success' => 'De thema bestanden zijn succesvol gescand!', + 'scan_messages_hint' => 'Klikken op Zoeken naar nieuwe berichten controleert de bestanden van het huidige thema op nieuwe berichten om te vertalen.', + 'scan_messages_process' => 'Dit proces zal proberen het huidige thema te scannen voor berichten om te vertalen', + 'scan_messages_process_limitations' => 'Sommige berichten zullen niet worden herkend en zullen pas verschijnen nadat ze voor de eerste keer zijn gebruikt', + 'scan_messages_purge_label' => 'Verwijder eerst alle berichten', + 'scan_messages_purge_help' => 'Als dit is aangevinkt zullen alle berichten worden verwijderd voordat de scan wordt uitgevoerd.', + 'scan_messages_purge_confirm' => 'Weet je zeker dat je alle berichten wilt verwijderen? Dit kan niet ongedaan gemaakt worden', + 'hint_translate' => 'Hier kan je berichten vertalen die worden gebruikt op de website. De velden worden automatisch opgeslagen.', + 'hide_translated' => 'Verberg vertaalde berichten', + ], +]; diff --git a/plugins/rainlab/translate/lang/pl/lang.php b/plugins/rainlab/translate/lang/pl/lang.php new file mode 100644 index 0000000..1b6e4af --- /dev/null +++ b/plugins/rainlab/translate/lang/pl/lang.php @@ -0,0 +1,66 @@ + [ + 'name' => 'Tłumaczenia', + 'description' => 'Umożliwia tworzenie stron wielojęzycznych.', + 'tab' => 'Tłumaczenie', + 'manage_locales' => 'Zarządzaj językami', + 'manage_messages' => 'Zarządzaj treścią' + ], + 'locale_picker' => [ + 'component_name' => 'Lista języków', + 'component_description' => 'Wyświetla menu wyboru języków strony.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatywne ustawienia hreflang', + 'component_description' => 'Ustawia alternatywne języki dla strony jako parametry hreflang' + ], + 'locale' => [ + 'title' => 'Zarządzaj językami', + 'update_title' => 'Edytuj język', + 'create_title' => 'Stwórz język', + 'select_label' => 'Wybierz język', + 'default_suffix' => 'domyślny', + 'unset_default' => 'Język ":locale" jest już domyślny i nie można go zmienić.', + 'delete_default' => 'Język ":locale" jest już domyślny i nie może zostać usunięty.', + 'disabled_default' => 'Język ":locale" jest wyłączony i nie można go ustawić jako domyślny.', + 'name' => 'Nazwa', + 'code' => 'Kod', + 'is_default' => 'Domyślny', + 'is_default_help' => 'Domyślny język to język treści strony przed tłumaczeniem.', + 'is_enabled' => 'Włączony', + 'is_enabled_help' => 'Wyłączone języki nie będą dostępne na stronie.', + 'not_available_help' => 'Nie skonfigurowano innych języków.', + 'hint_locales' => 'Stwórz nowe języki, na które chcesz tłumaczyć treść strony. Domyślny język to język treści strony przed tłumaczeniem. ', + 'reorder_title' => 'Zmień kolejność języków', + 'sort_order' => 'Sortowanie', + ], + 'messages' => [ + 'title' => 'Tłumacz Treść', + 'description' => 'Tłumaczenie treści strony', + 'clear_cache_link' => 'Wyczyść Cache', + 'clear_cache_loading' => 'Czyszczenie cache...', + 'clear_cache_success' => 'Pomyślnie wyczyszczono cache aplikacji!', + 'clear_cache_hint' => 'Jeśli nie widzisz zmian na stronie, kliknij przycisk Wyczyść cache.', + 'scan_messages_link' => 'Skanuj treść', + 'scan_messages_begin_scan' => 'Rozpocznij skanowanie', + 'scan_messages_loading' => 'Szukanie nowych pozycji...', + 'scan_messages_success' => 'Skanowanie plików motywu zakończyło się powodzeniem!', + 'scan_messages_hint' => 'Kliknięcie przycisku Skanuj treść rozpocznie skanowanie w poszukiwaniu nowych pozycji do przetłumaczenia.', + 'scan_messages_process' => 'Ten proces podejmie próbę przeskanowania aktywnego motywu w poszukiwaniu wiadomości, które można przetłumaczyć.', + 'scan_messages_process_limitations' => 'Niektóre wiadomości mogą nie zostać przechwycone i pojawią się dopiero po pierwszym użyciu.', + 'scan_messages_purge_label' => 'Najpierw usuń wszystkie wiadomości', + 'scan_messages_purge_help' => 'Zaznaczenie tej opcji spowoduje usunięcie wszystkich wiadomości, w tym ich tłumaczeń, przed wykonaniem skanowania.', + 'scan_messages_purge_confirm' => 'Czy jesteś pewny że chcesz usunąć wszystkie wiadomości? Po usunięciu nie będzie można ich przywrócić', + 'scan_messages_purge_deleted_label' => 'Usuń utracone wiadomości po zeskanowaniu', + 'scan_messages_purge_deleted_help' => 'Jeśli ta opcja jest zaznaczona, po zakończeniu skanowania wszystkie wiadomości, których skaner nie znalazł (w tym ich tłumaczenia) zostaną usunięte. Po zaznaczeniu tej opcji nie będzie możliwości przywrócenia zmian!', + 'hint_translate' => 'Możesz tu przetłumaczyć treść strony. Pola zapisują się automatycznie.', + 'hide_translated' => 'Ukryj przetłumaczone', + 'export_messages_link' => 'Wyeksportuj treść', + 'import_messages_link' => 'Zaimportuj treść', + 'not_found' => 'Nie znaleziono', + 'found_help' => 'Wystąpiły błędy podczas skanowania', + 'found_title' => 'Błąd skanowania', + ], +]; diff --git a/plugins/rainlab/translate/lang/pt-br/lang.php b/plugins/rainlab/translate/lang/pt-br/lang.php new file mode 100644 index 0000000..e1995f9 --- /dev/null +++ b/plugins/rainlab/translate/lang/pt-br/lang.php @@ -0,0 +1,53 @@ + [ + 'name' => 'Traduções', + 'description' => 'Permite sites com multi-idiomas.', + 'tab' => 'Tradução', + 'manage_locales' => 'Gerenciar locais', + 'manage_messages' => 'Gerenciar mensagens' + ], + 'locale_picker' => [ + 'component_name' => 'Seleção de idiomas', + 'component_description' => 'Exibe um campo de seleção de idiomas.', + ], + 'locale' => [ + 'title' => 'Gerenciar idiomas', + 'update_title' => 'Atualizar idioma', + 'create_title' => 'Criar idioma', + 'select_label' => 'Selecionar idioma', + 'default_suffix' => 'padrão', + 'unset_default' => '":locale" é o idioma padrão e não pode ser desativado.', + 'delete_default' => '":locale" é o padrão e não pode ser excluído.', + 'disabled_default' => '":locale" está desativado e não pode ser definido como padrão.', + 'name' => 'Nome', + 'code' => 'Código', + 'is_default' => 'Padrão', + 'is_default_help' => 'O idioma padrão apresenta o conteúdo antes das traduções.', + 'is_enabled' => 'Ativo', + 'is_enabled_help' => 'Idiomas desativados não estarão disponíveis na página.', + 'not_available_help' => 'Não há outros idiomas configurados.', + 'hint_locales' => 'Crie novos idiomas para traduzir o conteúdo da página. O idioma padrão apresenta o conteúdo antes das traduções.', + ], + 'messages' => [ + 'title' => 'Traduzir mensagens', + 'description' => 'Atualizar mensagens', + 'clear_cache_link' => 'Limpar cache', + 'clear_cache_loading' => 'Limpando o cache da aplicação...', + 'clear_cache_success' => 'Cache da aplicação limpo com sucesso!', + 'clear_cache_hint' => 'Talvez você terá que clicar em Limpar cache para visualizar as modificações na página.', + 'scan_messages_link' => 'Buscar por mensagens', + 'scan_messages_begin_scan' => 'Inciar Busca', + 'scan_messages_loading' => 'Buscando por novas mensagens...', + 'scan_messages_success' => 'Busca por novas mensagens nos arquivos concluída com sucesso!', + 'scan_messages_hint' => 'Clicando em Buscar por mensagens o sistema buscará por qualquer mensagem da aplicação que possa ser traduzida.', + 'scan_messages_process' => 'Este processo tentará scanear o tema ativo para mensagens que podem ser traduzidas.', + 'scan_messages_process_limitations' => 'Algumas mensagens podem não ser capturadas e só aparecerão depois da primeira vez que forem usadas.', + 'scan_messages_purge_label' => 'Limpar primeiro todas as mensagens', + 'scan_messages_purge_help' => 'Se selecionado, isso excluirá todas as mensagens antes de executar a verificação.', + 'scan_messages_purge_confirm' => 'Tem certeza de que deseja excluir todas as mensagens? Isto não pode ser desfeito!', + 'hint_translate' => 'Aqui você pode raduzir as mensagens utilizadas na página, os campos são salvos automaticamente.', + 'hide_translated' => 'Ocultar traduzidas', + ], +]; diff --git a/plugins/rainlab/translate/lang/ru/lang.php b/plugins/rainlab/translate/lang/ru/lang.php new file mode 100644 index 0000000..a3b9177 --- /dev/null +++ b/plugins/rainlab/translate/lang/ru/lang.php @@ -0,0 +1,61 @@ + [ + 'name' => 'Translate', + 'description' => 'Настройки мультиязычности сайта.', + 'tab' => 'Перевод', + 'manage_locales' => 'Управление языками', + 'manage_messages' => 'Управление сообщениями' + ], + 'locale_picker' => [ + 'component_name' => 'Выбор языка', + 'component_description' => 'Просмотр списка языков интерфейса.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Альтернативные элементы hrefLang', + 'component_description' => 'Внедряет языковые альтернативы для страницы в качестве элементов hreflang' + ], + 'locale' => [ + 'title' => 'Управление языками', + 'update_title' => 'Обновить язык', + 'create_title' => 'Создать язык', + 'select_label' => 'Выбрать язык', + 'default_suffix' => 'По умолчанию', + 'unset_default' => '":locale" уже установлен как язык по умолчанию.', + 'delete_default' => '":locale" используется по умолчанию и не может быть удален.', + 'disabled_default' => '":locale" отключен и не может быть использован как язык по умолчанию.', + 'name' => 'Название', + 'code' => 'Код', + 'is_default' => 'По умолчанию', + 'is_default_help' => 'Использовать этот язык, как язык по умолчанию.', + 'is_enabled' => 'Включено', + 'is_enabled_help' => 'Сделать язык доступным в интерфейсе сайта.', + 'not_available_help' => 'Нет настроек других языков.', + 'hint_locales' => 'Создание новых переводов содержимого интерфейса сайта.', + 'reorder_title' => 'Изменить порядок языков', + 'sort_order' => 'Порядок сортировки', + ], + 'messages' => [ + 'title' => 'Перевод сообщений', + 'description' => 'Перевод статических сообщений в шаблоне', + 'clear_cache_link' => 'Очистить кэш', + 'clear_cache_loading' => 'Очистка кэша приложения...', + 'clear_cache_success' => 'Очистка кэша завершена успешно!', + 'clear_cache_hint' => 'Используйте кнопку Очистить кэш, чтобы увидеть изменения в интерфейсе сайта.', + 'scan_messages_link' => 'Сканирование сообщений', + 'scan_messages_begin_scan' => 'Начать сканирование', + 'scan_messages_loading' => 'Сканирование наличия новых сообщений...', + 'scan_messages_success' => 'Сканирование файлов шаблона темы успешно завершено!', + 'scan_messages_hint' => 'Используйте кнопку Сканирование сообщений для поиска новых ключей перевода активной темы интерфейса сайта.', + 'scan_messages_process' => 'Этот процесс попытается отсканировать активную тему для сообщений, которые можно перевести.', + 'scan_messages_process_limitations' => 'Некоторые сообщения могут не быть отсканированы и появлятся только после первого использования.', + 'scan_messages_purge_label' => 'Сначала очистить все сообщения', + 'scan_messages_purge_help' => 'Если этот флажок установлен, это приведет к удалению всех сообщений перед выполнением сканирования.', + 'scan_messages_purge_confirm' => 'Вы действительно хотите удалить все сообщения? Операция не может быть отменена!', + 'hint_translate' => 'Здесь вы можете переводить сообщения, которые используются в интерфейсе сайта.', + 'hide_translated' => 'Скрыть перевод', + 'export_messages_link' => 'Экспорт сообщений', + 'import_messages_link' => 'Импорт сообщений', + ], +]; diff --git a/plugins/rainlab/translate/lang/sk/lang.php b/plugins/rainlab/translate/lang/sk/lang.php new file mode 100644 index 0000000..82b4827 --- /dev/null +++ b/plugins/rainlab/translate/lang/sk/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Preklady', + 'description' => 'Umožňuje viacjazyčné webové stránky.', + 'tab' => 'Preklad', + 'manage_locales' => 'Spravovať jazyky', + 'manage_messages' => 'Spravovať správy', + ], + 'locale_picker' => [ + 'component_name' => 'Výber jazyka', + 'component_description' => 'Zobrazí rozbaľovaciu ponuku na výber jazyka front-endu.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatívne prvky hrefLang', + 'component_description' => 'Vloží jazykové alternatívy pre stránku ako prvky hreflang' + ], + 'locale' => [ + 'title' => 'Spravovať jazyky', + 'update_title' => 'Aktualizovať jazyk', + 'create_title' => 'Vytvoriť jazyk', + 'select_label' => 'Zvoliť jazyk', + 'default_suffix' => 'predvolený', + 'unset_default' => '":locale" je už predvolený a nemožno ho nastaviť ako predvolený.', + 'delete_default' => '":locale" je predvolený a nemôže byť zmazaný.', + 'disabled_default' => '":locale" je neaktívny a nemôýe byť nastavený ako predvolený.', + 'name' => 'Meno', + 'code' => 'Kód', + 'is_default' => 'Predvolený', + 'is_default_help' => 'Predvolený jazyk predstavuje obsah pred prekladom.', + 'is_enabled' => 'Aktívny', + 'is_enabled_help' => 'Neaktívne jazyky nebudú dostupné na front-ende.', + 'not_available_help' => 'Nie sú nastavené žiadne ďalšie jazyky.', + 'hint_locales' => 'Vytvárajte tu nové jazyky pre preklad obsahu front-endu. Predvolený jazyk predstavuje obsah pred tým, než bol preložený.', + 'reorder_title' => 'Zmeniť poradie jazykov', + 'sort_order' => 'Smer zoradenia', + ], + 'messages' => [ + 'title' => 'Preložiť správy', + 'description' => 'Aktualizovať správy', + 'clear_cache_link' => 'Vymazať vyrovnávaciu pamäť', + 'clear_cache_loading' => 'Čistenie vyrovnávacej pamäte aplikácie...', + 'clear_cache_success' => 'Vyrovnávacia pamäť aplikácie vyčistená!', + 'clear_cache_hint' => 'Možno budete musieť kliknúť Vymazať vyrovnávaciu pamäť aby sa zmeny prejavili na front-ende.', + 'scan_messages_link' => 'Vyhľadávanie správ', + 'scan_messages_begin_scan' => 'Začať vyhľadávanie', + 'scan_messages_loading' => 'Vyhľadávanie nových správ...', + 'scan_messages_success' => 'Vyhľadávanie nových správ úspešne ukončené!', + 'scan_messages_hint' => 'Kliknutie na Vyhľadávanie správ skontroluje súbory aktívnej témy a nájde nové správy na preklad.', + 'scan_messages_process' => 'Tento proces vyhľadí v aktívnej téme správy, ktoré môžu byť preložené.', + 'scan_messages_process_limitations' => 'Niektoré správy nemusia byť zachytené a objavia sa po ich prvom použití.', + 'scan_messages_purge_label' => 'Najprv vyčistiť všetky správy', + 'scan_messages_purge_help' => 'Ak zaškrtnuté zmaže všetky správy pred vykonaním vyhľadávania.', + 'scan_messages_purge_confirm' => 'Naozaj chcete odstrániť všetky správy? Toto sa nedá vrátiť späť!', + 'hint_translate' => 'Tu môžete preložiť správy používané na front-ende, polia sa ukladajú automaticky.', + 'hide_translated' => 'Skryť preložené', + 'export_messages_link' => 'Exportovať správy', + 'import_messages_link' => 'Importovať správy', + ], +]; diff --git a/plugins/rainlab/translate/lang/sl/lang.php b/plugins/rainlab/translate/lang/sl/lang.php new file mode 100644 index 0000000..db99d8e --- /dev/null +++ b/plugins/rainlab/translate/lang/sl/lang.php @@ -0,0 +1,59 @@ + [ + 'name' => 'Večjezičnost', + 'description' => 'Podpora za večjezične spletne strani.', + 'tab' => 'Večjezičnost', + 'manage_locales' => 'Upravljanje jezikov', + 'manage_messages' => 'Upravljanje besedil', + ], + 'locale_picker' => [ + 'component_name' => 'Izbirnik jezikov', + 'component_description' => 'Prikaže spustni seznam jezikov na spletni strani.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Jezikovne alternative', + 'component_description' => 'Vstavi jezikovne alternative za stran kot elemente "hreflang".', + ], + 'locale' => [ + 'title' => 'Upravljanje jezikov', + 'update_title' => 'Posodobitev jezika', + 'create_title' => 'Ustvari nov jezik', + 'select_label' => 'Izberi jezik', + 'default_suffix' => 'privzeto', + 'unset_default' => '":locale" je že privzeti jezik in ga ni možno nastaviti za ne-privzetega.', + 'delete_default' => '":locale" je privzeti jezik in se ga ne da izbrisati.', + 'disabled_default' => '":locale" je onemogočen jezik in ga ni možno nastaviti za privzetega.', + 'name' => 'Ime', + 'code' => 'Koda', + 'is_default' => 'Privzeto', + 'is_default_help' => 'Privzeti jezik predstavlja vsebino pred prevodom.', + 'is_enabled' => 'Omogočeno', + 'is_enabled_help' => 'Onemogočeni jeziki na spletni strani ne bodo na voljo.', + 'not_available_help' => 'Ni nastavljenih drugih jezikov.', + 'hint_locales' => 'Tukaj lahko ustvarite nove jezike za prevajanje vsebine. Privzeti jezik predstavlja vsebino, preden je prevedena.', + 'reorder_title' => 'Spremeni vrstni red jezikov', + 'sort_order' => 'Vrstni red', + ], + 'messages' => [ + 'title' => 'Prevajanje besedil', + 'description' => 'Urejanje prevodov besedil.', + 'clear_cache_link' => 'Počisti predpomnilnik', + 'clear_cache_loading' => 'Praznenje predpomnilnika aplikacije...', + 'clear_cache_success' => 'Predpomnilnik aplikacije je uspešno izpraznjen!', + 'clear_cache_hint' => 'Morda boste morali klikniti Počisti predpomnilnik za ogled sprememb na spletni strani.', + 'scan_messages_link' => 'Skeniraj besedila', + 'scan_messages_begin_scan' => 'Začni skeniranje', + 'scan_messages_loading' => 'Skeniram za nova besedila...', + 'scan_messages_success' => 'Datoteke aktivne teme so bile uspešno skenirane!', + 'scan_messages_hint' => 'Klik na Skeniraj besedila bo preveril datoteke aktivne teme, če vsebujejo morebitna besedila za prevode.', + 'scan_messages_process' => 'Ta postopek bo poskusil skenirati aktivno temo za besedila, ki jih je mogoče prevesti.', + 'scan_messages_process_limitations' => 'Nekatera besedila morda ne bodo prikazana in se bodo prikazala šele po prvi uporabi.', + 'scan_messages_purge_label' => 'Najprej izbriši vsa besedila', + 'scan_messages_purge_help' => 'Če je označeno, bodo pred skeniranjem izbrisana vsa besedila, vključno z njihovimi prevodi!', + 'scan_messages_purge_confirm' => 'Ali ste prepričani, da želite izbrisati vsa besedila? Tega ukaza ni mogoče razveljaviti!', + 'hint_translate' => 'Tukaj lahko prevedete besedila, uporabljena na spletni strani. Vrednosti v poljih se shranijo samodejno.', + 'hide_translated' => 'Skrij prevedena besedila', + 'export_messages_link' => 'Izvozi besedila', + 'import_messages_link' => 'Uvozi besedila', + ], +]; diff --git a/plugins/rainlab/translate/lang/tr/lang.php b/plugins/rainlab/translate/lang/tr/lang.php new file mode 100644 index 0000000..5a20147 --- /dev/null +++ b/plugins/rainlab/translate/lang/tr/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => 'Çeviri', + 'description' => 'Çoklu dil destekli websiteleri oluşturmanızı sağlar.', + 'tab' => 'Çeviri', + 'manage_locales' => 'Dilleri yönet', + 'manage_messages' => 'Çevirileri yönet', + ], + 'locale_picker' => [ + 'component_name' => 'Çoklu Dil Seçimi', + 'component_description' => 'Sitenizin dilini değiştirebileceğiniz diller listesini gösterir.', + ], + 'alternate_hreflang' => [ + 'component_name' => 'Alternatif hrefLang elemanları', + 'component_description' => 'Sayfa için dil alternatiflerini hreflang elemanları olarak ekler.', + ], + 'locale' => [ + 'title' => 'Dilleri yönet', + 'update_title' => 'Dili güncelle', + 'create_title' => 'Dil ekle', + 'select_label' => 'Dil seç', + 'default_suffix' => 'ön tanımlı', + 'unset_default' => '":locale" zaten ön tanımlı olarak seçili.', + 'delete_default' => '":locale" ön tanımlı olarak seçilmiş olduğu için silinemez.', + 'disabled_default' => '":locale" pasifleştirilmiş olduğu için ön tanımlı yapılamaz.', + 'name' => 'Dil İsmi', + 'code' => 'Dil Kodu', + 'is_default' => 'Ön tanımlı', + 'is_default_help' => 'Ön tanımlı seçilen dil, sitenin orjinal içeriğini belirtmektedir.', + 'is_enabled' => 'Aktif', + 'is_enabled_help' => 'Pasifleştirilen diller site ön yüzünde görüntülenmez.', + 'not_available_help' => 'Başka dil ayarı yok.', + 'hint_locales' => 'Ön yüz çevirilerini yapmak için buradan dil ekleyebilirsiniz. Ön tanımlı seçilen dil, sitenin orjinal içeriğini belirtmektedir.', + 'reorder_title' => 'Dilleri sırala', + 'sort_order' => 'Sıralama', + ], + 'messages' => [ + 'title' => 'Metinleri çevir', + 'description' => 'Metinler ve çevirileri', + 'clear_cache_link' => 'Önbelleği temizle', + 'clear_cache_loading' => 'Önbellek temizleniyor..', + 'clear_cache_success' => 'Önbellek temizlendi!', + 'clear_cache_hint' => 'Yaptığınız çeviriler site ön yüzünde görünmüyorsa Önbelleği temizle butonuna tıklayabilirsiniz.', + 'scan_messages_link' => 'Yeni metinleri tara', + 'scan_messages_begin_scan' => 'Taramaya başla', + 'scan_messages_loading' => 'Yeni metinler taranıyor...', + 'scan_messages_success' => 'Tema dosyaları tarandı!', + 'scan_messages_hint' => 'Yeni metinleri tara butonuna tıklayarak tema içerisine yeni eklediğiniz metinleri de çevirebilirsiniz.', + 'scan_messages_process' => 'Bu işlem tema dosyaları içerisindeki çevrilecek metinleri tarar.', + 'scan_messages_process_limitations' => 'Bazı metinler yakalanamayabilir veya ilk kez kullanımdan sonra görüntülenebilir.', + 'scan_messages_purge_label' => 'Önce tüm eski çevirileri sil', + 'scan_messages_purge_help' => 'Eğer seçerseniz şimdiye kadar yaptığınız tüm çeviriler silinecek, tekrar çevirmeniz gerekecektir.', + 'scan_messages_purge_confirm' => 'Tüm çevirileri silmek istediğinize emin misiniz? Bu işlem geri alınamaz!', + 'scan_messages_purge_deleted_label' => 'Tarama tamamlandıktan sonra eksik çevirileri temizle', + 'scan_messages_purge_deleted_help' => 'İşaretlerseniz, tarama tamamlandıktan sonra tarayıcının bulamadığı tüm metinler (diğer dil çevirileri de dahil olmak üzere) silinecektir. Bu işlem geri alınamaz!', + 'hint_translate' => 'Bu kısımda site ön yüzünde görüntülenecek çeviri metinlerini bulabilirsiniz, çeviri yaptıktan sonra bir işlem yapmanıza gerek yoktur, hepsi otomatik kaydedilecek.', + 'hide_translated' => 'Çevrilen metinleri gizle', + 'export_messages_link' => 'Metinleri Dışa Aktar', + 'import_messages_link' => 'Metinleri İçe Aktar', + 'not_found' => 'Bulunamadı', + 'found_help' => 'Tarama sırasında herhangi bir hata oluşup oluşmadığı.', + 'found_title' => 'Tarama hataları', + ], +]; diff --git a/plugins/rainlab/translate/lang/zh-cn/lang.php b/plugins/rainlab/translate/lang/zh-cn/lang.php new file mode 100644 index 0000000..9ed1aa0 --- /dev/null +++ b/plugins/rainlab/translate/lang/zh-cn/lang.php @@ -0,0 +1,64 @@ + [ + 'name' => '翻译', + 'description' => '启用多语言网站。', + 'tab' => '翻译', + 'manage_locales' => '管理语言环境', + 'manage_messages' => '管理消息', + ], + 'locale_picker' => [ + 'component_name' => '语言环境选择器', + 'component_description' => '显示用于选择前端语言的下拉列表。', + ], + 'alternate_hreflang' => [ + 'component_name' => '备用 hrefLang 元素', + 'component_description' => '将页面的语言替代项作为 hreflang 元素注入' + ], + 'locale' => [ + 'title' => '管理语言', + 'update_title' => '更新语言', + 'create_title' => '创建语言', + 'select_label' => '选择语言', + 'default_suffix' => '默认', + 'unset_default' => '":locale" 已经是默认值,不能取消设置为默认值。', + 'delete_default' => '":locale" 是默认值,不能删除。', + 'disabled_default' => '":locale" 已禁用且不能设置为默认值。', + 'name' => '名称', + 'code' => '代码', + 'is_default' => '默认', + 'is_default_help' => '默认语言代表翻译前的内容。', + 'is_enabled' => '启用', + 'is_enabled_help' => '禁用的语言在前端将不可用。', + 'not_available_help' => '没有设置其他语言。', + 'hint_locales' => '在此处创建用于翻译前端内容的新语言。默认语言代表翻译之前的内容。', + 'reorder_title' => '重新排序语言', + 'sort_order' => '排序顺序', + ], + 'messages' => [ + 'title' => '翻译消息', + 'description' => '更新消息', + 'clear_cache_link' => '清除缓存', + 'clear_cache_loading' => '清除应用程序缓存...', + 'clear_cache_success' => '清除应用缓存成功!', + 'clear_cache_hint' => '您可能需要点击清除缓存才能查看前端的更改。', + 'scan_messages_link' => '扫描消息', + 'scan_messages_begin_scan' => '开始扫描', + 'scan_messages_loading' => '正在扫描新消息...', + 'scan_messages_success' => '已成功扫描主题模板文件!', + 'scan_messages_hint' => '点击扫描消息将检查活动主题文件中是否有任何要翻译的新消息。', + 'scan_messages_process' => '此进程将尝试扫描活动主题以查找可翻译的消息。', + 'scan_messages_process_limitations' => '有些消息可能无法捕获,只有在第一次使用后才会出现。', + 'scan_messages_purge_label' => '先清除所有消息', + 'scan_messages_purge_help' => '如果选中,这将在执行扫描之前删除所有消息,包括它们的翻译。', + 'scan_messages_purge_confirm' => '您确定要删除所有消息吗?这不能被撤消!', + 'scan_messages_purge_deleted_label' => '扫描后清除丢失的消息', + 'scan_messages_purge_deleted_help' => '如果选中,在扫描完成后,扫描器未找到的任何消息,包括它们的翻译,都将被删除。这不能被撤消!', + 'hint_translate' => '这里可以翻译前端使用的消息,字段会自动保存。', + 'hide_translated' => '隐藏翻译', + 'export_messages_link' => '导出消息', + 'import_messages_link' => '导入消息', + 'not_found' => '未找到', + 'found_help' => '扫描过程中是否发生任何错误。', + 'found_title' => '扫描错误', + ], +]; diff --git a/plugins/rainlab/translate/models/Attribute.php b/plugins/rainlab/translate/models/Attribute.php new file mode 100644 index 0000000..a6e7a41 --- /dev/null +++ b/plugins/rainlab/translate/models/Attribute.php @@ -0,0 +1,18 @@ + [] + ]; +} diff --git a/plugins/rainlab/translate/models/Locale.php b/plugins/rainlab/translate/models/Locale.php new file mode 100644 index 0000000..938643c --- /dev/null +++ b/plugins/rainlab/translate/models/Locale.php @@ -0,0 +1,218 @@ + 'required', + 'name' => 'required', + ]; + + public $timestamps = false; + + /** + * @var array Object cache of self, by code. + */ + protected static $cacheByCode = []; + + /** + * @var array A cache of enabled locales. + */ + protected static $cacheListEnabled; + + /** + * @var array A cache of available locales. + */ + protected static $cacheListAvailable; + + /** + * @var self Default locale cache. + */ + protected static $defaultLocale; + + public function afterCreate() + { + if ($this->is_default) { + $this->makeDefault(); + } + } + + public function beforeDelete() + { + if ($this->is_default) { + throw new ApplicationException(Lang::get('rainlab.translate::lang.locale.delete_default', ['locale'=>$this->name])); + } + } + + public function beforeUpdate() + { + if ($this->isDirty('is_default')) { + $this->makeDefault(); + + if (!$this->is_default) { + throw new ValidationException(['is_default' => Lang::get('rainlab.translate::lang.locale.unset_default', ['locale'=>$this->name])]); + } + } + } + + /** + * Makes this model the default + * @return void + */ + public function makeDefault() + { + if (!$this->is_enabled) { + throw new ValidationException(['is_enabled' => Lang::get('rainlab.translate::lang.locale.disabled_default', ['locale'=>$this->name])]); + } + + $this->newQuery()->where('id', $this->id)->update(['is_default' => true]); + $this->newQuery()->where('id', '<>', $this->id)->update(['is_default' => false]); + } + + /** + * Returns the default locale defined. + * @return self + */ + public static function getDefault() + { + if (self::$defaultLocale !== null) { + return self::$defaultLocale; + } + + if ($forceDefault = Config::get('rainlab.translate::forceDefaultLocale')) { + $locale = new self; + $locale->name = $locale->code = $forceDefault; + $locale->is_default = $locale->is_enabled = true; + return self::$defaultLocale = $locale; + } + + return self::$defaultLocale = self::where('is_default', true) + ->remember(1440, 'rainlab.translate.defaultLocale') + ->first() + ; + } + + /** + * Locate a locale table by its code, cached. + * @param string $code + * @return Model + */ + public static function findByCode($code = null) + { + if (!$code) { + return null; + } + + if (isset(self::$cacheByCode[$code])) { + return self::$cacheByCode[$code]; + } + + return self::$cacheByCode[$code] = self::whereCode($code)->first(); + } + + /** + * Scope for checking if model is enabled + * @param Builder $query + * @return Builder + */ + public function scopeIsEnabled($query) + { + return $query->where('is_enabled', true); + } + + /** + * Scope for ordering the locales + * @param Builder $query + * @return Builder + */ + public function scopeOrder($query) + { + return $query + ->orderBy('sort_order', 'asc') + ; + } + + /** + * Returns true if there are at least 2 locales available. + * @return boolean + */ + public static function isAvailable() + { + return count(self::listAvailable()) > 1; + } + + /** + * Lists available locales, used on the back-end. + * @return array + */ + public static function listAvailable() + { + if (self::$cacheListAvailable) { + return self::$cacheListAvailable; + } + + return self::$cacheListAvailable = self::order()->pluck('name', 'code')->all(); + } + + /** + * Lists the enabled locales, used on the front-end. + * @return array + */ + public static function listEnabled() + { + if (self::$cacheListEnabled) { + return self::$cacheListEnabled; + } + + $expiresAt = now()->addMinutes(1440); + $isEnabled = Cache::remember('rainlab.translate.locales', $expiresAt, function() { + return self::isEnabled()->order()->pluck('name', 'code')->all(); + }); + + return self::$cacheListEnabled = $isEnabled; + } + + /** + * Returns true if the supplied locale is valid. + * @return boolean + */ + public static function isValid($locale) + { + $languages = array_keys(Locale::listEnabled()); + + return in_array($locale, $languages); + } + + /** + * Clears all cache keys used by this model + * @return void + */ + public static function clearCache() + { + Cache::forget('rainlab.translate.locales'); + Cache::forget('rainlab.translate.defaultLocale'); + self::$cacheListEnabled = null; + self::$cacheListAvailable = null; + self::$cacheByCode = []; + } +} diff --git a/plugins/rainlab/translate/models/MLFile.php b/plugins/rainlab/translate/models/MLFile.php new file mode 100644 index 0000000..d3f04c3 --- /dev/null +++ b/plugins/rainlab/translate/models/MLFile.php @@ -0,0 +1,24 @@ +getFormFields() as $id => $field) { + if (!empty($field['translatable'])) { + $this->translatable[] = $id; + } + } + } +} diff --git a/plugins/rainlab/translate/models/Message.php b/plugins/rainlab/translate/models/Message.php new file mode 100644 index 0000000..e468585 --- /dev/null +++ b/plugins/rainlab/translate/models/Message.php @@ -0,0 +1,311 @@ +forLocale(Lang::getLocale()); + } + + /** + * Gets a message for a given locale, or the default. + * @param string $locale + * @return string + */ + public function forLocale($locale = null, $default = null) + { + if ($locale === null) { + $locale = self::DEFAULT_LOCALE; + } + + if (!array_key_exists($locale, $this->message_data)) { + // search parent locale (e.g. en-US -> en) before returning default + list($locale) = explode('-', $locale); + } + + if (array_key_exists($locale, $this->message_data)) { + return $this->message_data[$locale]; + } + + return $default; + } + + /** + * Writes a translated message to a locale. + * @param string $locale + * @param string $message + * @return void + */ + public function toLocale($locale = null, $message = null) + { + if ($locale === null) { + return; + } + + $data = $this->message_data; + $data[$locale] = $message; + + if (!$message) { + unset($data[$locale]); + } + + $this->message_data = $data; + $this->save(); + } + + /** + * Creates or finds an untranslated message string. + * @param string $messageId + * @param string $locale + * @return string + */ + public static function get($messageId, $locale = null) + { + $locale = $locale ?: self::$locale; + if (!$locale) { + return $messageId; + } + + $messageCode = static::makeMessageCode($messageId); + + /* + * Found in cache + */ + if (array_key_exists($locale . $messageCode, self::$cache)) { + return self::$cache[$locale . $messageCode]; + } + + /* + * Uncached item + */ + $item = static::firstOrNew([ + 'code' => $messageCode + ]); + + /* + * Create a default entry + */ + if (!$item->exists) { + $data = [static::DEFAULT_LOCALE => $messageId]; + $item->message_data = $item->message_data ?: $data; + $item->save(); + } + + /* + * Schedule new cache and go + */ + $msg = $item->forLocale($locale, $messageId); + self::$cache[$locale . $messageCode] = $msg; + self::$hasNew = true; + + return $msg; + } + + /** + * Import an array of messages. Only known messages are imported. + * @param array $messages + * @param string $locale + * @return void + */ + public static function importMessages($messages, $locale = null) + { + self::importMessageCodes(array_combine($messages, $messages), $locale); + } + + /** + * Import an array of messages. Only known messages are imported. + * @param array $messages + * @param string $locale + * @return void + */ + public static function importMessageCodes($messages, $locale = null) + { + if ($locale === null) { + $locale = static::DEFAULT_LOCALE; + } + + $existingIds = []; + + foreach ($messages as $code => $message) { + // Ignore empties + if (!strlen(trim($message))) { + continue; + } + + $code = static::makeMessageCode($code); + + $item = static::firstOrNew([ + 'code' => $code + ]); + + // Do not import non-default messages that do not exist + if (!$item->exists && $locale != static::DEFAULT_LOCALE) { + continue; + } + + $messageData = $item->exists || $item->message_data ? $item->message_data : []; + + // Do not overwrite existing translations. + if (isset($messageData[$locale])) { + $existingIds[] = $item->id; + continue; + } + + $messageData[$locale] = $message; + + $item->message_data = $messageData; + $item->found = true; + + $item->save(); + } + + // Set all messages found by the scanner as found + self::whereIn('id', $existingIds)->update(['found' => true]); + } + + /** + * Looks up and translates a message by its string. + * @param string $messageId + * @param array $params + * @param string $locale + * @return string + */ + public static function trans($messageId, $params = [], $locale = null) + { + $msg = static::get($messageId, $locale); + + $params = array_build($params, function($key, $value){ + return [':'.$key, e($value)]; + }); + + $msg = strtr($msg, $params); + + return $msg; + } + + /** + * Looks up and translates a message by its string WITHOUT escaping params. + * @param string $messageId + * @param array $params + * @param string $locale + * @return string + */ + public static function transRaw($messageId, $params = [], $locale = null) + { + $msg = static::get($messageId, $locale); + + $params = array_build($params, function($key, $value){ + return [':'.$key, $value]; + }); + + $msg = strtr($msg, $params); + + return $msg; + } + + /** + * Set the caching context, the page url. + * @param string $locale + * @param string $url + */ + public static function setContext($locale, $url = null) + { + if (!strlen($url)) { + $url = '/'; + } + + self::$url = $url; + self::$locale = $locale; + + if ($cached = Cache::get(static::makeCacheKey())) { + self::$cache = (array) $cached; + } + } + + /** + * Save context messages to cache. + * @return void + */ + public static function saveToCache() + { + if (!self::$hasNew || !self::$url || !self::$locale) { + return; + } + + $expiresAt = now()->addMinutes(Config::get('rainlab.translate::cacheTimeout', 1440)); + Cache::put(static::makeCacheKey(), self::$cache, $expiresAt); + } + + /** + * Creates a cache key for storing context messages. + * @return string + */ + protected static function makeCacheKey() + { + return 'translation.'.self::$locale.self::$url; + } + + /** + * Creates a sterile key for a message. + * @param string $messageId + * @return string + */ + protected static function makeMessageCode($messageId) + { + $separator = '.'; + + // Convert all dashes/underscores into separator + $messageId = preg_replace('!['.preg_quote('_').'|'.preg_quote('-').']+!u', $separator, $messageId); + + // Remove all characters that are not the separator, letters, numbers, or whitespace. + $messageId = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', mb_strtolower($messageId)); + + // Replace all separator characters and whitespace by a single separator + $messageId = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $messageId); + + return Str::limit(trim($messageId, $separator), 250); + } +} diff --git a/plugins/rainlab/translate/models/MessageExport.php b/plugins/rainlab/translate/models/MessageExport.php new file mode 100644 index 0000000..415e88d --- /dev/null +++ b/plugins/rainlab/translate/models/MessageExport.php @@ -0,0 +1,52 @@ +map(function ($message) use ($columns) { + $data = $message->message_data; + // Add code to data to simplify algorithm + $data[self::CODE_COLUMN_NAME] = $message->code; + + $result = []; + foreach ($columns as $column) { + $result[$column] = isset($data[$column]) ? $data[$column] : ''; + } + return $result; + })->toArray(); + } + + /** + * getColumns + * + * code, default column + all existing locales + * + * @return array + */ + public static function getColumns() + { + return array_merge([ + self::CODE_COLUMN_NAME => self::CODE_COLUMN_NAME, + Message::DEFAULT_LOCALE => self::DEFAULT_COLUMN_NAME, + ], Locale::lists(self::CODE_COLUMN_NAME, self::CODE_COLUMN_NAME)); + } +} diff --git a/plugins/rainlab/translate/models/MessageImport.php b/plugins/rainlab/translate/models/MessageImport.php new file mode 100644 index 0000000..3657727 --- /dev/null +++ b/plugins/rainlab/translate/models/MessageImport.php @@ -0,0 +1,70 @@ + 'required' + ]; + + /** + * Import the message data from a csv with the following schema: + * + * code | en | de | fr + * ------------------------------- + * title | Title | Titel | Titre + * name | Name | Name | Prénom + * ... + * + * The code column is required and must not be empty. + * + * Note: Messages with an existing code are not removed/touched if the import + * doesn't contain this code. As a result you can incrementally update the + * messages by just adding the new codes and messages to the csv. + * + * @param $results + * @param null $sessionKey + */ + public function importData($results, $sessionKey = null) + { + $codeName = MessageExport::CODE_COLUMN_NAME; + $defaultName = Message::DEFAULT_LOCALE; + + foreach ($results as $index => $result) { + try { + if (isset($result[$codeName]) && !empty($result[$codeName])) { + $code = $result[$codeName]; + + // Modify result to match the expected message_data schema + unset($result[$codeName]); + + $message = Message::firstOrNew(['code' => $code]); + + // Create empty array, if $message is new + $message->message_data = $message->message_data ?: []; + + if (!isset($message->message_data[$defaultName])) { + $default = (isset($result[$defaultName]) && !empty($result[$defaultName])) ? $result[$defaultName] : $code; + $result[$defaultName] = $default; + } + + $message->message_data = array_merge($message->message_data, $result); + + if ($message->exists) { + $this->logUpdated(); + } else { + $this->logCreated(); + } + + $message->save(); + } else { + $this->logSkipped($index, 'No code provided'); + } + } catch (\Exception $exception) { + $this->logError($index, $exception->getMessage()); + } + } + } +} diff --git a/plugins/rainlab/translate/models/locale/columns.yaml b/plugins/rainlab/translate/models/locale/columns.yaml new file mode 100644 index 0000000..602eae2 --- /dev/null +++ b/plugins/rainlab/translate/models/locale/columns.yaml @@ -0,0 +1,26 @@ +# =================================== +# Column Definitions +# =================================== + +columns: + name: + label: rainlab.translate::lang.locale.name + searchable: yes + + code: + label: rainlab.translate::lang.locale.code + searchable: yes + + is_default: + label: rainlab.translate::lang.locale.is_default + type: switch + + is_enabled: + label: rainlab.translate::lang.locale.is_enabled + type: switch + invisible: true + + sort_order: + label: rainlab.translate::lang.locale.sort_order + type: number + invisible: true diff --git a/plugins/rainlab/translate/models/locale/fields.yaml b/plugins/rainlab/translate/models/locale/fields.yaml new file mode 100644 index 0000000..dbda60d --- /dev/null +++ b/plugins/rainlab/translate/models/locale/fields.yaml @@ -0,0 +1,24 @@ +# =================================== +# Field Definitions +# =================================== + +fields: + name: + label: rainlab.translate::lang.locale.name + span: auto + + code: + label: rainlab.translate::lang.locale.code + span: auto + + is_enabled: + label: rainlab.translate::lang.locale.is_enabled + type: checkbox + comment: rainlab.translate::lang.locale.is_enabled_help + span: auto + + is_default: + label: rainlab.translate::lang.locale.is_default + type: checkbox + comment: rainlab.translate::lang.locale.is_default_help + span: auto diff --git a/plugins/rainlab/translate/phpunit.xml b/plugins/rainlab/translate/phpunit.xml new file mode 100644 index 0000000..1fb393c --- /dev/null +++ b/plugins/rainlab/translate/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./tests + + + + + + + + diff --git a/plugins/rainlab/translate/routes.php b/plugins/rainlab/translate/routes.php new file mode 100644 index 0000000..66f247b --- /dev/null +++ b/plugins/rainlab/translate/routes.php @@ -0,0 +1,42 @@ +handleLocaleRoute(); + if (!$locale) { + return; + } + + /* + * Register routes + */ + Route::group(['prefix' => $locale, 'middleware' => 'web'], function () { + Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?'); + }); + + Route::any($locale, 'Cms\Classes\CmsController@run')->middleware('web'); + + /* + * Ensure Url::action() retains the localized URL + * by re-registering the route after the CMS. + */ + Event::listen('cms.route', function () use ($locale) { + Route::group(['prefix' => $locale, 'middleware' => 'web'], function () { + Route::any('{slug?}', 'Cms\Classes\CmsController@run')->where('slug', '(.*)?'); + }); + }); +}); + +/* + * Save any used messages to the contextual cache. + */ +App::after(function ($request) { + if (class_exists('RainLab\Translate\Models\Message')) { + Message::saveToCache(); + } +}); diff --git a/plugins/rainlab/translate/tests/fixtures/classes/Feature.php b/plugins/rainlab/translate/tests/fixtures/classes/Feature.php new file mode 100644 index 0000000..eabe987 --- /dev/null +++ b/plugins/rainlab/translate/tests/fixtures/classes/Feature.php @@ -0,0 +1,24 @@ + true], 'states']; + + /** + * @var string The database table used by the model. + */ + public $table = 'translate_test_countries'; + + /** + * @var array Guarded fields + */ + protected $guarded = []; + + /** + * @var array Jsonable fields + */ + protected $jsonable = ['states']; +} diff --git a/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore b/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/plugins/rainlab/translate/tests/fixtures/themes/test/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php new file mode 100644 index 0000000..1228656 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableCmsObjectTest.php @@ -0,0 +1,110 @@ +themePath = __DIR__ . '/../../fixtures/themes/test'; + + $this->seedSampleSourceAndData(); + } + + public function tearDown(): void + { + $this->cleanUp(); + } + + protected function cleanUp() + { + @unlink($this->themePath.'/features/winning.htm'); + @unlink($this->themePath.'/features-fr/winning.htm'); + File::deleteDirectory($this->themePath.'/features'); + File::deleteDirectory($this->themePath.'/features-fr'); + } + + protected function seedSampleSourceAndData() + { + $datasource = new FileDatasource($this->themePath, new Filesystem); + $resolver = new Resolver(['theme1' => $datasource]); + $resolver->setDefaultDatasource('theme1'); + Model::setDatasourceResolver($resolver); + + LocaleModel::unguard(); + + LocaleModel::firstOrCreate([ + 'code' => 'fr', + 'name' => 'French', + 'is_enabled' => 1 + ]); + + LocaleModel::reguard(); + + $this->recycleSampleData(); + } + + protected function recycleSampleData() + { + $this->cleanUp(); + + FeatureModel::create([ + 'fileName' => 'winning.htm', + 'settings' => ['title' => 'Hash tag winning'], + 'markup' => 'Awww yiss', + ]); + } + + public function testGetTranslationValue() + { + $obj = FeatureModel::first(); + + $this->assertEquals('Awww yiss', $obj->markup); + + $obj->translateContext('fr'); + + $this->assertEquals('Awww yiss', $obj->markup); + } + + public function testGetTranslationValueNoFallback() + { + $obj = FeatureModel::first(); + + $this->assertEquals('Awww yiss', $obj->markup); + + $obj->noFallbackLocale()->translateContext('fr'); + + $this->assertEquals(null, $obj->markup); + } + + public function testSetTranslationValue() + { + $this->recycleSampleData(); + + $obj = FeatureModel::first(); + $obj->markup = 'Aussie'; + $obj->save(); + + $obj->translateContext('fr'); + $obj->markup = 'Australie'; + $obj->save(); + + $obj = FeatureModel::first(); + $this->assertEquals('Aussie', $obj->markup); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->markup); + } + +} diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php new file mode 100644 index 0000000..f33509d --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatableModelTest.php @@ -0,0 +1,202 @@ +seedSampleTableAndData(); + } + + protected function seedSampleTableAndData() + { + if (Schema::hasTable('translate_test_countries')) { + return; + } + + Model::unguard(); + + Schema::create('translate_test_countries', function($table) + { + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->string('name')->nullable(); + $table->string('code')->nullable(); + $table->text('states')->nullable(); + $table->timestamps(); + }); + + LocaleModel::firstOrCreate([ + 'code' => 'fr', + 'name' => 'French', + 'is_enabled' => 1 + ]); + + $this->recycleSampleData(); + + Model::reguard(); + } + + protected function recycleSampleData() + { + CountryModel::truncate(); + + CountryModel::create([ + 'name' => 'Australia', + 'code' => 'AU', + 'states' => ['NSW', 'ACT', 'QLD'], + ]); + } + + public function testGetTranslationValue() + { + $obj = CountryModel::first(); + + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + + $this->assertEquals('Australia', $obj->name); + } + + public function testGetTranslationValueNoFallback() + { + $obj = CountryModel::first(); + + $this->assertEquals('Australia', $obj->name); + + $obj->noFallbackLocale()->translateContext('fr'); + + $this->assertEquals(null, $obj->name); + } + + public function testSetTranslationValue() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->name = 'Aussie'; + $obj->states = ['VIC', 'SA', 'NT']; + $obj->save(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $obj = CountryModel::first(); + $this->assertEquals('Aussie', $obj->name); + $this->assertEquals(['VIC', 'SA', 'NT'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } + + public function testGetTranslationValueEagerLoading() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $objList = CountryModel::with([ + 'translations' + ])->get(); + + $obj = $objList[0]; + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } + + public function testTranslateWhere() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->save(); + + $this->assertEquals(0, CountryModel::transWhere('name', 'Australie')->count()); + + Translator::instance()->setLocale('fr'); + $this->assertEquals(1, CountryModel::transWhere('name', 'Australie')->count()); + + Translator::instance()->setLocale('en'); + } + + public function testTranslateOrderBy() + { + $this->recycleSampleData(); + + $obj = CountryModel::first(); + + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->save(); + + $obj = CountryModel::create([ + 'name' => 'Germany', + 'code' => 'DE' + ]); + + $obj->translateContext('fr'); + $obj->name = 'Allemagne'; + $obj->save(); + + $res = CountryModel::transOrderBy('name')->get()->pluck('name'); + $this->assertEquals(['Australia', 'Germany'], $res->toArray()); + + Translator::instance()->setLocale('fr'); + $res = CountryModel::transOrderBy('name')->get()->pluck('name'); + $this->assertEquals(['Allemagne', 'Australie'], $res->toArray()); + + Translator::instance()->setLocale('en'); + } + + public function testGetTranslationValueEagerLoadingWithMorphMap() + { + Relation::morphMap([ + 'morph.key' => CountryModel::class, + ]); + + $this->recycleSampleData(); + + $obj = CountryModel::first(); + $obj->translateContext('fr'); + $obj->name = 'Australie'; + $obj->states = ['a', 'b', 'c']; + $obj->save(); + + $objList = CountryModel::with([ + 'translations' + ])->get(); + + $obj = $objList[0]; + $this->assertEquals('Australia', $obj->name); + $this->assertEquals(['NSW', 'ACT', 'QLD'], $obj->states); + + $obj->translateContext('fr'); + $this->assertEquals('Australie', $obj->name); + $this->assertEquals(['a', 'b', 'c'], $obj->states); + } +} diff --git a/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php b/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php new file mode 100644 index 0000000..0bbfa44 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/behaviors/TranslatablePageTest.php @@ -0,0 +1,67 @@ +themePath = __DIR__ . '/../../fixtures/themes/test'; + + $datasource = new FileDatasource($this->themePath, new Filesystem); + $resolver = new Resolver(['theme1' => $datasource]); + $resolver->setDefaultDatasource('theme1'); + Model::setDatasourceResolver($resolver); + + TranslatablePage::extend(function($page) { + if (!$page->isClassExtendedWith('RainLab\Translate\Behaviors\TranslatablePage')) { + $page->addDynamicProperty('translatable', ['title']); + $page->extendClassWith('RainLab\Translate\Behaviors\TranslatablePage'); + } + }); + + } + + public function tearDown(): void + { + File::deleteDirectory($this->themePath.'/pages'); + } + + public function testUseFallback() + { + $page = TranslatablePage::create([ + 'fileName' => 'translatable', + 'title' => 'english title', + 'url' => '/test', + ]); + $page->translateContext('fr'); + $this->assertEquals('english title', $page->title); + $page->noFallbackLocale()->translateContext('fr'); + $this->assertEquals(null, $page->title); + } + + public function testAlternateLocale() + { + $page = TranslatablePage::create([ + 'fileName' => 'translatable', + 'title' => 'english title', + 'url' => '/test', + ]); + $page->setAttributeTranslated('title', 'titre francais', 'fr'); + $title_en = $page->title; + $this->assertEquals('english title', $title_en); + $page->translateContext('fr'); + $title_fr = $page->title; + $this->assertEquals('titre francais', $title_fr); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php b/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php new file mode 100644 index 0000000..76262c9 --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/ExportMessageTest.php @@ -0,0 +1,98 @@ +exportData([]); + + $this->assertEquals($expected, $result); + } + + public function testCanHandleNoColumn() + { + $exportModel = new MessageExport(); + $this->createMessages(); + $expected = [[], []]; + + $result = $exportModel->exportData([]); + + $this->assertEquals($expected, $result); + } + + public function testExportSomeColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['code' => 'hello'], + ['code' => 'bye'], + ]; + + $result = $exportMode->exportData(['code']); + + $this->assertEquals($expected, $result); + } + + public function testExportAllColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['code' => 'hello', 'de' => 'Hallo, Welt', 'en' => 'Hello, World'], + ['code' => 'bye', 'de' => 'Auf Wiedersehen', 'en' => 'Goodbye'], + ]; + + $result = $exportMode->exportData(['code', 'de', 'en']); + + $this->assertEquals($expected, $result); + } + + public function testCanHandleNonExistingColumns() + { + $exportMode = new MessageExport(); + $this->createMessages(); + $expected = [ + ['dummy' => ''], + ['dummy' => ''], + ]; + + $result = $exportMode->exportData(['dummy']); + + $this->assertEquals($expected, $result); + } + + private function createMessages() + { + Message::create([ + 'code' => 'hello', 'message_data' => ['de' => 'Hallo, Welt', 'en' => 'Hello, World'] + ]); + Message::create([ + 'code' => 'bye', 'message_data' => ['de' => 'Auf Wiedersehen', 'en' => 'Goodbye'] + ]); + } + + public function testGetColumns() + { + Locale::unguard(); + Locale::create(['code' => 'de', 'name' => 'German', 'is_enabled' => true]); + + $columns = MessageExport::getColumns(); + + $this->assertEquals([ + MessageExport::CODE_COLUMN_NAME => MessageExport::CODE_COLUMN_NAME, + Message::DEFAULT_LOCALE => MessageExport::DEFAULT_COLUMN_NAME, + 'en' => 'en', + 'de' => 'de', + ], $columns); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php b/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php new file mode 100644 index 0000000..8b9c02a --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/ImportMessageTest.php @@ -0,0 +1,95 @@ +importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(false, $stats->hasMessages); + } + + public function testCreateMessage() + { + $messageImport = new MessageImport(); + $data = [ + ['code' => 'new', 'de' => 'Neu', 'en' => 'new'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(1, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + } + + public function testUpdateMessage() + { + $messageImport = new MessageImport(); + Message::create(['code' => 'update', 'message_data' => ['en' => 'update', 'de' => 'aktualisieren']]); + $data = [ + ['code' => 'update', 'de' => 'Neu 2', 'en' => 'new 2'] + ]; + $expected = [ + Message::DEFAULT_LOCALE => 'update', 'de' => 'Neu 2', 'en' => 'new 2' + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(0, $stats->created); + $this->assertEquals(1, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + $updatedMessage = Message::whereCode('update')->first(); + $this->assertEquals($expected, $updatedMessage->message_data); + } + + public function testMissingCodeIsSkipped() + { + $messageImport = new MessageImport(); + $data = [ + ['de' => 'Neu 2', 'en' => 'new 2'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(0, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(1, $stats->skippedCount); + $this->assertEquals(true, $stats->hasMessages); + $this->assertEquals(Message::count(), 0); + } + + public function testDefaultLocaleIsImported() + { + $messageImport = new MessageImport(); + $data = [ + ['code' => 'test.me', 'x' => 'foo bar', 'de' => 'Neu 2', 'en' => 'new 2'] + ]; + + $messageImport->importData($data); + + $stats = $messageImport->getResultStats(); + $this->assertEquals(1, $stats->created); + $this->assertEquals(0, $stats->updated); + $this->assertEquals(0, $stats->skippedCount); + $this->assertEquals(false, $stats->hasMessages); + $this->assertEquals(Message::count(), 1); + + $message = Message::where('code', 'test.me')->first(); + + $this->assertEquals('foo bar', $message->message_data['x']); + } +} diff --git a/plugins/rainlab/translate/tests/unit/models/MessageTest.php b/plugins/rainlab/translate/tests/unit/models/MessageTest.php new file mode 100644 index 0000000..32a33eb --- /dev/null +++ b/plugins/rainlab/translate/tests/unit/models/MessageTest.php @@ -0,0 +1,61 @@ +assertNotNull(Message::whereCode('hello.world')->first()); + $this->assertNotNull(Message::whereCode('hello.piñata')->first()); + + Message::truncate(); + } + + public function testMakeMessageCode() + { + $this->assertEquals('hello.world', Message::makeMessageCode('hello world')); + $this->assertEquals('hello.world', Message::makeMessageCode(' hello world ')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello-world')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello--world')); + + // casing + $this->assertEquals('hello.world', Message::makeMessageCode('Hello World')); + $this->assertEquals('hello.world', Message::makeMessageCode('Hello World!')); + + // underscores + $this->assertEquals('hello.world', Message::makeMessageCode('hello_world')); + $this->assertEquals('hello.world', Message::makeMessageCode('hello__world')); + + // length limit + $veryLongString = str_repeat("10 charstr", 30); + $this->assertTrue(strlen($veryLongString) > 250); + $this->assertEquals(253, strlen(Message::makeMessageCode($veryLongString))); + $this->assertStringEndsWith('...', Message::makeMessageCode($veryLongString)); + + // unicode characters + // brrowered some test cases from Stringy, the library Laravel's + // `slug()` function depends on + // https://github.com/danielstjules/Stringy/blob/master/tests/CommonTest.php + $this->assertEquals('fòô.bàř', Message::makeMessageCode('fòô bàř')); + $this->assertEquals('ťéśţ', Message::makeMessageCode(' ŤÉŚŢ ')); + $this->assertEquals('φ.ź.3', Message::makeMessageCode('φ = ź = 3')); + $this->assertEquals('перевірка', Message::makeMessageCode('перевірка')); + $this->assertEquals('лысая.гора', Message::makeMessageCode('лысая гора')); + $this->assertEquals('щука', Message::makeMessageCode('щука')); + $this->assertEquals('foo.漢字', Message::makeMessageCode('foo 漢字')); // Chinese + $this->assertEquals('xin.chào.thế.giới', Message::makeMessageCode('xin chào thế giới')); + $this->assertEquals('xin.chào.thế.giới', Message::makeMessageCode('XIN CHÀO THẾ GIỚI')); + $this->assertEquals('đấm.phát.chết.luôn', Message::makeMessageCode('đấm phát chết luôn')); + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // no-break space (U+00A0) + $this->assertEquals('foo', Message::makeMessageCode('foo           ')); // spaces U+2000 to U+200A + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // narrow no-break space (U+202F) + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // medium mathematical space (U+205F) + $this->assertEquals('foo', Message::makeMessageCode('foo ')); // ideographic space (U+3000) + } +} diff --git a/plugins/rainlab/translate/traits/MLControl.php b/plugins/rainlab/translate/traits/MLControl.php new file mode 100644 index 0000000..cbef397 --- /dev/null +++ b/plugins/rainlab/translate/traits/MLControl.php @@ -0,0 +1,279 @@ +defaultLocale = Locale::getDefault(); + $this->isAvailable = Locale::isAvailable(); + } + + /** + * getParentViewPath returns the parent control's view path + * + * @return string + */ + protected function getParentViewPath() + { + // return base_path().'/modules/backend/formwidgets/parentcontrol/partials'; + } + + /** + * getParentAssetPath returns the parent control's asset path + * + * @return string + */ + protected function getParentAssetPath() + { + // return '/modules/backend/formwidgets/parentcontrol/assets'; + } + + /** + * actAsParent swaps the asset & view paths with the parent control's to + * act as the parent control + * + * @param boolean $switch Defaults to true, determines whether to act as the parent or revert to current + */ + protected function actAsParent($switch = true) + { + if ($switch) { + $this->originalAssetPath = $this->assetPath; + $this->originalViewPath = $this->viewPath; + $this->assetPath = $this->getParentAssetPath(); + $this->viewPath = $this->getParentViewPath(); + } + else { + $this->assetPath = $this->originalAssetPath; + $this->viewPath = $this->originalViewPath; + } + } + + /** + * {@inheritDoc} + */ + public function renderFallbackField() + { + return $this->makeMLPartial('fallback_field'); + } + + /** + * makeMLPartial is used by child classes to render in context of this view path. + * @param string $partial The view to load. + * @param array $params Parameter variables to pass to the view. + * @return string The view contents. + */ + public function makeMLPartial($partial, $params = []) + { + $oldViewPath = $this->viewPath; + $this->viewPath = $this->guessViewPathFrom(__TRAIT__, '/partials'); + $result = $this->makePartial($partial, $params); + $this->viewPath = $oldViewPath; + + return $result; + } + + /** + * prepareLocaleVars prepares the list data + */ + public function prepareLocaleVars() + { + $this->vars['defaultLocale'] = $this->defaultLocale; + $this->vars['locales'] = Locale::listAvailable(); + $this->vars['field'] = $this->makeRenderFormField(); + } + + /** + * loadLocaleAssets loads assets specific to ML Controls + */ + public function loadLocaleAssets() + { + $this->addJs('/plugins/rainlab/translate/assets/js/multilingual.js', 'RainLab.Translate'); + $this->addCss('/plugins/rainlab/translate/assets/css/multilingual.css', 'RainLab.Translate'); + + if (!class_exists('System')) { + $this->addCss('/plugins/rainlab/translate/assets/css/multilingual-v1.css', 'RainLab.Translate'); + } + } + + /** + * getLocaleValue returns a translated value for a given locale. + * @param string $locale + * @return string + */ + public function getLocaleValue($locale) + { + $key = $this->valueFrom ?: $this->fieldName; + + /* + * Get the translated values from the model + */ + $studKey = Str::studly(implode(' ', HtmlHelper::nameToArray($key))); + $mutateMethod = 'get'.$studKey.'AttributeTranslated'; + + if ($this->objectMethodExists($this->model, $mutateMethod)) { + $value = $this->model->$mutateMethod($locale); + } + elseif ($this->objectMethodExists($this->model, 'getAttributeTranslated') && $this->defaultLocale->code != $locale) { + $value = $this->model->noFallbackLocale()->getAttributeTranslated($key, $locale); + } + else { + $value = $this->formField->value; + } + + return $value; + } + + /** + * makeRenderFormField if translation is unavailable, render the original field type (text). + */ + protected function makeRenderFormField() + { + if ($this->isAvailable) { + return $this->formField; + } + + $field = clone $this->formField; + $field->type = $this->getFallbackType(); + + return $field; + } + + /** + * {@inheritDoc} + */ + public function getLocaleSaveValue($value) + { + $localeData = $this->getLocaleSaveData(); + $key = $this->valueFrom ?: $this->fieldName; + + /* + * Set the translated values to the model + */ + $studKey = Str::studly(implode(' ', HtmlHelper::nameToArray($key))); + $mutateMethod = 'set'.$studKey.'AttributeTranslated'; + + if ($this->objectMethodExists($this->model, $mutateMethod)) { + foreach ($localeData as $locale => $value) { + $this->model->$mutateMethod($value, $locale); + } + } + elseif ($this->objectMethodExists($this->model, 'setAttributeTranslated')) { + foreach ($localeData as $locale => $value) { + $this->model->setAttributeTranslated($key, $value, $locale); + } + } + + return array_get($localeData, $this->defaultLocale->code, $value); + } + + /** + * getLocaleSaveData returns an array of translated values for this field + * @return array + */ + public function getLocaleSaveData() + { + $values = []; + $data = post('RLTranslate'); + + if (!is_array($data)) { + return $values; + } + + $fieldName = implode('.', HtmlHelper::nameToArray($this->fieldName)); + $isJson = $this->isLocaleFieldJsonable(); + + foreach ($data as $locale => $_data) { + $value = array_get($_data, $fieldName); + $values[$locale] = $isJson ? json_decode($value, true) : $value; + } + + return $values; + } + + /** + * getFallbackType returns the fallback field type. + * @return string + */ + public function getFallbackType() + { + return defined('static::FALLBACK_TYPE') ? static::FALLBACK_TYPE : 'text'; + } + + /** + * isLocaleFieldJsonable returns true if widget is a repeater, or the field is specified + * as jsonable in the model. + * @return bool + */ + public function isLocaleFieldJsonable() + { + if ( + $this instanceof \Backend\FormWidgets\Repeater || + $this instanceof \Backend\FormWidgets\NestedForm + ) { + return true; + } + + if ($this instanceof \Media\FormWidgets\MediaFinder && $this->maxItems !== 1) { + return true; + } + + if ( + method_exists($this->model, 'isJsonable') && + $this->model->isJsonable($this->fieldName) + ) { + return true; + } + + return false; + } + + /** + * objectMethodExists is an internal helper for method existence checks. + * + * @param object $object + * @param string $method + * @return boolean + */ + protected function objectMethodExists($object, $method) + { + if (method_exists($object, 'methodExists')) { + return $object->methodExists($method); + } + + return method_exists($object, $method); + } +} diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm new file mode 100644 index 0000000..0ffd1ab --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_fallback_field.htm @@ -0,0 +1,3 @@ +
    + makePartial('~/modules/backend/widgets/form/partials/_field_'.$field->type.'.htm', ['field' => $field]) ?> +
    \ No newline at end of file diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm new file mode 100644 index 0000000..fb2c0b6 --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_selector.htm @@ -0,0 +1,18 @@ + + diff --git a/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm new file mode 100644 index 0000000..ae24007 --- /dev/null +++ b/plugins/rainlab/translate/traits/mlcontrol/partials/_locale_values.htm @@ -0,0 +1,15 @@ + + $name): ?> + getLocaleValue($code); + $value = $this->isLocaleFieldJsonable() ? json_encode($value) : $value; + if (is_array($value)) $value = array_first($value); + ?> + getAttributes() ?> + /> + diff --git a/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php b/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php new file mode 100644 index 0000000..0c1f319 --- /dev/null +++ b/plugins/rainlab/translate/updates/builder_table_update_rainlab_translate_locales.php @@ -0,0 +1,44 @@ +integer('sort_order')->default(0); + }); + } + + $locales = Locale::all(); + foreach($locales as $locale) { + $locale->sort_order = $locale->id; + $locale->save(); + } + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME)) { + return; + } + + if (Schema::hasColumn(self::TABLE_NAME, 'sort_order')) { + Schema::table(self::TABLE_NAME, function($table) + { + $table->dropColumn(['sort_order']); + }); + } + } +} diff --git a/plugins/rainlab/translate/updates/create_attributes_table.php b/plugins/rainlab/translate/updates/create_attributes_table.php new file mode 100644 index 0000000..7dc5b96 --- /dev/null +++ b/plugins/rainlab/translate/updates/create_attributes_table.php @@ -0,0 +1,27 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('locale')->index(); + $table->string('model_id')->index()->nullable(); + $table->string('model_type')->index()->nullable(); + $table->mediumText('attribute_data')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_attributes'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_indexes_table.php b/plugins/rainlab/translate/updates/create_indexes_table.php new file mode 100644 index 0000000..2e90ece --- /dev/null +++ b/plugins/rainlab/translate/updates/create_indexes_table.php @@ -0,0 +1,28 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('locale')->index(); + $table->string('model_id')->index()->nullable(); + $table->string('model_type')->index()->nullable(); + $table->string('item')->nullable()->index(); + $table->mediumText('value')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_indexes'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_locales_table.php b/plugins/rainlab/translate/updates/create_locales_table.php new file mode 100644 index 0000000..4d24e9e --- /dev/null +++ b/plugins/rainlab/translate/updates/create_locales_table.php @@ -0,0 +1,27 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('code')->index(); + $table->string('name')->index()->nullable(); + $table->boolean('is_default')->default(0); + $table->boolean('is_enabled')->default(0); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_locales'); + } + +} diff --git a/plugins/rainlab/translate/updates/create_messages_table.php b/plugins/rainlab/translate/updates/create_messages_table.php new file mode 100644 index 0000000..2532405 --- /dev/null +++ b/plugins/rainlab/translate/updates/create_messages_table.php @@ -0,0 +1,25 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('code')->index()->nullable(); + $table->mediumText('message_data')->nullable(); + }); + } + + public function down() + { + Schema::dropIfExists('rainlab_translate_messages'); + } + +} diff --git a/plugins/rainlab/translate/updates/migrate_morphed_attributes.php b/plugins/rainlab/translate/updates/migrate_morphed_attributes.php new file mode 100644 index 0000000..10b9b07 --- /dev/null +++ b/plugins/rainlab/translate/updates/migrate_morphed_attributes.php @@ -0,0 +1,32 @@ +getTable(); + foreach (Relation::$morphMap as $alias => $class) { + Db::table($table)->where('model_type', $class)->update(['model_type' => $alias]); + } + } + + public function down() + { + $table = (new Attribute())->getTable(); + foreach (Relation::$morphMap as $alias => $class) { + Db::table($table)->where('model_type', $alias)->update(['model_type' => $class]); + } + } +} diff --git a/plugins/rainlab/translate/updates/migrate_morphed_indexes.php b/plugins/rainlab/translate/updates/migrate_morphed_indexes.php new file mode 100644 index 0000000..29300f3 --- /dev/null +++ b/plugins/rainlab/translate/updates/migrate_morphed_indexes.php @@ -0,0 +1,32 @@ + $class) { + Db::table($this->table)->where('model_type', $class)->update(['model_type' => $alias]); + } + } + + public function down() + { + foreach (Relation::$morphMap as $alias => $class) { + Db::table($this->table)->where('model_type', $alias)->update(['model_type' => $class]); + } + } +} diff --git a/plugins/rainlab/translate/updates/seed_all_tables.php b/plugins/rainlab/translate/updates/seed_all_tables.php new file mode 100644 index 0000000..3157681 --- /dev/null +++ b/plugins/rainlab/translate/updates/seed_all_tables.php @@ -0,0 +1,21 @@ + 'en', + 'name' => 'English', + 'is_default' => true, + 'is_enabled' => true + ]); + } + } + +} diff --git a/plugins/rainlab/translate/updates/update_messages_table.php b/plugins/rainlab/translate/updates/update_messages_table.php new file mode 100644 index 0000000..5d9955b --- /dev/null +++ b/plugins/rainlab/translate/updates/update_messages_table.php @@ -0,0 +1,37 @@ +boolean('found')->default(1); + }); + } + } + + public function down() + { + if (!Schema::hasTable(self::TABLE_NAME)) { + return; + } + + if (Schema::hasColumn(self::TABLE_NAME, 'found')) { + Schema::table(self::TABLE_NAME, function($table) + { + $table->dropColumn(['found']); + }); + } + } +} diff --git a/plugins/rainlab/translate/updates/version.yaml b/plugins/rainlab/translate/updates/version.yaml new file mode 100644 index 0000000..a731208 --- /dev/null +++ b/plugins/rainlab/translate/updates/version.yaml @@ -0,0 +1,99 @@ +v1.0.1: + - First version of Translate + - create_messages_table.php + - create_attributes_table.php + - create_locales_table.php +v1.0.2: Languages and Messages can now be deleted. +v1.0.3: Minor updates for latest October release. +v1.0.4: Locale cache will clear when updating a language. +v1.0.5: Add Spanish language and fix plugin config. +v1.0.6: Minor improvements to the code. +v1.0.7: Fixes major bug where translations are skipped entirely! +v1.0.8: Minor bug fixes. +v1.0.9: Fixes an issue where newly created models lose their translated values. +v1.0.10: Minor fix for latest build. +v1.0.11: Fix multilingual rich editor when used in stretch mode. +v1.1.0: Introduce compatibility with RainLab.Pages plugin. +v1.1.1: Minor UI fix to the language picker. +v1.1.2: Add support for translating Static Content files. +v1.1.3: Improved support for the multilingual rich editor. +v1.1.4: Adds new multilingual markdown editor. +v1.1.5: Minor update to the multilingual control API. +v1.1.6: Minor improvements in the message editor. +v1.1.7: Fixes bug not showing content when first loading multilingual textarea controls. +v1.2.0: CMS pages now support translating the URL. +v1.2.1: Minor update in the rich editor and code editor language control position. +v1.2.2: Static Pages now support translating the URL. +v1.2.3: Fixes Rich Editor when inserting a page link. +v1.2.4: + - Translatable attributes can now be declared as indexes. + - create_indexes_table.php +v1.2.5: Adds new multilingual repeater form widget. +v1.2.6: Fixes repeater usage with static pages plugin. +v1.2.7: Fixes placeholder usage with static pages plugin. +v1.2.8: Improvements to code for latest October build compatibility. +v1.2.9: Fixes context for translated strings when used with Static Pages. +v1.2.10: Minor UI fix to the multilingual repeater. +v1.2.11: Fixes translation not working with partials loaded via AJAX. +v1.2.12: Add support for translating the new grouped repeater feature. +v1.3.0: Added search to the translate messages page. +v1.3.1: + - Added reordering to languages + - builder_table_update_rainlab_translate_locales.php + - seed_all_tables.php +v1.3.2: Improved compatibility with RainLab.Pages, added ability to scan Mail Messages for translatable variables. +v1.3.3: Fix to the locale picker session handling in Build 420 onwards. +v1.3.4: Add alternate hreflang elements and adds prefixDefaultLocale setting. +v1.3.5: Fix MLRepeater bug when switching locales. +v1.3.6: Fix Middleware to use the prefixDefaultLocale setting introduced in 1.3.4 +v1.3.7: Fix config reference in LocaleMiddleware +v1.3.8: Keep query string when switching locales +v1.4.0: Add importer and exporter for messages +v1.4.1: Updated Hungarian translation. Added Arabic translation. Fixed issue where default texts are overwritten by import. Fixed issue where the language switcher for repeater fields would overlap with the first repeater row. +v1.4.2: Add multilingual MediaFinder +v1.4.3: "!!! Please update OctoberCMS to Build 444 before updating this plugin. Added ability to translate CMS Pages fields (e.g. title, description, meta-title, meta-description)" +v1.4.4: Minor improvements to compatibility with Laravel framework. +v1.4.5: Fixed issue when using the language switcher +v1.5.0: Compatibility fix with Build 451 +v1.6.0: Make File Upload widget properties translatable. Merge Repeater core changes into MLRepeater widget. Add getter method to retrieve original translate data. +v1.6.1: Add ability for models to provide translated computed data, add option to disable locale prefix routing +v1.6.2: Implement localeUrl filter, add per-locale theme configuration support +v1.6.3: Add eager loading for translations, restore support for accessors & mutators +v1.6.4: Fixes PHP 7.4 compatibility +v1.6.5: Fixes compatibility issue when other plugins use a custom model morph map +v1.6.6: + - Introduce migration to patch existing translations using morph map + - migrate_morphed_attributes.php +v1.6.7: + - Introduce migration to patch existing indexes using morph map + - migrate_morphed_indexes.php +v1.6.8: Add support for transOrderBy; Add translation support for ThemeData; Update russian localization. +v1.6.9: Clear Static Page menu cache after saving the model; CSS fix for Text/Textarea input fields language selector. +v1.6.10: + - Add option to purge deleted messages when scanning messages, Add Scan error column on Messages page, Fix translations that were lost when clicking locale twice while holding ctrl key, Fix error with nested fields default locale value, Escape Message translate params value. + - update_messages_table.php +v1.7.0: "!!! Breaking change for the Message::trans() method (params are now escaped), fix message translation documentation, fix string translation key for scan errors column header." +v1.7.1: Fix YAML issue with previous tag/release. +v1.7.2: Fix regex when "|_" filter is followed by another filter, Try locale without country before returning default translation, Allow exporting default locale, Fire 'rainlab.translate.themeScanner.afterScan' event in the theme scanner for extendability. +v1.7.3: Make plugin ready for Laravel 6 update, Add support for translating RainLab.Pages MenuItem properties (requires RainLab.Pages v1.3.6), Restore multilingual button position for textarea, Fix translatableAttributes. +v1.7.4: Faster version of transWhere, Mail templates/views can now be localized, Fix messages table layout on mobile, Fix scopeTransOrderBy duplicates, Polish localization updates, Turkish localization updates, Add Greek language localization. +v1.8.0: Adds initial support for October v2.0 +v1.8.1: Minor bugfix +v1.8.2: Fixes translated file models and theme data for v2.0. The parent model must implement translatable behavior for their related file models to be translated. +v1.8.4: Fixes the multilingual mediafinder to work with the media module. +v1.8.6: Fixes invisible checkboxes when scanning for messages. +v1.8.7: Fixes Markdown editor translation. +v1.8.8: Fixes Laravel compatibility in custom Repeater. +v1.9.0: Restores ability to translate URLs with CMS Editor in October v2.0 +v1.9.1: Minor styling improvements +v1.9.2: Fixes issue creating new content in CMS Editor +v1.9.3: Improves support when using child themes +v1.10.0: Adds new multilingual nested form widget. Adds withFallbackLocale method. +v1.10.1: Improve support with October v2.0 +v1.10.2: Improve support with October v2.2 +v1.10.3: Multilingual control improvements +v1.10.4: Improve media finder support with October v2.2 +v1.10.5: Fixes media finder when only 1 locale is available +v1.11.0: Update to latest Media Finder changes in October v2.2 +v1.11.1: Improve support with October v3.0 +v1.12.0: Adds scopeTransWhereNoFallback method diff --git a/storage/cms/project.json b/storage/cms/project.json deleted file mode 100644 index c0a0cd8..0000000 --- a/storage/cms/project.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "project": "1AQNlBQVgH1cOBQu8JRgPHRE8AScQJRE8ZRkQGRfgBTZlBTVmZQWwZGEyAzR1AzHkMJRlAJRmZmt0BQAxAwD" -} \ No newline at end of file