Nali.extend View:
extension: -> if @_name isnt 'View' @_shortName = @_name.underscore().split( '_' )[ 1.. ].join( '_' ).camel() @_parseTemplate() @_parseEvents() @ cloning: -> @my = @model @_prepareElement() @ layout: -> null _onSourceUpdated: -> @_draw() _onSourceDestroyed: -> @hide() getOf: ( source, property ) -> @redrawOn source, "update.#{ property }" source[ property ] getMy: ( property ) -> @getOf @model, property redrawOn: ( source, event ) -> @subscribeTo source, event, @_onSourceUpdated insertTo: -> if ( layout = @layout() )?.childOf? 'View' then layout.show().yield else @Application.htmlContainer _draw: -> @_runAssistants 'draw' @onDraw?() @ show: ( insertTo = @insertTo() ) -> unless @visible @_runModelCallback 'beforeShow' @_draw()._bindEvents() @_runAssistants 'show' @subscribeTo @model, 'destroy', @_onSourceDestroyed @element.appendTo insertTo setTimeout ( => @onShow() ), 5 if @onShow? @trigger 'show' @visible = true @_runModelCallback 'afterShow' @ hide: ( delay = 0 ) -> if @visible @_runModelCallback 'beforeHide' @onHide?() @_unbindEvents() @trigger 'hide' @_runAssistants 'hide' @_hideElement if delay and typeof( delay ) is 'number' then delay else @hideDelay @destroyObservation() @visible = false @_runModelCallback 'afterHide' @ _hideElement: ( delay ) -> if delay then setTimeout ( => @_removeElement() ), delay else @_removeElement() @ _removeElement: -> @element[0].parentNode.removeChild @element[0] @ _runModelCallback: ( type ) -> @model[ type ]?[ @_shortName ]?.call @model @ _runLink: ( event ) -> event.preventDefault() @_runUrl event.currentTarget.getAttribute 'href' @ _runForm: ( event ) -> event.preventDefault() @_runUrl event.currentTarget.getAttribute( 'action' ), form2js event.currentTarget, '.', false @ _runUrl: ( url, params ) -> if match = url.match /^(@@?)(.+)/ [ chain, segments... ] = match[2].split '/' if result = @_analizeChain chain, ( if match[1].length is 1 then @ else @model ) [ source, method ] = result if method of source args = @_parseUrlSegments segments args.unshift params if params source[ method ] args... else console.warn "Method %s not exists of %O", method, source else @redirect url, params @ _parseUrlSegments: ( segments ) -> params = [] for segment in segments when segment isnt '' [ name, value ] = segment.split ':' if value last = params[ params.length - 1 ] params.push last = {} if typeof last isnt 'object' last[ name ] = value else params.push name params _parseEvents: -> @_eventsMap = [] if @events @events = [ @events ] if typeof @events is 'string' for event in @events try [ handlers, type, other ] = event.split /\s+(on|one)\s+/ [ events, selector ] = other.split /\s+at\s+/ handlers = handlers.split /\s*,\s*/ events = events.replace /\s*,\s*/, ' ' throw true unless type and events.length and handlers.length catch console.warn "Events parsing error: \"%s\" of %O", event, @ error = true if error then error = false else @_eventsMap.push [ selector, type, events, handlers ] @ _bindEvents: -> @_bindRoutedElements 'a', 'href', 'click', ( event ) => @_runLink event @_bindRoutedElements 'form', 'action', 'submit', ( event ) => @_runForm event for [ selector, type, events, handlers ] in @_eventsMap for handler in handlers do ( selector, type, events, handler ) => @element[ type ] events, selector, ( event ) => @[ handler ] event @ _bindRoutedElements: ( selector, urlProp, event, callback ) -> finded = ( el for el in @element.find( selector ) when el[ urlProp ].indexOf( window.location.origin ) >= 0 ) finded.push @element[0] if @element.is selector ( @_routedElements ?= {} )[ selector ] = @_( finded ).on event, callback @ _unbindEvents: -> @element.off() @_routedElements.a.off() @_routedElements.form.off() @ _prepareElement: -> unless @element @element = @_ @template @element[0].view = @ @_addAssistants() @ _getNode: ( path ) -> node = @element[0] node = node[ sub ] for sub in path node _parseTemplate: -> if container = document.querySelector '#' + @_name.underscore() @template = container.innerHTML.trim().replace( /\s+/g, ' ' ) .replace( /({\s*\+.+?\s*})/g, ' <assist>$1</assist>' ) unless RegExp( "^<[^>]+" + @_name ).test @template @template = "<div class=\"#{ @_name }\">#{ @template }</div>" @_parseAssistants() container.parentNode.removeChild container else console.warn 'Template %s not exists', @_name @ _parseAssistants: -> @_assistantsMap = [] if /{\s*.+?\s*}|bind=".+?"/.test @template tmp = document.createElement 'div' tmp.innerHTML = @template @_scanAssistants tmp.children[0] @ _scanAssistants: ( node, path = [] ) -> if node.nodeType is 3 if /{\s*yield\s*}/.test( node.textContent.trim() ) and node.parentNode.childNodes.length is 1 @_assistantsMap.push nodepath: path, type: 'Yield' else if /^{\s*\w+ of @\w*\s*}$/.test( node.textContent.trim() ) and node.parentNode.childNodes.length is 1 @_assistantsMap.push nodepath: path, type: 'Relation' else if /{\s*.+?\s*}/.test node.textContent @_assistantsMap.push nodepath: path, type: 'Text' else if node.nodeName is 'ASSIST' @_assistantsMap.push nodepath: path, type: 'Html' else if node.attributes for attribute, index in node.attributes if attribute.name is 'bind' @_assistantsMap.push nodepath: path, type: 'Form' else if /{\s*.+?\s*}/.test attribute.value @_assistantsMap.push nodepath: path.concat( 'attributes', index ), type: 'Attr' @_scanAssistants child, path.concat 'childNodes', index for child, index in node.childNodes @ _addAssistants: -> @_assistants = show: [], draw: [], hide: [] @[ "_add#{ type }Assistant" ] @_getNode nodepath for { nodepath, type } in @_assistantsMap @ _runAssistants: ( type ) -> assistant.call @ for assistant in @_assistants[ type ] @ _addTextAssistant: ( node ) -> initialValue = node.textContent @_assistants[ 'draw' ].push -> node.textContent = @_analize initialValue @ _addAttrAssistant: ( node ) -> initialValue = node.value @_assistants[ 'draw' ].push -> node.value = @_analize initialValue @ _addHtmlAssistant: ( node ) -> parent = node.parentNode initialValue = node.innerHTML index = Array::indexOf.call parent.childNodes, node after = parent.childNodes[ index - 1 ] or null before = parent.childNodes[ index + 1 ] or null @_assistants[ 'draw' ].push -> start = if after then Array::indexOf.call( parent.childNodes, after ) + 1 else 0 end = if before then Array::indexOf.call parent.childNodes, before else parent.childNodes.length parent.removeChild node for node in Array::slice.call( parent.childNodes, start, end ) parent.insertBefore element, before for element in @_( @_analize initialValue ) @ _addFormAssistant: ( node ) -> if bind = @_analizeChain node.attributes.removeNamedItem( 'bind' ).value [ source, property ] = bind $node = @_ node updateSource = -> ( params = {} )[ property ] = if node.type is 'checkbox' and !node.checked then null else if node.value is '' then null else node.value source.update params source.save() unless node.form? [ setValue, bindChange ] = switch when node.type in [ 'text', 'textarea', 'color', 'date', 'datetime', 'datetime-local', 'email', 'number', 'range', 'search', 'tel', 'time', 'url', 'month', 'week' ] [ -> node.value = source[ property ] or '' -> $node.on 'change', => updateSource.call @ ] when node.type in [ 'checkbox', 'radio' ] [ -> node.checked = source[ property ] + '' is node.value -> $node.on 'change', => updateSource.call @ if node.type is 'checkbox' or node.checked is true ] when node.type is 'select-one' [ -> option.selected = true for option in node when source[ property ] + '' is option.value -> $node.on 'change', => updateSource.call @ ] @_assistants[ 'show' ].push -> setValue.call @ bindChange.call @ source.subscribe @, "update.#{ property }", => setValue.call @ @_assistants[ 'hide' ].push -> $node.off 'change' @ _addYieldAssistant: ( node ) -> ( @yield = @_ node.parentNode )[0].removeChild node _addRelationAssistant: ( node ) -> [ match, name, chain ] = node.textContent.match /{\s*(\w+) of @(\w*)\s*}/ ( insertTo = node.parentNode ).removeChild node segments = if chain.length then chain.split '.' else [] @_assistants[ 'show' ].push -> if relation = @_getSource segments if relation.childOf 'Collection' relation.show name, insertTo, true relation.subscribeTo @, 'hide', relation.reset else view = relation.show name, insertTo view.subscribeTo @, 'hide', view.hide _analize: ( value ) -> value.replace /{\s*(.+?)\s*}/g, ( match, sub ) => @_analizeMatch sub _analizeMatch: ( sub ) -> if match = sub.match /^@([\w\.]+)(\?)?$/ if result = @_analizeChain match[1] [ source, property ] = result source.subscribe? @, "update.#{ property }", @_onSourceUpdated if match[2] is '?' if source[ property ] then property else '' else if source[ property ]? then source[ property ] else '' else '' else if match = sub.match /^[=|\+](\w+)$/ @helpers?[ match[1] ]?.call @ else sub _getSource: ( segments, source = @model ) -> for segment in segments if segment of source then source = source[ segment ] else console.warn "%s: chain \"%s\" is invalid, segment \"%s\" not exists in %O", @_name, segments.join( '.' ), segment, source return null source _analizeChain: ( chain, source = @model ) -> segments = chain.split '.' property = segments.pop() return null unless source = @_getSource segments, source [ source, property ]