PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.5.9
SiteOrigin CSS v1.5.9
1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0
so-css / js / editor.js
so-css / js Last commit date
URI.js 9 years ago URI.min.js 6 years ago css.js 4 years ago css.min.js 4 years ago csslint.js 9 years ago csslint.min.js 6 years ago editor.js 2 years ago editor.min.js 2 years ago inspector.js 4 years ago inspector.min.js 4 years ago jquery.sizes.js 11 years ago jquery.sizes.min.js 6 years ago specificity.js 11 years ago specificity.min.js 6 years ago
editor.js
1935 lines
1 /* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */
2
3 ( function ( $, _, socssOptions ) {
4
5 var socss = {
6 model: {},
7 collection: {},
8 view: {},
9 fn: {}
10 };
11
12 window.socss = socss;
13
14 socss.model.CustomCssModel = Backbone.Model.extend( {
15 defaults: {
16 postId: null,
17 postTitle: null,
18 css: null,
19 },
20
21 urlRoot: socssOptions.postCssUrlRoot,
22
23 url: function () {
24 return this.urlRoot + '&postId=' + this.get( 'postId' );
25 }
26 } );
27
28 socss.model.CustomCssCollection = Backbone.Collection.extend( {
29 model: socss.model.CustomCssModel,
30
31 modelId: function( attrs ) {
32 return attrs.postId;
33 },
34 } );
35
36 socss.model.CSSEditorModel = Backbone.Model.extend( {
37 defaults: {
38 customCssPosts: null,
39 }
40 } );
41
42 /**
43 * The toolbar view
44 */
45 socss.view.toolbar = Backbone.View.extend( {
46
47 button: _.template( '<li><a href="#<%= action %>" class="toolbar-button socss-button"><%= text %></a></li>' ),
48
49 events: {
50 'click .socss-button:not(.save)': 'triggerEvent',
51 },
52
53 triggerEvent: function ( event ) {
54 event.preventDefault();
55 var $target = $( event.currentTarget );
56 $target.trigger( 'blur' );
57 var value = $target.attr( 'href' ).replace( '#', '' );
58 this.$el.trigger( 'click_' + value );
59 },
60
61 addButton: function ( text, action ) {
62 var button = $( this.button( { text: text, action: action } ) )
63 .appendTo( this.$( '.toolbar-function-buttons .toolbar-buttons' ) );
64
65 return button;
66 },
67 } );
68
69 /**
70 * The editor view, which handles codemirror stuff
71 *
72 * model: socss.model.CSSEditorModel
73 *
74 */
75 socss.view.editor = Backbone.View.extend( {
76
77 codeMirror: null,
78 snippets: null,
79 toolbar: null,
80 visualProperties: null,
81
82 inspector: null,
83
84 cssSelectors: [],
85
86 initValue: null,
87
88 events: {
89 'click_expand .custom-css-toolbar': 'toggleExpand',
90 'click_visual .custom-css-toolbar': 'showVisualEditor',
91 'click .socss-button.save': 'save',
92 'submit': 'onSubmit',
93 },
94
95 initialize: function ( options ) {
96
97 this.listenTo( this.model, 'change:selectedPost', this.getSelectedPostCss );
98
99 this.getSelectedPostCss().then( function () {
100
101 if ( options.openVisualEditor ) {
102 this.showVisualEditor();
103 }
104 }.bind( this ) );
105
106 },
107
108 save: function () {
109 socss.save( this );
110 },
111
112 getSelectedPostCss: function () {
113 var selectedPost = this.model.get( 'selectedPost' );
114 var promise;
115 if ( selectedPost && ! selectedPost.has( 'css' ) ) {
116 promise = selectedPost.fetch();
117 } else {
118 promise = new $.Deferred().resolve();
119 }
120
121 return promise.then( this.render.bind( this ) );
122 },
123
124 render: function () {
125
126 var selectedPost = this.model.get( 'selectedPost' );
127
128 if ( selectedPost && !selectedPost.has( 'css' ) ) {
129 return this;
130 }
131
132 if ( !this.codeMirror ) {
133 this.setupEditor();
134 }
135
136 if ( ! this.toolbar ) {
137 this.toolbar = new socss.view.toolbar( {
138 el: this.$( '.custom-css-toolbar' ),
139 model: this.model,
140 } );
141 this.toolbar.render();
142 }
143
144 if ( !this.visualProperties ) {
145 this.visualProperties = new socss.view.properties( {
146 editor: this,
147 el: $( '#so-custom-css-properties' )
148 } );
149 this.visualProperties.render();
150 }
151
152 if ( !this.preview ) {
153 this.preview = new socss.view.preview( {
154 editor: this,
155 model: this.model,
156 el: this.$( '.custom-css-preview' ),
157 initURL: socssOptions.homeURL,
158 } );
159 this.preview.render();
160 }
161
162 if ( selectedPost ) {
163 this.codeMirror.setValue( selectedPost.get( 'css' ) );
164 this.codeMirror.clearHistory();
165 }
166
167 return this;
168 },
169
170 /**
171 * Do the initial setup of the CodeMirror editor
172 */
173 setupEditor: function () {
174 // Setup the Codemirror instance
175 var $textArea = this.$( 'textarea.css-editor' );
176 this.initValue = $textArea.val();
177 // Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste
178 // options in context menu.
179 var newlineMatches = this.initValue.match( /\n/gm );
180 var lineCount = newlineMatches ? newlineMatches.length + 1 : 1;
181 var paddedValue = this.initValue;
182 $textArea.val( paddedValue );
183
184 var codeMirrorSettings = {
185 tabSize: 2,
186 lineNumbers: true,
187 mode: 'css',
188 theme: $textArea.data( 'theme' ),
189 inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc.
190 gutters: [
191 "CodeMirror-lint-markers"
192 ],
193 lint: true,
194 search: true,
195 dialog: true,
196 annotateScrollbar: true,
197 extraKeys: {
198 'Ctrl-F': 'findPersistent',
199 'Alt-G': 'jumpToLine',
200 },
201 }
202
203 if ( typeof wp.codeEditor != "undefined" ) {
204 codeMirrorSettings = _.extend(
205 wp.codeEditor.defaultSettings.codemirror,
206 codeMirrorSettings
207 );
208 this.codeMirror = wp.codeEditor.initialize( $textArea.get( 0 ), codeMirrorSettings ).codemirror;
209 } else {
210 this.registerCodeMirrorAutocomplete();
211 this.codeMirror = CodeMirror.fromTextArea( $textArea.get( 0 ), codeMirrorSettings );
212 this.setupCodeMirrorExtensions();
213 }
214
215 var editor = this.codeMirror;
216 $( '#so_css_editor_theme' ).on( 'change', function() {
217 if ( $( this ).val() == 1 ) {
218 editor.setOption( 'theme', 'neat' );
219 } else {
220 editor.setOption( 'theme', 'ambiance' );
221 }
222 } );
223
224 this.codeMirror.on( 'change', function ( cm, change ) {
225 var selectedPost = this.model.get( 'selectedPost' );
226 if ( selectedPost && selectedPost.get( 'css' ) !== cm.getValue().trim() ) {
227 selectedPost.set( 'css', cm.getValue().trim() );
228 }
229 }.bind( this ) );
230
231 // Make sure the user doesn't leave without saving
232 $( window ).on( 'beforeunload', function () {
233 var editorValue = this.codeMirror.getValue().trim();
234 if ( editorValue !== this.initValue ) {
235 return socssOptions.loc.leave;
236 }
237 }.bind( this ) );
238
239
240 // Set the container to visible overflow once the editor is setup
241 this.$el.find( '.custom-css-container' ).css( 'overflow', 'visible' );
242 this.scaleEditor();
243
244 // Scale the editor whenever the window is resized
245 $( window ).on( 'resize', function () {
246 this.scaleEditor();
247 }.bind( this ) );
248 },
249
250 onSubmit: function () {
251 this.initValue = this.codeMirror.getValue().trim();
252 },
253
254 /**
255 * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder.
256 */
257 registerCodeMirrorAutocomplete: function () {
258 var pseudoClasses = {
259 link: 1, visited: 1, active: 1, hover: 1, focus: 1,
260 "first-letter": 1, "first-line": 1, "first-child": 1,
261 before: 1, after: 1, lang: 1
262 };
263
264 CodeMirror.registerHelper( "hint", "css", function ( cm ) {
265 var cur = cm.getCursor(), token = cm.getTokenAt( cur );
266 var inner = CodeMirror.innerMode( cm.getMode(), token.state );
267 if ( inner.mode.name !== "css" ) {
268 return;
269 }
270
271 if ( token.type === "keyword" && "!important".indexOf( token.string ) === 0 ) {
272 return {
273 list: [ "!important" ], from: CodeMirror.Pos( cur.line, token.start ),
274 to: CodeMirror.Pos( cur.line, token.end )
275 };
276 }
277
278 var start = token.start, end = cur.ch, word = token.string.slice( 0, end - start );
279 if ( /[^\w$_-]/.test( word ) ) {
280 word = "";
281 start = end = cur.ch;
282 }
283
284 var spec = CodeMirror.resolveMode( "text/css" );
285
286 var result = [];
287
288 function add( keywords ) {
289 for ( var name in keywords ) {
290 if ( !word || name.lastIndexOf( word, 0 ) === 0 ) {
291 result.push( name );
292 }
293 }
294 }
295
296 var st = inner.state.state;
297
298 if ( st === 'top' ) {
299 // We're going to autocomplete the selector using our own set of rules
300 var line = cm.getLine( cur.line ).trim();
301
302 var selectors = this.cssSelectors;
303 for ( var i = 0; i < selectors.length; i++ ) {
304 if ( selectors[ i ].selector.indexOf( line ) !== -1 ) {
305 result.push( selectors[ i ].selector );
306 }
307 }
308
309 if ( result.length ) {
310 return {
311 list: result,
312 from: CodeMirror.Pos( cur.line, 0 ),
313 to: CodeMirror.Pos( cur.line, end )
314 };
315 }
316 }
317 else {
318
319 if ( st === "pseudo" || token.type === "variable-3" ) {
320 add( pseudoClasses );
321 }
322 else if ( st === "block" || st === "maybeprop" ) {
323 add( spec.propertyKeywords );
324 }
325 else if ( st === "prop" || st === "parens" || st === "at" || st === "params" ) {
326 add( spec.valueKeywords );
327 add( spec.colorKeywords );
328 }
329 else if ( st === "media" || st === "media_parens" ) {
330 add( spec.mediaTypes );
331 add( spec.mediaFeatures );
332 }
333
334 if ( result.length ) {
335 return {
336 list: result,
337 from: CodeMirror.Pos( cur.line, start ),
338 to: CodeMirror.Pos( cur.line, end )
339 };
340 }
341
342 }
343
344 }.bind( this ) );
345 },
346
347 setupCodeMirrorExtensions: function () {
348
349 this.codeMirror.on( 'cursorActivity', function ( cm ) {
350 var cur = cm.getCursor(), token = cm.getTokenAt( cur );
351 var inner = CodeMirror.innerMode( cm.getMode(), token.state );
352
353 // If we have a qualifier selected, then highlight that in the preview
354 if ( token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin' ) {
355 var line = cm.getLine( cur.line );
356 var selector = line.substring( 0, token.end );
357
358 this.preview.highlight( selector );
359 }
360 else {
361 this.preview.clearHighlight();
362 }
363 }.bind( this ) );
364
365 if ( typeof CodeMirror.showHint == 'function' ) {
366 // This sets up automatic autocompletion at all times
367 this.codeMirror.on( 'keyup', function ( cm, e ) {
368 if (
369 ( e.keyCode >= 65 && e.keyCode <= 90 ) ||
370 ( e.keyCode === 189 && !e.shiftKey ) ||
371 ( e.keyCode === 190 && !e.shiftKey ) ||
372 ( e.keyCode === 51 && e.shiftKey ) ||
373 ( e.keyCode === 189 && e.shiftKey )
374 ) {
375 cm.showHint( {
376 completeSingle: false
377 } );
378 }
379 } );
380 }
381 },
382
383 /**
384 * Scale the size of the editor depending on whether it's expanded or not
385 */
386 scaleEditor: function () {
387 var windowHeight = $( window ).outerHeight();
388 var areaHeight;
389 if ( this.$el.hasClass( 'expanded' ) ) {
390 // If we're in the expanded view, then resize the editor
391 this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', '' );
392 areaHeight = windowHeight - this.$( '.custom-css-toolbar' ).outerHeight();
393 this.codeMirror.setSize( '100%', areaHeight );
394 this.$el.find( '.CodeMirror-scroll' ).css( 'height', '100%' );
395 }
396 else {
397 // Attempt to calculate approximate space available for editor when not expanded.
398 var $form = $( '#so-custom-css-form' );
399 var otherEltsHeight = $( '#wpadminbar' ).outerHeight( true ) +
400 $( '#siteorigin-custom-css' ).find( '> h2' ).outerHeight( true ) +
401 $form.find( '> .custom-css-toolbar' ).outerHeight( true ) +
402 $form.find( '> .so-css-footer' ).outerHeight( true ) +
403 parseFloat( $( '#wpbody-content' ).css( 'padding-bottom' ) );
404
405 areaHeight = windowHeight - otherEltsHeight;
406 // The container has a min-height of 300px so we need to ensure the areaHeight is at least that large.
407 if ( areaHeight < 300 ) {
408 areaHeight = 300;
409 }
410
411 this.codeMirror.setSize( '100%', 'auto' );
412 this.$el.find( '.CodeMirror-scroll' ).css( 'height', areaHeight + 'px' );
413 }
414 this.$el.find( '.CodeMirror-code' ).css( 'height', areaHeight + 'px' );
415 },
416
417 /**
418 * Check if the editor is in expanded mode
419 * @returns bool
420 */
421 isExpanded: function () {
422 return this.$el.hasClass( 'expanded' );
423 },
424
425 /**
426 * Toggle if this is expanded or not
427 */
428 toggleExpand: function ( e ) {
429 $( '.editor-expand' ).attr( 'title', $( '.so-css-icon-' + ( this.isExpanded() ? 'expand' : 'compress ') ).attr( 'title' ) );
430 this.$el.toggleClass( 'expanded' );
431 this.scaleEditor();
432 },
433
434 /**
435 * Set the expanded state of the editor
436 * @param expanded
437 */
438 setExpand: function ( expanded ) {
439 if ( expanded ) {
440 this.$el.addClass( 'expanded' );
441 }
442 else {
443 this.$el.removeClass( 'expanded' );
444 }
445 this.scaleEditor();
446 },
447
448 /**
449 * Show the visual editor view.
450 */
451 showVisualEditor: function () {
452 this.visualProperties.loadCSS( this.codeMirror.getValue().trim() );
453 this.visualProperties.show();
454 },
455
456 /**
457 * Set the snippets available to this editor
458 */
459 setSnippets: function ( snippets ) {
460 if ( !_.isEmpty( snippets ) ) {
461
462 this.snippets = new socss.view.snippets( {
463 snippets: snippets
464 } );
465 this.snippets.editor = this;
466
467 this.snippets.render();
468 this.toolbar.addButton( 'Snippets', 'snippets' );
469 this.toolbar.on( 'click_snippets', function () {
470 this.snippets.show();
471 }.bind( this ) );
472 }
473 },
474
475 /**
476 * Add some CSS to the editor.
477 * @param css
478 */
479 addCode: function ( css ) {
480 var editor = this.codeMirror;
481
482 var before_css = '';
483 if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
484 before_css = "";
485 }
486 else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
487 before_css = "\n";
488 }
489 else {
490 before_css = "\n\n";
491 }
492
493 // Now insert the code in the editor
494 editor.doc.setCursor(
495 editor.doc.lastLine(),
496 editor.doc.getLine( editor.doc.lastLine() ).length
497 );
498 editor.doc.replaceSelection( before_css + css );
499 },
500
501 addEmptySelector: function ( selector ) {
502 this.addCode( selector + " {\n \n}" );
503 },
504
505 /**
506 * Sets the inspector view that's being used by the editor
507 */
508 setInspector: function ( inspector ) {
509 this.inspector = inspector;
510 this.cssSelectors = inspector.pageSelectors;
511
512 // A selector is clicked in the inspector
513 inspector.on( 'click_selector', function ( selector ) {
514 if ( this.visualProperties.isVisible() ) {
515 this.visualProperties.addSelector( selector );
516 }
517 else {
518 this.addEmptySelector( selector );
519 }
520 }.bind( this ) );
521
522 // A property is clicked in the inspector
523 inspector.on( 'click_property', function ( property ) {
524 if ( !this.visualProperties.isVisible() ) {
525 this.codeMirror.replaceSelection( property + ";\n " );
526 }
527 }.bind( this ) );
528
529 inspector.on( 'set_active_element', function ( el, selectors ) {
530 if ( this.visualProperties.isVisible() && selectors.length ) {
531 this.visualProperties.addSelector( selectors[ 0 ].selector );
532 }
533 }.bind( this ) );
534 }
535
536 } );
537
538 /**
539 * The preview.
540 */
541 socss.view.preview = Backbone.View.extend( {
542
543 template: _.template( $( '#template-preview-window' ).html() ),
544 editor: null,
545 originalUri: null,
546 currentUri: null,
547
548 events: {
549 'mouseleave #preview-iframe': 'clearHighlight',
550 'keydown #preview-navigator input[type="text"]': 'reloadPreview',
551 },
552
553 initialize: function ( attr ) {
554 this.editor = attr.editor;
555
556 this.listenTo( this.model, 'change:selectedPost', this.render.bind( this ) );
557
558 this.originalUri = new URI( attr.initURL );
559 this.currentUri = new URI( attr.initURL );
560
561 this.editor.codeMirror.on( 'change', function ( cm, c ) {
562 this.updatePreviewCss();
563 }.bind( this ) );
564 },
565
566 render: function () {
567
568 var selectedPost = this.model.get( 'selectedPost' );
569
570 if ( selectedPost && !selectedPost.has( 'postUrl' ) ) {
571 selectedPost.fetch().then( this.render.bind( this ) );
572 return this;
573 }
574
575 this.$el.html( this.template() );
576
577 if ( selectedPost ) {
578 this.currentUri = new URI( selectedPost.get( 'postUrl' ) );
579 }
580
581 this.currentUri.removeQuery( 'so_css_preview', 1 );
582 this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
583 this.currentUri.addQuery( 'so_css_preview', 1 );
584
585 this.$( '#preview-iframe' )
586 .attr( 'src', this.currentUri.toString() )
587 // 'load' event doesn't bubble so can't be used in the events hash
588 .on( 'load', this.initPreview.bind( this ) );
589 },
590
591 initPreview: function () {
592 var $$ = this.$( '#preview-iframe' );
593
594 // Update the current URI with the iframe URI
595 this.currentUri = new URI( $$.contents().get( 0 ).location.href );
596 this.currentUri.removeQuery( 'so_css_preview' );
597 this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
598 this.currentUri.addQuery( 'so_css_preview', 1 );
599
600 var wcCheck = $$.contents().find( '.single-product' ).length;
601 $$.contents().find( 'a' ).each( function () {
602 var link = $( this );
603 var href = link.attr( 'href' );
604 if ( href === undefined || ( wcCheck && link.parents( '.wc-tabs' ).length ) ) {
605 return true;
606 }
607
608 var firstSeperator = ( href.indexOf( '?' ) === -1 ? '?' : '&' );
609 link.attr( 'href', href + firstSeperator + 'so_css_preview=1' );
610 } );
611
612 this.updatePreviewCss();
613 },
614
615 reloadPreview: function ( e ) {
616 var $$ = this.$( '#preview-navigator input[type="text"]' );
617
618 if ( e.keyCode === 13 ) {
619 e.preventDefault();
620
621 var newUri = new URI( $$.val() );
622
623 // Validate the URI
624 if (
625 this.originalUri.host() !== newUri.host() ||
626 this.originalUri.protocol() !== newUri.protocol()
627 ) {
628 $$.trigger( 'blur' );
629 alert( $$.data( 'invalid-uri' ) );
630 $$.trigger( 'focus' );
631 }
632 else {
633 newUri.addQuery( 'so_css_preview', 1 );
634 this.$( '#preview-iframe' ).attr( 'src', newUri.toString() );
635 }
636 }
637 },
638
639 /**
640 * Update the preview CSS from the CodeMirror value in the editor
641 */
642 updatePreviewCss: function () {
643 var preview = this.$( '#preview-iframe' );
644 if ( preview.length === 0 ) {
645 return;
646 }
647
648 var head = preview.contents().find( 'head' );
649 if ( head.find( 'style.siteorigin-custom-css' ).length === 0 ) {
650 head.append( '<style class="siteorigin-custom-css" type="text/css"></style>' );
651 }
652 var style = head.find( 'style.siteorigin-custom-css' );
653
654 // Update the CSS after a short delay
655 var css = this.editor.codeMirror.getValue().trim();
656 style.html( css );
657 },
658
659 /**
660 * Highlight all elements with a given selector
661 */
662 highlight: function ( selector ) {
663 try {
664 this.editor.inspector.hl.highlight( selector );
665 }
666 catch ( err ) {
667 console.log( 'No inspector to highlight with' );
668 }
669 },
670
671 /**
672 * Clear the currently highlighted elements in preview
673 */
674 clearHighlight: function () {
675 try {
676 this.editor.inspector.hl.clear();
677 }
678 catch ( err ) {
679 console.log( 'No inspector to highlight with' );
680 }
681 }
682
683 } );
684
685 /**
686 * The dialog for the snippets browser
687 */
688 socss.view.snippets = Backbone.View.extend( {
689 template: _.template( $( '#template-snippet-browser' ).html() ),
690 snippet: _.template( '<li class="snippet"><%- name %></li>' ),
691 className: 'css-editor-snippet-browser',
692 snippets: null,
693 editor: null,
694
695 events: {
696 'click .close': 'hide',
697 'click .buttons .insert-snippet': 'insertSnippet',
698 'click .snippet': 'clickSnippet',
699 },
700
701 currentSnippet: null,
702
703 initialize: function ( args ) {
704 this.snippets = args.snippets;
705 },
706
707 render: function () {
708 this.$el.html( this.template() );
709 for ( var i = 0; i < this.snippets.length; i++ ) {
710 $( this.snippet( { name: this.snippets[ i ].Name } ) )
711 .data( {
712 'description': this.snippets[ i ].Description,
713 'css': this.snippets[ i ].css
714 } )
715 .appendTo( this.$( 'ul.snippets' ) );
716 }
717
718 // Click on the first one
719 this.$( '.snippets li.snippet' ).eq( 0 ).trigger( 'click' );
720
721 this.attach();
722 return this;
723 },
724
725 clickSnippet: function ( event ) {
726 event.preventDefault();
727 var $$ = $( event.currentTarget );
728
729 this.$( '.snippets li.snippet' ).removeClass( 'active' );
730 $( this ).addClass( 'active' );
731 this.viewSnippet( {
732 name: $$.html(),
733 description: $$.data( 'description' ),
734 css: $$.data( 'css' )
735 } );
736 },
737
738 viewSnippet: function ( args ) {
739 var w = this.$( '.main .snippet-view' );
740
741 w.find( '.snippet-title' ).html( args.name );
742 w.find( '.snippet-description' ).html( args.description );
743 w.find( '.snippet-code' ).html( args.css );
744
745 this.currentSnippet = args;
746 },
747
748 insertSnippet: function () {
749 var editor = this.editor.codeMirror;
750 var css = this.currentSnippet.css;
751
752 var before_css = '';
753 if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
754 before_css = "";
755 }
756 else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
757 before_css = "\n";
758 }
759 else {
760 before_css = "\n\n";
761 }
762
763 // Now insert the code in the editor
764 editor.doc.setCursor(
765 editor.doc.lastLine(),
766 editor.doc.getLine( editor.doc.lastLine() ).length
767 );
768 editor.doc.replaceSelection( before_css + css );
769
770 this.hide();
771 },
772
773 attach: function () {
774 this.$el.appendTo( 'body' );
775 },
776
777 show: function () {
778 this.$el.show();
779 },
780
781 hide: function () {
782 this.$el.hide();
783 }
784 } );
785
786 socss.save = function ( view ) {
787 let saveBtn = $( '#siteorigin-custom-css .save' );
788 var css;
789
790 if ( ! saveBtn.hasClass( 'button-primary-disabled' ) ) {
791 saveBtn.addClass( 'button-primary-disabled' )
792
793 // Which view is the user using?
794 if ( typeof view.editor != 'undefined' ) {
795 // Visual.
796 css = view.editor.codeMirror.getValue().trim();
797 view.updateMainEditor( true );
798 } else {
799 // Expanded.
800 css = view.codeMirror.getValue().trim();
801 }
802 $.post(
803 socssOptions.ajaxurl,
804 {
805 action: 'socss_save_css',
806 css: css,
807 },
808 null,
809 'html'
810 ).done( function ( response ) {
811 if ( response.length ) {
812 // Update was successful. Update revisions list.
813 $( '.custom-revisions-list' ).html( response );
814 }
815 })
816 .fail( function ( error ) {
817 // Something went wrong. Output the error message as an alert.
818 alert( error.responseText );
819 } )
820 .always( function () {
821 saveBtn.removeClass( 'button-primary-disabled' )
822 } );
823 }
824 };
825
826 /**
827 * The visual properties editor
828 */
829 socss.view.properties = Backbone.View.extend( {
830
831 tabTemplate: _.template( '<li data-section="<%- id %>"><span class="so-css-icon so-css-icon-<%- icon %>"></span> <%- title %></li>' ),
832 sectionTemplate: _.template( '<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>' ),
833 controllerTemplate: _.template( '<tr><th scope="row"><%- title %></th><td></td></tr>' ),
834
835 /**
836 * The controllers for each of the properties
837 */
838 propertyControllers: [],
839
840 /**
841 * The editor view
842 */
843 editor: null,
844
845 /**
846 * The current, raw CSS
847 */
848 css: '',
849
850 /**
851 * Parsed CSS
852 */
853 parsed: {},
854
855 /**
856 * The current active selector
857 */
858 activeSelector: '',
859
860 /**
861 * Was the editor expanded before we went into the property editor
862 */
863 editorExpandedBefore: false,
864
865 events: {
866 'click .close': 'hide',
867 'click .save': 'save',
868 'click .section-tabs li': 'onTabClick',
869 'change .toolbar select': 'onToolbarSelectChange',
870 },
871
872 /**
873 * Initialize the properties editor with a new model
874 */
875 initialize: function ( options ) {
876 this.parser = window.css;
877 this.editor = options.editor;
878 },
879
880 /**
881 * Render the property editor
882 */
883 render: function () {
884 // Clean up for potential re-renders
885 this.$( '.section-tabs' ).empty();
886 this.$( '.sections' ).empty();
887 this.$( '.toolbar select' ).off();
888 this.propertyControllers = [];
889
890 var controllers = socssOptions.propertyControllers;
891
892 for ( var id in controllers ) {
893 // Create the tabs
894 var $t = $( this.tabTemplate( {
895 id: id,
896 icon: controllers[ id ].icon,
897 title: controllers[ id ].title
898 } ) ).appendTo( this.$( '.section-tabs' ) );
899
900 // Create the section wrapper
901 var $s = $( this.sectionTemplate( {
902 id: id
903 } ) ).appendTo( this.$( '.sections' ) );
904
905 // Now lets add the controllers
906 if ( !_.isEmpty( controllers[ id ].controllers ) ) {
907
908 for ( var i = 0; i < controllers[ id ].controllers.length; i++ ) {
909
910 var $c = $( this.controllerTemplate( {
911 title: controllers[ id ].controllers[ i ].title
912 } ) ).appendTo( $s.find( 'tbody' ) );
913
914 var controllerAtts = controllers[ id ].controllers[ i ];
915 var controller;
916
917 if ( typeof socss.view.properties.controllers[ controllerAtts.type ] === 'undefined' ) {
918 // Setup a default controller
919 controller = new socss.view.propertyController( {
920 el: $c.find( 'td' ),
921 propertiesView: this,
922 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
923 } );
924 }
925 else {
926 // Setup a specific controller
927 controller = new socss.view.properties.controllers[ controllerAtts.type ]( {
928 el: $c.find( 'td' ),
929 propertiesView: this,
930 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
931 } );
932 }
933
934 this.propertyControllers.push( controller );
935
936 // Setup and render the controller
937 controller.render();
938 }
939 }
940 }
941
942 // Switch to the first tab.
943 this.$( '.section-tabs li' ).eq( 0 ).trigger( 'click' );
944 },
945
946 onTabClick: function ( event ) {
947 var $$ = $( event.currentTarget );
948 var show = this.$( '.sections .section[data-section="' + $$.data( 'section' ) + '"]' );
949
950 this.$( '.sections .section' ).not( show ).hide().removeClass( 'active' );
951 show.show().addClass( 'active' );
952
953 this.$( '.section-tabs li' ).not( $$ ).removeClass( 'active' );
954 $$.addClass( 'active' );
955 },
956
957 onToolbarSelectChange: function ( event ) {
958 this.setActiveSelector( $( event.currentTarget ).find( ':selected' ).data( 'selector' ) );
959 },
960
961 /**
962 * Sets the rule value for the active selector
963 * @param rule
964 * @param value
965 */
966 setRuleValue: function ( rule, value ) {
967 if (
968 typeof this.activeSelector === 'undefined' ||
969 typeof this.activeSelector.declarations === 'undefined'
970 ) {
971 return;
972 }
973
974 var declarations = this.activeSelector.declarations;
975 var newRule = true;
976 var valueChanged = false;
977 for ( var i = 0; i < declarations.length; i++ ) {
978 if ( declarations[ i ].property === rule ) {
979 newRule = false;
980 var declaration = declarations[ i ];
981 if ( declaration.value !== value ) {
982 declaration.value = value;
983 valueChanged = true;
984 }
985
986 // Remove empty declarations
987 if ( _.isEmpty( declaration.value ) ) {
988 declarations.splice( declarations.indexOf( declaration ) );
989 }
990 break;
991 }
992 }
993
994 if ( newRule && !_.isEmpty( value ) ) {
995 declarations.push( {
996 property: rule,
997 value: value,
998 type: 'declaration',
999 } );
1000 valueChanged = true;
1001 }
1002
1003 if ( valueChanged ) {
1004 this.updateMainEditor( false );
1005 }
1006 },
1007
1008 /**
1009 * Adds the @import rule value if it doesn't already exist.
1010 *
1011 * @param newRule
1012 *
1013 */
1014 addImport: function ( newRule ) {
1015
1016 // get @import rules
1017 // check if any have the same value
1018 // if not, then add the new @ rule
1019
1020 var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule ) {
1021 return rule.type === 'import';
1022 } );
1023 var exists = _.any( importRules, function ( rule ) {
1024 return rule.import === newRule.import;
1025 } );
1026
1027 if ( !exists ) {
1028 // Add it to the top!
1029 // @import statements must precede other rule types.
1030 this.parsed.stylesheet.rules.unshift( newRule );
1031 this.updateMainEditor( false );
1032 }
1033
1034 },
1035
1036 /**
1037 * Find @import which completely or partially contains the specified value.
1038 *
1039 * @param value
1040 */
1041 findImport: function ( value ) {
1042 return _.find( this.parsed.stylesheet.rules, function ( rule ) {
1043 return rule.type === 'import' && rule.import.indexOf( value ) > -1;
1044 } );
1045 },
1046
1047 /**
1048 * Find @import which completely or partially contains the identifier value and update it's import property.
1049 *
1050 * @param identifier
1051 * @param value
1052 */
1053 updateImport: function ( identifier, value ) {
1054 var importRule = this.findImport( identifier );
1055 if ( importRule.import !== value.import ) {
1056 importRule.import = value.import;
1057 this.updateMainEditor( false );
1058 }
1059 },
1060
1061 /**
1062 * Find @import which completely or partially contains the identifier value and remove it.
1063 *
1064 * @param identifier
1065 */
1066 removeImport: function ( identifier ) {
1067 var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) {
1068 return rule.type === 'import' && rule.import.indexOf( identifier ) > -1;
1069 } );
1070 if ( importIndex > -1 ) {
1071 this.parsed.stylesheet.rules.splice( importIndex, 1 );
1072 }
1073 },
1074
1075 /**
1076 * Get the rule value for the active selector
1077 * @param rule
1078 */
1079 getRuleValue: function ( rule ) {
1080 if ( typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined' ) {
1081 return '';
1082 }
1083
1084 var declarations = this.activeSelector.declarations;
1085 for ( var i = 0; i < declarations.length; i++ ) {
1086 if ( declarations[ i ].property === rule ) {
1087 return declarations[ i ].value;
1088 }
1089 }
1090 return '';
1091 },
1092
1093 /**
1094 * Update the main editor with the value of the parsed CSS
1095 */
1096 updateMainEditor: function ( compress ) {
1097 //TODO: add back compress option to remove/merge duplicated CSS selectors.
1098 this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) );
1099 },
1100
1101 /**
1102 * Show the properties editor
1103 */
1104 show: function () {
1105 this.editorExpandedBefore = this.editor.isExpanded();
1106 this.editor.setExpand( true );
1107
1108 this.$el.show().animate( { 'left': 0 }, 'fast' );
1109 },
1110
1111 /**
1112 * Hide the properties editor
1113 */
1114 hide: function () {
1115 this.editor.setExpand( this.editorExpandedBefore );
1116 this.$el.animate( { 'left': -338 }, 'fast', function () {
1117 $( this ).hide();
1118 } );
1119
1120 // Update the main editor with compressed CSS when we close the properties editor
1121 this.updateMainEditor( true );
1122 },
1123
1124 save: function () {
1125 socss.save( this );
1126 },
1127
1128 /**
1129 * @returns boolean
1130 */
1131 isVisible: function () {
1132 return this.$el.is( ':visible' );
1133 },
1134
1135 /**
1136 * Loads a single CSS selector and associated properties into the model
1137 * @param css
1138 */
1139 loadCSS: function ( css, activeSelector ) {
1140 this.css = css;
1141
1142 // Load the CSS
1143 this.parsed = this.parser.parse( css, {
1144 silent: true
1145 } );
1146 var rules = this.parsed.stylesheet.rules;
1147
1148 // Add the dropdown menu items
1149 var dropdown = this.$( '.toolbar select' ).empty();
1150 for ( var i = 0; i < rules.length; i++ ) {
1151 var rule = rules[ i ];
1152
1153 // Exclude @import statements
1154 if ( !_.contains( [ 'rule', 'media' ], rule.type ) ) {
1155 continue;
1156 }
1157
1158 if ( rule.type === 'media' ) {
1159
1160 for ( var j = 0; j < rule.rules.length; j++ ) {
1161 var mediaRule = '@media ' + rule.media;
1162 var subRule = rule.rules[ j ];
1163 if ( subRule.type != 'rule' ) {
1164 continue;
1165 }
1166 dropdown.append(
1167 $( '<option>' )
1168 .html( mediaRule + ': ' + subRule.selectors.join( ',' ) )
1169 .attr( 'val', mediaRule + ': ' + subRule.selectors.join( ',' ) )
1170 .data( 'selector', subRule )
1171 );
1172 }
1173
1174 }
1175 else {
1176 dropdown.append(
1177 $( '<option>' )
1178 .html( rule.selectors.join( ',' ) )
1179 .attr( 'val', rule.selectors.join( ',' ) )
1180 .data( 'selector', rule )
1181 );
1182 }
1183 }
1184
1185 if ( typeof activeSelector === 'undefined' ) {
1186 activeSelector = dropdown.find( 'option' ).eq( 0 ).attr( 'val' );
1187 }
1188 if ( !_.isEmpty( activeSelector ) ) {
1189 dropdown.val( activeSelector ).trigger( 'change' );
1190 }
1191 },
1192
1193 /**
1194 * Set the selector that we're currently dealing with
1195 * @param selector
1196 */
1197 setActiveSelector: function ( selector ) {
1198 this.activeSelector = selector;
1199 for ( var i = 0; i < this.propertyControllers.length; i++ ) {
1200 this.propertyControllers[ i ].refreshFromRule();
1201 }
1202 },
1203
1204 /**
1205 * Add or select a selector.
1206 *
1207 * @param selector
1208 */
1209 addSelector: function ( selector ) {
1210 // Check if this selector already exists
1211 var dropdown = this.$( '.toolbar select' );
1212 dropdown.val( selector );
1213
1214 if ( dropdown.val() === selector ) {
1215 // Trigger a change event to load the existing selector
1216 dropdown.trigger( 'change' );
1217 }
1218 else {
1219 // The selector doesn't exist, so add it to the CSS, then reload
1220 this.editor.addEmptySelector( selector );
1221 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
1222 }
1223
1224 dropdown.addClass( 'highlighted' );
1225 setTimeout( function () {
1226 dropdown.removeClass( 'highlighted' );
1227 }, 2000 );
1228 }
1229
1230 } );
1231
1232 // The basic property controller
1233 socss.view.propertyController = Backbone.View.extend( {
1234
1235 template: _.template( '<input type="text" value="" class="socss-property-controller-input"/>' ),
1236 activeRule: null,
1237 args: null,
1238 propertiesView: null,
1239
1240 events: {
1241 'change .socss-property-controller-input': 'onChange',
1242 'keyup input.socss-property-controller-input': 'onChange',
1243 },
1244
1245 initialize: function ( args ) {
1246
1247 this.args = args.args;
1248 this.propertiesView = args.propertiesView;
1249
1250 // If sub-views items define their own events hash with the same keys as above they will override those on
1251 // the above events hash.
1252 this.events = _.extend( socss.view.propertyController.prototype.events, this.events );
1253 this.delegateEvents( this.events );
1254
1255 // By default, update the active rule whenever things change
1256 this.on( 'set_value', this.updateRule, this );
1257 this.on( 'change', this.updateRule, this );
1258 },
1259
1260 /**
1261 * Render the property field controller
1262 */
1263 render: function () {
1264 this.$el.append( $( this.template( {} ) ) );
1265 this.field = this.$( 'input.socss-property-controller-input' );
1266 },
1267
1268 onChange: function () {
1269 this.trigger( 'change', this.field.val() );
1270 },
1271
1272 /**
1273 * Update the value of an active rule
1274 */
1275 updateRule: function () {
1276 this.propertiesView.setRuleValue(
1277 this.args.property,
1278 this.getValue()
1279 );
1280 },
1281
1282 /**
1283 * This is called when the selector changes
1284 */
1285 refreshFromRule: function () {
1286 var value = this.propertiesView.getRuleValue( this.args.property );
1287 this.setValue( value, { silent: true } );
1288 },
1289
1290 /**
1291 * Get the current value
1292 * @return string
1293 */
1294 getValue: function () {
1295 return this.field.val();
1296 },
1297
1298 /**
1299 * Set the current value
1300 * @param socss.view.properties val
1301 */
1302 setValue: function ( val, options ) {
1303 options = _.extend( { silent: false }, options );
1304
1305 this.field.val( val );
1306
1307 if ( !options.silent ) {
1308 this.trigger( 'set_value', val );
1309 }
1310 },
1311
1312 /**
1313 * Reset the current value
1314 */
1315 reset: function ( options ) {
1316 options = _.extend( { silent: false }, options );
1317
1318 this.setValue( '', options );
1319 }
1320
1321 } );
1322
1323 // All the value controllers
1324 socss.view.properties.controllers = {};
1325
1326 // The color controller
1327 socss.view.properties.controllers.color = socss.view.propertyController.extend( {
1328
1329 render: function () {
1330 socss.view.propertyController.prototype.render.apply( this, arguments );
1331 // Set this up as a color picker
1332 this.field.minicolors( {} );
1333
1334 },
1335
1336 onChange: function () {
1337 this.trigger( 'change', this.field.minicolors( 'value' ) );
1338 },
1339
1340 getValue: function () {
1341 return this.field.minicolors( 'value' ).trim();
1342 },
1343
1344 setValue: function ( val, options ) {
1345 options = _.extend( { silent: false }, options );
1346
1347 this.field.minicolors( 'value', val );
1348
1349 if ( !options.silent ) {
1350 this.trigger( 'set_value', val );
1351 }
1352 }
1353
1354 } );
1355
1356 // The dropdown select box controller
1357 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1358 template: _.template( '<select class="socss-property-controller-input"></select>' ),
1359
1360 events: {
1361 'click .select-tab': 'onSelect',
1362 },
1363
1364 render: function () {
1365 this.$el.append( $( this.template( {} ) ) );
1366 this.field = this.$( 'select' );
1367
1368 // Add the unchanged option
1369 this.field.append( $( '<option value=""></option>' ).html( '' ) );
1370
1371 // Add all the options to the dropdown
1372 for ( var k in this.args.options ) {
1373 this.field.append( $( '<option></option>' ).attr( 'value', k ).html( this.args.options[ k ] ) );
1374 }
1375
1376 if ( typeof this.args.option_icons !== 'undefined' ) {
1377 this.setupVisualSelect();
1378 }
1379 },
1380
1381 setupVisualSelect: function () {
1382 this.field.hide();
1383
1384 var $tc = $( '<div class="select-tabs"></div>' ).appendTo( this.$el );
1385
1386 // Add the none value
1387 $( '<div class="select-tab" data-value=""><span class="so-css-icon so-css-icon-circle"></span></div>' ).appendTo( $tc );
1388
1389 // Now add one for each of the option icons
1390 for ( var k in this.args.option_icons ) {
1391 $( '<div class="select-tab"></div>' )
1392 .appendTo( $tc )
1393 .append(
1394 $( '<span class="so-css-icon"></span>' )
1395 .addClass( 'so-css-icon-' + this.args.option_icons[ k ] )
1396 )
1397 .attr( 'data-value', k )
1398 ;
1399 }
1400
1401 $tc.find( '.select-tab' ).css( 'width', 100 / ( $tc.find( '>div' ).length ) + "%" );
1402 },
1403
1404 onSelect: function ( event ) {
1405 this.$( '.select-tab' ).removeClass( 'active' );
1406 var $t = $( event.currentTarget );
1407 $t.addClass( 'active' );
1408 this.field.val( $t.data( 'value' ) ).trigger( 'change' );
1409 },
1410
1411 /**
1412 * Set the current value
1413 * @param socss.view.properties val
1414 */
1415 setValue: function ( val, options ) {
1416 options = _.extend( { silent: false }, options );
1417
1418 this.field.val( val );
1419
1420 this.$( '.select-tabs .select-tab' ).removeClass( 'active' ).filter( '[data-value="' + val + '"]' ).addClass( 'active' );
1421
1422 if ( !options.silent ) {
1423 this.trigger( 'set_value', val );
1424 }
1425 }
1426
1427 } );
1428
1429 // A field that lets a user upload an image
1430 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1431 template: _.template( '<input type="text" value="" /> <span class="select socss-button"><span class="so-css-icon so-css-icon-upload"></span></span>' ),
1432
1433 events: {
1434 'click .select': 'openMedia',
1435 },
1436
1437 render: function () {
1438 this.media = wp.media( {
1439 // Set the title of the modal.
1440 title: socssOptions.loc.select_image,
1441
1442 // Tell the modal to show only images.
1443 library: {
1444 type: 'image'
1445 },
1446
1447 // Customize the submit button.
1448 button: {
1449 // Set the text of the button.
1450 text: socssOptions.loc.select,
1451 // Tell the button not to close the modal, since we're
1452 // going to refresh the page when the image is selected.
1453 close: false
1454 }
1455 } );
1456
1457 this.$el.append( $( this.template( {
1458 select: socssOptions.loc.select
1459 } ) ) );
1460
1461 this.field = this.$el.find( 'input' );
1462
1463 this.media.on( 'select', function () {
1464 // Grab the selected attachment.
1465 var attachment = this.media.state().get( 'selection' ).first().attributes;
1466 var val = this.args.value.replace( '{{url}}', attachment.url );
1467
1468 // Change the field value and trigger a change event
1469 this.field.val( val ).trigger( 'change' );
1470 this.trigger( 'set_value', val );
1471
1472 // Close the image selector
1473 this.media.close();
1474
1475 }.bind( this ) );
1476 },
1477
1478 openMedia: function () {
1479 this.media.open();
1480 },
1481
1482 } );
1483
1484 // A simple measurement field
1485 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1486
1487 wrapperClass: 'socss-field-measurement',
1488
1489 events: {
1490 'click .toggle-dropdown': 'toggleUnitDropdown',
1491 'click .dropdown li': 'onSelectUnit',
1492 'keydown .socss-field-input': 'onInputKeyPress',
1493 'keyup .socss-field-input': 'onInputKeyUp',
1494 },
1495
1496 render: function () {
1497 socss.view.propertyController.prototype.render.apply( this, arguments );
1498
1499 this.setupMeasurementField();
1500 },
1501
1502 setValue: function ( val, options ) {
1503 options = _.extend( { silent: false }, options );
1504 this.field.val( val ).trigger( 'measurement_refresh' );
1505 if ( !options.silent ) {
1506 this.trigger( 'set_value', val );
1507 }
1508 },
1509
1510 units: [
1511 'px',
1512 '%',
1513 'em',
1514 'cm',
1515 'mm',
1516 'in',
1517 'pt',
1518 'pc',
1519 'ex',
1520 'ch',
1521 'rem',
1522 'vw',
1523 'vh',
1524 'vmin',
1525 'vmax'
1526 ],
1527
1528 parseUnits: function ( value ) {
1529 var escapeRegExp = function ( str ) {
1530 return str.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" );
1531 };
1532
1533 var regexUnits = this.units.map( escapeRegExp );
1534 var regex = new RegExp( '([0-9\\.\\-]+)(' + regexUnits.join( '|' ) + ')?', 'i' );
1535 var result = regex.exec( value );
1536
1537 if ( result === null ) {
1538 return {
1539 value: '',
1540 unit: ''
1541 };
1542 }
1543 else {
1544 return {
1545 value: result[ 1 ],
1546 unit: result[ 2 ] === undefined ? '' : result[ 2 ]
1547 };
1548 }
1549 },
1550
1551 setupMeasurementField: function () {
1552 var defaultUnit = 'px';
1553
1554 this.field.hide();
1555 this.$el.addClass( this.wrapperClass ).data( 'unit', defaultUnit );
1556
1557 // Create the fake input field
1558 var $fi = $( '<input type="text" class="socss-field-input"/>' ).appendTo( this.$el );
1559 $( '<span class="toggle-dropdown dashicons dashicons-arrow-down"></span>' ).appendTo( this.$el );
1560 var $dd = $( '<ul class="dropdown"></ul>' ).appendTo( this.$el );
1561 var $u = $( '<span class="units"></span>' ).html( defaultUnit ).appendTo( this.$el );
1562
1563 for ( var i = 0; i < this.units.length; i++ ) {
1564 var $o = $( '<li></li>' ).html( this.units[ i ] ).data( 'unit', this.units[ i ] );
1565 if ( this.units[ i ] === defaultUnit ) {
1566 $o.addClass( 'active' );
1567 }
1568 $dd.append( $o );
1569 }
1570
1571 this.field.on( 'measurement_refresh', function () {
1572 var value = this.parseUnits( this.field.val() );
1573 $fi.val( value.value );
1574
1575 var unit = value.unit === '' ? defaultUnit : value.unit;
1576 this.$el.data( 'unit', unit );
1577 $u.html( unit );
1578
1579 var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
1580 .css( {
1581 'font-size': '14px'
1582 } )
1583 .html( value.value )
1584 .appendTo( 'body' );
1585 var width = $pl.width();
1586 width = Math.min( width, 63 );
1587 $pl.remove();
1588
1589 $u.css( 'left', width + 12 );
1590 }.bind( this ) );
1591
1592 // Now add the increment/decrement buttons
1593 var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
1594 var $dec = $( '<div class="dec-button socss-button"><span class="so-css-icon so-css-icon-minus"></span></div>' ).appendTo( $diw );
1595 var $inc = $( '<div class="inc-button socss-button"><span class="so-css-icon so-css-icon-plus"></span></div>' ).appendTo( $diw );
1596
1597 this.setupStepButton( $dec );
1598 this.setupStepButton( $inc );
1599
1600 },
1601
1602 updateValue: function () {
1603 var $fi = this.$( '.socss-field-input' );
1604 var value = this.parseUnits( $fi.val() );
1605
1606 if ( value.unit !== '' && value.unit !== this.$el.data( 'unit' ) ) {
1607 $fi.val( value.value );
1608 this.setUnit( value.unit );
1609 }
1610
1611 if ( value.value === '' ) {
1612 this.field.val( '' );
1613 }
1614 else {
1615 this.field.val( value.value + this.$el.data( 'unit' ) );
1616 }
1617 this.field.trigger( 'change' );
1618 },
1619
1620 setUnit: function ( unit ) {
1621 this.$( '.units' ).html( unit );
1622 this.$el.data( 'unit', unit );
1623 this.$( '.socss-field-input' ).trigger( 'keydown' );
1624 },
1625
1626 toggleUnitDropdown: function () {
1627 this.$( '.dropdown' ).toggle();
1628 },
1629
1630 onSelectUnit: function ( event ) {
1631 this.toggleUnitDropdown();
1632 this.setUnit( $( event.currentTarget ).data( 'unit' ) );
1633 this.updateValue();
1634 },
1635
1636 onInputKeyUp: function( event ) {
1637 this.onInputKeyPress( event );
1638 this.updateValue();
1639 },
1640
1641 onInputKeyPress: function ( event ) {
1642 var $fi = this.$( '.socss-field-input' );
1643
1644 var char = '';
1645 if ( event.type === 'keydown' ) {
1646 if ( event.keyCode >= 48 && event.keyCode <= 57 ) {
1647 char = String.fromCharCode( event.keyCode );
1648 }
1649 else if ( event.keyCode === 189 ) {
1650 char = '-';
1651 }
1652 else if ( event.keyCode === 190 ) {
1653 char = '.';
1654 }
1655 }
1656
1657 var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
1658 .css( {
1659 'font-size': '14px'
1660 } )
1661 .html( $fi.val() + char )
1662 .appendTo( 'body' );
1663 var width = $pl.width();
1664 width = Math.min( width, 63 );
1665 $pl.remove();
1666
1667 this.$( '.units' ).css( 'left', width + 12 );
1668 },
1669
1670 stepValue: function ( direction ) {
1671 var value = Number.parseInt( this.parseUnits( this.field.val() ).value );
1672
1673 if ( Number.isNaN( value ) ) {
1674 value = 0;
1675 }
1676
1677 var newVal = value + direction;
1678
1679 this.$( '.socss-field-input' ).val( newVal );
1680 this.updateValue();
1681 this.field.trigger( 'measurement_refresh' );
1682 },
1683
1684 setupStepButton: function ( $button ) {
1685 var direction = $button.is( '.dec-button' ) ? -1 : 1;
1686 var intervalId;
1687 var timeoutId;
1688 $button.on( 'mousedown', function() {
1689 this.stepValue( direction );
1690 timeoutId = setTimeout( function () {
1691 intervalId = setInterval( function () {
1692 this.stepValue( direction );
1693 }.bind( this ), 50 );
1694 }.bind( this ), 500 );
1695 }.bind( this ) ).on( 'mouseup mouseout', function () {
1696 if ( timeoutId ) {
1697 clearTimeout( timeoutId );
1698 timeoutId = null;
1699 }
1700 if ( intervalId ) {
1701 clearInterval( intervalId );
1702 intervalId = null;
1703 }
1704 } );
1705 },
1706 } );
1707
1708 // A simple measurement field
1709 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1710
1711 initialize: function ( args ) {
1712 socss.view.propertyController.prototype.initialize.apply( this, arguments );
1713
1714 this.args = _.extend( {
1715 change: null,
1716 default: 0,
1717 increment: 1,
1718 decrement: -1,
1719 max: null,
1720 min: null
1721 }, args.args );
1722 },
1723
1724 render: function () {
1725 socss.view.propertyController.prototype.render.apply( this, arguments );
1726
1727 this.setupNumberField();
1728 },
1729
1730 setupNumberField: function () {
1731
1732 this.$el.addClass( 'socss-field-number' );
1733
1734 // Now add the increment/decrement buttons
1735 var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
1736 var $dec = $( '<div class="dec-button socss-button"><span class="so-css-icon so-css-icon-minus"></span></div>' ).appendTo( $diw );
1737 var $inc = $( '<div class="inc-button socss-button"><span class="so-css-icon so-css-icon-plus"></span></div>' ).appendTo( $diw );
1738
1739 this.setupStepButton( $dec );
1740 this.setupStepButton( $inc );
1741
1742 return this;
1743 },
1744
1745 stepValue: function ( direction ) {
1746 var value = Number.parseFloat( this.field.val() );
1747
1748 if ( Number.isNaN( value ) ) {
1749 value = this.args.default;
1750 }
1751
1752 var newVal = value + direction;
1753
1754 newVal = Math.round( newVal * 100 ) / 100;
1755
1756 if ( this.args.max !== null ) {
1757 newVal = Math.min( this.args.max, newVal );
1758 }
1759
1760 if ( this.args.min !== null ) {
1761 newVal = Math.max( this.args.min, newVal );
1762 }
1763
1764 this.field.val( newVal );
1765 this.field.trigger( 'change' );
1766 },
1767
1768 setupStepButton: function ( $button ) {
1769 var direction = $button.is( '.dec-button' ) ? this.args.decrement : this.args.increment;
1770 var intervalId;
1771 var timeoutId;
1772 $button.on( 'mousedown', function() {
1773 this.stepValue( direction );
1774 timeoutId = setTimeout( function () {
1775 intervalId = setInterval( function () {
1776 this.stepValue( direction );
1777 }.bind( this ), 50 );
1778 }.bind( this ), 500 );
1779 }.bind( this ) ).on( 'mouseup mouseout', function () {
1780 if ( timeoutId ) {
1781 clearTimeout( timeoutId );
1782 timeoutId = null;
1783 }
1784 if ( intervalId ) {
1785 clearInterval( intervalId );
1786 intervalId = null;
1787 }
1788 } );
1789 },
1790
1791 } );
1792
1793
1794 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1795
1796 template: _.template( $( '#template-sides-field' ).html().trim() ),
1797
1798 controllers: [],
1799
1800 events: {
1801 'click .select-tab': 'onTabClick',
1802 },
1803
1804 render: function () {
1805
1806 socss.view.propertyController.prototype.render.apply( this, arguments );
1807
1808 if ( !this.args.hasAll ) {
1809 this.$( '.select-tab' ).eq( 0 ).remove();
1810 this.$( '.select-tab' ).css( 'width', '25%' );
1811 }
1812
1813 if ( ! this.args.isRadius ) {
1814 this.$( '.select-tabs[data-type="radius"]' ).remove();
1815 } else {
1816 this.$( '.select-tabs[data-type="box"]' ).remove();
1817 }
1818
1819 this.$( '.select-tab' ).each( function ( index, element ) {
1820 var dir = $( element ).data( 'direction' );
1821
1822 var container = $( '<li class="side">' )
1823 .appendTo( this.$( '.sides' ) )
1824 .hide();
1825
1826 for ( var i = 0; i < this.args.controllers.length; i++ ) {
1827
1828 var controllerArgs = this.args.controllers[ i ];
1829
1830 if ( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1831
1832 // Create the measurement view
1833 var property = '';
1834 if ( dir === 'all' ) {
1835 property = controllerArgs.args.propertyAll;
1836 }
1837 else {
1838 property = controllerArgs.args.property.replace( '{dir}', dir );
1839 }
1840
1841 var theseControllerArgs = _.extend( {}, controllerArgs.args, { property: property } );
1842
1843 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1844 el: $( '<div>' ).appendTo( container ),
1845 propertiesView: this.propertiesView,
1846 args: theseControllerArgs
1847 } );
1848
1849 // Setup and render the measurement controller and register it with the properties view
1850 controller.render();
1851 this.propertiesView.propertyControllers.push( controller );
1852 }
1853 }
1854
1855 }.bind( this ) );
1856
1857 // Select the first tab by default
1858 this.$( '.select-tab' ).eq( 0 ).click();
1859 },
1860
1861 onTabClick: function ( event ) {
1862 var $tabs = this.$( '.select-tab' );
1863 $tabs.removeClass( 'active' );
1864
1865 var $tab = $( event.currentTarget );
1866 $tab.addClass( 'active' );
1867
1868 var $sides = this.$( '.sides .side' )
1869 $sides.hide();
1870
1871 $sides.eq( $tabs.index( $tab ) ).show();
1872 },
1873 } );
1874
1875 // This is a placeholder for the full font_select in SiteOrigin Premium
1876 socss.view.properties.controllers.font_select = socss.view.propertyController.extend( {
1877 template: _.template( $('#template-webfont-teaser').html().trim() )
1878 });
1879
1880 } )( jQuery, _, socssOptions );
1881
1882 // Setup the main editor
1883 jQuery( function ( $ ) {
1884 var socss = window.socss;
1885
1886 var editorModel = new socss.model.CSSEditorModel( {
1887 customCssPosts: socssOptions.customCssPosts,
1888 } );
1889
1890 // Setup the editor
1891 var editor = new socss.view.editor( {
1892 el: $( '#so-custom-css-form' ).get( 0 ),
1893 model: editorModel,
1894 openVisualEditor: socssOptions.openVisualEditor,
1895 } );
1896 // editor.render();
1897 editor.setSnippets( socssOptions.snippets );
1898
1899 // This is for hiding the getting started video
1900 $( '#so-custom-css-getting-started a.hide' ).on( 'click', function( e ) {
1901 e.preventDefault();
1902 $( '#so-custom-css-getting-started' ).slideUp();
1903 $.get( $( this ).attr( 'href' ) );
1904 } );
1905
1906 window.socss.mainEditor = editor;
1907 $( socss ).trigger( 'initialized' );
1908
1909 $( '.button-primary[name="siteorigin_custom_css_save"]' ).on( 'click', function() {
1910 $( '#so-custom-css-form' ).trigger( 'submit' );
1911 } );
1912
1913 $( '.installer-link' ).on( 'click', function( e ) {
1914 e.preventDefault();
1915 $( this ).hide();
1916 $( '.installer-container' ).slideDown( 'fast' );
1917 } );
1918
1919 $( '.installer_status' ).on( 'change', function() {
1920 var $$ = $( this );
1921 $$.prop( 'disabled', true );
1922 jQuery.post(
1923 ajaxurl,
1924 {
1925 action: 'so_installer_status',
1926 nonce: $$.data( 'nonce' ),
1927 status: $$.is( ':checked' )
1928 },
1929 function() {
1930 $$.prop( 'disabled', false );
1931 }
1932 );
1933 } );
1934 } );
1935