UNPKG

ad-layout

Version:
557 lines (514 loc) 16.4 kB
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