From b89a7ee8b956c96a1dcee995ea840feddc5d4b27 Mon Sep 17 00:00:00 2001 From: Pierre Frisch Date: Thu, 22 Dec 2011 07:25:50 -0800 Subject: First commit of Ninja to ninja-internal Signed-off-by: Valerio Virgillito --- .../CSSPanel/CSSPanelBase.reel/CSSPanelBase.js | 2885 ++++++++++++++++++++ 1 file changed, 2885 insertions(+) create mode 100644 js/panels/CSSPanel/CSSPanelBase.reel/CSSPanelBase.js (limited to 'js/panels/CSSPanel/CSSPanelBase.reel/CSSPanelBase.js') diff --git a/js/panels/CSSPanel/CSSPanelBase.reel/CSSPanelBase.js b/js/panels/CSSPanel/CSSPanelBase.reel/CSSPanelBase.js new file mode 100644 index 00000000..ae7e5c14 --- /dev/null +++ b/js/panels/CSSPanel/CSSPanelBase.reel/CSSPanelBase.js @@ -0,0 +1,2885 @@ +/* +This file contains proprietary software owned by Motorola Mobility, Inc.
+No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
+(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved. +
*/ + +var cssPropertyNameList = require("js/panels/CSSPanel/css-property-name-list").cssPropertyNameList, + cssCompletionMap = require("js/panels/CSSPanel/css-value-completion-map").cssValueCompletionMap, + CSS_SHORTHAND_MAP = require("js/panels/CSSPanel/css-shorthand-map").CSS_SHORTHAND_MAP, + keyboardModule = require("js/mediators/keyboard-mediator").Keyboard, + nj = require("js/lib/NJUtils.js").NJUtils; + + + +var CSSPanel = exports.CSSPanelBase = (require("montage/core/core").Montage).create(require("montage/ui/component").Component, { + prepareForDraw: { + value: function() { + var self = this; + + this.sections = { + sheets : { + container : nj.$('nj-section-stylesheets'), + heading : nj.$('nj-css-sheets-header'), + arrow : nj.$('nj-css-sheets-arrow'), + doc : null, + listEl : nj.$('nj-sheet-list'), + docNameEl : nj.$('nj-current-doc-name'), + collapsed : false, + toolbar : nj.$('nj-css-stylesheet-toolbar'), + addSheetEl: nj.$('nj-css-add-stylesheet') + }, + styles : { + container : nj.$('nj-section-styles'), + heading : nj.$('nj-css-styles-header'), + statusMsg : nj.$('nj-status-heading'), + arrow : nj.$('nj-css-styles-arrow'), + listEl : nj.$('nj-css-rule-list'), + elNameEl : nj.$('nj-current-element-name'), + collapsed : false, + currentEl : null, + toolbar : nj.$('nj-css-styles-toolbar'), + addRuleEl : nj.$('nj-css-add-rule'), + showComputedEl : nj.$('nj-css-show-computed'), + numItemsEl : nj.$('nj-num-items') + } + }; + + ///// Set up collapsible sub sections + ['sheets', 'styles'].forEach(function(section) { + var s = section; + self.sections[s].heading.addEventListener('click', function(e) { + self.toggleSectionCollapse(s); + }, false); + }); + + //// Hook into selection manager + + this.eventManager.addEventListener("selectionChange", this, true); + this.eventManager.addEventListener("elementChange", this, true); + this.eventManager.addEventListener("openDocument", this, true); + + if(this.application.ninja.currentDocument) { + this.captureOpenDocument(); + } + + this.addEventListener('webkitTransitionEnd', this, false); + ['sheets', 'styles'].forEach(function(section) { + this.sections[section].container.style.height = 'auto'; + // //console.log('setting height to auto for section "' + section + '".'); + }.bind(this)); + + this._setUpStyleEditMode(); + this._setUpToolbars(); + } + }, + captureOpenDocument : { + value : function(e) { + this.populateStyleSheetList(); + } + }, + handleWebkitTransitionEnd : { + value: function(e) { + //console.log('transition end at panel base'); + e.stopPropagation(); + } + }, + + populateStyleSheetList: { + value: function() { + this.sections.sheets.doc = this.application.ninja.currentDocument._document; + var styleTagCount = 0, + sect = this.sections.sheets, + sheetsArray = nj.toArray(sect.doc.styleSheets), + listEl = sect.listEl, + contEl = sect.container, + userDocName = nj.getFileNameFromPath(sect.doc.defaultView.location.href), + self = this; + + ///// Set current document name in Stylesheets section header + nj.empty(sect.docNameEl).appendChild(nj.textNode(' - ' + userDocName)); + + ///// LOOP through stylesheet list + ///// ------------------------------------------------------- + sheetsArray.forEach(function(sheet, index) { + var isStageStyleSheet = (sheet.ownerNode.id === this._stageStyleSheetId), + isDefaultStyleSheet = (sheet.ownerNode.id === this._defaultStyleSheetId), + sheetObj; + + if(!isStageStyleSheet) { + sheetObj = new NJStyleSheet(sheet, index); + if(isDefaultStyleSheet) { + sheetObj.isProtected = sheetObj.isCurrent = true; + this.currentStyleSheet = this.defaultStyleSheet = sheetObj; + } + + //// Add Default stylesheet selection + sheetObj.sheetNameEl.addEventListener('click', function(e) { + //console.log('clicking sheet'); + e.preventDefault(); + //e.stopPropagation(); + self.currentStyleSheet = sheetObj; + }, false); + + sheetObj.deleteEl.addEventListener('click', function(e) { + if(sheetObj.isCurrent) { + self.defaultStyleSheet.makeCurrent(); + } + }, false); + + sheetObj.render(listEl); + } + + + }, this); + ///// ________________________________________________________ + + ///// save height of content, and convert height from "auto" to pixels + //sect.height = contEl.style.height = nj.height(contEl); + + + + //contEl.style.webkitTransition = 'all 0.15s ease-out'; + + } + }, + clearStyleSheetList : { + value: function() { + nj.empty(this.sections.sheets.listEl); + } + }, + _preventAdvanceToNext : { // don't advance if there's an error on stop edit + value: false + }, + _setUpStyleEditMode: { + value: function() { + var self = this; + ///// Add onchange event to NJCSSStyle + NJCSSStyle.prototype.onStyleChange.push(function() { + self._stageUpdate(); + }); + + ///// Add some NJEditable functionality + NJEditable.prototype.onStartEdit.push(function(e) { + if(this.isSelector && this.el.nodeContent === 'element.style') { + return; + } + var njStyle = this.el.parentNode.njcssstyle; + // //console.log('added start edit'); + this.el.parentNode.classList.add('nj-editing'); + if(this.el.nodeName === "DD") { + this.el.parentNode.classList.add('nj-editing-val'); // controls wrapping of text + if(cssCompletionMap[njStyle.property]) { + this.suggestions = cssCompletionMap[njStyle.property]; + } + } + }); + NJEditable.prototype.onStopEdit.push(function(e) { + var nextEl = this.el, + isAddButton = false, + autoSuggestArray = null, + nextEditable, sibling, njStyle, isAddBtn; + + this.el.parentNode.classList.remove('nj-editing'); + this.el.parentNode.classList.remove('nj-editing-val'); + + if(this.isSelector) { + if(e && [9,13].indexOf(e._event.keyCode) !== -1) { + // console.log('selector onStopEdit function'); + var propertyEl = nj.children(this.el.nextSibling.firstChild, function(item){ + return item.nodeName === 'DT'; + })[0]; + if(propertyEl.parentNode.njcssstyle.activate) { + ///// still the Add button + propertyEl.parentNode.njcssstyle.activate.bind(propertyEl)(); + delete propertyEl.parentNode.njcssstyle.activate; + } else { + nextEditable = propertyEl.njedit || new NJEditable(propertyEl, null, self.CSS_PROPERTY_NAMES); + nextEditable.startEditable(); + } + + } + return false; + } + + ////console.log('NJEditable onStopEdit callback'); + + if(e && [9,13,186].indexOf(e._event.keyCode) !== -1) { // if the user is tabbing between styles + e.preventDefault(); + sibling = (e._event.keyCode === 9 && e._event.shiftKey) ? ['previousSibling', 'lastChild'] : ['nextSibling', 'firstChild']; + // //console.log('enter pressed - skip to next editable.'); + // move to the next editable dt/dd elements + do { + if(nextEl[sibling[0]]) { + nextEl = nextEl[sibling[0]]; + } else { + + if(!nextEl.parentNode[sibling[0]]) { // no next style element + /// get njcssrule and create add button, + /// and activate it if the new styles isn't dirtied + // //console.log('reached the end'); + njStyle = nextEl.parentNode.njcssstyle; + if(njStyle.isNewStyle) { + njStyle.container.classList.remove('nj-css-no-error'); + if(nextEl.njedit) { + nextEl.njedit.onRevert.length = 0; + } + } + // //console.log('Prototype onStopEdit - creating Add button'); + addStyleBtn = njStyle.njRule.createAddButton(); + // bind the element as 'this', emulating the 'click' event's context + addStyleBtn.activate.bind(addStyleBtn.propEl)(); + nextEl = false; + break; + } else { + nextEl = (nextEl.parentNode[sibling[0]]) ? + nextEl.parentNode[sibling[0]][sibling[1]]: + nextEl.parentNode.parentNode[sibling[1]][sibling[1]]; + } + } + njStyle = njStyle || nextEl.parentNode.njcssstyle; + } + while (!isEditableNode(nextEl)); + + if(nextEl) { + if(!self._preventAdvanceToNext) { + if (nextEl.nodeName === 'DT') { + autoSuggestArray = self.CSS_PROPERTY_NAMES; + if(njStyle.activate) { /// if the next style is the Add button + isAddButton = true; + } + } else if(nextEl.nodeName === 'DD' && cssCompletionMap[njStyle.property]) { + autoSuggestArray = cssCompletionMap[njStyle.property]; + } + nextEditable = nextEl.njedit || new NJEditable(nextEl, null, autoSuggestArray); + if(isAddButton) { + njStyle.activate.bind(nextEl)(); + } else { + nextEditable.startEditable(); + } + } else{ + self._preventAdvanceToNext = false; + } + } + } + + function isEditableNode(n) { + return n && n.nodeType === 1 && (n.nodeName === 'DT' || n.nodeName === 'DD'); + } + }); + + NJEditable.prototype.onChange.push(_onEditableChange); + + ///// Event delegation for editable nodes + this.sections.styles.container.addEventListener('click', function(e) { + if(!this.njedit && (this.nodeName === 'DT' || this.nodeName === 'DD') && !self._inComputedStyleMode) { + if(this.parentNode.className.indexOf('nj-css-style-add') === -1) { + // //console.log('set up editable node!'); + var edit = new NJEditable(this, null, self.CSS_PROPERTY_NAMES); // TODO: window.propertyKeywords); + edit.startEditable(); + } + } + }, false); + + function _onEditableChange(val, oldVal) { + if(this.isSelector) { + ////console.log('selector val = ' + val); + return false; + } + + var parent = this.el.parentNode, + oldValue = oldVal, + edit = this, + style, propName, propVal, isNewStyle, modifyCommand; + + ///// Find NJCSSRule corresponding to this NJEditable element + ///// (the style container has reference to NJCSSStyle object) + while (!style) { + if(parent.njcssstyle) { + style = parent.njcssstyle; + } else { + parent = parent.parentNode; + } + } + + // //console.log("Found style obj!"); + // //console.log(style); + + isNewStyle = style.isNewStyle; + + //// remove semi-colons + val = val.trim().replace(';',''); + this.val(val); + + ///// set up command for undo/redo + modifyCommand = Object.create(Object.prototype, { + description: { value: "Style Change" }, + //receiver: { value: receiver }, + execute: { + value: function() { + //console.log('updating to property name to ' + val); + style.updateProperty(val); + return this; + } + }, + unexecute: { + value: function() { + //console.log('Undo property name change back to ' + oldVal); + if(isNewStyle) { + style.remove(false, true); + } + style.updateProperty(oldVal); + } + } + }); + + ///// is this an edit to the prop or val? + if(this.el.nodeName === 'DT') { + //// property name was edited + if(val) { + modifyCommand.execute(); + NJevent("sendToUndo", modifyCommand); + } else { + ///// let the remove method take care of Undo/Redo + style.updateProperty(val); + } + + } else if (this.el.nodeName === 'DD') { + //// property value was edited + if(!style.updateValue(val, isNewStyle)) { + if(isNewStyle) { + //console.log('is new style : true'); + style.container.classList.remove('nj-css-no-error'); + this.onRevert.length = 0; + style.isNewStyle = false; + } else { + self._preventAdvanceToNext = true; + parent.addEventListener('webkitAnimationEnd', function njShake(e) { + this.classList.remove('nj-css-shake'); + ///// and revert value back to original value + edit.revert(null, true); + edit.startEditable(); + this.removeEventListener('webkitAnimationEnd', njShake, false); + }, false); + parent.classList.add('nj-css-shake'); + } + } + + } + } + } + }, + _stageStyleSheetId : { + value: 'nj-stage-stylesheet', + enumerable: false + }, + _defaultStyleSheetId : { + value: 'nj-default-stylesheet', + enumerable: false + }, + _defaultStyleSheet : { + value: null, + enumerable: false + }, + _userContentContainerId : { + value: '#UserContent', + enumarable: false + }, + _styleTagCount : { + value: 0 + }, + _setUpToolbars : { + value: function() { + var self = this, + command; + + this.sections.sheets.addSheetEl.addEventListener('click', function(e) { + var doc = self.sections.sheets.doc, + handleRemoval, + njSheet; + + handleRemoval = function(njSheet) { + if(njSheet.isCurrent) { + self.currentStyleSheet = self.defaultStyleSheet; + } + if(self._currentRuleList) { + self._currentRuleList.update(); + } + }; + + var rec = { + addSheet : function() { + ////console.log('Add Sheet'); + njSheet = self.createStyleSheet(new String(++self._styleTagCount)); + self.scrollTo('bottom', self.sections.sheets.container); + //// Add Default stylesheet selection + njSheet.sheetNameEl.addEventListener('click', function(e) { + //console.log('clicking sheet'); + e.preventDefault(); + //e.stopPropagation(); + self.currentStyleSheet = njSheet; + }, false); + + njSheet.deleteEl.addEventListener('click', function(e) { + handleRemoval(njSheet); + }, false); + + self.currentStyleSheet = njSheet; + }, + removeSheet : function() { + ////console.log('Remove Sheet'); + handleRemoval(njSheet); + njSheet.remove(); + } + }; + + command = Object.create(Object.prototype, { + description: { value: "Add Stylehsset" }, + receiver: { value: rec }, + execute: { + value: function() { + this.receiver.addSheet(); + ////console.log('execute'); + return this; + } + }, + unexecute: { + value: function() { + ////console.log('unexecute'); + this.receiver.removeSheet(); + } + } + }); + command.execute(); + NJevent("sendToUndo", command); + + }); + + this.sections.styles.addRuleEl.addEventListener('click', function(e) { + var selectorText, addRuleCommand, newNJRule; + + e.preventDefault(); + + selectorText = (self._inMultiSelectMode) ? self._userContentContainerId + ' .newClass' : null; + + addRuleCommand = Object.create(Object.prototype, { + description: { value: "Add Rule" }, + //receiver: { value: rec }, + execute: { + value: function() { + newNJRule = self._currentRuleList.initNewRule(self._currentStyleSheet, selectorText); + ////console.log('execute'); + return this; + } + }, + unexecute: { + value: function() { + ////console.log('unexecute'); + var list = self._currentRuleList, + elements = (list.el.length) ? list.el : [list.el]; + + newNJRule.delete(); + + if(list.addedClassName) { + elements.forEach(function(el) { + el.classList.remove(list.addedClassName); + }); + } + } + } + }); + + if(!self._currentStyleSheet) { + self.currentStyleSheet = self.createStyleSheet('Temp'); + } + + self._currentStyleSheet.dirty(); + + //self._currentRuleList.initNewRule(self._currentStyleSheet, selectorText); + addRuleCommand.execute(); + NJevent("sendToUndo", addRuleCommand); + }, false); + + this.sections.styles.showComputedEl.addEventListener('click', function(e) { + var computedStyleList; + e.preventDefault(); + self.inComputedStyleMode = !self.inComputedStyleMode; + }); + } + }, + captureSelectionChange: { + value: function(event) { + //console.log('capture selection change'); + var items = this.application.ninja.selectedElements, + itemIndex = -1, + currentEl, currentRuleList, nextEl, nextRuleList, commonRules; + + if(items.length > 1) { + this.clearCSSRules(); + this._inMultiSelectMode = true; + this.inComputedStyleMode = false; // No computed styles mode for multiple items + + ///// if multiple items are selected, then show common rules + var elements = items.map(function(item) { + return item._element; + }); + + ///// show toolbar, but hide computed style button + this.sections.styles.toolbar.style.display = ''; + this.sections.styles.showComputedEl.classList.add('nj-css-panel-hide');// .style.display = 'none'; + this._currentRuleList = new NJCSSRuleList(elements, this); + this.sections.styles.statusMsg.classList.remove('nj-css-panel-hide'); + nj.empty(this.sections.styles.numItemsEl).appendChild(nj.textNode(items.length)); + this._currentRuleList.render(this.sections.styles.container); + + } else if(items.length === 1) { + //console.log('Selection change: One element selected'); + this._inMultiSelectMode = false; + this.sections.styles.statusMsg.classList.add('nj-css-panel-hide'); + this.sections.styles.showComputedEl.classList.remove('nj-css-panel-hide');// .style.display = ''; + this.sections.styles.toolbar.style.display = ''; + this.showStylesForElement(items[0]._element, null); + } else { + this.sections.styles.statusMsg.classList.add('nj-css-panel-hide'); + this._inMultiSelectMode = false; + this.sections.styles.toolbar.style.display = 'none'; + ///// If no elements are selected, clear styles + this.computedStyleSubPanel.hide(); + this.clearCSSRules(); + } + + } + }, + captureElementChange:{ + value:function(event){ + if(this._ignoreElementChangeEventOnce) { + ///// TODO: Change this by having the event object have a custom flag + ///// for identifying events originating from this panel + this._ignoreElementChangeEventOnce = false; + return false; + } + //console.log('capture element change'); + var items = this.application.ninja.selectedElements; + if(items.length === 0 && event._event.eventType === 'style') { + //// stage style has changed + if(event._event.item.ownerDocument.styleSheets[0].njStyleSheet) { + event._event.item.ownerDocument.styleSheets[0].njStyleSheet.dirty(); + } else { + // TODO: Need a way of identifying the changing CSS rule (Stylesheet Manager) + ////console.log('could not find njStyleSheet'); + } + } else if(event._event.eventType === 'style' && items.length === 1) { + this.showStylesForElement(this.sections.styles.currentEl, event._event.data); + } + } + }, + _ignoreElementChangeEventOnce : { + value: false, + enumerable: false + }, + _currentRuleList : { + value : null, + enumerable: false + }, + _currentComputedStyleList : { + value : null, + enumerable: false + }, + _currentStyleSheet : { + value: null, + enumerable: false + }, + currentStyleSheet : { + get: function() { + return this._currentStyleSheet; + }, + set: function(njStyleSheet) { + if(this._currentStyleSheet) { + this._currentStyleSheet.unMakeCurrent(); + } + njStyleSheet.makeCurrent(); + this._currentStyleSheet = njStyleSheet; + } + }, + setDefaultSheet : { + value: function(njSheet) { + this._currentStyleSheet = njSheet; + } + }, + _inMultiSelectMode : { + value: false, + enumerable: false + }, + _inComputedStyleMode : { + value: false, + enumerable: false + }, + inComputedStyleMode : { + get : function() { + return this._inComputedStyleMode; + }, + set : function(turnOn) { + var btnOnClass = 'nj-css-computed-on', + hideClass = 'nj-css-panel-hide'; + + if(turnOn) { + ///// Turn ON computed style mode + //console.log('Turning ON computed style mode'); + this.computedStyleSubPanel.declaration = this._currentRuleList.computed; + this.computedStyleSubPanel.show(); + this.sections.styles.container.classList.add(hideClass); + this.sections.styles.addRuleEl.classList.add(hideClass); + this.sections.styles.showComputedEl.parentNode.classList.add(btnOnClass); + } else { + ///// Turn OFF computed style mode + //console.log('Turning OFF computed style mode'); + this.computedStyleSubPanel.hide(); + this.sections.styles.container.classList.remove(hideClass); + this.sections.styles.addRuleEl.classList.remove(hideClass); + this.sections.styles.showComputedEl.parentNode.classList.remove(btnOnClass); + } + + this._inComputedStyleMode = turnOn; + } + }, + createStyleSheet : { + value : function(title) { + var listEl = this.sections.sheets.listEl, + sheet, njSheet; + + title = title || ''; + + sheet = nj.make('style', { + type : 'text/css', + id : title, + media : 'screen', + title : 'Temp' + }); + + this.sections.sheets.doc.head.appendChild(this.sections.sheets.doc.createComment('User-added stylesheet number ' + title)); + this.sections.sheets.doc.head.appendChild(sheet); + + njSheet = new NJStyleSheet(sheet.sheet, title); // TODO: Fix index + njSheet.render(listEl); + + return njSheet; + } + }, + showStylesForElement : { + value: function(el, updateList, preventUpdate) { + var sect = this.sections.styles, + contEl = sect.container, + elNameEl = sect.elNameEl, + identifier, njRuleList, computedHeight; + + ///// Save current DOM element to section object + ///// so that it is retrievable by elementChange + sect.currentEl = el; + + ///// Show element name in panel header + identifier = el.nodeName.toLowerCase(); + if(el.id) { + // use append id if avail + identifier += '#'+el.id; + } else if (el.className) { + // or, use combined class +// identifier += '.' + el.className.trim().replace(' ', '.'); + } + + this.clearCSSRules(); ///// Clear css styles subsection + + ///// set new element name in header + //nj.empty(elNameEl); + elNameEl.appendChild(nj.textNode(' - ' + identifier)); + + if(el.njcssrulelist) { + ///// use the existing NJCSSRuleList object +// // //console.log('user existing njcssrulelist object'); + this._currentRuleList = njRuleList = el.njcssrulelist; + if(!preventUpdate) { + njRuleList.update(updateList); + } + njRuleList.show(); + } else { + ///// Create list of css rules from selected element + this._currentRuleList = njRuleList = new NJCSSRuleList(el, this); + // Render rule list (pass in container) + njRuleList.render(contEl); + } + + if(this._inComputedStyleMode) { + ////console.log('in computed style mode'); + this.computedStyleSubPanel.declaration = el; + this.computedStyleSubPanel.show(); + return; + } else { + this.computedStyleSubPanel.hide(); + } + + + ///// set height to "" (empty) to capture computed height and remove transition + contEl.style.webkitTransition = ''; + contEl.style.height = ''; + computedHeight = nj.height(contEl); + + //// re-apply transition + sect.height = contEl.style.height = 'auto'; + //contEl.style.webkitTransition = 'all 0.15s ease-out'; + } + }, + clearCSSRules : { + value: function(callback) { + + if(this._currentRuleList) { + this._currentRuleList.hide(); + } + nj.empty(this.sections.styles.elNameEl); + + } + }, + _stageUpdate : { + value: function() { + this._ignoreElementChangeEventOnce = true; + //documentControllerModule.DocumentController.DispatchElementChangedEvent(this.application.ninja.selectedElements); + // TODO: might need to remove this + } + }, + toggleSectionCollapse : { + value: function(section) { + var isClosed = this.sections[section].collapsed, + action = (isClosed) ? 'expandSection' : 'collapseSection'; + + this[action](section).collapsed = !isClosed; + } + }, + collapseSection : { // returns section object literal + value: function(sect) { + var section = this.sections[sect], + contEl = section.container, + arrow = section.arrow, + cssClass = 'closed'; + + if(sect === 'styles' && this._inComputedStyleMode) { + contEl = this.computedStyleSubPanel.element; + } + + contEl.addEventListener('webkitTransitionEnd', function njCollapse(e) { + e.stopPropagation(); + e.preventDefault(); + this.style.webkitTransition = ''; + this.removeEventListener('webkitTransitionEnd', njCollapse, false); + }, false); + section.height = nj.height(contEl); + contEl.style.webkitTransition = 'height 0.15s ease-out'; + arrow.className = (arrow.className + ' ' + cssClass).trim(); + contEl.className = (contEl.className + ' ' + cssClass).trim(); // controls non-height transitions + contEl.style.height = '0px'; + + section.toolbar.classList.add('nj-css-panel-hide'); + + return section; + } + }, + expandSection : { // returns sections object literal + value: function(sect) { + var section = this.sections[sect], + contEl = section.container, + arrow = section.arrow, + cssClass = 'closed'; + + contEl.style.webkitTransition = 'height 0.15s ease-out'; + arrow.className = arrow.className.replace(cssClass, '').trim(); + contEl.className = contEl.className.replace(cssClass, '').trim(); // controls non-height transitions + //console.log('section height: ' + section.height); + contEl.style.height = section.height; + + contEl.addEventListener('webkitTransitionEnd', function njExpando(e) { + e.stopPropagation(); + e.preventDefault(); + this.style.webkitTransition = ''; + this.style.height = 'auto'; + this.removeEventListener('webkitTransitionEnd', njExpando, false); + }, false); + + section.toolbar.classList.remove('nj-css-panel-hide'); + + return section; + } + }, + computedStyleSubPanel : { + value: null + }, + scrollTo : { + value: function(x, container) { + //// Control scroll position of the CSS Panel itself + //// or the option container + var panelEl = container || document.getElementById('cssPanelContent'); + if(x === 'bottom') { + x = panelEl.scrollHeight; + } else if(x === 'top') { + x = '0'; + } + panelEl.scrollTop = x; + } + }, + getAllRelatedRules : { + value: function(element) { + var pseudos = [null],//, 'link', 'visited', 'active', 'hover', 'focus', 'first-letter', + //'first-line', 'first-child', 'before', 'after', 'lang'], + rules = [], + self = this; + + pseudos.forEach(function(pseudo) { + rules = rules.concat(nj.toArray(this.getMatchedCSSRules(element, ':'+pseudo)).filter(function(rule) { + var sheetId = (rule.parentStyleSheet) ? rule.parentStyleSheet.ownerNode.id : null; + return sheetId !== self._stageStyleSheetId; + })); + }, element.ownerDocument.defaultView); + + return rules; + } + }, + CSS_PROPERTY_NAMES : { + value: cssPropertyNameList, + enumerable: true + } +}); + + +/* -------------------------------------------------------- + * NJCSSRuleList - Object for managing list of NJCSSRules + * Renders rules to the document + * If an array of elements is passed, the NJCSSRuleList + * will find the common rules and render them + * --------------------------------------------------------*/ + +function NJCSSRuleList(el, cssPanel) { + this.el = el; + this.njRules = []; + this.cssPanel = cssPanel; + this.container = nj.make('ul', 'nj-css-rule-list'); + this.computed = null; + this.commonRules = null; + this.inlineStyleRule = null; + this.addedClassName = null; + + var self = this, + styleAttrRule; + + if(el instanceof Array) { + this.rules = this.getCommonRules(); + } else { + styleAttrRule = el.style; + + el.njcssrulelist = this; + styleAttrRule.isInlineStyle = true; + this.computed = el.ownerDocument.defaultView.getComputedStyle(el); + + ///// converts CSSRuleList to array + this.rules = this.cssPanel.getAllRelatedRules(el); + this.rules.splice(0, 0, { + selectorText : 'element.style', + parentStyleSheet : 'Inline Style', + style : styleAttrRule + }); + + } + +} + +NJCSSRuleList.prototype.getCommonRules = function() { + var itemIndex = -1, + currentEl, currentRuleList, nextEl, nextRuleList, commonRules; + + do { + ///// Get current element's matched rules + currentEl = this.el[++itemIndex]; + currentRuleList = this.cssPanel.getAllRelatedRules(currentEl); + //currentRuleList = nj.toArray(currentEl.ownerDocument.defaultView.getMatchedCSSRules(currentEl)); + + ///// Get next element's matched rules + nextEl = this.el[itemIndex+1]; + nextRuleList = this.cssPanel.getAllRelatedRules(nextEl); + + ///// use filter to see if any rules exist in the next set of rules + commonRules = currentRuleList.filter(function(rule) { + return nextRuleList.indexOf(rule) !== -1; + }); + + } while (itemIndex+2 < this.el.length && commonRules.length > 0); + + //console.log('Selection change: ' + commonRules.length + ' common rule(s)'); + this.commonRules = commonRules; + return commonRules; +}; + +NJCSSRuleList.prototype.update = function(updateList) { + var deleteRules = [], matchedRules; + ////console.log('NJCSSRuleList::update'); + if(this.el.length > 1) { + matchedRules = this.getCommonRules(); + } else { + matchedRules = this.cssPanel.getAllRelatedRules(this.el) + } + + this.inlineStyleRule.update(updateList); + + ///// Update NEW and CHANGED rules + matchedRules.forEach(function(rule, index) { + var njRule = rule.njcssrule; + ///// if matched rule is already in list, check to see if it has changed + if (njRule && this.rules.indexOf(rule) !== -1) { + ////console.log('NJCSSRuleList::update - found njRule for "' + rule.cssText + '"' ); + njRule.update(updateList); + } else { + ////console.log('NJCSSRuleList::update - no njRule found. creating one for "' + rule.cssText + '"'); + njRule = new NJCSSRule(rule, this); + this.rules.push(rule); + this.njRules.push(njRule); + this.container.appendChild(njRule.container); + } + }, this); + ///// For each rule in rulelist, check to see if it exists in matched rules + ///// if not found, not inline, and not an "unapplied" style, remove it + this.njRules.forEach(function(rule) { + if(matchedRules.indexOf(rule.rule) === -1 && !rule.shownAsUnapplied && !rule.rule.style.isInlineStyle) { + ////console.log("Found rule to delete"); + deleteRules.push(rule); + } + }, this); + deleteRules.forEach(function(rule) { + rule.delete(true); + }); +}; + +NJCSSRuleList.prototype.initNewRule = function(njStyleSheet, selectorName) { + var selectorText = (this.cssPanel._inMultiSelectMode) ? '' : this.el.nodeName.toLowerCase(), + sheet = this.cssPanel._currentStyleSheet.sheet, + index = sheet.rules.length, + self = this, + rule, njRule, selector, height, padTop, padBot, intervalId, stopKeys; + + //// Derive default selector + if(selectorName) { + selectorText = selectorName; + } else if(this.el.id) { + selectorText += '#' + this.el.id; + } else if(this.el.classList.length > 0) { + selectorText += '.' + this.el.classList[0]; + } + + //// Insert new rule, and create NJCSSRule by passing reference to rule + sheet.insertRule(selectorText + ' { }', index); + rule = sheet.rules.item(index); + njRule = new NJCSSRule(rule, this); + this.rules.push(rule); + this.njRules.push(njRule); + ////console.log('Init rule : pushed rule to rule list:'); + ////console.log(rule); + + njRule.container.classList.add('nj-get-height'); + this.container.appendChild(njRule.container); + + // Slide-in new rule from bottom of viewport + ///// Calculate height; + height = nj.height(njRule.container); + padTop = njRule.container.ownerDocument.defaultView.getComputedStyle(njRule.container).getProperty('paddingTop'); + padBot = njRule.container.ownerDocument.defaultView.getComputedStyle(njRule.container).getProperty('paddingBottom'); + njRule.container.style.paddingTop = '0'; + njRule.container.style.paddingBottom = '0'; + + ///// Set height to zero, make visible, and apply transition + + njRule.container.classList.add('nj-pre-slide'); + njRule.container.classList.remove('nj-get-height'); + + njRule.container.addEventListener('webkitTransitionEnd', function scroller() { + //console.log('scroller end'); + clearInterval(intervalId); + njRule.container.style.height = ''; + //njRule.container.style.webkitTransition = ''; + njRule.container.classList.remove('nj-pre-slide'); + self.cssPanel.scrollTo('bottom', self.container.parentNode); + this.removeEventListener('webkitTransitionEnd', scroller, false); + }, false); + + ///// Apply new height; + njRule.container.style.height = height; + njRule.container.style.paddingTop = padTop; + njRule.container.style.paddingBottom = padBot; + ///// set interval to pin scroll position to bottom + intervalId = setInterval(function() { + this.cssPanel.scrollTo('bottom', self.container.parentNode); + }.bind(this), 5); + + ///// Make selector editable, and attach selector specific event handling + selector = njRule.selectorEditable; + + ///// Remove ':' from key actions config + stopKeys = selector.defaults.keyActions.noHint.stop; + stopKeys.splice(stopKeys.indexOf(186), 1); + + //console.log('NJCSSRuleList::initNewRule - attaching onStepEdit and onChange events'); + /*selector.onStopEdit = [function(e) { + // TODO: Fix event management, rather than clobbering prototype + if(e && [9,13].indexOf(e._event.keyCode) !== -1) { + // //console.log('selector onStopEdit function'); + var propertyEl = nj.children(this.el.nextSibling.firstChild, function(item){ + return item.nodeName === 'DT'; + })[0]; + if(propertyEl.parentNode.njcssstyle.activate) { + ///// still the Add button + propertyEl.parentNode.njcssstyle.activate.bind(propertyEl)(); + delete propertyEl.parentNode.njcssstyle.activate; + } else { + propertyEl.njedit.startEditable(); + } + + } + }];*/ + if(this.cssPanel._inMultiSelectMode) { + selector.onStopEdit.push(function(e) { + var val = this.val(), + results = /.*\.([A-Za-z0-9_-]+):?[a-z]*$/.exec(val); //'#UserContent div.myClass:hover' + ///// if the selector is a class, apply to the elements + if(results) { + //console.log('NJCSSRuleList::initNewRule - selector has a classname - ' + results[1]); + self.addedClassName = results[1]; + self.el.forEach(function(el) { + el.classList.add(results[1]); + }); + self.cssPanel._stageUpdate(); + } + }); + } + selector.onChange = [function(val) { + if(val === '') { + njRule.delete(); + return; + } + + var elArray = [], doesApply; + //console.log('NJCSSRuleList::initNewRule - on change event'); + njRule.rule.selectorText = val; + + if(self.el.length) { + elArray = self.el; + } else { + elArray.push(self.el); + } + + doesApply = elArray.every(function(item, index, arr) { + return njRule.appliesToElement(item); + }); + + if(doesApply) { + ///// Success - selector change applies to element + if(njRule.shownAsUnapplied) { + njRule.showAsApplied(); + } + } else { + ///// Failed - unapply style for this rule list + njRule.showAsUnapplied(); + } + // //console.log('selector text is now: ' + njRule.rule.selectorText); + }]; + selector.startEditable(); + + return njRule; +}; + +NJCSSRuleList.prototype.getChangedRules = function() { + return this.njRules.filter(function(rule) { + return rule.hasChanged(); + }); +}; + +NJCSSRuleList.prototype.render = function(parent) { + if(this.njRules.length) { + // //console.log('NJCSSRuleList has rules already'); + } else { + this.rules.forEach(function(rule) { + var njcssrule = new NJCSSRule(rule, this); + this.njRules.push(njcssrule); + + if(rule.style.isInlineStyle) { + this.inlineStyleRule = njcssrule; + } + + this.container.appendChild(njcssrule.container); + }, this); + + //// append list to parent container + parent.appendChild(this.container); + } +}; + +NJCSSRuleList.prototype.hide = function() { + this.container.style.display = 'none'; +}; + +NJCSSRuleList.prototype.show = function() { + this.container.style.display = ''; +}; + +/* -------------------------------------------------------- + * NJCSSRule - Object for to represent CSSRule in CSSPanel + * Responsible for rendering html in the following format: +
  • + myCSS.css + #mySelector +
    +
    background-color
    black
    +
    color
    white
    +
    + +
  • + * --------------------------------------------------------*/ + +function NJCSSRule(rule, njRuleList) { + var els = this.defaults.elements, + self = this; + + this.rule = rule; + this.njRuleList = njRuleList; + this.declaration = rule.style; + this.styleSheet = rule.parentStyleSheet; + this.njStyleSheet = (rule.parentStyleSheet) ? rule.parentStyleSheet.njStyleSheet : null; + this.cssText = rule.cssText; + this.styles = {}; + this.unappliedStyles = []; + this.shownAsUnapplied = false; + this.addButton = null; + this.selectorEditable = null; + + ///// Create selector, link, and containers + ['container', 'sheetLink', 'selector', 'listContainer'].forEach(function(el) { + self[el] = nj.make(els[el].tag, els[el].attr); + }); + + ///// Add reference to CSSRule to container element + this.container.rule = rule; + + ///// Add reference of self to host CSSRule object + rule.njcssrule = this; + + ///// Populate selector, sheetLink elements + this.selector.appendChild(nj.textNode(rule.selectorText)); + if(this.njStyleSheet) { + this.sheetLink.appendChild(nj.textNode(this.njStyleSheet.name)); + } + + if(!this.declaration.isInlineStyle) { + ///// Make selector an NJEditable object + this.selectorEditable = new NJEditable(this.selector); + this.selectorEditable.isSelector = true; + this.selectorEditable.onChange = [function(val) { + if(val === '') { + self.delete(); + return; + } + + var elArray = [], doesApply; + self.rule.selectorText = val; + + if(self.njRuleList.el.length) { + elArray = self.njRuleList.el; + } else { + elArray.push(self.njRuleList.el); + } + + doesApply = elArray.every(function(item, index, arr) { + return self.appliesToElement(item); + }); + + if(doesApply) { + ///// Success - selector change applies to element + if(self.shownAsUnapplied) { + self.showAsApplied(); + } + } else { + ///// Failed - unapply style for this rule list + self.showAsUnapplied(); + } + // console.log('selector text is now: ' + njRule.rule.selectorText); + }]; + } + this.createStyles(); + + ///// sort list and render sorted list to page + Object.keys(this.styles).sort().forEach(function(style) { + // //console.log('Style property: ' + style); + self.styles[style].render(self.listContainer); + }); + ////console.log('NJCSSRule::constructor - creating Add button'); + this.createAddButton(); + + ///// Add elements to container + ['sheetLink', 'selector', 'listContainer'].forEach(function(el) { + self.container.appendChild(self[el]); + }); +} + +NJCSSRule.prototype.createStyles = function() { + var self = this; + + nj.toArray(this.declaration).forEach(function(prop, index) { + var style = new NJCSSStyle(self, index); + self.styles[style.property] = style; + }); +}; + +NJCSSRule.prototype.createAddButton = function() { + if(this.addButton) { + removeAddButtonBehavior(this.addButton); + } + var btn = this.addButton = new NJCSSStyle(this), + self = this, + propEditable, valuEditable; + + ///// Create "Add" style btn + btn.defaults.elements.itemWrapper.attr = 'nj-css-add-style nj-css-no-checkbox nj-css-no-error'; // classes to be added to the style container + //btn.skipValueUpdateOnce = true; // don't show error until the user has tried to input value + btn.render(this.listContainer); + delete btn.defaults.elements.itemWrapper.attr; + + btn.propEl.addEventListener('click', addButtonClickHandler, false); + btn.propEl.addEventListener('focus', addButtonClickHandler, false); + + propEditable = new NJEditable(btn.propEl, null, self.njRuleList.cssPanel.CSS_PROPERTY_NAMES); + valuEditable = new NJEditable(btn.valEl); + + function removeAddButtonBehavior(btn) { + //console.log('removing add button behavior'); + var propEdit = btn.propEl.njedit, + valEdit = btn.valEl.njedit; + + [propEdit, valEdit].forEach(function(edit) { + edit.onBlur.length = 0; + edit.onRevert.length = 0; + }); + + btn.container.classList.remove('nj-css-no-checkbox'); + btn.isNewStyle = false; + + if(!propEdit.isDirty || !valEdit.isDirty) { + btn.container.classList.add('nj-css-error'); + } + + delete btn.activate; + + } + + function addButtonClickHandler(e) { + var propEl = this, + valuEl = valuEditable.el, + container = propEl.parentNode, + propOnRevertIndex, valuOnRevertIndex; + + if(e) { // event is undefined if calling this programmatically + e.stopPropagation(); + e.preventDefault(); + } + + // //console.log('Add btn click event'); + propEl.removeEventListener('click', addButtonClickHandler, false); + propEl.removeEventListener('focus', addButtonClickHandler, false); + + if(propEditable.onBlur.length === 0) { /// TODO: check for existance of onblur method, instead + propEditable.onBlur.push(function(e) { + //console.log('NJCSSRule::createAddButton - onblur for propEditable. this node name : ' + propEditable.el.nodeName); + ///// if not selecting value element, recreate button + if(e.target != valuEl) { + propEditable.onBlur.length = 0; + if(propEditable.isDirty || valuEditable.isDirty) { + //console.log('propEditable onblue - creating add button'); + self.createAddButton(); + /// remove error indicator hiding class + container.classList.remove('nj-css-no-error'); + } else { + ///// render new Add button + reCreateAddBtn(); + } + } + }); + } + + // //console.log('CreateAddButton:: adding onblur event to value field w/ namename: ' + valuEditable.el.nodeName); + if(valuEditable.onBlur.length === 0) { /// TODO: check for existance of onblur method, instead + valuEditable.onBlur.push(function(e) { + //console.log('NJCSSRule::createAddButton - onblur for valuEditable. this node name : ' + valuEditable.el.nodeName); + ///// if not selecting value element, recreate button + if(e.target != propEl) { + valuEditable.onBlur.length = 0; + ///// render new Add button + if(propEditable.isDirty || valuEditable.isDirty) { + //console.log('valEditable onblue - creating add button'); + self.createAddButton(); + /// remove error indicator hiding class + container.classList.remove('nj-css-no-error'); + valuEditable.onRevert.length = 0; + } else { + reCreateAddBtn(); + } + } + }); + } + + propEditable.onRevert.push(reCreateAddBtn); + valuEditable.onRevert.push(reCreateAddBtn); + + function reCreateAddBtn() { + ////console.log('recreating button'); + ///// assign "Add" btn css class + container.classList.add('nj-css-add-style'); + ///// Remove error class (if there) + container.classList.remove('nj-css-error'); + ///// Re-bind event listener to property el (aka the "Add" btn) + propEl.addEventListener('click', addButtonClickHandler, false); + ///// Revert property element's "Add" btn text + propEditable.val('Add'); + } + + propEditable.startEditable(null, true); + propEditable.val(''); + // //console.log('adding transition event listener before adding transition class'); + propEl.addEventListener('webkitTransitionEnd', function trans(e) { + e.stopPropagation(); + ///// remove button/transition classes + propEl.parentNode.classList.remove('trans'); + propEl.parentNode.classList.remove('nj-css-add-style'); + + ///// Add "clean" (not dirty) state to newly transformed editable objects + + propEl.classList.add('nj-css-clean'); + propEditable.val('property'); + propEditable.onDirty.push(function() { + this.el.classList.remove('nj-css-clean'); + }); + + valuEl.classList.add('nj-css-clean'); + valuEditable.onDirty.push(function() { + this.el.classList.remove('nj-css-clean'); + }); + + propEditable.selectAll(); + this.removeEventListener('webkitTransitionEnd', trans, false); + // //console.log('button transition end'); + }, false); + + setTimeout(function() { + propEl.parentNode.classList.add('trans'); + }, 10); + + } + // add non-event-based hook (used for programmatically transforming to new style) + btn.activate = addButtonClickHandler; + return btn; +}; + +///// Update Rules if they have changed +///// Use optional parameter to specify rules to update +///// The styles object should be an array of objects +///// that look like this: { "style":"....", "value":"...." } + +NJCSSRule.prototype.update = function(updateList) { + if(this.hasChanged()) { + var self = this, + allStyles = nj.toArray(this.rule.style), + //iterateList = (updateList) ? updateList : allStyles, + iterateList = allStyles, /// remove + removedStyles = [], + useUpdateList = false, + shorthand, index; + + + /// Only use update list if left and/or top, for now. + if(updateList) { + useUpdateList = updateList.every(function(obj) { + return (obj.style === 'left' || obj.style === 'top'); + }, this); + } + + if(useUpdateList) { + iterateList = updateList; + } + + //// loop through styles in host rule object or provided style array + iterateList.forEach(function(prop, index) { + var currentValue, oldValue, newStyle, propName; + + if(updateList && useUpdateList) { + currentValue = prop.value; + propName = prop.style; + ///// index needs to be re-calculated to make sense within rule + ///// for njcssstyle object creation + index = allStyles.indexOf(propName); + } else { + currentValue = NJCSSStyle.roundUnits(this.rule.style.getPropertyValue(prop)); + propName = prop; + } + + if(this.styles.hasOwnProperty(propName)) { ///// if the property was already defined + oldValue = this.styles[propName].value; + ///// if the value doesn't match saved value, update text + if(currentValue !== oldValue) { + //console.log('Prop: ' + prop + '. val: ' + currentValue + '. oldVal: ' + oldValue); + this.styles[propName].setValueText(currentValue); + if(this.styles[propName].disabled) { + this.styles[propName].enable(); + } + } + } else { // else, create a new style object and render to page + if(index !== -1) { + ////console.log('creating style with index : ' + index); + shorthand = this.getShorthandStylePresent(propName); + /// Does shorthand property exist in list? If not, render this style property + if(!shorthand) { + newStyle = new NJCSSStyle(this, index); + this.styles[newStyle.property] = newStyle; + + ///// find index of new style (listed alphabetically) + index = Object.keys(this.styles).sort().indexOf(newStyle.property); + ///// pass the index to insert style in right spot (uses insertBefore) + newStyle.render(this.listContainer, index); + } else { + shorthand.update(); + } + } else { + // //console.log('index not foudn'); + } + } + + }, this); + + if(!updateList) { + ///// Remove styles that are no longer defined in rule + Object.keys(self.styles).forEach(function(style) { + if(allStyles.indexOf(style) === -1 && !self.styles[style].unapplied) { // not found + self.styles[style].remove(true); + } + }); + } + + } +}; + +NJCSSRule.prototype.getShorthandStylePresent = function(property) { + ///// Get list of shorthand properties for this property name + var shorthands = CSS_SHORTHAND_MAP[property], i, shorthand; + + ///// If the list does not exist, then there is no shorthand for this + if(!shorthands) { + return false; + } + + ///// There may be multiple shorthands, for example: + ///// (background-position-x has background-position, and background) + ///// If any match an existing style in our list of styles, return true + for(i = 0; i