test
Server : Apache System : Linux festive-vaughan.27-254-111-205.plesk.page 4.19.0-14-amd64 #1 SMP Debian 4.19.171-2 (2021-01-30) x86_64 User : acaindustrial ( 10114) PHP Version : 8.2.25 Disable Function : opcache_get_status Directory : /var/www/vhosts/acaindustrial.co.th/httpdocs/wp-content/plugins/so-css/js/ |
Upload File : |
/* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */ ( function ( $, _, socssOptions ) { var socss = { model: {}, collection: {}, view: {}, fn: {} }; window.socss = socss; socss.model.CustomCssModel = Backbone.Model.extend( { defaults: { postId: null, postTitle: null, css: null, }, urlRoot: socssOptions.postCssUrlRoot, url: function () { return this.urlRoot + '&postId=' + this.get( 'postId' ); } } ); socss.model.CustomCssCollection = Backbone.Collection.extend( { model: socss.model.CustomCssModel, modelId: function( attrs ) { return attrs.postId; }, } ); socss.model.CSSEditorModel = Backbone.Model.extend( { defaults: { customCssPosts: null, } } ); /** * The toolbar view */ socss.view.toolbar = Backbone.View.extend( { button: _.template( '<li><a href="#<%= action %>" class="toolbar-button socss-button"><%= text %></a></li>' ), events: { 'click .socss-button:not(.save)': 'triggerEvent', }, triggerEvent: function ( event ) { event.preventDefault(); var $target = $( event.currentTarget ); $target.trigger( 'blur' ); var value = $target.attr( 'href' ).replace( '#', '' ); this.$el.trigger( 'click_' + value ); }, addButton: function ( text, action ) { var button = $( this.button( { text: text, action: action } ) ) .appendTo( this.$( '.toolbar-function-buttons .toolbar-buttons' ) ); return button; }, } ); /** * The editor view, which handles codemirror stuff * * model: socss.model.CSSEditorModel * */ socss.view.editor = Backbone.View.extend( { codeMirror: null, snippets: null, toolbar: null, visualProperties: null, inspector: null, cssSelectors: [], initValue: null, events: { 'click_expand .custom-css-toolbar': 'toggleExpand', 'click_visual .custom-css-toolbar': 'showVisualEditor', 'click .socss-button.save': 'save', 'submit': 'onSubmit', }, initialize: function ( options ) { this.listenTo( this.model, 'change:selectedPost', this.getSelectedPostCss ); this.getSelectedPostCss().then( function () { if ( options.openVisualEditor ) { this.showVisualEditor(); } }.bind( this ) ); }, save: function () { socss.save( this ); }, getSelectedPostCss: function () { var selectedPost = this.model.get( 'selectedPost' ); var promise; if ( selectedPost && ! selectedPost.has( 'css' ) ) { promise = selectedPost.fetch(); } else { promise = new $.Deferred().resolve(); } return promise.then( this.render.bind( this ) ); }, render: function () { var selectedPost = this.model.get( 'selectedPost' ); if ( selectedPost && !selectedPost.has( 'css' ) ) { return this; } if ( !this.codeMirror ) { this.setupEditor(); } if ( ! this.toolbar ) { this.toolbar = new socss.view.toolbar( { el: this.$( '.custom-css-toolbar' ), model: this.model, } ); this.toolbar.render(); } if ( !this.visualProperties ) { this.visualProperties = new socss.view.properties( { editor: this, el: $( '#so-custom-css-properties' ) } ); this.visualProperties.render(); } if ( !this.preview ) { this.preview = new socss.view.preview( { editor: this, model: this.model, el: this.$( '.custom-css-preview' ), initURL: socssOptions.homeURL, } ); this.preview.render(); } if ( selectedPost ) { this.codeMirror.setValue( selectedPost.get( 'css' ) ); this.codeMirror.clearHistory(); } return this; }, /** * Do the initial setup of the CodeMirror editor */ setupEditor: function () { // Setup the Codemirror instance var $textArea = this.$( 'textarea.css-editor' ); this.initValue = $textArea.val(); // Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste // options in context menu. var newlineMatches = this.initValue.match( /\n/gm ); var lineCount = newlineMatches ? newlineMatches.length + 1 : 1; var paddedValue = this.initValue; $textArea.val( paddedValue ); var codeMirrorSettings = { tabSize: 2, lineNumbers: true, mode: 'css', theme: $textArea.data( 'theme' ), inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc. gutters: [ "CodeMirror-lint-markers" ], lint: true, search: true, dialog: true, annotateScrollbar: true, extraKeys: { 'Ctrl-F': 'findPersistent', 'Alt-G': 'jumpToLine', }, } if ( typeof wp.codeEditor != "undefined" ) { codeMirrorSettings = _.extend( wp.codeEditor.defaultSettings.codemirror, codeMirrorSettings ); this.codeMirror = wp.codeEditor.initialize( $textArea.get( 0 ), codeMirrorSettings ).codemirror; } else { this.registerCodeMirrorAutocomplete(); this.codeMirror = CodeMirror.fromTextArea( $textArea.get( 0 ), codeMirrorSettings ); this.setupCodeMirrorExtensions(); } var editor = this.codeMirror; $( '#so_css_editor_theme' ).on( 'change', function() { if ( $( this ).val() == 1 ) { editor.setOption( 'theme', 'neat' ); } else { editor.setOption( 'theme', 'ambiance' ); } } ); this.codeMirror.on( 'change', function ( cm, change ) { var selectedPost = this.model.get( 'selectedPost' ); if ( selectedPost && selectedPost.get( 'css' ) !== cm.getValue().trim() ) { selectedPost.set( 'css', cm.getValue().trim() ); } }.bind( this ) ); // Make sure the user doesn't leave without saving $( window ).on( 'beforeunload', function () { var editorValue = this.codeMirror.getValue().trim(); if ( editorValue !== this.initValue ) { return socssOptions.loc.leave; } }.bind( this ) ); // Set the container to visible overflow once the editor is setup this.$el.find( '.custom-css-container' ).css( 'overflow', 'visible' ); this.scaleEditor(); // Scale the editor whenever the window is resized $( window ).on( 'resize', function () { this.scaleEditor(); }.bind( this ) ); }, onSubmit: function () { this.initValue = this.codeMirror.getValue().trim(); }, /** * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder. */ registerCodeMirrorAutocomplete: function () { var pseudoClasses = { link: 1, visited: 1, active: 1, hover: 1, focus: 1, "first-letter": 1, "first-line": 1, "first-child": 1, before: 1, after: 1, lang: 1 }; CodeMirror.registerHelper( "hint", "css", function ( cm ) { var cur = cm.getCursor(), token = cm.getTokenAt( cur ); var inner = CodeMirror.innerMode( cm.getMode(), token.state ); if ( inner.mode.name !== "css" ) { return; } if ( token.type === "keyword" && "!important".indexOf( token.string ) === 0 ) { return { list: [ "!important" ], from: CodeMirror.Pos( cur.line, token.start ), to: CodeMirror.Pos( cur.line, token.end ) }; } var start = token.start, end = cur.ch, word = token.string.slice( 0, end - start ); if ( /[^\w$_-]/.test( word ) ) { word = ""; start = end = cur.ch; } var spec = CodeMirror.resolveMode( "text/css" ); var result = []; function add( keywords ) { for ( var name in keywords ) { if ( !word || name.lastIndexOf( word, 0 ) === 0 ) { result.push( name ); } } } var st = inner.state.state; if ( st === 'top' ) { // We're going to autocomplete the selector using our own set of rules var line = cm.getLine( cur.line ).trim(); var selectors = this.cssSelectors; for ( var i = 0; i < selectors.length; i++ ) { if ( selectors[ i ].selector.indexOf( line ) !== -1 ) { result.push( selectors[ i ].selector ); } } if ( result.length ) { return { list: result, from: CodeMirror.Pos( cur.line, 0 ), to: CodeMirror.Pos( cur.line, end ) }; } } else { if ( st === "pseudo" || token.type === "variable-3" ) { add( pseudoClasses ); } else if ( st === "block" || st === "maybeprop" ) { add( spec.propertyKeywords ); } else if ( st === "prop" || st === "parens" || st === "at" || st === "params" ) { add( spec.valueKeywords ); add( spec.colorKeywords ); } else if ( st === "media" || st === "media_parens" ) { add( spec.mediaTypes ); add( spec.mediaFeatures ); } if ( result.length ) { return { list: result, from: CodeMirror.Pos( cur.line, start ), to: CodeMirror.Pos( cur.line, end ) }; } } }.bind( this ) ); }, setupCodeMirrorExtensions: function () { this.codeMirror.on( 'cursorActivity', function ( cm ) { var cur = cm.getCursor(), token = cm.getTokenAt( cur ); var inner = CodeMirror.innerMode( cm.getMode(), token.state ); // If we have a qualifier selected, then highlight that in the preview if ( token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin' ) { var line = cm.getLine( cur.line ); var selector = line.substring( 0, token.end ); this.preview.highlight( selector ); } else { this.preview.clearHighlight(); } }.bind( this ) ); if ( typeof CodeMirror.showHint == 'function' ) { // This sets up automatic autocompletion at all times this.codeMirror.on( 'keyup', function ( cm, e ) { if ( ( e.keyCode >= 65 && e.keyCode <= 90 ) || ( e.keyCode === 189 && !e.shiftKey ) || ( e.keyCode === 190 && !e.shiftKey ) || ( e.keyCode === 51 && e.shiftKey ) || ( e.keyCode === 189 && e.shiftKey ) ) { cm.showHint( { completeSingle: false } ); } } ); } }, /** * Scale the size of the editor depending on whether it's expanded or not */ scaleEditor: function () { var windowHeight = $( window ).outerHeight(); var areaHeight; if ( this.$el.hasClass( 'expanded' ) ) { // If we're in the expanded view, then resize the editor this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', '' ); areaHeight = windowHeight - this.$( '.custom-css-toolbar' ).outerHeight(); this.codeMirror.setSize( '100%', areaHeight ); this.$el.find( '.CodeMirror-scroll' ).css( 'height', '100%' ); } else { // Attempt to calculate approximate space available for editor when not expanded. var $form = $( '#so-custom-css-form' ); var otherEltsHeight = $( '#wpadminbar' ).outerHeight( true ) + $( '#siteorigin-custom-css' ).find( '> h2' ).outerHeight( true ) + $form.find( '> .custom-css-toolbar' ).outerHeight( true ) + $form.find( '> .so-css-footer' ).outerHeight( true ) + parseFloat( $( '#wpbody-content' ).css( 'padding-bottom' ) ); areaHeight = windowHeight - otherEltsHeight; // The container has a min-height of 300px so we need to ensure the areaHeight is at least that large. if ( areaHeight < 300 ) { areaHeight = 300; } this.codeMirror.setSize( '100%', 'auto' ); this.$el.find( '.CodeMirror-scroll' ).css( 'height', areaHeight + 'px' ); } this.$el.find( '.CodeMirror-code' ).css( 'height', areaHeight + 'px' ); }, /** * Check if the editor is in expanded mode * @returns bool */ isExpanded: function () { return this.$el.hasClass( 'expanded' ); }, /** * Toggle if this is expanded or not */ toggleExpand: function ( e ) { $( '.editor-expand' ).attr( 'title', $( '.so-css-icon-' + ( this.isExpanded() ? 'expand' : 'compress ') ).attr( 'title' ) ); this.$el.toggleClass( 'expanded' ); this.scaleEditor(); }, /** * Set the expanded state of the editor * @param expanded */ setExpand: function ( expanded ) { if ( expanded ) { this.$el.addClass( 'expanded' ); } else { this.$el.removeClass( 'expanded' ); } this.scaleEditor(); }, /** * Show the visual editor view. */ showVisualEditor: function () { this.visualProperties.loadCSS( this.codeMirror.getValue().trim() ); this.visualProperties.show(); }, /** * Set the snippets available to this editor */ setSnippets: function ( snippets ) { if ( !_.isEmpty( snippets ) ) { this.snippets = new socss.view.snippets( { snippets: snippets } ); this.snippets.editor = this; this.snippets.render(); this.toolbar.addButton( 'Snippets', 'snippets' ); this.toolbar.on( 'click_snippets', function () { this.snippets.show(); }.bind( this ) ); } }, /** * Add some CSS to the editor. * @param css */ addCode: function ( css ) { var editor = this.codeMirror; var before_css = ''; if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) { before_css = ""; } else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) { before_css = "\n"; } else { before_css = "\n\n"; } // Now insert the code in the editor editor.doc.setCursor( editor.doc.lastLine(), editor.doc.getLine( editor.doc.lastLine() ).length ); editor.doc.replaceSelection( before_css + css ); }, addEmptySelector: function ( selector ) { this.addCode( selector + " {\n \n}" ); }, /** * Sets the inspector view that's being used by the editor */ setInspector: function ( inspector ) { this.inspector = inspector; this.cssSelectors = inspector.pageSelectors; // A selector is clicked in the inspector inspector.on( 'click_selector', function ( selector ) { if ( this.visualProperties.isVisible() ) { this.visualProperties.addSelector( selector ); } else { this.addEmptySelector( selector ); } }.bind( this ) ); // A property is clicked in the inspector inspector.on( 'click_property', function ( property ) { if ( !this.visualProperties.isVisible() ) { this.codeMirror.replaceSelection( property + ";\n " ); } }.bind( this ) ); inspector.on( 'set_active_element', function ( el, selectors ) { if ( this.visualProperties.isVisible() && selectors.length ) { this.visualProperties.addSelector( selectors[ 0 ].selector ); } }.bind( this ) ); } } ); /** * The preview. */ socss.view.preview = Backbone.View.extend( { template: _.template( $( '#template-preview-window' ).html() ), editor: null, originalUri: null, currentUri: null, events: { 'mouseleave #preview-iframe': 'clearHighlight', 'keydown #preview-navigator input[type="text"]': 'reloadPreview', }, initialize: function ( attr ) { this.editor = attr.editor; this.listenTo( this.model, 'change:selectedPost', this.render.bind( this ) ); this.originalUri = new URI( attr.initURL ); this.currentUri = new URI( attr.initURL ); this.editor.codeMirror.on( 'change', function ( cm, c ) { this.updatePreviewCss(); }.bind( this ) ); }, render: function () { var selectedPost = this.model.get( 'selectedPost' ); if ( selectedPost && !selectedPost.has( 'postUrl' ) ) { selectedPost.fetch().then( this.render.bind( this ) ); return this; } this.$el.html( this.template() ); if ( selectedPost ) { this.currentUri = new URI( selectedPost.get( 'postUrl' ) ); } this.currentUri.removeQuery( 'so_css_preview', 1 ); this.$( '#preview-navigator input' ).val( this.currentUri.toString() ); this.currentUri.addQuery( 'so_css_preview', 1 ); this.$( '#preview-iframe' ) .attr( 'src', this.currentUri.toString() ) // 'load' event doesn't bubble so can't be used in the events hash .on( 'load', this.initPreview.bind( this ) ); }, initPreview: function () { var $$ = this.$( '#preview-iframe' ); // Update the current URI with the iframe URI this.currentUri = new URI( $$.contents().get( 0 ).location.href ); this.currentUri.removeQuery( 'so_css_preview' ); this.$( '#preview-navigator input' ).val( this.currentUri.toString() ); this.currentUri.addQuery( 'so_css_preview', 1 ); var wcCheck = $$.contents().find( '.single-product' ).length; $$.contents().find( 'a' ).each( function () { var link = $( this ); var href = link.attr( 'href' ); if ( href === undefined || ( wcCheck && link.parents( '.wc-tabs' ).length ) ) { return true; } var firstSeperator = ( href.indexOf( '?' ) === -1 ? '?' : '&' ); link.attr( 'href', href + firstSeperator + 'so_css_preview=1' ); } ); this.updatePreviewCss(); }, reloadPreview: function ( e ) { var $$ = this.$( '#preview-navigator input[type="text"]' ); if ( e.keyCode === 13 ) { e.preventDefault(); var newUri = new URI( $$.val() ); // Validate the URI if ( this.originalUri.host() !== newUri.host() || this.originalUri.protocol() !== newUri.protocol() ) { $$.trigger( 'blur' ); alert( $$.data( 'invalid-uri' ) ); $$.trigger( 'focus' ); } else { newUri.addQuery( 'so_css_preview', 1 ); this.$( '#preview-iframe' ).attr( 'src', newUri.toString() ); } } }, /** * Update the preview CSS from the CodeMirror value in the editor */ updatePreviewCss: function () { var preview = this.$( '#preview-iframe' ); if ( preview.length === 0 ) { return; } var head = preview.contents().find( 'head' ); if ( head.find( 'style.siteorigin-custom-css' ).length === 0 ) { head.append( '<style class="siteorigin-custom-css" type="text/css"></style>' ); } var style = head.find( 'style.siteorigin-custom-css' ); // Update the CSS after a short delay var css = this.editor.codeMirror.getValue().trim(); style.html( css ); }, /** * Highlight all elements with a given selector */ highlight: function ( selector ) { try { this.editor.inspector.hl.highlight( selector ); } catch ( err ) { console.log( 'No inspector to highlight with' ); } }, /** * Clear the currently highlighted elements in preview */ clearHighlight: function () { try { this.editor.inspector.hl.clear(); } catch ( err ) { console.log( 'No inspector to highlight with' ); } } } ); /** * The dialog for the snippets browser */ socss.view.snippets = Backbone.View.extend( { template: _.template( $( '#template-snippet-browser' ).html() ), snippet: _.template( '<li class="snippet"><%- name %></li>' ), className: 'css-editor-snippet-browser', snippets: null, editor: null, events: { 'click .close': 'hide', 'click .buttons .insert-snippet': 'insertSnippet', 'click .snippet': 'clickSnippet', }, currentSnippet: null, initialize: function ( args ) { this.snippets = args.snippets; }, render: function () { this.$el.html( this.template() ); for ( var i = 0; i < this.snippets.length; i++ ) { $( this.snippet( { name: this.snippets[ i ].Name } ) ) .data( { 'description': this.snippets[ i ].Description, 'css': this.snippets[ i ].css } ) .appendTo( this.$( 'ul.snippets' ) ); } // Click on the first one this.$( '.snippets li.snippet' ).eq( 0 ).trigger( 'click' ); this.attach(); return this; }, clickSnippet: function ( event ) { event.preventDefault(); var $$ = $( event.currentTarget ); this.$( '.snippets li.snippet' ).removeClass( 'active' ); $( this ).addClass( 'active' ); this.viewSnippet( { name: $$.html(), description: $$.data( 'description' ), css: $$.data( 'css' ) } ); }, viewSnippet: function ( args ) { var w = this.$( '.main .snippet-view' ); w.find( '.snippet-title' ).html( args.name ); w.find( '.snippet-description' ).html( args.description ); w.find( '.snippet-code' ).html( args.css ); this.currentSnippet = args; }, insertSnippet: function () { var editor = this.editor.codeMirror; var css = this.currentSnippet.css; var before_css = ''; if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) { before_css = ""; } else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) { before_css = "\n"; } else { before_css = "\n\n"; } // Now insert the code in the editor editor.doc.setCursor( editor.doc.lastLine(), editor.doc.getLine( editor.doc.lastLine() ).length ); editor.doc.replaceSelection( before_css + css ); this.hide(); }, attach: function () { this.$el.appendTo( 'body' ); }, show: function () { this.$el.show(); }, hide: function () { this.$el.hide(); } } ); socss.save = function ( view ) { let saveBtn = $( '#siteorigin-custom-css .save' ); var css; if ( ! saveBtn.hasClass( 'button-primary-disabled' ) ) { saveBtn.addClass( 'button-primary-disabled' ) // Which view is the user using? if ( typeof view.editor != 'undefined' ) { // Visual. css = view.editor.codeMirror.getValue().trim(); view.updateMainEditor( true ); } else { // Expanded. css = view.codeMirror.getValue().trim(); } $.post( socssOptions.ajaxurl, { action: 'socss_save_css', css: css, }, null, 'html' ).done( function ( response ) { if ( response.length ) { // Update was successful. Update revisions list. $( '.custom-revisions-list' ).html( response ); } }) .fail( function ( error ) { // Something went wrong. Output the error message as an alert. alert( error.responseText ); } ) .always( function () { saveBtn.removeClass( 'button-primary-disabled' ) } ); } }; /** * The visual properties editor */ socss.view.properties = Backbone.View.extend( { tabTemplate: _.template( '<li data-section="<%- id %>"><span class="so-css-icon so-css-icon-<%- icon %>"></span> <%- title %></li>' ), sectionTemplate: _.template( '<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>' ), controllerTemplate: _.template( '<tr><th scope="row"><%- title %></th><td></td></tr>' ), /** * The controllers for each of the properties */ propertyControllers: [], /** * The editor view */ editor: null, /** * The current, raw CSS */ css: '', /** * Parsed CSS */ parsed: {}, /** * The current active selector */ activeSelector: '', /** * Was the editor expanded before we went into the property editor */ editorExpandedBefore: false, events: { 'click .close': 'hide', 'click .save': 'save', 'click .section-tabs li': 'onTabClick', 'change .toolbar select': 'onToolbarSelectChange', }, /** * Initialize the properties editor with a new model */ initialize: function ( options ) { this.parser = window.css; this.editor = options.editor; }, /** * Render the property editor */ render: function () { // Clean up for potential re-renders this.$( '.section-tabs' ).empty(); this.$( '.sections' ).empty(); this.$( '.toolbar select' ).off(); this.propertyControllers = []; var controllers = socssOptions.propertyControllers; for ( var id in controllers ) { // Create the tabs var $t = $( this.tabTemplate( { id: id, icon: controllers[ id ].icon, title: controllers[ id ].title } ) ).appendTo( this.$( '.section-tabs' ) ); // Create the section wrapper var $s = $( this.sectionTemplate( { id: id } ) ).appendTo( this.$( '.sections' ) ); // Now lets add the controllers if ( !_.isEmpty( controllers[ id ].controllers ) ) { for ( var i = 0; i < controllers[ id ].controllers.length; i++ ) { var $c = $( this.controllerTemplate( { title: controllers[ id ].controllers[ i ].title } ) ).appendTo( $s.find( 'tbody' ) ); var controllerAtts = controllers[ id ].controllers[ i ]; var controller; if ( typeof socss.view.properties.controllers[ controllerAtts.type ] === 'undefined' ) { // Setup a default controller controller = new socss.view.propertyController( { el: $c.find( 'td' ), propertiesView: this, args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args ) } ); } else { // Setup a specific controller controller = new socss.view.properties.controllers[ controllerAtts.type ]( { el: $c.find( 'td' ), propertiesView: this, args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args ) } ); } this.propertyControllers.push( controller ); // Setup and render the controller controller.render(); } } } // Switch to the first tab. this.$( '.section-tabs li' ).eq( 0 ).trigger( 'click' ); }, onTabClick: function ( event ) { var $$ = $( event.currentTarget ); var show = this.$( '.sections .section[data-section="' + $$.data( 'section' ) + '"]' ); this.$( '.sections .section' ).not( show ).hide().removeClass( 'active' ); show.show().addClass( 'active' ); this.$( '.section-tabs li' ).not( $$ ).removeClass( 'active' ); $$.addClass( 'active' ); }, onToolbarSelectChange: function ( event ) { this.setActiveSelector( $( event.currentTarget ).find( ':selected' ).data( 'selector' ) ); }, /** * Sets the rule value for the active selector * @param rule * @param value */ setRuleValue: function ( rule, value ) { if ( typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined' ) { return; } var declarations = this.activeSelector.declarations; var newRule = true; var valueChanged = false; for ( var i = 0; i < declarations.length; i++ ) { if ( declarations[ i ].property === rule ) { newRule = false; var declaration = declarations[ i ]; if ( declaration.value !== value ) { declaration.value = value; valueChanged = true; } // Remove empty declarations if ( _.isEmpty( declaration.value ) ) { declarations.splice( declarations.indexOf( declaration ) ); } break; } } if ( newRule && !_.isEmpty( value ) ) { declarations.push( { property: rule, value: value, type: 'declaration', } ); valueChanged = true; } if ( valueChanged ) { this.updateMainEditor( false ); } }, /** * Adds the @import rule value if it doesn't already exist. * * @param newRule * */ addImport: function ( newRule ) { // get @import rules // check if any have the same value // if not, then add the new @ rule var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule ) { return rule.type === 'import'; } ); var exists = _.any( importRules, function ( rule ) { return rule.import === newRule.import; } ); if ( !exists ) { // Add it to the top! // @import statements must precede other rule types. this.parsed.stylesheet.rules.unshift( newRule ); this.updateMainEditor( false ); } }, /** * Find @import which completely or partially contains the specified value. * * @param value */ findImport: function ( value ) { return _.find( this.parsed.stylesheet.rules, function ( rule ) { return rule.type === 'import' && rule.import.indexOf( value ) > -1; } ); }, /** * Find @import which completely or partially contains the identifier value and update it's import property. * * @param identifier * @param value */ updateImport: function ( identifier, value ) { var importRule = this.findImport( identifier ); if ( importRule.import !== value.import ) { importRule.import = value.import; this.updateMainEditor( false ); } }, /** * Find @import which completely or partially contains the identifier value and remove it. * * @param identifier */ removeImport: function ( identifier ) { var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) { return rule.type === 'import' && rule.import.indexOf( identifier ) > -1; } ); if ( importIndex > -1 ) { this.parsed.stylesheet.rules.splice( importIndex, 1 ); } }, /** * Get the rule value for the active selector * @param rule */ getRuleValue: function ( rule ) { if ( typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined' ) { return ''; } var declarations = this.activeSelector.declarations; for ( var i = 0; i < declarations.length; i++ ) { if ( declarations[ i ].property === rule ) { return declarations[ i ].value; } } return ''; }, /** * Update the main editor with the value of the parsed CSS */ updateMainEditor: function ( compress ) { //TODO: add back compress option to remove/merge duplicated CSS selectors. this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) ); }, /** * Show the properties editor */ show: function () { this.editorExpandedBefore = this.editor.isExpanded(); this.editor.setExpand( true ); this.$el.show().animate( { 'left': 0 }, 'fast' ); }, /** * Hide the properties editor */ hide: function () { this.editor.setExpand( this.editorExpandedBefore ); this.$el.animate( { 'left': -338 }, 'fast', function () { $( this ).hide(); } ); // Update the main editor with compressed CSS when we close the properties editor this.updateMainEditor( true ); }, save: function () { socss.save( this ); }, /** * @returns boolean */ isVisible: function () { return this.$el.is( ':visible' ); }, /** * Loads a single CSS selector and associated properties into the model * @param css */ loadCSS: function ( css, activeSelector ) { this.css = css; // Load the CSS this.parsed = this.parser.parse( css, { silent: true } ); var rules = this.parsed.stylesheet.rules; // Add the dropdown menu items var dropdown = this.$( '.toolbar select' ).empty(); for ( var i = 0; i < rules.length; i++ ) { var rule = rules[ i ]; // Exclude @import statements if ( !_.contains( [ 'rule', 'media' ], rule.type ) ) { continue; } if ( rule.type === 'media' ) { for ( var j = 0; j < rule.rules.length; j++ ) { var mediaRule = '@media ' + rule.media; var subRule = rule.rules[ j ]; if ( subRule.type != 'rule' ) { continue; } dropdown.append( $( '<option>' ) .html( mediaRule + ': ' + subRule.selectors.join( ',' ) ) .attr( 'val', mediaRule + ': ' + subRule.selectors.join( ',' ) ) .data( 'selector', subRule ) ); } } else { dropdown.append( $( '<option>' ) .html( rule.selectors.join( ',' ) ) .attr( 'val', rule.selectors.join( ',' ) ) .data( 'selector', rule ) ); } } if ( typeof activeSelector === 'undefined' ) { activeSelector = dropdown.find( 'option' ).eq( 0 ).attr( 'val' ); } if ( !_.isEmpty( activeSelector ) ) { dropdown.val( activeSelector ).trigger( 'change' ); } }, /** * Set the selector that we're currently dealing with * @param selector */ setActiveSelector: function ( selector ) { this.activeSelector = selector; for ( var i = 0; i < this.propertyControllers.length; i++ ) { this.propertyControllers[ i ].refreshFromRule(); } }, /** * Add or select a selector. * * @param selector */ addSelector: function ( selector ) { // Check if this selector already exists var dropdown = this.$( '.toolbar select' ); dropdown.val( selector ); if ( dropdown.val() === selector ) { // Trigger a change event to load the existing selector dropdown.trigger( 'change' ); } else { // The selector doesn't exist, so add it to the CSS, then reload this.editor.addEmptySelector( selector ); this.loadCSS( this.editor.codeMirror.getValue().trim(), selector ); } dropdown.addClass( 'highlighted' ); setTimeout( function () { dropdown.removeClass( 'highlighted' ); }, 2000 ); } } ); // The basic property controller socss.view.propertyController = Backbone.View.extend( { template: _.template( '<input type="text" value="" class="socss-property-controller-input"/>' ), activeRule: null, args: null, propertiesView: null, events: { 'change .socss-property-controller-input': 'onChange', 'keyup input.socss-property-controller-input': 'onChange', }, initialize: function ( args ) { this.args = args.args; this.propertiesView = args.propertiesView; // If sub-views items define their own events hash with the same keys as above they will override those on // the above events hash. this.events = _.extend( socss.view.propertyController.prototype.events, this.events ); this.delegateEvents( this.events ); // By default, update the active rule whenever things change this.on( 'set_value', this.updateRule, this ); this.on( 'change', this.updateRule, this ); }, /** * Render the property field controller */ render: function () { this.$el.append( $( this.template( {} ) ) ); this.field = this.$( 'input.socss-property-controller-input' ); }, onChange: function () { this.trigger( 'change', this.field.val() ); }, /** * Update the value of an active rule */ updateRule: function () { this.propertiesView.setRuleValue( this.args.property, this.getValue() ); }, /** * This is called when the selector changes */ refreshFromRule: function () { var value = this.propertiesView.getRuleValue( this.args.property ); this.setValue( value, { silent: true } ); }, /** * Get the current value * @return string */ getValue: function () { return this.field.val(); }, /** * Set the current value * @param socss.view.properties val */ setValue: function ( val, options ) { options = _.extend( { silent: false }, options ); this.field.val( val ); if ( !options.silent ) { this.trigger( 'set_value', val ); } }, /** * Reset the current value */ reset: function ( options ) { options = _.extend( { silent: false }, options ); this.setValue( '', options ); } } ); // All the value controllers socss.view.properties.controllers = {}; // The color controller socss.view.properties.controllers.color = socss.view.propertyController.extend( { render: function () { socss.view.propertyController.prototype.render.apply( this, arguments ); // Set this up as a color picker this.field.minicolors( {} ); }, onChange: function () { this.trigger( 'change', this.field.minicolors( 'value' ) ); }, getValue: function () { return this.field.minicolors( 'value' ).trim(); }, setValue: function ( val, options ) { options = _.extend( { silent: false }, options ); this.field.minicolors( 'value', val ); if ( !options.silent ) { this.trigger( 'set_value', val ); } } } ); // The dropdown select box controller socss.view.properties.controllers.select = socss.view.propertyController.extend( { template: _.template( '<select class="socss-property-controller-input"></select>' ), events: { 'click .select-tab': 'onSelect', }, render: function () { this.$el.append( $( this.template( {} ) ) ); this.field = this.$( 'select' ); // Add the unchanged option this.field.append( $( '<option value=""></option>' ).html( '' ) ); // Add all the options to the dropdown for ( var k in this.args.options ) { this.field.append( $( '<option></option>' ).attr( 'value', k ).html( this.args.options[ k ] ) ); } if ( typeof this.args.option_icons !== 'undefined' ) { this.setupVisualSelect(); } }, setupVisualSelect: function () { this.field.hide(); var $tc = $( '<div class="select-tabs"></div>' ).appendTo( this.$el ); // Add the none value $( '<div class="select-tab" data-value=""><span class="so-css-icon so-css-icon-circle"></span></div>' ).appendTo( $tc ); // Now add one for each of the option icons for ( var k in this.args.option_icons ) { $( '<div class="select-tab"></div>' ) .appendTo( $tc ) .append( $( '<span class="so-css-icon"></span>' ) .addClass( 'so-css-icon-' + this.args.option_icons[ k ] ) ) .attr( 'data-value', k ) ; } $tc.find( '.select-tab' ).css( 'width', 100 / ( $tc.find( '>div' ).length ) + "%" ); }, onSelect: function ( event ) { this.$( '.select-tab' ).removeClass( 'active' ); var $t = $( event.currentTarget ); $t.addClass( 'active' ); this.field.val( $t.data( 'value' ) ).trigger( 'change' ); }, /** * Set the current value * @param socss.view.properties val */ setValue: function ( val, options ) { options = _.extend( { silent: false }, options ); this.field.val( val ); this.$( '.select-tabs .select-tab' ).removeClass( 'active' ).filter( '[data-value="' + val + '"]' ).addClass( 'active' ); if ( !options.silent ) { this.trigger( 'set_value', val ); } } } ); // A field that lets a user upload an image socss.view.properties.controllers.image = socss.view.propertyController.extend( { template: _.template( '<input type="text" value="" /> <span class="select socss-button"><span class="so-css-icon so-css-icon-upload"></span></span>' ), events: { 'click .select': 'openMedia', }, render: function () { this.media = wp.media( { // Set the title of the modal. title: socssOptions.loc.select_image, // Tell the modal to show only images. library: { type: 'image' }, // Customize the submit button. button: { // Set the text of the button. text: socssOptions.loc.select, // Tell the button not to close the modal, since we're // going to refresh the page when the image is selected. close: false } } ); this.$el.append( $( this.template( { select: socssOptions.loc.select } ) ) ); this.field = this.$el.find( 'input' ); this.media.on( 'select', function () { // Grab the selected attachment. var attachment = this.media.state().get( 'selection' ).first().attributes; var val = this.args.value.replace( '{{url}}', attachment.url ); // Change the field value and trigger a change event this.field.val( val ).trigger( 'change' ); this.trigger( 'set_value', val ); // Close the image selector this.media.close(); }.bind( this ) ); }, openMedia: function () { this.media.open(); }, } ); // A simple measurement field socss.view.properties.controllers.measurement = socss.view.propertyController.extend( { wrapperClass: 'socss-field-measurement', events: { 'click .toggle-dropdown': 'toggleUnitDropdown', 'click .dropdown li': 'onSelectUnit', 'keydown .socss-field-input': 'onInputKeyPress', 'keyup .socss-field-input': 'onInputKeyUp', }, render: function () { socss.view.propertyController.prototype.render.apply( this, arguments ); this.setupMeasurementField(); }, setValue: function ( val, options ) { options = _.extend( { silent: false }, options ); this.field.val( val ).trigger( 'measurement_refresh' ); if ( !options.silent ) { this.trigger( 'set_value', val ); } }, units: [ 'px', '%', 'em', 'cm', 'mm', 'in', 'pt', 'pc', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax' ], parseUnits: function ( value ) { var escapeRegExp = function ( str ) { return str.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" ); }; var regexUnits = this.units.map( escapeRegExp ); var regex = new RegExp( '([0-9\\.\\-]+)(' + regexUnits.join( '|' ) + ')?', 'i' ); var result = regex.exec( value ); if ( result === null ) { return { value: '', unit: '' }; } else { return { value: result[ 1 ], unit: result[ 2 ] === undefined ? '' : result[ 2 ] }; } }, setupMeasurementField: function () { var defaultUnit = 'px'; this.field.hide(); this.$el.addClass( this.wrapperClass ).data( 'unit', defaultUnit ); // Create the fake input field var $fi = $( '<input type="text" class="socss-field-input"/>' ).appendTo( this.$el ); $( '<span class="toggle-dropdown dashicons dashicons-arrow-down"></span>' ).appendTo( this.$el ); var $dd = $( '<ul class="dropdown"></ul>' ).appendTo( this.$el ); var $u = $( '<span class="units"></span>' ).html( defaultUnit ).appendTo( this.$el ); for ( var i = 0; i < this.units.length; i++ ) { var $o = $( '<li></li>' ).html( this.units[ i ] ).data( 'unit', this.units[ i ] ); if ( this.units[ i ] === defaultUnit ) { $o.addClass( 'active' ); } $dd.append( $o ); } this.field.on( 'measurement_refresh', function () { var value = this.parseUnits( this.field.val() ); $fi.val( value.value ); var unit = value.unit === '' ? defaultUnit : value.unit; this.$el.data( 'unit', unit ); $u.html( unit ); var $pl = $( '<span class="socss-hidden-placeholder"></span>' ) .css( { 'font-size': '14px' } ) .html( value.value ) .appendTo( 'body' ); var width = $pl.width(); width = Math.min( width, 63 ); $pl.remove(); $u.css( 'left', width + 12 ); }.bind( this ) ); // Now add the increment/decrement buttons var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el ); var $dec = $( '<div class="dec-button socss-button"><span class="so-css-icon so-css-icon-minus"></span></div>' ).appendTo( $diw ); var $inc = $( '<div class="inc-button socss-button"><span class="so-css-icon so-css-icon-plus"></span></div>' ).appendTo( $diw ); this.setupStepButton( $dec ); this.setupStepButton( $inc ); }, updateValue: function () { var $fi = this.$( '.socss-field-input' ); var value = this.parseUnits( $fi.val() ); if ( value.unit !== '' && value.unit !== this.$el.data( 'unit' ) ) { $fi.val( value.value ); this.setUnit( value.unit ); } if ( value.value === '' ) { this.field.val( '' ); } else { this.field.val( value.value + this.$el.data( 'unit' ) ); } this.field.trigger( 'change' ); }, setUnit: function ( unit ) { this.$( '.units' ).html( unit ); this.$el.data( 'unit', unit ); this.$( '.socss-field-input' ).trigger( 'keydown' ); }, toggleUnitDropdown: function () { this.$( '.dropdown' ).toggle(); }, onSelectUnit: function ( event ) { this.toggleUnitDropdown(); this.setUnit( $( event.currentTarget ).data( 'unit' ) ); this.updateValue(); }, onInputKeyUp: function( event ) { this.onInputKeyPress( event ); this.updateValue(); }, onInputKeyPress: function ( event ) { var $fi = this.$( '.socss-field-input' ); var char = ''; if ( event.type === 'keydown' ) { if ( event.keyCode >= 48 && event.keyCode <= 57 ) { char = String.fromCharCode( event.keyCode ); } else if ( event.keyCode === 189 ) { char = '-'; } else if ( event.keyCode === 190 ) { char = '.'; } } var $pl = $( '<span class="socss-hidden-placeholder"></span>' ) .css( { 'font-size': '14px' } ) .html( $fi.val() + char ) .appendTo( 'body' ); var width = $pl.width(); width = Math.min( width, 63 ); $pl.remove(); this.$( '.units' ).css( 'left', width + 12 ); }, stepValue: function ( direction ) { var value = Number.parseInt( this.parseUnits( this.field.val() ).value ); if ( Number.isNaN( value ) ) { value = 0; } var newVal = value + direction; this.$( '.socss-field-input' ).val( newVal ); this.updateValue(); this.field.trigger( 'measurement_refresh' ); }, setupStepButton: function ( $button ) { var direction = $button.is( '.dec-button' ) ? -1 : 1; var intervalId; var timeoutId; $button.on( 'mousedown', function() { this.stepValue( direction ); timeoutId = setTimeout( function () { intervalId = setInterval( function () { this.stepValue( direction ); }.bind( this ), 50 ); }.bind( this ), 500 ); }.bind( this ) ).on( 'mouseup mouseout', function () { if ( timeoutId ) { clearTimeout( timeoutId ); timeoutId = null; } if ( intervalId ) { clearInterval( intervalId ); intervalId = null; } } ); }, } ); // A simple measurement field socss.view.properties.controllers.number = socss.view.propertyController.extend( { initialize: function ( args ) { socss.view.propertyController.prototype.initialize.apply( this, arguments ); this.args = _.extend( { change: null, default: 0, increment: 1, decrement: -1, max: null, min: null }, args.args ); }, render: function () { socss.view.propertyController.prototype.render.apply( this, arguments ); this.setupNumberField(); }, setupNumberField: function () { this.$el.addClass( 'socss-field-number' ); // Now add the increment/decrement buttons var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el ); var $dec = $( '<div class="dec-button socss-button"><span class="so-css-icon so-css-icon-minus"></span></div>' ).appendTo( $diw ); var $inc = $( '<div class="inc-button socss-button"><span class="so-css-icon so-css-icon-plus"></span></div>' ).appendTo( $diw ); this.setupStepButton( $dec ); this.setupStepButton( $inc ); return this; }, stepValue: function ( direction ) { var value = Number.parseFloat( this.field.val() ); if ( Number.isNaN( value ) ) { value = this.args.default; } var newVal = value + direction; newVal = Math.round( newVal * 100 ) / 100; if ( this.args.max !== null ) { newVal = Math.min( this.args.max, newVal ); } if ( this.args.min !== null ) { newVal = Math.max( this.args.min, newVal ); } this.field.val( newVal ); this.field.trigger( 'change' ); }, setupStepButton: function ( $button ) { var direction = $button.is( '.dec-button' ) ? this.args.decrement : this.args.increment; var intervalId; var timeoutId; $button.on( 'mousedown', function() { this.stepValue( direction ); timeoutId = setTimeout( function () { intervalId = setInterval( function () { this.stepValue( direction ); }.bind( this ), 50 ); }.bind( this ), 500 ); }.bind( this ) ).on( 'mouseup mouseout', function () { if ( timeoutId ) { clearTimeout( timeoutId ); timeoutId = null; } if ( intervalId ) { clearInterval( intervalId ); intervalId = null; } } ); }, } ); socss.view.properties.controllers.sides = socss.view.propertyController.extend( { template: _.template( $( '#template-sides-field' ).html().trim() ), controllers: [], events: { 'click .select-tab': 'onTabClick', }, render: function () { socss.view.propertyController.prototype.render.apply( this, arguments ); if ( !this.args.hasAll ) { this.$( '.select-tab' ).eq( 0 ).remove(); this.$( '.select-tab' ).css( 'width', '25%' ); } if ( ! this.args.isRadius ) { this.$( '.select-tabs[data-type="radius"]' ).remove(); } else { this.$( '.select-tabs[data-type="box"]' ).remove(); } this.$( '.select-tab' ).each( function ( index, element ) { var dir = $( element ).data( 'direction' ); var container = $( '<li class="side">' ) .appendTo( this.$( '.sides' ) ) .hide(); for ( var i = 0; i < this.args.controllers.length; i++ ) { var controllerArgs = this.args.controllers[ i ]; if ( typeof socss.view.properties.controllers[ controllerArgs.type ] ) { // Create the measurement view var property = ''; if ( dir === 'all' ) { property = controllerArgs.args.propertyAll; } else { property = controllerArgs.args.property.replace( '{dir}', dir ); } var theseControllerArgs = _.extend( {}, controllerArgs.args, { property: property } ); var controller = new socss.view.properties.controllers[ controllerArgs.type ]( { el: $( '<div>' ).appendTo( container ), propertiesView: this.propertiesView, args: theseControllerArgs } ); // Setup and render the measurement controller and register it with the properties view controller.render(); this.propertiesView.propertyControllers.push( controller ); } } }.bind( this ) ); // Select the first tab by default this.$( '.select-tab' ).eq( 0 ).click(); }, onTabClick: function ( event ) { var $tabs = this.$( '.select-tab' ); $tabs.removeClass( 'active' ); var $tab = $( event.currentTarget ); $tab.addClass( 'active' ); var $sides = this.$( '.sides .side' ) $sides.hide(); $sides.eq( $tabs.index( $tab ) ).show(); }, } ); // This is a placeholder for the full font_select in SiteOrigin Premium socss.view.properties.controllers.font_select = socss.view.propertyController.extend( { template: _.template( $('#template-webfont-teaser').html().trim() ) }); } )( jQuery, _, socssOptions ); // Setup the main editor jQuery( function ( $ ) { var socss = window.socss; var editorModel = new socss.model.CSSEditorModel( { customCssPosts: socssOptions.customCssPosts, } ); // Setup the editor var editor = new socss.view.editor( { el: $( '#so-custom-css-form' ).get( 0 ), model: editorModel, openVisualEditor: socssOptions.openVisualEditor, } ); // editor.render(); editor.setSnippets( socssOptions.snippets ); // This is for hiding the getting started video $( '#so-custom-css-getting-started a.hide' ).on( 'click', function( e ) { e.preventDefault(); $( '#so-custom-css-getting-started' ).slideUp(); $.get( $( this ).attr( 'href' ) ); } ); window.socss.mainEditor = editor; $( socss ).trigger( 'initialized' ); $( '.button-primary[name="siteorigin_custom_css_save"]' ).on( 'click', function() { $( '#so-custom-css-form' ).trigger( 'submit' ); } ); $( '.installer-link' ).on( 'click', function( e ) { e.preventDefault(); $( this ).hide(); $( '.installer-container' ).slideDown( 'fast' ); } ); $( '.installer_status' ).on( 'change', function() { var $$ = $( this ); $$.prop( 'disabled', true ); jQuery.post( ajaxurl, { action: 'so_installer_status', nonce: $$.data( 'nonce' ), status: $$.is( ':checked' ) }, function() { $$.prop( 'disabled', false ); } ); } ); } );