├── .gitignore ├── languages ├── layotter.mo └── layotter-de_DE.mo ├── assets ├── img │ ├── loading.gif │ └── loading-small.gif ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── js │ ├── app │ │ ├── controllers │ │ │ ├── form.js │ │ │ ├── templates.js │ │ │ └── editor.js │ │ ├── services │ │ │ ├── view.js │ │ │ ├── data.js │ │ │ ├── state.js │ │ │ ├── history.js │ │ │ ├── modals.js │ │ │ ├── layouts.js │ │ │ ├── forms.js │ │ │ ├── templates.js │ │ │ └── content.js │ │ └── app.js │ ├── settings.js │ └── vendor │ │ ├── angular-sanitize.min.js │ │ ├── jquery.serialize-object.compiled.js │ │ ├── angular-animate.min.js │ │ └── angular-ui-sortable-old.js └── css │ ├── frontend.css.map │ ├── frontend.scss │ └── frontend.css ├── views ├── confirm.php ├── prompt.php ├── add-element.php ├── templates.php ├── load-layout.php ├── form.php └── editor.php ├── composer.json ├── example ├── element.php └── field-group.php ├── core ├── views.php ├── acf-locations.php ├── shortcode.php ├── interface.php ├── revisions.php ├── layouts.php ├── templates.php ├── core.php ├── acf-abstraction.php ├── assets.php └── ajax.php ├── components ├── form.php ├── options.php ├── row.php ├── col.php ├── editable.php ├── post.php └── element.php ├── README.md ├── index.php └── lib └── wp-post-meta-revisions.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /languages/layotter.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/languages/layotter.mo -------------------------------------------------------------------------------- /assets/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/img/loading.gif -------------------------------------------------------------------------------- /assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /assets/img/loading-small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/img/loading-small.gif -------------------------------------------------------------------------------- /languages/layotter-de_DE.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/languages/layotter-de_DE.mo -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hingst/layotter/HEAD/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/js/app/controllers/form.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller for all overlays 3 | */ 4 | app.controller('ModalCtrl', function($scope, content, layouts, forms) { 5 | angular.extend($scope, content, layouts); 6 | $scope.elementTypes = layotterData.elementTypes; 7 | $scope.form = forms.data; 8 | }); -------------------------------------------------------------------------------- /views/confirm.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ confirm.message }} 5 |

6 |
7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hingst/layotter", 3 | "description": "Add and arrange your content freely with an intuitive drag and drop interface!", 4 | "type": "wordpress-plugin", 5 | "require": { 6 | "php": ">=5.3.0", 7 | "composer/installers": "^1.2" 8 | }, 9 | "homepage": "http://www.layotter.com/", 10 | "license": "GPL-2.0", 11 | "authors": [ 12 | { 13 | "name": "Dennis Hingst", 14 | "email": "dennis@layotter.com" 15 | }, 16 | { 17 | "name": "Ben Kremer", 18 | "email": "ben@layotter.com" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /views/prompt.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ prompt.message }} 5 |

6 |

7 | 8 |

9 |
10 |
11 | 12 | 13 |
14 |
-------------------------------------------------------------------------------- /assets/css/frontend.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,OAAQ;EACJ,aAAa,EAAE,IAAI;EACnB,WAAW,EAAE,KAAK;EAClB,YAAY,EAAE,KAAK;EAEnB,yCAA0C;IAL9C,OAAQ;MAMA,aAAa,EAAE,CAAC;EAGpB,aAAQ;IACJ,OAAO,EAAE,EAAE;IACX,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,KAAK;EAGlB,yBAAkB;IACd,KAAK,EAAE,QAAQ;EAGnB,yBAAkB;IACd,KAAK,EAAE,SAAS;EAGpB,yBAAkB;IACd,KAAK,EAAE,GAAG;EAGd,yBAAkB;IACd,KAAK,EAAE,SAAS;EAGpB,yBAAkB;IACd,KAAK,EAAE,SAAS;EAGpB,yBAAkB;IACd,KAAK,EAAE,GAAG;EAGd,yBAAkB;IACd,KAAK,EAAE,SAAS;EAGpB,yBAAkB;IACd,KAAK,EAAE,SAAS;EAGpB,yBAAkB;IACd,KAAK,EAAE,GAAG;EAGd,0BAAmB;IACf,KAAK,EAAE,SAAS;EAGpB,0BAAmB;IACf,KAAK,EAAE,SAAS;EAGpB,0BAAmB;IACf,KAAK,EAAE,IAAI;EAGf;;;;;;;;;;;4BAWmB;IACf,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,GAAG;IAEf,yCAA0C;MAjB9C;;;;;;;;;;;gCAWmB;QAOX,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,CAAC;QACb,KAAK,EAAE,IAAI;QACX,aAAa,EAAE,IAAI", 4 | "sources": ["frontend.scss"], 5 | "names": [], 6 | "file": "frontend.css" 7 | } -------------------------------------------------------------------------------- /assets/js/app/services/view.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Things that change the view but have no effect on data 3 | */ 4 | app.service('view', function() { 5 | /** 6 | * Show/hide templates sidebar 7 | */ 8 | this.showTemplates = function() { 9 | jQuery('#layotter-templates').addClass('layotter-visible'); 10 | }; 11 | this.toggleTemplates = function() { 12 | jQuery('#layotter-templates').toggleClass('layotter-visible'); 13 | }; 14 | 15 | 16 | /** 17 | * Show/hide toolbar when scrolling 18 | */ 19 | jQuery(window).scroll(function(){ 20 | var scrolled = jQuery(document).scrollTop(); 21 | var trigger = jQuery('#layotter-top-buttons-1').offset().top; 22 | var left = jQuery('#adminmenuwrap').width(); 23 | if (scrolled > trigger) { 24 | jQuery('#layotter-top-buttons-2').addClass('layotter-visible').css('left', left); 25 | } else { 26 | jQuery('#layotter-top-buttons-2').removeClass('layotter-visible'); 27 | } 28 | }); 29 | }); -------------------------------------------------------------------------------- /views/add-element.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |

8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |

{{ element.title }}

17 | {{ element.description }} 18 |
19 |
20 |
21 |
22 | 23 |
24 |
-------------------------------------------------------------------------------- /example/element.php: -------------------------------------------------------------------------------- 1 | title = __('Example element', 'layotter'); 10 | $this->description = __('Use this element to play around and get started with Layotter.', 'layotter'); 11 | $this->icon = 'star'; 12 | $this->field_group = Layotter_ACF::get_example_field_group_name(); 13 | } 14 | 15 | protected function frontend_view($fields) { 16 | echo '
'; 17 | echo $fields['content']; 18 | echo '
'; 19 | } 20 | 21 | protected function backend_view($fields) { 22 | echo '
'; 23 | 24 | if (empty($fields['content'])) { 25 | echo '
'; 26 | _e('This element is empty. Click the edit button at the top right to add some content.', 'layotter'); 27 | echo '
'; 28 | } else { 29 | echo $fields['content']; 30 | } 31 | 32 | echo '
'; 33 | } 34 | } 35 | 36 | Layotter::register_element('layotter_example_element', 'Layotter_Example_Element'); -------------------------------------------------------------------------------- /core/views.php: -------------------------------------------------------------------------------- 1 | 14 | 21 | 28 | 35 | 42 | 49 | 2 | 3 |
4 |

5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /assets/js/app/services/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Central provider for content structure and blank templates for rows, columns and elements 3 | */ 4 | app.service('data', function(){ 5 | // use default post options for new posts 6 | if (layotterData.contentStructure === null) { 7 | this.contentStructure = { 8 | options: layotterData.options.post.defaults, 9 | rows: [] 10 | }; 11 | } else { 12 | this.contentStructure = angular.copy(layotterData.contentStructure); 13 | } 14 | 15 | 16 | // empty templates 17 | this.templates = {}; 18 | var defaultRowLayout = layotterData.defaultRowLayout; 19 | 20 | 21 | // new element template 22 | this.templates.element = { 23 | type: undefined, 24 | values: [], 25 | view: '', 26 | options: layotterData.options.element.defaults 27 | }; 28 | 29 | 30 | // new col template 31 | this.templates.col = { 32 | elements: [], 33 | options: layotterData.options.col.defaults 34 | }; 35 | 36 | 37 | // set up default cols for new row template 38 | var defaultColCount = defaultRowLayout.split(' ').length; 39 | var defaultCols = []; 40 | for (var i = 0; i < defaultColCount; i++) { 41 | defaultCols.push(angular.copy(this.templates.col)); 42 | } 43 | 44 | 45 | // new row template 46 | this.templates.row = { 47 | layout: defaultRowLayout, 48 | cols: defaultCols, 49 | options: layotterData.options.row.defaults 50 | }; 51 | }); -------------------------------------------------------------------------------- /views/load-layout.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |

8 |
9 |
10 |
11 |
12 |
13 |
14 |

{{ layout.name }}

15 | 16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /views/form.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |
8 |

{{ form.title }}

9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
-------------------------------------------------------------------------------- /core/acf-locations.php: -------------------------------------------------------------------------------- 1 | allowed_fields = $allowed_fields; 26 | $this->provided_values = $provided_values; 27 | } 28 | 29 | 30 | /** 31 | * Set human-readable title for this form 32 | * 33 | * @param string $title Form title 34 | */ 35 | public function set_title($title) { 36 | $this->title = $title; 37 | } 38 | 39 | 40 | /** 41 | * Set an icon for this form 42 | * 43 | * @param string $icon Font Awesome icon name (without the fa- prefix) 44 | */ 45 | public function set_icon($icon) { 46 | $this->icon = $icon; 47 | } 48 | 49 | 50 | /** 51 | * Get form data as array 52 | * 53 | * @return array Form data 54 | */ 55 | public function get_data() { 56 | // used in the form.php template 57 | $title = $this->title; 58 | $icon = $this->icon; 59 | $fields = array(); 60 | 61 | // loop through allowed fields and add field values to the array (where provided) 62 | foreach ($this->allowed_fields as $field) { 63 | $field_name = $field['name']; 64 | 65 | if (isset($this->provided_values[$field_name])) { 66 | $field['value'] = $this->provided_values[$field_name]; 67 | } 68 | 69 | $fields[] = $field; 70 | } 71 | 72 | return array( 73 | 'title' => $title, 74 | 'icon' => $icon, 75 | 'nonce' => wp_create_nonce('post'), 76 | 'fields' => Layotter_ACF::get_form_html($fields) 77 | ); 78 | } 79 | 80 | 81 | } -------------------------------------------------------------------------------- /assets/js/app/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('layotter', ['ngAnimate', 'ngSanitize', 'ui.sortable']); 2 | 3 | 4 | /** 5 | * Bootstrap the application 6 | */ 7 | jQuery(document).ready(function() { 8 | angular.bootstrap(document, ['layotter']); 9 | }); 10 | 11 | 12 | /** 13 | * Initialize UI after bootstrapping 14 | */ 15 | app.run(function() { 16 | // show layotter, hide loading spinner 17 | jQuery('#layotter').show(); 18 | jQuery('#layotter-loading').hide(); 19 | 20 | // make the scrollable saved elements list fill the viewport height 21 | var adjustTemplatesContainerHeight = function() { 22 | var $templatesContainer = jQuery('#layotter-templates .layotter-elements'); 23 | var height = jQuery('#layotter-templates').height() - $templatesContainer.position().top - 10; 24 | $templatesContainer.height(height); 25 | }; 26 | 27 | // adjust saved elements height on page load and browser resize 28 | adjustTemplatesContainerHeight(); 29 | jQuery(window).on('resize', function() { 30 | adjustTemplatesContainerHeight(); 31 | }); 32 | }); 33 | 34 | 35 | /** 36 | * Slide/fade in and out animations 37 | */ 38 | app.animation('.layotter-animate', function() { 39 | return { 40 | enter: function(element, done) { 41 | jQuery(element).hide().css('visibility', 'hidden').slideDown(400, function() { 42 | jQuery(this).css('visibility', 'visible').hide().fadeIn(400, done); 43 | }); 44 | }, 45 | leave: function(element, done) { 46 | jQuery(element).fadeTo(400, 0).slideUp(400, done); 47 | } 48 | }; 49 | }); 50 | 51 | 52 | /** 53 | * Show/hide saved elements sidebar 54 | */ 55 | app.directive('toggleTemplates', function(view) { 56 | return { 57 | restrict: 'A', 58 | link: function (scope, element, attrs) { 59 | element.on('click', function(){ 60 | view.toggleTemplates(); 61 | }); 62 | } 63 | }; 64 | }); 65 | 66 | 67 | /** 68 | * Show unsafe content (form fields) in raw HTML output 69 | */ 70 | app.filter('rawHtml', ['$sce', function($sce){ 71 | return function(text) { 72 | return $sce.trustAsHtml(text); 73 | }; 74 | }]); -------------------------------------------------------------------------------- /core/shortcode.php: -------------------------------------------------------------------------------- 1 | get_frontend_view(); 30 | 31 | // apply wptexturize manually after post HTML has been parsed because automatic wptexturizing is disabled for 32 | // Layotter content (see layotter_disable_wptexturize() below) 33 | return wptexturize($html); 34 | } 35 | 36 | 37 | /** 38 | * Disable wptexturize for [layotter] shortcode 39 | * 40 | * Wordpress replaces some characters with html entities, e.g. < becomes < - this breaks post previews, so we'll 41 | * disable it for Layotter contents. 42 | */ 43 | add_filter('no_texturize_shortcodes', 'layotter_disable_wptexturize'); 44 | function layotter_disable_wptexturize($shortcodes) { 45 | $shortcodes[] = 'layotter'; 46 | return $shortcodes; 47 | } 48 | 49 | 50 | /** 51 | * Disable wpautop for [layotter] shortcode 52 | * 53 | * When previewing changes to a post, Wordpress normally adds

tags that break JSON, so we'll disable 54 | */ 55 | add_filter('the_content', 'layotter_disable_wpautop', 1); 56 | function layotter_disable_wpautop($content) { 57 | if (Layotter::is_enabled_for_post(get_the_ID())) { 58 | remove_filter('the_content', 'wpautop'); 59 | } 60 | return $content; 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Layotter 2 | 3 | **Build fully customizable and easy to use Wordpress websites – with Layotter, the drag-and-drop page builder for developers!** 4 | 5 | As a professional Wordpress developer, you've probably got your own theme boilerplate, a favorite grid system, project structure, … you name it. So why should your page builder dictate its own HTML or CSS structure? Why should it come with a ton of predesigned modules that won't fit your client's design anyway? We believe it shouldn't. So Layotter doesn't. 6 | 7 | ## If you like ACF, you'll love Layotter 8 | 9 | Layotter is based on [Advanced Custom Fields (ACF)](http://www.advancedcustomfields.com), a very popular Wordpress plugin that lets you create wildly complex forms without having to write any code. Thanks to ACF, building a simple Layotter element takes as little as 15 lines of code: 10 | 11 | ```php 12 | class Text_Element extends Layotter_Element { 13 | protected function attributes() { 14 | $this->title = 'Text'; 15 | $this->description = 'A very simple text element.'; 16 | $this->icon = 'font'; // pick an icon from Font Awesome 17 | $this->field_group = 'group_abc1337'; // your ACF field group 18 | } 19 | protected function frontend_view($fields) { 20 | echo $fields['content']; // what visitors will see 21 | } 22 | protected function backend_view($fields) { 23 | echo $fields['content']; // what editors will see 24 | } 25 | } 26 | Layotter::register_element('text', 'Text_Element'); 27 | ``` 28 | 29 | Read the [introduction](http://docs.layotter.com/) or the [installation instructions](http://docs.layotter.com/getting-started/installation/) to get started, or head directly to the [tutorial on how to create an element type](http://docs.layotter.com/basics/element-types/). 30 | 31 | ## Some more features you'll enjoy 32 | 33 | * Super clean, object oriented API 34 | * Full HTML and CSS customization 35 | * Settings filters for programmatic configuration 36 | * Integrates so nicely, your clients will think it's a part of Wordpress 37 | * Works with the Pro and regular versions of ACF 38 | * Open source and free to include even in commercial themes 39 | 40 | ## By the way, Layotter 2 is in the making… 41 | 42 | Check out the (terribly named) [modular branch](https://github.com/hingst/layotter/tree/modular) to learn more about the the upcoming release. -------------------------------------------------------------------------------- /assets/js/app/controllers/templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller for the element templates sidebar 3 | */ 4 | app.controller('TemplatesCtrl', function($scope, $animate, templates, $timeout, history) { 5 | angular.extend($scope, templates); 6 | 7 | 8 | // options for jQuery UI's sortable (via ui.sortable) 9 | var savedTemplatesBackup; 10 | $scope.templateSortableOptions = { 11 | items: '.layotter-element', 12 | cancel: '.layotter-element-buttons', 13 | placeholder: 'layotter-placeholder', 14 | forcePlaceholderSize: true, 15 | revert: 300, 16 | connectWith: '#layotter .layotter-elements', 17 | helper: 'clone', 18 | start: function (event, ui) { 19 | $animate.enabled(false); // prevent animation when savedTemplatesBackup is restored after a template was dragged 20 | templates.unhighlightTemplate(ui.item.sortable.model); // unhighlight hovered template on drag start 21 | savedTemplatesBackup = angular.copy($scope.savedTemplates); // save current set of templates to be restored after sorting 22 | jQuery(ui.item).show(); // show clone while dragging 23 | jQuery(ui.item.parent()).sortable('option', 'revert', false); // prevent revert animation when dropping on saved elements list 24 | }, 25 | stop: function (event, ui) { 26 | if (ui.item.sortable.droptarget && event.target !== ui.item.sortable.droptarget[0]) { 27 | angular.extend(templates.savedTemplates, savedTemplatesBackup); // re-add element to saved elements if it was removed 28 | } 29 | history.pushStep(layotterData.i18n.history.create_element_from_template); 30 | $timeout(function(){ 31 | $animate.enabled(true); // reenable all animations after sorting 32 | }, 1); 33 | }, 34 | update: function (event, ui) { 35 | if (ui.item.sortable.droptarget && event.target === ui.item.sortable.droptarget[0]) { 36 | ui.item.sortable.cancel(); // disable sorting saved elements 37 | } 38 | }, 39 | change: function (event, ui) { 40 | if (ui.placeholder.parent()[0] !== ui.item.parent()[0]) { 41 | jQuery(ui.item.parent()).sortable('option', 'revert', 300); // enable revert animation when dropping on a column 42 | } 43 | } 44 | }; 45 | }); -------------------------------------------------------------------------------- /core/interface.php: -------------------------------------------------------------------------------- 1 | '; 39 | 40 | if (Layotter_Settings::is_debug_mode_enabled()) { 41 | echo '

'; 42 | printf(__('Debug mode enabled: Inspect and manually edit the JSON structure generated by Layotter. Use with caution. A faulty structure will break your page layout and content. Go to Layotter\'s settings page to disable debug mode.', 'layotter'), admin_url('admin.php?page=layotter-settings')); 43 | echo '

'; 44 | echo ''; 45 | } else { 46 | echo ''; 47 | } 48 | 49 | require_once __DIR__ . '/../views/editor.php'; 50 | } 51 | 52 | 53 | /** 54 | * Disables Wordpress 5 block editor for Layotter-enabled posts. 55 | * 56 | * @param bool $enabled Previous setting for the post type 57 | * @param string $post_type The post type in question 58 | * @return bool 59 | */ 60 | add_filter('use_block_editor_for_post_type', 'layotter_disable_gutenberg', 10, 2); 61 | function layotter_disable_gutenberg($enabled, $post_type) { 62 | $layotter_post_types = Layotter_Settings::get_enabled_post_types(); 63 | 64 | if (in_array($post_type, $layotter_post_types)) { 65 | return false; 66 | } 67 | 68 | return $enabled; 69 | } 70 | -------------------------------------------------------------------------------- /example/field-group.php: -------------------------------------------------------------------------------- 1 | $key, 14 | 'title' => $title, 15 | 'fields' => array( 16 | array( 17 | 'key' => 'field_5605a6602418d', 18 | 'label' => $message_label, 19 | 'name' => '', 20 | 'type' => 'message', 21 | 'instructions' => '', 22 | 'required' => 0, 23 | 'conditional_logic' => 0, 24 | 'wrapper' => array( 25 | 'width' => '', 26 | 'class' => '', 27 | 'id' => '', 28 | ), 29 | 'message' => $message, 30 | 'esc_html' => 0, 31 | ), 32 | array( 33 | 'key' => 'field_5605a6ed2418e', 34 | 'label' => $content_label, 35 | 'name' => 'content', 36 | 'type' => 'wysiwyg', 37 | 'instructions' => '', 38 | 'required' => 0, 39 | 'conditional_logic' => 0, 40 | 'wrapper' => array( 41 | 'width' => '', 42 | 'class' => '', 43 | 'id' => '', 44 | ), 45 | 'default_value' => $default_text, 46 | 'tabs' => 'all', 47 | 'toolbar' => 'full', 48 | 'media_upload' => 1, 49 | ), 50 | ), 51 | 'location' => array( 52 | array( 53 | array( 54 | 'param' => 'layotter', 55 | 'operator' => '==', 56 | 'value' => 'element', 57 | ), 58 | ), 59 | ), 60 | 'menu_order' => 0, 61 | 'position' => 'normal', 62 | 'style' => 'default', 63 | 'label_placement' => 'top', 64 | 'instruction_placement' => 'label', 65 | 'hide_on_screen' => '', 66 | 'active' => 1, 67 | 'description' => '', 68 | )); -------------------------------------------------------------------------------- /assets/js/settings.js: -------------------------------------------------------------------------------- 1 | jQuery(function($){ 2 | 3 | 4 | var selectTab = function(tab) { 5 | $('.nav-tab').removeClass('nav-tab-active'); 6 | $('.nav-tab[href="' + tab + '"]').addClass('nav-tab-active'); 7 | $('#layotter-last-edited-tab').val(tab); 8 | $('.layotter-settings-tab-content:visible').hide(); 9 | $(tab).show(); 10 | if ($('#layotter-settings-saved-notice').length) { 11 | $('#layotter-settings-saved-notice').css('visibility', 'hidden').css('display', 'block').slideUp(400, function(){ 12 | $(this).remove(); 13 | }); 14 | } 15 | }; 16 | $('.nav-tab').click(function(event){ 17 | event.preventDefault(); 18 | if (!$(this).hasClass('nav-tab-active')) { 19 | selectTab($(this).attr('href')); 20 | } 21 | }); 22 | 23 | 24 | $('.layotter-default-value').click(function(){ 25 | $(this).parents('tr').find('input, textarea').val($(this).html().replace(/</g, '<').replace(/>/g, '>')); 26 | }); 27 | 28 | 29 | function update_default_row_dropdown() { 30 | $('#layotter-row-layouts input[type="checkbox"]').each(function(){ 31 | var layout = $(this).data('layout'); 32 | var $option = $('#layotter-default-row-layout').children('option[value="' + layout + '"]'); 33 | if ($(this).is(':checked')) { 34 | $option.show(); 35 | } else { 36 | $option.hide(); 37 | if ($option.is(':selected')) { 38 | $('#layotter-default-row-layout').children('option:visible').first().prop('selected', true); 39 | display_default_row_option_message(); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | 46 | $('#layotter-row-layouts input[type="checkbox"]').change(function(){ 47 | if ($('#layotter-row-layouts input[type="checkbox"]:checked').length === 0) { 48 | $(this).prop('checked', true); 49 | return; 50 | } 51 | update_default_row_dropdown(); 52 | }); 53 | update_default_row_dropdown(); 54 | 55 | 56 | function display_default_row_option_message() { 57 | var layout = $('#layotter-default-row-layout').val(); 58 | $('#layotter-row-layouts .layotter-default-row-layout-message').hide(); 59 | $('#layotter-row-layouts input[data-layout="' + layout + '"]') 60 | .siblings('.layotter-default-row-layout-message') 61 | .show(); 62 | } 63 | 64 | 65 | $('#layotter-default-row-layout').change(function(){ 66 | display_default_row_option_message(); 67 | }); 68 | display_default_row_option_message(); 69 | 70 | }); -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | type = $type; 25 | } 26 | 27 | $fields = $this->get_fields($post_id); 28 | $this->apply_values($fields, $values); 29 | 30 | if (!empty($fields)) { 31 | $this->enabled = true; 32 | } 33 | 34 | $this->form->set_icon('cog'); 35 | $titles = array( 36 | 'post' => __('Post options', 'layotter'), 37 | 'row' => __('Row options', 'layotter'), 38 | 'col' => __('Column options', 'layotter'), 39 | 'element' => __('Element options', 'layotter') 40 | ); 41 | if (isset($titles[$this->type])) { 42 | $this->form->set_title($titles[$this->type]); 43 | } 44 | } 45 | 46 | 47 | /** 48 | * Get ACF fields for options in a specific post 49 | * 50 | * @param int $post_id Post ID 51 | * @return array ACF fields 52 | */ 53 | private function get_fields($post_id) { 54 | $post_id = intval($post_id); 55 | $post_type = get_post_type($post_id); 56 | $fields = array(); 57 | 58 | // get ACF field groups for this option and post type 59 | $field_groups = Layotter_ACF::get_filtered_field_groups(array( 60 | 'post_type' => $post_type, 61 | 'layotter' => $this->type . '_options' 62 | )); 63 | 64 | foreach ($field_groups as $field_group) { 65 | $fields = array_merge($fields, Layotter_ACF::get_fields($field_group)); 66 | } 67 | 68 | return $fields; 69 | } 70 | 71 | 72 | /** 73 | * Check if this option type is enabled for the current post (i.e. an ACF field group exists) 74 | * 75 | * @return boolean Whether options are enabled 76 | */ 77 | public function is_enabled() { 78 | return $this->enabled; 79 | } 80 | 81 | 82 | /** 83 | * Return array representation of option values for use in json_encode() 84 | * 85 | * PHP's JsonSerializable interface would be cleaner, but it's only available >= 5.4.0 86 | * 87 | * @return array Array representation of option values 88 | */ 89 | public function to_array() { 90 | return $this->clean_values; 91 | } 92 | 93 | 94 | } 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /core/revisions.php: -------------------------------------------------------------------------------- 1 | get_frontend_view(); 29 | 30 | // save JSON to a custom field (oddly enough, Wordpress breaks JSON if it's stripslashed) 31 | update_post_meta($post_id, 'layotter_json', $json); 32 | 33 | // insert spaces to prevent

foo

bar

becoming "foobar" instead of "foo bar" 34 | // then strip all tags except 35 | // then remove excess whitespace 36 | $spaced_content = str_replace('<', ' <', $content); 37 | $clean_content = strip_tags($spaced_content, ''); 38 | $normalized_content = trim($clean_content); 39 | 40 | if (function_exists('mb_ereg_replace')) { 41 | $normalized_content = mb_ereg_replace('/\s+/', ' ', $normalized_content); 42 | } 43 | 44 | // wrap search dump with a [layotter] shortcode and return modified post data to be saved to the database 45 | // add the post ID because otherwise the shortcode handler would have no reliable way to get the post ID through 46 | // which the JSON data will be fetched 47 | $shortcoded_content = '[layotter post="' . $post_id . '"]' . $normalized_content . '[/layotter]'; 48 | $data['post_content'] = $shortcoded_content; 49 | return $data; 50 | } 51 | 52 | 53 | /** 54 | * Track custom field in post revisions 55 | * 56 | * Wordpress normally doesn't track custom field data in post revisions. Layotter includes the WP Post Meta Revisions 57 | * plugin to remedy this. This filter tells the plugin to keep track of the custom field used by Layotter. 58 | * See https://wordpress.org/plugins/wp-post-meta-revisions/ 59 | */ 60 | add_filter('wp_post_revision_meta_keys', 'layotter_track_custom_field'); 61 | function layotter_track_custom_field($keys) { 62 | $keys[] = 'layotter_json'; 63 | return $keys; 64 | } -------------------------------------------------------------------------------- /assets/js/app/controllers/editor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller for the main editor 3 | */ 4 | app.controller('EditorCtrl', function($scope, $animate, data, content, templates, layouts, history) { 5 | angular.extend($scope, content, templates, layouts, history); 6 | $scope.data = data.contentStructure; 7 | 8 | 9 | $scope.$watch(function() { 10 | return history.data; 11 | }, function(value) { 12 | $scope.history = value; 13 | }); 14 | 15 | 16 | // data received from php 17 | $scope.allowedRowLayouts = layotterData.allowedRowLayouts; 18 | $scope.optionsEnabled = { 19 | post: layotterData.options.post.enabled, 20 | row: layotterData.options.row.enabled, 21 | col: layotterData.options.col.enabled, 22 | element: layotterData.options.element.enabled 23 | }; 24 | $scope.enablePostLayouts = layotterData.enablePostLayouts; 25 | $scope.enableElementTemplates = layotterData.enableElementTemplates; 26 | $scope.savedLayouts = layouts.savedLayouts; 27 | $scope.savedemplates = templates.savedTemplates; 28 | 29 | 30 | // on content change, update textarea 31 | $scope.$watch('data', function(value) { 32 | // remove views from JSON data, no need to store them as they're always regenerated before output 33 | var valueClone = angular.copy(value); 34 | angular.forEach(valueClone.rows, function(row){ 35 | angular.forEach(row.cols, function(col){ 36 | angular.forEach(col.elements, function(element){ 37 | delete element.view; 38 | }); 39 | }); 40 | }); 41 | 42 | var json = angular.toJson(valueClone, false); // change to true for pretty JSON 43 | 44 | // put shortcoded JSON into #content to make post previews work 45 | // this will be replaced with a search dump when the post is saved 46 | jQuery('#content').val('[layotter]' + json + '[/layotter]'); 47 | 48 | // enter JSON string into textarea 49 | jQuery('#layotter-json').val(json); 50 | }, true); 51 | 52 | 53 | // options for jQuery UI's sortable (via ui.sortable) 54 | $scope.rowSortableOptions = { 55 | items: '.layotter-row', 56 | placeholder: 'layotter-placeholder', 57 | forcePlaceholderSize: true, 58 | revert: 300, 59 | handle: '.layotter-row-move', 60 | stop: function(){ 61 | history.pushStep(layotterData.i18n.history.move_row); 62 | } 63 | }; 64 | $scope.elementSortableOptions = { 65 | items: '.layotter-element', 66 | cancel: '.layotter-element-buttons', 67 | placeholder: 'layotter-placeholder', 68 | forcePlaceholderSize: true, 69 | revert: 300, 70 | connectWith: '#layotter .layotter-elements', 71 | // prevent slide-in animation after moving an element 72 | start: function(){ 73 | $animate.enabled(false); 74 | }, 75 | stop: function(){ 76 | $animate.enabled(true); 77 | history.pushStep(layotterData.i18n.history.move_element); 78 | } 79 | }; 80 | }); -------------------------------------------------------------------------------- /components/row.php: -------------------------------------------------------------------------------- 1 | validate_structure($structure); 22 | $structure = $this->apply_layout($structure); 23 | 24 | $this->layout = $structure['layout']; 25 | $this->options = new Layotter_Options('row', $structure['options']); 26 | 27 | foreach ($structure['cols'] as $col) { 28 | $this->cols[] = new Layotter_Col($col); 29 | } 30 | } 31 | 32 | 33 | /** 34 | * Validate an array containing a row's structure 35 | * 36 | * Validates array structure and presence of required key/value pairs 37 | * 38 | * @param array $structure Row structure 39 | * @return array Validated row structure 40 | */ 41 | private function validate_structure($structure) { 42 | if (!is_array($structure)) { 43 | $structure = array(); 44 | } 45 | 46 | if (!isset($structure['layout']) OR !is_string($structure['layout'])) { 47 | $structure['layout'] = Layotter_Settings::get_default_row_layout(); 48 | } 49 | 50 | if (!isset($structure['options']) OR !is_array($structure['options'])) { 51 | $structure['options'] = array(); 52 | } 53 | 54 | if (!isset($structure['cols']) OR !is_array($structure['cols'])) { 55 | $structure['cols'] = array(); 56 | } 57 | 58 | return $structure; 59 | } 60 | 61 | 62 | /** 63 | * Take a row structure and apply the row layout (e.g. '1/3 1/3 1/3') to the contained columns 64 | * 65 | * @param array $structure Row structure with layout and columns 66 | * @return array Row structure with layout applied to columns 67 | */ 68 | private function apply_layout($structure) { 69 | $layout_array = explode(' ', $structure['layout']); 70 | 71 | foreach ($structure['cols'] as $i => &$col) { 72 | $col['width'] 73 | = isset($layout_array[$i]) 74 | ? $layout_array[$i] 75 | : ''; 76 | } 77 | 78 | return $structure; 79 | } 80 | 81 | 82 | /** 83 | * Return array representation of this row for use in json_encode() 84 | * 85 | * PHP's JsonSerializable interface would be cleaner, but it's only available >= 5.4.0 86 | * 87 | * @return array Array representation of this row 88 | */ 89 | public function to_array() { 90 | $cols = array(); 91 | 92 | foreach ($this->cols as $col) { 93 | $cols[] = $col->to_array(); 94 | } 95 | 96 | return array( 97 | 'layout' => $this->layout, 98 | 'options' => $this->options->to_array(), 99 | 'cols' => $cols 100 | ); 101 | } 102 | 103 | 104 | /** 105 | * Return frontend HTML for this row 106 | * 107 | * @param array $post_options Formatted options for the parent post 108 | * @return string Frontend HTML 109 | */ 110 | public function get_frontend_view($post_options) { 111 | $cols_html = ''; 112 | foreach ($this->cols as $col) { 113 | $cols_html .= $col->get_frontend_view($this->options->get_formatted_values(), $post_options); 114 | } 115 | 116 | if (has_filter('layotter/view/row')) { 117 | return apply_filters('layotter/view/row', $cols_html, $this->options->get_formatted_values(), $post_options); 118 | } else { 119 | $html_wrapper = Layotter_Settings::get_html_wrapper('rows'); 120 | return $html_wrapper['before'] . $cols_html . $html_wrapper['after']; 121 | } 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /components/col.php: -------------------------------------------------------------------------------- 1 | validate_structure($structure); 22 | 23 | $this->width = $structure['width']; 24 | $this->options = new Layotter_Options('col', $structure['options']); 25 | 26 | foreach ($structure['elements'] as $element) { 27 | $element_object = false; 28 | 29 | // if a template_id is set, try to create a template 30 | if (isset($element['template_id'])) { 31 | $element_object = Layotter_Templates::create_element($element); 32 | } 33 | 34 | // if the template doesn't exist anymore, create a regular element 35 | if (!$element_object) { 36 | $element_object = Layotter::create_element($element); 37 | } 38 | 39 | if ($element_object) { 40 | $this->elements[] = $element_object; 41 | } 42 | } 43 | } 44 | 45 | 46 | /** 47 | * Validate an array containing a columns's structure 48 | * 49 | * Validates array structure and presence of required key/value pairs 50 | * 51 | * @param array $structure Column structure 52 | * @return array Validated column structure 53 | */ 54 | private function validate_structure($structure) { 55 | if (!is_array($structure)) { 56 | $structure = array(); 57 | } 58 | 59 | if (!isset($structure['width']) OR !is_string($structure['width'])) { 60 | $structure['width'] = ''; 61 | } 62 | 63 | if (!isset($structure['options']) OR !is_array($structure['options'])) { 64 | $structure['options'] = array(); 65 | } 66 | 67 | if (!isset($structure['elements']) OR !is_array($structure['elements'])) { 68 | $structure['elements'] = array(); 69 | } 70 | 71 | return $structure; 72 | } 73 | 74 | 75 | /** 76 | * Return array representation of this column for use in json_encode() 77 | * 78 | * PHP's JsonSerializable interface would be cleaner, but it's only available >= 5.4.0 79 | * 80 | * @return array Array representation of this column 81 | */ 82 | public function to_array() { 83 | $elements = array(); 84 | 85 | foreach ($this->elements as $element) { 86 | $elements[] = $element->to_array(); 87 | } 88 | 89 | return array( 90 | 'options' => $this->options->to_array(), 91 | 'elements' => $elements 92 | ); 93 | } 94 | 95 | 96 | /** 97 | * Return frontend HTML for this column 98 | * 99 | * @param array $row_options Formatted options for the parent row 100 | * @param array $post_options Formatted options for the parent post 101 | * @return string Frontend HTML 102 | */ 103 | public function get_frontend_view($row_options, $post_options) { 104 | $elements_html = ''; 105 | foreach ($this->elements as $element) { 106 | $elements_html .= $element->get_frontend_view($this->options->get_formatted_values(), $row_options, $post_options, $this->width); 107 | } 108 | 109 | $class = Layotter_Settings::get_col_layout_class($this->width); 110 | 111 | if (has_filter('layotter/view/column')) { 112 | return apply_filters('layotter/view/column', $elements_html, $class, $this->options->get_formatted_values(), $row_options, $post_options); 113 | } else { 114 | $html_wrapper = Layotter_Settings::get_html_wrapper('cols'); 115 | $html_before = str_replace('%%CLASS%%', $class, $html_wrapper['before']); 116 | return $html_before . $elements_html . $html_wrapper['after']; 117 | } 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /assets/js/vendor/angular-sanitize.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.26 3 | (c) 2010-2014 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(q,g,r){'use strict';function F(a){var d=[];t(d,g.noop).chars(a);return d.join("")}function m(a){var d={};a=a.split(",");var c;for(c=0;c=c;e--)d.end&&d.end(f[e]);f.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,l,f=[],n=a,h;for(f.last=function(){return f[f.length-1]};a;){h="";l=!0;if(f.last()&&y[f.last()])a=a.replace(RegExp("(.*)<\\s*\\/\\s*"+f.last()+"[^>]*>","i"),function(a,b){b=b.replace(I,"$1").replace(J,"$1");d.chars&&d.chars(s(b));return""}),e("",f.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e",b)===b&&(d.comment&&d.comment(a.substring(4, 8 | b)),a=a.substring(b+3),l=!1);else if(z.test(a)){if(b=a.match(z))a=a.replace(b[0],""),l=!1}else if(K.test(a)){if(b=a.match(A))a=a.substring(b[0].length),b[0].replace(A,e),l=!1}else L.test(a)&&((b=a.match(B))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(B,c)),l=!1):(h+="<",a=a.substring(1)));l&&(b=a.indexOf("<"),h+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),d.chars&&d.chars(s(h)))}if(a==n)throw M("badparse",a);n=a}e()}function s(a){if(!a)return"";var d=N.exec(a);a=d[1];var c=d[3];if(d=d[2])p.innerHTML= 9 | d.replace(//g,">")}function t(a,d){var c=!1,e=g.bind(a,a.push);return{start:function(a,l,f){a=g.lowercase(a);!c&&y[a]&&(c=a);c||!0!==D[a]||(e("<"),e(a),g.forEach(l,function(c,f){var k= 10 | g.lowercase(f),l="img"===a&&"src"===k||"background"===k;!0!==Q[k]||!0===E[k]&&!d(c,l)||(e(" "),e(f),e('="'),e(C(c)),e('"'))}),e(f?"/>":">"))},end:function(a){a=g.lowercase(a);c||!0!==D[a]||(e(""));a==c&&(c=!1)},chars:function(a){c||e(C(a))}}}var M=g.$$minErr("$sanitize"),B=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,A=/^<\/\s*([\w:-]+)[^>]*>/,H=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,L=/^]*?)>/i,J=/"]/,c=/^mailto:/;return function(e,b){function l(a){a&&k.push(F(a))}function f(a,c){k.push("');l(c);k.push("")} 14 | if(!e)return e;for(var n,h=e,k=[],m,p;n=h.match(d);)m=n[0],n[2]==n[3]&&(m="mailto:"+m),p=n.index,l(h.substr(0,p)),f(m,n[0].replace(c,"")),h=h.substring(p+n[0].length);l(h);return a(k.join(""))}}])})(window,window.angular); 15 | //# sourceMappingURL=angular-sanitize.min.js.map 16 | -------------------------------------------------------------------------------- /assets/js/vendor/jquery.serialize-object.compiled.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery serializeObject 3 | * @copyright 2014, macek 4 | * @link https://github.com/macek/jquery-serialize-object 5 | * @license BSD 6 | * @version 2.5.0 7 | */ 8 | (function(root, factory) { 9 | 10 | // AMD 11 | if (typeof define === "function" && define.amd) { 12 | define(["exports", "jquery"], function(exports, $) { 13 | return factory(exports, $); 14 | }); 15 | } 16 | 17 | // CommonJS 18 | else if (typeof exports !== "undefined") { 19 | var $ = require("jquery"); 20 | factory(exports, $); 21 | } 22 | 23 | // Browser 24 | else { 25 | factory(root, (root.jQuery || root.Zepto || root.ender || root.$)); 26 | } 27 | 28 | }(this, function(exports, $) { 29 | 30 | var patterns = { 31 | validate: /^[a-z_][a-z0-9_]*(?:\[(?:\d*|[a-z0-9_]+)\])*$/i, 32 | key: /[a-z0-9_]+|(?=\[\])/gi, 33 | push: /^$/, 34 | fixed: /^\d+$/, 35 | named: /^[a-z0-9_]+$/i 36 | }; 37 | 38 | function FormSerializer(helper, $form) { 39 | 40 | // private variables 41 | var data = {}, 42 | pushes = {}; 43 | 44 | // private API 45 | function build(base, key, value) { 46 | base[key] = value; 47 | return base; 48 | } 49 | 50 | function makeObject(root, value) { 51 | 52 | var keys = root.match(patterns.key), k; 53 | 54 | // nest, nest, ..., nest 55 | while ((k = keys.pop()) !== undefined) { 56 | // foo[] 57 | if (patterns.push.test(k)) { 58 | var idx = incrementPush(root.replace(/\[\]$/, '')); 59 | value = build([], idx, value); 60 | } 61 | 62 | // foo[n] 63 | /* 64 | else if (patterns.fixed.test(k)) { 65 | value = build([], k, value); 66 | } 67 | */ 68 | 69 | // foo; foo[bar] 70 | else if (patterns.named.test(k)) { 71 | value = build({}, k, value); 72 | } 73 | } 74 | 75 | return value; 76 | } 77 | 78 | function incrementPush(key) { 79 | if (pushes[key] === undefined) { 80 | pushes[key] = 0; 81 | } 82 | return pushes[key]++; 83 | } 84 | 85 | function encode(pair) { 86 | switch ($('[name="' + pair.name + '"]', $form).attr("type")) { 87 | case "checkbox": 88 | return pair.value === "on" ? true : pair.value; 89 | default: 90 | return pair.value; 91 | } 92 | } 93 | 94 | function addPair(pair) { 95 | if (!patterns.validate.test(pair.name)) return this; 96 | var obj = makeObject(pair.name, encode(pair)); 97 | data = helper.extend(true, data, obj); 98 | return this; 99 | } 100 | 101 | function addPairs(pairs) { 102 | if (!helper.isArray(pairs)) { 103 | throw new Error("formSerializer.addPairs expects an Array"); 104 | } 105 | for (var i=0, len=pairs.length; i 0); 71 | }; 72 | 73 | 74 | /** 75 | * Check if redo is available 76 | */ 77 | var canRedo = function() { 78 | return (currentStep < steps.length - 1); 79 | }; 80 | 81 | 82 | /** 83 | * Add an undo-able step, must be called after any changes to the content structure 84 | */ 85 | this.pushStep = function(title) { 86 | // remove all steps that have previously been undone 87 | if (canRedo()) { 88 | steps.splice(currentStep + 1, steps.length); 89 | } 90 | 91 | steps.push({ 92 | title : title, 93 | content: angular.copy(data.contentStructure) 94 | }); 95 | currentStep++; 96 | updateData(); 97 | }; 98 | this.pushStep('Loaded page'); 99 | 100 | 101 | /** 102 | * Undo a step 103 | */ 104 | this.undoStep = function() { 105 | if (canUndo()) { 106 | $animate.enabled(false); 107 | 108 | currentStep--; 109 | var restore = angular.copy(steps[currentStep].content); 110 | restore = refreshTemplates(restore); 111 | data.contentStructure.options = restore.options; 112 | data.contentStructure.rows = restore.rows; 113 | updateData(); 114 | 115 | $timeout(function(){ 116 | $animate.enabled(true); 117 | }, 1); 118 | } 119 | }; 120 | 121 | 122 | /** 123 | * Redo a step 124 | */ 125 | this.redoStep = function() { 126 | if (canRedo()) { 127 | $animate.enabled(false); 128 | 129 | currentStep++; 130 | var restore = angular.copy(steps[currentStep].content); 131 | restore = refreshTemplates(restore); 132 | data.contentStructure.options = restore.options; 133 | data.contentStructure.rows = restore.rows; 134 | updateData(); 135 | 136 | $timeout(function(){ 137 | $animate.enabled(true); 138 | }, 1); 139 | } 140 | }; 141 | }); -------------------------------------------------------------------------------- /assets/js/app/services/modals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides methos to create confirm() and alert() style modals 3 | */ 4 | app.service('modals', function($compile, $rootScope, $timeout){ 5 | 6 | 7 | // when enter is pressend in a prompt, submit the value 8 | angular.element(document).on('keydown', '#layotter-modal-prompt-input', function(e){ 9 | if (e.keyCode === 13) { 10 | $rootScope.prompt.okAction(); 11 | } 12 | }); 13 | 14 | 15 | // when ESC is pressed, cancel confirmation or close prompt 16 | angular.element(document).on('keyup', function(e){ 17 | if (e.keyCode === 27) { 18 | if (angular.element('.layotter-modal-confirm').length) { 19 | $rootScope.confirm.cancelAction(); 20 | } else if (angular.element('.layotter-modal-prompt').length) { 21 | $rootScope.prompt.cancelAction(); 22 | } 23 | } 24 | }); 25 | 26 | 27 | /** 28 | * Create a confirm()-style modal prompting the user to input a string 29 | * 30 | * @param options Confirmation message as well as OK and cancel button texts and actions 31 | */ 32 | this.prompt = function(options) { 33 | // try to create the lightbox 34 | if (!create(angular.element('#layotter-modal-prompt').html())) { 35 | return; 36 | } 37 | 38 | $rootScope.prompt = { 39 | message: options.message, 40 | initialValue: options.initialValue, 41 | okText: options.okText, 42 | cancelText: options.cancelText, 43 | okAction: function() { 44 | var $input = angular.element('#layotter-modal-prompt-input'); 45 | var value = $input.val(); 46 | 47 | if (value == '') { 48 | $input.focus(); 49 | return; 50 | } 51 | 52 | close(); 53 | if (typeof options.okAction === 'function') { 54 | var newValue = value; 55 | options.okAction(newValue); 56 | } 57 | }, 58 | cancelAction: function() { 59 | close(); 60 | if (typeof options.cancelAction === 'function') { 61 | options.cancelAction(); 62 | } 63 | } 64 | }; 65 | 66 | // without the $timeout, pressing ESC in the input field would remove the value 67 | $timeout(function(){ 68 | angular.element('#layotter-modal-prompt-input').select().focus(); 69 | }, 1); 70 | }; 71 | 72 | 73 | /** 74 | * Create a confirm()-style modal 75 | * 76 | * @param options Confirmation message as well as OK and cancel button texts and actions 77 | */ 78 | this.confirm = function(options) { 79 | // try to create the lightbox 80 | if (!create(angular.element('#layotter-modal-confirm').html())) { 81 | return; 82 | } 83 | 84 | $rootScope.confirm = { 85 | message: options.message, 86 | okText: options.okText, 87 | cancelText: options.cancelText, 88 | okAction: function() { 89 | close(); 90 | if (typeof options.okAction === 'function') { 91 | options.okAction(); 92 | } 93 | }, 94 | cancelAction: function() { 95 | close(); 96 | if (typeof options.cancelAction === 'function') { 97 | options.cancelAction(); 98 | } 99 | } 100 | }; 101 | 102 | angular.element('#layotter-modal-confirm-button').focus(); 103 | }; 104 | 105 | 106 | /** 107 | * Open up the lightbox 108 | * 109 | * @param content HTML string to be displayed 110 | */ 111 | var create = function(content) { 112 | // only one instance is allowed at any time 113 | if (angular.element('#dennisbox-modal').length) { 114 | return false; 115 | } 116 | 117 | var box = angular.element('
'); 118 | var topOffset = parseInt(angular.element(document).scrollTop() + angular.element(window).height() / 2); // vertical center of current screen 119 | 120 | box.appendTo('body') 121 | .find('.dennisbox-content') 122 | .html(content) 123 | .css('height', 210) 124 | .css('width', 350) 125 | .css('margin-top', -90) 126 | .css('top', topOffset) 127 | .css('opacity', 0) 128 | .animate({ marginTop: -100, opacity: 1 }, 300); 129 | 130 | // compile lightbox contents 131 | $timeout(function(){ 132 | $rootScope.$apply($compile(angular.element('#dennisbox-modal'))($rootScope)); 133 | },1); 134 | 135 | return true; 136 | }; 137 | 138 | 139 | /** 140 | * Close the lightbox 141 | */ 142 | var close = function() { 143 | var box = angular.element('#dennisbox-modal'); 144 | if (box.length) { 145 | box.children().fadeOut(300, function(){ 146 | angular.element(this).parent().remove(); 147 | }); 148 | } 149 | } 150 | 151 | 152 | }); -------------------------------------------------------------------------------- /core/layouts.php: -------------------------------------------------------------------------------- 1 | $id, // redundant, but simplifies handling in JS 115 | 'name' => $name, 116 | 'json' => $json, 117 | 'time_created' => time() 118 | ); 119 | 120 | update_option('layotter_post_layouts', $layouts); 121 | 122 | return $layouts[$id]; 123 | } 124 | 125 | 126 | /** 127 | * Rename an existing post layout 128 | * 129 | * @param int $id Layout ID 130 | * @param string $name Human-readable new name 131 | * @return array|bool Array with new layout data, or false on failure 132 | */ 133 | public static function rename($id, $name) { 134 | $layouts = get_option('layotter_post_layouts'); 135 | 136 | if (!is_array($layouts)) { 137 | return false; 138 | } 139 | 140 | if (!is_int($id) OR !is_string($name)) { 141 | return false; 142 | } 143 | 144 | if (isset($layouts[$id]) AND is_array($layouts[$id])) { 145 | $layouts[$id]['name'] = $name; 146 | update_option('layotter_post_layouts', $layouts); 147 | return array( 148 | 'name' => $name 149 | ); 150 | } 151 | 152 | return false; 153 | } 154 | 155 | 156 | /** 157 | * Delete an existing post layout 158 | * 159 | * @param int $id Layout ID 160 | * @return bool Was the layout deleted? 161 | */ 162 | public static function delete($id) { 163 | $layouts = get_option('layotter_post_layouts'); 164 | 165 | if (!is_array($layouts)) { 166 | return false; 167 | } 168 | 169 | if (!is_int($id)) { 170 | return false; 171 | } 172 | 173 | if (isset($layouts[$id])) { 174 | $layouts[$id] = null; 175 | update_option('layotter_post_layouts', $layouts); 176 | return true; 177 | } 178 | 179 | return false; 180 | } 181 | 182 | 183 | } -------------------------------------------------------------------------------- /assets/js/app/services/layouts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * All things related to post layouts 3 | */ 4 | app.service('layouts', function($rootScope, $http, $animate, $timeout, data, forms, modals, state, history){ 5 | 6 | 7 | var _this = this; 8 | 9 | 10 | // data received from php 11 | this.savedLayouts = layotterData.savedLayouts; 12 | 13 | 14 | /** 15 | * Save current post content as a new layout 16 | */ 17 | this.saveNewLayout = function() { 18 | var json = angular.toJson(data.contentStructure); 19 | 20 | modals.prompt({ 21 | message: layotterData.i18n.save_new_layout_confirmation, 22 | initialValue: angular.element('#title').val(), 23 | okText: layotterData.i18n.save_layout, 24 | okAction: function(value) { 25 | angular.element('.layotter-save-layout-button-wrapper').addClass('layotter-loading'); 26 | $http({ 27 | url: ajaxurl + '?action=layotter_save_new_layout', 28 | method: 'POST', 29 | data: { 30 | name: value, 31 | json: json 32 | } 33 | }).success(function(reply) { 34 | _this.savedLayouts.push(reply); 35 | angular.element('.layotter-save-layout-button-wrapper').removeClass('layotter-loading'); 36 | }); 37 | }, 38 | cancelText: layotterData.i18n.cancel 39 | }); 40 | }; 41 | 42 | 43 | /** 44 | * Show a list of existing post layouts 45 | */ 46 | this.loadLayout = function() { 47 | forms.showHTML(angular.element('#layotter-load-layout').html()); 48 | }; 49 | 50 | 51 | /** 52 | * Triggered when selecting an existing post layout to be loaded 53 | */ 54 | this.selectSavedLayout = function(layout) { 55 | var id = layout.layout_id; 56 | 57 | // ask for confirmation if current post content is not empy 58 | if (data.contentStructure.rows.length === 0) { 59 | _this.loadSelectedLayout(id); 60 | } else { 61 | modals.confirm({ 62 | message: layotterData.i18n.load_layout_confirmation, 63 | okText: layotterData.i18n.load_layout, 64 | okAction: function(){ 65 | _this.loadSelectedLayout(id); 66 | }, 67 | cancelText: layotterData.i18n.cancel 68 | }); 69 | } 70 | }; 71 | 72 | 73 | /** 74 | * Fetch data for an existing post layout and overwrite current content 75 | */ 76 | this.loadSelectedLayout = function(id) { 77 | state.reset(); 78 | angular.element('#layotter').addClass('layotter-loading'); 79 | 80 | $http({ 81 | url: ajaxurl + '?action=layotter_load_layout', 82 | method: 'POST', 83 | data: { 84 | layout_id: id 85 | } 86 | }).success(function(reply) { 87 | $animate.enabled(false); 88 | data.contentStructure.options = reply.options; 89 | data.contentStructure.rows = reply.rows; 90 | $timeout(function(){ 91 | $animate.enabled(true); 92 | }, 1); 93 | angular.element('#layotter').removeClass('layotter-loading'); 94 | history.pushStep(layotterData.i18n.history.load_post_layout); 95 | }); 96 | }; 97 | 98 | 99 | /** 100 | * Rename an existing post layout 101 | */ 102 | this.renameLayout = function(index, $event) { 103 | $event.stopPropagation(); 104 | var layout = _this.savedLayouts[index]; 105 | 106 | modals.prompt({ 107 | message: layotterData.i18n.rename_layout_confirmation, 108 | initialValue: layout.name, 109 | okText: layotterData.i18n.rename_layout, 110 | okAction: function(value) { 111 | var id = layout.layout_id; 112 | layout.isLoading = true; 113 | 114 | $http({ 115 | url: ajaxurl + '?action=layotter_rename_layout', 116 | method: 'POST', 117 | data: { 118 | layout_id: id, 119 | name: value 120 | } 121 | }).success(function(reply) { 122 | layout.isLoading = undefined; 123 | layout.name = reply.name; 124 | }); 125 | }, 126 | cancelText: layotterData.i18n.cancel 127 | }); 128 | }; 129 | 130 | 131 | /** 132 | * Delete a post layout 133 | */ 134 | this.deleteLayout = function(index, $event) { 135 | $event.stopPropagation(); 136 | var layout = _this.savedLayouts[index]; 137 | 138 | modals.confirm({ 139 | message: layotterData.i18n.delete_layout_confirmation, 140 | okText: layotterData.i18n.delete_layout, 141 | okAction: function(){ 142 | var id = layout.layout_id; 143 | layout.isLoading = true; 144 | 145 | $http({ 146 | url: ajaxurl + '?action=layotter_delete_layout', 147 | method: 'POST', 148 | data: { 149 | layout_id: id 150 | } 151 | }).success(function() { 152 | _this.savedLayouts.splice(index, 1); 153 | }); 154 | }, 155 | cancelText: layotterData.i18n.cancel 156 | }); 157 | }; 158 | 159 | }); -------------------------------------------------------------------------------- /components/editable.php: -------------------------------------------------------------------------------- 1 | clean_values and $this->formatted values, create $this->form 17 | * 18 | * @param array $fields Existing fields as provided by an ACF field group 19 | * @param array $values Array with user-provided values, or empty array if dealing with a new element 20 | */ 21 | final protected function apply_values($fields, $values) { 22 | if (!is_array($fields)) { 23 | $fields = array(); 24 | } 25 | 26 | if (!is_array($values)) { 27 | $values = array(); 28 | } 29 | 30 | // parse provided values for use in different contexts 31 | $this->clean_values = $this->clean_values($fields, $values); 32 | $this->formatted_values = $this->format_values($fields, $this->clean_values); 33 | 34 | // create edit form 35 | $this->form = new Layotter_Form($fields, $this->clean_values); 36 | } 37 | 38 | 39 | /** 40 | * Clean user-provided values 41 | * 42 | * This method normalizes an array of user-provided field values. The return value is an array where the keys are 43 | * human-readable field names (as provided by the user in an ACF field group), and the values are unfiltered data 44 | * of any type (as provided by an element edit form or an existing post's JSON data). 45 | * 46 | * @param array $existing_fields Existing fields as provided by an ACF field group 47 | * @param array $provided_values Array with user-provided values, or empty array if dealing with a new element 48 | * @return array Clean values for use in forms and JSON 49 | */ 50 | final protected static function clean_values($existing_fields, $provided_values = array()) { 51 | $values = array(); 52 | 53 | // loop through existing fields and see if there's a user provided value for each one 54 | foreach ($existing_fields as $field_data) { 55 | $field_name = $field_data['name']; 56 | $field_key = $field_data['key']; 57 | 58 | // skip ACF 'tab' and 'message' fields to prevent pollution of $values with empty keys and values 59 | if (empty($field_name)) { 60 | continue; 61 | } 62 | 63 | // assign a value to this field 64 | if (isset($provided_values[$field_name])) { 65 | // either user provided value identified by name ... 66 | // (when $provided_values came from an existing post's JSON data) 67 | $values[$field_name] = $provided_values[$field_name]; 68 | } else if (isset($provided_values[$field_key])) { 69 | // ... or user provided value identified by ACF key ... 70 | // (when $provided_values came from an edit form generated by ACF) 71 | $values[$field_name] = $provided_values[$field_key]; 72 | } else if (isset($field_data['default_value'])) { 73 | // ... or set to default value ... 74 | $values[$field_name] = $field_data['default_value']; 75 | } else { 76 | // ... or fallback to null, to make sure all fields are present in the array 77 | $values[$field_name] = null; 78 | } 79 | 80 | // quick fix for broken Repeater fields 81 | if ($field_data['type'] == 'repeater' && is_array($values[$field_name])) { 82 | $values[$field_name] = array_values($values[$field_name]); 83 | } 84 | 85 | // note: 86 | // in default ACF, field values are run through the acf/validate_value and acf/update_value filters 87 | // before saving them to the database 88 | // these filters can break fields in Layotter's context and are therefore not applied 89 | } 90 | 91 | return $values; 92 | } 93 | 94 | 95 | /** 96 | * Format user-provided values for output 97 | * 98 | * @param array $existing_fields Existing fields as provided by an ACF field group 99 | * @param array $clean_values Array with clean values (that were run through $this->clean_values() first) 100 | * @return array Formatted values for output 101 | */ 102 | final protected static function format_values($existing_fields, $clean_values) { 103 | $values = array(); 104 | 105 | // run all provided values through formatting filters 106 | foreach ($existing_fields as $field_data) { 107 | $field_name = $field_data['name']; 108 | 109 | // skip ACF 'tab' and 'message' fields to prevent pollution of $values with empty keys and values 110 | if (empty($field_name)) { 111 | continue; 112 | } 113 | 114 | // note: 115 | // in default ACF, field values are run through the acf/load_value filter before formatting 116 | // this filter can break fields in Layotter's context and is therefore not applied 117 | 118 | // format values using ACF's formatting filters 119 | $values[$field_name] = Layotter_ACF::format_value($clean_values[$field_name], $field_data); 120 | } 121 | 122 | return $values; 123 | } 124 | 125 | 126 | /** 127 | * Get clean values 128 | * 129 | * @return array Clean values 130 | */ 131 | final public function get_clean_values() { 132 | return $this->clean_values; 133 | } 134 | 135 | 136 | /** 137 | * Get formatted values 138 | * 139 | * @return array Formatted values 140 | */ 141 | final public function get_formatted_values() { 142 | return $this->formatted_values; 143 | } 144 | 145 | 146 | /** 147 | * Output edit form for this component 148 | */ 149 | final public function get_form_data() { 150 | return $this->form->get_data(); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /assets/js/app/services/forms.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides methods for form overlay creation 3 | */ 4 | app.service('forms', function($http, $compile, $rootScope, $timeout){ 5 | 6 | 7 | var _this = this; 8 | this.data = {}; // contains field data for the form that's currently being displayed 9 | 10 | 11 | // talk to ACF for form validation and submit 12 | 13 | // prevent form changes while validation is running 14 | angular.element(document).on('submit', '#layotter-edit', function(event){ 15 | event.preventDefault(); 16 | jQuery(':focus').blur(); 17 | jQuery('.layotter-modal-loading-container').addClass('layotter-loading'); 18 | jQuery('.layotter-modal-foot button').prop('disabled', true); 19 | }); 20 | 21 | // allow form changes after ACF validation is complete 22 | acf.addFilter('validation_complete', function(validation) { 23 | angular.element('.layotter-modal-loading-container').removeClass('layotter-loading'); 24 | angular.element('.layotter-modal-foot button').prop('disabled', false); 25 | return validation; 26 | }); 27 | 28 | // submit form after successful validation 29 | acf.addAction('validation_success', function(e){ 30 | if (e[0].id === 'layotter-edit') { 31 | angular.element('#layotter-edit-submit').trigger('click'); 32 | } 33 | }); 34 | 35 | 36 | /** 37 | * Display HTML content in the lightbox 38 | * 39 | * @param html HTML string 40 | */ 41 | this.showHTML = function(html) { 42 | create(html); 43 | }; 44 | 45 | 46 | /** 47 | * Fetch HTML via POST and display the result 48 | * 49 | * @param url URL to get content from 50 | * @param data POST data 51 | */ 52 | this.fetchDataAndShowForm = function(url, data) { 53 | create(); // loading 54 | $http.post(url, data).success(function(reply){ 55 | _this.data = reply; 56 | create(jQuery('#layotter-form').html()); 57 | }); 58 | }; 59 | 60 | 61 | /** 62 | * Open up the lightbox and show passed HTML content 63 | */ 64 | var create = function(html) { 65 | // animate if opening a new lightbox, don't animate if replacing another lightbox 66 | var animate = true; 67 | if (angular.element('#dennisbox').length) { 68 | angular.element('#dennisbox').remove(); 69 | animate = false; 70 | } 71 | 72 | var box = angular.element('
'); 73 | var topOffset = parseInt(angular.element(document).scrollTop() + angular.element(window).height() / 2); // vertical center of current screen 74 | var height = parseInt(angular.element(window).height() * 0.8); // 80% of viewport height 75 | var marginTop = animate ? (-height/2) + 10 : -height/2; // add a bit of an offset if animation is enabled 76 | 77 | box.appendTo('body'); 78 | var contentBox = box.children('.dennisbox-content'); 79 | 80 | contentBox 81 | .css('height', height) 82 | .css('width', 700) 83 | .css('margin-top', marginTop) 84 | .css('top', topOffset); 85 | 86 | // fade and slide in if animation is enabled 87 | if (animate) { 88 | contentBox 89 | .css('opacity', 0) 90 | .animate({ marginTop: -height/2, opacity: 1 }, 300); 91 | } 92 | 93 | // undefined content means "just show a loading spinner for now" 94 | if (typeof html === 'undefined') { 95 | contentBox.addClass('dennisbox-loading'); 96 | return; 97 | } else if (typeof html === 'string') { 98 | contentBox.html(html); 99 | } 100 | 101 | // compile lightbox contents 102 | $timeout(function(){ 103 | $rootScope.$apply($compile(angular.element('#dennisbox'))($rootScope)); 104 | 105 | // setup javascript for fields 106 | acf.get_fields({}, jQuery('#layotter-form')).each(function() { 107 | acf.do_action('ready_field', jQuery(this)); 108 | acf.do_action('ready_field/type=' + acf.get_field_type(jQuery(this)), jQuery(this)); 109 | }); 110 | acf.do_action('append', jQuery('#layotter-edit')); 111 | }, 1); 112 | }; 113 | 114 | 115 | /** 116 | * Toggle fullscreen editing 117 | */ 118 | this.toggleFullscreen = function() { 119 | var box = jQuery('#dennisbox .dennisbox-content'); 120 | var isFullscreen = box.outerWidth() !== 700; 121 | var top, height, marginTop; 122 | 123 | if (isFullscreen) { 124 | top = parseInt(angular.element(document).scrollTop() + angular.element(window).height() / 2); // vertical center of current screen 125 | height = parseInt(angular.element(window).height() * 0.8); // 80% of viewport height 126 | marginTop = -height/2; 127 | box.animate({ 128 | height: height, 129 | width: 700, 130 | marginTop: marginTop, 131 | top: top 132 | }, function() { 133 | jQuery('.layotter-modal-head-fullscreen-expand').show(); 134 | jQuery('.layotter-modal-head-fullscreen-compress').hide(); 135 | }); 136 | } else { 137 | top = jQuery(document).scrollTop(); 138 | box.animate({ 139 | height: '100%', 140 | width: '100%', 141 | marginTop: 0, 142 | top: top 143 | }, function() { 144 | jQuery('.layotter-modal-head-fullscreen-expand').hide(); 145 | jQuery('.layotter-modal-head-fullscreen-compress').show(); 146 | }); 147 | } 148 | }; 149 | 150 | 151 | /** 152 | * Close the lightbox 153 | */ 154 | this.close = function() { 155 | var box = angular.element('#dennisbox'); 156 | if (box.length) { 157 | box.children().fadeOut(300, function(){ 158 | angular.element(this).parent().remove(); 159 | }); 160 | } 161 | } 162 | 163 | 164 | }); -------------------------------------------------------------------------------- /assets/js/app/services/templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * All things related to element templates 3 | */ 4 | app.service('templates', function($rootScope, $http, $animate, $timeout, view, forms, modals, state, data, history) { 5 | 6 | 7 | var _this = this; 8 | 9 | 10 | // data received from php 11 | this.savedTemplates = layotterData.savedTemplates; 12 | 13 | 14 | /** 15 | * Show edit form for an $element template 16 | */ 17 | this.editTemplate = function(element) { 18 | modals.confirm({ 19 | message: layotterData.i18n.edit_template_confirmation, 20 | okText: layotterData.i18n.edit_template, 21 | okAction: function() { 22 | state.setElement(element); 23 | forms.fetchDataAndShowForm(ajaxurl + '?action=layotter_edit_template', { 24 | template_id: element.template_id 25 | }); 26 | }, 27 | cancelText: layotterData.i18n.cancel 28 | }); 29 | }; 30 | 31 | 32 | /** 33 | * Delete element template at $index 34 | */ 35 | this.deleteTemplate = function(index) { 36 | modals.confirm({ 37 | message: layotterData.i18n.delete_template_confirmation, 38 | okText: layotterData.i18n.delete_template, 39 | okAction: function() { 40 | _this.savedTemplates[index].isLoading = true; 41 | $http({ 42 | url: ajaxurl + '?action=layotter_delete_template', 43 | method: 'POST', 44 | data: { 45 | template_id: _this.savedTemplates[index].template_id 46 | } 47 | }).success(function(reply) { 48 | history.deletedTemplates.push(_this.savedTemplates[index].template_id); 49 | _this.savedTemplates[index].type = reply.type; 50 | _this.savedTemplates[index].values = reply.values; 51 | _this.savedTemplates[index].view = reply.view; 52 | _this.savedTemplates[index].isLoading = undefined; 53 | _this.savedTemplates[index].isHighlighted = undefined; 54 | _this.savedTemplates[index].template_deleted = true; 55 | _this.savedTemplates.splice(index, 1); 56 | if (_this.savedTemplates.length == 0) { 57 | jQuery('#layotter-templates').removeClass('layotter-visible'); 58 | } 59 | }); 60 | }, 61 | cancelText: layotterData.i18n.cancel 62 | }); 63 | }; 64 | 65 | 66 | /** 67 | * Create a new template from an existing element's data 68 | */ 69 | this.saveNewTemplate = function(element) { 70 | delete element.template_deleted; 71 | element.isLoading = true; 72 | view.showTemplates(); 73 | $http({ 74 | url: ajaxurl + '?action=layotter_save_new_template', 75 | method: 'POST', 76 | data: { 77 | type: element.type, 78 | values: element.values 79 | } 80 | }).success(function(reply) { 81 | $animate.enabled(false); 82 | _this.savedTemplates.push(angular.copy(reply)); 83 | $timeout(function() { 84 | $animate.enabled(true); 85 | }, 1); 86 | element.isLoading = undefined; 87 | element.template_id = reply.template_id; 88 | element.type = undefined; 89 | element.values = undefined; 90 | _this.watchTemplate(element); 91 | history.pushStep(layotterData.i18n.history.save_element_as_template); 92 | }); 93 | }; 94 | 95 | 96 | /** 97 | * Save template data from the form that's currently being displayed 98 | */ 99 | this.saveTemplate = function() { 100 | // copy editing.element so state can be reset while ajax is still loading 101 | var editingElement = state.getElement(); 102 | state.reset(); 103 | editingElement.isLoading = true; 104 | 105 | var values = jQuery('#layotter-edit, .layotter-modal #post').serialize() 106 | + '&' 107 | + jQuery.param({ 108 | template_id: editingElement.template_id 109 | }); 110 | 111 | $http({ 112 | url: ajaxurl + '?action=layotter_update_template', 113 | method: 'POST', 114 | data: values, 115 | headers: { 116 | 'Content-Type': 'application/x-www-form-urlencoded' 117 | } 118 | }).success(function(reply) { 119 | editingElement.view = reply.view; 120 | editingElement.isLoading = undefined; 121 | }); 122 | }; 123 | 124 | 125 | /** 126 | * Highlight a template instance (triggered when hovering over the template in the sidebar) 127 | */ 128 | this.highlightTemplate = function(element) { 129 | element.isHighlighted = true; 130 | }; 131 | this.unhighlightTemplate = function(element) { 132 | element.isHighlighted = undefined; 133 | }; 134 | 135 | 136 | /** 137 | * Watch all template instances to be able to highlight them when hovering over the template in the sidebar 138 | * 139 | * TODO: improve performance, currently all elements are inspected every time a template changes 140 | */ 141 | this.watchTemplate = function(template) { 142 | $rootScope.$watch(function() { 143 | return template; 144 | }, function(value) { 145 | var template_id = value.template_id; 146 | 147 | angular.forEach(data.contentStructure.rows, function(row) { 148 | angular.forEach(row.cols, function(col) { 149 | angular.forEach(col.elements, function(element) { 150 | if (element.template_id == template_id) { 151 | var templateCopy = angular.copy(value); 152 | templateCopy.options = element.options; 153 | angular.extend(element, templateCopy); 154 | } 155 | }); 156 | }); 157 | }); 158 | }, true); 159 | }; 160 | 161 | }); -------------------------------------------------------------------------------- /components/post.php: -------------------------------------------------------------------------------- 1 | get_structure($id_or_json); 21 | $structure = $this->validate_structure($structure); 22 | 23 | $this->options = new Layotter_Options('post', $structure['options']); 24 | 25 | foreach ($structure['rows'] as $row) { 26 | $this->rows[] = new Layotter_Row($row); 27 | } 28 | } 29 | 30 | 31 | /** 32 | * Create a post structure array using a post ID or JSON data 33 | * 34 | * @param int|string $id_or_json Post ID, JSON or post content 35 | * @return string JSON string containing post structure or null for new posts 36 | */ 37 | private function get_structure($id_or_json) { 38 | if (is_int($id_or_json)) { 39 | $json = $this->get_json_by_post_id($id_or_json); 40 | } else { 41 | $json = $id_or_json; 42 | } 43 | 44 | if ($this->is_json($json)) { 45 | return json_decode($json, true); 46 | } else { 47 | return json_decode(null, true); // TODO: what am I doing here? 48 | } 49 | } 50 | 51 | 52 | /** 53 | * Validate an array containing the post structure 54 | * 55 | * Validates array structure and presence of required key/value pairs 56 | * 57 | * @param array $structure Post structure 58 | * @return array Validated post structure 59 | */ 60 | private function validate_structure($structure) { 61 | if (!is_array($structure)) { 62 | $structure = array(); 63 | } 64 | 65 | if (!isset($structure['options']) OR !is_array($structure['options'])) { 66 | $structure['options'] = array(); 67 | } 68 | 69 | if (!isset($structure['rows']) OR !is_array($structure['rows'])) { 70 | $structure['rows'] = array(); 71 | } 72 | 73 | return $structure; 74 | } 75 | 76 | 77 | /** 78 | * Return array representation of this post for use in json_encode() 79 | * 80 | * PHP's JsonSerializable interface would be cleaner, but it's only available >= 5.4.0 81 | * 82 | * @return array Array representation of this post 83 | */ 84 | public function to_array() { 85 | $rows = array(); 86 | 87 | foreach ($this->rows as $row) { 88 | $rows[] = $row->to_array(); 89 | } 90 | 91 | return array( 92 | 'options' => $this->options->to_array(), 93 | 'rows' => $rows 94 | ); 95 | } 96 | 97 | 98 | /** 99 | * Return frontend HTML for this post 100 | * 101 | * @return string Frontend HTML 102 | */ 103 | public function get_frontend_view() { 104 | $rows_html = ''; 105 | foreach ($this->rows as $row) { 106 | $rows_html .= $row->get_frontend_view($this->options->get_formatted_values()); 107 | } 108 | 109 | // if a custom filter for frontend was hooked, run through that filter and return HTML 110 | if (has_filter('layotter/view/post')) { 111 | return apply_filters('layotter/view/post', $rows_html, $this->options->get_formatted_values()); 112 | } else { 113 | // otherwise, get HTML wrapper from settings, apply and return HTML 114 | $html_wrapper = Layotter_Settings::get_html_wrapper('wrapper'); 115 | return $html_wrapper['before'] . $rows_html . $html_wrapper['after']; 116 | } 117 | } 118 | 119 | 120 | /** 121 | * Check if a string contains the JSON representation of an array 122 | * 123 | * @param mixed $maybe_json Something that might be a string containing JSON data 124 | * @return bool Whether the parameter contained a JSON array 125 | */ 126 | private function is_json($maybe_json) { 127 | $maybe_array = json_decode($maybe_json, true); 128 | return is_array($maybe_array); 129 | } 130 | 131 | 132 | /** 133 | * Check if post 1.5.0 data structure is present for this post 134 | * 135 | * i.e. if JSON is in a custom field instead of the post content 136 | * 137 | * @param int $post_id Post ID 138 | * @return bool 139 | */ 140 | private function has_new_data_structure($post_id) { 141 | $json = get_post_meta($post_id, 'layotter_json', true); 142 | 143 | if (!empty($json)) { 144 | return true; 145 | } 146 | 147 | return false; 148 | } 149 | 150 | 151 | /** 152 | * Get post JSON by post ID 153 | * 154 | * @param int $post_id Post ID 155 | * @return string|null JSON string containing post structure or null for new posts 156 | */ 157 | private function get_json_by_post_id($post_id) { 158 | if ($this->has_new_data_structure($post_id) !== false) { 159 | // if post 1.5.0 data structure is present, get JSON from custom field 160 | return get_post_meta($post_id, 'layotter_json', true); 161 | } else { 162 | // otherwise, try to extract data from the post content 163 | return $this->get_json_from_legacy_post_content($post_id); 164 | } 165 | } 166 | 167 | 168 | /** 169 | * Extract post JSON from post content for a post ID 170 | * 171 | * JSON used to be stored in the main content wrapped like this: [layotter]json[/layotter] 172 | * This method extracts JSON from posts that haven't been updated to the new style yet. 173 | * 174 | * @param int $post_id Post ID with pre 1.5.0 style post content 175 | * @return string|null JSON string containing post structure or null for new posts 176 | */ 177 | private function get_json_from_legacy_post_content($post_id) { 178 | $content_raw = get_post_field('post_content', $post_id); 179 | 180 | // verify that the content is correctly formatted, unwrap from shortcode 181 | $matches = array(); 182 | if (preg_match('/\[layotter\](.*)\[\/layotter\]/ms', $content_raw, $matches)) { 183 | $content_json = $matches[1]; 184 | return $content_json; 185 | } else { 186 | return null; 187 | } 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /core/templates.php: -------------------------------------------------------------------------------- 1 | is_enabled_for($post_id) AND (!isset($template['deleted']) OR !$template['deleted'])) { 86 | $templates[] = $template_object->to_array(); 87 | } 88 | } 89 | 90 | return $templates; 91 | } 92 | 93 | 94 | /** 95 | * Save a new template 96 | * 97 | * @param object $element Layotter_Element object to be saved as a new template 98 | * @return object Layotter_Element object with template ID 99 | */ 100 | public static function save($element) { 101 | $templates = get_option('layotter_element_templates'); 102 | if (!is_array($templates)) { 103 | $templates = array(); 104 | } 105 | 106 | $id = count($templates); 107 | $element->set_template_id($id); 108 | $templates[$id] = $element->get_template_data(); 109 | update_option('layotter_element_templates', $templates); 110 | return $element; 111 | } 112 | 113 | 114 | /** 115 | * Update an existing template's data 116 | * 117 | * @param string $id Template ID 118 | * @param array $structure Element data (keys: type, values) 119 | * @return bool True if template has been updated, false on failure 120 | */ 121 | public static function update($id, $structure) { 122 | $templates = get_option('layotter_element_templates'); 123 | 124 | if (isset($templates[$id])) { 125 | $structure = self::validate_structure($structure); 126 | $templates[$id] = $structure; 127 | update_option('layotter_element_templates', $templates); 128 | return true; 129 | } 130 | 131 | return false; 132 | } 133 | 134 | 135 | /** 136 | * Delete an existing template 137 | * 138 | * @param string $id Template ID 139 | * @return bool True if template has been deleted, false on failure 140 | */ 141 | public static function delete($id) { 142 | $templates = get_option('layotter_element_templates'); 143 | 144 | if (!is_int($id)) { 145 | return false; 146 | } 147 | 148 | if (isset($templates[$id])) { 149 | $templates[$id]['deleted'] = true; 150 | update_option('layotter_element_templates', $templates); 151 | return true; 152 | } 153 | 154 | return false; 155 | } 156 | 157 | 158 | /** 159 | * Create a new element instance from a saved template 160 | * 161 | * @param int|array $id_or_structure Template ID or element structure with template_id 162 | * @return mixed New element instance, or false on failure 163 | */ 164 | public static function create_element($id_or_structure, $options = array()) { 165 | $element = false; 166 | 167 | if (is_array($id_or_structure)) { 168 | $structure = self::validate_structure($id_or_structure); 169 | return self::create_element($structure['template_id'], $structure['options']); 170 | } else if (is_int($id_or_structure)) { 171 | $id = $id_or_structure; 172 | 173 | $templates = get_option('layotter_element_templates'); 174 | if (is_array($templates) AND isset($templates[$id])) { 175 | $template = self::validate_structure($templates[$id]); 176 | $element = Layotter::create_element($template['type'], $template['values'], $options); 177 | } 178 | } 179 | 180 | if (!$element) { 181 | return false; 182 | } 183 | 184 | if (!isset($template['deleted']) OR !$template['deleted']) { 185 | $element->set_template_id($id); 186 | } 187 | return $element; 188 | } 189 | 190 | 191 | } -------------------------------------------------------------------------------- /core/core.php: -------------------------------------------------------------------------------- 1 | $type_or_structure, 58 | 'values' => $values, 59 | 'options' => $option_values 60 | ); 61 | } else if (is_array($type_or_structure)) { 62 | $structure = $type_or_structure; 63 | } else { 64 | return false; 65 | } 66 | 67 | if (isset($structure['type']) AND isset(self::$registered_elements[$structure['type']])) { 68 | try { 69 | $type = $structure['type']; 70 | return new self::$registered_elements[$type]($structure); 71 | } catch(Exception $e) { 72 | trigger_error($e->getMessage(), E_USER_WARNING); 73 | } 74 | } 75 | 76 | return false; 77 | } 78 | 79 | 80 | /** 81 | * Remove illegal characters from a type identifier 82 | * 83 | * @param string $type Dirty type identifier 84 | * @return string Clean type identifier 85 | */ 86 | private static function clean_type($type) { 87 | if (!is_string($type)) { 88 | return ''; 89 | } 90 | 91 | return preg_replace('/[^a-z_]/', '', $type); // only a-z and _ allowed 92 | } 93 | 94 | 95 | /** 96 | * Get element types enabled for a specific post 97 | * 98 | * @param int $post_id Post ID 99 | * @return array Element instances 100 | */ 101 | public static function get_filtered_element_types($post_id) { 102 | $elements = array(); 103 | 104 | foreach (array_keys(self::$registered_elements) as $element_type) { 105 | $element = Layotter::create_element($element_type); 106 | if ($element AND $element->is_enabled_for($post_id)) { 107 | $elements[] = $element; 108 | } 109 | } 110 | 111 | usort($elements, array(__CLASS__, 'sort_element_types_helper')); 112 | 113 | return $elements; 114 | } 115 | 116 | 117 | /** 118 | * Helper used to sort a set of element types (to be used with usort()) 119 | * 120 | * Sorts using the order attribute. Elements with the same order attribute are sorted alphabetically 121 | * by name. Elements without an order attribute are treated as order = 0. 122 | * 123 | * @param Layotter_Element $element_type_a First element type for comparison 124 | * @param Layotter_Element $element_type_b Second element type for comparison 125 | * @return int -1 if A comes first, 1 if B comes first, 0 if equal 126 | */ 127 | public static function sort_element_types_helper($element_type_a, $element_type_b) { 128 | $a_order = $element_type_a->get('order'); 129 | $b_order = $element_type_b->get('order'); 130 | $a_name = $element_type_a->get('title'); 131 | $b_name = $element_type_b->get('title'); 132 | 133 | if ($a_order < $b_order) { 134 | return -1; 135 | } else if ($a_order > $b_order) { 136 | return 1; 137 | } else { 138 | return strcasecmp($a_name, $b_name); 139 | } 140 | } 141 | 142 | 143 | /** 144 | * Check if Layotter is enabled for the current screen 145 | * 146 | * @return bool Whether Layotter is enabled 147 | */ 148 | public static function is_enabled() { 149 | // fail if not in the backend 150 | if (!is_admin()) { 151 | return false; 152 | } 153 | 154 | // fail if not on a relevant edit screen 155 | global $pagenow; 156 | if ($pagenow != 'post.php' AND $pagenow != 'post-new.php') { 157 | return false; 158 | } 159 | 160 | // fail if layotter isn't enabled for the current post 161 | if (!self::is_enabled_for_post(get_the_ID())) { 162 | return false; 163 | } 164 | 165 | return true; 166 | } 167 | 168 | 169 | /** 170 | * Check if Layotter is enabled for a specific post 171 | * 172 | * @param int $post_id Post ID 173 | * @return bool Whether Layotter is enabled 174 | */ 175 | public static function is_enabled_for_post($post_id) { 176 | $override_enabled = apply_filters('layotter/enable_for_posts', array()); 177 | $override_disabled = apply_filters('layotter/disable_for_posts', array()); 178 | 179 | if (in_array($post_id, $override_enabled)) { 180 | return true; 181 | } 182 | 183 | if (in_array($post_id, $override_disabled)) { 184 | return false; 185 | } 186 | 187 | $post_type = get_post_type($post_id); 188 | $enabled_post_types = Layotter_Settings::get_enabled_post_types(); 189 | return in_array($post_type, $enabled_post_types); 190 | } 191 | 192 | 193 | } 194 | -------------------------------------------------------------------------------- /core/acf-abstraction.php: -------------------------------------------------------------------------------- 1 | = 0; 32 | } 33 | 34 | 35 | /** 36 | * Check if a compatible version of ACF is installed and output an error message if not 37 | * 38 | * @return bool 39 | */ 40 | public static function is_available() { 41 | if (!self::is_installed()) { 42 | self::$error_message = sprintf(__('Layotter requires the Advanced Custom Fields plugin, please install it before using Layotter.', 'layotter'), 'http://www.advancedcustomfields.com'); 43 | } else if (!self::is_version_compatible()) { 44 | self::$error_message = sprintf(__('Your version of Advanced Custom Fields is outdated. Please install version %s or higher to be able to use Layotter.', 'layotter'), self::REQUIRED_VERSION); 45 | } 46 | 47 | if (!empty(self::$error_message)) { 48 | add_action('admin_notices', array(__CLASS__, 'print_error')); 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | 56 | /** 57 | * Output an error message if ACF isn't installed (hooked to admin_notices by self::is_available()) 58 | */ 59 | public static function print_error() { 60 | ?> 61 |
62 |

63 | 64 |

65 |
66 | 196 |
197 |
198 | 199 | 200 |
201 |
202 |
203 | _wp_post_revision_meta_keys() as $meta_key ) { 70 | 71 | if ( 72 | isset( $posted_data[ $meta_key ] ) && 73 | get_post_meta( $new_autosave['ID'], $meta_key, true ) !== wp_unslash( $posted_data[ $meta_key ] ) 74 | ) { 75 | 76 | /* 77 | * Use the underlying delete_metadata() and add_metadata() functions 78 | * vs delete_post_meta() and add_post_meta() to make sure we're working 79 | * with the actual revision meta. 80 | */ 81 | delete_metadata( 'post', $new_autosave['ID'], $meta_key ); 82 | 83 | /** 84 | * One last check to ensure meta value not empty(). 85 | */ 86 | if ( ! empty( $posted_data[ $meta_key ] ) ) { 87 | 88 | /** 89 | * Add the revisions meta data to the autosave. 90 | */ 91 | add_metadata( 'post', $new_autosave['ID'], $meta_key, $posted_data[ $meta_key ] ); 92 | } 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Determine which post meta fields should be revisioned. 99 | * 100 | * @access public 101 | * @since 4.5.0 102 | * 103 | * @return array An array of meta keys to be revisioned. 104 | */ 105 | public function _wp_post_revision_meta_keys() { 106 | /** 107 | * Filter the list of post meta keys to be revisioned. 108 | * 109 | * @since 4.5.0 110 | * 111 | * @param array $keys An array of default meta fields to be revisioned. 112 | */ 113 | return apply_filters( 'wp_post_revision_meta_keys', array() ); 114 | } 115 | 116 | /** 117 | * Check whether revisioned post meta fields have changed. 118 | * 119 | * @since 4.5.0 120 | */ 121 | public function _wp_check_revisioned_meta_fields_have_changed( $post_has_changed, WP_Post $last_revision, WP_Post $post ) { 122 | foreach ( $this->_wp_post_revision_meta_keys() as $meta_key ) { 123 | if ( get_post_meta( $post->ID, $meta_key ) !== get_post_meta( $last_revision->ID, $meta_key ) ) { 124 | $post_has_changed = true; 125 | break; 126 | } 127 | } 128 | return $post_has_changed; 129 | } 130 | 131 | /** 132 | * Save the revisioned meta fields. 133 | * 134 | * @since 4.5.0 135 | */ 136 | public function _wp_save_revisioned_meta_fields( $revision_id ) { 137 | $revision = get_post( $revision_id ); 138 | $post_id = $revision->post_parent; 139 | 140 | // Save revisioned meta fields. 141 | foreach ( $this->_wp_post_revision_meta_keys() as $meta_key ) { 142 | $meta_value = get_post_meta( $post_id, $meta_key ); 143 | 144 | /* 145 | * Use the underlying add_metadata() function vs add_post_meta() 146 | * to ensure metadata is added to the revision post and not its parent. 147 | */ 148 | add_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta_value ) ); 149 | } 150 | } 151 | 152 | /** 153 | * Restore the revisioned meta values for a post. 154 | * 155 | * @since 4.5.0 156 | */ 157 | public function _wp_restore_post_revision_meta( $post_id, $revision_id ) { 158 | // Restore revisioned meta fields. 159 | $metas_revisioned = $this->_wp_post_revision_meta_keys(); 160 | if ( isset( $metas_revisioned ) && 0 !== sizeof( $metas_revisioned ) ) { 161 | foreach ( $metas_revisioned as $meta_key ) { 162 | // Clear any existing metas 163 | delete_post_meta( $post_id, $meta_key ); 164 | // Get the stored meta, not stored === blank 165 | $meta_values = get_post_meta( $revision_id, $meta_key, true ); 166 | if ( 0 !== sizeof( $meta_values ) && is_array( $meta_values ) ) { 167 | foreach ( $meta_values as $meta_value ) { 168 | add_post_meta( $post_id, $meta_key, wp_slash( $meta_value ) ); 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Filters post meta retrieval to get values from the actual autosave post, 177 | * and not its parent. 178 | * 179 | * Filters revisioned meta keys only. 180 | * 181 | * @access public 182 | * @since 4.5.0 183 | * 184 | * @param mixed $value Meta value to filter. 185 | * @param int $object_id Object ID. 186 | * @param string $meta_key Meta key to filter a value for. 187 | * @param bool $single Whether to return a single value. Default false. 188 | * @return mixed Original meta value if the meta key isn't revisioned, the object doesn't exist, 189 | * the post type is a revision or the post ID doesn't match the object ID. 190 | * Otherwise, the revisioned meta value is returned for the preview. 191 | */ 192 | public function _wp_preview_meta_filter( $value, $object_id, $meta_key, $single ) { 193 | 194 | $post = get_post(); 195 | if ( 196 | empty( $post ) || 197 | $post->ID !== $object_id || 198 | ! in_array( $meta_key, $this->_wp_post_revision_meta_keys(), true ) || 199 | 'revision' === $post->post_type 200 | ) { 201 | return $value; 202 | } 203 | 204 | // Grab the autosave. 205 | $preview = wp_get_post_autosave( $post->ID ); 206 | if ( ! is_object( $preview ) ) { 207 | return $value; 208 | } 209 | 210 | return get_post_meta( $preview->ID, $meta_key, $single ); 211 | } 212 | } 213 | 214 | $wp_post_meta_revisioning = new WP_Post_Meta_Revisioning; 215 | -------------------------------------------------------------------------------- /views/editor.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |
7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 |
61 |
62 |
63 | 64 | 65 |
66 | 67 |
68 | 69 | 70 | 71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 | 81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 89 |
90 |
91 |
92 |
-------------------------------------------------------------------------------- /core/assets.php: -------------------------------------------------------------------------------- 1 | 'assets/js/vendor/angular.js', 38 | 'angular-animate' => 'assets/js/vendor/angular-animate.js', 39 | 'angular-sanitize' => 'assets/js/vendor/angular-sanitize.js', 40 | 'angular-ui-sortable' => 'assets/js/vendor/angular-ui-sortable.js', 41 | 'layotter' => 'assets/js/app/app.js', 42 | 'layotter-controller-editor' => 'assets/js/app/controllers/editor.js', 43 | 'layotter-controller-templates' => 'assets/js/app/controllers/templates.js', 44 | 'layotter-controller-form' => 'assets/js/app/controllers/form.js', 45 | 'layotter-service-state' => 'assets/js/app/services/state.js', 46 | 'layotter-service-data' => 'assets/js/app/services/data.js', 47 | 'layotter-service-content' => 'assets/js/app/services/content.js', 48 | 'layotter-service-templates' => 'assets/js/app/services/templates.js', 49 | 'layotter-service-layouts' => 'assets/js/app/services/layouts.js', 50 | 'layotter-service-view' => 'assets/js/app/services/view.js', 51 | 'layotter-service-forms' => 'assets/js/app/services/forms.js', 52 | 'layotter-service-modals' => 'assets/js/app/services/modals.js', 53 | 'layotter-service-history' => 'assets/js/app/services/history.js' 54 | ); 55 | foreach ($scripts as $name => $path) { 56 | wp_enqueue_script( 57 | $name, 58 | plugins_url($path, __DIR__) 59 | ); 60 | } 61 | 62 | 63 | // fetch allowed row layouts and default layout 64 | $allowed_row_layouts = Layotter_Settings::get_allowed_row_layouts(); 65 | $default_row_layout = Layotter_Settings::get_default_row_layout(); 66 | 67 | 68 | // fetch default values for post, row and element options 69 | $default_post_options = new Layotter_Options('post'); 70 | $default_row_options = new Layotter_Options('row'); 71 | $default_col_options = new Layotter_Options('col'); 72 | $default_element_options = new Layotter_Options('element'); 73 | 74 | 75 | // fetch content structure for the current post 76 | $post_id = get_the_ID(); 77 | $content_structure = new Layotter_Post($post_id); 78 | 79 | 80 | // fetch post layouts and element templates 81 | $saved_layouts = Layotter_Layouts::get_all(); 82 | $saved_templates = Layotter_Templates::get_all_for_post($post_id); 83 | 84 | 85 | // fetch available element types 86 | $element_objects = Layotter::get_filtered_element_types($post_id); 87 | $element_types = array(); 88 | 89 | foreach ($element_objects as $element_object) { 90 | $element_types[] = array( 91 | 'type' => $element_object->get('type'), 92 | 'title' => $element_object->get('title'), 93 | 'description' => $element_object->get('description'), 94 | 'icon' => $element_object->get('icon'), 95 | ); 96 | } 97 | 98 | 99 | // fetch general settings 100 | $enable_post_layouts = Layotter_Settings::post_layouts_enabled(); 101 | $enable_element_templates = Layotter_Settings::element_templates_enabled(); 102 | 103 | 104 | // inject data for use with Javascript 105 | wp_localize_script( 106 | 'layotter', 107 | 'layotterData', 108 | array( 109 | 'postID' => $post_id, 110 | 'contentStructure' => $content_structure->to_array(), 111 | 'allowedRowLayouts' => $allowed_row_layouts, 112 | 'defaultRowLayout' => $default_row_layout, 113 | 'savedLayouts' => $saved_layouts, 114 | 'savedTemplates' => $saved_templates, 115 | 'enablePostLayouts' => $enable_post_layouts, 116 | 'enableElementTemplates' => $enable_element_templates, 117 | 'elementTypes' => $element_types, 118 | 'options' => array( 119 | 'post' => array( 120 | 'enabled' => $default_post_options->is_enabled(), 121 | 'defaults' => $default_post_options->get_clean_values(), 122 | ), 123 | 'row' => array( 124 | 'enabled' => $default_row_options->is_enabled(), 125 | 'defaults' => $default_row_options->get_clean_values(), 126 | ), 127 | 'col' => array( 128 | 'enabled' => $default_col_options->is_enabled(), 129 | 'defaults' => $default_col_options->get_clean_values(), 130 | ), 131 | 'element' => array( 132 | 'enabled' => $default_element_options->is_enabled(), 133 | 'defaults' => $default_element_options->get_clean_values(), 134 | ) 135 | ), 136 | 'i18n' => array( 137 | 'delete_row' => __('Delete row', 'layotter'), 138 | 'delete_element' => __('Delete element', 'layotter'), 139 | 'delete_template' => __('Delete template', 'layotter'), 140 | 'edit_template' => __('Edit template', 'layotter'), 141 | 'cancel' => __('Cancel', 'layotter'), 142 | 'discard_changes' => __('Discard changes', 'layotter'), 143 | 'discard_changes_confirmation' => __('Do you want to cancel and discard all changes?', 'layotter'), 144 | 'delete_row_confirmation' => __('Do you want to delete this row and all its elements?', 'layotter'), 145 | 'delete_element_confirmation' => __('Do you want to delete this element?', 'layotter'), 146 | 'delete_template_confirmation' => __('Do you want to delete this template? You can not undo this action.', 'layotter'), 147 | 'edit_template_confirmation' => __('When editing a template, your changes will be reflected on all pages that are using it. Do you want to edit this template?', 'layotter'), 148 | 'save_new_layout_confirmation' => __('Please enter a name for your layout:', 'layotter'), 149 | 'save_layout' => __('Save layout', 'layotter'), 150 | 'rename_layout_confirmation' => __('Please enter the new name for this layout:', 'layotter'), 151 | 'rename_layout' => __('Rename layout', 'layotter'), 152 | 'delete_layout_confirmation' => __('Do want to delete this layout? You can not undo this action.', 'layotter'), 153 | 'delete_layout' => __('Delete layout', 'layotter'), 154 | 'load_layout_confirmation' => __('Do want to load this layout and overwrite the existing content?', 'layotter'), 155 | 'load_layout' => __('Load layout', 'layotter'), 156 | 'history' => array( 157 | 'undo' => __('Undo:', 'layotter'), 158 | 'redo' => __('Redo:', 'layotter'), 159 | 'add_element' => __('Add element', 'layotter'), 160 | 'edit_element' => __('Edit element', 'layotter'), 161 | 'duplicate_element' => __('Duplicate element', 'layotter'), 162 | 'delete_element' => __('Delete element', 'layotter'), 163 | 'move_element' => __('Move element', 'layotter'), 164 | 'save_element_as_template' => __('Save element as template', 'layotter'), 165 | 'create_element_from_template' => __('Create element from template', 'layotter'), 166 | 'add_row' => __('Add row', 'layotter'), 167 | 'change_row_layout' => __('Change row layout', 'layotter'), 168 | 'duplicate_row' => __('Duplicate row', 'layotter'), 169 | 'delete_row' => __('Delete row', 'layotter'), 170 | 'move_row' => __('Move row', 'layotter'), 171 | 'edit_post_options' => __('Edit post options', 'layotter'), 172 | 'edit_row_options' => __('Edit row options', 'layotter'), 173 | 'edit_column_options' => __('Edit column options', 'layotter'), 174 | 'edit_element_options' => __('Edit element options', 'layotter'), 175 | 'load_post_layout' => __('Load layout', 'layotter') 176 | ) 177 | ) 178 | ) 179 | ); 180 | 181 | } 182 | 183 | 184 | /** 185 | * Include basic CSS in the frontend if enabled in settings 186 | */ 187 | add_action('wp_enqueue_scripts', 'layotter_frontend_assets'); 188 | function layotter_frontend_assets() { 189 | if (!is_admin() AND Layotter_Settings::default_css_enabled()) { 190 | wp_enqueue_style('layotter-frontend', plugins_url('assets/css/frontend.css', __DIR__)); 191 | } 192 | } -------------------------------------------------------------------------------- /core/ajax.php: -------------------------------------------------------------------------------- 1 | get_form_data()); 39 | } 40 | } 41 | 42 | die(); // required by Wordpress after any AJAX call 43 | } 44 | 45 | 46 | /** 47 | * Output JSON-encoded element data (called after editing an element) 48 | */ 49 | add_action('wp_ajax_layotter_parse_element', 'layotter_ajax_parse_element'); 50 | function layotter_ajax_parse_element() { 51 | $post_data = stripslashes_deep($_POST); // strip Wordpress magic quotes 52 | 53 | if (isset($post_data['type']) AND is_string($post_data['type'])) { 54 | $values = Layotter_ACF::unwrap_post_values(); 55 | $element = Layotter::create_element($post_data['type'], $values); 56 | if ($element) { 57 | echo json_encode($element->to_array()); 58 | } 59 | } 60 | 61 | die(); // required by Wordpress after any AJAX call 62 | } 63 | 64 | 65 | 66 | 67 | 68 | 69 | /** 70 | * Output the edit form for post, row or element options 71 | */ 72 | add_action('wp_ajax_layotter_edit_options', 'layotter_ajax_edit_options'); 73 | function layotter_ajax_edit_options() { 74 | $post_data = layotter_get_angular_post_data(); 75 | 76 | // type and option values are required 77 | if (isset($post_data['type']) AND is_string($post_data['type'])) { 78 | if (isset($post_data['values'])) { 79 | $values = $post_data['values']; 80 | } else { 81 | $values = array(); 82 | } 83 | 84 | if (isset($post_data['post_id'])) { 85 | $post_id = $post_data['post_id']; 86 | } else { 87 | $post_id = ''; 88 | } 89 | 90 | $options = new Layotter_Options($post_data['type'], $values, $post_id); 91 | if ($options->is_enabled()) { 92 | echo json_encode($options->get_form_data()); 93 | } 94 | } 95 | 96 | die(); // required by Wordpress after any AJAX call 97 | } 98 | 99 | 100 | /** 101 | * Output JSON-encoded options data (called after editing post, row or element options) 102 | */ 103 | add_action('wp_ajax_layotter_parse_options', 'layotter_ajax_parse_options'); 104 | function layotter_ajax_parse_options() { 105 | $post_data = stripslashes_deep($_POST); // strip Wordpress magic quotes 106 | 107 | if (isset($post_data['type']) AND is_string($post_data['type'])) { 108 | if (isset($post_data['post_id'])) { 109 | $post_id = $post_data['post_id']; 110 | } else { 111 | $post_id = ''; 112 | } 113 | 114 | $values = Layotter_ACF::unwrap_post_values(); 115 | $options = new Layotter_Options($post_data['type'], $values, $post_id); 116 | if($options->is_enabled()) { 117 | echo json_encode($options->to_array()); 118 | } 119 | } 120 | 121 | die(); // required by Wordpress after any AJAX call 122 | } 123 | 124 | 125 | 126 | 127 | 128 | 129 | /** 130 | * Save element as a new template and output the new template's JSON-encoded data 131 | */ 132 | add_action('wp_ajax_layotter_save_new_template', 'layotter_ajax_save_new_template'); 133 | function layotter_ajax_save_new_template() { 134 | $post_data = layotter_get_angular_post_data(); 135 | 136 | // type and field values are required 137 | if (isset($post_data['type']) AND is_string($post_data['type'])) { 138 | if (isset($post_data['values'])) { 139 | $values = $post_data['values']; 140 | } else { 141 | $values = array(); 142 | } 143 | 144 | $element = Layotter::create_element($post_data['type'], $values); 145 | if ($element) { 146 | $template = Layotter_Templates::save($element); 147 | echo json_encode($template->to_array()); 148 | } 149 | } 150 | 151 | die(); // required by Wordpress after any AJAX call 152 | } 153 | 154 | 155 | /** 156 | * Output the edit form for a template 157 | */ 158 | add_action('wp_ajax_layotter_edit_template', 'layotter_ajax_edit_template'); 159 | function layotter_ajax_edit_template() { 160 | $post_data = layotter_get_angular_post_data(); 161 | 162 | // template ID is required 163 | if (isset($post_data['template_id']) AND is_int($post_data['template_id'])) { 164 | $element = Layotter_Templates::create_element($post_data['template_id']); 165 | if ($element) { 166 | echo json_encode($element->get_form_data()); 167 | } 168 | } 169 | 170 | die(); // required by Wordpress after any AJAX call 171 | } 172 | 173 | 174 | /** 175 | * Update element template and output the template's JSON-encoded data 176 | */ 177 | add_action('wp_ajax_layotter_update_template', 'layotter_ajax_update_template'); 178 | function layotter_ajax_update_template() { 179 | $post_data = stripslashes_deep($_POST); // strip Wordpress magic quotes 180 | 181 | if (isset($post_data['template_id'])) { 182 | $id = $post_data['template_id']; 183 | $template = Layotter_Templates::get($id); 184 | 185 | if ($template) { 186 | $values = Layotter_ACF::unwrap_post_values(); 187 | $element = Layotter::create_element($template['type'], $values); 188 | if ($element) { 189 | $element->set_template_id($id); 190 | Layotter_Templates::update($id, $element->get_template_data()); 191 | echo json_encode($element->to_array()); 192 | } 193 | } 194 | } 195 | 196 | die(); // required by Wordpress after any AJAX call 197 | } 198 | 199 | 200 | /** 201 | * Delete a template 202 | */ 203 | add_action('wp_ajax_layotter_delete_template', 'layotter_ajax_delete_template'); 204 | function layotter_ajax_delete_template() { 205 | $post_data = layotter_get_angular_post_data(); 206 | 207 | // template ID is required 208 | if (isset($post_data['template_id'])) { 209 | $template_object = Layotter_Templates::create_element($post_data['template_id']); 210 | if ($template_object) { 211 | Layotter_Templates::delete($post_data['template_id']); 212 | $template_object->unset_template_id(); 213 | echo json_encode($template_object->to_array()); 214 | } 215 | } 216 | 217 | die(); // required by Wordpress after any AJAX call 218 | } 219 | 220 | 221 | 222 | 223 | 224 | 225 | /** 226 | * Save JSON structure as a post layout 227 | */ 228 | add_action('wp_ajax_layotter_save_new_layout', 'layotter_ajax_save_new_layout'); 229 | function layotter_ajax_save_new_layout() { 230 | $post_data = layotter_get_angular_post_data(); 231 | 232 | // name and JSON are required 233 | if (isset($post_data['name']) AND isset($post_data['json'])) { 234 | $layout = Layotter_Layouts::save($post_data['name'], $post_data['json']); 235 | echo json_encode($layout); 236 | } 237 | 238 | die(); // required by Wordpress after any AJAX call 239 | } 240 | 241 | 242 | /** 243 | * Load a post layout 244 | */ 245 | add_action('wp_ajax_layotter_load_layout', 'layotter_ajax_load_layout'); 246 | function layotter_ajax_load_layout() { 247 | $post_data = layotter_get_angular_post_data(); 248 | 249 | // template ID is required 250 | if (isset($post_data['layout_id'])) { 251 | $post = Layotter_Layouts::get($post_data['layout_id']); 252 | if ($post) { 253 | echo json_encode($post->to_array()); 254 | } 255 | } 256 | 257 | die(); // required by Wordpress after any AJAX call 258 | } 259 | 260 | 261 | /** 262 | * Rename a post layout 263 | */ 264 | add_action('wp_ajax_layotter_rename_layout', 'layotter_ajax_rename_layout'); 265 | function layotter_ajax_rename_layout() { 266 | $post_data = layotter_get_angular_post_data(); 267 | 268 | // template ID and new name are required 269 | if (isset($post_data['layout_id']) AND isset($post_data['name'])) { 270 | $renamed = Layotter_Layouts::rename($post_data['layout_id'], $post_data['name']); 271 | if ($renamed) { 272 | echo json_encode($renamed); 273 | } 274 | } 275 | 276 | die(); // required by Wordpress after any AJAX call 277 | } 278 | 279 | 280 | /** 281 | * Delete a post layout 282 | */ 283 | add_action('wp_ajax_layotter_delete_layout', 'layotter_ajax_delete_layout'); 284 | function layotter_ajax_delete_layout() { 285 | $post_data = layotter_get_angular_post_data(); 286 | 287 | // template ID is required 288 | if (isset($post_data['layout_id'])) { 289 | Layotter_Layouts::delete($post_data['layout_id']); 290 | } 291 | 292 | die(); // required by Wordpress after any AJAX call 293 | } -------------------------------------------------------------------------------- /assets/js/vendor/angular-animate.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.2.25 3 | (c) 2010-2014 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(F,e,O){'use strict';e.module("ngAnimate",["ng"]).directive("ngAnimateChildren",function(){return function(G,s,g){g=g.ngAnimateChildren;e.isString(g)&&0===g.length?s.data("$$ngAnimateChildren",!0):G.$watch(g,function(e){s.data("$$ngAnimateChildren",!!e)})}}).factory("$$animateReflow",["$$rAF","$document",function(e,s){return function(g){return e(function(){g()})}}]).config(["$provide","$animateProvider",function(G,s){function g(e){for(var g=0;g=y&&b>=v&&e()}var h=g(b);a=b.data(q);if(-1!=h.getAttribute("class").indexOf(d)&&a){var m="";u(d.split(" "),function(a,b){m+=(0title, $this->description, $this->icon and $this->field_group 27 | * 28 | * May assign $this->order to override alphabetical odering in the "Add Element" screen. 29 | */ 30 | abstract protected function attributes(); 31 | 32 | 33 | /** 34 | * Should output HTMl for the element's backend representation 35 | * 36 | * @param array $fields Field values 37 | */ 38 | abstract protected function backend_view($fields); 39 | 40 | 41 | /** 42 | * Should output HTMl for the element's frontend representation 43 | * 44 | * @param array $fields Field values 45 | */ 46 | abstract protected function frontend_view($fields); 47 | 48 | 49 | /** 50 | * backend_assets() is optional and should be used to enqueue scripts and styles for the backend 51 | */ 52 | public static function backend_assets() {} 53 | 54 | 55 | /** 56 | * frontend_assets() is optional and should be used to enqueue scripts and styles for the backend 57 | */ 58 | public static function frontend_assets() {} 59 | 60 | 61 | /** 62 | * Create a new element 63 | * 64 | * @param array $structure Element structure 65 | * @throws Exception If the ACF field group defined for this element doesn't exist 66 | */ 67 | final public function __construct($structure) { 68 | $this->attributes(); 69 | 70 | $structure = $this->validate_structure($structure); 71 | $this->type = $structure['type']; 72 | $values = $structure['values']; 73 | $option_values = $structure['options']; 74 | 75 | $fields = $this->get_fields(); 76 | $this->apply_values($fields, $values); 77 | 78 | $this->form->set_title($this->title); 79 | $this->form->set_icon($this->icon); 80 | 81 | $this->register_frontend_hooks(); 82 | 83 | $this->options = new Layotter_Options('element', $option_values); 84 | } 85 | 86 | 87 | /** 88 | * Get ACF fields for this element 89 | * 90 | * @return array ACF fields 91 | * @throws Exception If $this->field_group wasn't assigned correctly in $this->attributes() 92 | */ 93 | final protected function get_fields() { 94 | // ACF field group can be provided as post id (int) or slug ('group_xyz') 95 | if (!is_int($this->field_group) AND !is_string($this->field_group)) { 96 | throw new Exception('$this->field_group must be assigned in attributes() (error in class ' . get_called_class() . ')'); 97 | } 98 | 99 | $field_group = Layotter_ACF::get_field_group($this->field_group); 100 | 101 | // check if the field group exists 102 | if (!$field_group) { 103 | throw new Exception('No ACF field group found for ID or key ' . $this->field_group . ' (error in class ' . get_called_class() . ')'); 104 | } 105 | 106 | // return fields for the provided ACF field group 107 | return Layotter_ACF::get_fields($field_group); 108 | } 109 | 110 | 111 | /** 112 | * Register hooks for backend assets 113 | * 114 | * This allows element type developers to enqueue scripts and styles required to display this element 115 | * correctly in the backend. 116 | */ 117 | final public static function register_backend_hooks() { 118 | add_action('admin_footer', array(get_called_class(), 'register_backend_hooks_helper')); 119 | } 120 | 121 | 122 | /** 123 | * Helper function for register_backend_hooks 124 | * 125 | * To make sure that Layotter::is_enabled() returns the correct value, the check is delayed until admin_footer. 126 | * Without the check, assets would be included on every single page in the backend. 127 | */ 128 | final public static function register_backend_hooks_helper() { 129 | if (Layotter::is_enabled()) { 130 | call_user_func(array(get_called_class(), 'backend_assets')); 131 | } 132 | } 133 | 134 | 135 | /** 136 | * Register hooks for frontend assets 137 | * 138 | * This allows element type developers to enqueue scripts and styles required to display this element 139 | * correctly in the frontend. 140 | */ 141 | final private function register_frontend_hooks() { 142 | if (!is_admin()) { 143 | call_user_func(array(get_called_class(), 'frontend_assets')); 144 | } 145 | } 146 | 147 | 148 | /** 149 | * Check if this element type is enabled for a specific post 150 | * 151 | * @param int $post_id Post ID 152 | * @return bool Whether this element type is enabled 153 | */ 154 | final public function is_enabled_for($post_id) { 155 | $post_id = intval($post_id); 156 | $post_type = get_post_type($post_id); 157 | 158 | $field_group = Layotter_ACF::get_field_group($this->field_group); 159 | 160 | return Layotter_ACF::is_field_group_visible($field_group, array( 161 | 'post_id' => $post_id, 162 | 'post_type' => $post_type, 163 | 'layotter' => 'element' 164 | )); 165 | } 166 | 167 | 168 | /** 169 | * Get element data 170 | * 171 | * @param string $what What to get - can be 'type', 'title', 'description', 'icon' 172 | * @return string Requested data 173 | */ 174 | final public function get($what) { 175 | switch ($what) { 176 | case 'type': 177 | return $this->type; 178 | 179 | case 'title': 180 | return $this->title; 181 | 182 | case 'description': 183 | return $this->description; 184 | 185 | case 'icon': 186 | return $this->icon; 187 | 188 | case 'order': 189 | return $this->order; 190 | 191 | default: 192 | return null; 193 | } 194 | } 195 | 196 | 197 | /** 198 | * Declare this element as a template 199 | * 200 | * Templates are managed through the Layotter_Templates class, this method simply declares this element as an 201 | * instance of a saved template. The template ID will be present in this element's JSON representation. 202 | * 203 | * @param int $template_id Template ID 204 | */ 205 | final public function set_template_id($template_id) { 206 | $this->template_id = $template_id; 207 | } 208 | 209 | 210 | /** 211 | * Remove template ID and treat as a regular element 212 | */ 213 | final public function unset_template_id() { 214 | $this->template_id = -1; 215 | } 216 | 217 | 218 | /** 219 | * Get element data to be saved in the database as a template 220 | * 221 | * Options and view are not necessary because: 222 | * 1. Templates never have options, only an instance of a template has options 223 | * 2. View is refreshed before every output, no need to save it to the database 224 | * 225 | * @return array Array representation of this element to be saved as a template 226 | */ 227 | final public function get_template_data() { 228 | return array( 229 | 'template_id' => $this->template_id, 230 | 'type' => $this->type, 231 | 'values' => $this->clean_values 232 | ); 233 | } 234 | 235 | 236 | /** 237 | * Validate an array containing an element's structure 238 | * 239 | * Validates array structure and presence of required key/value pairs 240 | * 241 | * @param array $structure Element structure 242 | * @return array Validated element structure 243 | */ 244 | private function validate_structure($structure) { 245 | if (!isset($structure['type']) OR !is_string($structure['type'])) { 246 | $structure['type'] = ''; 247 | } 248 | 249 | if (!isset($structure['values']) OR !is_array($structure['values'])) { 250 | $structure['values'] = array(); 251 | } 252 | 253 | if (!isset($structure['options']) OR !is_array($structure['options'])) { 254 | $structure['options'] = array(); 255 | } 256 | 257 | return $structure; 258 | } 259 | 260 | 261 | /** 262 | * Return array representation of this element for use in json_encode() 263 | * 264 | * PHP's JsonSerializable interface would be cleaner, but it's only available >= 5.4.0 265 | * 266 | * @return array Array representation of this element 267 | */ 268 | public function to_array() { 269 | if ($this->template_id > -1) { 270 | return array( 271 | 'template_id' => $this->template_id, 272 | 'options' => $this->options->to_array(), 273 | 'view' => $this->get_backend_view() 274 | ); 275 | } else { 276 | return array( 277 | 'type' => $this->type, 278 | 'values' => $this->clean_values, 279 | 'options' => $this->options->to_array(), 280 | 'view' => $this->get_backend_view() 281 | ); 282 | } 283 | } 284 | 285 | 286 | /** 287 | * Get the backend view 288 | * 289 | * @return string Backend view HTML 290 | */ 291 | final public function get_backend_view() { 292 | ob_start(); 293 | $this->backend_view($this->formatted_values); 294 | return ob_get_clean(); 295 | } 296 | 297 | 298 | /** 299 | * Get the frontend view 300 | * 301 | * @param array $col_options Formatted options for the parent column 302 | * @param array $row_options Formatted options for the parent row 303 | * @param array $post_options Formatted options for the parent post 304 | * @param string $col_width Width of the parent column, e.g. '1/3' 305 | * @return string Frontend view HTML 306 | */ 307 | final public function get_frontend_view($col_options, $row_options, $post_options, $col_width) { 308 | $element_options = $this->options->get_formatted_values(); 309 | 310 | ob_start(); 311 | $this->frontend_view($this->formatted_values, $col_width, $col_options, $row_options, $post_options, $element_options); 312 | $element_html = ob_get_clean(); 313 | 314 | if (has_filter('layotter/view/element')) { 315 | return apply_filters('layotter/view/element', $element_html, $element_options, $col_options, $row_options, $post_options); 316 | } else { 317 | $html_wrapper = Layotter_Settings::get_html_wrapper('elements'); 318 | return $html_wrapper['before'] . $element_html . $html_wrapper['after']; 319 | } 320 | } 321 | 322 | 323 | } 324 | -------------------------------------------------------------------------------- /assets/js/app/services/content.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main provider for content and editing 3 | */ 4 | app.service('content', function($rootScope, $http, $animate, $timeout, data, forms, modals, state, templates, history) { 5 | 6 | 7 | var _this = this; 8 | this.showBackButton = state.showBackButton; 9 | this.toggleFullscreen = forms.toggleFullscreen; 10 | $rootScope.$watch(function() { 11 | return state.showBackButton; 12 | }, function(value) { 13 | _this.showBackButton = value; 14 | }); 15 | 16 | 17 | /** 18 | * Show a list of available element types - new element will be inserted at $index in the $parent col 19 | */ 20 | this.showNewElementTypes = function(parent, index) { 21 | state.setElement(angular.copy(data.templates.element)); 22 | state.setParent(parent); 23 | state.setIndex(index); 24 | forms.showHTML(angular.element('#layotter-add-element').html()); 25 | }; 26 | 27 | 28 | /** 29 | * Go back to show the list of available element types when editing a new element 30 | */ 31 | this.backToShowNewElementTypes = function() { 32 | forms.showHTML(angular.element('#layotter-add-element').html()); 33 | }; 34 | 35 | 36 | /** 37 | * Select the desired element $type from the showNewElementTypes list 38 | */ 39 | this.selectNewElementType = function(type) { 40 | state.setElement(angular.extend({}, data.templates.element, {type: type})); 41 | state.setBackButton(true); 42 | _this.editElement(state.getElement()); 43 | }; 44 | 45 | 46 | /** 47 | * Save values from the edit form being currently displayed - can be an options form or an element edit form 48 | */ 49 | this.saveForm = function() { 50 | var editingElement = state.getElement(); 51 | if (state.getOptionsType()) { 52 | _this.saveOptions(); 53 | } else if (typeof editingElement.template_id !== 'undefined') { 54 | templates.saveTemplate(); 55 | } else { 56 | _this.saveElement(); 57 | } 58 | }; 59 | 60 | 61 | /** 62 | * Show edit form for an $element 63 | */ 64 | this.editElement = function(element) { 65 | state.setElement(element); 66 | forms.fetchDataAndShowForm(ajaxurl + '?action=layotter_edit_element', { 67 | type: element.type, 68 | values: element.values 69 | }); 70 | }; 71 | 72 | 73 | /** 74 | * Save values from the element edit form being currently displayed 75 | */ 76 | this.saveElement = function() { 77 | // add element to model if creating a new element 78 | var isNewElement = false; 79 | if (state.getParent() !== null) { 80 | isNewElement = true; 81 | state.getParent().splice(state.getIndex() + 1, 0, state.getElement()); 82 | } 83 | 84 | // copy editing.element so state can be reset while ajax is still loading 85 | var editingElement = state.getElement(); 86 | state.reset(); 87 | editingElement.isLoading = true; 88 | 89 | // ACF wraps all form fields in a required object called 'acf' 90 | var values = jQuery('#layotter-edit, .layotter-modal #post').serialize() 91 | + '&' 92 | + jQuery.param({ 93 | type: editingElement.type 94 | }); 95 | 96 | $http({ 97 | url: ajaxurl + '?action=layotter_parse_element', 98 | method: 'POST', 99 | data: values, 100 | headers: { 101 | 'Content-Type': 'application/x-www-form-urlencoded' 102 | } 103 | }).success(function(reply) { 104 | editingElement.values = reply.values; 105 | editingElement.view = reply.view; 106 | editingElement.isLoading = undefined; 107 | if (isNewElement) { 108 | history.pushStep(layotterData.i18n.history.add_element); 109 | } else { 110 | history.pushStep(layotterData.i18n.history.edit_element); 111 | } 112 | 113 | // ACF compatibility 114 | acf.validation.unlockForm(); 115 | }); 116 | }; 117 | 118 | 119 | /** 120 | * Show edit form for an $item's (post, row, col or element) $options 121 | */ 122 | this.editOptions = function(type, item) { 123 | state.setOptionsType(type); 124 | state.setElement(item); 125 | forms.fetchDataAndShowForm(ajaxurl + '?action=layotter_edit_options', { 126 | type: type, 127 | values: item.options, 128 | post_id: layotterData.postID 129 | }); 130 | }; 131 | 132 | 133 | /** 134 | * Save values from the options edit form being currently displayed 135 | */ 136 | this.saveOptions = function() { 137 | // copy editing.element so editing can be reset while ajax is still loading 138 | var editingItem = state.getElement(); 139 | var optionsType = state.getOptionsType(); 140 | state.reset(); 141 | editingItem.isLoading = true; 142 | 143 | // ACF wraps all form fields in a required object called 'acf' 144 | var values = jQuery('#layotter-edit, .layotter-modal #post').serialize() 145 | + '&' 146 | + jQuery.param({ 147 | type: optionsType, 148 | post_id: layotterData.postID 149 | }); 150 | 151 | $http({ 152 | url: ajaxurl + '?action=layotter_parse_options', 153 | method: 'POST', 154 | data: values, 155 | headers: { 156 | 'Content-Type': 'application/x-www-form-urlencoded' 157 | } 158 | }).success(function(reply) { 159 | editingItem.options = reply; 160 | editingItem.isLoading = undefined; 161 | history.pushStep(layotterData.i18n.history['edit_' + optionsType + '_options']); 162 | 163 | // ACF compatibility 164 | acf.validation.unlockForm(); 165 | }); 166 | }; 167 | 168 | 169 | /** 170 | * Delete element at $index in the $parent col 171 | */ 172 | this.deleteElement = function(parent, index) { 173 | modals.confirm({ 174 | message: layotterData.i18n.delete_element_confirmation, 175 | okText: layotterData.i18n.delete_element, 176 | okAction: function() { 177 | parent.splice(index, 1); 178 | history.pushStep(layotterData.i18n.history.delete_element); 179 | }, 180 | cancelText: layotterData.i18n.cancel 181 | }); 182 | }; 183 | 184 | 185 | /** 186 | * Delete row at $index 187 | */ 188 | this.deleteRow = function(index) { 189 | var hasElements = false; 190 | 191 | angular.forEach(data.contentStructure.rows[index].cols, function(col) { 192 | if (col.elements.length) { 193 | hasElements = true; 194 | } 195 | }); 196 | 197 | // ask for confirmation only if the row contains any elements 198 | if (!hasElements) { 199 | data.contentStructure.rows.splice(index, 1); 200 | history.pushStep(layotterData.i18n.history.delete_row); 201 | return; 202 | } 203 | 204 | modals.confirm({ 205 | message: layotterData.i18n.delete_row_confirmation, 206 | okText: layotterData.i18n.delete_row, 207 | okAction: function() { 208 | data.contentStructure.rows.splice(index, 1); 209 | history.pushStep(layotterData.i18n.history.delete_row); 210 | }, 211 | cancelText: layotterData.i18n.cancel 212 | }); 213 | }; 214 | 215 | 216 | /** 217 | * Add a new empty row at $index 218 | */ 219 | this.addRow = function(index) { 220 | data.contentStructure.rows.splice(index + 1, 0, angular.copy(data.templates.row)); 221 | history.pushStep(layotterData.i18n.history.add_row); 222 | }; 223 | 224 | 225 | /** 226 | * Create an exact copy of the row at $index 227 | */ 228 | this.duplicateRow = function(index) { 229 | data.contentStructure.rows.splice(index, 0, angular.copy(data.contentStructure.rows[index])); 230 | history.pushStep(layotterData.i18n.history.duplicate_row); 231 | }; 232 | 233 | 234 | /** 235 | * Create an exact copy of the element at $index in the $parent col 236 | */ 237 | this.duplicateElement = function(parent, index) { 238 | parent.splice(index, 0, angular.copy(parent[index])); 239 | history.pushStep(layotterData.i18n.history.duplicate_element); 240 | }; 241 | 242 | 243 | /** 244 | * Get column layout string ('1/2', '2/3', etc.) for column at $index in $row 245 | */ 246 | this.getColLayout = function(row, index) { 247 | return row.layout.split(' ')[index]; 248 | }; 249 | 250 | 251 | /** 252 | * Change row layout for $row to new $layout (e.g. '1/2 1/4 1/4') 253 | */ 254 | this.setRowLayout = function(row, layout) { 255 | var oldColCount = row.layout.split(' ').length; 256 | var newColCount = layout.split(' ').length; 257 | row.layout = layout; 258 | 259 | // add empty cols if number of cols is increased 260 | if (newColCount > oldColCount) { 261 | for (var i = oldColCount; i < newColCount; i++) { 262 | row.cols.push(angular.copy(data.templates.col)); 263 | } 264 | } else { // move surplus elements to last remaining col if number of cols is decreased 265 | $animate.enabled(false); 266 | for (var i = newColCount; i < oldColCount; i++) { 267 | angular.forEach(row.cols[i].elements, function(element) { 268 | row.cols[newColCount - 1].elements.push(element); 269 | }); 270 | } 271 | row.cols.splice(newColCount); 272 | $timeout(function() { 273 | $animate.enabled(true); 274 | }, 1); 275 | } 276 | 277 | history.pushStep(layotterData.i18n.history.change_row_layout); 278 | }; 279 | 280 | 281 | /** 282 | * Close Lightbox only if no edit form is currently present 283 | */ 284 | this.cancelEditing = function() { 285 | if (angular.element('#layotter-changed').val() === '1') { 286 | modals.confirm({ 287 | message: layotterData.i18n.discard_changes_confirmation, 288 | okText: layotterData.i18n.discard_changes, 289 | okAction: function() { 290 | state.reset(); 291 | }, 292 | cancelText: layotterData.i18n.cancel 293 | }); 294 | } else { 295 | state.reset(); 296 | } 297 | }; 298 | 299 | 300 | /** 301 | * Close current overlay when clicking the dark background 302 | */ 303 | // forms 304 | angular.element(document).on('click', '#dennisbox .dennisbox-overlay', function() { 305 | _this.cancelEditing(); 306 | }); 307 | // modals 308 | angular.element(document).on('click', '#dennisbox-modal .dennisbox-overlay', function() { 309 | if (typeof $rootScope.confirm !== 'undefined') { 310 | $rootScope.confirm.cancelAction(); 311 | } 312 | if (typeof $rootScope.prompt !== 'undefined') { 313 | $rootScope.prompt.cancelAction(); 314 | } 315 | }); 316 | // when ESC is pressed and an edit form is open (but no confirmation or prompt modal), cancel editing 317 | angular.element(document).on('keyup', function(e) { 318 | if (e.keyCode == 27 && angular.element('#dennisbox').length && !angular.element('.layotter-modal-confirm').length && !angular.element('.layotter-modal-prompt').length) { 319 | angular.element('#layotter-edit :focus, .layotter-modal #post :focus').blur(); 320 | _this.cancelEditing(); 321 | } 322 | }); 323 | 324 | }); -------------------------------------------------------------------------------- /assets/js/vendor/angular-ui-sortable-old.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery UI Sortable plugin wrapper 3 | 4 | @param [ui-sortable] {object} Options to pass to $.fn.sortable() merged onto ui.config 5 | */ 6 | angular.module('ui.sortable', []) 7 | .value('uiSortableConfig',{}) 8 | .directive('uiSortable', [ 9 | 'uiSortableConfig', '$timeout', '$log', 10 | function(uiSortableConfig, $timeout, $log) { 11 | return { 12 | require: '?ngModel', 13 | link: function(scope, element, attrs, ngModel) { 14 | var savedNodes; 15 | 16 | function combineCallbacks(first,second){ 17 | if(second && (typeof second === 'function')) { 18 | return function(e, ui) { 19 | first(e, ui); 20 | second(e, ui); 21 | }; 22 | } 23 | return first; 24 | } 25 | 26 | function hasSortingHelper (element, ui) { 27 | var helperOption = element.sortable('option','helper'); 28 | return helperOption === 'clone' || (typeof helperOption === 'function' && ui.item.sortable.isCustomHelperUsed()); 29 | } 30 | 31 | // thanks jquery-ui 32 | function isFloating (item) { 33 | return (/left|right/).test(item.css('float')) || (/inline|table-cell/).test(item.css('display')); 34 | } 35 | 36 | function afterStop(e, ui) { 37 | ui.item.sortable._destroy(); 38 | } 39 | 40 | var opts = {}; 41 | 42 | // directive specific options 43 | var directiveOpts = { 44 | 'ui-floating': undefined 45 | }; 46 | 47 | var callbacks = { 48 | receive: null, 49 | remove:null, 50 | start:null, 51 | stop:null, 52 | update:null 53 | }; 54 | 55 | var wrappers = { 56 | helper: null 57 | }; 58 | 59 | angular.extend(opts, directiveOpts, uiSortableConfig, scope.$eval(attrs.uiSortable)); 60 | 61 | if (!angular.element.fn || !angular.element.fn.jquery) { 62 | $log.error('ui.sortable: jQuery should be included before AngularJS!'); 63 | return; 64 | } 65 | 66 | if (ngModel) { 67 | 68 | // When we add or remove elements, we need the sortable to 'refresh' 69 | // so it can find the new/removed elements. 70 | scope.$watch(attrs.ngModel+'.length', function() { 71 | // Timeout to let ng-repeat modify the DOM 72 | $timeout(function() { 73 | // ensure that the jquery-ui-sortable widget instance 74 | // is still bound to the directive's element 75 | if (!!element.data('ui-sortable')) { 76 | element.sortable('refresh'); 77 | } 78 | }); 79 | }); 80 | 81 | callbacks.start = function(e, ui) { 82 | if (opts['ui-floating'] === 'auto') { 83 | // since the drag has started, the element will be 84 | // absolutely positioned, so we check its siblings 85 | var siblings = ui.item.siblings(); 86 | angular.element(e.target).data('ui-sortable').floating = isFloating(siblings); 87 | } 88 | 89 | // Save the starting position of dragged item 90 | ui.item.sortable = { 91 | model: ngModel.$modelValue[ui.item.index()], 92 | index: ui.item.index(), 93 | source: ui.item.parent(), 94 | sourceModel: ngModel.$modelValue, 95 | cancel: function () { 96 | ui.item.sortable._isCanceled = true; 97 | }, 98 | isCanceled: function () { 99 | return ui.item.sortable._isCanceled; 100 | }, 101 | isCustomHelperUsed: function () { 102 | return !!ui.item.sortable._isCustomHelperUsed; 103 | }, 104 | _isCanceled: false, 105 | _isCustomHelperUsed: ui.item.sortable._isCustomHelperUsed, 106 | _destroy: function () { 107 | angular.forEach(ui.item.sortable, function(value, key) { 108 | ui.item.sortable[key] = undefined; 109 | }); 110 | } 111 | }; 112 | }; 113 | 114 | callbacks.activate = function(/*e, ui*/) { 115 | // We need to make a copy of the current element's contents so 116 | // we can restore it after sortable has messed it up. 117 | // This is inside activate (instead of start) in order to save 118 | // both lists when dragging between connected lists. 119 | savedNodes = element.contents(); 120 | 121 | // If this list has a placeholder (the connected lists won't), 122 | // don't inlcude it in saved nodes. 123 | var placeholder = element.sortable('option','placeholder'); 124 | 125 | // placeholder.element will be a function if the placeholder, has 126 | // been created (placeholder will be an object). If it hasn't 127 | // been created, either placeholder will be false if no 128 | // placeholder class was given or placeholder.element will be 129 | // undefined if a class was given (placeholder will be a string) 130 | if (placeholder && placeholder.element && typeof placeholder.element === 'function') { 131 | var phElement = placeholder.element(); 132 | // workaround for jquery ui 1.9.x, 133 | // not returning jquery collection 134 | phElement = angular.element(phElement); 135 | 136 | // exact match with the placeholder's class attribute to handle 137 | // the case that multiple connected sortables exist and 138 | // the placehoilder option equals the class of sortable items 139 | var excludes = element.find('[class="' + phElement.attr('class') + '"]'); 140 | 141 | savedNodes = savedNodes.not(excludes); 142 | } 143 | }; 144 | 145 | callbacks.update = function(e, ui) { 146 | // Save current drop position but only if this is not a second 147 | // update that happens when moving between lists because then 148 | // the value will be overwritten with the old value 149 | if(!ui.item.sortable.received) { 150 | ui.item.sortable.dropindex = ui.item.index(); 151 | var droptarget = ui.item.parent(); 152 | ui.item.sortable.droptarget = droptarget; 153 | ui.item.sortable.droptargetModel = droptarget.scope().$eval(droptarget.attr('ng-model')); 154 | 155 | // Cancel the sort (let ng-repeat do the sort for us) 156 | // Don't cancel if this is the received list because it has 157 | // already been canceled in the other list, and trying to cancel 158 | // here will mess up the DOM. 159 | element.sortable('cancel'); 160 | } 161 | 162 | // Put the nodes back exactly the way they started (this is very 163 | // important because ng-repeat uses comment elements to delineate 164 | // the start and stop of repeat sections and sortable doesn't 165 | // respect their order (even if we cancel, the order of the 166 | // comments are still messed up). 167 | if (hasSortingHelper(element, ui) && !ui.item.sortable.received && 168 | element.sortable( 'option', 'appendTo' ) === 'parent') { 169 | // restore all the savedNodes except .ui-sortable-helper element 170 | // (which is placed last). That way it will be garbage collected. 171 | savedNodes = savedNodes.not(savedNodes.last()); 172 | } 173 | savedNodes.appendTo(element); 174 | 175 | // If this is the target connected list then 176 | // it's safe to clear the restored nodes since: 177 | // update is currently running and 178 | // stop is not called for the target list. 179 | if(ui.item.sortable.received) { 180 | savedNodes = null; 181 | } 182 | 183 | // If received is true (an item was dropped in from another list) 184 | // then we add the new item to this list otherwise wait until the 185 | // stop event where we will know if it was a sort or item was 186 | // moved here from another list 187 | if(ui.item.sortable.received && !ui.item.sortable.isCanceled()) { 188 | scope.$apply(function () { 189 | ngModel.$modelValue.splice(ui.item.sortable.dropindex, 0, 190 | ui.item.sortable.moved); 191 | }); 192 | } 193 | }; 194 | 195 | callbacks.stop = function(e, ui) { 196 | // If the received flag hasn't be set on the item, this is a 197 | // normal sort, if dropindex is set, the item was moved, so move 198 | // the items in the list. 199 | if(!ui.item.sortable.received && 200 | ('dropindex' in ui.item.sortable) && 201 | !ui.item.sortable.isCanceled()) { 202 | 203 | scope.$apply(function () { 204 | ngModel.$modelValue.splice( 205 | ui.item.sortable.dropindex, 0, 206 | ngModel.$modelValue.splice(ui.item.sortable.index, 1)[0]); 207 | }); 208 | } else { 209 | // if the item was not moved, then restore the elements 210 | // so that the ngRepeat's comment are correct. 211 | if ((!('dropindex' in ui.item.sortable) || ui.item.sortable.isCanceled()) && 212 | !hasSortingHelper(element, ui)) { 213 | savedNodes.appendTo(element); 214 | } 215 | } 216 | 217 | // It's now safe to clear the savedNodes 218 | // since stop is the last callback. 219 | savedNodes = null; 220 | }; 221 | 222 | callbacks.receive = function(e, ui) { 223 | // An item was dropped here from another list, set a flag on the 224 | // item. 225 | ui.item.sortable.received = true; 226 | }; 227 | 228 | callbacks.remove = function(e, ui) { 229 | // Workaround for a problem observed in nested connected lists. 230 | // There should be an 'update' event before 'remove' when moving 231 | // elements. If the event did not fire, cancel sorting. 232 | if (!('dropindex' in ui.item.sortable)) { 233 | element.sortable('cancel'); 234 | ui.item.sortable.cancel(); 235 | } 236 | 237 | // Remove the item from this list's model and copy data into item, 238 | // so the next list can retrive it 239 | if (!ui.item.sortable.isCanceled()) { 240 | scope.$apply(function () { 241 | ui.item.sortable.moved = ngModel.$modelValue.splice( 242 | ui.item.sortable.index, 1)[0]; 243 | }); 244 | } 245 | }; 246 | 247 | wrappers.helper = function (inner) { 248 | if (inner && typeof inner === 'function') { 249 | return function (e, item) { 250 | var innerResult = inner(e, item); 251 | item.sortable._isCustomHelperUsed = item !== innerResult; 252 | return innerResult; 253 | }; 254 | } 255 | return inner; 256 | }; 257 | 258 | scope.$watch(attrs.uiSortable, function(newVal /*, oldVal*/) { 259 | // ensure that the jquery-ui-sortable widget instance 260 | // is still bound to the directive's element 261 | if (!!element.data('ui-sortable')) { 262 | angular.forEach(newVal, function(value, key) { 263 | // if it's a custom option of the directive, 264 | // handle it approprietly 265 | if (key in directiveOpts) { 266 | if (key === 'ui-floating' && (value === false || value === true)) { 267 | element.data('ui-sortable').floating = value; 268 | } 269 | 270 | opts[key] = value; 271 | return; 272 | } 273 | 274 | if (callbacks[key]) { 275 | if( key === 'stop' ){ 276 | // call apply after stop 277 | value = combineCallbacks( 278 | value, function() { scope.$apply(); }); 279 | 280 | value = combineCallbacks(value, afterStop); 281 | } 282 | // wrap the callback 283 | value = combineCallbacks(callbacks[key], value); 284 | } else if (wrappers[key]) { 285 | value = wrappers[key](value); 286 | } 287 | 288 | opts[key] = value; 289 | element.sortable('option', key, value); 290 | }); 291 | } 292 | }, true); 293 | 294 | angular.forEach(callbacks, function(value, key) { 295 | opts[key] = combineCallbacks(value, opts[key]); 296 | if( key === 'stop' ){ 297 | opts[key] = combineCallbacks(opts[key], afterStop); 298 | } 299 | }); 300 | 301 | } else { 302 | $log.info('ui.sortable: ngModel not provided!', element); 303 | } 304 | 305 | // Create sortable 306 | element.sortable(opts); 307 | } 308 | }; 309 | } 310 | ]); 311 | --------------------------------------------------------------------------------