ad-layout
Version:
557 lines (514 loc) • 16.4 kB
JavaScript
import { makeError, makeWarning, guid } from '../common.js'
import { locale as L } from '../lang/zh_TW.js'
import Page from './ADPage.js'
import Block from './ADBlock.js'
import Collection from './ADCollection.js'
import { constants as C } from '../constants'
import $ from 'jquery'
import './ADLayout.css'
const _properties = new WeakMap()
const domTemplate = (
`<div id="adlayout" class="adlayout">
<div id="dockTop" class="dock-top"></div>
<div id="dockCenter" class="dock-center">
<div id="dockLeft" class="dock-left"></div>
<div id="layoutWrapper" class="layout-wrapper">
<div id="canvas" class="canvas"></div>
</div>
<div id="dockRight" class="dock-right"></div>
</div>
<div id="dockBottom" class="dock-bottom"></div>
</div>`
)
function domTreeCompare (treeA, treeB) {
if (!treeB) return false
if ((treeA.tagName === treeB.tagName)) {
const classListA = treeA.classList
const classListB = treeB.classList
for (let c = 0; c < classListA.length; c++) {
if (!classListB.contains(classListA[c])) return false
}
} else {
return false
}
if (treeA.children && treeB.children) {
for (let i = 0; i < treeA.children.length; i++) {
if (!domTreeCompare(treeA.children[i], treeB.children[i])) return false
}
}
return true
}
/**
* 建立Layout 編輯器
* @memberof AD
* @class Layout
* @constructor Layout
* @returns {Layout}
**/
function Layout (property, target) {
/// Private Variable ///
// 初始化期標
/**
* Layout 某些定義的函式的this無法參考到Layout,使用_this替代
* @private { Layout }
* @alias this
*/
const _this = this
/**
* Layout實體DOM
* @private { jQuery }
*/
let _$this = $(domTemplate)
if (target && domTreeCompare(_$this[0], target)) {
_$this = $(target)
} else if (property.target && domTreeCompare(_$this[0], property.target)) {
_$this = $(property.target)
}
if (_$this[0].adlayout instanceof Layout) {
return _$this[0].adlayout
}
/**
* 編輯器的狀態
* @private { Object }
*/
const _state = {
tool: 'select',
mode: '',
status: {}
}
/// Private Function ///
/**
* 綁定ADLayout相關事件
* @private
*/
function _bindEvents () {
_$this
.on('mousemove', '.canvas', function () {
_this.triggerEvent(C`ADLEVENT_CANVAS_MOUSEMOVE`, arguments)
})
.on('mouseup', '.canvas', function () {
_this.triggerEvent(C`ADLEVENT_CANVAS_MOUSEUP`, arguments)
})
.on('click', '.canvas', function () {
_this.triggerEvent(C`ADLEVENT_CANVAS_CLICK`, arguments)
})
.on('mousedown', '.canvas', function () {
_this.triggerEvent(C`ADLEVENT_CANVAS_MOUSEDOWN`, arguments)
})
.on('mouseleave', '.canvas', function () {
_this.triggerEvent(C`ADLEVENT_CANVAS_MOUSELEAVE`, arguments)
})
.on('click', '.page', function () {
_this.triggerEvent(C`ADLEVENT_PAGE_CLICK`, arguments)
})
.on('mousedown', '.page', function () {
_this.triggerEvent(C`ADLEVENT_PAGE_MOUSEDOWN`, arguments)
})
.on('mouseup', '.page', function () {
_this.triggerEvent(C`ADLEVENT_PAGE_MOUSEUP`, arguments)
})
.on('mousemove', '.page', function () {
_this.triggerEvent(C`ADLEVENT_PAGE_MOUSEMOVE`, arguments)
})
.on('click', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_CLICK`, arguments)
})
.on('mousedown', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_MOUSEDOWN`, arguments)
})
.on('mouseup', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_MOUSEUP`, arguments)
})
.on('mousemove', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_MOUSEMOVE`, arguments)
})
.on('mouseenter', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_MOUSEENTER`, arguments)
})
.on('mouseleave', '.block', function (e) {
_this.triggerEvent(C`ADLEVENT_BLOCK_MOUSELEAVE`, arguments)
})
.on('dblclick', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_DBLCLICK`, arguments)
})
.on('dragstart', '.block', function (e) {
e.preventDefault()
_this.triggerEvent(C`ADLEVENT_BLOCK_DRAGSTART`, arguments)
})
.on('dragend', '.block', function (e) {
e.preventDefault()
_this.triggerEvent(C`ADLEVENT_BLOCK_DRAGEND`, arguments)
})
.on('paste', '.block', function () {
_this.triggerEvent(C`ADLEVENT_BLOCK_PASTE`, arguments)
})
}
/// Public Function ///
/**
* 將組件附加至編輯器中
* @public
* @param {string} position 可以是'top', 'bottom', 'left', 'right',決定模組在編輯器中停靠的位置
* @param {jQuery} content 組件的jQuery
* @returns {AD.Layout}
*/
this.append = function append (position, content) {
var dTop = _$this.find('.dock-top')
var dLeft = _$this.find('.dock-left')
var dCenter = _$this.find('.layout-wrapper')
var dRight = _$this.find('.dock-right')
var dBottom = _$this.find('.dock-bottom')
switch (position) {
case 'top':
dTop.append(content)
break
case 'bottom':
dBottom.append(content)
break
case 'center':
dCenter.append(content)
break
case 'left':
dLeft.append(content)
break
case 'right':
dRight.append(content)
break
}
return this
}
/**
* 將AD.Layout編輯器附加至已存在的Dom上
* @public
* @param {(string|jQuery)} selector 要被附加的Dom,型態為jQuery物件,如果selector是string,則用以建立$(selector)的jQuery物件
*/
this.attachTo = function attachTo (selector) {
if (selector === null) {
if (_$this !== null) {
_$this.remove()
_$this = null
}
} else {
var container = selector instanceof $ ? selector : $(selector)
if (container.length === 1) {
container.append(_$this)
_$this[0].adlayout = this
var style = '<style type=\'text/css\' id=\'ad_style\'></style>'
$('head').append(style)
this.triggerEvent(C`ADLEVENT_ATTACHED`, [container])
}
}
}
/// Variable ///
// this.document.body.page.count = function count () {
// return _$this.find('.page').size()
// }
// this.document.body.page.add = function add (a, b) {
// if (a instanceof Page) {
// a.appendTo(_$this.find('.canvas'))
// return _this.document.body.page
// } else {
// makeWarning(C`NO_PAGE_CREATED`, L`_STR_ERR_NO_PAGE_CREATED)`)
// return false
// }
// }
/**
* 文件狀態物件,包含相關於文件狀態的函式與設定值
* @type {Object}
* @public
*/
this.state = {
/**
* 取得或設定已選取的區塊物件
* @function
* @param {(string|AD.Block)} [value] 指定要選取的區塊物件,若傳入空字串,則代表取消選取所有區塊
* @public
* @returns {(AD.Collection<Block>|this.state)} 若是無指定參數,則回傳已選取的區塊物件,若是有指定,則回傳state本身物件
*/
select: function (value) {
if (value !== undefined) {
if (value === '' || value === null) {
_this.document.body.block().deselect()
} else if (value instanceof Block) {
value.select(true)
}
return _this.state
} else {
var blocks = new Collection(Block)
_$this.find('.block.selected').each(function () {
blocks.push(Block(this))
})
return blocks
}
},
/**
* 取得或設定目前的編輯模式
* @function
* @param {string} [value] 指定目前的編輯模式
* @public
* @returns {string} 若是無指定參數,則回傳目前的編輯模式,若是有指定,則回傳state本身物件
*/
mode: function (value) {
if (value) {
_state.mode = value
_this.triggerEvent(C`ADLEVENT_MODE_CHANGED`, [value])
return _this.state
} else {
return _state.mode
}
},
/**
* 取得或設定目前的編輯工具
* @function
* @param {string} [value] 指定目前的編輯工具
* @public
* @returns {string} 若是無指定參數,則回傳目前的編輯工具,若是有指定,則回傳state本身物件
*/
tool: function (value) {
if (value) {
_state.tool = value
_this.triggerEvent(C`ADLEVENT_AD_LAYOUT_TOOL_CHANGED`, [value])
return _this.state
} else {
return _state.tool
}
},
/**
* 取得或設定目前的文件狀態
* @function
* @param {string} [value] 指定目前的文件狀態
* @public
* @returns {string} 若是無指定參數,則回傳目前的文件狀態,若是有指定,則回傳state本身物件
*/
status: function (key, value) {
switch (arguments.length) {
case 2:
_state.status[key] = value
_this.triggerEvent(C`ADLEVENT_AD_LAYOUT_STATUS_CHANGED`, [key, value])
return _this.state
case 1:
return _state.status[key]
case 0:
default:
return _state.status
}
}
}
/**
* 已註冊的事件callback函式,若向AD.Layout編輯器註冊callback函式,callback函式則會儲存在此。儲存的方式為:event.{event} : array
* @example
* //a = AD.Layout
* a.registerEvent(C`ADLEVENT_AD_LAYOUT_CANVAS_CLICK`, function(e,param) {
* //TODO here
* }
* console.log(a.events)
* //輸出Object {
* // C`ADLEVENT_AD_LAYOUT_CANVAS_CLICK` : function(e,param){...}
* //}
* @type {object}
* @public
*/
_this.events = {}
_properties.set(this, {
$this: _$this,
grid: 1,
multipleSelection: true,
areaSelector: true
})
_bindEvents()
_this.registerEvent(C`ADLEVENT_BLOCK_SELECT_BEFORECHANGE`, function (e) {
const _ = _properties.get(this) || {}
if (!_.multipleSelection && e.detail.select === true) {
const currentSelect = _.$this.find('.block.selected')
if (currentSelect.length) {
currentSelect.each(function () {
if (this.adblock !== e.detail.block) this.adblock.select(false)
})
}
}
})
_this.triggerEvent(C`ADLEVENT_AD_LAYOUT_ON_LOAD`)
return _this
}
const prototype = Layout.prototype
prototype.add = function add (obj) {
const _ = _properties.get(this) || {}
if (obj instanceof Page) {
obj.appendTo(_.$this.find('.canvas'))
return this
} else {
makeWarning(C`NO_PAGE_CREATED`, L`_STR_ERR_NO_PAGE_CREATED)`)
return this
}
}
/**
* 取得包含目前編輯器的母jQuery物件
* @function
* @public
* @readonly
* @returns {jQuery} 包含目前編輯器的母jQuery物件
*/
prototype.container = function () {
const _ = _properties.get(this) || {}
return _.container
}
prototype.block = function (pageId, index) {
const _ = _properties.get(this) || {}
const blocks = new Collection(Block)
switch (arguments.length) {
case 0 :
$('.block', _.$this).each(function () {
blocks.push(new Block(this))
})
return blocks
case 1 :
$('.page:eq(' + pageId + ') .block').each(function () {
blocks.push(new Block(this))
})
return blocks
}
}
/**
* 取得在一區域內的所有blocks
* @function
* @public
* @param {Object} area 區域{x, y, width, height}
* @returns {Collection<Block>}
*/
prototype.blocksFromArea = function (area) {
const _ = _properties.get(this) || {}
const blocks = new Collection(Block)
_.$this
.find('.block')
.map(function () { return Block(this) })
.filter(function () {
const bound = this.bound()
return ((bound.x + bound.width) > area.x) && (bound.x < (area.x + area.width)) && ((bound.y + bound.height) > area.y) && (bound.y < (area.y + area.height))
})
.each(function () { blocks.push(this) })
return blocks
}
/**
* 向AD.Layout註冊事件callback函式
* @function
* @public
* @param {symbol} event 事件名稱
* @param {function} fun callback函式
* @param {string} [method=append] 事件的註冊方式
* @param {boolean} [returnID=false] 是否回傳已註冊事件的event id,event id用已未來刪除該事件使用
* @returns {(AD.Layout|string)} 若returnID為true,則回傳該事件的event id,否則回傳AD.Layout本身
*/
prototype.registerEvent = function (event, func, method = 'append', returnID) {
if (event === undefined) makeWarning(C`EMPTY_INPUT`, L`_STR_ERR_BAD_PARAMETERS`)
var id = 'event' + guid()
if ((this.events[event] instanceof Array) || !(this.events[event] instanceof Object)) {
this.events[event] = {}
}
switch (method) {
case '/':
case 'replace':
this.events[event] = {}
break
case '+':
case 'append':
this.events[event][id] = func
break
default:
return false
}
return returnID ? id : this
}
/**
* 向AD.Layout取消註冊事件
* @function
* @public
* @param {symbol} event 事件名稱
* @param {string} id 事件id
* @returns {AD.Layout} 回傳AD.Layout本身
*/
prototype.unrigisterEvent = function (event, id) {
if (this.events[event] && this.events[event][id]) {
delete this.events[event][id]
} else {
makeWarning(C`TARGET_NOT_EXIST`, L`_STR_WAR_EVENT_NOT_EXIST`)
}
return this
}
/**
* 觸發向AD.Layout註冊事件callback函式
* @function
* @public
* @param {string} type 事件名稱
* @param {Event|jQuery.Event|[Event|jQuery.Event, any]|any} paraments 要送至callback函式的參數,如果parameters為非Event like物件,則callback會建立CustomEvent(type, {detail: parameters}),parameters會放在CustomEvent的detail中以及第二參數之後;如果parameters為Array like物件,則會以[0]當作Event物件,若[0]不為Event like物件,則同樣會建立CustomEvent(type, {detail: paraments})
*/
prototype.triggerEvent = function (type, paraments) {
if (this.events[type] !== undefined) {
for (var k in this.events[type]) {
const paramentsIsArr = (Array.isArray(paraments) || (typeof paraments === 'object' && {}.hasOwnProperty.call(paraments, 'length') && typeof paraments.length === 'number'))
let newParaments = []
if (paramentsIsArr) {
newParaments = Array.from(paraments)
if (!(newParaments[0] instanceof window.Event) && !(newParaments[0] instanceof $.Event)) {
newParaments.unshift(new window.CustomEvent(type.description, { detail: paraments }))
}
} else {
newParaments = [new window.CustomEvent(type.description, { detail: paraments }), paraments]
}
const result = this.events[type][k].apply(this, newParaments)
if (result === C`EVENT_INTERRUPTED`) break
}
}
return this
}
prototype.grid = function grid (val) {
const _ = _properties.get(this) || {}
const _$this = _.$this
if (arguments.length === 0) {
return _.grid
} else {
_.grid = val
_$this.css('--grid-size', val > 0 ? val + 'px' : '1px')
_$this.toggleClass('grid', val !== 0)
return this
}
}
prototype.multipleSelection = function multipleSelection (val) {
const _ = _properties.get(this) || {}
if (arguments.length === 0) {
return _.multipleSelection
} else {
_.multipleSelection = val
return this
}
}
prototype.areaSelector = function areaSelector (val) {
const _ = _properties.get(this) || {}
if (arguments.length === 0) {
return _.areaSelector
} else {
_.areaSelector = val
return this
}
}
prototype.gridThickness = function gridThickness (val) {
const _ = _properties.get(this) || {}
const _$this = _.$this
if (arguments.length === 0) {
return _.gridThickness
} else {
_.gridThickness = val
_$this.css('--grid-thickness', (val && +val > 0) ? val + 'px' : 0)
return this
}
}
prototype.gridColor = function gridColor (val) {
const _ = _properties.get(this) || {}
const _$this = _.$this
if (arguments.length === 0) {
return _.gridColor
} else {
_.gridColor = val
_$this.css('--grid-color', val)
return this
}
}
export default Layout