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