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