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