├── .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 | {{ confirm.okText }}
9 | {{ confirm.cancelText }}
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 | {{ prompt.okText }}
12 | {{ prompt.cancelText }}
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 |
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 |
24 |
27 |
--------------------------------------------------------------------------------
/views/form.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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(""),e(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=/^,
11 | K=/^<\//,I=/\x3c!--(.*?)--\x3e/g,z=/]*?)>/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 |
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 |
19 |
20 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
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 |
--------------------------------------------------------------------------------